# Introduction (/docs) > Assemble modular full-stack TypeScript monorepos with Stanza. **Stanza** is a [shadcn/ui](https://ui.shadcn.com)-style CLI for assembling modular full-stack TypeScript monorepos. Pick a framework, UI, database, ORM, and auth — Stanza vendors idiomatic code directly into your repo, with no additional runtime dependencies to maintain afterwards. Expect breaking changes, rough edges, and incomplete modules. Take extra care when pointing `stanza-cli` at an existing project; commit your work first, and review the output thoroughly before keeping it. ## Why Stanza [#why-stanza] * **`add` works after `init`.** Run `stanza add` on an existing project. It reads your `stanza.json` manifest, resolves peer constraints, and picks the right adapter for your stack — so adding auth to a Next + Drizzle app writes different code than adding it to a TanStack Start + Prisma app. * **Your code, vendored.** Generated files land in your repo verbatim. There's no `@stanza/runtime` package to install, and nothing to upgrade out from under you — the code is yours to edit. * **Open registry.** Modules are plain static JSON. The CLI ships with a default registry, but you can point it at your own host with `STANZA_REGISTRY` and serve custom modules. ## How it fits together [#how-it-fits-together] You choose modules across a small set of **categories** — `framework`, `ui`, `db`, `orm`, `auth`, `tooling`, `testing`, [and more](/docs/registry). Stanza scaffolds a clean monorepo, vendoring each module's templates, dependencies, and environment variables into the right place: app shell code into your app, data and auth layers into their own workspace packages. Later, `stanza add` and `stanza remove` layer modules in and out without you hand-editing the wiring. ## Next steps [#next-steps] * **[Getting started](/docs/getting-started)** — create your first project and add a module. * **[Concepts](/docs/concepts)** — the mental model: categories, vendoring, peers, and the manifest. * **[CLI reference](/docs/cli)** — every verb, flag, and environment variable. * **[Agents](/docs/agents)** — drive Stanza from Claude Code, Codex, or any coding agent. Or skip ahead and assemble a stack visually with the [builder](/). # Getting started (/docs/getting-started) > Create your first Stanza project and add modules. Scaffold a new monorepo, then layer in modules as you go. ## Prerequisites [#prerequisites] * **Node.js** 22 or newer. * A package manager — **pnpm** (recommended), **npm**, or **bun**. Stanza writes a workspace-based monorepo, so a workspace-aware package manager is required. ## Create a project [#create-a-project] The create command launches an interactive wizard that walks you through each category: ```sh pnpm create stanza my-app ``` The same command works across package managers: ```sh npm init stanza -- my-app bun create stanza my-app ``` To run it without the wizard — in CI, for example — see [Non-interactive setup](/docs/getting-started#non-interactive-setup) below. ## What you get [#what-you-get] Stanza generates a clean monorepo: your app under `apps/web/` (one of potentially many apps as the registry grows), internal packages under `packages/`, and a `stanza.json` manifest at the root. The manifest records the apps in the project, the modules you selected, and which files each one owns — it's how `add` and `remove` know what's installed and what to clean up. You generally don't edit it by hand. ## Install and run [#install-and-run] ```sh cd my-app pnpm install pnpm dev ``` ## Add a module [#add-a-module] Every category is one command away — `framework`, `ui`, `db`, `orm`, `auth`, `tooling`, `testing`, [and more](/docs/registry): ```sh npx stanza-cli add auth better-auth ``` Stanza resolves peer constraints, picks the adapter that matches your stack, and writes the module's templates, dependencies, and environment variables into the appropriate place. Some modules require one or more peer modules to be set up first. Better Auth, for example, peers on `framework` and `orm`, so the command above assumes your project already has an ORM like Drizzle or Prisma. If it doesn't, Stanza fails with `missing-peer` and directs you to add a module from the absent category first. After adding a module, re-run your package manager's install so the new workspace package(s) are linked: ```sh pnpm install ``` ## Inspect and remove [#inspect-and-remove] ```sh npx stanza-cli list # show installed modules, grouped by category npx stanza-cli doctor # check the project still matches its manifest npx stanza-cli search drizzle # find modules in the registry by query npx stanza-cli remove auth # remove a module and sweep the files it owns ``` `stanza search` prints each module's `category/id` pair — use the **id**, not the display label, when passing it to `add`. ## Non-interactive setup [#non-interactive-setup] Pass `--yes` and select each category explicitly with a flag. Single-choice categories take one id; multi-choice categories (like `testing`) take a comma-separated list: ```sh npx -y stanza-cli@latest init my-app --yes \ --framework=next \ --ui=tailwind \ --db=postgres \ --orm=drizzle \ --auth=better-auth \ --testing=vitest,playwright \ --pm=pnpm ``` `--yes` never fills in defaults for categories you omit — any category without a flag is simply skipped. ## Next steps [#next-steps] * [Concepts](/docs/concepts) — the mental model behind apps, categories, vendoring, peers, and the manifest. * [CLI reference](/docs/cli) — every verb, flag, and environment variable. * [Registry](/docs/registry) — the roadmap of available modules and how to pull from [third-party registries](/docs/registry#third-party-registries). * [Authoring](/docs/authoring) — build your own modules and host them in your own registry. * [Agents](/docs/agents) — drive Stanza from Claude Code, Codex, Cursor, or any other coding agent. # Concepts (/docs/concepts) > The mental model behind Stanza — apps, categories, vendoring, peers, and the manifest. A handful of ideas explain how Stanza decides what to write and where. Once they click, the CLI's behavior is predictable. ## Apps [#apps] A Stanza project contains one or more **apps** — each a buildable thing under `apps//` with its own framework and tests. An app is declared in `stanza.json` as: ```json { "id": "web", "dir": "apps/web", "kind": "web" } ``` * **`id`** — short, stable handle. Doubles as the workspace-package suffix (`@/web`) and as the value you pass to `--app=` in the CLI. * **`dir`** — repo-relative directory the app lives in. * **`kind`** — `web` or `native`. Lets Stanza reject obviously-wrong installs (e.g. installing Next.js into a `kind: "native"` app). `stanza init` scaffolds a single web app today (`{ id: "web", dir: "apps/web", kind: "web" }`). Multi-app projects — a web app and an Expo native app sharing a `packages/db/`, for example — are on the roadmap, and the schema is already shaped for them. App-home module records carry an `apps: [""]` field so `add`/`remove` know which app to target; package-home modules ship their core code once and route shims into the apps that consume them. ## Categories [#categories] Every module belongs to exactly one **category**. A category has two independent properties that govern how its modules behave: * **Cardinality** — `one` (single-choice) or `many` (coexisting). * **Home** — where a module's output lands: `app`, `repo`, or `package`. | Category | Cardinality | Home | | ----------- | ----------- | ------- | | `framework` | one | app | | `ui` | one | package | | `db` | one | package | | `orm` | one | package | | `auth` | one | package | | `tooling` | one | repo | | `testing` | many | app | A `one` category holds at most one module — adding a second fails until you remove the first. For app-home categories that limit is **per app**: a project with two apps could pick `next` for the web app and `expo` for the native one. A `many` category lets modules coexist (e.g. Vitest and Playwright side by side). **Home** decides placement: * `app` modules wire a specific app's shell (in `apps/web/`, say). They install into the app(s) named in the module record's `apps` field. * `repo` modules write config at the monorepo root. * `package` modules install into their own internal workspace package under `packages//`, named `@/`, which every consuming app depends on via `workspace:*`. `db` and `orm` share a single `packages/db/` so the ORM client sits next to the schema it queries. **`framework` is one category, not many.** A module's `appKind` (`web` or `native`) pins it to a platform — Next is `appKind: "web"`, a future Expo module would be `appKind: "native"`. Splitting the category per platform would force every peer-aware category (ui, testing, …) to split too, which would be a worse design. See the [registry](/docs/registry) for the roadmap. The table above is a representative subset. The full taxonomy also includes `ai`, `payments`, `email`, and `monorepo` (single-choice) and `deploy` (coexisting) — most with modules available today — plus `api` (single-choice), defined with its modules on the roadmap. See the [module registry](/docs/registry) for the complete list and per-module status. ## Vendoring [#vendoring] Stanza copies a module's code into your repo verbatim. There's no shared runtime package — the generated files are ordinary source you can read, edit, and commit. That means upgrades never silently change your app's behavior, and you're never locked into Stanza's abstractions. ## Peers and adapters [#peers-and-adapters] Modules declare **peers** — the other categories they care about. When you `add` a module, Stanza reads your current selections and picks the **adapter** that matches. Better Auth, for instance, peers on both `framework` and `orm`, so adding it to a Next + Drizzle app produces different wiring than a TanStack Start + Prisma app. You pick the module; Stanza picks the right variant. Peers are scoped to the app being targeted: in a multi-app project, the `ui` module's adapters peer-match against the active app's framework. That's how the same Tailwind module can ship a `postcss.config.mjs` for a Next.js web app while a future NativeWind module ships `tailwind.config.js` for an Expo native app. ## The manifest (`stanza.json`) [#the-manifest-stanzajson] The `stanza.json` file at your repo root is the source of truth for what's installed: ```json { "$schema": "https://stanza.tools/schema.json", "version": "0.4", "projectShape": "monorepo", "packageManager": "pnpm", "name": "acme", "apps": [{ "id": "web", "dir": "apps/web", "kind": "web" }], "modules": { "framework": [{ "id": "next", "version": "0.1.0", "adapter": "default", "apps": ["web"] }], "db": [{ "id": "postgres", "version": "0.1.0", "adapter": "default" }], "auth": [ { "id": "better-auth", "version": "0.1.0", "adapter": "next+drizzle", "apps": ["web"] } ], "tooling": [{ "id": "oxlint-oxfmt", "version": "0.1.0", "adapter": "default" }] }, "regions": { /* … */ } } ``` `add`, `remove`, and `list` all read it. Treat it as generated state: don't hand-edit it unless you're repairing something deliberately. A module record's `apps` field tells Stanza which app(s) the install targets: * **Required** for `home: "app"` modules (framework, testing). One record per app. * **Optional** for `home: "package"` modules. Omitted means "ship app-scoped shims into every app"; an explicit list restricts to those apps. The package itself is always written once under `packages//`. * **Forbidden** for `home: "repo"` modules — they're project-wide. ## Regions [#regions] A **region** is a claim on a file (or a section of one) by a specific module. This is how `stanza remove` knows exactly what to delete and what to leave alone: it sweeps only the regions the removed module owns. Because regions key on the module, two modules can safely write disjoint parts of the same file — Vitest owning the `test` script and Playwright owning `test:e2e`, for example, without conflict. Per-app writes live at distinct paths (`apps/web/page.tsx` vs `apps/native/app/(tabs)/index.tsx`), so they never collide either. ## Open registry [#open-registry] A registry is just static JSON: an index plus one file per module, each carrying its templates, dependencies, env vars, and codemod invocations. The CLI ships with a default registry under the `@stanza` namespace, and you can publish your own modules under any `@scope` you want and consume them alongside the first-party ones — see [Third-party registries](/docs/registry#third-party-registries) for the manifest field and the `@ns/id` CLI syntax. The full module + registry surface is documented in the [Authoring manual](/docs/authoring). # CLI (/docs/cli) > The Stanza command-line verbs, flags, and environment variables. Stanza ships six verbs against the category taxonomy. Run any verb with `--help` for its full flag list: ```sh npx stanza-cli add --help ``` ## `init` [#init] Scaffold a new monorepo. Without flags it launches an interactive wizard; with `--yes` it takes every pick from flags. ```sh npx stanza-cli init [name] --yes --framework=next --orm=drizzle --db=postgres --pm=pnpm ``` * `name` — project directory name (positional; prompted if omitted). * `--yes` — non-interactive; take selections from category flags. * `--=` — pick modules for a category. There's one flag per category in the [registry](/docs/registry) (`--framework`, `--ui`, `--db`, `--orm`, `--auth`, `--ai`, `--payments`, `--email`, `--tooling`, `--testing`, `--deploy`, `--monorepo`, …); single-choice categories take one id, multi-choice categories take a comma-separated list. Omitted categories are skipped — `--yes` chooses no defaults. * `--pm=` — package manager recorded in the manifest. `init` always scaffolds a **single web app** today — `{ id: "web", dir: "apps/web", kind: "web" }` in `stanza.json` — and tags every app-home record with `apps: ["web"]`. Multi-app init (scaffolding `apps/native` alongside `apps/web`, for example) is planned; the schema and runtime already support it. See [Concepts → Apps](/docs/concepts#apps). ## `add` [#add] Add one module to an existing project. ```sh npx stanza-cli add [--app=] ``` Resolves peers, selects the matching adapter, and writes the module's templates, deps, env, and scripts to the right home. If an apply step fails partway through — including inside a codemod — Stanza rolls the changes back, so a failed `add` leaves your tree as it was. Adding a module to a single-choice category that's already filled fails until you remove the existing one — and that limit is **per app** for `home: "app"` categories (so picking a framework for one app doesn't block a different framework for another). * `--app=` — pick which app the install targets. Required for app-relevant modules when the project has multiple apps and you're not running inside one of them. Single-app projects auto-target; multi-app projects also auto-pick when `cwd` is inside one app's directory. Interactive runs (TTY, no `--yes`) fall back to a prompt; non-interactive runs error out asking for the flag. * The flag is meaningless for `home: "repo"` modules like `tooling`. ## `remove` [#remove] Remove a module and sweep the files (regions) it owns. ```sh npx stanza-cli remove [id] [--app=] ``` For single-choice categories the `id` is optional; for multi-choice categories (like `testing`) it's required. `--app=` scopes removal in projects with multiple apps — without it, `remove` looks across every app for a matching record. The package-dir sweep (when removing the last module under `packages//`) strips the `workspace:*` dep from *every* app's `package.json`, not just the first one. ## `list` [#list] Print installed modules, grouped by category, from the nearest `stanza.json`. ```sh npx stanza-cli list ``` ## `search` [#search] List registry modules and their `category/id` pairs. Pass a query to filter. ```sh npx stanza-cli search [query] ``` Use the printed **id** (not the display label) when passing a module to `add`. ## `doctor` [#doctor] Check `stanza.json` against the filesystem and report drift, without changing anything. ```sh npx stanza-cli doctor ``` Walks every [region](/docs/concepts#regions) claim in the manifest and verifies it still holds on disk — claimed files exist, claimed dependencies/scripts/env vars are still present, and each internal package with claims is wired into its consuming apps. Read-only; exits non-zero when it finds drift, so it slots into CI or a pre-commit check. It doesn't repair anything — use `add` / `remove` for that. **Planned:** `swap` (replace a module with another in the same category) and `update` (re-pull a module at a newer version) are on the roadmap. The manifest already reserves the fields they need; the verbs aren't implemented yet. ## Global flags [#global-flags] These apply to the mutating verbs (`init`, `add`, `remove`): * `--dry-run` — print the actions that would be taken and write nothing. * `--dangerously-allow-dirty` — allow a mutating command to run with a dirty git working tree. By default Stanza refuses, so its edits never mix with uncommitted changes. Commit or stash first when you can. * `--no-telemetry` — disable anonymous usage events for this invocation. ## Environment variables [#environment-variables] * `STANZA_REGISTRY=` — override the `@stanza` default namespace's source: the full URL or filesystem path to a registry's **main JSON file** (not a directory). For per-namespace third-party registries, use the `registries` field in `stanza.json` instead — see [Third-party registries](/docs/registry#third-party-registries). * `STANZA_NO_NPM_LOOKUP=1` — skip npm version lookups and write dependency ranges verbatim. * `STANZA_NPM_REGISTRY=` — override the npm registry used for version lookups. * `STANZA_TELEMETRY=0` / `DO_NOT_TRACK=1` — disable telemetry persistently. Telemetry is also auto-skipped in CI. * `STANZA_TELEMETRY_URL=` — point telemetry at a self-hosted ingest endpoint instead of `https://stanza.tools/api/events`. ## Telemetry [#telemetry] Stanza captures a small set of anonymous events to help us see which modules people actually pick. The aggregates are surfaced publicly on the [Stats page](/stats). **What's sent** * The command name (`init`, `add`, `remove`, `list`, `search`, `doctor`), its duration in milliseconds, and whether it succeeded. * CLI version, Node version, OS, and architecture. * For installs and removes: the module id, its category, and the namespace it came from (e.g. `@stanza` or `@acme`). * An ephemeral UUID generated per process so events from the same run can be grouped. It's regenerated next time and never persisted. **Third-party modules** Third-party module installs are counted in the aggregate totals at the top of the [Stats page](/stats) (so adoption of Stanza-as-a-platform shows up), but they're filtered out of the per-category leaderboards underneath. A private `@acme/auth-internal` doesn't outrank `@stanza/better-auth` in a public ranking. **What's never sent** * File paths, project names, the contents of templates or env files, or anything else that could identify a project or person. * Your IP address — the CLI posts to a server-side proxy on `stanza.tools` that hands off to PostHog without forwarding the request IP. * Anything from CI environments. The CLI auto-detects `CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `CIRCLECI`, and `BUILD_NUMBER`. **Opting out** * One invocation: `--no-telemetry`. * Persistently: `STANZA_TELEMETRY=0` or `DO_NOT_TRACK=1` in your shell. The implementation is a single file: [`apps/cli/src/lib/telemetry.ts`](https://github.com/jakejarvis/stanza/blob/main/apps/cli/src/lib/telemetry.ts). ## Dependency versioning [#dependency-versioning] On `init` and `add`, Stanza bumps each `^`/`~` dependency range to the latest npm version that satisfies it, keeping the modifier. Other ranges and `workspace:*` specifiers are written as-is. When offline, it falls back to the range declared in the module. # Registry (/docs/registry) > The roadmap of first-party modules Stanza ships — what's available today and what's planned. Every module fills exactly one **category**. This page is the canonical roadmap of the first-party modules Stanza ships: what's available today, and what's on the way. Inspired by [shadcn/ui](https://ui.shadcn.com), third parties can publish their own modules under any `@scope` and have users pull them in alongside these. See [Third-party registries](/docs/registry#third-party-registries) below, or the [Authoring manual](/docs/authoring) for the full implementation details. ## Categories [#categories] A category has two independent properties: its **cardinality** — `one` (single-choice, like `framework` or `auth`) or `many` (coexisting, like `testing`) — and its **home**, where a module's output lands (`app`, `repo`, or `package`). See [Concepts](/docs/concepts#categories) for the full mental model. | Category | Cardinality | Home | What it covers | | ----------- | ----------- | ------- | ----------------------------------------------------------- | | `framework` | one | app | App shell + router (Next, TanStack Start, Expo, …) | | `api` | one | package | Typed RPC layer between framework and services | | `ai` | one | package | AI SDK + provider wiring | | `auth` | one | package | Authentication (Better Auth, Clerk, …) | | `db` | one | package | Database driver (Postgres, SQLite) | | `orm` | one | package | Schema + query layer (Drizzle, Prisma) | | `ui` | one | package | Styling system + component primitives (Tailwind, shadcn, …) | | `payments` | one | package | Checkout + webhooks (Stripe, Polar, …) | | `email` | one | package | Transactional email (Resend) | | `tooling` | one | repo | Lint/format toolchain | | `testing` | many | app | Unit + e2e (Vitest, Playwright) | | `deploy` | many | repo | Deploy targets (Vercel, Cloudflare, …) | | `monorepo` | one | repo | Monorepo task runner (Turborepo) | Single-choice categories with `home: "app"` (currently `framework`) are enforced **per app** — a multi-app project can pick `next` for the web app and `expo` for a native app side by side. See [Concepts → Apps](/docs/concepts#apps). ## Framework [#framework] Single-choice per app — the chosen framework wires the app shell, router, and React (or future) runtime. Modules declare an **`appKind`** (`web` or `native`) which the runner uses to validate they're installed into a compatible app: you can't install Next.js into a `kind: "native"` app, or Expo into a `kind: "web"` one. A multi-app project can pick one framework per app. | Module | `appKind` | Status | Notes | | ---------------- | --------- | ------------ | ---------------------------------------------------------------------------------------- | | `tanstack-start` | web | 🟢 Available | TanStack Start on Vite (no Vinxi). Provides `web`, `react`, `ssr`, `node`. | | `next` | web | 🟢 Available | Next.js 16 (App Router). Provides `web`, `react`, `ssr`, `rsc`, `node`, `edge`. | | `nuxt` | web | 🟨 Planned | Vue. Requires relaxing the React-implicit assumption in peer modules (capability `vue`). | | `svelte` | web | 🟨 Planned | SvelteKit (capability `svelte`). | | `solid` | web | 🟨 Planned | SolidStart (capability `solid`). | | `expo` | native | 🟨 Planned | Expo + React Native. Unblocks the first `kind: "native"` app. | ## API [#api] Single-choice, constraint-bearing. An optional layer between the framework and the database or services, installed as its own workspace package. | Module | Status | Notes | | ------ | ---------- | --------------------------------- | | `trpc` | 🟨 Planned | tRPC v11; per-framework adapters. | | `orpc` | 🟨 Planned | oRPC. | ## AI [#ai] Single-choice, constraint-bearing, installs into `packages/ai/`. | Module | Status | Notes | | --------------- | ------------ | ----------------------------------------------------------------------------------- | | `vercel-ai-sdk` | 🟢 Available | `ai` + `@ai-sdk/openai` with a streaming `/api/chat` route. Peers on `framework`. | | `tanstack-ai` | 🟢 Available | `@tanstack/ai` + `@tanstack/ai-openai` with a streaming `/api/chat` route over SSE. | ## Authentication [#authentication] Single-choice, installs into `packages/auth/`. | Module | Status | Notes | | ------------- | ------------ | ------------------------------------------- | | `better-auth` | 🟢 Available | Local data. Peers on `orm` and `framework`. | | `clerk` | 🟢 Available | Hosted database & UI. Peers on `framework`. | | `workos` | 🟨 Planned | WorkOS AuthKit. | ## Database [#database] Single-choice, installs into `packages/db/`. | Module | Status | Notes | | ---------- | ------------ | ------------------------ | | `postgres` | 🟢 Available | `postgres` driver. | | `sqlite` | 🟢 Available | `better-sqlite3` driver. | ## ORM [#orm] Single-choice, shares `packages/db/` with the database driver. | Module | Status | Notes | | --------- | ------------ | --------------------------------------------------- | | `drizzle` | 🟢 Available | Drizzle ORM 0.45. Peers on `db` (postgres, sqlite). | | `prisma` | 🟢 Available | Prisma 7. Peers on `db` (postgres, sqlite). | ## UI [#ui] Single-choice. Installs into `packages/ui/` so styles + component primitives are shared across every app via `workspace:*`. Adapters branch on the active app's `framework` peer (PostCSS for Next, Vite plugin for TanStack Start, …). | Module | For | Status | Notes | | -------------- | ------ | ------------ | --------------------------------------------------------------------- | | `tailwind` | web | 🟢 Available | Bare Tailwind v4 in `packages/ui/`; apps re-export postcss config. | | `shadcn-radix` | web | 🟢 Available | shadcn/ui registry preset `radix-nova` (Radix primitives) + Tailwind. | | `shadcn-base` | web | 🟢 Available | shadcn/ui registry preset `base-nova` (Base UI) + Tailwind. | | `nativewind` | native | 🟨 Planned | Tailwind-style className API for React Native. | | `unistyles` | native | 🟨 Planned | Native StyleSheet with theme + breakpoint support. | ## Payments [#payments] Single-choice, constraint-bearing. Installs into `packages/payments/`, plus a Better Auth plugin when auth is selected. | Module | Status | Notes | | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `polar` | 🟢 Available | `@polar-sh/sdk` + framework-native route handlers. Bridge variant (when `auth: better-auth` is selected) ships `@polar-sh/better-auth` and wires the plugin into `auth.ts`; the Better Auth catchall serves checkout / portal / webhooks. | | `stripe` | 🟢 Available | `stripe` SDK + Checkout Sessions / webhook route handlers. Bridge variant ships `@better-auth/stripe`; the Better Auth catchall serves `/api/auth/stripe/webhook` plus subscription endpoints. | | `autumn` | 🟨 Planned | Autumn. | | `dodo` | 🟨 Planned | Dodo Payments. | ## Email [#email] Single-choice, installs into `packages/email/`. | Module | Status | Notes | | -------- | ------------ | ----------------------------------------------------------------------------------------------------- | | `resend` | 🟢 Available | `resend` SDK + a sample `react-email` template; framework-native `/api/webhook/resend` Svix-verifier. | ## Tooling [#tooling] Single-choice, repo-scoped. The three toolchains are mutually exclusive substitutes. | Module | Status | Notes | | ----------------- | ------------ | ------------------------------------------------------ | | `eslint-prettier` | 🟢 Available | ESLint flat config + Prettier; per-framework adapters. | | `biome` | 🟢 Available | Biome (lint + format), framework-agnostic. | | `oxlint-oxfmt` | 🟢 Available | Oxlint + oxfmt, framework-agnostic. | ## Testing [#testing] Coexisting, app-scoped. Vitest and Playwright are independent and routinely run side by side. | Module | Status | Notes | | ------------ | ------------ | ----------------------------------------------------------------- | | `vitest` | 🟢 Available | Unit + integration; `jsdom` + RTL; `test`/`test:watch` scripts. | | `playwright` | 🟢 Available | e2e; per-framework `webServer`; `test:e2e`/`test:e2e:ui` scripts. | ## Deploy [#deploy] Coexisting, repo-scoped. | Module | Status | Notes | | ------------ | ---------- | ------------------------------------------------------------------------------ | | `vercel` | 🟨 Planned | `vercel.json` + framework-specific output. | | `cloudflare` | 🟨 Planned | Workers / Pages adapter per framework (e.g. Vite plugins, `wrangler.json`, …). | | `railway` | 🟨 Planned | `railway.toml` + Dockerfile. | | `docker` | 🟨 Planned | Generic `Dockerfile` + compose for self-host. | ## Monorepo [#monorepo] Coexisting, repo-scoped. Currently hardcoded as Turborepo; becomes a real configurable category when a second option lands. | Module | Status | Notes | | ----------- | ------------ | ------------------------------------------------------------------ | | `turbo` | 🟢 Available | Turborepo v2 | | `vite-plus` | 🟨 Planned | Locked to Vite-compatible frameworks (TanStack, etc.) and modules. | ## Package manager [#package-manager] Not a category — a top-level field in `stanza.json` (`packageManager: "pnpm" | "bun" | "npm"`). The wizard prompts for it; codemods only ever touch `package.json`, never lockfiles. | Manager | Status | | ------- | ---------------------- | | pnpm | 🟢 Available (default) | | bun | 🟢 Available | | npm | 🟢 Available | | yarn | 🛑 Not Planned | ## Third-party registries [#third-party-registries] The categories above are first-party — Stanza ships them under the default `@stanza` namespace. Anyone can publish additional modules under their own `@scope` and have users install them with `stanza add @scope/`. The category taxonomy itself stays first-party (third-party registries can't redefine it). For consumers, declaring a registry is the full URL of its main JSON file in `stanza.json`: ```jsonc { "registries": { "@acme": "https://reg.acme.example/index.json" } } ``` For authors, the full module surface — `defineModule`, templates, codemods, the build pipeline, hosting, namespace conventions, and a worked example — lives in the [Authoring manual](/docs/authoring). # Authoring (/docs/authoring) > Build your own Stanza modules and host them in a registry — first-party or third-party. A **module** is a self-contained recipe for adding one piece of functionality to a Stanza project: templates, dependencies, env vars, scripts, and optional codemod invocations. A **registry** is a directory of modules served as static JSON. This page is for both kinds of authors: * **First-party contributors** working inside this repo (`registry/modules//`) — your modules ship under the default `@stanza` namespace. * **Third-party authors** publishing modules under your own `@scope` for users to pull in via [`registries` in `stanza.json`](/docs/registry#third-party-registries). The module surface is identical for both. Only distribution differs. ## Module anatomy [#module-anatomy] A module lives in a directory named `-/` and exports a default `defineModule({ … })` from `module.ts`: ```ts // registry/modules/testing-vitest/module.ts import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "vitest", category: "testing", label: "Vitest", description: "Fast unit + integration test runner powered by Vite.", version: "0.1.0", homepage: "https://vitest.dev", // Peers this module needs filled. The resolver uses these to pick an adapter // and to refuse `add` when a required peer category is empty. peers: { framework: ["next", "tanstack-start"] }, // Shared install fields — apply to every adapter unless overridden. devDependencies: { vitest: "^4.1.7", jsdom: "^29.1.1", "@testing-library/react": "^16.3.2", }, scripts: { test: "vitest run", "test:watch": "vitest", }, // One install recipe per peer combination. `match: {}` is the default. adapters: [ { key: "next", match: { framework: "next" }, devDependencies: { "@vitejs/plugin-react": "^6.0.2" }, templates: [ { src: "vitest.config.ts", dest: "vitest.config.ts", scope: "app" }, { src: "example.test.ts", dest: "tests/example.test.ts", scope: "app" }, ], }, { key: "tanstack-start", match: { framework: "tanstack-start" }, templates: [ { src: "vitest.config.ts", dest: "vitest.config.ts", scope: "app" }, { src: "example.test.ts", dest: "tests/example.test.ts", scope: "app" }, ], }, ], }); ``` The whole shape is typed and validated at runtime — `defineModule` throws on the structural rules TypeScript can't express (e.g. `app` install-field overlays on `home: "repo"` modules), and `ModuleSchema` parses the JSON form the CLI fetches. | Field | Required | What it does | | -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `id` | ✓ | Stable identifier within a category. Used in `stanza add `. | | `category` | ✓ | One of `KNOWN_CATEGORIES` — see [Categories](/docs/concepts#categories). | | `label` | ✓ | Display name for the wizard, search results, and the web builder. | | `description` | ✓ | One-line summary. Surfaced in `stanza search` and module cards. | | `version` | ✓ | Semver string. Pinned into `stanza.json` at install time so future `update`/`swap` can read it. | | `peers` | | `Partial>` — categories this module needs filled. | | `consumesPackages` | | Dirs of other internal packages this one imports from (see [Cross-package consumption](/docs/authoring#cross-package-consumption)). | | `adapters` | ✓ | At least one. Each carries its own install fields, templates, and codemods. | | `appKind` | | `"web"` or `"native"`. The runner refuses to install into an incompatible app. | | `homepage`, `author` | | Surfaced in the web builder; otherwise informational. | Module-level install fields (`dependencies`, `devDependencies`, `env`, `scripts`) are merged into every adapter — adapter-level values override per key, and `env` merges by `name`. Hoist anything that doesn't vary across adapters. ## Categories and homes [#categories-and-homes] Every module fills exactly one [category](/docs/concepts#categories). A category's **home** (`app`, `repo`, or `package`) decides where the module's output lands; it isn't something the module declares — it's a property of the category: * **`home: "app"`** (`framework`, `testing`) — templates land in `apps//`. Module records track which app(s) they installed into via the `apps` field. * **`home: "repo"`** (`tooling`, `monorepo`, `deploy`) — config + scripts land at the monorepo root. * **`home: "package"`** (`auth`, `db`, `orm`, `ui`, `api`, `ai`, `payments`, `email`) — the module installs into its own workspace package at `packages//`, named `@/`. Every consuming app gets a `workspace:*` dep. Template `scope` derives from this. For a package-home module, prefer `scope: "package"` for everything that can live inside the package, and reach for `scope: "app"` only when a framework convention forces a file to the app root (e.g. Next's `proxy.ts`). ## Adapters and peer resolution [#adapters-and-peer-resolution] When you `add` a module, Stanza picks the **most specific** adapter whose `match` is consistent with your current stack. Better Auth's adapter list has entries like `next+drizzle+postgres` and `tanstack-start+prisma+sqlite`; the resolver scores each adapter by the number of peers it matches and picks the highest. A module can pin a default adapter with `match: {}` (an empty match wins when no peer-specific adapter applies). If a module declares `peers` but no adapter matches the current selection, `add` fails with `no-adapter` — fall back to a more permissive `match: {}` adapter or narrow your `peers` to what's actually supported. ```ts adapters: [ // Adapter for projects using Next + Drizzle + Postgres specifically. { key: "next+drizzle+postgres", match: { framework: "next", orm: "drizzle", db: "postgres" }, templates: [/* … */], }, // Catch-all when no peer-specific adapter applies (or as a fallback). { key: "default", match: {} }, ], ``` ## Templates [#templates] Templates are files copied into the user's project. They live under `templates/` next to `module.ts` and are referenced by `src` (path under `templates/`) and `dest` (path in the generated project, resolved against `scope`): ```ts templates: [ { src: "vitest.config.ts", dest: "vitest.config.ts", scope: "app" }, { src: "example.test.ts", dest: "tests/example.test.ts", scope: "app", template: true }, ], ``` | `scope` value | Where `dest` resolves | | ----------------- | --------------------------------------------------------------------------------- | | `"app"` (default) | Each targeted app's `dir` — emits once per consuming app. | | `"repo"` | The monorepo root. | | `"package"` | `packages//` where `` is the category's `home.dir` (package-home only). | Set `template: true` to run the file through [Handlebars](https://handlebarsjs.com/) before writing. The render context is: | Token | Value | | --------------------------------------------- | ----------------------------------------------------------------------------------- | | `{{project.name}}` | The manifest's `name` (npm-scope-style). | | `{{app.id}}` / `{{app.dir}}` / `{{app.kind}}` | The active target app. | | `{{package.name}}` | The active module's own package (e.g. `@my-app/auth`); empty for non-package homes. | | `{{packages..name}}` | Any other package by its dir, e.g. `{{packages.db.name}}` → `@my-app/db`. | | `{{peers.}}` | Id of the active one-cardinality pick in that category, e.g. `next`. | | `{{pm}}` | The project's package manager (`pnpm` \| `bun` \| `npm`). | | `{{env}}` | Sorted list of every env var name declared so far. | Conditional blocks use Handlebars' [built-ins](https://handlebarsjs.com/guide/builtin-helpers.html) plus an `eq` helper: ```hbs {{#if (eq peers.framework "next")}} import { NextResponse } from "next/server"; {{/if}} ``` The render context is rebound per target app, so a package-home module shipped into both a web and a native app renders each correctly. **Template bodies are inlined by the build.** [`scripts/compile-registry.ts`](https://github.com/jakejarvis/stanza/blob/main/scripts/compile-registry.ts) reads every template file and bakes its contents onto `tpl.content` in the per-module JSON. HTTP-loaded modules are self-contained — the CLI never makes follow-up requests for template files. ## Codemod invocations [#codemod-invocations] Modules can ask the runner to invoke a generic codemod from Stanza's built-in catalog with module-specific arguments: ```ts adapters: [ { key: "next", match: { framework: "next" }, codemods: [ { id: "wrap-root-layout", args: { providerImport: "{{package.name}}", providerName: "ClerkProvider", }, }, ], }, ], ``` The catalog lives at [`packages/codemods/src/builtins/`](https://github.com/jakejarvis/stanza/tree/main/packages/codemods/src/builtins). Each codemod is parameterized by its own `args` type so the same generic transformation serves multiple modules (for example, [`wrap-root-layout`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/wrap-root-layout.ts) handles both Clerk and any future provider-style auth/state library). **Modules cannot ship codemod code** — the registry JSON carries data (`{ id, args }`), never executable code. This is enforced for first-party and third-party modules alike: the runner throws when an adapter references a codemod id that isn't in the catalog. If your module needs a transformation no existing codemod covers: * **First-party**: design a new generic codemod with the right argument surface and add it to the catalog ([packages/codemods/src/builtins/index.ts](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/index.ts)). * **Third-party**: open an issue or PR for the codemod you'd need. Per-registry codemod sandboxing isn't on the near-term roadmap. String values inside `args` go through the same mustache substitution as templates, so you can reference `{{package.name}}`, `{{packages.db.name}}`, etc. ### The built-in catalog [#the-built-in-catalog] These ten codemods ship in Stanza today. Each is idempotent (re-applying is a no-op) and reversible (`stanza remove` runs its inverse), and each claims a [region](/docs/concepts#regions) so two modules can't silently fight over the same edit. | `id` | What it does | Required args | | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | [`add-plugin-to-call`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/add-plugin-to-call.ts) | Splice a call into a flat `: [...]` array inside a call's object argument (`defineConfig({ plugins: [...] })`, `betterAuth({ plugins: [...] })`). Creates the array if missing. | `file`, `callee`, `property`, `call` | | [`add-array-entry-in-call`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/add-array-entry-in-call.ts) | The nested sibling of `add-plugin-to-call` — walks a dotted `property` path to a deeper array; suffix a segment with `()` to dive into an arrow-returned object (`head().links`). | `file`, `callee`, `property`, `entry` | | [`add-jsx-child`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/add-jsx-child.ts) | Insert a JSX element as a child of a named parent element (`` into a starter `
`). Gate with `onlyIfContains` to skip user-customized files. | `file`, `parent`, `element` | | [`wrap-root-layout`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/wrap-root-layout.ts) | Wrap the framework root layout's children with a provider element. Dispatches per framework — Next `app/layout.tsx` `{children}`, TanStack Start `src/routes/__root.tsx` `` — so the module never names the file. | `providerName`, `providerImport` | | [`set-html-attributes`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/set-html-attributes.ts) | Set or merge attributes on the root `` element. Each attribute is a bare `boolean`, a string `value` (token-merged for `className`), or an `expression`. | `file`, `attributes` | | [`replace-import`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/replace-import.ts) | Swap the module specifier of an existing `import` in place (`./globals.css` → `@my-app/ui/globals.css`), preserving the import kind and bindings. | `file`, `from`, `to` | | [`re-export`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/re-export.ts) | Append an `export … from "X"` to a barrel file. Star-over-star and named merges are no-ops; mismatched shapes throw. | `file`, `from` | | [`add-package-dep`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/add-package-dep.ts) | Add a dependency to a target `package.json`. For cross-package wiring only — prefer the declarative `dependencies` / `devDependencies` fields when the dep belongs in the module's own package. | `name` | | [`set-tsconfig-paths`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/set-tsconfig-paths.ts) | Merge entries into a tsconfig's `compilerOptions.paths` (sets `baseUrl: "."` when missing). Refuses when the file `extends` a parent. | `paths` | | [`append-to-file`](https://github.com/jakejarvis/stanza/blob/main/packages/codemods/src/builtins/append-to-file.ts) | Append or prepend a marker-wrapped text block to a non-TS file (Prisma schema, CSS `@import`, YAML, `.env`). The markers give robust idempotency without parsing the host format. | `file`, `content`, `marker` | Several conventions are shared across the catalog: * **`base`** — where `file` resolves: `"app"` (default, the active app's dir), `"repo"` (monorepo root), or `"package:"` (`packages//`). `add-package-dep` and `set-tsconfig-paths` use it to pick the target file; `re-export` accepts only `"app"` / `"package:"`; `append-to-file` splits it into `scope: "repo" | "app"` plus a separate `base: "package:"`; `wrap-root-layout` has no `base` (the path is derived from the selected framework). * **`imports`** — `Array<{ from, named?, default? }>` on the three codemods that introduce new symbols (`add-plugin-to-call`, `add-array-entry-in-call`, `add-jsx-child`); each entry merges into one import declaration. * **`position`** — `"start"` / `"end"` for placement, plus `before:` / `after:` on the array codemods (a missing anchor warns and falls back to `"end"`). * **`regionKey`** — every codemod derives a sensible default; override it only when two invocations would otherwise collide on the same key. When none of these fit a transformation your module needs, [add a new generic codemod](/docs/authoring#codemod-invocations) rather than reaching for a bespoke one — the catalog is the only execution surface for first- and third-party modules alike. ## Install fields [#install-fields] `dependencies`, `devDependencies`, `env`, and `scripts` can sit at either the module level (shared across adapters) or the adapter level (variation per peer combination). Adapter wins per-key on conflicts; `env` merges by `name`. ```ts defineModule({ // … // Declared once — every adapter inherits these. dependencies: { "better-auth": "^1.6.11" }, env: [ { name: "BETTER_AUTH_SECRET", example: "change-me-in-prod", required: true, description: "Better Auth signing secret.", }, ], adapters: [ { key: "next+drizzle", match: { framework: "next", orm: "drizzle" }, // Only this adapter ships the drizzle adapter package. dependencies: { "better-auth-drizzle": "^1.0.0" }, // … }, ], }); ``` ### The `app` overlay [#the-app-overlay] Package-home modules sometimes need to install a dep into the consuming app, not the package itself — e.g. shadcn's theme provider lives in `packages/ui/` but imports `next-themes` from `apps/web/`. Use the `app:` overlay for that: ```ts defineModule({ category: "ui", // … app: { dependencies: { "next-themes": "^0.3.0" }, }, }); ``` The runner routes overlay fields into every consuming app's `package.json` (vs the main fields, which route to `packages//`). It's forbidden on `home: "repo"` modules — no app target to route to — and redundant on `home: "app"` modules. ## Cross-package consumption [#cross-package-consumption] If your module's source imports from another internal package (e.g. Better Auth's `auth.ts` reads `db` from the ORM package for its database schema), declare the dependency at the module level: ```ts defineModule({ category: "auth", consumesPackages: ["db"], // … }); ``` The runner adds `@/db: workspace:*` to this module's own `package.json`. Use the substitution token in templates: ```ts // templates/auth.drizzle.ts import { db } from "{{packages.db.name}}"; ``` Module-level, not adapter-level: source imports are shared infrastructure that doesn't vary across adapters. ## Sidecar files [#sidecar-files] Drop these next to `module.ts`: | File | What it does | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `logo.svg` | Theme-agnostic SVG, inlined onto `mod.logo`. Used by the web builder and module cards. | | `logo-light.svg` + `logo-dark.svg` | Theme pair — inlined as `mod.logo = { light, dark }`. Takes precedence over a single `logo.svg`. | | `readme.md` | Markdown contribution to the generated project's README. Renders with the same Handlebars context as templates. | The build runs [SVGO](https://svgo.dev/) on logos and prefixes every `id` so multiple module logos on the same page can't collide. First-party logos generally come from [svgl.app](https://svgl.app). ## Validating [#validating] Module authors don't run a separate validation step — the same Zod schemas the CLI uses are the source of truth: * `defineModule` (in [`packages/schema/src/module.ts`](https://github.com/jakejarvis/stanza/blob/main/packages/schema/src/module.ts)) throws at runtime on illegal field combinations. * `ModuleSchema.parse` runs on every HTTP fetch and rejects malformed manifests with structured errors. * `vp test` runs the whole repo's schema + resolver tests; add a fixture under `registry/modules//` and the existing suite picks it up. * `vp check` does a type-aware lint pass that catches most authoring mistakes statically. For a third-party registry, the same `ModuleSchema` ships from `@withstanza/schema` (published to npm). The simplest test loop is to build your registry and point the consumer's `STANZA_REGISTRY` at the built main file (`STANZA_REGISTRY=./out/index.json stanza search`) and watch for parse errors. ## Building a registry [#building-a-registry] The build script ([`scripts/compile-registry.ts`](https://github.com/jakejarvis/stanza/blob/main/scripts/compile-registry.ts)) scans `registry/modules/*` and writes the main file plus one JSON per module directly under the output directory: ``` / ├── index.json # main file: categories + per-module summaries (each with a `path`) └── modules/ ├── -.json # one file per module, templates inlined └── … ``` Each per-module JSON is self-contained: template bodies, deps, env vars, codemod invocations, the optimized logo SVG, and the rendered readme are all in one document. The CLI never makes follow-up requests for module assets. To run the build: ```sh # Default output: /dist (writes flat dist/index.json + dist/-.json) jiti scripts/compile-registry.ts # Or point it elsewhere (this is what the web app's compile-registry task does): jiti scripts/compile-registry.ts apps/web/.registry ``` The script is a thin wrapper over `@withstanza/schema` (`CATEGORIES` + the contract types) and SVGO — for a third-party registry, fork it (or copy it) against your own `registry/modules/` tree. `compileRegistry({ outDir })` is also exported for in-process callers. The output shape is identical. ## Hosting a registry [#hosting-a-registry] A registry is plain static JSON — host it anywhere that serves files: a CDN, Vercel/Netlify/Cloudflare Pages, GitHub Pages, S3 + CloudFront, your own Nginx, etc. No runtime, no DB, no SSR. A registry is addressed by the **full URL to its main JSON file** (the index). Every module entry in that file carries a relative `path`, which the CLI resolves against the main file's URL — so the directory layout is a convention, not a requirement: ``` https://reg.acme.example/ ├── index.json # the main file — each module entry carries a `path` └── modules/ ├── testing-cosmos.json └── auth-something.json ``` Declared as the full URL to that main file: ```jsonc { "registries": { "@acme": "https://reg.acme.example/index.json" } } ``` The build names the main file `index.json`, but the loader resolves whatever URL you give it — the filename isn't special. For auth headers or query params, use the object form: ```jsonc { "registries": { "@acme": { "url": "https://reg.acme.example/index.json", "headers": { "Authorization": "Bearer ${ACME_TOKEN}" }, "params": { "version": "stable" }, }, }, } ``` `url` is the full URL to the main file — there's no filename convention and no `{category}`/`{id}` templating, since each module's location comes from its `path` in the main file. Headers and params support `${ENV_VAR}` expansion against `process.env`; a header whose template references an unset variable is silently dropped, while an unset variable in `params` is a hard error. ## Publishing under your own namespace [#publishing-under-your-own-namespace] There's no central registry to list with. Pick an `@scope` (the naming rule is the same as npm scopes — `/^@[a-zA-Z0-9][a-zA-Z0-9-_]*[a-zA-Z0-9]$/`), publish your `registry/` to a URL, and document how to consume it: ```jsonc // users add to their stanza.json { "registries": { "@acme": "https://reg.acme.example/index.json", }, } ``` ```sh # then install with the @scope/id syntax stanza add testing @acme/cosmos ``` Unknown namespaces fail fast — there's no implicit fallback to `@stanza`, so a typo can't leak a private module name to the public registry. The chosen namespace is recorded on the manifest module record (`stanza remove` knows where to refetch from). `@stanza` is reserved. Declaring it under `registries` is a schema error; to override its source (for a fully-mirrored self-hosted registry or to pin a fixture in CI), set the `STANZA_REGISTRY` env var to the full URL or filesystem path of a main JSON file. ## What third-party modules can do [#what-third-party-modules-can-do] The full module surface, with one exception: * ✓ Ship any combination of templates, deps (`dependencies`, `devDependencies`), env vars, and scripts. * ✓ Declare `peers` and ship multiple adapters keyed to the host's stack. * ✓ Invoke any codemod from Stanza's built-in catalog with custom args. * ✓ Ship logos, READMEs, and the `app:` overlay. * ✓ Declare `consumesPackages` to import from other internal packages. * ✗ Ship new codemod code. The catalog is the only execution surface — adapters that reference an unknown codemod id are rejected at install time. A third-party `payments` module that needs to wrap the root layout uses `wrap-root-layout` with its own provider name; one that needs to extend a barrel uses `re-export`; one that needs to register a Vite plugin uses `add-plugin-to-call`. If a real need surfaces a catalog gap, the right fix is to land the new generic codemod upstream — not to grant arbitrary code execution to fetched JSON. ## Worked example: `@acme/cosmos` [#worked-example-acmecosmos] A minimal third-party module from scratch. **1. Author `module.ts`** at `registry/modules/testing-cosmos/module.ts` in your registry repo: ```ts import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "cosmos", category: "testing", label: "Cosmos", description: "Visual component sandbox.", version: "1.0.0", homepage: "https://reactcosmos.org", devDependencies: { "react-cosmos": "^7.0.0" }, scripts: { cosmos: "cosmos" }, adapters: [{ key: "default", match: {} }], }); ``` **2. Build the registry** with the script above. Output: ``` out/ ├── index.json └── modules/ └── testing-cosmos.json ``` **3. Host it.** Push `out/` to wherever you serve static files. Say the main file ends up at `https://reg.acme.example/index.json`. **4. Tell users how to install.** Their `stanza.json`: ```jsonc { "registries": { "@acme": "https://reg.acme.example/index.json", }, } ``` Then: ```sh stanza add testing @acme/cosmos ``` The manifest records the install with `namespace: "@acme"` so `stanza remove` refetches from the right registry on rollback. ## Conventions [#conventions] A few things experienced authors follow to keep registries consistent: * **Hoist shared install fields** to the module level. Better Auth's `dependencies: { "better-auth": "^1.6.11" }` and its env vars are declared once; only the per-(framework, orm) templates and codemods sit in the adapter blocks. * **Don't ship a `package.json` template.** Let the runner merge deps into the host's `package.json` (for app/repo homes) or bootstrap the slot package itself (for package homes). Hand-rolled `package.json` templates collide with `addPackageDependency`. * **Multi-cardinality categories need disjoint regions.** Vitest claims `scripts.test`, Playwright claims `scripts.test:e2e` — both can install into the same `package.json` because their region claims don't overlap. * **Bump `version` on schema-affecting changes** (new templates, dep upgrades). The upcoming `swap`/`update` verbs read it. * **Codemod catalog ids are part of the public contract.** Renaming a builtin codemod id breaks every third-party manifest that references it — treat them like npm package names. # Agents (/docs/agents) > Drive Stanza from Claude Code, Codex, Cursor, or any other coding agent. Stanza is built to be agent-friendly: every verb has a non-interactive form, the manifest is declarative state, and `--dry-run` previews any mutation before it runs. An agent that follows a handful of rules can scaffold and modify a Stanza project as reliably as a human at a prompt. ## SKILL.md [#skillmd] For Claude Code, Codex, and other agent runtimes that load skills, Stanza ships a self-contained skill bundle. Install it with the [Skills CLI](https://skills.sh): ```sh npx skills add https://github.com/jakejarvis/stanza --skill stanza-cli ``` That drops the skill into your agent's skill directory; the agent picks it up on the next prompt that matches its triggers (running `stanza-cli`, creating a project, adding modules, etc.). The skill is designed to work without access to this source repo — it encodes the published CLI contract alone. The source lives at [`skills/stanza-cli/`](https://github.com/jakejarvis/stanza/tree/main/skills/stanza-cli) if you'd rather vendor it directly. ## Agent guidance [#agent-guidance] A few rules on top of the [CLI surface](/docs/cli) keep agent runs predictable: * **Discover before assuming.** The registry evolves — confirm any module id with [`npx -y stanza-cli@latest search`](/docs/cli#search) before passing it to `add`. Use the printed `category/id` value, not the display label. * **Prefer `npx` for automation.** `npx -y stanza-cli@latest ` avoids the package-manager argument-forwarding ambiguity that `pnpm create` / `npm init` / `bun create` introduce. * **Preview with [`--dry-run`](/docs/cli#global-flags)** before mutating commands when you want to show the user what's about to happen. * **Respect the [dirty-worktree refusal](/docs/cli#global-flags).** Commit or stash first. Only reach for `--dangerously-allow-dirty` with the user's explicit approval. * **Pass [`--app=`](/docs/cli#add)** to `add`/`remove` in multi-app projects. Single-app projects auto-target, but if `stanza.json`'s `apps` array has more than one entry, scripts should pass the flag explicitly rather than relying on cwd inference. * **`stanza.json` is generated state.** Don't hand-edit it; use the verbs that maintain it (`add`, `remove`). * **Generated files are user-owned source.** Once Stanza writes a template, edit it like any other file in the repo. * **Verify with [`stanza doctor`](/docs/cli#doctor).** It reports drift between `stanza.json` and the filesystem and exits non-zero, so a script can gate on it after a run or after hand-edits. ## Common errors [#common-errors] | Error | What it means and what to do | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `Module not found` | The id doesn't exist in the registry. Run `stanza search` and use the printed id. | | `missing-peer` / `incompatible-peer` | The selected module needs a peer category filled first (e.g. Better Auth needs an `orm`). Add the peer, then re-run. | | `no-adapter` | None of the module's adapters match the current stack. Try a different module in the same category. | | `No stanza.json found` | You're not in a Stanza project. Run from the project root or a child directory containing a parent `stanza.json`. | | Dirty worktree refusal | Commit or stash, or ask the user before using `--dangerously-allow-dirty`. | ## Putting it together [#putting-it-together] A minimal agent script that scaffolds a project, adds auth, and verifies nothing else changed: ```sh # 1. Confirm the modules exist. npx -y stanza-cli@latest search # 2. Scaffold. npx -y stanza-cli@latest init my-app --yes \ --framework=tanstack-start \ --ui=tailwind \ --db=postgres \ --orm=drizzle \ --pm=pnpm cd my-app pnpm install # 3. Preview, then add auth. npx -y stanza-cli@latest add auth better-auth --dry-run npx -y stanza-cli@latest add auth better-auth pnpm install # 4. Inspect + verify. npx -y stanza-cli@latest list npx -y stanza-cli@latest doctor # confirm the project matches its manifest ``` When reporting results back to the user, include the exact command run, whether it wrote files, and any follow-up install needed.