create-patties 0.0.6 → 0.0.8

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 +7 -1
  4. package/package.json +7 -3
  5. package/src/index.ts +143 -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 +14 -3
  31. package/templates/default/app/server.ts +25 -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,11 @@
1
1
  #!/usr/bin/env bun
2
- import { run } from "../src/index.ts";
2
+ if (typeof Bun === "undefined") {
3
+ console.error(
4
+ "create-patties requires Bun. Install Bun first: https://bun.sh",
5
+ );
6
+ process.exit(1);
7
+ }
3
8
 
9
+ const { run } = await import("../src/index.ts");
4
10
  const code = await run(process.argv.slice(2));
5
11
  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.8",
4
4
  "type": "module",
5
5
  "description": "Scaffolder for new Patties projects.",
6
6
  "bin": {
@@ -13,6 +13,10 @@
13
13
  "bin",
14
14
  "src",
15
15
  "templates",
16
- "README.md"
17
- ]
16
+ "README.md",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "engines": {
20
+ "bun": ">=1.0.0"
21
+ }
18
22
  }
package/src/index.ts CHANGED
@@ -1,15 +1,30 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
-
4
- type AgentTemplate = "claude" | "codex" | "none";
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";
5
14
 
6
15
  interface Args {
7
16
  name?: string;
8
17
  template: AgentTemplate;
18
+ templateExplicit: boolean;
9
19
  target: "bun" | "edge";
20
+ targetExplicit: boolean;
10
21
  deploy: "cloudflare" | "vercel" | "deno" | "netlify" | "bun" | "none";
22
+ deployExplicit: boolean;
11
23
  install: boolean;
12
24
  git: boolean;
25
+ yes: boolean;
26
+ scaffold: "demo" | "blank";
27
+ scaffoldExplicit: boolean;
13
28
  }
14
29
 
15
30
  const TEMPLATES_ROOT = resolve(dirname(import.meta.dir), "templates");
@@ -18,10 +33,19 @@ const VALID_TEMPLATES: AgentTemplate[] = ["claude", "codex", "none"];
18
33
 
19
34
  export async function run(argv: string[]): Promise<number> {
20
35
  const args = parseArgs(argv);
36
+ const interactive = !args.yes && isInteractive();
21
37
 
22
38
  if (!args.name) {
23
- printUsage();
24
- return 2;
39
+ if (interactive) {
40
+ args.name = promptName(undefined, isValidName);
41
+ if (!args.name) {
42
+ stderr("✗ failed to read a valid project name");
43
+ return 2;
44
+ }
45
+ } else {
46
+ printUsage();
47
+ return 2;
48
+ }
25
49
  }
26
50
 
27
51
  if (!isValidName(args.name)) {
@@ -29,6 +53,15 @@ export async function run(argv: string[]): Promise<number> {
29
53
  return 2;
30
54
  }
31
55
 
56
+ if (interactive) {
57
+ if (!args.templateExplicit) args.template = promptAgent();
58
+ if (!args.scaffoldExplicit) args.scaffold = promptScaffold();
59
+ if (!args.targetExplicit) args.target = promptTarget();
60
+ if (args.target === "edge" && !args.deployExplicit) {
61
+ args.deploy = promptDeploy();
62
+ }
63
+ }
64
+
32
65
  if (!VALID_TEMPLATES.includes(args.template)) {
33
66
  stderr(
34
67
  `✗ unknown --template "${args.template}" (expected: ${VALID_TEMPLATES.join(", ")})`,
@@ -51,6 +84,8 @@ export async function run(argv: string[]): Promise<number> {
51
84
  return 1;
52
85
  }
53
86
 
87
+ probeTools();
88
+
54
89
  await Bun.$`mkdir -p ${targetDir}`.quiet();
55
90
  await Bun.$`cp -R ${baseDir}/. ${targetDir}`.quiet();
56
91
 
@@ -65,22 +100,43 @@ export async function run(argv: string[]): Promise<number> {
65
100
  }
66
101
  }
67
102
 
103
+ if (args.scaffold === "blank") {
104
+ await applyBlankScaffold(targetDir);
105
+ }
106
+
107
+ await renderTemplatesInTree(targetDir, {
108
+ name: args.name,
109
+ agent: args.template,
110
+ target: args.target,
111
+ deploy: args.deploy,
112
+ scaffold: args.scaffold,
113
+ });
114
+
68
115
  if (args.install) {
69
116
  await Bun.$`bun install`.cwd(targetDir).quiet().nothrow();
70
117
  }
71
118
 
119
+ let gitSkippedReason: string | undefined;
72
120
  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();
121
+ if (hasGit()) {
122
+ await Bun.$`git init`.cwd(targetDir).quiet().nothrow();
123
+ await Bun.$`git add -A`.cwd(targetDir).quiet().nothrow();
124
+ await Bun.$`git commit -m ${"chore: initial commit from create-patties"}`
125
+ .cwd(targetDir)
126
+ .quiet()
127
+ .nothrow();
128
+ } else {
129
+ gitSkippedReason = "git-missing";
130
+ }
79
131
  }
80
132
 
81
- process.stdout.write(
82
- `\n✓ created ${args.name}\n\n cd ${args.name}\n bun dev\n`,
83
- );
133
+ const nextSteps = args.git
134
+ ? `\n cd ${args.name}\n bun dev\n`
135
+ : `\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`;
136
+ process.stdout.write(`\n✓ created ${args.name}\n${nextSteps}`);
137
+ if (gitSkippedReason === "git-missing") {
138
+ stderr("create-patties: `git` not found — skipping `git init`.");
139
+ }
84
140
  if (args.template === "claude") {
85
141
  process.stdout.write(
86
142
  "\nClaude Code is configured (CLAUDE.md). Run `claude` in the project to start a session.\n",
@@ -96,30 +152,66 @@ export async function run(argv: string[]): Promise<number> {
96
152
  function parseArgs(argv: string[]): Args {
97
153
  const out: Args = {
98
154
  template: "claude",
155
+ templateExplicit: false,
99
156
  target: "bun",
157
+ targetExplicit: false,
100
158
  deploy: "none",
159
+ deployExplicit: false,
101
160
  install: true,
102
- git: true,
161
+ git: false,
162
+ yes: false,
163
+ scaffold: "demo",
164
+ scaffoldExplicit: false,
165
+ };
166
+ const setTemplate = (raw: string) => {
167
+ out.template = aliasTemplate(raw);
168
+ out.templateExplicit = true;
103
169
  };
104
170
  for (let i = 0; i < argv.length; i++) {
105
171
  const a = argv[i];
106
172
  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="))
173
+ if (a === "--template") setTemplate(next(argv, ++i));
174
+ else if (a.startsWith("--template=")) setTemplate(a.slice(11));
175
+ // --agent: spec-05 alias kept for backwards compatibility.
176
+ else if (a === "--agent") setTemplate(next(argv, ++i));
177
+ else if (a.startsWith("--agent=")) setTemplate(a.slice(8));
178
+ else if (a === "--target") {
179
+ out.target = next(argv, ++i) as Args["target"];
180
+ out.targetExplicit = true;
181
+ } else if (a.startsWith("--target=")) {
112
182
  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="))
183
+ out.targetExplicit = true;
184
+ } else if (a === "--deploy") {
185
+ out.deploy = next(argv, ++i) as Args["deploy"];
186
+ out.deployExplicit = true;
187
+ } else if (a.startsWith("--deploy=")) {
115
188
  out.deploy = a.slice(9) as Args["deploy"];
116
- else if (a === "--no-install") out.install = false;
189
+ out.deployExplicit = true;
190
+ } else if (a === "--no-install") out.install = false;
191
+ else if (a === "--git") out.git = true;
192
+ // --no-git: kept as a no-op for back-compat. Git is now opt-in
193
+ // (default off) so most users don't need either flag.
117
194
  else if (a === "--no-git") out.git = false;
118
- else if (!out.name && !a.startsWith("-")) out.name = a;
195
+ else if (a === "--yes" || a === "-y") out.yes = true;
196
+ else if (a === "--blank" || a === "--empty") {
197
+ out.scaffold = "blank";
198
+ out.scaffoldExplicit = true;
199
+ } else if (a === "--demo") {
200
+ out.scaffold = "demo";
201
+ out.scaffoldExplicit = true;
202
+ } else if (!out.name && !a.startsWith("-")) out.name = a;
119
203
  }
120
204
  return out;
121
205
  }
122
206
 
207
+ // Translate spec-05 --agent values (claude-code/none) and current --template
208
+ // values (claude/codex/none) into the unified AgentTemplate type.
209
+ function aliasTemplate(raw: string): AgentTemplate {
210
+ if (raw === "claude-code") return "claude";
211
+ if (raw === "claude" || raw === "codex" || raw === "none") return raw;
212
+ return raw as AgentTemplate;
213
+ }
214
+
123
215
  function next(argv: string[], i: number): string {
124
216
  return argv[i] ?? "";
125
217
  }
@@ -144,6 +236,28 @@ async function renameTemplateFiles(dir: string): Promise<void> {
144
236
  }
145
237
  }
146
238
 
239
+ // --blank scaffold: drop the interactive demo and ship a single hello page.
240
+ // We start from the same default template and prune so we keep exactly one
241
+ // source-of-truth for things like patties.config.ts / tsconfig.json.
242
+ async function applyBlankScaffold(dir: string): Promise<void> {
243
+ await Bun.$`rm -rf ${dir}/app/islands`.quiet();
244
+ await Bun.write(
245
+ `${dir}/app/routes/index.tsx`,
246
+ `export default function Index(): JSX.Element {
247
+ return (
248
+ <main>
249
+ <h1>Hello from {{name}}</h1>
250
+ <p>
251
+ This page is server-rendered by Patties. Add more files under{" "}
252
+ <code>app/routes/</code> to grow your app.
253
+ </p>
254
+ </main>
255
+ );
256
+ }
257
+ `,
258
+ );
259
+ }
260
+
147
261
  async function writePackageJson(dir: string, name: string): Promise<void> {
148
262
  const pkg = {
149
263
  name,
@@ -202,11 +316,15 @@ Options:
202
316
  --target <bun|edge> Runtime target (default: bun)
203
317
  --deploy <cloudflare|vercel|deno|netlify|bun|none>
204
318
  --no-install Skip 'bun install'
205
- --no-git Skip 'git init'
319
+ --git Run 'git init' + initial commit (opt-in)
320
+ --yes, -y Accept all defaults, skip prompts
321
+ --blank, --empty Scaffold a hello-world page only (no demo)
322
+ --demo Scaffold the interactive todo demo (default)
206
323
 
207
324
  Examples:
208
325
  bunx create-patties@latest my-app
209
326
  bunx create-patties@latest my-app --template codex
210
- bunx create-patties@latest my-app --template none --no-git
327
+ bunx create-patties@latest my-app --template none
328
+ bunx create-patties@latest my-app --git # opt-in git init
211
329
  `);
212
330
  }
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
@@ -0,0 +1,17 @@
1
+ # Build-time discovery rule
2
+
3
+ Routes, islands, agents, and tools are discovered from the filesystem
4
+ at **build time**, not at request time.
5
+
6
+ - `patties build` scans `app/routes/`, `app/islands/`, `app/agents/`,
7
+ `app/tools/`, and `app/jobs/` and inlines the discovered tables into
8
+ the server bundle. The production server never calls `Bun.Glob` /
9
+ `scanRoutes` / `scanIslands`.
10
+ - This is what makes Patties cold-start fast and adapter-portable
11
+ (edge runtimes that disallow filesystem access still work).
12
+ - **Dev mode is the exception.** The dev server re-scans on file
13
+ change to drive HMR.
14
+ - **Don't break the rule:** never write code that calls `Bun.Glob` or
15
+ reads `app/**` at request time. If you find yourself wanting to,
16
+ the answer is probably a build-time macro or a config option in
17
+ `patties.config.ts`.
@@ -0,0 +1,28 @@
1
+ # Bun-native rule
2
+
3
+ This project runs on Bun. Reach for Bun primitives first; do not
4
+ reintroduce Node-era replacements just because a familiar library
5
+ provides them.
6
+
7
+ - **HTTP server:** `Bun.serve`. The framework already wires it — do
8
+ not add Express / Fastify / Hono / `node:http`.
9
+ - **Filesystem discovery:** `Bun.Glob`. Do not pull in `fast-glob`,
10
+ `globby`, or `chokidar`.
11
+ - **Bundling:** `Bun.build`. The framework runs it for you at
12
+ `patties build`; you should not add Webpack / Rollup / esbuild /
13
+ Vite / tsup.
14
+ - **File I/O:** `Bun.file(path).text()` / `.json()` / `.bytes()` and
15
+ `Bun.write(path, data)` before reaching for `node:fs`.
16
+ - **Hashing / crypto:** `Bun.CryptoHasher`, `Bun.password`. Avoid
17
+ `crypto` / `bcrypt` unless you have a real reason.
18
+ - **Databases:** `bun:sqlite`, `Bun.sql` (Postgres), `Bun.RedisClient`,
19
+ `Bun.S3Client` before reaching for npm wrappers.
20
+ - **Environment:** `Bun.env`. The framework injects the public subset
21
+ declared in `patties.config.ts#env.public`; secrets live in the OS
22
+ keychain (see `patties-cli` skill).
23
+ - **Tests:** `bun test`. Do not add Jest or Vitest.
24
+ - **Package manager:** `bun` / `bunx`. Do not run `npm` / `npx` /
25
+ `yarn` / `pnpm` in this project.
26
+
27
+ If you genuinely need a Node-only library, that's fine — Bun's Node
28
+ compatibility is strong. But default to Bun primitives.