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.
- package/CHANGELOG.md +52 -0
- package/README.md +53 -0
- package/bin/create-patties.ts +7 -1
- package/package.json +7 -3
- package/src/index.ts +143 -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 +14 -3
- package/templates/default/app/server.ts +25 -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,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
`\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:
|
|
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")
|
|
108
|
-
else if (a.startsWith("--template="))
|
|
109
|
-
|
|
110
|
-
else if (a === "--
|
|
111
|
-
else if (a.startsWith("--
|
|
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
|
-
|
|
114
|
-
else if (a
|
|
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
|
-
|
|
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 (
|
|
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
|
-
--
|
|
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
|
|
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.
|