feat: add api category, improve stanza add failure reporting, and ship npm READMEs

- Add `api` to the category taxonomy (`cardinality: "one"`, `home: package` → `packages/api/`) for a future typed RPC layer; no first-party modules yet — wizard skips it and the web builder hides it while empty
- Replace the raw `throw err` in `cmdAdd` with `reportApplyFailure`: a `RegionConflictError` states plainly that nothing was written; a mid-apply failure points at `stanza remove …` to sweep tracked files and (when a clean baseline was enforced) `git restore . && git clean -fd` to reset the worktree
- Add npm READMEs to `apps/cli` and `packages/create-stanza` covering quick-start, commands, safety flags, registries, and telemetry
- Unwrap hard-wrapped lines in `category-taxonomy.md` changeset and update AGENTS.md to reference the `compile-registry` vp task instead of the old `prebuild` script
This commit is contained in:
2026-05-30 11:35:14 -04:00
parent 4fbb060226
commit 7349b53b82
10 changed files with 263 additions and 23 deletions
@@ -0,0 +1,10 @@
---
"stanza-cli": minor
"create-stanza": patch
---
Add the **`api`** category to the taxonomy (single-choice, `home: package``packages/api/`), matching the documented roadmap for a typed RPC layer between the framework and your services. No first-party modules ship for it yet, so it's inert until a `trpc`/`orpc` module lands — the wizard skips it and the web builder hides it while empty. This is purely additive: `stanza.json`'s schema is unchanged (the `modules` record already keys on arbitrary categories).
`stanza add` now reports a mid-apply failure with recovery guidance instead of a raw stack trace: a region conflict states plainly that nothing was written, while a failure after files were touched points at `stanza remove …` (to sweep what Stanza tracked) and `git restore . && git clean -fd` (to reset a clean worktree) — the latter only suggested when a clean baseline was enforced.
Both published packages (`stanza-cli`, `create-stanza`) now ship npm READMEs.
+2 -13
View File
@@ -2,17 +2,6 @@
"stanza-cli": minor
---
Unify the module taxonomy into one `Category` concept. The old slot/add-on split
conflated two orthogonal properties; categories now carry them explicitly:
`cardinality` (`"one"` single-choice / `"many"` coexisting) and `home`
(`app` / `repo` / `package`, a tagged union replacing the `packageDir` +
`repoScoped` pair). A `Module` is no longer a discriminated union — it carries a
single `category` field.
Unify the module taxonomy into one `Category` concept. The old slot/add-on split conflated two orthogonal properties; categories now carry them explicitly: `cardinality` (`"one"` single-choice / `"many"` coexisting) and `home` (`app` / `repo` / `package`, a tagged union replacing the `packageDir` + `repoScoped` pair). A `Module` is no longer a discriminated union — it carries a single `category` field.
The manifest unifies to one `modules` record keyed by category, holding arrays
(`cardinality: "one"` categories are kept to ≤ 1 record at install time). This
bumps `stanza.json`'s version to `0.2` — a clean break with no migration
(Stanza is pre-1.0 and unpublished). Constraint-bearing is now emergent: the
resolver treats only `cardinality: "one"` categories as peers, so a multi-choice
category like `testing` can never accidentally become a peer. Install routing
lives in one `categoryHome` lookup shared by the CLI runner and the web preview.
The manifest unifies to one `modules` record keyed by category, holding arrays (`cardinality: "one"` categories are kept to ≤ 1 record at install time). This bumps `stanza.json`'s version to `0.2` — a clean break with no migration (Stanza is pre-1.0 and unpublished). Constraint-bearing is now emergent: the resolver treats only `cardinality: "one"` categories as peers, so a multi-choice category like `testing` can never accidentally become a peer. Install routing lives in one `categoryHome` lookup shared by the CLI runner and the web preview.
+2 -2
View File
@@ -20,7 +20,7 @@ The repo runs on the **Vite+ toolchain** (`vp`). All config lives in the root [`
- `vp check` — format + lint + type-check (oxfmt + oxlint + type-aware `tsgolint`). **Use this for validation loops.** `--fix` autofixes
- `vp test` — Vitest 4 via `vite-plus/test`; project selection via `test.projects` in root config
- `vp run -r build` — build everything (`vp pack` per package, wraps tsdown)
- `vp run @stanza/web#dev` — TanStack Start dev server. `prebuild` runs [`prepare-registry.sh`](apps/web/scripts/prepare-registry.sh) → `jiti packages/registry/src/build.ts apps/web/public`, emitting `apps/web/public/registry/{index,modules/*}.json` + `apps/web/public/schema.json`
- `vp run @stanza/web#dev` — TanStack Start dev server. Its `dev`/`build` scripts first run the `compile-registry` vp task (defined under `run.tasks` in [`apps/web/vite.config.ts`](apps/web/vite.config.ts)) → `jiti packages/registry/src/build.ts apps/web/public`, emitting `apps/web/public/registry/{index,modules/*}.json` + `apps/web/public/schema.json`
- `src/routeTree.gen.ts` is generated by `vp run @stanza/web#build` — run it before the first `vp check` if missing
- E2E smoke: seed `$TMPDIR/x` with `stanza.json` + `apps/web/package.json`, then `tsx apps/cli/src/bin.ts add <category> <module>`
- `pnpm changeset` — drop a markdown file after a substantive PR; the release workflow handles versioning + publish. Only `stanza-cli` + `create-stanza` ship to npm
@@ -52,7 +52,7 @@ Use `agent-browser` for web automation (`agent-browser --help`); prefer it over
- **Region ownership** in `stanza.json` is the source of truth for `stanza remove`. Two modules claiming the same region throws `RegionConflictError`. Regions key on `module.id`, so disjoint dot-paths (vitest `scripts.test` vs playwright `scripts.test:e2e`) coexist. Package bootstrap files (`package.json`, `tsconfig.json`, workspace deps) are system-owned, not tracked in regions — `stanza remove`'s sweep over `PACKAGE_DIRS` cleans them when no claims remain. _Regions are not yet sufficient for `swap`_ — that verb needs adapter-region remapping; design pending
- **Reserved manifest fields**: `modules[category][].version` and `regions` are written today but only fully consumed by upcoming `swap`/`update`. Don't drop them
- **Web previews are SSR.** Shiki runs in [`highlighter.server.ts`](apps/web/src/server/highlighter.server.ts); the client-safe `Preview` type lives in [`highlighter.ts`](apps/web/src/server/highlighter.ts). Never import `shiki` from a client component. After `vp run @stanza/web#build`, grep `.output/public/assets/*.js` for the runtime (`createHighlighter`/`codeToHtml`/`loadWasm`) — must be absent. A bare `grep shiki` yields false positives because MDX docs ship statically-rendered code blocks with `class="shiki"` + CSS vars (fine)
- **Web hosts the canonical registry.** `prebuild` writes the registry into `apps/web/public/registry/`; the deployed site serves it at the published CLI's default `DEFAULT_REGISTRY_URL`. `STANZA_REGISTRY` (URL or FS path) overrides for self-hosters / CI. `public/registry/` is also a Nitro `serverAssets` dir (vite.config.ts) — SSR reads via `useStorage("assets:registry")` (see [`registry-base.server.ts`](apps/web/src/server/registry-base.server.ts)), which is why Vercel works where `public/` is absent from the function fs
- **Web hosts the canonical registry.** The `compile-registry` task writes the registry into `apps/web/public/registry/`; the deployed site serves it at the published CLI's default `DEFAULT_REGISTRY_URL`. `STANZA_REGISTRY` (URL or FS path) overrides for self-hosters / CI. `public/registry/` is also a Nitro `serverAssets` dir (vite.config.ts) — SSR reads via `useStorage("assets:registry")` (see [`registry-base.server.ts`](apps/web/src/server/registry-base.server.ts)), which is why Vercel works where `public/` is absent from the function fs
- **Third-party registries**: namespaces declared under `stanza.json#registries`. Modules addressed as `<category> @<ns>/<id>`. `telemetry.captureModule` redacts non-`@stanza` ids to `<redacted>`; stderr still prints full ids (CI log forwarders will see them)
## Module authoring
+79
View File
@@ -0,0 +1,79 @@
# stanza-cli
Modular monorepo template CLI — shadcn for full-stack TypeScript stacks.
Pick a framework, ORM, database, auth provider, UI, and more — get a clean monorepo with idiomatic code **vendored into your repo**. There's no runtime to install: generated files land verbatim and are yours to edit. Unlike a one-shot scaffolder, `stanza add` keeps working after `init`, so you can layer modules into an existing project at any time.
> [!WARNING]
> **Major work in progress!** See the [module registry](https://stanza.tools/docs/registry) for what's available today and what's on the roadmap.
## Quick start
```sh
npm create stanza my-app # → runs the init wizard (see create-stanza)
cd my-app
npm install
npm run dev
```
Or drive the CLI directly:
```sh
npx stanza-cli init my-app # interactive wizard
npx stanza-cli add auth better-auth # add a module to an existing project
```
Non-interactive (CI / agents) — pass each category explicitly:
```sh
npx stanza-cli init my-app --yes \
--framework=next --ui=tailwind \
--db=postgres --orm=drizzle \
--auth=better-auth --testing=vitest,playwright --pm=pnpm
```
## Commands
| Command | What it does |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stanza init [name]` | Scaffold a new monorepo via the wizard (or `--yes` + `--<category>=<id>` flags). |
| `stanza add <category> [@<ns>/]<module>` | Add one module to an existing project; picks the right adapter for your stack and wires deps, env, scripts, and templates into the correct workspace package. `--app=<id>` targets a specific app. |
| `stanza remove <category> [[@<ns>/]<id>]` | Remove a module and clean up its files, deps, and codemods. The id is optional for single-choice categories. |
| `stanza list` | Print installed modules grouped by category. |
| `stanza search [query]` | List registry modules and their `category/id` pairs. |
Run `add` / `remove` / `list` from the project root or any child directory under a `stanza.json`.
Every module fills exactly one **category**. Single-choice categories (`framework`, `ui`, `db`, `orm`, `auth`, `payments`, `email`, `ai`, `tooling`, `monorepo`) hold one module; multi-choice (`testing`, `deploy`) coexist. `auth`/`db`/`orm` and friends install into their own internal workspace packages (`packages/auth/`, `packages/db/`, …); your apps consume them via `workspace:*`, so swapping a provider replaces a package's contents without touching your app imports.
## Safety
- `--dry-run` previews any mutating command without writing.
- Mutating commands refuse to run in a dirty git worktree so Stanza's edits stay in their own reviewable diff. Override with `--dangerously-allow-dirty`.
- Declared `^`/`~` dep ranges are bumped to the latest satisfying npm version (modifier kept); `workspace:*` and other ranges are written as-is. `STANZA_NO_NPM_LOOKUP=1` skips lookups; `STANZA_NPM_REGISTRY=<url>` overrides the npm registry.
## Registries
Modules ship from the first-party `@stanza` namespace by default. To pull from another publisher, declare it in `stanza.json` and address modules as `@<scope>/<id>`:
```jsonc
{ "registries": { "@acme": "https://reg.acme.dev" } }
```
```sh
stanza add testing @acme/cosmos
```
`STANZA_REGISTRY=<url-or-path>` overrides the `@stanza` namespace's source (self-hosted mirror, air-gapped install, CI fixture).
## Telemetry
The CLI sends anonymous usage events (command run, modules installed/removed — no PII, no persisted identifier; third-party module ids are redacted). Disable per-run with `--no-telemetry`, or persistently with `STANZA_TELEMETRY=0` / `DO_NOT_TRACK=1`. Auto-skipped in CI.
## Docs
Full guides, the CLI reference, and the module roadmap live at **[stanza.tools/docs](https://stanza.tools/docs)**. Assemble a stack visually at **[stanza.tools](https://stanza.tools)**.
## License
[MIT](https://github.com/jakejarvis/stanza/blob/main/LICENSE)
+64 -2
View File
@@ -15,7 +15,7 @@ import {
import { defineCommand } from "citty";
import pc from "picocolors";
import { applyModule } from "../lib/codemod-runner";
import { applyModule, RegionConflictError } from "../lib/codemod-runner";
import { ensureCleanWorktree } from "../lib/git";
import { findProjectRoot, readManifest, writeManifest } from "../lib/manifest";
import { regenerateReadmeIfUnmodified } from "../lib/readme";
@@ -212,7 +212,20 @@ export async function cmdAdd(args: CliArgs): Promise<void> {
});
} catch (err) {
spinner.stop(`${mod.label} ${pc.red("failed")}`);
throw err;
reportApplyFailure({
category,
moduleId,
namespace,
dryRun,
// The dirty-worktree guard ran (unless overridden), so the repo had a
// clean baseline a `git restore`/`clean` can safely reset to. With
// `--dangerously-allow-dirty` that's untrue — a reset would also discard
// the user's own pending work, so we don't suggest it.
cleanBaseline: !dryRun && !args["dangerously-allow-dirty"],
err,
});
process.exitCode = 1;
return;
}
// Always counted in the aggregate install total; the `namespace` property
@@ -305,6 +318,55 @@ async function pickTargetApp(args: {
return null;
}
/**
* Surface a mid-apply failure with recovery guidance instead of a raw stack.
* Templates/deps are flushed before codemods run, so a codemod throw can leave
* a partial change on disk; the manifest record + region claims are persisted
* incrementally, so `stanza remove` can sweep what Stanza tracked. When the
* worktree had a clean baseline, a git reset is the fuller escape hatch.
*/
function reportApplyFailure(args: {
category: string;
moduleId: string;
namespace: string | undefined;
dryRun: boolean;
cleanBaseline: boolean;
err: unknown;
}): void {
const { category, moduleId, namespace, dryRun, cleanBaseline, err } = args;
const detail = err instanceof Error ? err.message : String(err);
const spec = `${category} ${namespace ? `${namespace}/` : ""}${moduleId}`;
// A region conflict throws before any disk write (claims are staged in
// memory first), so nothing was changed — retrying won't help, the stacks
// are incompatible as installed.
if (err instanceof RegionConflictError) {
p.log.error(
`Couldn't add ${spec}: ${detail}.\n` +
`Another installed module already owns that region — remove the conflicting module first, or pick a different one. No files were changed.`,
);
return;
}
if (dryRun) {
p.log.error(`Dry-run for ${spec} failed: ${detail}`);
return;
}
const lines = [
`Failed while adding ${spec}: ${detail}`,
"",
"A partial change may have been written. To recover:",
` ${pc.cyan(`stanza remove ${category} ${moduleId}`)} ${pc.dim("# sweep what Stanza tracked")}`,
];
if (cleanBaseline) {
lines.push(
` ${pc.cyan("git restore . && git clean -fd")} ${pc.dim("# or reset the worktree to its last-clean state")}`,
);
}
p.log.error(lines.join("\n"));
}
function describeResolveError(kind: string): string {
switch (kind) {
case "missing-peer":
+53 -4
View File
@@ -138,6 +138,22 @@ describe("cmdAdd", () => {
await cmdAdd(args({ slot: "nonsense", moduleId: "x" }));
expect(process.exitCode).toBe(1);
});
it("rejects an unknown --app target", async () => {
await cmdAdd(args({ slot: "db", moduleId: "postgres", app: "nope" }));
expect(process.exitCode).toBe(1);
const manifest = JSON.parse(fs.readFileSync("stanza.json", "utf8"));
expect(manifest.modules.db).toBeUndefined();
});
it("surfaces a field-path error for a malformed stanza.json", async () => {
const m = JSON.parse(fs.readFileSync("stanza.json", "utf8"));
m.apps = "not-an-array"; // schema expects an array of app specs
fs.writeFileSync("stanza.json", JSON.stringify(m, null, 2));
await expect(cmdAdd(args({ slot: "db", moduleId: "postgres" }))).rejects.toThrow(
/Malformed stanza\.json[\s\S]*apps/,
);
});
});
describe("cmdRemove", () => {
@@ -619,14 +635,47 @@ describe("third-party registries", () => {
});
// The runner validates codemod ids against the first-party catalog up
// front, so this throws before any files change.
await expect(cmdAdd(args({ slot: "testing", moduleId: "@fixture/cosmos" }))).rejects.toThrow(
/not-a-real-codemod/,
);
// front (before any files change); cmdAdd surfaces it as a handled failure
// with recovery guidance rather than a raw stack.
await cmdAdd(args({ slot: "testing", moduleId: "@fixture/cosmos" }));
expect(process.exitCode).toBe(1);
const manifest = JSON.parse(fs.readFileSync("stanza.json", "utf8"));
expect(manifest.modules.testing).toBeUndefined();
});
it("surfaces a clear error when the registry has no such module (non-200)", async () => {
await cmdInit(args({ name: "app", yes: true, framework: "next" }));
process.chdir(path.join(tmp, "app"));
writeStanza(process.cwd(), { "@fixture": baseUrl });
// fixture.modules is empty → the stub server 404s for testing-ghost.
await cmdAdd(args({ slot: "testing", moduleId: "@fixture/ghost" }));
expect(process.exitCode).toBe(1);
const manifest = JSON.parse(fs.readFileSync("stanza.json", "utf8"));
expect(manifest.modules.testing).toBeUndefined();
});
it("surfaces a region conflict cleanly and writes nothing", async () => {
await cmdInit(args({ name: "app", yes: true, framework: "next" }));
process.chdir(path.join(tmp, "app"));
writeStanza(process.cwd(), { "@fixture": baseUrl });
// First-party vitest claims `scripts.test` on the app's package.json.
await cmdAdd(args({ slot: "testing", moduleId: "vitest" }));
expect(process.exitCode).toBeFalsy();
const before = fs.readFileSync("apps/web/package.json", "utf8");
// A fixture add-on that also wants `scripts.test` → claim conflict. Region
// claims are staged in memory before any flush, so the throw lands before a
// single byte is written.
fixture.modules["testing-cosmos"] = cosmosModule({ scripts: { test: "cosmos run" } });
process.exitCode = undefined;
await cmdAdd(args({ slot: "testing", moduleId: "@fixture/cosmos" }));
expect(process.exitCode).toBe(1);
expect(fs.readFileSync("apps/web/package.json", "utf8")).toBe(before);
const manifest = JSON.parse(fs.readFileSync("stanza.json", "utf8"));
expect(manifest.modules.testing.map((r: { id: string }) => r.id)).toEqual(["vitest"]);
});
});
function writeStanza(projectRoot: string, registries: Record<string, unknown>) {
+2 -1
View File
@@ -199,7 +199,8 @@ export async function cmdInit(args: CliArgs): Promise<void> {
if (!dryRun) {
p.log.error(
`Init failed after ${applied.length} module(s): ${applied.join(", ") || "(none)"}.\n` +
`Project left at ${projectRoot} for inspection. Remove it manually to retry.`,
`Project left at ${projectRoot} for inspection. ` +
`Remove it with \`rm -rf ${result.name}\` and re-run to retry.`,
);
}
throw err;
+27 -1
View File
@@ -175,7 +175,7 @@ adapters: [
],
```
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 (`wrap-root-layout` handles both Clerk and any future provider-style auth/state library).
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:
@@ -184,6 +184,32 @@ The catalog lives at [`packages/codemods/src/builtins/`](https://github.com/jake
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
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 `<property>: [...]` 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 (`<ThemeToggle />` into a starter `<main>`). 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` `<Outlet />` — 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 `<html>` 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:<dir>"` (`packages/<dir>/`). `add-package-dep` and `set-tsconfig-paths` use it to pick the target file; `re-export` accepts only `"app"` / `"package:<dir>"`; `append-to-file` splits it into `scope: "repo" | "app"` plus a separate `base: "package:<dir>"`; `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:<anchor>` / `after:<anchor>` 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
`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`.
+17
View File
@@ -0,0 +1,17 @@
# create-stanza
The canonical entry point to the [Stanza](https://stanza.tools) wizard — `npm create stanza` (and friends) forwards straight to [`stanza-cli`](https://www.npmjs.com/package/stanza-cli)'s `init` command.
```sh
npm create stanza my-app
pnpm create stanza my-app
bun create stanza my-app
```
Pick a framework, ORM, database, auth provider, and UI — get a clean full-stack TypeScript monorepo with idiomatic code vendored into your repo. Pass `--yes` with `--<category>=<id>` flags to skip the prompts in CI.
Everything beyond launching the wizard lives in `stanza-cli`. Full docs at **[stanza.tools/docs](https://stanza.tools/docs)**.
## License
[MIT](https://github.com/jakejarvis/stanza/blob/main/LICENSE)
+7
View File
@@ -54,6 +54,13 @@ export const CATEGORIES = [
cardinality: "one",
home: { kind: "package", dir: "ui" },
},
{
id: "api",
label: "API",
description: "Typed RPC layer between the framework and your services.",
cardinality: "one",
home: { kind: "package", dir: "api" },
},
{
id: "db",
label: "Database",