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.
- package/CHANGELOG.md +52 -0
- package/README.md +53 -0
- package/bin/create-patties.ts +9 -1
- package/package.json +12 -3
- package/src/index.ts +194 -25
- package/src/probes.ts +28 -0
- package/src/prompts.ts +98 -0
- package/src/readme.ts +67 -0
- package/templates/_claude/.claude/hooks/biome-check.sh +17 -0
- package/templates/_claude/.claude/rules/build-time-discovery.md +17 -0
- package/templates/_claude/.claude/rules/bun-native.md +28 -0
- package/templates/_claude/.claude/rules/filesystem-routing.md +31 -0
- package/templates/_claude/.claude/rules/islands.md +32 -0
- package/templates/_claude/.claude/rules/optional-ai.md +15 -0
- package/templates/_claude/.claude/rules/web-standards-boundary.md +25 -0
- package/templates/_claude/.claude/settings.json +18 -1
- package/templates/_claude/.claude/skills/patties-cli/SKILL.md +113 -0
- package/templates/_claude/CLAUDE.md +16 -51
- package/templates/_codex/.codex/README.md +10 -0
- package/templates/_codex/.codex/rules/build-time-discovery.md +17 -0
- package/templates/_codex/.codex/rules/bun-native.md +28 -0
- package/templates/_codex/.codex/rules/filesystem-routing.md +31 -0
- package/templates/_codex/.codex/rules/islands.md +32 -0
- package/templates/_codex/.codex/rules/optional-ai.md +15 -0
- package/templates/_codex/.codex/rules/patties-cli.md +113 -0
- package/templates/_codex/.codex/rules/web-standards-boundary.md +25 -0
- package/templates/_codex/AGENTS.md +21 -41
- package/templates/default/README-template.md +160 -6
- package/templates/default/app/islands/TodoApp.tsx +83 -0
- package/templates/default/app/routes/index.tsx +22 -3
- package/templates/default/app/server.ts +28 -0
- 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).
|
package/bin/create-patties.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
`\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:
|
|
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")
|
|
108
|
-
else if (a.startsWith("--template="))
|
|
109
|
-
|
|
110
|
-
else if (a === "--
|
|
111
|
-
else if (a.startsWith("--
|
|
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
|
-
|
|
114
|
-
else if (a
|
|
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
|
-
|
|
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 (
|
|
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
|
-
--
|
|
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
|
|
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
|