create-patties 0.0.6 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +53 -0
  3. package/bin/create-patties.ts +9 -1
  4. package/package.json +12 -3
  5. package/src/index.ts +194 -25
  6. package/src/probes.ts +28 -0
  7. package/src/prompts.ts +98 -0
  8. package/src/readme.ts +67 -0
  9. package/templates/_claude/.claude/hooks/biome-check.sh +17 -0
  10. package/templates/_claude/.claude/rules/build-time-discovery.md +17 -0
  11. package/templates/_claude/.claude/rules/bun-native.md +28 -0
  12. package/templates/_claude/.claude/rules/filesystem-routing.md +31 -0
  13. package/templates/_claude/.claude/rules/islands.md +32 -0
  14. package/templates/_claude/.claude/rules/optional-ai.md +15 -0
  15. package/templates/_claude/.claude/rules/web-standards-boundary.md +25 -0
  16. package/templates/_claude/.claude/settings.json +18 -1
  17. package/templates/_claude/.claude/skills/patties-cli/SKILL.md +113 -0
  18. package/templates/_claude/CLAUDE.md +16 -51
  19. package/templates/_codex/.codex/README.md +10 -0
  20. package/templates/_codex/.codex/rules/build-time-discovery.md +17 -0
  21. package/templates/_codex/.codex/rules/bun-native.md +28 -0
  22. package/templates/_codex/.codex/rules/filesystem-routing.md +31 -0
  23. package/templates/_codex/.codex/rules/islands.md +32 -0
  24. package/templates/_codex/.codex/rules/optional-ai.md +15 -0
  25. package/templates/_codex/.codex/rules/patties-cli.md +113 -0
  26. package/templates/_codex/.codex/rules/web-standards-boundary.md +25 -0
  27. package/templates/_codex/AGENTS.md +21 -41
  28. package/templates/default/README-template.md +160 -6
  29. package/templates/default/app/islands/TodoApp.tsx +83 -0
  30. package/templates/default/app/routes/index.tsx +22 -3
  31. package/templates/default/app/server.ts +28 -0
  32. package/templates/default/app/islands/Counter.tsx +0 -12
package/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # create-patties
2
+
3
+ ## 0.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 3b3f80d: Scaffolder DX overhaul (specs 09 + 10).
8
+
9
+ - **Tool probes** — fail fast with a clear message when `bun` is missing
10
+ from PATH; skip-and-warn when `git` is missing instead of silently
11
+ swallowing failures.
12
+ - **Interactive prompts** — when stdin is a TTY and flags were not passed,
13
+ prompt for project name, target, deploy, and agent. Add `--yes` / `-y`
14
+ to accept all defaults non-interactively.
15
+ - **`--agent` alias** — the spec-05 `--agent claude-code` form is kept as
16
+ a backwards-compatible alias for `--template claude`.
17
+ - **Expanded agent overlays** — `_claude` now ships
18
+ `.claude/settings.json` (with `bun`/`bunx`/`patties` permissions and a
19
+ Biome PostToolUse hook), `.claude/hooks/biome-check.sh` (no-ops when
20
+ Biome is not installed), and empty `.claude/agents/` /
21
+ `.claude/commands/` directories with README pointers. `_codex` now
22
+ ships a `.codex/README.md`.
23
+ - **Templated README** — `templates/default/README-template.md` is now a
24
+ real onboarding doc with `{{name}}` / `{{agent}}` / `{{target}}` /
25
+ `{{deploy}}` placeholders and HTML-comment conditional blocks. A
26
+ generic templating pass also interpolates `{{name}}` into CLAUDE.md
27
+ and AGENTS.md.
28
+
29
+ - **Todo demo template** — the default scaffold now ships a `TodoApp.tsx`
30
+ island and a route that mounts it. The README explains that buttons
31
+ don't react under `bun dev` today (dev hydration lands with framework
32
+ spec 18) and tells the user to `bun run build && bun start` to see the
33
+ island work; it also includes a "Remove the demo" section for when the
34
+ user is ready to start their real app.
35
+ - **`--blank` flag** — `bunx create-patties@latest <name> --blank`
36
+ (alias: `--empty`) scaffolds a hello-world page with no demo and no
37
+ `app/islands/` directory.
38
+ - **Demo prompt** — interactive flow asks "Include the interactive
39
+ todo demo? [Y/n]" right after the agent question.
40
+ - **Git is now opt-in** — `git init` no longer runs by default. Pass
41
+ `--git` to opt in. The success output reminds the user how to
42
+ initialise a repo when they're ready. `--no-git` is kept as a no-op
43
+ for back-compat.
44
+ - **`_claude` overlay restructured.** The empty `.claude/agents/` and
45
+ `.claude/commands/` placeholders are gone. Replaced with:
46
+ - `.claude/skills/patties-cli/SKILL.md` — a real skill teaching
47
+ Claude how to use the project CLI (`patties dev/build/start/deploy/secret`).
48
+ - `.claude/rules/` — scaffolded meta-framework knowledge:
49
+ `bun-native.md`, `web-standards-boundary.md`,
50
+ `filesystem-routing.md`, `islands.md`, `build-time-discovery.md`,
51
+ `optional-ai.md`. Mirrors the framework's own rules but tuned
52
+ for users of the framework.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # create-patties
2
+
3
+ The official scaffolder for [Patties](https://github.com/bihaviour-ai/bun-patties-framework) —
4
+ a Bun-native full-stack meta-framework with React 19 rendering.
5
+
6
+ ```sh
7
+ bunx create-patties@latest my-app
8
+ ```
9
+
10
+ Runs an interactive prompt (project name, agent overlay, demo template,
11
+ runtime target, deploy target), then scaffolds, installs, and gets you
12
+ to `bun dev` in one shot.
13
+
14
+ ## Options
15
+
16
+ ```sh
17
+ bunx create-patties@latest my-app \
18
+ --template claude|codex|none \
19
+ --target bun|edge \
20
+ --deploy cloudflare|vercel|deno|netlify|bun|none \
21
+ --blank \
22
+ --git \
23
+ --no-install \
24
+ --yes
25
+ ```
26
+
27
+ | Flag | Default | What it does |
28
+ |---|---|---|
29
+ | `--template` | `claude` | Agent-platform overlay (`claude`, `codex`, or `none`). |
30
+ | `--target` | `bun` | Runtime target written into `patties.config.ts`. |
31
+ | `--deploy` | `none` | Records the intended deploy target. |
32
+ | `--blank` / `--empty` | off | Skip the interactive todo demo; ship a hello-world page. |
33
+ | `--git` | off | Run `git init` + initial commit (opt-in). |
34
+ | `--no-install` | off | Skip `bun install`. |
35
+ | `--yes` / `-y` | off | Accept all defaults; skip prompts. |
36
+
37
+ `--agent claude-code` is retained as a back-compat alias for
38
+ `--template claude`.
39
+
40
+ ## What you get
41
+
42
+ A minimal Patties app with filesystem routing under `app/routes/`,
43
+ optional client islands under `app/islands/`, and either the `_claude`
44
+ or `_codex` agent overlay scaffolding conventions for your chosen
45
+ coding agent.
46
+
47
+ See [the Patties docs](https://bun-patties.com) for the full framework
48
+ guide.
49
+
50
+ ## Requires
51
+
52
+ Bun 1.0+. `bunx create-patties@latest` won't work under Node — install
53
+ Bun first from [bun.sh](https://bun.sh).
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env bun
2
- import { run } from "../src/index.ts";
2
+ export {};
3
3
 
4
+ if (typeof Bun === "undefined") {
5
+ console.error(
6
+ "create-patties requires Bun. Install Bun first: https://bun.sh",
7
+ );
8
+ process.exit(1);
9
+ }
10
+
11
+ const { run } = await import("../src/index.ts");
4
12
  const code = await run(process.argv.slice(2));
5
13
  process.exit(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-patties",
3
- "version": "0.0.6",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "description": "Scaffolder for new Patties projects.",
6
6
  "bin": {
@@ -13,6 +13,15 @@
13
13
  "bin",
14
14
  "src",
15
15
  "templates",
16
- "README.md"
17
- ]
16
+ "README.md",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "scripts": {
20
+ "test": "bun test",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "biome check ."
23
+ },
24
+ "engines": {
25
+ "bun": ">=1.0.0"
26
+ }
18
27
  }
package/src/index.ts CHANGED
@@ -1,15 +1,56 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { hasGit, probeTools } from "./probes.ts";
4
+ import {
5
+ type AgentTemplate,
6
+ isInteractive,
7
+ promptAgent,
8
+ promptDeploy,
9
+ promptName,
10
+ promptScaffold,
11
+ promptTarget,
12
+ } from "./prompts.ts";
13
+ import { renderTemplatesInTree } from "./readme.ts";
3
14
 
4
- type AgentTemplate = "claude" | "codex" | "none";
15
+ // Step logger visible progress so the user can see what scaffolding does.
16
+ // Honors NO_COLOR; degrades gracefully on non-TTY streams.
17
+ function colorOn(): boolean {
18
+ if (process.env.NO_COLOR) return false;
19
+ return Boolean((process.stdout as NodeJS.WriteStream).isTTY);
20
+ }
21
+ const COL = colorOn();
22
+ const c = {
23
+ dim: (s: string) => (COL ? `\x1b[2m${s}\x1b[0m` : s),
24
+ green: (s: string) => (COL ? `\x1b[32m${s}\x1b[0m` : s),
25
+ cyan: (s: string) => (COL ? `\x1b[36m${s}\x1b[0m` : s),
26
+ bold: (s: string) => (COL ? `\x1b[1m${s}\x1b[0m` : s),
27
+ };
28
+ function step(msg: string): void {
29
+ process.stdout.write(` ${c.green("✓")} ${msg}\n`);
30
+ }
31
+ function pending(msg: string): void {
32
+ process.stdout.write(` ${c.dim("…")} ${msg}\n`);
33
+ }
34
+ function header(msg: string): void {
35
+ process.stdout.write(`\n${c.bold(c.cyan("▲"))} ${msg}\n\n`);
36
+ }
37
+ function fmtMs(ms: number): string {
38
+ return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
39
+ }
5
40
 
6
41
  interface Args {
7
42
  name?: string;
8
43
  template: AgentTemplate;
44
+ templateExplicit: boolean;
9
45
  target: "bun" | "edge";
46
+ targetExplicit: boolean;
10
47
  deploy: "cloudflare" | "vercel" | "deno" | "netlify" | "bun" | "none";
48
+ deployExplicit: boolean;
11
49
  install: boolean;
12
50
  git: boolean;
51
+ yes: boolean;
52
+ scaffold: "demo" | "blank";
53
+ scaffoldExplicit: boolean;
13
54
  }
14
55
 
15
56
  const TEMPLATES_ROOT = resolve(dirname(import.meta.dir), "templates");
@@ -18,10 +59,19 @@ const VALID_TEMPLATES: AgentTemplate[] = ["claude", "codex", "none"];
18
59
 
19
60
  export async function run(argv: string[]): Promise<number> {
20
61
  const args = parseArgs(argv);
62
+ const interactive = !args.yes && isInteractive();
21
63
 
22
64
  if (!args.name) {
23
- printUsage();
24
- return 2;
65
+ if (interactive) {
66
+ args.name = promptName(undefined, isValidName);
67
+ if (!args.name) {
68
+ stderr("✗ failed to read a valid project name");
69
+ return 2;
70
+ }
71
+ } else {
72
+ printUsage();
73
+ return 2;
74
+ }
25
75
  }
26
76
 
27
77
  if (!isValidName(args.name)) {
@@ -29,6 +79,15 @@ export async function run(argv: string[]): Promise<number> {
29
79
  return 2;
30
80
  }
31
81
 
82
+ if (interactive) {
83
+ if (!args.templateExplicit) args.template = promptAgent();
84
+ if (!args.scaffoldExplicit) args.scaffold = promptScaffold();
85
+ if (!args.targetExplicit) args.target = promptTarget();
86
+ if (args.target === "edge" && !args.deployExplicit) {
87
+ args.deploy = promptDeploy();
88
+ }
89
+ }
90
+
32
91
  if (!VALID_TEMPLATES.includes(args.template)) {
33
92
  stderr(
34
93
  `✗ unknown --template "${args.template}" (expected: ${VALID_TEMPLATES.join(", ")})`,
@@ -51,36 +110,84 @@ export async function run(argv: string[]): Promise<number> {
51
110
  return 1;
52
111
  }
53
112
 
113
+ probeTools();
114
+
115
+ header(`create-patties — scaffolding ${c.bold(args.name)}`);
116
+
54
117
  await Bun.$`mkdir -p ${targetDir}`.quiet();
118
+ step(`created directory ${c.dim(targetDir)}`);
119
+
55
120
  await Bun.$`cp -R ${baseDir}/. ${targetDir}`.quiet();
121
+ step(`copied base template ${c.dim("(default)")}`);
56
122
 
57
123
  await renameTemplateFiles(targetDir);
58
124
  await writePackageJson(targetDir, args.name);
125
+ step(`wrote package.json ${c.dim(`(name: ${args.name})`)}`);
59
126
  await patchPattiesConfig(targetDir, args);
127
+ step(`patched patties.config.ts ${c.dim(`(target: ${args.target})`)}`);
60
128
 
61
129
  if (args.template !== "none") {
62
130
  const overlay = resolve(TEMPLATES_ROOT, `_${args.template}`);
63
131
  if (existsSync(overlay)) {
64
132
  await Bun.$`cp -R ${overlay}/. ${targetDir}`.quiet();
133
+ step(`applied agent overlay ${c.dim(`(${args.template})`)}`);
65
134
  }
66
135
  }
67
136
 
137
+ if (args.scaffold === "blank") {
138
+ await applyBlankScaffold(targetDir);
139
+ step("applied blank scaffold (no demo)");
140
+ } else {
141
+ step("included interactive todo demo");
142
+ }
143
+
144
+ await renderTemplatesInTree(targetDir, {
145
+ name: args.name,
146
+ agent: args.template,
147
+ target: args.target,
148
+ deploy: args.deploy,
149
+ scaffold: args.scaffold,
150
+ });
151
+ step("rendered template variables (README, AGENTS.md, …)");
152
+
68
153
  if (args.install) {
69
- await Bun.$`bun install`.cwd(targetDir).quiet().nothrow();
154
+ pending("installing dependencies (bun install)…");
155
+ const t0 = performance.now();
156
+ const proc = await Bun.$`bun install`.cwd(targetDir).quiet().nothrow();
157
+ const dt = performance.now() - t0;
158
+ if (proc.exitCode === 0) {
159
+ step(`installed dependencies ${c.dim(`(${fmtMs(dt)})`)}`);
160
+ } else {
161
+ step(
162
+ `bun install exited with code ${proc.exitCode} — you may need to retry it manually`,
163
+ );
164
+ }
165
+ } else {
166
+ step(`skipped ${c.dim("`bun install`")} (--no-install)`);
70
167
  }
71
168
 
169
+ let gitSkippedReason: string | undefined;
72
170
  if (args.git) {
73
- await Bun.$`git init`.cwd(targetDir).quiet().nothrow();
74
- await Bun.$`git add -A`.cwd(targetDir).quiet().nothrow();
75
- await Bun.$`git commit -m ${"chore: initial commit from create-patties"}`
76
- .cwd(targetDir)
77
- .quiet()
78
- .nothrow();
171
+ if (hasGit()) {
172
+ await Bun.$`git init`.cwd(targetDir).quiet().nothrow();
173
+ await Bun.$`git add -A`.cwd(targetDir).quiet().nothrow();
174
+ await Bun.$`git commit -m ${"chore: initial commit from create-patties"}`
175
+ .cwd(targetDir)
176
+ .quiet()
177
+ .nothrow();
178
+ step("initialized git and committed");
179
+ } else {
180
+ gitSkippedReason = "git-missing";
181
+ }
79
182
  }
80
183
 
81
- process.stdout.write(
82
- `\n✓ created ${args.name}\n\n cd ${args.name}\n bun dev\n`,
83
- );
184
+ const nextSteps = args.git
185
+ ? `\n cd ${args.name}\n bun dev\n`
186
+ : `\n cd ${args.name}\n bun dev\n\n # when you're ready to track this in git:\n git init && git add -A && git commit -m "initial commit"\n`;
187
+ process.stdout.write(`\n✓ created ${args.name}\n${nextSteps}`);
188
+ if (gitSkippedReason === "git-missing") {
189
+ stderr("create-patties: `git` not found — skipping `git init`.");
190
+ }
84
191
  if (args.template === "claude") {
85
192
  process.stdout.write(
86
193
  "\nClaude Code is configured (CLAUDE.md). Run `claude` in the project to start a session.\n",
@@ -96,30 +203,66 @@ export async function run(argv: string[]): Promise<number> {
96
203
  function parseArgs(argv: string[]): Args {
97
204
  const out: Args = {
98
205
  template: "claude",
206
+ templateExplicit: false,
99
207
  target: "bun",
208
+ targetExplicit: false,
100
209
  deploy: "none",
210
+ deployExplicit: false,
101
211
  install: true,
102
- git: true,
212
+ git: false,
213
+ yes: false,
214
+ scaffold: "demo",
215
+ scaffoldExplicit: false,
216
+ };
217
+ const setTemplate = (raw: string) => {
218
+ out.template = aliasTemplate(raw);
219
+ out.templateExplicit = true;
103
220
  };
104
221
  for (let i = 0; i < argv.length; i++) {
105
222
  const a = argv[i];
106
223
  if (a === undefined) continue;
107
- if (a === "--template") out.template = next(argv, ++i) as AgentTemplate;
108
- else if (a.startsWith("--template="))
109
- out.template = a.slice(11) as AgentTemplate;
110
- else if (a === "--target") out.target = next(argv, ++i) as Args["target"];
111
- else if (a.startsWith("--target="))
224
+ if (a === "--template") setTemplate(next(argv, ++i));
225
+ else if (a.startsWith("--template=")) setTemplate(a.slice(11));
226
+ // --agent: spec-05 alias kept for backwards compatibility.
227
+ else if (a === "--agent") setTemplate(next(argv, ++i));
228
+ else if (a.startsWith("--agent=")) setTemplate(a.slice(8));
229
+ else if (a === "--target") {
230
+ out.target = next(argv, ++i) as Args["target"];
231
+ out.targetExplicit = true;
232
+ } else if (a.startsWith("--target=")) {
112
233
  out.target = a.slice(9) as Args["target"];
113
- else if (a === "--deploy") out.deploy = next(argv, ++i) as Args["deploy"];
114
- else if (a.startsWith("--deploy="))
234
+ out.targetExplicit = true;
235
+ } else if (a === "--deploy") {
236
+ out.deploy = next(argv, ++i) as Args["deploy"];
237
+ out.deployExplicit = true;
238
+ } else if (a.startsWith("--deploy=")) {
115
239
  out.deploy = a.slice(9) as Args["deploy"];
116
- else if (a === "--no-install") out.install = false;
240
+ out.deployExplicit = true;
241
+ } else if (a === "--no-install") out.install = false;
242
+ else if (a === "--git") out.git = true;
243
+ // --no-git: kept as a no-op for back-compat. Git is now opt-in
244
+ // (default off) so most users don't need either flag.
117
245
  else if (a === "--no-git") out.git = false;
118
- else if (!out.name && !a.startsWith("-")) out.name = a;
246
+ else if (a === "--yes" || a === "-y") out.yes = true;
247
+ else if (a === "--blank" || a === "--empty") {
248
+ out.scaffold = "blank";
249
+ out.scaffoldExplicit = true;
250
+ } else if (a === "--demo") {
251
+ out.scaffold = "demo";
252
+ out.scaffoldExplicit = true;
253
+ } else if (!out.name && !a.startsWith("-")) out.name = a;
119
254
  }
120
255
  return out;
121
256
  }
122
257
 
258
+ // Translate spec-05 --agent values (claude-code/none) and current --template
259
+ // values (claude/codex/none) into the unified AgentTemplate type.
260
+ function aliasTemplate(raw: string): AgentTemplate {
261
+ if (raw === "claude-code") return "claude";
262
+ if (raw === "claude" || raw === "codex" || raw === "none") return raw;
263
+ return raw as AgentTemplate;
264
+ }
265
+
123
266
  function next(argv: string[], i: number): string {
124
267
  return argv[i] ?? "";
125
268
  }
@@ -144,6 +287,28 @@ async function renameTemplateFiles(dir: string): Promise<void> {
144
287
  }
145
288
  }
146
289
 
290
+ // --blank scaffold: drop the interactive demo and ship a single hello page.
291
+ // We start from the same default template and prune so we keep exactly one
292
+ // source-of-truth for things like patties.config.ts / tsconfig.json.
293
+ async function applyBlankScaffold(dir: string): Promise<void> {
294
+ await Bun.$`rm -rf ${dir}/app/islands`.quiet();
295
+ await Bun.write(
296
+ `${dir}/app/routes/index.tsx`,
297
+ `export default function Index(): JSX.Element {
298
+ return (
299
+ <main>
300
+ <h1>Hello from {{name}}</h1>
301
+ <p>
302
+ This page is server-rendered by Patties. Add more files under{" "}
303
+ <code>app/routes/</code> to grow your app.
304
+ </p>
305
+ </main>
306
+ );
307
+ }
308
+ `,
309
+ );
310
+ }
311
+
147
312
  async function writePackageJson(dir: string, name: string): Promise<void> {
148
313
  const pkg = {
149
314
  name,
@@ -202,11 +367,15 @@ Options:
202
367
  --target <bun|edge> Runtime target (default: bun)
203
368
  --deploy <cloudflare|vercel|deno|netlify|bun|none>
204
369
  --no-install Skip 'bun install'
205
- --no-git Skip 'git init'
370
+ --git Run 'git init' + initial commit (opt-in)
371
+ --yes, -y Accept all defaults, skip prompts
372
+ --blank, --empty Scaffold a hello-world page only (no demo)
373
+ --demo Scaffold the interactive todo demo (default)
206
374
 
207
375
  Examples:
208
376
  bunx create-patties@latest my-app
209
377
  bunx create-patties@latest my-app --template codex
210
- bunx create-patties@latest my-app --template none --no-git
378
+ bunx create-patties@latest my-app --template none
379
+ bunx create-patties@latest my-app --git # opt-in git init
211
380
  `);
212
381
  }
package/src/probes.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Tool probes for the scaffolder. See spec cli/10-scaffold-probes.
2
+ //
3
+ // `bun` is required to run the resulting project — if it is missing from PATH
4
+ // at scaffold time, fail before any FS writes so the user does not end up with
5
+ // half a project on disk.
6
+ // `git` is optional — if it is missing, skip the `git init` step and warn at
7
+ // the end of the scaffold rather than letting git fail mid-pipeline.
8
+
9
+ export interface ProbeIO {
10
+ stderr?: (msg: string) => void;
11
+ exit?: (code: number) => never;
12
+ }
13
+
14
+ export function probeTools(io: ProbeIO = {}): void {
15
+ const stderr = io.stderr ?? ((m: string) => process.stderr.write(`${m}\n`));
16
+ const exit = io.exit ?? ((c: number) => process.exit(c) as never);
17
+
18
+ if (!Bun.which("bun")) {
19
+ stderr(
20
+ "create-patties: `bun` not found in PATH. Install Bun from https://bun.sh and re-run.",
21
+ );
22
+ exit(1);
23
+ }
24
+ }
25
+
26
+ export function hasGit(): boolean {
27
+ return Bun.which("git") !== null;
28
+ }
package/src/prompts.ts ADDED
@@ -0,0 +1,98 @@
1
+ // Interactive prompts for create-patties. See spec cli/09-create-patties-dx
2
+ // items 3 + 5. Uses Bun's built-in prompt() — no new dependency.
3
+ //
4
+ // Non-TTY callers must supply the equivalent flags; the run loop only invokes
5
+ // these helpers when stdin is a TTY and --yes was not passed.
6
+
7
+ export type AgentTemplate = "claude" | "codex" | "none";
8
+ export type Target = "bun" | "edge";
9
+ export type Scaffold = "demo" | "blank";
10
+ export type Deploy =
11
+ | "cloudflare"
12
+ | "vercel"
13
+ | "deno"
14
+ | "netlify"
15
+ | "bun"
16
+ | "none";
17
+
18
+ export interface PromptIO {
19
+ prompt?: (q: string) => string | null;
20
+ stdout?: (msg: string) => void;
21
+ isTTY?: boolean;
22
+ }
23
+
24
+ const DEFAULT_NAME = "my-patties-app";
25
+
26
+ export function isInteractive(io: PromptIO = {}): boolean {
27
+ if (io.isTTY !== undefined) return io.isTTY;
28
+ return Boolean(process.stdin.isTTY);
29
+ }
30
+
31
+ export function promptName(
32
+ current: string | undefined,
33
+ isValid: (name: string) => boolean,
34
+ io: PromptIO = {},
35
+ ): string | undefined {
36
+ if (current) return current;
37
+ const ask = io.prompt ?? prompt;
38
+ const out = io.stdout ?? ((m: string) => process.stdout.write(m));
39
+
40
+ for (let attempt = 0; attempt < 5; attempt++) {
41
+ const answer = (ask(`Project name: (${DEFAULT_NAME}) `) ?? "").trim();
42
+ const candidate = answer === "" ? DEFAULT_NAME : answer;
43
+ if (isValid(candidate)) return candidate;
44
+ out(
45
+ `✗ invalid project name: "${candidate}" — use lowercase letters, digits, _ or -\n`,
46
+ );
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ export function promptAgent(io: PromptIO = {}): AgentTemplate {
52
+ const ask = io.prompt ?? prompt;
53
+ const answer = (
54
+ ask("Which AI coding agent will you use? [claude/codex/none] (claude) ") ??
55
+ ""
56
+ )
57
+ .trim()
58
+ .toLowerCase();
59
+ if (answer === "codex") return "codex";
60
+ if (answer === "none") return "none";
61
+ return "claude";
62
+ }
63
+
64
+ export function promptScaffold(io: PromptIO = {}): Scaffold {
65
+ const ask = io.prompt ?? prompt;
66
+ const answer = (ask("Include the interactive todo demo? [Y/n] (Y) ") ?? "")
67
+ .trim()
68
+ .toLowerCase();
69
+ if (answer === "n" || answer === "no" || answer === "blank") return "blank";
70
+ return "demo";
71
+ }
72
+
73
+ export function promptTarget(io: PromptIO = {}): Target {
74
+ const ask = io.prompt ?? prompt;
75
+ const answer = (ask("Runtime target? [bun/edge] (bun) ") ?? "")
76
+ .trim()
77
+ .toLowerCase();
78
+ return answer === "edge" ? "edge" : "bun";
79
+ }
80
+
81
+ export function promptDeploy(io: PromptIO = {}): Deploy {
82
+ const ask = io.prompt ?? prompt;
83
+ const answer = (
84
+ ask("Deploy plugin? [cloudflare/vercel/deno/netlify/bun/none] (none) ") ??
85
+ ""
86
+ )
87
+ .trim()
88
+ .toLowerCase();
89
+ const valid: Deploy[] = [
90
+ "cloudflare",
91
+ "vercel",
92
+ "deno",
93
+ "netlify",
94
+ "bun",
95
+ "none",
96
+ ];
97
+ return (valid as string[]).includes(answer) ? (answer as Deploy) : "none";
98
+ }
package/src/readme.ts ADDED
@@ -0,0 +1,67 @@
1
+ // README + template-file rendering. See spec cli/09-create-patties-dx item 2.
2
+ //
3
+ // `applyTemplate` does two passes:
4
+ // 1. Strip/keep conditional blocks delimited by:
5
+ // <!-- if:KEY=VALUE -->...<!-- /if -->
6
+ // The block is kept iff `vars[KEY] === VALUE`.
7
+ // 2. Replace `{{name}}`, `{{agent}}`, `{{target}}`, `{{deploy}}` placeholders
8
+ // with the corresponding `vars` entry.
9
+ //
10
+ // `renderTemplatesInTree` walks a scaffolded project and applies the template
11
+ // to every text file we care about (README.md, *.md, *.json, *.ts, *.tsx).
12
+ // Anything else is left untouched so we don't corrupt binary assets.
13
+
14
+ export interface TemplateVars {
15
+ name: string;
16
+ agent: "claude" | "codex" | "none";
17
+ target: "bun" | "edge";
18
+ deploy: "cloudflare" | "vercel" | "deno" | "netlify" | "bun" | "none";
19
+ scaffold: "demo" | "blank";
20
+ }
21
+
22
+ const CONDITIONAL_RE =
23
+ /<!--\s*if:([a-zA-Z_]+)=([a-zA-Z0-9_-]+)\s*-->([\s\S]*?)<!--\s*\/if\s*-->\n?/g;
24
+
25
+ const TEXT_EXT_RE =
26
+ /\.(md|mdx|json|jsonc|ts|tsx|js|jsx|sh|toml|yml|yaml|gitignore)$/;
27
+
28
+ export function applyTemplate(source: string, vars: TemplateVars): string {
29
+ const stripped = source.replace(
30
+ CONDITIONAL_RE,
31
+ (_match, key: string, value: string, body: string) => {
32
+ const v = (vars as unknown as Record<string, string>)[key];
33
+ return v === value ? body : "";
34
+ },
35
+ );
36
+ return (
37
+ stripped
38
+ .replaceAll("{{name}}", vars.name)
39
+ .replaceAll("{{agent}}", vars.agent)
40
+ .replaceAll("{{target}}", vars.target)
41
+ .replaceAll("{{deploy}}", vars.deploy)
42
+ .replaceAll("{{scaffold}}", vars.scaffold)
43
+ // Legacy placeholders from earlier overlay revisions — kept so existing
44
+ // CLAUDE.md / AGENTS.md template text keeps interpolating.
45
+ .replaceAll("{{PROJECT_NAME}}", vars.name)
46
+ .replaceAll("{{DEPLOY_TARGET}}", vars.deploy)
47
+ );
48
+ }
49
+
50
+ export async function renderTemplatesInTree(
51
+ dir: string,
52
+ vars: TemplateVars,
53
+ ): Promise<void> {
54
+ const glob = new Bun.Glob("**/*");
55
+ for await (const rel of glob.scan({ cwd: dir, onlyFiles: true, dot: true })) {
56
+ if (!TEXT_EXT_RE.test(rel) && !rel.endsWith("README.md")) continue;
57
+ const path = `${dir}/${rel}`;
58
+ const file = Bun.file(path);
59
+ const text = await file.text();
60
+ if (!hasTemplateMarkers(text)) continue;
61
+ await Bun.write(path, applyTemplate(text, vars));
62
+ }
63
+ }
64
+
65
+ function hasTemplateMarkers(text: string): boolean {
66
+ return text.includes("{{") || text.includes("<!-- if:");
67
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUse hook: run Biome on the file Claude just touched.
3
+ # No-ops cleanly when Biome is not installed so it's safe to ship by default.
4
+ set -euo pipefail
5
+
6
+ file="${CLAUDE_FILE_PATH:-${1:-}}"
7
+ if [[ -z "${file}" ]]; then exit 0; fi
8
+ case "${file}" in
9
+ *.ts|*.tsx|*.js|*.jsx|*.json|*.jsonc) ;;
10
+ *) exit 0 ;;
11
+ esac
12
+
13
+ if command -v biome >/dev/null 2>&1; then
14
+ biome check --write --no-errors-on-unmatched "${file}" || exit 2
15
+ elif command -v bunx >/dev/null 2>&1; then
16
+ bunx --bun @biomejs/biome check --write --no-errors-on-unmatched "${file}" 2>/dev/null || exit 0
17
+ fi