63 Commits
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bd0a4cda1e chore: release
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-13 15:14:19 -04:00
jake da603bd1f8 chore: bump deps 2026-06-13 15:08:44 -04:00
jakeandGitHub 8d4a4ec415 feat: interactive module picker for stanza add and stanza remove (#33) 2026-06-13 14:49:08 -04:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
eee2e80b85 chore: release
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-13 12:11:26 -04:00
jakeandGitHub dc34c92c21 feat: add --dry-run plan output and post-apply summary to stanza add (#28) 2026-06-13 12:06:25 -04:00
jake 6ada5c0a88 fix: correct batch of CLI correctness bugs found in critical-bug sweep
- `stanza init --dry-run` no longer creates the project directory, monorepo shell, or `stanza.json`; the dry-run manifest is built with `emptyManifest` and package.json existence checks are skipped throughout `applyModule`
- Direct-fs codemods (`append-to-file`, `add-package-dep`, `set-tsconfig-paths`) now claim their region before writing so a failed `add` rolls back to true pre-apply bytes and `RegionConflictError` fires before any disk change
- `stanza remove` reads the `consumesPackages` snapshot persisted on each installed record (falling back to a live registry fetch only for legacy records), so package-dir protection holds offline and after upstream renames
- `stanza remove` only applies the legacy bare-id owner fallback when no sibling install of the same module remains, preventing one app's removal from sweeping another app's files on pre-composite-owner manifests
- `stanza doctor` no longer reports false drift for package-home modules installed with an `--app` restriction; `expectedConsumerApps` now intersects targeted apps from the manifest records
- `wrap-root-layout` resolves the framework per dispatched app, fixing multi-app projects with differing frameworks
- The codemod render context now carries `packageManager` so `{{pm}}`/`{{run …}}` templates render bun/npm/pnpm correctly instead of always defaulting to pnpm
2026-06-09 19:04:38 -04:00
jake 4df8d80c79 chore: bump deps and migrate .inputValidator.validator
- Update `@types/node`, `semver`, TanStack Router/Start, `posthog-node`, `shadcn`, `fumadocs-mdx`, and a handful of other minor dep bumps across the monorepo
- Upgrade pnpm to 11.5.2
- Replace deprecated `.inputValidator(…)` calls with `.validator(…)` on all server functions
2026-06-09 12:57:22 -04:00
jake e776dc74f6 fix: harden CLI against path traversal, env injection, and cleartext registry fetches
- **`app.dir` and region key path traversal.** `appSpecSchema` now validates `dir` with `safeRelativePath` at parse time (rejecting `..`, absolute paths, and null bytes); the `regions` record key receives the same treatment. `applyModule` and `cmdRemove` re-check every resolved write/delete destination with the new `assertWithinRoot` helper, which walks the full symlink chain so a planted link can't redirect file operations outside the project root even if the lexical check passes.
- **Env var line injection in `.env.example`.** `envVarSchema` now validates `name` against `^[A-Za-z_][A-Za-z0-9_]*$` and rejects control characters in `example` and `description`; the `appendEnvVar` helper applies the same check as defense-in-depth so a crafted third-party module can't smuggle extra `KEY=value` lines into the generated file.
- **Cleartext registry/npm endpoint rejection.** Remote `http://` URLs for `STANZA_REGISTRY`, `registries[*].url`, and `STANZA_NPM_REGISTRY` are now refused (`STANZA_REGISTRY`/`STANZA_NPM_REGISTRY` hard-error; third-party `http://` entries warn and skip so the rest of the CLI keeps working). `file://`, bare filesystem paths, and loopback hosts stay allowed for local-dev and CI-fixture workflows. Set `STANZA_ALLOW_INSECURE_REGISTRY=1` to opt into a trusted internal `http://` mirror with a one-time stderr warning.
2026-06-09 12:50:43 -04:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
22d046171c chore: release
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 15:18:55 -04:00
jakeandGitHub 04a196e4e9 refactor: serve registry and manifest schema from Vercel Blob as canonical origin (#18) 2026-06-04 15:08:36 -04:00
jakeandGitHub ea2d8c45bd refactor: extract @withstanza/schema and @withstanza/utils packages (#16) 2026-05-30 21:47:16 -04:00
github-actions[bot]andjake 06bd24838c chore: release 2026-05-30 14:05:31 -04:00
jake ea7fe48d00 chore: tighten changeset config, add stanza-cli bin alias, and sync docs
- Set `disableThanks`, `prettier: false`, `privatePackages: { version: false, tag: false }`, and `updateInternalDependents: "always"` in `.changeset/config.json`; simplify the release workflow by dropping `NPM_TOKEN` and `NPM_CONFIG_PROVENANCE` from the CI env and switching to `vp run version`
- Add `stanza-cli` as a second bin entry in `publishConfig` alongside `stanza` so the package is callable by either name; reset the CLI package version to `0.0.0` so changesets drive versioning from a clean baseline
- Update docs throughout to reflect the `doctor` command surface (CLI reference, agents guide, telemetry list), the main-file registry URL convention (`registries` entries now point at the full `index.json` URL, not a directory), and the `STANZA_REGISTRY` env var description
2026-05-30 13:59:46 -04:00
jake e89fb63c31 feat: unify registry loading behind a main file, add apply rollback, and add stanza doctor
- **Registry main file.** The CLI now addresses any registry (first-party, third-party, or `STANZA_REGISTRY` override) by the full URL/path to its JSON index file. The `modules/<category>-<id>.json` convention, the in-repo source-tree loader, the directory-based auto-detect, and the inline-template disk fallback are all removed — one loader, no heuristics. `STANZA_REGISTRY` must be the full path/URL to the main file (not a directory); `registries` entries now carry `url` pointing at the main file instead of an `indexUrl`+template. **Breaking** (pre-release, clean break): registry index bumped to `schemaVersion: 2`; every module entry carries a `path` field resolved relative to the main file.
- **Auto-rollback.** `stanza add` (and each module in `stanza init`) now wraps its file writes in a transaction; a mid-apply failure restores touched files and `stanza.json` to their pre-apply state. `reportApplyFailure` updated to say "The change was rolled back" and demotes the `stanza remove` / `git restore` steps to a backstop note.
- **`stanza doctor`.** New read-only command that checks `stanza.json` against the filesystem (tracked files, deps, scripts, env vars, internal packages) and reports drift, exiting non-zero when found.
- Update integration tests to build the first-party registry once into a temp dir and point `STANZA_REGISTRY` at its main file; update the third-party HTTP fixture server to serve `/index.json` as the main file alongside per-module paths.
- Update `AGENTS.md` E2E smoke instructions and `apps/cli/README.md` to reflect the new loader and `doctor` command.
2026-05-30 12:49:11 -04:00
jake 7349b53b82 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
2026-05-30 11:35:14 -04:00
jake 9237dd7f46 chore: update deps 2026-05-29 08:58:53 -04:00
jake e3f5b977ba fix: migrate JSON helpers to jsonc-parser for format-preserving in-place edits
- Replace hand-rolled `JSON.parse`/`JSON.stringify` round-trips in `json.ts` with `jsonc-parser` `modify` + `applyEdits` so comments, trailing commas, indentation, and key ordering are preserved across all `setJsonPath`, `unsetJsonPath`, `mergeJson`, and `addPackageDependency` calls
- Add `setJsonPathSegments` / `unsetJsonPathSegments` for callers whose keys contain `.` (tsconfig `paths` aliases like `"@acme/ui.foo/*"` would be mis-split by the dot-path API); export both from `packages/codemods/src/index.ts`
- Refactor `set-tsconfig-paths` to use the new segment-aware helpers and drop its local `parseJsonc` + `applyEdits` usage — the builtin no longer performs raw `fs.readFileSync`/`writeFileSync` itself
- Add `jsonc-parser` to `apps/cli/package.json` (was already a transitive dep; now explicit)
- Add `"format preservation (JSONC)"` test suite covering comment/trailing-comma survival, key-ordering stability under `addPackageDependency`, dotted-key segment addressing, and no-op behavior when a parent key is absent
2026-05-26 22:34:18 -04:00
jake cf233f8275 fix: harden remove sweep safety, snapshot codemods on records, and fix package-home cardinality scope
- Refuse to sweep `packages/<dir>/` in two new cases: (1) stray non-Stanza files exist under the dir (user-authored secrets, scratch scripts, etc.), and (2) a still-installed module lists the dir in `consumesPackages` — both append to `manualCleanup` with targeted hints instead of silently destroying user work; `findStrayFiles` and `collectConsumesPackagesProtectors` (parallel `Promise.all` over all installed records) implement the checks
- Snapshot `codemods` onto each `StanzaModuleRecord` at install time; `remove` drives `revertCodemods` from that snapshot so reversal is correct even when the upstream registry has renamed the adapter or reshuffled codemod ids since install — falls back to the live registry entry only when no snapshot is present
- Fix cardinality scope in `add` for package-home and repo-home categories: pass `undefined` (not `pickedAppId`) to `selectedAll` so the per-project uniqueness check covers all apps; previously a second install targeting a different app could slip past the check only to fail on the next read
- Use composite owner keys (`<id>@<app>`) in `regionsOwnedBy` for app-home installs so two apps installing the same module don't share a region owner and accidentally release each other's claims during remove; bare `<id>` fallback covers pre-snapshot manifests and non-app homes
- Refactor `applyCodemods` / `revertCodemods` to accept `codemods`, `adapterKey`, and `consumesPackages` directly rather than full `Module` + adapter objects; add a comprehensive `codemod-runner.test.ts` covering apply, revert, dry-run, idempotency, and error paths for all built-in codemod types
- Add `packages/registry/src/safe-path.ts` with `safePath` / `isSafePath` guards and tests; use in the codemod runner to reject path-traversal write targets before any disk touch
- Add `set-tsconfig-paths` builtin codemod with tests; harden `add-array-entry-in-call`, `add-jsx-child`, `wrap-root-layout`, and `replace-import` for idempotency and edge cases
- Validate unknown `--app` flags early in `remove` with a targeted error before any registry fetch; replace the `posthog-provider` React component and `posthog.server.ts` in the web app with a unified `analytics.ts` / `api.events.ts` pattern
2026-05-26 22:25:10 -04:00
jake 1fc133c7dc fix: harden module-id validation, parallelize registry loading, and redact third-party ids in telemetry
- Add `isLikelyNamespaceTypo` and `isValidModuleId` to `@stanza/registry` and gate both `add` and `remove` on them before any registry fetch or disk write — bare `@ns` specs and path-traversal-style ids now fail fast with a targeted hint instead of an opaque 404
- Add `captureModule` to the telemetry client: mirrors `capture("cli_module", ...)` but redacts the module id when the namespace is not `@stanza` so proprietary ids never leave the user's machine; replace all `capture("cli_module", ...)` call sites in `add`, `remove`, and `init`
- Parallelize `loadRegistries` via `Promise.all` so custom-namespace index fetches happen concurrently; split `tryFetchIndex` into three discrete try/catch blocks that differentiate URL-build errors (surfaced as warnings), network failures (silent), 404s (legitimate no-index), and schema-invalid 200s (warned)
- Re-stamp `version` to `CURRENT_MANIFEST_VERSION` in `readManifest` so older manifests are transparently upgraded in memory and the next `writeManifest` persists the new version; export `CURRENT_MANIFEST_VERSION` and `SUPPORTED_MANIFEST_VERSIONS` from `@stanza/registry`
- Move codemod-catalog id validation before `ensureSlotPackage` in `applyModule` so an unknown codemod id can't bootstrap a slot package and leave it orphaned on a mid-flight throw; validation still runs on `--dry-run`
- Add a dedicated empty-state branch in `search` for namespaces that are configured but expose no browsable index, directing users to `stanza add <category> <ns>/<id>` instead of a generic "No modules found"
2026-05-26 21:03:53 -04:00
jake f195505a48 feat: add third-party registry support with @namespace/id module addressing
- Add `packages/registry/src/registry-config.ts` with a Zod schema for the `registries` field in `stanza.json` (string URL prefix or `{ url, indexUrl?, headers?, params? }` object), plus `parseModuleSpec` to split `@ns/id` specs and `expandEnv` to substitute `${VAR}` tokens in header values (dropping the header silently when the env var is unset)
- Replace `loadRegistry` with `loadRegistries(manifest)` in `registry-loader.ts`; the loader reads `registries` from `stanza.json`, builds a per-namespace fetch client with auth headers, and routes module loads to the correct HTTP endpoint — `@stanza` remains the implicit default namespace backed by the existing registry URL
- Update `add`, `remove`, `search`, and `list` commands to accept `[@<namespace>/]<id>` syntax; `namespace` is recorded alongside each module entry in `stanza.json` at install time so `remove` knows which registry to refetch from
- Enforce that codemods are catalog-only: third-party modules may reference first-party codemod IDs but cannot introduce new ones; catalog ID validation runs before any writes so unknown IDs are caught on `--dry-run` too
- Add a full third-party registry test suite in `commands.test.ts` using a stub `node:http` server: covers install + manifest recording, unknown-namespace rejection, remove round-trip (asserts correct namespace URL is hit), `${ENV_VAR}` header expansion, and codemod-catalog enforcement
- Export `DEFAULT_NAMESPACE` and `parseModuleSpec` from `@stanza/registry`; telemetry `cli_module` events now include a `namespace` field so first- vs. third-party installs can be distinguished in PostHog
- Add `apps/web/content/docs/authoring.mdx` covering the full module-authoring reference (schema fields, templates, adapters, codemods, third-party registry config)
2026-05-26 20:12:01 -04:00
jake 659f441343 fix: harden applyModule ordering, partial-init diagnostics, and consumesPackages context
- Add `consumesPackages` field to `Module` and thread it into `buildRenderContext` / `TemplateContext` so templates can reference sibling package names; update `manifest.ts`, `package-json.ts`, `template.ts`, and their tests
- Queue all file/package.json/env writes behind a `deferredWrites` array so `writeManifest` always persists before any disk mutation — a mid-flush crash leaves the manifest as ground truth with no orphan files it can't account for
- Move codemod-catalog ID validation before any writes so `--dry-run` catches unknown codemod IDs without touching the filesystem; codemods now run in a second phase after deferred writes flush, with a follow-up `writeManifest` after claim gathering
- Wrap `init`'s module-apply loop in try/catch; on failure, log which modules were already applied and leave the partial project on disk for inspection rather than silently exiting
2026-05-26 19:20:01 -04:00
jake 17c0026f49 feat: add {{env}} render context, {{json}} helper, and declaredEnvNames to wire turbo.json globalEnv automatically
- Add `declaredEnvNames(manifest)` to `@stanza/registry`: extracts a sorted, deduped list of env var names from `manifest.regions[".env.example"]` — the same keys the CLI records as modules apply; exported from the package index
- Add `env: string[]` to `TemplateContext` and an `envNames` option to `buildRenderContext`: sorts + dedupes the input so rendered output is stable regardless of module-apply order; defaults to `[]` when omitted
- Register a `{{json}}` Handlebars helper that `JSON.stringify`s any value (primary use: `"globalEnv": {{json env}}` in `turbo.json`, but generic enough for any template embedding a JS value into a JSON literal)
- Thread `envNames` through `codemod-runner`'s `renderContextFor` (derived from `declaredEnvNames(manifest)` so late-running repo-home modules like `monorepo-turbo` see all prior env claims) and through `synthesizeTemplates` + `synthesizeReadme` (aggregated from every resolved entry's merged install fields, matching what the CLI derives post-apply so CLI and web preview render identically)
- Update `monorepo-turbo/templates/turbo.json` to emit `"globalEnv": {{json env}}` so Turborepo task hashes track every project-wide env var; update `readme.md` to document the auto-population behavior
- Add `declaredEnvNames` tests (empty manifest, multi-key `.env.example` regions, non-env file regions ignored), `buildRenderContext` tests (sort+dedupe, omitted default), `{{json}}` helper tests (env array, empty array, missing key), and a `synthesizeTemplates` integration test verifying the aggregated env list flows into a rendered `turbo.json`
2026-05-25 20:33:46 -04:00
jake 2e392508b9 feat: add monorepo-turbo module, pm-aware workspace fan-out scripts, and monorepo cardinality fix
- Add `monorepo-turbo`: `turbo ^2.9.0` dev dep + a Handlebars-templated `turbo.json` at the repo root; module scripts (`dev`/`build`/`test`/`lint`) overwrite the pm-native fan-out defaults seeded by `rootPackageJson`; an `append-to-file` codemod adds `.turbo/` to `.gitignore` idempotently so both fresh `init` and `stanza add` projects are covered; light/dark logos inlined from svgl
- Add `pmRecursive(pm, script)` to `@stanza/registry` (and export it): produces the pm-native workspace-wide invocation (`pnpm -r run <s>`, `bun run --filter '*' <s>`, `npm run <s> --workspaces --if-present`); `rootPackageJson` now seeds all four root scripts via `pmRecursive` instead of hard-coding the pnpm form, and gains a `lint` entry that was previously missing
- Change `applyFields` script merging from guard-if-undefined to unconditional overwrite so a `monorepo` module's declared scripts can replace the seeded defaults; the same fix is applied to the `app`-overlay script path
- Fix `monorepo` category cardinality from `"many"` to `"one"` (only one task orchestrator makes sense per repo) and update its description
- Remove `.turbo/` from the hardcoded `.gitignore` written by `bootstrapShell` in `init.ts` — the module codemod now owns that entry
- Add `package-json.test.ts` coverage for `rootPackageJson` default scripts per pm and for monorepo module script overrides
2026-05-25 20:17:26 -04:00
jake 6b8a46ac71 fix: apply jsx: "react-jsx" to all slot packages, not just ui 2026-05-25 16:57:54 -04:00
jake dd806479d8 feat: add auto-generated project README with checksum-based edit detection and app install-fields overlay
- Add `synthesizeReadme(resolved, ctx)` to `@stanza/registry`: generates a Markdown README from the assembled module selection using each module's `readme` / `description` copy; exported alongside new `Resolved` / `ResolvedEntry` types that capture the module + adapter pair after resolution
- Add `apps/cli/src/lib/readme.ts` with `writeFreshReadme` (used by `init` into a guaranteed-empty dir), `regenerateReadmeIfUnmodified` (used by `add`/`remove` to skip user-edited files), and `readmeChecksum` / `userModifiedReadme` helpers that compare SHA-256 against a new `readmeChecksum` field on `StanzaManifest`; a missing stored checksum with a present file is treated as user-owned (conservative, covers legacy projects)
- Wire README generation into all three commands: `init` calls `writeFreshReadme` after all modules apply and records the checksum; `add` and `remove` call `regenerateReadmeIfUnmodified` post-mutation and warn when the file has been edited; both update `stanza.json` with the new checksum on write
- Add `app` install-fields overlay in `codemod-runner.ts`: a per-module `installFields.app` bucket routes `dependencies`, `devDependencies`, and `scripts` into each consuming app's `package.json` rather than the workspace root or `packages/ui`; use case is package-home modules whose app-scoped shims import npm packages that belong in the app (e.g. `next-themes` for `ui-shadcn-*`); claims are recorded under `app.dependencies.<name>` region keys so removal can reverse them
- Fix `remove.ts` region handling: strip the `app.` prefix before dispatching `dependencies.*` / `devDependencies.*` / `scripts.*` removals so `app`-overlay claims are reversed correctly
- Add `pmRun(pm, script)` helper to `@stanza/registry` and use it in the `init` outro so the suggested dev command respects the project's package manager (e.g. `npm run dev` vs `pnpm dev`)
- Update `ui-shadcn-base` and `ui-shadcn-radix` module definitions to use the new `app` install-fields overlay for app-scoped peer deps; expand `package-json.test.ts`, `synthesize.test.ts`, and `template.test.ts` with coverage for the new paths
2026-05-25 16:23:00 -04:00
jake 460e888b7e feat: pin packageManager field to the latest matching release at init time
- Add `resolveExactVersion(name, version)` to `npm-version.ts`: wraps `resolveRange` with an implied `^${version}` range and strips the modifier from the result so callers get a bare pinned version suitable for Corepack; falls back to the input version on any lookup failure
- Rename the private `PM_VERSION` constant to `PM_FLOOR_VERSION` (exported from `@stanza/registry`) and store bare versions (no `${pm}@` prefix) so they can be fed directly into the resolver; the `rootPackageJson` helper now accepts an optional `packageManagerVersion` override and interpolates it into the `packageManager` field, falling back to the floor when absent
- Make `bootstrapShell` async in `init.ts` and call `resolveExactVersion` before writing `package.json` so newly-generated projects are pinned to the latest compatible release rather than a hardcoded floor; the web preview's synth path continues to use the floor as-is to avoid per-render npm lookups
- Add `resolveExactVersion` tests covering the happy path, `STANZA_NO_NPM_LOOKUP` bypass, network rejection fallback, and unsatisfied range fallback
2026-05-25 15:50:22 -04:00
jake 678b5bc1af feat: add ui-shadcn-base, ui-shadcn-radix, and ui-tailwind modules; rename styling category to ui
- Rename `styling` → `ui` across the entire codebase (CATEGORIES array, docs, AGENTS.md, CLI tests, resolver, template tests, codemod runner); `ui` is `home: package` so shadcn installs into `packages/ui/` as a workspace dep rather than wiring directly into the app shell
- Add `ui-shadcn-base` (Tailwind v4 / CSS-first, no Radix peer) and `ui-shadcn-radix` (Tailwind v4 + Radix primitives) with full per-framework template sets for Next.js and TanStack Start: `components.json`, `globals.css`, `postcss.config.mjs`, `theme-provider.tsx`, shared `Button` + `utils.ts`; light/dark logos inlined from svgl
- Rename `styling-tailwind` → `ui-tailwind` to match the new category id; update its module definition to `category: "ui"` and `home: package`
- Add four new codemod builtins with tests: `add-array-entry-in-call` (inserts an element into a named array argument of any `CallExpression`), `replace-import` (rewrites a specifier in an existing import declaration), `set-html-attributes` (sets attributes on an HTML/JSX element by tag name or selector), `set-tsconfig-paths` (merges `compilerOptions` entries into a `tsconfig.json` AST node); register all four in the builtins index and export from `@stanza/codemods`
- Add `wrap-root-layout` builtin tests
- Update registry.mdx, concepts.mdx, cli.mdx, getting-started.mdx, agents.mdx, index.mdx, AGENTS.md, README.md, TODO.md, and SKILL.md to reflect the category rename and new modules
2026-05-25 15:26:08 -04:00
jake 16295952a8 feat: add payments-stripe module and sync file-preview selection to URL hash
- Add `payments-stripe`: `stripe` SDK + framework-native Checkout Session / webhook route handlers for Next.js and TanStack Start; Better Auth bridge variant ships `@better-auth/stripe` and wires the plugin into `auth.ts` when `auth: better-auth` is selected; light logo inlined from svgl
- Sync the web builder's open file to the URL hash so refreshes and shared links restore the same selection; latch the last file path so folder-expand clicks don't wipe the preview; back/forward navigation reflects hash changes back into the tree; default-file selections don't pollute the hash (keeps shareable URLs clean)
- Trim over-explained JSDoc comments in `add-package-dep.ts` and `add-plugin-to-call.ts` to the terse one-liner style; condense the `init.ts` `fullPending` comment to match
- Update `registry.mdx` stripe row from 🟨 Planned to 🟢 Available with a brief feature summary
2026-05-25 14:06:07 -04:00
jake e28eba2702 feat: add Handlebars template engine with peer-aware render context and consolidate eslint-prettier into a single adaptive template
- Replace per-framework template files (`eslint.config.next.mjs`, `eslint.config.tanstack.mjs`) with a single `eslint.config.mjs` that branches via Handlebars `{{#if peers.framework}}` conditionals; add `handlebars` as a direct dependency to `@stanza/registry`, `apps/cli`, and `apps/web`
- Extend the template render context with a `peers: Partial<Record<CategoryId, string>>` map so any template can inspect the active framework, auth provider, etc.; thread it through `buildRenderContext`, `synthesizeTemplates`, the CLI `codemod-runner`, and the web `getBuilderState` server function
- Extract and export `activePeerIds(manifest, appId?)` from `@stanza/registry` to derive the peer map from a manifest (app-scoped for `home: app` categories); add `selectedPeerIds(selections)` in `apps/web/src/lib/selection.ts` as the URL-params-derived equivalent
- Add integration tests verifying framework-conditional eslint-prettier output for `default`, `next`, and `tanstack-start` adapters; expand resolver tests for `activePeerIds` app-scoping and for modules that declare both a `default` adapter and a framework-specific override
2026-05-25 13:35:27 -04:00
jake 3304abbe98 feat: add payments-polar module, consolidate registry build into packages/registry, and generalize plugin codemods
- Add `payments-polar`: `@polar-sh/sdk` + framework-native checkout/webhook route handlers for Next.js and TanStack Start; Better Auth bridge variant ships `@polar-sh/better-auth` and wires the plugin into `auth.ts` when `auth: better-auth` is selected; light/dark logos inlined from svgl
- Consolidate the registry build: delete `scripts/registry-build.ts` and move the emit logic into `packages/registry/src/build.ts`, which now accepts an optional positional output-base arg (defaults to `dist/`) and writes `{base}/registry/{index,modules/*}.json` + `{base}/schema.json` directly; `prepare-registry.sh` is reduced to a single `jiti packages/registry/src/build.ts apps/web/public` invocation; CI/release workflows drop the Bun setup + `registry:build` steps since the build now runs inline at web-app prebuild time
- Replace `add-vite-plugin` codemod with the more general `add-plugin-to-call` (targets any `CallExpression` by callee + arg position, not just `vite.config` `plugins` arrays); add `add-package-dep` builtin for imperatively injecting deps into a `package.json` AST node; both are fully tested and registered in the builtins index
- Fix `init` adapter resolution order: precompute a `fullPending` map from all peer-category picks before the apply loop so `resolveAdapter` sees the complete peer set regardless of `categoryOrder` traversal sequence; export `PEER_CATEGORIES` from `@stanza/registry` to support this
2026-05-25 13:19:06 -04:00
jake be001b0037 feat: add multi-app support with per-app module targeting and app-aware manifest schema
- Introduce `AppSpec` (`{ id, dir, kind }`) and `AppKind = "web" | "native"` types; `stanza.json` now declares `apps: AppSpec[]` alongside modules, and `stanza init` scaffolds a default web app via `defaultWebApp()`
- Add optional `apps?: string[]` to `StanzaModuleRecord` to encode which apps a record targets; `home: app` modules require it, `home: package` modules use it to scope workspace deps (omitting means all apps), `home: repo` modules forbid it
- Make cardinality enforcement app-scoped for `home: app` categories: `selectedOne(m, cat, appId?)` and `selectedAll(m, cat, appId?)` now filter by app, and `add`/`remove` thread a `targetApp` through the runner so per-app single-choice rejection works correctly
- Extend the template render context with an `app: { id, dir, kind }` binding (plus the legacy `project.appDir` alias) re-bound per target app; the codemod runner fans out template/codemod/dep application once per consuming app in a loop
- Update `synthesizePackageJsons`, `synthesizeTemplates`, `buildRenderContext`, and related web-preview helpers to accept an `apps` array and produce per-app output
- Update `package-json.ts` to strip workspace deps from all apps' `package.json` on remove and to inject them into every consuming app on install
- Expand `manifest.test.ts` and `template.test.ts` with multi-app fixture coverage; add `"use client"` directive to codemod test files to satisfy the RSC boundary
- Refresh `AGENTS.md`, `SKILL.md`, and all affected docs (`concepts.mdx`, `registry.mdx`, `cli.mdx`, `getting-started.mdx`, `agents.mdx`) to document the new apps schema and routing semantics
2026-05-23 22:26:09 -04:00
jake e6709b24f7 fix: capitalize "Stanza" consistently across docs, code, and templates 2026-05-23 21:40:15 -04:00
jake 017b8620fc feat: centralize project-name validation in @stanza/registry and wire it into the CLI and web builder
- Add `packages/registry/src/project-name.ts` with a `validateProjectName` helper backed by `validate-npm-package-name`; rejects scoped names (the user supplies only the bare name, stanza synthesizes `@<name>/db` etc.) and returns a typed `ProjectNameValidation` discriminated union; export from `@stanza/registry`
- Replace the inline regex in `apps/cli/src/lib/wizard.ts` (both interactive and `--yes` paths) with `validateProjectName` so error messages are consistent
- Update `apps/web/src/components/builder/project-setup.tsx` to call `validateProjectName` via `useMemo`, gate the debounced `onNameChange` callback to only fire on valid input, and display inline `FieldError` feedback (suppressed while the field is empty so the placeholder fallback isn't alarming); add `maxLength`, `aria-invalid`, and `aria-describedby` for accessibility
2026-05-22 20:35:58 -04:00
jake 8c5433aa3c feat: move template substitution into @stanza/registry and switch to dotted Mustache paths
- Promote `renderTemplate` and `buildRenderContext` from `@stanza/codemods` into `@stanza/registry` alongside `synthesizeManifest`/`synthesizeEnvExample`/`synthesizePackageJsons`; add a new `synthesizeTemplates` export that returns the fully-substituted file list for a resolved selection so the web preview and the CLI apply path share identical logic
- Change the template DSL to dotted paths: `{{project.name}}` (was `{{projectName}}`), `{{project.appDir}}` (was `{{appDir}}`), `{{package.name}}` (was `{{packageName}}`), and `{{packages.<dir>.name}}` (was `{{<dir>PackageName}}`, e.g. `{{packages.db.name}}`); add a `TemplateContext` type exported from `@stanza/registry`
- Migrate all first-party modules (`auth-better-auth`, `auth-clerk`) to the new dotted syntax; fixes the web builder preview regression where tokens like `{{dbPackageName}}` appeared unsubstituted
- Update AGENTS.md docs and CLI `codemod-runner.ts` to reflect the new context shape and import locations
2026-05-22 19:58:09 -04:00
jake 78f8e44721 chore: migrate to vite+ 2026-05-22 18:37:24 -04:00
jake 9bf80ce2e8 docs: expand README and documentation site with concepts, getting-started, and CLI reference pages
- Add `concepts.mdx` covering categories, cardinality, home, vendoring, peers/adapters, manifest, regions, and the open registry model
- Flesh out `getting-started.mdx` with prerequisites, install/run steps, package-manager variants, inspect/remove commands, and a non-interactive `--yes` example
- Rewrite `cli-reference.mdx` with per-verb sections, global flags, environment variables, and dependency-versioning semantics
- Expand `README.md` with a "Why Stanza?" section, quick-start snippet, docs link, and updated module list; swap `pnpm create` example for `npm init`
- Add `concepts` to `meta.json` nav order
2026-05-22 16:03:15 -04:00
jake 71f4c9d89b refactor: unify module taxonomy into a single Category concept with cardinality and home
- Replace the `slot` / `add-on` discriminated-union split with one `Category` shape carrying two orthogonal fields: `cardinality` (`"one"` for single-choice, `"many"` for coexisting) and `home` (`"app"` / `"repo"` / `"package"`) — the old `packageDir` + `repoScoped` pair is dropped
- `Module` is no longer a discriminated union; it carries a single `category` field referencing a `Category` id; `KNOWN_SLOTS`, `KNOWN_ADDONS`, `ADDON_CATEGORIES`, and all related helpers are replaced by a single `CATEGORIES` array and derived `categoryHome` / `categoryLabel` / `PACKAGE_DIRS` lookups
- Manifest schema bumped to version `0.2` — `modules` is now one record keyed by category holding arrays; `cardinality: "one"` categories are enforced to ≤ 1 entry at install time; no migration (stanza is pre-1.0 and unpublished)
- Peer/constraint logic is now emergent from cardinality: the resolver treats only `cardinality: "one"` categories as peers, so `testing` (many) can never accidentally become a peer; install routing is a single `categoryHome` lookup shared by the CLI runner, codemod runner, and web preview
- Update all CLI commands (`add`, `remove`, `init`, `list`, `search`), the wizard, `codemod-runner`, `registry-loader`, all web builder/detail/search components, server functions, OG route, sitemap, analytics, and selection logic to use the new category API
- Update all first-party `module.ts` files, `registry-build.ts`, tests, and docs (`AGENTS.md`, `REGISTRY.md`, `SKILL.md`) to reflect the unified taxonomy
2026-05-21 23:03:02 -04:00
jake c454f30798 feat: add optimistic slot/addon selection, warm Shiki on load, and fix file-tree expand replay
- Wrap `setSelection` and `toggleAddon` in `startTransition` + `useOptimistic` so card borders and checkmarks flip instantly on click; the preview pane continues to wait on committed loader data (server-rendered Shiki HTML) and shows its own spinner for that round-trip
- Fire-and-forget `getHighlighter()` during the initial `getBuilderState` call so the Shiki singleton is warm before the first slot toggle, eliminating cold-start latency on the first preview render
- Fix `initialExpandedPaths` replay in `FilePreview`: filter the expanded-dir set to only directories whose entire ancestor chain is also expanded, preventing a collapsed parent from being re-opened when its still-flagged descendant is replayed
- Hoist the loading overlay outside the empty/non-empty branch so it also covers the initial `filePaths === []` → content transition, where `filePaths` is still stale while the loader is in flight
2026-05-21 22:22:03 -04:00
jake 65c02d4a46 feat: add tooling slot with eslint-prettier, biome, and oxlint-oxfmt modules
- Promote `tooling` from an add-on category to a single-choice **slot** in `KNOWN_SLOTS`/`SLOTS` and remove it from `KNOWN_ADDONS`/`ADDON_CATEGORIES`; the three toolchains are mutually exclusive substitutes so single-choice semantics fit, and nothing peers on `tooling` so it bears no outbound dispatch constraints
- Add `registry/modules/tooling-eslint-prettier` — ESLint flat config + Prettier with per-framework adapters (`next` → `eslint.config.next.mjs`; `tanstack-start` → `eslint.config.tanstack.mjs`) and a shared `prettier.config.mjs`; scripts: `lint: "eslint ."`, `format: "prettier --write ."`
- Add `registry/modules/tooling-biome` — Biome 2.x with a single `default` adapter (framework-agnostic `biome.json`); scripts: `lint`, `format`, `check`
- Add `registry/modules/tooling-oxlint-oxfmt` — Oxlint + oxfmt with a single `default` adapter (`dot_oxlintrc.json`, `dot_oxfmtrc.json`); scripts: `lint`, `format`
- Drop `lint: "next lint"` from `framework-next` — `next lint` was removed in Next 16, so the tooling module now owns `scripts.lint` cleanly
- Add CLI tests covering `init --yes --tooling eslint-prettier`, framework-next shipping no lint script without a tooling pick, `add tooling biome` on an existing project, and slot-already-filled rejection
2026-05-21 21:50:33 -04:00
jake b6e9b55c56 feat: generate and publish stanza.json JSON Schema at https://stanza.tools/schema.json
- Add `MANIFEST_SCHEMA_URL` constant and `manifestJsonSchema()` function to `packages/registry/src/manifest.ts`; the schema is derived from `StanzaManifestSchema` via `z.toJSONSchema` and decorated with `$id`, `title`, and `description`
- Add `$schema` field to the `StanzaManifest` type and `StanzaManifestSchema`; `emptyManifest` populates it automatically and `writeManifest` in the CLI spreads `$schema` first so pre-existing manifests gain the pointer on their next write
- Extend `scripts/registry-build.ts` to write `dist/schema.json` alongside the registry output; update `prepare-registry.sh` to copy `dist/schema.json` to `apps/web/public/schema.json` so it is served at the web root
- Gitignore `apps/web/public/schema.json` as a generated artifact
2026-05-21 21:32:58 -04:00
jake 69e02522be refactor: replace mri with citty for CLI argument parsing
- Swap `mri` for `citty` and wrap every command handler with `defineCommand` so citty owns parsing, validation, and `--help` output; remove the hand-rolled `mri` parse block from `bin.ts` and simplify `run()` to accept no arguments
- Extract shared global flags (`--dry-run`, `--dangerously-allow-dirty`, `--no-telemetry`) into a new `commands/_args.ts` as `commonArgs` that is spread into each subcommand's `args` definition; introduce a `CliArgs` open-record type for handler signatures
- Migrate `add`/`remove`/`init`/`list`/`search` command signatures from `({ positional, argv: Argv }) => …` to a flat `(args: CliArgs) => …` shape; declare `slot`, `moduleId`, and `name` as `type: "positional"` in their respective `defineCommand` blocks
- Update the `commands.test.ts` helper from `argv(flags)` to `args(flags)` to match the flat citty-parsed shape (named positionals promoted to top-level keys alongside booleans)
2026-05-21 21:16:10 -04:00
jake f6384e3ce7 feat: add opt-out CLI telemetry via a fire-and-forget /api/events PostHog proxy
- Add `apps/cli/src/lib/telemetry.ts` — a dependency-free telemetry module that accumulates events in memory and flushes them as a single `fetch` POST at the end of each run; identity is an ephemeral per-process UUID (no on-disk state); suppressed by `--no-telemetry`, `STANZA_TELEMETRY=0`, `DO_NOT_TRACK=1`, or any recognized CI environment variable
- Wire `telemetry.configure` / `telemetry.capture` / `telemetry.flush` into `run.ts` so every command emits a `cli_command` event (with `status` and `duration_ms`) and individual `init`/`add`/`remove` calls emit `cli_module` events (with `action`, `group`, and `module`)
- Rename the `mri` boolean from `no-telemetry` to `telemetry` in `bin.ts` so mri's `--no-` prefix convention populates `argv.telemetry === false` as `isTelemetryDisabled` expects
- Add `apps/web/src/routes/api.events.ts` — a TanStack Start API route that receives the CLI's batched event payload and forwards each event to PostHog via the REST `capture` endpoint using `POSTHOG_API_KEY`; non-`POST` requests, missing keys, and upstream errors all return appropriate status codes without leaking details
- Document the `--no-telemetry` flag, `STANZA_TELEMETRY`, and `DO_NOT_TRACK` env vars in `skills/stanza-cli/SKILL.md`
2026-05-21 20:17:25 -04:00
jake 27f90c1765 refactor: replace Fumadocs-owned docs chrome with custom DocsSidebar / DocsToc components
- Add `src/components/docs/docs-sidebar.tsx` — a bespoke sidebar that reads the Fumadocs page tree and renders a `<NavList>` of links using TanStack Router's `<Link>`; shows a `<details>` disclosure on mobile and a sticky `<aside>` rail on `md`+, active item highlighted via the site's `--accent` token
- Add `src/components/docs/docs-toc.tsx` — a bespoke table of contents that wraps Fumadocs' `AnchorProvider`/`useActiveAnchor` in a plain `<ul>` with depth-based padding; hidden below `xl`
- Replace `<DocsLayout>` / `<DocsPage>` / `<DocsTitle>` / `<DocsDescription>` in `docs.$.tsx` with the custom components and plain `<article>` markup so the docs page is fully owned by the site shell rather than Fumadocs' layout primitives; `baseOptions()` and `layout.shared.tsx` are deleted
- Simplify the Fumadocs CSS token override in `styles.css` — the `#nd-sidebar` specificity hack is no longer needed now that Fumadocs renders no sidebar of its own
- Move `bin` from the top-level field into `publishConfig` in both `apps/cli/package.json` and `packages/create-stanza/package.json` so the binary entries only apply to the published artifact, not local workspace resolution
- Add a `tslib` → `tslib/tslib.es6.mjs` alias in `vite.config.ts` to avoid Rolldown's CJS interop issue with tslib's UMD bundle in Vite 8
- Set `[--trees-border-radius-override:0px]` on the file-tree in `FilePreview` so selected rows render as flush rectangles rather than rounded pills
2026-05-21 18:58:22 -04:00
jake 0efdeac33a feat: extract appendEnvVar/synthesizeEnvExample/synthesizeManifest into @stanza/registry and surface .env.example + stanza.json in web preview
- Add `packages/registry/src/synthesize.ts` with three pure (fs-free) exports: `ENV_EXAMPLE_HEADER` (the managed comment stanza writes at the top of `.env.example`), `appendEnvVar` (idempotent env-var upsert — same logic the CLI used inline, now shared), and `synthesizeEnvExample` / `synthesizeManifest` which walk resolved slots + add-ons in apply order to reproduce the files stanza writes at install time
- Refactor `packages/codemods/src/env.ts` — `addEnvVar` now delegates to `appendEnvVar` from `@stanza/registry` and just wraps the fs read/write; the inline split/splice logic is deleted; `ENV_EXAMPLE_HEADER` replaces the hardcoded string in `init.ts`
- Wire `synthesizeManifest` and `synthesizeEnvExample` into `builder-state.functions.ts` so the web builder preview tree now includes a synthesized `stanza.json` and `.env.example` alongside the already-synthesized `package.json` files — the preview matches what `stanza init` actually writes byte-for-byte
- `regions` is intentionally left empty in the synthesized manifest (it's internal per-file codemod bookkeeping that would require duplicating codemod-internal path logic to reproduce faithfully)
2026-05-21 18:11:02 -04:00
jake cca42071ee feat: add Fumadocs-powered /docs section with Introduction, Getting Started, and CLI reference pages
- Install `fumadocs-core`, `fumadocs-mdx`, and `fumadocs-ui`; add `fumadocs-mdx/vite` plugin and wire it first in `vite.config.ts` so MDX files are processed before TanStack Start
- Add `source.config.ts` (single `defineDocs` collection pointing at `content/docs`), `src/lib/source.ts` (Fumadocs loader with `/docs` base URL), and `src/lib/layout.shared.tsx` (disables Fumadocs' own nav, theme switch, and search toggle so the site shell owns those concerns)
- Add `src/components/mdx.tsx` exporting `getMDXComponents`/`useMDXComponents` built on `fumadocs-ui/mdx` defaults; declare global `MDXProvidedComponents` for type-safe component overrides
- Add `src/routes/docs.$.tsx` — splat route that runs a server function to look up the page + serialize the page tree, preloads the client MDX bundle, renders `<DocsLayout>` inside `<RootProvider theme/search disabled>`, and feeds `buildHead` for per-page SEO
- Add `content/docs/index.mdx` (Introduction), `content/docs/getting-started.mdx`, `content/docs/cli-reference.mdx`, and `content/docs/meta.json` controlling sidebar order
- Add `collections/*` path alias pointing at `.source/` in `tsconfig.json`; add `.source/` to `.gitignore`
- Import `fumadocs-ui/css/neutral.css` and `fumadocs-ui/css/preset.css` plus the Tailwind source glob in `styles.css`
- Add a "Docs" nav link in `<Header>` pointing to `/docs/$` with an empty splat
2026-05-21 18:08:39 -04:00
jake 5e966f7143 feat: resolve ^/~ dep ranges to latest satisfying npm version at install time
- Add `apps/cli/src/lib/npm-version.ts` with `resolveRange(name, range)` and `resolveRanges(deps)` — fetches the abbreviated packument from the npm registry (or `STANZA_NPM_REGISTRY`), picks the `semver.maxSatisfying` version, and re-attaches the original modifier (e.g. `^1.6.11` → `^1.8.3`); non-`^`/`~` ranges, workspace specs, and tags are left verbatim without hitting the network; results are cached per package name for the process lifetime so a multi-module `init` fetches each package at most once
- Wire `resolveRanges` into `applyModule` in `codemod-runner.ts` — both `dependencies` and `devDependencies` are re-pinned before being written to `package.json`; dry-run skips the network entirely since nothing is written
- Add `STANZA_NO_NPM_LOOKUP=1` env var to bypass lookups and fall back to verbatim ranges (network errors also fall back per-entry); set it in `commands.test.ts` `beforeEach` to keep integration tests hermetic
- Add `npm-version.test.ts` covering caret/tilde bumping, pass-through of other range shapes, `STANZA_NO_NPM_LOOKUP` bypass, network error fallback, non-ok response fallback, nothing-satisfies fallback, per-package caching, and scoped name URL encoding
- Document the new behavior and env vars in `skills/stanza-cli/SKILL.md` under a new "Dependency Versions" section
2026-05-21 17:47:54 -04:00
jake c044a56487 feat: guard mutating commands against a dirty git working tree
- Add `apps/cli/src/lib/git.ts` with `worktreeStatus()` (runs `git status --porcelain` in the target dir; treats missing git / non-repo as clean) and `ensureCleanWorktree()` which prints a formatted error listing the dirty paths and returns false so callers can bail before writing anything
- Wire the guard into `cmdInit`, `cmdAdd`, and `cmdRemove` — all three check the worktree before the interactive wizard or any file mutations; `--dry-run` skips the check since nothing is written
- Add `--dangerously-allow-dirty` boolean flag (registered in `mri` and documented in the `--help` output) to override the refusal with a warning when callers explicitly opt in
2026-05-21 17:24:27 -04:00
jake 74abfecc53 feat: package-manager selector, CommandPreview component, and package.json default in file tree
- Add `apps/web/src/lib/package-manager.ts` — a `usePackageManager` hook backed by `localStorage` (key `stanza:pm`) that persists the user's preferred package manager across sessions; defaults to `npm`
- Add `<PackageManagerSelect>` (Base UI `Select`) rendering npm / pnpm / bun / yarn options with their respective icons, sized to match the `h-8` command bar
- Extract `<CommandPreview name selections addons>` — owns the PM picker, `<pre>` command text, and `<CopyButton>`; `buildCommand` gains an optional `pm` parameter so the emitted command is prefixed with the chosen manager (e.g. `pnpm create stanza …`); `project-setup.tsx` and `try-it.tsx` both drop their inline `<pre>` + `CopyButton` in favour of `<CommandPreview>`
- `ProjectSetup` now receives `selections` + `addons` and delegates command construction to `CommandPreview` — the parent (`builder/index.tsx`) no longer calls `buildCommand` itself and no longer passes a pre-built string down the tree
- `TryIt` on the module detail page (`m.$slot.$id.tsx`) updated the same way: accepts `name / selections / addons` and renders `<CommandPreview>`
- `FilePreview` defaults the active path to `"package.json"` when that file exists in the tree (the synthesized root package.json is the most useful at-a-glance summary), falling back to `filePaths[0]` otherwise
- Remove the `slot/id · vX.Y.Z` monospace footer from non-disabled `ModuleCard` buttons — it added noise without much value
- Add `selection.test.ts` coverage for the `pm`-parameterised `buildCommand` variants
2026-05-21 17:19:31 -04:00
jake da9f8a8b69 refactor: extract package-json builders into @stanza/registry and surface synthesized files in web preview
- Move `mergeInstallFields`, `rootPackageJson`, `appPackageJsonBase`, and `slotPackageJsonBase` out of `codemod-runner.ts` / `init.ts` and into a new `packages/registry/src/package-json.ts` module; all consumers now import from `@stanza/registry` — the CLI no longer hand-rolls inline object literals for workspace roots or app packages, and the bun/npm "read-parse-mutate-write workspaces" dance collapses into the single `rootPackageJson()` call that emits `workspaces` for non-pnpm managers
- Add `synthesizePackageJsons(resolved, resolvedAddons, { name })` which produces a `path → PackageJson` map for every `package.json` the codemod runner would write (root, app, each slot package); `builder-state.functions.ts` calls it and appends the results to `previewFiles` so the web builder tree shows merged deps/scripts as they'll actually appear on disk — previously package.json files were entirely absent from the preview
- Command bar `<pre>` in `project-setup.tsx` and `try-it.tsx` gets `h-8 flex items-center no-scrollbar` so the command text aligns with the copy button and hides the scrollbar gutter; `CopyButton` drops the `className="h-auto!"` override that was fighting the button's own size token
- Builder grid columns gain `min-w-0` to prevent flex children from overflowing their track on narrow viewports
2026-05-21 17:00:28 -04:00
jake 19b5e51075 feat: add-on module kind, testing-vitest + testing-playwright, and multi-choice CLI/web support
- Introduce a `kind: "slot" | "addon"` discriminated union on `Module` — slot modules carry `slot`, add-on modules carry `category` (one of `KNOWN_ADDONS`). `isAddon()` narrows the union; `moduleGroup()` returns the slot-or-category key. Add-on categories (`testing`, `tooling`, `deploy`, `email`, `monorepo`) live in `KNOWN_ADDONS`/`ADDON_CATEGORIES`, deliberately disjoint from `KNOWN_SLOTS` so the resolver never lets add-ons participate in peer dispatch (they can still declare a one-way `peers` + per-framework adapters)
- Ship the first two add-ons: `testing-vitest` (jsdom + RTL, `test`/`test:watch` scripts, per-framework adapters) and `testing-playwright` (per-framework `webServer`, `test:e2e`/`test:e2e:ui` scripts — disjoint from vitest's so region claims don't collide); both under `registry/modules/testing-*/`
- Manifest gains `addons: Partial<Record<AddonCategoryId, StanzaAddonRecord[]>>` (`.default({})` keeps existing `stanza.json` valid); resolver gains `addonOrder` for deterministic codemod sequencing
- CLI: `stanza add <slot|category> <module>` accepts both slots and add-on categories; duplicate-id guard for add-ons instead of the single-fill slot guard; `stanza remove <slot|category> [id]` removes a specific add-on by id (or the whole slot); `stanza list` shows add-ons alongside slots; `stanza init --yes` gains `--testing vitest,playwright` flag; wizard prompts multi-select for add-on categories
- Web builder: add-on slots render as checkbox multi-select cards (`SlotSection` detects `isAddon` and switches to `<CheckboxCard>`), `?testing=vitest,playwright` comma-joined URL param, `builder-state.functions.ts` parses/serializes per-category add-on lists, OG and sitemap routes updated for add-on module paths
- Codemod runner branches on `isAddon` to resolve add-on records from `manifest.addons[category]` rather than `manifest.modules[slot]`; region ownership keys on `module.id` so co-existing add-ons writing the same file at disjoint dot-paths never conflict
- Add `commands.test.ts`, `selection.test.ts`, `manifest.test.ts`, and `resolver.test.ts` coverage for add-on add/remove/list flows and schema validation
2026-05-21 16:34:09 -04:00