@sylvanstack/cli
v1.0.0
Published
CLI for the Sylvan stack — project upgrades (codemods / migrations) and tooling
Readme
@sylvanstack/cli
CLI for the Sylvan stack. Today it ships project upgrades (codemods / migrations);
it's structured to grow more commands (init, etc.).
npx @sylvanstack/cli status # version + applied + pending
npx @sylvanstack/cli upgrade # interactive wizard
npx @sylvanstack/cli upgrade --pending # apply all pending, in order
npx @sylvanstack/cli upgrade --pending --dry-run # preview pending
npx @sylvanstack/cli upgrade --list # list all upgrades
npx @sylvanstack/cli upgrade 20260616-migrate-to-sylvanstack --dry-run # preview one
npx @sylvanstack/cli upgrade 20260616-migrate-to-sylvanstack # apply one (cwd)Run with no key for an interactive wizard (pick the upgrade → choose the directory →
see the compatibility check → preview/apply). Pass a key + flags for scriptable/CI use.
Built on commander (commands) +
@clack/prompts (interactivity).
Commands
| Command | Purpose |
| --- | --- |
| status [dir] | Read-only: current @sylvanstack version, origin, applied + pending upgrades. |
| upgrade [key] [dir] | Run a project upgrade. --pending applies all pending in order; --dry-run previews; --force overrides the version check; --commit commits each upgrade; --allow-dirty skips the clean-tree check; --install runs the package manager install when deps change; --param name=value sets a parameter (repeatable); --yes is non-interactive; --verbose lists every changed file (default: summary only); --list lists them. |
Pending = upgrades not yet in the ledger, whose targetVersion is newer than the project's current version, and whose appliesTo matches now — computed in order, so a project that started higher ignores older ones.
Git safety
Applying refuses on a dirty working tree (so each upgrade lands as one reviewable diff) — pass --allow-dirty to override. By default the CLI does not commit; review the diff and commit yourself. --commit makes one commit per upgrade (its changed files + the manifest, isolated via a pathspec), which pairs well with --pending across several upgrades; outside a git repo it no-ops. The only git-merge surface is the manifest's append-only appliedUpgrades — resolve any conflict by keeping both entries (the CLI dedupes by key).
Dependencies (install)
When an upgrade rewrites a package.json, the lockfile is left stale on purpose — install is not run by default (it's a heavy, network-dependent side effect, and a migration's renamed deps may not be published yet). Instead the CLI prints a next-step hint. Pass --install to run the install once after applying (the package manager is auto-detected from the lockfile — pnpm/yarn/npm/bun, defaulting to pnpm); across --pending it runs a single install at the end, not per upgrade. Interactively, it only asks to install when an auth token is actually available (env var or ~/.npmrc) — otherwise it tells you to set the token first, since the install would 404. A failed install is reported, not fatal.
By default the apply output is a one-line summary per upgrade; pass --verbose to list every changed file.
Parameters
Some upgrades need values (a registry URL, a token). The CLI resolves every parameter before writing anything — so a missing or invalid value fails fast, never halfway through a codemod (across --pending, params for all queued upgrades are resolved up-front).
- Interactive (a TTY, no
--yes): you're prompted for each value, pre-filled with any--param/default. - Non-interactive (
--yes, or piped/CI): values come from--param name=valueand env; an optional param falls back to its default, a missing required one is a clear error telling you which--param(or env var) to pass. - Secrets (
secret: trueparams, e.g. a token) are masked, read from their env var as a fallback, and never written to the project. The migration writes a${NPM_TOKEN}reference into the project.npmrcand uses the resolved value only to authenticate the install in that run — prefer the env var over--paramso it stays out of shell history. - Local credentials — a token param flagged
npmAuthForRegistrytriggers an interactive setup: if a token is already available (the$SYLVANSTACK_NPM_TOKENenv var or~/.npmrc) it's used as-is; otherwise you're asked where to put it — theSYLVANSTACK_NPM_TOKENenv var (appended to your shell profile, e.g.~/.zshrc; recommended — scoped to Sylvan projects, mirrors CI/Vercel) or~/.npmrc(0600, machine-wide) — then for the token, and it's written accordingly. Interactive-only; non-interactive/CI read$SYLVANSTACK_NPM_TOKENfrom the environment.
Compatibility check
Before running, the CLI checks the project against the upgrade's appliesTo (semver,
workspace-aware scan of all package.json files):
appliesTo.packages[]— each entry's declared version in the project must satisfy itsrange;appliesTo.project(optional) — the project's own version (rootpackage.json) must satisfy itsrange. This is how a future major upgrade enforces the project-major ↔ sylvanstack-major lock (e.g.project: { range: "1.x" }).
Outcome:
- in range →
✓and proceeds; - out of range →
⚠warning, and a real apply is refused unless--force; - not found / unparseable →
⚠warning (already migrated, or not a consumer), proceeds as a no-op.
Upgrades
Each upgrade lives in src/upgrades/<YYYYMMDD-kebab-name>/ and exports an Upgrade
(key, targetVersion = version it yields, appliesTo = initial-version guard,
title, description, run). The date prefix keeps them
sorted chronologically; targetVersion carries the semver mapping. To add one, create
the folder and register it in src/upgrades/registry.ts. Upgrades run code transforms
today and can wrap Sanity schema migrations later.
20260616-migrate-to-sylvanstack — "Migrate to SylvanStack 1.0.0" (→ v1.0.0)
Renames imports @origen-studio/* → @sylvanstack/* (subpaths like …/hooks/next.js
preserved) and rewrites package.json dependency keys. The 4 lockstep libs
(components, sanity, tracking, forms) are pinned to ^1.0.0; the vendored
babel-plugin-remove-import-js-extension is renamed too but keeps its own version
(it isn't part of the lockstep group). It rewrites .npmrc — drops the old Verdaccio
registry + any stored auth token, and points the @sylvanstack scope at the registry
with a ${SYLVANSTACK_NPM_TOKEN} reference (never the literal). It records the migration
in the project's .sylvanstack/manifest.json.
Parameters: registry (default https://registry.npmjs.org/) and npmToken
(secret, also read from $SYLVANSTACK_NPM_TOKEN). The token authenticates the follow-up
install in that run — use a READ-ONLY token (it can reach the client bundle). Run
interactively and it offers to save the token to your ~/.npmrc for future local
installs. For CI / Vercel, set SYLVANSTACK_NPM_TOKEN in the environment (the committed
.npmrc references it).
npm keys auth tokens by registry host, not package scope — so on the public npmjs registry this line covers that host (you can't write a
@sylvanstack-only token). The protection is the token itself: a read-only granular token scoped to@sylvanstack. The scope-specific env var name also means only projects referencing${SYLVANSTACK_NPM_TOKEN}ever send it — unrelated projects on your machine don't.
Project state — .sylvanstack/manifest.json
Applying an upgrade records it in a committed, tool-managed .sylvanstack/manifest.json
(flagged with a leading "//" note — JSON has no comments — so it reads as do-not-edit).
It holds appliedUpgrades (what ran) and origin (migration when stamped by the first
upgrade, or scaffold when written by the planned init, which records a baseline instead
of inheriting the starter's history). status / pending-detection read it. It never holds
secrets.
Each record (and the origin) carries at (ISO timestamp), by (the git identity
Name <email> that ran it, best-effort — omitted if git isn't configured), and via
(the tool, e.g. @sylvanstack/[email protected]) — so by is who and via is what.
