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.

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:

  1. Reads the sibling’s package.json.name (the canonical name) and confirms manifest.ts is present.
  2. 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).
  3. 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.
  4. 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.