Adding a package
The app shell ships empty. To enable a feature - mail, contacts, calendar, drive, anything third-party - you link its sibling repository into your app shell checkout. Two commands cover the common cases: packages:install when you don’t have the repo cloned yet, and packages:link when you do.
Fresh-clone default
Out of the box, tinycld.packages.ts is [] as const and tinycld/packages/ contains only the bundled @tinycld/core. The app shell runs without any feature packages - you get authentication and an empty workspace, and that’s about it. You add exactly the siblings you want.
Install from a git URL
From inside the app shell (tinycld/), if you want to clone a package and link it in a single step:
npm run packages:install https://github.com/tinycld/contacts
npm run dev
The default clone location is ../<slug> relative to the app shell - so ../contacts in this example. You can change it with --path, and you can pin a specific ref with --ref:
npm run packages:install https://github.com/tinycld/mail --path ../custom-mail --ref v0.2.0
If the target directory already exists and contains a valid package (any package.json with a name plus a manifest.ts), packages:install skips the clone step and just links - it becomes a safe “re-link” operation.
Link an already-cloned sibling
If you cloned the sibling yourself (or you’re authoring one locally), use packages:link:
npm run packages:link contacts # bare slug → ../contacts
npm run packages:link ../elsewhere/mail # explicit relative path
npm run packages:link /abs/path/to/pkg # absolute path works too
The argument is a sibling-directory locator, not a package name. A bare slug (contacts) is treated as ../contacts relative to the app shell; anything containing a path separator is used as-is. The canonical package name - whether @tinycld/contacts, @acme/custom, or a bare my-pkg - is read from the sibling’s own package.json.name. Third-party packages are first-class: the tooling reads whatever scope the sibling declares, without favoring @tinycld/.
What linking does
Whether you arrive via packages:install or packages:link, the end state is the same:
- Reads the sibling’s
package.json.name(the canonical name) and confirmsmanifest.tsis present. - Creates a symlink under
tinycld/packages/<name>pointing at the sibling’s real path. Scoped packages nest (tinycld/packages/@tinycld/mail,tinycld/packages/@acme/custom); unscoped packages are flat (tinycld/packages/my-pkg). - Adds the full package name to
tinycld.packages.ts. This file is checked in - it’s the source of truth for which packages this checkout wires in. - Runs the code generator (
scripts/generate-packages.ts) which materializes route re-exports, collection registration, settings panel entries, go.mod directives, and everything else the package declares.
Remove a package
npm run packages:unlink contacts
This removes the entry from tinycld.packages.ts, drops the symlink from tinycld/packages/, and re-runs the generator to clean up the old re-exports, generated wiring, and go.mod entries. The sibling repo itself is untouched.
Typical workflow
cd ~/code/tinycld/tinycld
npm run packages:install https://github.com/tinycld/contacts
npm run packages:install https://github.com/tinycld/mail
npm run packages:install https://github.com/tinycld/drive
npm run dev
Those three commands turn an empty shell into a working mail-contacts-drive app. tinycld.packages.ts is now a committable record of the feature set this checkout is configured for - pushing that change lets a CI job reproduce the same build.
For the anatomy of what you just linked, see Manifest. For scaffolding a new package of your own, see Creating a package.