- Enhanced descriptions in `AGENTS.md`, `cli.mdx`, and `getting-started.mdx` to provide clearer guidance on module categories and commands. - Added information about third-party module publishing in `registry.mdx`. - Adjusted the order of pages in `meta.json` for better navigation. - Improved warning and info callouts for better user experience.
23 KiB
Stanza Agent Guide
Stanza is a shadcn-style CLI for assembling modular full-stack TypeScript monorepos. It has two core product constraints that should shape every change:
- Generated code is vendored into the user's repo. Do not introduce a
@stanza/runtimedependency or any hidden runtime contract. stanza addmust work against an existing Stanza project, not only duringinit. Treat additions and removals as reversible edits to user-owned code.
Current CLI verbs are init, add, remove, list, search, and doctor. swap and update are planned; the manifest already preserves modules[category][].version and regions for them.
Start Here
- Read the local source before making architectural assumptions. The schemas in
packages/schema/src/are the public contract; module manifests and CLI behavior should derive from them. - Prefer focused changes. The registry, schema, codemods, CLI runner, and web preview share contracts; update every affected boundary together.
- Validate with the Vite+ toolchain. This repo is not a Turbo/Vite/Vitest standalone setup.
- Keep template output realistic. Files under
registry/modules/*/templates/target generated user projects, not this repo's toolchain. - Update
skills/stanza-cli/SKILL.mdwhenever the public CLI contract changes. Agents using that skill may not have access to this source repo.
Repository Map
| Path | Role |
|---|---|
apps/cli/ |
stanza-cli; binary entry is src/bin.ts; publishable ESM built by tsdown through vp pack. |
apps/web/ |
@withstanza/web; TanStack Start visual builder and docs site; private, deployed to Vercel. |
packages/schema/ |
@withstanza/schema; Zod source of truth for stanza.json, module manifests, registry config, package managers, and categories. Publishes the JSON Schema. |
packages/registry/ |
Resolver and synthesis layer: adapter selection, package/env/script/README/template rendering, package JSON previews. Private and inlined into the CLI bundle. |
packages/codemods/ |
Generic ts-morph codemod catalog. Codemods must be idempotent and reversible where removal depends on them. |
packages/utils/ |
Shared path-safety and env-file helpers such as safeRelativePath and appendEnvVar. |
packages/create-stanza/ |
pnpm create stanza / create-package shim; publishable. |
registry/modules/<category>-<id>/ |
First-party registry modules. Each module is data: package.json, module.ts, optional logos/readme, and templates. |
scripts/compile-registry.ts |
Standalone static registry compiler. Emits flat JSON registry files and exports compileRegistry(). |
scripts/publish-registry.ts |
Publishes registry and manifest schema JSON to Vercel Blob. |
skills/stanza-cli/SKILL.md |
Source-repo-independent agent instructions for the published CLI. |
Toolchain
The repo runs on Vite+ (vp). Configuration lives in the root vite.config.ts and package-level Vite+ config where needed. Do not add turbo.json, .oxlintrc.json, .oxfmtrc.json, or per-package vitest.config.ts.
Repo source should import from vite-plus and vite-plus/test. Template files under registry/modules/*/templates/ should keep using stock vite and vitest imports because they are copied into user projects.
Common commands:
vpx jiti apps/cli/src/bin.ts <verb>
vp check
vp check --fix
vp test
vp run -r build
vp run @withstanza/web#dev
vp run @withstanza/web#build
pnpm changeset
Command notes:
- Use
vpx jiti apps/cli/src/bin.ts <verb>for CLI development; no build step is required. - Use
vp checkfor validation loops. It runs format, lint, type checking, and type-awaretsgolint. - Use
vp testfor Vitest 4 throughvite-plus/test; project selection comes from rootvite.config.ts. vp run -r buildruns package builds throughvp packand tsdown.- The web app's
devandbuildscripts first run itscompile-registryVite+ task, which writesapps/web/.registry/{index.json,<slug>.json}. src/routeTree.gen.tsis generated by the web build. If it is missing, runvp run @withstanza/web#buildbefore the firstvp check.- Add a changeset after substantive publishable changes.
stanza-cli,create-stanza, and@withstanza/schemapublish to npm; other workspaces are private.
For web automation, use agent-browser --help and prefer agent-browser over built-in browser tooling unless the immediate environment requires otherwise.
Runtime And Packaging Invariants
- Runtime is Node only. The CLI is published as plain ESM JavaScript with a
#!/usr/bin/env nodebinary. Bun shebangs inscripts/*.tsare convenience only; do not useBun.*APIs. - Root package manager is pnpm. The current root
packageManagerfield is the source of truth for the exact version. - The build pipeline is
vp pack-> tsdown -> ESMdist/. - Source
mainandtypespoint at./src/for workspace development.publishConfigoverrides tarball entrypoints. - External npm dependencies are not bundled. Workspace dependencies are inlined into publishable packages, so transitive runtime packages such as
zodandts-morphmust be directdependenciesof any publishable package that needs them. - The workspace uses isolated pnpm linking. Every workspace package needs its own
@types/nodedev dependency andtypes: ["node"]in its tsconfig when it uses Node globals or APIs. - TypeScript is configured for
allowImportingTsExtensions: trueandnoEmit: true. The repo tsconfig excludes**/templates/**; templates have their own target environment after generation. - Zod 4 finite-key partial maps should use
z.partialRecord(K, V). - TanStack Start must run before React in Vite plugins. In
apps/web, keeptanstackStart()beforereact()and keepverbatimModuleSyntax: falsein its tsconfig.
Core Architecture
Stanza has three layers:
- Schema:
@withstanza/schemadefines the manifest, registry, category, and module contracts. - Registry data: module JSON describes templates, deps, env vars, scripts, logos, README text, and codemod invocations.
- CLI runtime: the runner resolves adapters, renders templates, applies codemods, writes manifests, tracks regions, and performs rollback/removal.
Important boundaries:
- Registry JSON must never ship executable codemod code. It may only reference codemods by
{ id, args }. - Generic codemod implementations live in
packages/codemods/src/builtins/and are statically imported into the CLI bundle. renderTemplatelives in@withstanza/registryso CLI apply and web preview render byte-identical output.safeRelativePathand related path guards are part of the safety boundary. Do not replace them with ad hoc string checks.- Generated projects should not share a root tsconfig base. Emit self-contained
apps/*/tsconfig.jsonandpackages/*/tsconfig.jsonfiles; the Stanza repo root tsconfig is only for this repo.
Categories And Install Homes
The canonical category list is CATEGORIES in packages/schema/src/category.ts. Add or change categories there first.
Each category has:
cardinality:"one"or"many"home:"app","repo", or"package"with a package dir
Derived helpers include KNOWN_CATEGORIES, PEER_CATEGORIES, PACKAGE_DIRS, categoryHome(), categoryLabel(), and categoryCardinality(). Do not maintain hand-written duplicate lists.
Ordering matters. CATEGORIES is topological: a category appears after everything it can peer on, and many categories are leaves.
Peer-resolution rules:
- Only
cardinality: "one"categories are peer candidates. - The resolver iterates
PEER_CATEGORIES. - A
manycategory never dispatches other modules.
Install routing:
home: "app"writes to targeted app dirs.home: "repo"writes root config, root scripts, and root files.home: "package"writespackages/<dir>/, names it@<manifest.name>/<dir>, bootstraps the package through the runner, and addsworkspace:*dependencies from consuming apps.
categoryHome(id) is the single decision point for CLI apply/remove and web package JSON synthesis.
Manifest Rules
StanzaManifestSchema is the single source of truth for stanza.json.
Selections live in:
modules: Partial<Record<CategoryId, StanzaModuleRecord[]>>;
Every module record stores at least id, version, and adapter. It may also store apps, namespace, codemods, and consumesPackages.
apps semantics:
- Required for
home: "app"module records. - Optional for
home: "package"records. Omitted means app-scoped shims apply to every app; an explicit list restricts shim targets. - Forbidden for
home: "repo"records.
Cardinality enforcement:
- For
home: "app"single-choice categories, cardinality is per app. - For
home: "repo"andhome: "package"single-choice categories, cardinality is per project. selectedOne(manifest, category, appId)is the app-aware read helper.
Reserved and runner-managed fields:
- Preserve
modules[category][].version;swapandupdatewill consume it. - Preserve
regions; remove and future update flows rely on ownership data. - Preserve
readmeChecksum; it protects user-edited README files from automatic overwrites. registries,apps, andpackageManagerare user configuration, but schema validation still applies.
The app id doubles as the workspace package suffix:
id: "web"->@<project>/web- This is true regardless of the app's
dir.
Regions And Removal
Region ownership in stanza.json is the source of truth for stanza remove. Two modules claiming the same region should throw RegionConflictError.
Region keys are per-file dot paths owned by module id. Disjoint paths can coexist, for example scripts.test and scripts.test:e2e.
Package bootstrap files are system-owned, not region-owned:
- generated
package.json - generated
tsconfig.json - workspace dependency edges created for slot packages
stanza remove sweeps package dirs derived from PACKAGE_DIRS when no claims remain. Do not add per-module region ownership for bootstrap files just to make removal work.
Regions are not enough for swap yet. That verb needs adapter-region remapping; do not imply it is already designed.
Registry Build And Serving
scripts/compile-registry.ts compiles first-party modules into a flat registry:
<out>/index.json
<out>/<category>-<id>.json
There is no modules/ subdirectory in compiled output. The CLI reads a registry main file from a full URL or filesystem path. The index carries each module's relative path; loaders must resolve that path relative to the index file rather than infer filenames.
Compilation behavior:
- Imports
registry/modules/*/module.ts. - Reads each module's
package.jsonversion as the module version source of truth. - Inlines template file contents into
templates[].content. - Inlines optional
readme.md. - Optimizes and inlines
logo.svgorlogo-light.svgpluslogo-dark.svg. - Writes lightweight metadata to
index.jsonand full module JSON to each module file.
Runner behavior:
- Prefer
tpl.contentwhen present. - Fall back to local
templates/files only when content is absent in local dev/test contexts. - New templates do not need extra build wiring.
Public serving:
- Vercel Blob is the canonical registry and schema origin.
DEFAULT_REGISTRY_URLishttps://stanza.tools/registry/index.json.MANIFEST_SCHEMA_URLishttps://stanza.tools/schema.json.REGISTRY_BASE_URLin@withstanza/schemais the branded registry base.scripts/publish-registry.tsuploads:registry/index.jsonregistry/<slug>.jsonregistry/<slug>@<version>.jsonschema.jsonschema@<version>.json
Vercel routing:
apps/web/vite.config.tsuses legacyroutes, notrewrites, forschema*.jsonandregistry/*.json.- These routes must stay scoped to
.jsonso HTML pages such as/registry,/registry/<cat>, and/registry/<cat>/<id>fall through to TanStack Start. - Keep matching
.jsonfiles out ofpublic/;routeswould shadow them. - Blob must be seeded before these routes go live because
index.jsonandschema.jsonhave no static fallback.
The web app does not fetch the public registry during SSR. It reads the build-time compiled copy at apps/web/.registry/ through Nitro server assets and useStorage("assets:registry").
STANZA_REGISTRY=<url-or-path> overrides the first-party registry source for the CLI. It must point to a registry main file, not a directory.
Third-Party Registries
Third-party registry namespaces are declared under stanza.json#registries. Modules are addressed as:
<category> @<namespace>/<id>
Rules:
@stanzais reserved and cannot be redeclared inregistries.- Unknown namespaces fail fast.
STANZA_REGISTRYonly overrides the first-party source; it does not enable third-party modules.- Registry config can include headers and params with environment-variable expansion.
- Telemetry redacts non-
@stanzamodule ids to<redacted>, but stderr still prints full ids; CI log forwarders may see them.
Third-party codemod implementations are intentionally deferred until a sandboxing and signing model exists. Third-party modules may invoke existing catalog codemod ids but may not ship new codemod code.
Module Authoring Checklist
Modules live at registry/modules/<category>-<id>/.
A normal module directory contains:
package.json
module.ts
templates/
readme.md # optional
logo.svg # optional
logo-light.svg # optional, pair with logo-dark.svg
logo-dark.svg # optional, pair with logo-light.svg
Authoring rules:
module.tsdefault-exportsdefineModule({...}).- Each module belongs to exactly one category.
- Each module has one or more adapters.
- Use
match: {}for the default/no-peer adapter. - Adapter
matchkeys encode peer choices; the resolver picks the most specific matching adapter. - A module can declare one-way
peers, such as{ framework: [...] }, and can have framework-varying adapters regardless of its own category cardinality. - For
manycategories, make scripts, files, env vars, and regions disjoint so modules can coexist. - Declare
consumesPackages: ["<dir>"]at module level when source imports another internal package. Do not hide cross-package imports in an adapter. - Reference internal packages in templates and codemod args with
{{packages.<dir>.name}}, for example{{packages.db.name}}. - Full template context is
{ project, app, package, packages }. app.*is rebound per target app in the apply loop.- Hoist shared
dependencies,devDependencies,env,scripts, andconsumesPackagesto module level. Adapter-level fields merge over them per key;envmerges byname. - Every module package needs a semver
versionin itspackage.json. Bump it when templates, dependencies, env, scripts, README output, or schema-relevant behavior changes.
Template scope:
- Omitted or
"app"resolvesdestinside each targeted app dir. "repo"resolvesdestat repo root."package"resolvesdestinsidepackages/<dir>/for package-home categories only.
For package-home modules, default to scope: "package" for package-owned source. Use app-scoped templates only for thin framework shims that must live in the consuming app, such as a Next route/proxy file. App shims should import from {{package.name}} and usually use template: true.
Do not ship these templates:
package.jsontsconfig.json- framework module
package.json.tpl - package-home
packages/<dir>/package.json
The runner owns package bootstrap through helpers such as ensureSlotPackage and dependency synthesis. Shipping bootstrap templates causes collisions and breaks removal.
Logo rules:
- Use
logo.svgfor a theme-agnostic mark, orlogo-light.svgandlogo-dark.svgfor a theme pair. - First-party logos can usually be pulled from https://api.svgl.app.
- The registry compiler optimizes, namespaces, and inlines SVG markup.
README rules:
- Put module README contribution in sidecar
readme.md. - The compiler inlines it; module authors should not set
readmedirectly inmodule.ts. - README content renders with the same template context as templates.
Codemod Rules
Codemod catalog ids are public API. Once published, renaming an id breaks third-party manifests and existing installed records.
To add a generic codemod:
- Add
packages/codemods/src/builtins/<id>.ts. - Default-export a
Codemod<TArgs>. - Add focused tests beside it.
- Register it in
packages/codemods/src/builtins/index.ts. - Invoke it from modules as
codemods: [{ id, args }].
Rules:
- Codemods must be generic and parameterized by args. Module-specific ids do not belong in the catalog.
- String values in codemod
argsare template-rendered with the same context as files. - For codemods operating inside a package slot, pass
base: "package:<dir>"where supported, for example byre-exportandappend-to-file. - Additions are free. Renames need a deprecation cycle. Removals need a manifest schema version bump and migration plan.
- Keep apply and revert behavior aligned with region ownership and
remove.
CLI Behavior To Preserve
Mutation safety:
- Mutating commands refuse dirty git worktrees unless the user explicitly opts into the dangerous dirty-worktree flag.
adduses a file transaction and should roll back on failure.removeshould use installed manifest snapshots where needed so it can work even after the upstream registry changes.doctoris read-only and should exit non-zero on drift.
Dependency resolution:
initandaddmay refresh compatible npm ranges unless disabled by env.- Keep
STANZA_NO_NPM_LOOKUPandSTANZA_NPM_REGISTRYbehavior intact. - Workspace dependencies between generated packages should use
workspace:*.
Telemetry:
- Honor
--no-telemetry,STANZA_TELEMETRY=0,DO_NOT_TRACK=1, and CI auto-disable. - Do not persist identifiers.
- Redact third-party module ids in telemetry.
When changing public flags, command names, manifest fields, registry config, module addressing, telemetry behavior, or error semantics, update:
- CLI implementation and tests
packages/schematypes/schemas if contract-shaped- docs in
apps/web/content/docs/ skills/stanza-cli/SKILL.md
Web App Rules
The web app is a TanStack Start application with SSR/RSC enabled through its Vite config.
Registry data:
- Web dev/build runs
vp run compile-registryfirst. - Compiled registry data lives in
apps/web/.registry/and is gitignored. - Nitro serves that folder as server assets.
- SSR reads registry data through
apps/web/src/server/registry-base.server.tsanduseStorage("assets:registry"). - Do not fetch
https://stanza.tools/registry/index.jsonfrom production SSR; production loopback to the public site is intentionally avoided.
Syntax highlighting:
- Shiki must stay server-side.
apps/web/src/server/highlighter.server.tsmay import Shiki.apps/web/src/server/highlighter.tscontains client-safe preview types.- Never import
shiki,createHighlighter,codeToHtml, or WASM-loading Shiki runtime from client components. - After
vp run @withstanza/web#build, inspect.output/public/assets/*.jsforcreateHighlighter,codeToHtml, orloadWasm. They should be absent. A plaingrep shikican produce false positives from static MDX code-block classes and CSS vars.
Vite plugin order:
- Keep
tanstackStart()beforereact(). - Keep the Vercel Blob JSON route rules in Nitro/Vercel config scoped to JSON.
Release And Publishing
The release workflow has two independent jobs on pushes to main.
NPM job:
- Installs with
voidzero-dev/setup-vp. - Builds, checks, and tests.
- Uses Changesets to create a release PR or publish.
- Publishes
stanza-cli,create-stanza, and@withstanza/schema.
Registry job:
- Detects changes under
registry/modules/,packages/schema/, orscripts/. - Runs
vpx jiti scripts/publish-registry.tswhen needed. - Uploads latest registry/schema JSON plus immutable version pins to Vercel Blob.
- Is decoupled from npm releases so module changes can go live on merge.
If a registry change modifies an already-published module version, bump that module's package version. Immutable Blob pins are expected not to drift.
Validation Recipes
Default validation:
vp check
vp test
Build everything:
vp run -r build
Run the CLI from source:
vpx jiti apps/cli/src/bin.ts --help
vpx jiti apps/cli/src/bin.ts search
Smoke-test stanza add against a compiled local registry:
vpx jiti scripts/compile-registry.ts "$TMPDIR/stanza-reg"
mkdir -p "$TMPDIR/stanza-smoke/apps/web"
# Seed a minimal stanza.json and apps/web/package.json appropriate for the module under test.
STANZA_REGISTRY="$TMPDIR/stanza-reg/index.json" \
vpx jiti apps/cli/src/bin.ts add <category> <module>
Important details:
STANZA_REGISTRYpoints at the fullindex.jsonpath, not the registry dir.- Compiled registry output is flat under the out dir.
- The CLI has no source-tree registry loader when
STANZA_REGISTRYis unset; it hits the production registry.
Web build checks:
vp run @withstanza/web#build
rg "createHighlighter|codeToHtml|loadWasm" apps/web/.output/public/assets
The rg command above should not find Shiki runtime code in client assets.
Quick Do / Do Not
Do:
- Derive category behavior from
CATEGORIES. - Keep registry modules as data.
- Keep codemods generic and cataloged.
- Preserve manifest
version,regions,readmeChecksum, modulenamespace, and module version pins. - Use structured parsers/helpers for JSON, JSONC, tsconfig, package JSON, env, and TypeScript edits.
- Keep generated project files self-contained.
- Validate with
vp checkand focused tests.
Do not:
- Add a Stanza runtime package to generated projects.
- Add standalone Vite/Vitest config or dependencies to this repo.
- Import Shiki in client code.
- Ship package bootstrap files as module templates.
- Make a
manycategory participate in peer dispatch. - Infer compiled registry filenames instead of reading
pathfrom the index. - Rename or remove published codemod ids casually.
- Drop reserved manifest fields because the current verb does not consume them.