@tinycld/bootstrap
v2.0.3
Published
Scaffolder for @tinycld feature packages
Readme
@tinycld/bootstrap
@tinycld/bootstrap does two jobs:
--new <slug>— scaffold a new feature package (manifest, CI workflow, sample screens, seed, migrations, optionally a Go server).--assemble-only— assemble a workspace root in the current directory by cloning theappshell, thecorelibrary, and any features named with--with <slug>.
Modeled after create-vite: tiny CLI, templates embedded in the published npm package, no runtime network fetch.
Requirements
- npm on Node ≥ 24 (the package's
engines.nodeis>=24; older Node may work but is unsupported). git(always) andgh(only for the suggested "initial push" next-step) on$PATH.
The two modes are independent — scaffold mode does not require a pre-existing workspace, and assemble mode does not require a scaffolded package. They compose: a common flow is --assemble-only once to set up ~/code/tinycld/, then --new <slug> from inside that workspace to add a feature.
Assemble a workspace (--assemble-only)
mkdir ~/code/tinycld && cd ~/code/tinycld
npx @tinycld/bootstrap@latest --assemble-only --with mail --with contacts
npm install # links members + runs the generator (postinstall)
cd app && npm run devThe CLI writes the workspace coordination files (package.json, tinycld.packages.ts, vitest.config.ts, shared test stubs, the package-scripts/ CLI) from embedded templates, then clones app + core as siblings. Each --with <slug> adds one feature sibling. app and core are always cloned; everything else is opt-in.
--with name@ref pins a clone to a tag, branch, or commit:
npx @tinycld/bootstrap@latest --assemble-only \
--with [email protected] --with [email protected] --with [email protected]Skipped if the target directory already exists, so re-running is safe.
Set TINYCLD_REPO_BASE to clone over HTTPS instead of the default SSH ([email protected]:tinycld). CI uses this:
TINYCLD_REPO_BASE=https://github.com/tinycld \
npx @tinycld/bootstrap@latest --assemble-only --with mailScaffold a new package (--new)
npx @tinycld/bootstrap --new my-featureYou'll be walked through an interactive prompt. The positional argument (my-feature) is the slug — kebab-case, 3–40 chars, becomes @tinycld/my-feature, the URL segment /a/<orgSlug>/my-feature/, and the Go module tinycld.org/packages/my-feature. Leave it off to be asked for it.
If --new runs from inside an existing workspace root (app/ and core/ siblings detected), the new package is scaffolded as a sibling and the link step adds it to the workspace package.json. Otherwise the CLI creates a wrapper directory ./tinycld-<slug>/ with the package at ./tinycld-<slug>/<slug>/, assembles a workspace around it (cloning app + core), and links — leaving you a self-contained, runnable workspace.
Prompts
| Prompt | Example | Notes |
|---|---|---|
| Package slug | my-feature | Skipped if given as argv. Validates kebab-case, minimum 3 chars. |
| Human-readable name | My Feature | Defaults to title-cased slug; used in the manifest's name + nav label. |
| One-sentence description | Does a thing well. | Used in manifest description, package.json, and README. |
| Preset | full / settings-only | See below. |
| Lucide icon name (full only) | box | Any lucide-react-native name. Default box. |
| Nav order (full only) | 20 | Integer 0–99, controls sidebar position. |
| Keyboard shortcut (full only) | f | Single lowercase letter, or blank. |
| Include a Go server? (full only) | y / n | If no, server/ and the manifest's server field are omitted. |
| Target directory | ./my-feature | Default creates the new repo as a child of the current directory. Must not exist or must be empty. |
| Link into the workspace? | y / n | After scaffolding, the CLI adds the package to the workspace package.json's workspaces array and runs npm install at the workspace root. If cwd isn't a workspace root, it assembles one (cloning app + core) first. Suppress with --no-link. |
Flags (non-interactive use)
Every prompt has a corresponding flag. Pass --yes (or -y) to accept all defaults and skip everything that wasn't given a flag — useful for scripted scaffolding.
| Flag | Maps to | Notes |
|---|---|---|
| --new | Mode selector | Required for scaffold mode. Mutually exclusive with --assemble-only. |
| --assemble-only | Mode selector | Required for workspace-assembly mode. Mutually exclusive with --new. |
| --with <slug> | Assemble-only | Repeatable. Each adds one feature sibling. Accepts --with name@ref to pin to a tag/branch/commit. |
| (positional) | Package slug (scaffold) | First non-flag argument. Pair with --new to set the slug non-interactively. |
| --name <s> | Human-readable name | Defaults to title-cased slug. |
| --description <s> | Description | |
| --preset <full\|settings-only> | Preset | |
| --icon <name> | Lucide icon | Full preset only. |
| --nav-order <n> | Nav order | Integer 0–99. |
| --shortcut <c> | Keyboard shortcut | Single lowercase letter. |
| --server / --no-server | Include a Go server | Full preset only. |
| --target <dir> | Target directory | Default ./<slug>. |
| --link / --no-link | Link into the workspace | Forces the post-scaffold link step on or off, skipping the prompt. |
| --yes, -y | — | Accept all defaults; with --no-link, fully non-interactive. |
Example, fully non-interactive:
npx @tinycld/bootstrap --new my-feature \
--yes --no-link \
--description "Tracks widgets across the org" \
--preset full --icon box --nav-order 25 --shortcut w--help is not wired (yet) — run npx @tinycld/bootstrap with no argv to get a usage summary listing both modes.
Presets
The scaffolder offers two starting points, corresponding to the two shapes already present in the tinycld ecosystem.
full — data package
Matches the shape of @tinycld/contacts, @tinycld/mail, @tinycld/calendar, @tinycld/drive. You get routes, a sidebar, an optional provider, pbtsdb collections, PocketBase migrations, seed data, and a Go server stub.
my-feature/
├── .github/workflows/ci.yml # assembles the workspace via bootstrap --assemble-only, runs tinycld-pkg check + test:e2e
├── .gitignore # node_modules, *.tsbuildinfo, lockfiles, .DS_Store
├── README.md # developer-facing onboarding for this package
├── manifest.ts # name, slug, routes, nav, collections, seed, server, ...
├── package.json # @tinycld/my-feature, peer deps, scripts, exports map
├── tsconfig.json # extends ../app/tsconfig.package-base.json
├── pb-migrations/
│ └── 1800000000_create_my-feature.js # creates my_feature_items collection
├── server/
│ ├── go.mod # module tinycld.org/packages/my-feature; replaces tinycld.org/core → ../../core/server
│ └── register.go # func Register(app) hook for server-side wiring
├── tests/
│ └── manifest.test.ts # vitest smoke test of manifest shape
└── tinycld/my-feature/ # all package TypeScript lives under this prefix
├── collections.ts # registerCollections() for pbtsdb
├── provider.tsx # optional context provider
├── seed.ts # default-export async seed(pb, ctx)
├── sidebar.tsx # sidebar rendered for this package's routes
├── types.ts # MyFeatureSchema + record interfaces
└── screens/
├── _layout.tsx # Stack layout for /a/[orgSlug]/my-feature/**
├── [id].tsx # detail route
└── index.tsx # list routeThe tinycld/my-feature/ nesting gives the package a stable public API surface accessible via the package.json exports map: @tinycld/my-feature/screens/*, /sidebar, /collections, etc.
settings-only — service package
Matches @tinycld/google-takeout-import. The package contributes only a settings panel — no routes, no nav entry, no collections, no server. Use this for integrations or admin-style tools that live under /a/<orgSlug>/settings/**.
my-service/
├── .github/workflows/ci.yml
├── .gitignore
├── README.md
├── manifest.ts # name, slug, description, settings[] only
├── package.json
├── tsconfig.json
├── tests/
│ └── manifest.test.ts
└── tinycld/my-service/
├── types.ts # public type exports (empty by default)
└── settings/
└── main.tsx # the settings panel componentManifest fields
The scaffolded manifest.ts is the single source of truth for what a package contributes. Templates only fill in the fields appropriate for the chosen preset; the full reference (every field, when to use it) lives in the manifest schema docs. Quick summary:
| Field | Meaning |
|---|---|
| name, slug, version, description | Identity. slug is the URL segment and the npm name's last segment. |
| routes.directory | Subpath (resolved through package.json exports) where org-scoped screens live. Generator re-exports each screen file under app/a/[orgSlug]/<slug>/. |
| nav | { label, icon, order, shortcut } — sidebar entry for the org workspace. |
| sidebar.component | Subpath to the package's sidebar component, rendered when on its routes. |
| provider.component | Optional context provider mounted around the package's routes. |
| migrations.directory | Folder of PocketBase JS migrations; symlinked into the app shell. |
| collections.register, collections.types | Subpaths to the pbtsdb registration function and the schema types. |
| seed.script | Default-export async function called by the dev seeder for this package. |
| server | { package, module } — relative dir + Go module path for the optional server extension. |
| settings[] | One entry per panel contributed under /a/<orgSlug>/settings/<slug>. |
All path-shaped fields use short subpaths ('screens', 'sidebar', 'collections') that match the keys in package.json's exports map — the generator follows the exports map to find the actual files under tinycld/<slug>/....
After scaffolding
If you accepted the link-into-workspace prompt, the package is already a workspace member and npm install has run. Otherwise the CLI prints next-steps you can copy verbatim:
# 1. Initialize git and push to GitHub
cd my-feature
git init
git add .
git commit -m 'chore: initial scaffold'
gh repo create tinycld/my-feature --public --source=. --push
# 2. Link into the workspace (add as a member, then install)
cd ..
# ensure "my-feature" is listed in the workspace package.json's "workspaces" array, then:
npm install
# 3. Verify (scoped to this member)
cd my-feature
npx tinycld-pkg checkOnce linked, the app shell's generator wires your manifest in automatically: routes appear at /a/<orgSlug>/my-feature/**, the sidebar renders, the settings panel shows up, migrations get picked up by PocketBase. No further changes to app/ or core/ are needed.
⚠️
app/metro.config.cjswatches the workspace root, but Expo's resolver caches package metadata at boot. If you add a new sibling whilenpm run devis already running, restart it (Ctrl-C, thennpm run dev) so the new member is picked up. CI is fine — it always starts fresh.
Day-to-day development
Most work happens from inside the package with the workspace assembled around it:
cd my-feature
npx tinycld-pkg check # biome + tsc + vitest, scoped to this member
npx tinycld-pkg test # vitest only
npx tinycld-pkg test:e2e # playwright for this memberTo run the app itself, drop into the app shell:
cd ../app
npm run dev # expo + pocketbase, fronted by a single-port dev proxy
npm run checks # biome + tsc, ecosystem-wideHot reload picks up changes in your package the same way as core code, since members are symlinked.
Running the scaffolded package's own CI locally
The package's .github/workflows/ci.yml mirrors what GitHub Actions runs:
- Assemble a workspace via
npx @tinycld/bootstrap --assemble-only. The job has--with <this-pkg>@<sha>so it lands the exact commit under test. npm installat the workspace root (this also runs the package generator via the postinstall hook).npx tinycld-pkg checkfrom inside the package directory — runs biome (scoped), tsc, and vitest.npx tinycld-pkg test:e2eif the package ships Playwright specs undertests/.
Biome lives only in app/biome.json (one config across every member). There is no biome.json in the scaffolded package repo. Typecheck runs against the app shell's tsconfig via tinycld-pkg, so the expo base, uniwind global augments, and the live pbSchema types are all in scope.
Import conventions the templates assume
Sibling packages should import core utilities via the scoped path:
// ✓ right
import { useOrgLiveQuery } from '@tinycld/core/lib/use-org-live-query'
import { useThemeColor } from '@tinycld/core/lib/use-app-theme'
import { Modal } from '@tinycld/core/ui/modal'
// ✗ wrong (legacy; resolved in earlier core layouts but not the current one)
import { useOrgLiveQuery } from '~/lib/use-org-live-query'Within the package, intra-package imports use relative paths:
import { registerCollections } from './collections'
import { ContactForm } from '../components/ContactForm'~/tinycld/<slug>/* is also aliased to your own nested source, for cases where you want an absolute import within the package — but relative paths are usually clearer.
Cross-package dependencies are not supported at compile time. If your package needs data from another package (e.g. mail wanting to read contacts), use the runtime usePackages() helper from @tinycld/core/lib/packages/use-packages and do the lookup at runtime. See mail/tinycld/mail/components/ContactSuggestionsList.tsx or drive/tinycld/drive/components/ContactSuggestionsSource.tsx for the canonical pattern.
Contributing to the templates
Templates live under templates/:
templates/
├── shared/ # files identical across presets (tsconfig, CI workflow, README, .gitignore, tests/manifest.test.ts)
├── full/ # data-package preset (manifest, package.json, screens, sidebar, provider, collections, types, seed, pb-migrations, server)
└── settings-only/ # settings-only preset (manifest, package.json, types, settings/main.tsx)shared/ is copied first; the selected preset is copied on top. A preset can override a shared file just by naming it at the same relative path. After copying, if --no-server was chosen, server/ is removed and the server: { … } field is stripped from manifest.ts.
Files contain {{PLACEHOLDER}} tokens that get substituted at scaffold time:
| Placeholder | Derivation |
|---|---|
| {{PKG_SLUG}} | user input, kebab-case |
| {{PKG_NAME}} | user input, human-readable |
| {{PKG_SCOPED}} | kept as alias for {{PKG_SLUG}} (back-compat; prefer {{PKG_SLUG}} in new templates) |
| {{PKG_PASCAL}} | PascalCase of slug |
| {{PKG_CAMEL}} | camelCase of slug |
| {{PKG_SNAKE}} | snake_case of slug (for DB table names) |
| {{PKG_DESCRIPTION}} | user input |
| {{PKG_ICON}} | user input, lucide icon name |
| {{PKG_NAV_ORDER}} | user input, integer |
| {{PKG_NAV_SHORTCUT}} | user input, single letter or empty |
| {{GO_MODULE}} | tinycld.org/packages/ + slug |
Substitution runs on both file content and file/directory names — that's how tinycld/{{PKG_SLUG}}/** becomes tinycld/my-feature/**. Binary files (.png, .jpg, .woff, etc) are copied byte-for-byte and skipped during substitution; the full list lives in BINARY_EXTENSIONS in src/copy-template.ts.
Adding a new placeholder requires one line in src/substitute.ts's buildPlaceholders() plus whatever tokens you scatter through the templates.
Local development of the scaffolder itself
npm install
npm run dev my-feature --target /tmp/scratch # tsx live-runs src/index.ts
npm run lint # biome
npm run typecheck # tsc --noEmit
npm run checks # both of the above
npm run test # vitest: substitute + validate + end-to-end scaffold into tmpdir
npm run build # compile src/ → dist/ (what gets published)The scaffolder tests invoke copyTemplate into a tmp directory and assert the expected tree, file contents, and placeholder substitutions for both presets. The end-to-end flow (link into a real tinycld/ checkout and boot the dev server) is covered manually — see git history for the validation playbook.
Publishing
The repo's .github/workflows/ci.yml has a publish job that runs on tag pushes (v*). Tag a release and it ships to npm under the @tinycld scope:
# Bump version in package.json, commit, then
git tag v0.1.1
git push --tagsPublishing needs an NPM_TOKEN repo secret (npm "automation" token scoped to @tinycld). Add it once at Settings → Secrets → Actions.
prepublishOnly runs npm run checks && npm run test && npm run build before any publish, so a broken tree never reaches npm.
Design notes
- Templates are embedded in the npm package, not fetched from a separate repo.
npxgrabs them once; scaffolding is offline thereafter. - Direct string replacement, no handlebars / EJS. Simpler, fewer moving parts, no runtime template compiler.
- No destructive actions: the CLI refuses to overwrite a non-empty target directory and never touches git, gh, or your local repos without consent. The clone-and-link step is opt-in (
--link/ interactive prompt). - Two presets, not N flags. We have exactly two shapes of feature package in the tinycld ecosystem today (data package, settings-only); offering a fine-grained matrix of "routes y/n, server y/n, …" adds prompt-fatigue with no real benefit.
@clack/promptsfor the interactive UX: smaller and nicer than inquirer.picocolorsfor output, nochalkweight.
License
MIT.
