`bun patties add` — UI component stamper
Ship the foundation that every UI component spec in phase-1 through phase-4 depends on: a patties add subcommand that copies the canonical source for a compo…
Purpose
Ship the foundation that every UI component spec in phase-1 through phase-4 depends on: a patties add subcommand that copies the canonical source for a component (and its required helpers + peer deps) into the user's project, idempotently.
This spec does not ship any components. It ships the plumbing that components plug into. No component spec in a later phase is mergeable until this one is completed.
Public surface
patties add <component> [...components]
patties add --all
patties add --list
patties add --dry-run <component>
Behaviour:
<component>is the kebab-case name from the index in ../00-overview.md (accordion,alert-dialog,data-table, …). Numbers are not part of the name.--allstamps every component whose spec status iscompleted(skips drafts).--listprints a table ofname | phase | island | statusand exits.--dry-runprints the file list andpackage.jsondiff without writing.
Flags inherited from global CLI (--cwd, --config, --verbose) work unchanged. The command refuses to run if process.env.NODE_ENV === "production", mirroring the rule in secret.
Wiring into the framework
Mounts in bun-patties-framework as:
src/cli/commands/add.ts—export async function runAdd(argv: string[], ctx: CliContext): Promise<number>. Mirrors the shape ofrunSecret/runDeploy.src/cli/index.ts— newcase "add":in the dispatch switch, plus a line inprintHelp().src/cli/commands/add/(subdir) — implementation modules:registry.ts— the static component table (name → spec path, peer deps, island flag, helpers, kind, phase, status).stamper.ts— writes files underapp/components/ui/, never overwriting unless--force.peer-deps.ts— patchespackage.jsondependenciesblock; uses pinned versions from the registry.tokens.ts— idempotent merge intoapp/styles/tokens.css.internal.ts— installs_internal/cn.ts,_internal/slot.ts,_internal/variants.tson first use.
The framework's package exports do not gain a new entry point. Component source lives in the user's repo; the CLI is the only public touchpoint.
Component registry shape
export interface ComponentEntry {
name: string; // "alert-dialog"
spec: string; // "ui/phase-3/03-alert-dialog"
phase: 0 | 1 | 2 | 3 | 4;
kind: "primitive" | "recipe" | "provider";
island: "no" | "yes" | "subtree" | "yes-downgrade";
status: "draft" | "completed";
files: { from: string; to: string }[]; // source-of-truth paths relative to framework repo
peerDeps: Record<string, string>; // npm name -> semver range, merged into user's package.json
internalHelpers: ("cn" | "slot" | "variants")[];
tokens?: string[]; // CSS variable group keys merged into tokens.css
}
The registry is a single TS module (registry.ts), hand-maintained alongside each phase's components. No filesystem scan at runtime.
Stamping rules
- Destination is fixed:
<cwd>/app/components/ui/<name>.tsx. Recipes that produce multiple files (Data Table, Sidebar, Form) emit each file individually as declared infiles. - Never overwrite without
--force. If the destination exists, print a one-line skip notice and continue. - Helpers stamp once: each
_internal/*.tsis written only if missing. Helper files are append-only; the CLI does not rewrite them. - Peer deps are merged, not replaced: only add missing keys. If a user has a stricter range, leave it alone and warn.
- tokens.css merge is idempotent: keyed by CSS-variable block comments (
/* @patties:tokens base */). Re-running the sameaddis a no-op. - No network at install time: the CLI does not call
npm install. It only editspackage.jsonand exits with a line telling the user to runbun install.
Source-of-truth layout
Inside bun-patties-framework, canonical component source lives at:
templates/ui/<name>.tsx # single-file primitives
templates/ui/<name>/<filename>.tsx # multi-file recipes
templates/ui/_internal/{cn,slot,variants}.ts
templates/ui/tokens.css
The add command reads from these paths inside the installed patties package (resolved via import.meta.resolve("patties/package.json") and a relative templates/ui/ lookup). This keeps the spec catalog and the shipped code in lock-step: a component is "implementable" only when both the markdown spec exists in agent_specs/ui/progress/phase-N/ and the source exists under templates/ui/.
Cross-cutting test harness (foundation for every later phase)
Phase-0 also ships test utilities under bun-patties-framework/tests/ui/:
renderStatic(component)— wrapsrenderToReadableStreamwith the project's defaulttokens.cssand returns the resulting HTML string.hydrate(component)— boots the island bundle in ahappy-dom(or Bun's built-in DOM) environment and asserts no console errors during hydration.assertJsonProps(component, props)— fails if any prop crossing the island boundary failsJSON.stringifyround-trip.
These three helpers are what every component-level "Acceptance criteria" in later phases will reference. Their public signatures are frozen by this spec.
Acceptance criteria
patties add --listprints all 60 components from ../00-overview.md plus their currentstatus.patties add alert(after the alert spec ships in phase-1) writes exactlyapp/components/ui/alert.tsxand any required_internal/*files, patchespackage.json, and mergestokens.css. A second invocation is a no-op.patties add unknown-nameexits non-zero withunknown component: unknown-name.patties add --dry-run badgewrites nothing but prints the would-be file list andpackage.jsondiff.- Running
patties addfrom a directory without apackage.jsonexits non-zero withnot a Patties project (no package.json found at <cwd>). tests/ui/renderStatic.test.tsandtests/ui/hydrate.test.tsexist and pass against a fixture component shipped undertests/ui/__fixtures__/.bun run check(Biome + tsc) passes on every new file. ThePostToolUsehook from framework conventions gates this.- The command is registered in
printHelp()and listed inREADME.mdunder "Commands".
Out of scope (deferred to later specs)
- Per-component spec authoring — each phase's components own their own spec files.
patties remove <component>— additive-only for now.patties update <component>(re-stamp from new spec version) — deferred to a phase-5 maintenance spec.- Theming editor / token UI — userland concern.
- Registry-driven plugin components (third-party
patties addproviders) — out of scope until the core 60 are shipped.
Open questions
- Should
--forcebe a per-file confirmation (interactive) or all-or-nothing? Default proposal: all-or-nothing, since the CLI is meant to be re-runnable in CI. - Where does the registry live long-term — hand-maintained TS, or generated from frontmatter in
agent_specs/ui/progress/**/*.md? Phase-0 ships hand-maintained; revisit once 20+ components are completed.