refactor: dissolve packages/internal into scripts/, reorganize CLI into src/lib/, and wire stanza remove codemod revert

- Delete `packages/internal` workspace package — `registry-build.ts` and `module-new.ts` move to top-level `scripts/` (no `package.json`, not shipped); `pnpm registry:build` and `pnpm module:new` scripts updated in root `package.json` and `turbo.json`; CLAUDE.md / TODO.md references updated
- Relocate CLI internals from `apps/cli/src/` → `apps/cli/src/lib/` — `codemod-runner.ts`, `manifest.ts`, `region-tracker.ts`, `registry-loader.ts`, `wizard.ts`; all intra-CLI imports updated to `@/lib/*` path alias via tsconfig `paths`; add `@types/bun` to CLI devDependencies
- `stanza remove` now dispatches `revertCodemods()` as step 1 (before declarative cleanup) — re-loads the module adapter from the registry, calls imperative reverts while framework files are still intact, then sweeps remaining region claims; fixes `wrap-root-layout` / `re-export` regions previously left as "needs manual cleanup" despite shipping working reverts
This commit is contained in:
2026-05-20 16:10:17 -04:00
parent f83e84eb85
commit 2651daf573
36 changed files with 231 additions and 148 deletions
+6 -6
View File
@@ -21,7 +21,7 @@ Three things differentiate stanza from other scaffolders:
- `packages/registry/` — shared schema, slot/peer resolver, Zod manifest validator
- `packages/codemods/` — ts-morph helpers (idempotent + reversible)
- `packages/create-stanza/``pnpm create stanza` shim
- `packages/internal/` — Internal maintenance scripts: `registry-build.ts` (emits static CDN JSON), `module-new.ts` (scaffolds a new module)
- `scripts/` — repo-root maintainer helpers (not shipped): `registry-build.ts` emits static CDN JSON, `module-new.ts` scaffolds a new module
- `registry/modules/<slot>-<id>/` — first-party modules: `module.ts` + `templates/` (modules don't ship codemod code; see Architecture rules)
In a **generated project**, `auth`, `db`, and `orm` modules install into their own internal workspace package at `packages/<dir>/` (named `@<manifest.name>/<dir>`); the app consumes them via `workspace:*` deps. `framework` and `styling` stay app-scoped because they wire the app shell itself. The mapping is hardcoded in [`SLOT_PACKAGE_DIR`](packages/registry/src/module.ts) — `auth → "auth"`, both `db` and `orm → "db"` (they share a single `packages/db/` package).
@@ -29,7 +29,7 @@ In a **generated project**, `auth`, `db`, and `orm` modules install into their o
## Commands
- `bun apps/cli/src/bin.ts <verb>` — run CLI directly without build
- `pnpm registry:build` (or `bun packages/internal/src/registry-build.ts`) — regenerate `dist/registry/{index,modules/*}.json`
- `pnpm registry:build` (or `bun scripts/registry-build.ts`) — regenerate `dist/registry/{index,modules/*}.json`
- `pnpm module:new [slot] [id]` — scaffold a new module under `registry/modules/`
- `pnpm --filter @stanza/web dev` — TanStack Start dev server; `predev` hook auto-copies `dist/registry/``apps/web/public/registry/` so the same-domain registry path resolves
- `pnpm lint` / `pnpm lint:fix` — Oxlint across the whole repo (config: `.oxlintrc.json`)
@@ -42,8 +42,8 @@ In a **generated project**, `auth`, `db`, and `orm` modules install into their o
## Toolchain invariants
- pnpm 10 + `node-linker: isolated` — each workspace MUST declare `@types/node` in its own devDeps and set `types: ["node"]` in tsconfig (auto-discovery doesn't reach into the isolated `node_modules/@types`)
- TypeScript 6 — `allowImportingTsExtensions: true` + `noEmit: true` is set in `tsconfig.base.json`; build with `bun build`, not `tsc`
- `tsconfig.base.json` excludes `**/templates/**` globally — template files target user projects, not this repo
- TypeScript 6 — `allowImportingTsExtensions: true` + `noEmit: true` is set in `tsconfig.json`; build with `bun build`, not `tsc`
- `tsconfig.json` excludes `**/templates/**` globally — template files target user projects, not this repo
- Zod 4: use `z.partialRecord(K, V)` for finite-key partial records (`z.record(z.enum, V)` requires exhaustive keys)
- TanStack Start: `verbatimModuleSyntax: false` in `apps/web/tsconfig.json` (server bundles leak otherwise); `tanstackStart()` MUST precede `react()` in vite plugins
- Bun runs the CLI directly from `.ts`; don't add a TS compile step
@@ -51,7 +51,7 @@ In a **generated project**, `auth`, `db`, and `orm` modules install into their o
## Architecture rules
- **Modules are vendored**: their templates land in the user's repo verbatim; no `@stanza/runtime` dep
- **Template distribution**: `packages/internal`'s `registry-build.ts` inlines each template file's contents into the per-module JSON's `templates[].content` field so HTTP-loaded manifests are self-contained. The runner prefers `tpl.content` and only reads from `registry/modules/<x>/templates/` when it's absent (local dev). New templates need no build wiring — they're picked up automatically
- **Template distribution**: `scripts/registry-build.ts` inlines each template file's contents into the per-module JSON's `templates[].content` field so HTTP-loaded manifests are self-contained. The runner prefers `tpl.content` and only reads from `registry/modules/<x>/templates/` when it's absent (local dev). New templates need no build wiring — they're picked up automatically
- **Module logos**: drop `logo.svg` (theme-agnostic) or `logo-light.svg` + `logo-dark.svg` (theme pair) in a module's directory. The registry build auto-detects and inlines as `mod.logo` (string or `{ light, dark }`) — module authors don't declare anything in `module.ts`. First-party logos come from [svgl.app](https://svgl.app). The web builder renders inline via `dangerouslySetInnerHTML`
- **Registry is data; CLI is the runtime**: the per-module JSON ships templates (text), deps (strings), env (strings), scripts (strings), logos (SVG markup), and codemod **invocations** (`{ id, args }`). It does NOT ship codemod _code_. The catalog of generic codemods lives in [packages/codemods/src/builtins/](packages/codemods/src/builtins/) and is exposed via the `@stanza/codemods/builtins` subpath export — each codemod is parameterized by `TArgs` and reusable across modules (`wrap-root-layout` serves both Clerk and any future provider-style auth/state library). The catalog is statically imported into the CLI binary at build time, so distribution shape (single binary, pnpm-isolated, npm-hoisted, `npx`, `bun --compile`) doesn't matter — implementations always travel with the runtime
- **Adding a generic codemod**: drop `<id>.ts` under [packages/codemods/src/builtins/](packages/codemods/src/builtins/), default-export a `Codemod<TArgs>`, and register it in [packages/codemods/src/builtins/index.ts](packages/codemods/src/builtins/index.ts). Codemods that bake in module-specific identifiers don't belong — factor them into args
@@ -60,7 +60,7 @@ In a **generated project**, `auth`, `db`, and `orm` modules install into their o
- **Slot taxonomy** is currently `framework | styling | db | orm | auth` (see `KNOWN_SLOTS`); adding a slot is a manifest schema bump — update `KNOWN_SLOTS`, `slotOrder`, the Zod manifest schema, and `SLOT_PACKAGE_DIR` together (decide whether the new slot extracts into its own package or stays app-scoped)
- **Adapter keys** encode peer choices (e.g., `next+drizzle`); the resolver picks the most specific match
- **Slot-package extraction**: `auth`/`db`/`orm` modules install into `packages/<dir>/` workspace packages (named `@<manifest.name>/<dir>`); templates `scope: "package"`, deps/devDeps/scripts route there, and the app gets a `workspace:*` dep wired by the runner. `framework`/`styling` stay app-scoped (their `SLOT_PACKAGE_DIR` entry is `null`). The bootstrap files (the package's `package.json` + `tsconfig.json` and the host app's workspace dep) are **system-owned** — not tracked in `regions`. `stanza remove`'s sweep deletes them when no claims remain under `packages/<dir>/`
- **Generated projects don't share a tsconfig base**: every `apps/*/tsconfig.json` and `packages/*/tsconfig.json` is self-contained. The framework module ships the app's tsconfig; the runner's `ensureSlotPackage` writes a matching self-contained config when bootstrapping a slot package. `tsconfig.base.json` lives in the stanza repo only; do not emit it in generated trees
- **Generated projects don't share a tsconfig base**: every `apps/*/tsconfig.json` and `packages/*/tsconfig.json` is self-contained. The framework module ships the app's tsconfig; the runner's `ensureSlotPackage` writes a matching self-contained config when bootstrapping a slot package. `tsconfig.json` lives in the stanza repo only; do not emit it in generated trees
- **Cross-package wiring**: when an adapter's source code imports from another internal package (e.g. `better-auth`'s `auth.ts` reads `db` from the orm package), declare `peerPackages: ["db"]` on the adapter. The runner adds `@<project>/db: workspace:*` to the current package's `package.json`. Templates can reference other packages via `{{<dir>PackageName}}` substitution (e.g. `{{dbPackageName}}``@my-app/db`) — substitution runs over both template bodies (when `template: true`) and codemod-invocation `args` string values
- **Region ownership** in `stanza.json` is the source of truth for `remove`/future-`swap`; two modules claiming the same region is a hard error (`RegionConflictError`)
- **Declarative beats imperative**: prefer `templates`/`dependencies`/`env`/`scripts` over imperative codemods; the runner applies declarative fields generically
+2 -3
View File
@@ -19,7 +19,7 @@ The builder is functionally wired but visually unfinished. It's inline-styled ra
- [ ] Layout: header (logo, GitHub link, docs link), footer
- [ ] Dark mode (Tailwind 4 `light-dark()` + system pref)
- [ ] SEO: meta tags via `head()` on routes, OG image, sitemap
- [ ] Host the registry on the same domain — wire `packages/internal`'s registry-build output to `apps/web/public/registry/` and serve statically, or via a Vercel route handler
- [ ] Host the registry on the same domain — wire the registry-build output (`scripts/registry-build.ts``dist/registry/`) to `apps/web/public/registry/` and serve statically, or via a Vercel route handler
- [ ] Vercel deploy config — `vercel.json` if needed, env vars for `STANZA_REGISTRY` (point dev at local FS, prod at the deployed CDN path)
- [ ] Docs section (could be MDX routes): overview, authoring guide, registry spec
@@ -30,10 +30,9 @@ The wizard and verbs work but a few things from the plan are stubbed.
- [ ] Implement opt-out PostHog telemetry — wire `posthog-node` (already a dep), prompt on first run, store a `telemetryId` in `stanza.json`, respect `--no-telemetry` and `DO_NOT_TRACK=1`
- [ ] `--yes` flag for non-interactive `init` — pick defaults via flags (`--framework=next` etc.); essential for CI tests
- [ ] HTTP registry loader path is implemented but unverified — smoke test against the static JSON output
- [ ] `stanza init`: today's `bootstrapShell` doesn't include `tsconfig.json` at the repo root for the generated project, and doesn't emit `turbo.json`. Decide whether stanza ships those or modules do. Also: the root `pnpm-workspace.yaml` must cover `packages/*` so the runner's bootstrapped slot packages link correctly
- [ ] `stanza init`: today's `bootstrapShell` doesn't emit `turbo.json`. Decide whether stanza ships that or a tooling module does. Also: the root `pnpm-workspace.yaml` must cover `packages/*` so the runner's bootstrapped slot packages link correctly
- [ ] Better error messages for `RegionConflictError` (current message is technical; should suggest `stanza remove <slot>` or manual cleanup)
- [ ] `bun build` the CLI for publish — script exists, not yet exercised
- [ ] `stanza remove` doesn't dispatch to the inverse `revert()` on imperative codemods — regions touched by `wrap-root-layout` and `re-export` get flagged as "needs manual cleanup" even though the codemod ships a working revert. Fix: look up `CODEMOD_CATALOG[id].revert` for region keys that match a codemod-claim pattern (`imports.*`, `providers.*`, `re-exports.*`, `append.*`) and call it
- [ ] Tests for command handlers (`init`, `add`, `remove`) — current coverage is just codemods + resolver
## Modules
+1
View File
@@ -33,6 +33,7 @@
"semver": "^7.8.0"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^25.9.1",
"@types/semver": "^7.7.1",
"typescript": "^6.0.3",
+3 -3
View File
@@ -6,9 +6,9 @@ import { resolveAdapter, type SlotId, KNOWN_SLOTS } from "@stanza/registry";
import kleur from "kleur";
import type { Argv } from "mri";
import { applyModule } from "../codemod-runner";
import { findProjectRoot, readManifest } from "../manifest";
import { loadRegistry } from "../registry-loader";
import { applyModule } from "@/lib/codemod-runner";
import { findProjectRoot, readManifest } from "@/lib/manifest";
import { loadRegistry } from "@/lib/registry-loader";
export async function cmdAdd(args: {
slot?: string;
+4 -4
View File
@@ -6,10 +6,10 @@ import { resolveAdapter, slotOrder } from "@stanza/registry";
import kleur from "kleur";
import type { Argv } from "mri";
import { applyModule } from "../codemod-runner";
import { initManifest } from "../manifest";
import { loadRegistry } from "../registry-loader";
import { runInitWizard } from "../wizard";
import { applyModule } from "@/lib/codemod-runner";
import { initManifest } from "@/lib/manifest";
import { loadRegistry } from "@/lib/registry-loader";
import { runInitWizard } from "@/lib/wizard";
export async function cmdInit(args: { name?: string; argv: Argv }): Promise<void> {
const registry = await loadRegistry();
+1 -1
View File
@@ -3,7 +3,7 @@ import { slotOrder, type SlotId } from "@stanza/registry";
import kleur from "kleur";
import type { Argv } from "mri";
import { findProjectRoot, readManifest } from "../manifest";
import { findProjectRoot, readManifest } from "@/lib/manifest";
export async function cmdList(_args: { argv: Argv }): Promise<void> {
const projectRoot = findProjectRoot();
+50 -22
View File
@@ -7,8 +7,10 @@ import { KNOWN_SLOTS, SLOT_PACKAGE_DIR, type SlotId } from "@stanza/registry";
import kleur from "kleur";
import type { Argv } from "mri";
import { findProjectRoot, readManifest, writeManifest } from "../manifest";
import { regionsOwnedBy } from "../region-tracker";
import { revertCodemods } from "@/lib/codemod-runner";
import { findProjectRoot, readManifest, writeManifest } from "@/lib/manifest";
import { regionsOwnedBy } from "@/lib/region-tracker";
import { loadRegistry } from "@/lib/registry-loader";
export async function cmdRemove(args: { slot?: string; argv: Argv }): Promise<void> {
if (!args.slot) {
@@ -30,21 +32,43 @@ export async function cmdRemove(args: { slot?: string; argv: Argv }): Promise<vo
return;
}
const manifest = readManifest(projectRoot);
let manifest = readManifest(projectRoot);
const installed = manifest.modules[slot];
if (!installed) {
p.log.warn(`Slot "${slot}" is not filled.`);
return;
}
const owned = regionsOwnedBy(manifest, installed.id);
const dryRun = Boolean(args.argv["dry-run"]);
// Best-effort reversal: deps, env vars, and whole-file templates revert
// cleanly. Regions touched by imperative codemods get reported as "needs
// manual cleanup" until proper inverse codemods land.
const manualCleanup: string[] = [];
// Step 1: revert imperative codemods first. They modify framework- or
// peer-owned files (root layout, schema barrels, vite config) — reverts
// need to see those files intact, before any template deletions below.
const registry = await loadRegistry();
const mod = await registry.loadModule(slot, installed.id).catch(() => null);
const adapter = mod?.adapters.find((a) => a.key === installed.adapter);
if (mod && adapter) {
const revertResult = await revertCodemods({
projectRoot,
manifest,
module: mod,
adapter,
dryRun,
});
manifest = revertResult.manifest;
for (const id of revertResult.manualCleanup) {
manualCleanup.push(`codemod:${id} (no revert or revert threw)`);
}
} else {
p.log.warn(
`Couldn't re-load ${installed.id}@${installed.adapter} from the registry — skipping imperative codemod reversal.`,
);
}
// Step 2: reverse the declarative side — files, deps, scripts, env vars —
// driven by whatever region claims remain after the codemod reverts.
const owned = regionsOwnedBy(manifest, installed.id);
for (const { file, region } of owned) {
const abs = path.join(projectRoot, file);
if (file === ".env.example") {
@@ -67,14 +91,19 @@ export async function cmdRemove(args: { slot?: string; argv: Argv }): Promise<vo
}
}
if (region === "file") {
// Whole-file template — safe to delete since stanza wrote it.
if (!dryRun && fs.existsSync(abs)) fs.unlinkSync(abs);
continue;
}
// Anything still here is a codemod-claimed region whose revert wasn't
// dispatched (no revert defined, or it threw). Surface it for manual
// cleanup — but don't drop the claim, since the artifact is still in
// the file and an operator may still want to find it.
manualCleanup.push(`${file}:${region}`);
}
// Update manifest: drop module + its regions.
// Strip the module record + its remaining region claims (the codemod
// reverts already released their own; this picks up the declarative ones
// we just processed above).
const nextRegions = { ...manifest.regions };
for (const { file, region } of owned) {
if (nextRegions[file]) {
@@ -86,19 +115,20 @@ export async function cmdRemove(args: { slot?: string; argv: Argv }): Promise<vo
}
const nextModules = { ...manifest.modules };
delete nextModules[slot];
manifest = { ...manifest, modules: nextModules, regions: nextRegions };
// Sweep: for each slot that maps to an internal package, if nothing remains
// claimed under packages/<dir>/, tear down the bootstrap (package.json,
// tsconfig.json, the dir itself) and drop the workspace dep from the app.
// The system-owned bootstrap files aren't tracked as regions, so they'd
// otherwise linger forever.
// Step 3: sweep any internal package whose claims have all been released.
// The bootstrap files (package.json, tsconfig.json, the workspace dep on
// the app) are system-owned — not tracked in regions, so they'd otherwise
// linger forever.
const sweptPackages: string[] = [];
const appPkgRel = `${manifest.appDir}/package.json`;
const appPkgAbs = path.join(projectRoot, appPkgRel);
const appPkgAbs = path.join(projectRoot, manifest.appDir, "package.json");
for (const dir of new Set(
Object.values(SLOT_PACKAGE_DIR).filter((d): d is string => d !== null),
)) {
const stillUsed = Object.keys(nextRegions).some((file) => file.startsWith(`packages/${dir}/`));
const stillUsed = Object.keys(manifest.regions).some((file) =>
file.startsWith(`packages/${dir}/`),
);
if (stillUsed) continue;
const pkgRoot = path.join(projectRoot, "packages", dir);
if (!fs.existsSync(pkgRoot)) continue;
@@ -111,9 +141,7 @@ export async function cmdRemove(args: { slot?: string; argv: Argv }): Promise<vo
sweptPackages.push(dir);
}
if (!dryRun) {
writeManifest(projectRoot, { ...manifest, modules: nextModules, regions: nextRegions });
}
if (!dryRun) writeManifest(projectRoot, manifest);
p.log.success(`${kleur.green("✓")} Removed ${installed.id} from ${slot}`);
if (sweptPackages.length > 0) {
@@ -121,7 +149,7 @@ export async function cmdRemove(args: { slot?: string; argv: Argv }): Promise<vo
}
if (manualCleanup.length > 0) {
p.log.warn(
`${manualCleanup.length} region(s) need manual cleanup:\n` +
`${manualCleanup.length} item(s) need manual cleanup:\n` +
manualCleanup.map((r) => `${r}`).join("\n"),
);
}
+1 -1
View File
@@ -1,7 +1,7 @@
import kleur from "kleur";
import type { Argv } from "mri";
import { loadRegistry } from "../registry-loader";
import { loadRegistry } from "@/lib/registry-loader";
export async function cmdSearch(args: { query?: string; argv: Argv }): Promise<void> {
const registry = await loadRegistry();
@@ -21,7 +21,7 @@ import type {
import { SLOT_PACKAGE_DIR } from "@stanza/registry";
import { writeManifest } from "./manifest";
import { claim, RegionConflictError } from "./region-tracker";
import { claim, release, RegionConflictError } from "./region-tracker";
export type RunResult = {
manifest: StanzaManifest;
@@ -86,20 +86,7 @@ export async function applyModule(args: {
if (created) bootstrappedPackage = { dir: packageDir, name: packageName };
}
// Render context provides `{{packageName}}` for the active module's own
// package, plus shorthand `{{<dir>PackageName}}` keys for every slot package
// so cross-package imports (e.g. better-auth's auth.ts importing `db` from
// `@<project>/db`) can be templated declaratively.
const renderContext: Record<string, string> = {
appDir: manifest.appDir,
projectName: manifest.name,
packageName,
};
for (const dir of new Set(
Object.values(SLOT_PACKAGE_DIR).filter((d): d is string => d !== null),
)) {
renderContext[`${dir}PackageName`] = `@${manifest.name}/${dir}`;
}
const renderContext = buildRenderContext(manifest, packageName);
// 1. Templates (claim regions per-template-file).
for (const tpl of adapter.templates ?? []) {
@@ -413,7 +400,8 @@ function buildContext(args: {
project: () => Project;
touchedFiles: Set<string>;
dryRun: boolean;
onClaim: (file: string, region: string) => void;
onClaim?: (file: string, region: string) => void;
onRelease?: (file: string, region: string) => void;
}): CodemodContext {
return {
projectRoot: args.projectRoot,
@@ -423,12 +411,118 @@ function buildContext(args: {
owner: { slot: args.module.slot, module: args.module.id },
adapter: args.adapter.key,
claimRegion(file, region) {
args.onClaim(file, region);
args.onClaim?.(file, region);
},
releaseRegion() {
// No-op during `add`. The remove path uses regionsOwnedBy() to reverse.
releaseRegion(file, region) {
args.onRelease?.(file, region);
},
};
}
/**
* Build the render context used by both template-body substitution and
* codemod-args substitution. Provides:
* - `{{appDir}}`, `{{projectName}}` project-level
* - `{{packageName}}` the active module's own package (empty for slots
* without a package mapping)
* - `{{<dir>PackageName}}` (e.g. `{{dbPackageName}}`) shorthand for every
* slot package, so cross-package imports stay declarative
*/
function buildRenderContext(manifest: StanzaManifest, packageName: string): Record<string, string> {
const ctx: Record<string, string> = {
appDir: manifest.appDir,
projectName: manifest.name,
packageName,
};
for (const dir of new Set(
Object.values(SLOT_PACKAGE_DIR).filter((d): d is string => d !== null),
)) {
ctx[`${dir}PackageName`] = `@${manifest.name}/${dir}`;
}
return ctx;
}
export type RevertResult = {
manifest: StanzaManifest;
touchedFiles: string[];
dryRun: boolean;
/**
* Codemods that couldn't be reverted automatically either no `revert()`
* is defined or one threw. Each entry is the codemod id; the caller
* surfaces this as "needs manual cleanup".
*/
manualCleanup: string[];
};
/**
* Replay an installed module's imperative codemods in reverse via their
* `revert()` functions. Each revert releases the regions it claimed, which
* we propagate into the manifest. Declarative reversals (files, deps, env)
* stay in `commands/remove.ts` they're region-driven and don't need the
* module to be re-loaded.
*
* Args are reconstructed by running the original `args` template strings
* through the same `renderTemplate` substitution used on apply, so reverts
* see the same concrete values (e.g. `providerImport: "@my-app/auth"`).
*/
export async function revertCodemods(args: {
projectRoot: string;
manifest: StanzaManifest;
module: Module;
adapter: ModuleAdapter;
dryRun: boolean;
}): Promise<RevertResult> {
const { projectRoot, module, adapter, dryRun } = args;
let manifest = args.manifest;
const touchedFiles = new Set<string>();
const manualCleanup: string[] = [];
const appRoot = path.join(projectRoot, manifest.appDir);
const codemods = adapter.codemods ?? [];
if (codemods.length === 0) return { manifest, touchedFiles: [], dryRun, manualCleanup };
const packageDir = SLOT_PACKAGE_DIR[module.slot];
const packageName = packageDir ? `@${manifest.name}/${packageDir}` : "";
const renderContext = buildRenderContext(manifest, packageName);
const project = lazyProject(appRoot);
const ctx = buildContext({
projectRoot,
appRoot,
manifest,
module,
adapter,
project,
touchedFiles,
dryRun,
onRelease: (file, region) => {
manifest = release(manifest, file, region);
},
});
// Reverse order: if codemod B layered on top of A's output, undo B first.
for (const invocation of codemods.toReversed()) {
const fn = CODEMOD_CATALOG[invocation.id];
if (!fn || !fn.revert) {
manualCleanup.push(invocation.id);
continue;
}
if (dryRun) continue;
try {
const renderedArgs = renderArgs(invocation.args ?? {}, renderContext);
const result = await fn.revert(ctx, renderedArgs);
result.touchedFiles.forEach((f) => touchedFiles.add(f));
} catch {
// Don't surface the exception body — the caller already prints a
// "manual cleanup" warning with the codemod id; the user can re-run
// with stack traces if they need details. Keep going so other codemods
// still get a chance to revert.
manualCleanup.push(invocation.id);
}
}
if (!dryRun) await project.save?.();
return { manifest, touchedFiles: [...touchedFiles], dryRun, manualCleanup };
}
export { RegionConflictError };
+5 -2
View File
@@ -1,7 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["node"]
"paths": {
"@/*": ["./src/*"]
},
"types": ["node", "bun"]
},
"include": ["src"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "react",
+6 -2
View File
@@ -14,11 +14,15 @@
"lint:fix": "oxlint --fix",
"fmt": "oxfmt",
"fmt:check": "oxfmt --check",
"registry:build": "turbo run registry:build --filter=@stanza/internal",
"module:new": "pnpm --filter=@stanza/internal run module:new"
"registry:build": "bun scripts/registry-build.ts",
"module:new": "bun scripts/module-new.ts"
},
"devDependencies": {
"@clack/prompts": "^1.4.0",
"@stanza/registry": "workspace:*",
"@types/node": "^25.9.1",
"kleur": "^4.1.5",
"mri": "^1.2.0",
"oxfmt": "^0.51.0",
"oxlint": "^1.66.0",
"turbo": "^2.9.14",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
+2 -2
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env bun
import { run } from "@stanza/cli";
import mri from "mri";
/**
* `pnpm create stanza my-app` lands here. We forward straight to the CLI's
* init command — no extra logic, since the wizard wants to live in one place.
@@ -7,8 +9,6 @@ import { run } from "@stanza/cli";
* The argv shape from npm's `create-` convention is the same as a normal CLI
* invocation, with the project name as the first positional arg.
*/
import mri from "mri";
const argv = mri(process.argv.slice(2), {
alias: { h: "help", v: "version" },
boolean: ["help", "version", "yes", "dry-run", "no-telemetry"],
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["node"]
-24
View File
@@ -1,24 +0,0 @@
{
"name": "@stanza/internal",
"version": "0.1.0",
"private": true,
"description": "Internal maintenance scripts for the stanza monorepo.",
"license": "MIT",
"type": "module",
"scripts": {
"registry:build": "bun run ./src/registry-build.ts",
"module:new": "bun run ./src/module-new.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@clack/prompts": "^1.4.0",
"@stanza/registry": "workspace:*",
"kleur": "^4.1.5",
"mri": "^1.2.0"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^25.9.1",
"typescript": "^6.0.3"
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["node", "bun"]
},
"include": ["src"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
+15 -25
View File
@@ -8,9 +8,21 @@ importers:
.:
devDependencies:
'@clack/prompts':
specifier: ^1.4.0
version: 1.4.0
'@stanza/registry':
specifier: workspace:*
version: link:packages/registry
'@types/node':
specifier: ^25.9.1
version: 25.9.1
kleur:
specifier: ^4.1.5
version: 4.1.5
mri:
specifier: ^1.2.0
version: 1.2.0
oxfmt:
specifier: ^0.51.0
version: 0.51.0
@@ -48,6 +60,9 @@ importers:
specifier: ^7.8.0
version: 7.8.0
devDependencies:
'@types/bun':
specifier: ^1.3.14
version: 1.3.14
'@types/node':
specifier: ^25.9.1
version: 25.9.1
@@ -208,31 +223,6 @@ importers:
specifier: ^6.0.3
version: 6.0.3
packages/internal:
dependencies:
'@clack/prompts':
specifier: ^1.4.0
version: 1.4.0
'@stanza/registry':
specifier: workspace:*
version: link:../registry
kleur:
specifier: ^4.1.5
version: 4.1.5
mri:
specifier: ^1.2.0
version: 1.2.0
devDependencies:
'@types/bun':
specifier: ^1.3.14
version: 1.3.14
'@types/node':
specifier: ^25.9.1
version: 25.9.1
typescript:
specifier: ^6.0.3
version: 6.0.3
packages/registry:
dependencies:
zod:
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"types": ["node"]
},
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["module.ts"],
"exclude": ["templates"]
}
@@ -3,9 +3,9 @@
* Scaffold a new first-party module under registry/modules/<slot>-<id>/.
*
* Usage:
* bun module:new fully interactive
* bun module:new <slot> <id> positional, prompts for the rest
* bun module:new <slot> <id> [flags] non-interactive
* bun scripts/module-new.ts fully interactive
* bun scripts/module-new.ts <slot> <id> positional, prompts for the rest
* bun scripts/module-new.ts <slot> <id> [flags] non-interactive
*
* Flags:
* --label "Display Name" Default: title-cased id
@@ -195,7 +195,7 @@ function packageJson(a: Args): string {
function tsconfig(withCodemod: boolean): string {
const cfg = {
extends: "../../../tsconfig.base.json",
extends: "../../../tsconfig.json",
...(withCodemod ? { compilerOptions: { types: ["node"] } } : {}),
include: withCodemod ? ["module.ts", "codemods/**/*.ts"] : ["module.ts"],
exclude: ["templates"],
@@ -286,7 +286,7 @@ function printHelp() {
${kleur.bold("module:new")} scaffold a new first-party stanza module
${kleur.bold("Usage")}
bun module:new [slot] [id] [flags]
bun scripts/module-new.ts [slot] [id] [flags]
${kleur.bold("Flags")}
--label "Display Name" Default: title-cased id
@@ -297,9 +297,9 @@ ${kleur.bold("Flags")}
-h, --help Show this help
${kleur.bold("Examples")}
bun module:new # fully interactive
bun module:new email resend # prompt only for label/description
bun module:new ui shadcn-radix --yes # no prompts
bun module:new auth workos --with-codemod # include codemods/index.ts stub
bun scripts/module-new.ts # fully interactive
bun scripts/module-new.ts email resend # prompt only for label/description
bun scripts/module-new.ts ui shadcn-radix --yes # no prompts
bun scripts/module-new.ts auth workos --with-codemod # include codemods/index.ts stub
`);
}
-4
View File
@@ -20,10 +20,6 @@
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"registry:build": {
"dependsOn": ["^build"],
"outputs": ["dist/registry/**"]
}
}
}