@pyreon/create-zero 0.14.0 → 0.16.0
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/README.md +85 -22
- package/bin/create-pyreon-app.js +2 -0
- package/lib/index.js +1254 -191
- package/package.json +5 -2
- package/templates/{default → app}/src/routes/_layout.tsx +5 -2
- package/templates/{default → app}/src/routes/posts/[id].tsx +14 -0
- package/templates/blog/.mcp.json +8 -0
- package/templates/blog/CLAUDE.md +59 -0
- package/templates/blog/index.html +18 -0
- package/templates/blog/public/favicon.svg +4 -0
- package/templates/blog/src/content/posts/static-vs-ssr.tsx +54 -0
- package/templates/blog/src/content/posts/welcome.tsx +70 -0
- package/templates/blog/src/content/posts/why-signals.tsx +57 -0
- package/templates/blog/src/entry-client.ts +5 -0
- package/templates/blog/src/global.css +292 -0
- package/templates/blog/src/lib/posts.ts +45 -0
- package/templates/blog/src/routes/_layout.tsx +40 -0
- package/templates/blog/src/routes/about.tsx +28 -0
- package/templates/blog/src/routes/api/rss.ts +55 -0
- package/templates/blog/src/routes/blog/[slug].tsx +73 -0
- package/templates/blog/src/routes/blog/index.tsx +43 -0
- package/templates/blog/src/routes/index.tsx +52 -0
- package/templates/blog/tsconfig.json +16 -0
- package/templates/dashboard/.mcp.json +8 -0
- package/templates/dashboard/CLAUDE.md +50 -0
- package/templates/dashboard/index.html +16 -0
- package/templates/dashboard/public/favicon.svg +4 -0
- package/templates/dashboard/src/entry-client.ts +5 -0
- package/templates/dashboard/src/global.css +451 -0
- package/templates/dashboard/src/lib/auth.ts +106 -0
- package/templates/dashboard/src/lib/db.ts +118 -0
- package/templates/dashboard/src/routes/_layout.tsx +28 -0
- package/templates/dashboard/src/routes/api/signout.ts +15 -0
- package/templates/dashboard/src/routes/app/_layout.tsx +76 -0
- package/templates/dashboard/src/routes/app/dashboard.tsx +92 -0
- package/templates/dashboard/src/routes/app/invoices/[id].tsx +214 -0
- package/templates/dashboard/src/routes/app/invoices/index.tsx +61 -0
- package/templates/dashboard/src/routes/app/settings/account.tsx +31 -0
- package/templates/dashboard/src/routes/app/settings/billing.tsx +28 -0
- package/templates/dashboard/src/routes/app/settings/index.tsx +29 -0
- package/templates/dashboard/src/routes/app/users.tsx +50 -0
- package/templates/dashboard/src/routes/index.tsx +40 -0
- package/templates/dashboard/src/routes/login.tsx +79 -0
- package/templates/dashboard/src/routes/signup.tsx +78 -0
- package/templates/dashboard/tsconfig.json +16 -0
- package/lib/index.js.map +0 -1
- /package/templates/{default → app}/.mcp.json +0 -0
- /package/templates/{default → app}/CLAUDE.md +0 -0
- /package/templates/{default → app}/index.html +0 -0
- /package/templates/{default → app}/public/favicon.svg +0 -0
- /package/templates/{default → app}/src/entry-client.ts +0 -0
- /package/templates/{default → app}/src/features/posts.ts +0 -0
- /package/templates/{default → app}/src/global.css +0 -0
- /package/templates/{default → app}/src/routes/(admin)/dashboard.tsx +0 -0
- /package/templates/{default → app}/src/routes/_error.tsx +0 -0
- /package/templates/{default → app}/src/routes/_loading.tsx +0 -0
- /package/templates/{default → app}/src/routes/about.tsx +0 -0
- /package/templates/{default → app}/src/routes/api/health.ts +0 -0
- /package/templates/{default → app}/src/routes/api/posts.ts +0 -0
- /package/templates/{default → app}/src/routes/counter.tsx +0 -0
- /package/templates/{default → app}/src/routes/index.tsx +0 -0
- /package/templates/{default → app}/src/routes/posts/index.tsx +0 -0
- /package/templates/{default → app}/src/routes/posts/new.tsx +0 -0
- /package/templates/{default → app}/src/stores/app.ts +0 -0
- /package/templates/{default → app}/tsconfig.json +0 -0
package/lib/index.js
CHANGED
|
@@ -1,9 +1,172 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { cp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
1
|
import * as p from "@clack/prompts";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { cp, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
|
|
6
|
-
//#region src/
|
|
6
|
+
//#region src/args.ts
|
|
7
|
+
const TEMPLATE_VALUES = [
|
|
8
|
+
"app",
|
|
9
|
+
"blog",
|
|
10
|
+
"dashboard"
|
|
11
|
+
];
|
|
12
|
+
const ADAPTER_VALUES = [
|
|
13
|
+
"vercel",
|
|
14
|
+
"cloudflare",
|
|
15
|
+
"netlify",
|
|
16
|
+
"node",
|
|
17
|
+
"bun",
|
|
18
|
+
"static"
|
|
19
|
+
];
|
|
20
|
+
const MODE_VALUES = [
|
|
21
|
+
"ssr-stream",
|
|
22
|
+
"ssr-string",
|
|
23
|
+
"ssg",
|
|
24
|
+
"spa"
|
|
25
|
+
];
|
|
26
|
+
const INTEGRATION_VALUES = ["supabase", "email"];
|
|
27
|
+
const AI_VALUES = [
|
|
28
|
+
"mcp",
|
|
29
|
+
"claude",
|
|
30
|
+
"cursor",
|
|
31
|
+
"copilot",
|
|
32
|
+
"agents"
|
|
33
|
+
];
|
|
34
|
+
const COMPAT_VALUES = [
|
|
35
|
+
"none",
|
|
36
|
+
"react",
|
|
37
|
+
"vue",
|
|
38
|
+
"solid",
|
|
39
|
+
"preact"
|
|
40
|
+
];
|
|
41
|
+
const PKG_STRATEGY_VALUES = ["meta", "individual"];
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const out = {
|
|
44
|
+
name: void 0,
|
|
45
|
+
yes: false,
|
|
46
|
+
help: false,
|
|
47
|
+
template: void 0,
|
|
48
|
+
adapter: void 0,
|
|
49
|
+
mode: void 0,
|
|
50
|
+
features: void 0,
|
|
51
|
+
integrations: void 0,
|
|
52
|
+
ai: void 0,
|
|
53
|
+
compat: void 0,
|
|
54
|
+
packageStrategy: void 0,
|
|
55
|
+
lint: void 0
|
|
56
|
+
};
|
|
57
|
+
for (let i = 0; i < argv.length; i++) {
|
|
58
|
+
const a = argv[i];
|
|
59
|
+
if (a === void 0) continue;
|
|
60
|
+
if (a.startsWith("--") || a === "-h") {
|
|
61
|
+
const eq = a.indexOf("=");
|
|
62
|
+
const key = eq >= 0 ? a.slice(2, eq) : a.startsWith("--") ? a.slice(2) : "h";
|
|
63
|
+
const inlineValue = eq >= 0 ? a.slice(eq + 1) : void 0;
|
|
64
|
+
const consumeValue = () => {
|
|
65
|
+
if (inlineValue !== void 0) return inlineValue;
|
|
66
|
+
const next = argv[i + 1];
|
|
67
|
+
if (next === void 0 || next.startsWith("--")) return void 0;
|
|
68
|
+
i++;
|
|
69
|
+
return next;
|
|
70
|
+
};
|
|
71
|
+
switch (key) {
|
|
72
|
+
case "help":
|
|
73
|
+
case "h":
|
|
74
|
+
out.help = true;
|
|
75
|
+
break;
|
|
76
|
+
case "yes":
|
|
77
|
+
out.yes = true;
|
|
78
|
+
break;
|
|
79
|
+
case "lint":
|
|
80
|
+
out.lint = true;
|
|
81
|
+
break;
|
|
82
|
+
case "no-lint":
|
|
83
|
+
out.lint = false;
|
|
84
|
+
break;
|
|
85
|
+
case "template":
|
|
86
|
+
out.template = pickEnum(consumeValue(), TEMPLATE_VALUES, "--template");
|
|
87
|
+
break;
|
|
88
|
+
case "adapter":
|
|
89
|
+
out.adapter = pickEnum(consumeValue(), ADAPTER_VALUES, "--adapter");
|
|
90
|
+
break;
|
|
91
|
+
case "mode":
|
|
92
|
+
out.mode = pickEnum(consumeValue(), MODE_VALUES, "--mode");
|
|
93
|
+
break;
|
|
94
|
+
case "features":
|
|
95
|
+
out.features = parseCsv(consumeValue());
|
|
96
|
+
break;
|
|
97
|
+
case "integrations":
|
|
98
|
+
out.integrations = parseEnumCsv(consumeValue(), INTEGRATION_VALUES, "--integrations");
|
|
99
|
+
break;
|
|
100
|
+
case "ai":
|
|
101
|
+
out.ai = parseEnumCsv(consumeValue(), AI_VALUES, "--ai");
|
|
102
|
+
break;
|
|
103
|
+
case "compat":
|
|
104
|
+
out.compat = pickEnum(consumeValue(), COMPAT_VALUES, "--compat");
|
|
105
|
+
break;
|
|
106
|
+
case "pm":
|
|
107
|
+
case "packages":
|
|
108
|
+
case "package-strategy":
|
|
109
|
+
out.packageStrategy = pickEnum(consumeValue(), PKG_STRATEGY_VALUES, "--packages");
|
|
110
|
+
break;
|
|
111
|
+
default: throw new Error(`Unknown flag: ${a}. Run with --help for usage.`);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (out.name === void 0) out.name = a;
|
|
116
|
+
else throw new Error(`Unexpected extra positional argument: ${a}`);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
function parseCsv(raw) {
|
|
121
|
+
if (raw === void 0) return void 0;
|
|
122
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
function parseEnumCsv(raw, allowed, flag) {
|
|
125
|
+
const parts = parseCsv(raw);
|
|
126
|
+
if (parts === void 0) return void 0;
|
|
127
|
+
for (const p of parts) if (!allowed.includes(p)) throw new Error(`Invalid value "${p}" for ${flag}. Expected one of: ${allowed.join(", ")}.`);
|
|
128
|
+
return parts;
|
|
129
|
+
}
|
|
130
|
+
function pickEnum(raw, allowed, flag) {
|
|
131
|
+
if (raw === void 0) return void 0;
|
|
132
|
+
if (!allowed.includes(raw)) throw new Error(`Invalid value "${raw}" for ${flag}. Expected one of: ${allowed.join(", ")}.`);
|
|
133
|
+
return raw;
|
|
134
|
+
}
|
|
135
|
+
function helpText(invokedAs) {
|
|
136
|
+
return `Usage: ${invokedAs} [name] [flags]
|
|
137
|
+
|
|
138
|
+
Scaffold a new Pyreon Zero project.
|
|
139
|
+
|
|
140
|
+
Templates:
|
|
141
|
+
--template <id> app | blog | dashboard
|
|
142
|
+
|
|
143
|
+
Deployment:
|
|
144
|
+
--adapter <id> vercel | cloudflare | netlify | node | bun | static
|
|
145
|
+
|
|
146
|
+
Rendering:
|
|
147
|
+
--mode <id> ssr-stream | ssr-string | ssg | spa
|
|
148
|
+
|
|
149
|
+
Features (csv):
|
|
150
|
+
--features <list> store,query,forms,table,virtual,i18n,charts,…
|
|
151
|
+
--integrations <list> supabase,email
|
|
152
|
+
--ai <list> mcp,claude,cursor,copilot,agents
|
|
153
|
+
|
|
154
|
+
Other:
|
|
155
|
+
--compat <id> none | react | vue | solid | preact
|
|
156
|
+
--packages <id> meta | individual
|
|
157
|
+
--lint / --no-lint toggle @pyreon/lint
|
|
158
|
+
--yes skip prompts, accept defaults
|
|
159
|
+
--help, -h show this help
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
${invokedAs} my-app
|
|
163
|
+
${invokedAs} my-app --template dashboard --adapter vercel --integrations supabase,email --yes
|
|
164
|
+
${invokedAs} my-blog --template blog --adapter cloudflare --yes
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/templates.ts
|
|
7
170
|
const FEATURES = {
|
|
8
171
|
store: {
|
|
9
172
|
label: "State Management (@pyreon/store)",
|
|
@@ -103,168 +266,1032 @@ const FEATURES = {
|
|
|
103
266
|
deps: ["@pyreon/rx"]
|
|
104
267
|
}
|
|
105
268
|
};
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
269
|
+
const TEMPLATES = {
|
|
270
|
+
app: {
|
|
271
|
+
id: "app",
|
|
272
|
+
label: "App",
|
|
273
|
+
hint: "full-featured starter — counter, posts, layout, admin route group",
|
|
274
|
+
defaultMode: "ssr-stream",
|
|
275
|
+
defaultFeatures: [
|
|
276
|
+
"store",
|
|
277
|
+
"query",
|
|
278
|
+
"forms"
|
|
279
|
+
],
|
|
280
|
+
forcesMode: false,
|
|
281
|
+
adapters: [
|
|
282
|
+
"vercel",
|
|
283
|
+
"cloudflare",
|
|
284
|
+
"netlify",
|
|
285
|
+
"node",
|
|
286
|
+
"bun",
|
|
287
|
+
"static"
|
|
288
|
+
],
|
|
289
|
+
defaultAdapter: "vercel",
|
|
290
|
+
defaultIntegrations: []
|
|
291
|
+
},
|
|
292
|
+
blog: {
|
|
293
|
+
id: "blog",
|
|
294
|
+
label: "Blog",
|
|
295
|
+
hint: "SSG markdown blog with RSS feed and SEO",
|
|
296
|
+
defaultMode: "ssg",
|
|
297
|
+
defaultFeatures: [],
|
|
298
|
+
forcesMode: true,
|
|
299
|
+
adapters: [
|
|
300
|
+
"static",
|
|
301
|
+
"vercel",
|
|
302
|
+
"cloudflare",
|
|
303
|
+
"netlify"
|
|
304
|
+
],
|
|
305
|
+
defaultAdapter: "static",
|
|
306
|
+
defaultIntegrations: []
|
|
307
|
+
},
|
|
308
|
+
dashboard: {
|
|
309
|
+
id: "dashboard",
|
|
310
|
+
label: "Dashboard",
|
|
311
|
+
hint: "SaaS-shape SSR app — auth-gated routes, full integration suite available",
|
|
312
|
+
defaultMode: "ssr-stream",
|
|
313
|
+
defaultFeatures: [
|
|
314
|
+
"store",
|
|
315
|
+
"query",
|
|
316
|
+
"forms",
|
|
317
|
+
"table"
|
|
318
|
+
],
|
|
319
|
+
forcesMode: true,
|
|
320
|
+
adapters: [
|
|
321
|
+
"vercel",
|
|
322
|
+
"cloudflare",
|
|
323
|
+
"netlify",
|
|
324
|
+
"node",
|
|
325
|
+
"bun"
|
|
326
|
+
],
|
|
327
|
+
defaultAdapter: "vercel",
|
|
328
|
+
defaultIntegrations: ["supabase", "email"]
|
|
112
329
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
330
|
+
};
|
|
331
|
+
const TEMPLATES_ROOT = resolve(import.meta.dirname, "../templates");
|
|
332
|
+
function templateDir(id) {
|
|
333
|
+
return resolve(TEMPLATES_ROOT, id);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/prompts.ts
|
|
338
|
+
const ADAPTER_LABELS = {
|
|
339
|
+
vercel: {
|
|
340
|
+
label: "Vercel",
|
|
341
|
+
hint: "serverless / edge — vercel.json + one-click deploy badge"
|
|
342
|
+
},
|
|
343
|
+
cloudflare: {
|
|
344
|
+
label: "Cloudflare Pages",
|
|
345
|
+
hint: "workers — wrangler.toml + _routes.json"
|
|
346
|
+
},
|
|
347
|
+
netlify: {
|
|
348
|
+
label: "Netlify",
|
|
349
|
+
hint: "netlify functions — netlify.toml"
|
|
350
|
+
},
|
|
351
|
+
node: {
|
|
352
|
+
label: "Node.js",
|
|
353
|
+
hint: "Dockerfile + start script for self-hosting"
|
|
354
|
+
},
|
|
355
|
+
bun: {
|
|
356
|
+
label: "Bun",
|
|
357
|
+
hint: "Dockerfile (bun-based) for self-hosting on Bun runtimes"
|
|
358
|
+
},
|
|
359
|
+
static: {
|
|
360
|
+
label: "Static (no server)",
|
|
361
|
+
hint: "works with any static host (GitHub Pages, S3, …)"
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Run the interactive prompt flow. Every prompt is skipped when its
|
|
366
|
+
* corresponding CLI flag is set or when `--yes` was passed; in that
|
|
367
|
+
* case the template-default (or flag-supplied) value is used.
|
|
368
|
+
*
|
|
369
|
+
* Cancellation at any prompt aborts via `process.exit(0)`.
|
|
370
|
+
*/
|
|
371
|
+
async function runPrompts(args) {
|
|
372
|
+
const yes = args.yes;
|
|
373
|
+
let name;
|
|
374
|
+
if (args.name !== void 0) name = args.name;
|
|
375
|
+
else if (yes) {
|
|
376
|
+
p.cancel("Project name is required when using --yes (pass it as the first argument).");
|
|
377
|
+
process.exit(2);
|
|
378
|
+
} else {
|
|
379
|
+
const value = await p.text({
|
|
380
|
+
message: "Project name",
|
|
381
|
+
placeholder: "my-zero-app",
|
|
382
|
+
validate: (v) => {
|
|
383
|
+
if (!v?.trim()) return "Project name is required";
|
|
384
|
+
if (existsSync(resolve(process.cwd(), v))) return `Directory "${v}" already exists`;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
if (p.isCancel(value)) {
|
|
388
|
+
p.cancel("Cancelled.");
|
|
389
|
+
process.exit(0);
|
|
120
390
|
}
|
|
121
|
-
|
|
122
|
-
if (p.isCancel(name)) {
|
|
123
|
-
p.cancel("Cancelled.");
|
|
124
|
-
process.exit(0);
|
|
391
|
+
name = value;
|
|
125
392
|
}
|
|
126
393
|
const targetDir = resolve(process.cwd(), name);
|
|
127
394
|
if (existsSync(targetDir)) {
|
|
128
395
|
p.cancel(`Directory "${name}" already exists.`);
|
|
129
396
|
process.exit(1);
|
|
130
397
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
value: "spa",
|
|
151
|
-
label: "SPA",
|
|
152
|
-
hint: "client-only, no server rendering"
|
|
153
|
-
}
|
|
154
|
-
]
|
|
155
|
-
});
|
|
156
|
-
if (p.isCancel(renderMode)) {
|
|
157
|
-
p.cancel("Cancelled.");
|
|
158
|
-
process.exit(0);
|
|
398
|
+
let template;
|
|
399
|
+
if (args.template) template = args.template;
|
|
400
|
+
else if (yes) template = "app";
|
|
401
|
+
else {
|
|
402
|
+
const value = await p.select({
|
|
403
|
+
message: "Template",
|
|
404
|
+
options: Object.values(TEMPLATES).map((t) => ({
|
|
405
|
+
value: t.id,
|
|
406
|
+
label: t.label,
|
|
407
|
+
hint: t.hint
|
|
408
|
+
}))
|
|
409
|
+
});
|
|
410
|
+
if (p.isCancel(value)) {
|
|
411
|
+
p.cancel("Cancelled.");
|
|
412
|
+
process.exit(0);
|
|
413
|
+
}
|
|
414
|
+
template = value;
|
|
159
415
|
}
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
416
|
+
const tmpl = TEMPLATES[template];
|
|
417
|
+
let renderMode;
|
|
418
|
+
if (tmpl.forcesMode) renderMode = tmpl.defaultMode;
|
|
419
|
+
else if (args.mode) renderMode = args.mode;
|
|
420
|
+
else if (yes) renderMode = tmpl.defaultMode;
|
|
421
|
+
else {
|
|
422
|
+
const value = await p.select({
|
|
423
|
+
message: "Rendering mode",
|
|
424
|
+
options: [
|
|
425
|
+
{
|
|
426
|
+
value: "ssr-stream",
|
|
427
|
+
label: "SSR Streaming",
|
|
428
|
+
hint: "recommended — progressive HTML with Suspense"
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
value: "ssr-string",
|
|
432
|
+
label: "SSR String",
|
|
433
|
+
hint: "buffered HTML, simpler but slower TTFB"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
value: "ssg",
|
|
437
|
+
label: "Static (SSG)",
|
|
438
|
+
hint: "pre-rendered at build time"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
value: "spa",
|
|
442
|
+
label: "SPA",
|
|
443
|
+
hint: "client-only, no server rendering"
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
initialValue: tmpl.defaultMode
|
|
447
|
+
});
|
|
448
|
+
if (p.isCancel(value)) {
|
|
449
|
+
p.cancel("Cancelled.");
|
|
450
|
+
process.exit(0);
|
|
451
|
+
}
|
|
452
|
+
renderMode = value;
|
|
176
453
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
454
|
+
let adapter;
|
|
455
|
+
if (args.adapter) {
|
|
456
|
+
if (!tmpl.adapters.includes(args.adapter)) {
|
|
457
|
+
p.cancel(`Adapter "${args.adapter}" is not supported by template "${template}". Allowed: ${tmpl.adapters.join(", ")}.`);
|
|
458
|
+
process.exit(2);
|
|
459
|
+
}
|
|
460
|
+
adapter = args.adapter;
|
|
461
|
+
} else if (yes) adapter = tmpl.defaultAdapter;
|
|
462
|
+
else {
|
|
463
|
+
const value = await p.select({
|
|
464
|
+
message: "Deployment target",
|
|
465
|
+
options: tmpl.adapters.map((id) => ({
|
|
466
|
+
value: id,
|
|
467
|
+
label: ADAPTER_LABELS[id].label,
|
|
468
|
+
hint: ADAPTER_LABELS[id].hint
|
|
469
|
+
})),
|
|
470
|
+
initialValue: tmpl.defaultAdapter
|
|
471
|
+
});
|
|
472
|
+
if (p.isCancel(value)) {
|
|
473
|
+
p.cancel("Cancelled.");
|
|
474
|
+
process.exit(0);
|
|
475
|
+
}
|
|
476
|
+
adapter = value;
|
|
192
477
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
478
|
+
let features;
|
|
479
|
+
if (args.features !== void 0) features = args.features;
|
|
480
|
+
else if (yes) features = [...tmpl.defaultFeatures];
|
|
481
|
+
else {
|
|
482
|
+
const value = await p.multiselect({
|
|
483
|
+
message: "Select features (space to toggle, enter to confirm)",
|
|
484
|
+
options: Object.entries(FEATURES).map(([key, { label }]) => ({
|
|
485
|
+
value: key,
|
|
486
|
+
label
|
|
487
|
+
})),
|
|
488
|
+
initialValues: [...tmpl.defaultFeatures],
|
|
489
|
+
required: false
|
|
490
|
+
});
|
|
491
|
+
if (p.isCancel(value)) {
|
|
492
|
+
p.cancel("Cancelled.");
|
|
493
|
+
process.exit(0);
|
|
494
|
+
}
|
|
495
|
+
features = value;
|
|
200
496
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
hint: "use createSignal, createEffect, etc."
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
value: "preact",
|
|
226
|
-
label: "Preact",
|
|
227
|
-
hint: "use useState, signals, etc."
|
|
228
|
-
}
|
|
229
|
-
]
|
|
230
|
-
});
|
|
231
|
-
if (p.isCancel(compat)) {
|
|
232
|
-
p.cancel("Cancelled.");
|
|
233
|
-
process.exit(0);
|
|
497
|
+
let packageStrategy;
|
|
498
|
+
if (args.packageStrategy) packageStrategy = args.packageStrategy;
|
|
499
|
+
else if (yes) packageStrategy = "meta";
|
|
500
|
+
else {
|
|
501
|
+
const value = await p.select({
|
|
502
|
+
message: "Package imports",
|
|
503
|
+
options: [{
|
|
504
|
+
value: "meta",
|
|
505
|
+
label: "@pyreon/meta (single barrel)",
|
|
506
|
+
hint: "one import for everything — simpler, tree-shaken at build"
|
|
507
|
+
}, {
|
|
508
|
+
value: "individual",
|
|
509
|
+
label: "Individual packages",
|
|
510
|
+
hint: "only install what you selected — smaller node_modules"
|
|
511
|
+
}]
|
|
512
|
+
});
|
|
513
|
+
if (p.isCancel(value)) {
|
|
514
|
+
p.cancel("Cancelled.");
|
|
515
|
+
process.exit(0);
|
|
516
|
+
}
|
|
517
|
+
packageStrategy = value;
|
|
234
518
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
519
|
+
let integrations;
|
|
520
|
+
if (args.integrations !== void 0) integrations = args.integrations;
|
|
521
|
+
else if (yes) integrations = [...tmpl.defaultIntegrations];
|
|
522
|
+
else {
|
|
523
|
+
const value = await p.multiselect({
|
|
524
|
+
message: "Backend integrations (space to toggle)",
|
|
525
|
+
options: [{
|
|
526
|
+
value: "supabase",
|
|
527
|
+
label: "Supabase",
|
|
528
|
+
hint: "Postgres + auth + storage — replaces dashboard auth/db stubs"
|
|
529
|
+
}, {
|
|
530
|
+
value: "email",
|
|
531
|
+
label: "Email (Resend)",
|
|
532
|
+
hint: "Resend transport + document-primitives email templates"
|
|
533
|
+
}],
|
|
534
|
+
initialValues: [...tmpl.defaultIntegrations],
|
|
535
|
+
required: false
|
|
536
|
+
});
|
|
537
|
+
if (p.isCancel(value)) {
|
|
538
|
+
p.cancel("Cancelled.");
|
|
539
|
+
process.exit(0);
|
|
540
|
+
}
|
|
541
|
+
integrations = value;
|
|
542
|
+
}
|
|
543
|
+
let aiTools;
|
|
544
|
+
if (args.ai !== void 0) aiTools = args.ai;
|
|
545
|
+
else if (yes) aiTools = ["mcp", "claude"];
|
|
546
|
+
else {
|
|
547
|
+
const value = await p.multiselect({
|
|
548
|
+
message: "AI tooling (space to toggle, enter to confirm)",
|
|
549
|
+
options: [
|
|
550
|
+
{
|
|
551
|
+
value: "mcp",
|
|
552
|
+
label: "MCP server",
|
|
553
|
+
hint: ".mcp.json — Claude Code, Continue.dev"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
value: "claude",
|
|
557
|
+
label: "CLAUDE.md",
|
|
558
|
+
hint: "Claude Code project rules"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
value: "cursor",
|
|
562
|
+
label: "Cursor rules",
|
|
563
|
+
hint: ".cursor/rules/pyreon.md"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
value: "copilot",
|
|
567
|
+
label: "GitHub Copilot",
|
|
568
|
+
hint: ".github/copilot-instructions.md"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
value: "agents",
|
|
572
|
+
label: "AGENTS.md",
|
|
573
|
+
hint: "Aider, Continue, editor agents"
|
|
574
|
+
}
|
|
575
|
+
],
|
|
576
|
+
initialValues: ["mcp", "claude"],
|
|
577
|
+
required: false
|
|
578
|
+
});
|
|
579
|
+
if (p.isCancel(value)) {
|
|
580
|
+
p.cancel("Cancelled.");
|
|
581
|
+
process.exit(0);
|
|
582
|
+
}
|
|
583
|
+
aiTools = value;
|
|
584
|
+
}
|
|
585
|
+
let compat;
|
|
586
|
+
if (args.compat) compat = args.compat;
|
|
587
|
+
else if (yes) compat = "none";
|
|
588
|
+
else {
|
|
589
|
+
const value = await p.select({
|
|
590
|
+
message: "Migrating from another framework?",
|
|
591
|
+
options: [
|
|
592
|
+
{
|
|
593
|
+
value: "none",
|
|
594
|
+
label: "No — native Pyreon",
|
|
595
|
+
hint: "recommended"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
value: "react",
|
|
599
|
+
label: "React",
|
|
600
|
+
hint: "use useState, useEffect, etc."
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
value: "vue",
|
|
604
|
+
label: "Vue",
|
|
605
|
+
hint: "use ref, computed, watch, etc."
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
value: "solid",
|
|
609
|
+
label: "Solid",
|
|
610
|
+
hint: "use createSignal, createEffect, etc."
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
value: "preact",
|
|
614
|
+
label: "Preact",
|
|
615
|
+
hint: "use useState, signals, etc."
|
|
616
|
+
}
|
|
617
|
+
]
|
|
618
|
+
});
|
|
619
|
+
if (p.isCancel(value)) {
|
|
620
|
+
p.cancel("Cancelled.");
|
|
621
|
+
process.exit(0);
|
|
622
|
+
}
|
|
623
|
+
compat = value;
|
|
242
624
|
}
|
|
243
|
-
|
|
625
|
+
let lint;
|
|
626
|
+
if (args.lint !== void 0) lint = args.lint;
|
|
627
|
+
else if (yes) lint = true;
|
|
628
|
+
else {
|
|
629
|
+
const value = await p.confirm({
|
|
630
|
+
message: "Include @pyreon/lint? (59 Pyreon-specific rules)",
|
|
631
|
+
initialValue: true
|
|
632
|
+
});
|
|
633
|
+
if (p.isCancel(value)) {
|
|
634
|
+
p.cancel("Cancelled.");
|
|
635
|
+
process.exit(0);
|
|
636
|
+
}
|
|
637
|
+
lint = value;
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
244
640
|
name,
|
|
245
641
|
targetDir,
|
|
642
|
+
template,
|
|
246
643
|
renderMode,
|
|
644
|
+
adapter,
|
|
247
645
|
features,
|
|
248
646
|
packageStrategy,
|
|
249
|
-
|
|
647
|
+
integrations,
|
|
648
|
+
aiTools,
|
|
250
649
|
compat,
|
|
251
650
|
lint
|
|
252
651
|
};
|
|
253
|
-
const s = p.spinner();
|
|
254
|
-
s.start("Scaffolding project...");
|
|
255
|
-
await scaffold(config);
|
|
256
|
-
s.stop("Project created!");
|
|
257
|
-
p.note([
|
|
258
|
-
`cd ${config.name}`,
|
|
259
|
-
"bun install",
|
|
260
|
-
"bun run dev"
|
|
261
|
-
].join("\n"), "Next steps");
|
|
262
|
-
p.outro("Happy building!");
|
|
263
652
|
}
|
|
653
|
+
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/adapters.ts
|
|
656
|
+
const vercel = {
|
|
657
|
+
id: "vercel",
|
|
658
|
+
viteFactory: "vercelAdapter",
|
|
659
|
+
async apply(config) {
|
|
660
|
+
await writeFile(join(config.targetDir, "vercel.json"), JSON.stringify({
|
|
661
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
662
|
+
buildCommand: "bun run build",
|
|
663
|
+
outputDirectory: "dist",
|
|
664
|
+
framework: null
|
|
665
|
+
}, null, 2) + "\n");
|
|
666
|
+
},
|
|
667
|
+
badge() {
|
|
668
|
+
return "[](https://vercel.com/new/clone?repository-url=)";
|
|
669
|
+
},
|
|
670
|
+
envKeys() {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
const cloudflare = {
|
|
675
|
+
id: "cloudflare",
|
|
676
|
+
viteFactory: "cloudflareAdapter",
|
|
677
|
+
async apply(config) {
|
|
678
|
+
const wranglerToml = `name = "${slug(config.name)}"
|
|
679
|
+
compatibility_date = "2026-01-01"
|
|
680
|
+
compatibility_flags = ["nodejs_compat"]
|
|
681
|
+
pages_build_output_dir = "dist"
|
|
682
|
+
|
|
683
|
+
[vars]
|
|
684
|
+
# NODE_ENV = "production"
|
|
685
|
+
`;
|
|
686
|
+
await writeFile(join(config.targetDir, "wrangler.toml"), wranglerToml);
|
|
687
|
+
await writeFile(join(config.targetDir, "_routes.json"), JSON.stringify({
|
|
688
|
+
version: 1,
|
|
689
|
+
include: ["/*"],
|
|
690
|
+
exclude: ["/build/*"]
|
|
691
|
+
}, null, 2) + "\n");
|
|
692
|
+
},
|
|
693
|
+
badge() {
|
|
694
|
+
return "[](https://deploy.workers.cloudflare.com/?url=)";
|
|
695
|
+
},
|
|
696
|
+
envKeys() {
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
const netlify = {
|
|
701
|
+
id: "netlify",
|
|
702
|
+
viteFactory: "netlifyAdapter",
|
|
703
|
+
async apply(config) {
|
|
704
|
+
await writeFile(join(config.targetDir, "netlify.toml"), `[build]
|
|
705
|
+
command = "bun run build"
|
|
706
|
+
publish = "dist"
|
|
707
|
+
|
|
708
|
+
[functions]
|
|
709
|
+
directory = "dist/.netlify/functions"
|
|
710
|
+
node_bundler = "esbuild"
|
|
711
|
+
|
|
712
|
+
[[redirects]]
|
|
713
|
+
from = "/*"
|
|
714
|
+
to = "/.netlify/functions/server/:splat"
|
|
715
|
+
status = 200
|
|
716
|
+
force = false
|
|
717
|
+
`);
|
|
718
|
+
},
|
|
719
|
+
badge() {
|
|
720
|
+
return "[](https://app.netlify.com/start/deploy?repository=)";
|
|
721
|
+
},
|
|
722
|
+
envKeys() {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const node = {
|
|
727
|
+
id: "node",
|
|
728
|
+
viteFactory: "nodeAdapter",
|
|
729
|
+
async apply(config) {
|
|
730
|
+
await writeFile(join(config.targetDir, "Dockerfile"), `FROM node:22-alpine AS build
|
|
731
|
+
WORKDIR /app
|
|
732
|
+
COPY package.json bun.lock* ./
|
|
733
|
+
RUN corepack enable && corepack prepare bun@latest --activate && bun install --frozen-lockfile
|
|
734
|
+
COPY . .
|
|
735
|
+
RUN bun run build
|
|
736
|
+
|
|
737
|
+
FROM node:22-alpine
|
|
738
|
+
WORKDIR /app
|
|
739
|
+
COPY --from=build /app/dist ./dist
|
|
740
|
+
COPY --from=build /app/package.json ./
|
|
741
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
742
|
+
EXPOSE 3000
|
|
743
|
+
CMD ["node", "dist/server.js"]
|
|
744
|
+
`);
|
|
745
|
+
await writeFile(join(config.targetDir, ".dockerignore"), "node_modules\ndist\n.git\n.env\n.env.*\n");
|
|
746
|
+
},
|
|
747
|
+
badge() {
|
|
748
|
+
return "";
|
|
749
|
+
},
|
|
750
|
+
envKeys() {
|
|
751
|
+
return ["PORT"];
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const bun = {
|
|
755
|
+
id: "bun",
|
|
756
|
+
viteFactory: "bunAdapter",
|
|
757
|
+
async apply(config) {
|
|
758
|
+
await writeFile(join(config.targetDir, "Dockerfile"), `FROM oven/bun:1 AS build
|
|
759
|
+
WORKDIR /app
|
|
760
|
+
COPY package.json bun.lock* ./
|
|
761
|
+
RUN bun install --frozen-lockfile
|
|
762
|
+
COPY . .
|
|
763
|
+
RUN bun run build
|
|
764
|
+
|
|
765
|
+
FROM oven/bun:1
|
|
766
|
+
WORKDIR /app
|
|
767
|
+
COPY --from=build /app/dist ./dist
|
|
768
|
+
COPY --from=build /app/package.json ./
|
|
769
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
770
|
+
EXPOSE 3000
|
|
771
|
+
CMD ["bun", "run", "dist/server.js"]
|
|
772
|
+
`);
|
|
773
|
+
await writeFile(join(config.targetDir, ".dockerignore"), "node_modules\ndist\n.git\n.env\n.env.*\n");
|
|
774
|
+
},
|
|
775
|
+
badge() {
|
|
776
|
+
return "";
|
|
777
|
+
},
|
|
778
|
+
envKeys() {
|
|
779
|
+
return ["PORT"];
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
const staticAdapter = {
|
|
783
|
+
id: "static",
|
|
784
|
+
viteFactory: null,
|
|
785
|
+
async apply() {},
|
|
786
|
+
badge() {
|
|
787
|
+
return "";
|
|
788
|
+
},
|
|
789
|
+
envKeys() {
|
|
790
|
+
return [];
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
const ADAPTERS = {
|
|
794
|
+
vercel,
|
|
795
|
+
cloudflare,
|
|
796
|
+
netlify,
|
|
797
|
+
node,
|
|
798
|
+
bun,
|
|
799
|
+
static: staticAdapter
|
|
800
|
+
};
|
|
801
|
+
function adapterFor(id) {
|
|
802
|
+
return ADAPTERS[id];
|
|
803
|
+
}
|
|
804
|
+
function slug(name) {
|
|
805
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/ai-tools.ts
|
|
810
|
+
/**
|
|
811
|
+
* Pyreon-specific guidance shared across every AI-tool rule file. Each tool
|
|
812
|
+
* wraps this in its own preamble (frontmatter, header, etc.) so the styling
|
|
813
|
+
* matches the tool's conventions, but the substance is identical: this is
|
|
814
|
+
* Pyreon, not React; here is how reactivity, JSX, and routing actually work.
|
|
815
|
+
*
|
|
816
|
+
* `doctorLine` is appended to the Commands section when the consumer is the
|
|
817
|
+
* primary "knows about doctor" file (CLAUDE.md). Other tools omit it.
|
|
818
|
+
*/
|
|
819
|
+
function pyreonPrinciples(opts) {
|
|
820
|
+
return `## Reactivity (Pyreon, not React)
|
|
821
|
+
|
|
822
|
+
- \`signal()\` not \`useState\`; \`computed()\` not \`useMemo\`; \`effect()\` not \`useEffect\`.
|
|
823
|
+
- Write signals via \`signal.set(value)\` or \`signal.update(fn)\`. Calling \`signal(value)\` does NOT write — it reads.
|
|
824
|
+
- Components run **once** at mount. Reactivity comes from signals reading themselves at use sites; the framework subscribes the surrounding DOM node, not the whole component.
|
|
825
|
+
- In JSX, signals auto-call: \`{count}\` (compiler inserts \`()\`). Outside JSX, call explicitly: \`count()\`.
|
|
826
|
+
- Don't destructure props (\`const { x } = props\` captures getters once and loses reactivity). Read \`props.x\` directly, or use \`splitProps(props, ['x'])\`.
|
|
827
|
+
|
|
828
|
+
## JSX
|
|
829
|
+
|
|
830
|
+
- \`class=\` not \`className\`; \`for=\` not \`htmlFor\`; camelCase events (\`onClick\`, \`onMouseEnter\`).
|
|
831
|
+
- Lists: \`<For each={items} by={r => r.id}>{r => <li>...</li>}</For>\`. The prop is \`by\` (not \`key\`) — JSX extracts \`key\` for VNode reconciliation.
|
|
832
|
+
- Conditionals: \`<Show when={cond}>...</Show>\` or accessor form \`{() => cond() ? <A /> : null}\`.
|
|
833
|
+
- \`onChange\` → \`onInput\` for keypress-by-keypress text updates.
|
|
834
|
+
|
|
835
|
+
## File-Based Routing
|
|
836
|
+
|
|
837
|
+
- \`src/routes/index.tsx\` → \`/\`
|
|
838
|
+
- \`src/routes/about.tsx\` → \`/about\`
|
|
839
|
+
- \`src/routes/[id].tsx\` → \`/:id\`
|
|
840
|
+
- \`src/routes/_layout.tsx\` → layout wrapper
|
|
841
|
+
- \`(group)/\` → route group (no URL segment)
|
|
842
|
+
|
|
843
|
+
Per-route exports: \`default\` (component), \`loader\` (server data), \`guard\` (nav guard), \`middleware\`, \`meta\`, \`renderMode\`.
|
|
844
|
+
|
|
845
|
+
## Don't reach for raw DOM APIs
|
|
846
|
+
|
|
847
|
+
- Use \`useEventListener\` / \`useClickOutside\` / \`useScrollLock\` from \`@pyreon/hooks\` instead of \`addEventListener\` / \`removeEventListener\`. The hook handles cleanup on unmount.
|
|
848
|
+
- For controlled state in primitives, use \`useControllableState({ value, defaultValue, onChange })\`.
|
|
849
|
+
|
|
850
|
+
## Don't paste React patterns
|
|
851
|
+
|
|
852
|
+
- No \`useState\` / \`useEffect\` / \`useMemo\` / \`useCallback\` / \`useRef\`. None of those exist.
|
|
853
|
+
- No \`React.Fragment\` — just \`<></>\`.
|
|
854
|
+
- No "children as function" trick — Pyreon supports JSX children directly.
|
|
855
|
+
|
|
856
|
+
## Commands
|
|
857
|
+
|
|
858
|
+
- \`bun run dev\` — dev server with HMR (signals preserve across reload)
|
|
859
|
+
- \`bun run build\` — production build
|
|
860
|
+
- \`bun run preview\` — serve build${opts.doctorLine ? "\n- `bun run doctor` — checks for React patterns and other anti-patterns" : ""}
|
|
861
|
+
`;
|
|
862
|
+
}
|
|
863
|
+
const RULE_FILES = [
|
|
864
|
+
{
|
|
865
|
+
id: "claude",
|
|
866
|
+
path: "CLAUDE.md",
|
|
867
|
+
render: () => `# Project
|
|
868
|
+
|
|
869
|
+
This project uses Pyreon Zero, a signal-based full-stack meta-framework. Do NOT use React patterns.
|
|
870
|
+
|
|
871
|
+
${pyreonPrinciples({ doctorLine: true })}`
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
id: "cursor",
|
|
875
|
+
path: ".cursor/rules/pyreon.md",
|
|
876
|
+
render: () => `---
|
|
877
|
+
description: Pyreon Zero project rules
|
|
878
|
+
globs:
|
|
879
|
+
- "**/*.{ts,tsx}"
|
|
880
|
+
alwaysApply: true
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
# Pyreon Zero
|
|
884
|
+
|
|
885
|
+
This is a Pyreon Zero project — a signal-based full-stack meta-framework. **Do not use React patterns** (useState, useEffect, className, etc.).
|
|
886
|
+
|
|
887
|
+
${pyreonPrinciples({ doctorLine: false })}
|
|
888
|
+
|
|
889
|
+
## When in doubt
|
|
890
|
+
|
|
891
|
+
The MCP server at \`.mcp.json\` exposes a \`validate\` tool that statically catches React→Pyreon mistakes. Run it on suspicious snippets before committing.
|
|
892
|
+
`
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
id: "copilot",
|
|
896
|
+
path: ".github/copilot-instructions.md",
|
|
897
|
+
render: () => `# Copilot Instructions
|
|
898
|
+
|
|
899
|
+
This repository uses **Pyreon Zero** — a signal-based meta-framework. Do not generate React code.
|
|
900
|
+
|
|
901
|
+
${pyreonPrinciples({ doctorLine: false })}
|
|
902
|
+
|
|
903
|
+
## Quick reference
|
|
904
|
+
|
|
905
|
+
| Need | Use |
|
|
906
|
+
|---|---|
|
|
907
|
+
| Reactive value | \`signal()\` |
|
|
908
|
+
| Derived value | \`computed()\` |
|
|
909
|
+
| Side effect | \`effect()\` or \`onMount(() => { … return cleanup })\` |
|
|
910
|
+
| Form state | \`useForm()\` from \`@pyreon/form\` |
|
|
911
|
+
| Server data | \`useQuery()\` from \`@pyreon/query\` |
|
|
912
|
+
| Global state | \`defineStore()\` from \`@pyreon/store\` |
|
|
913
|
+
`
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
id: "agents",
|
|
917
|
+
path: "AGENTS.md",
|
|
918
|
+
render: () => `# AGENTS.md
|
|
919
|
+
|
|
920
|
+
A generic AI-agent instruction file picked up by Aider, Continue.dev, and various editor agents that read \`AGENTS.md\` at the project root.
|
|
921
|
+
|
|
922
|
+
This is a Pyreon Zero project. Do not use React patterns (no useState / useEffect / className).
|
|
923
|
+
|
|
924
|
+
${pyreonPrinciples({ doctorLine: false })}`
|
|
925
|
+
}
|
|
926
|
+
];
|
|
927
|
+
async function applyAiTools(config) {
|
|
928
|
+
const selected = new Set(config.aiTools);
|
|
929
|
+
for (const gen of RULE_FILES) if (selected.has(gen.id)) {
|
|
930
|
+
const target = join(config.targetDir, gen.path);
|
|
931
|
+
await mkdir(dirname(target), { recursive: true });
|
|
932
|
+
await writeFile(target, gen.render(config));
|
|
933
|
+
}
|
|
934
|
+
if (!selected.has("claude")) await removeIfExists$1(join(config.targetDir, "CLAUDE.md"));
|
|
935
|
+
if (!selected.has("mcp")) await removeIfExists$1(join(config.targetDir, ".mcp.json"));
|
|
936
|
+
}
|
|
937
|
+
async function removeIfExists$1(path) {
|
|
938
|
+
if (!existsSync(path)) return;
|
|
939
|
+
await unlink(path);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region src/integrations.ts
|
|
944
|
+
const REGISTRY = {
|
|
945
|
+
supabase: {
|
|
946
|
+
id: "supabase",
|
|
947
|
+
deps() {
|
|
948
|
+
return { "@supabase/supabase-js": "^2.49.0" };
|
|
949
|
+
},
|
|
950
|
+
envKeys() {
|
|
951
|
+
return ["SUPABASE_URL", "SUPABASE_ANON_KEY"];
|
|
952
|
+
},
|
|
953
|
+
async apply(config) {
|
|
954
|
+
await writeFileEnsuringDir(join(config.targetDir, "src/lib/supabase.ts"), supabaseClient());
|
|
955
|
+
if (config.template === "dashboard") {
|
|
956
|
+
await writeFile(join(config.targetDir, "src/lib/auth.ts"), supabaseAuth());
|
|
957
|
+
await writeFile(join(config.targetDir, "src/lib/db.ts"), supabaseDb());
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
email: {
|
|
962
|
+
id: "email",
|
|
963
|
+
deps() {
|
|
964
|
+
return {
|
|
965
|
+
resend: "^4.0.0",
|
|
966
|
+
"@pyreon/document-primitives": "workspace:^",
|
|
967
|
+
"@pyreon/document": "workspace:^",
|
|
968
|
+
"@pyreon/connector-document": "workspace:^"
|
|
969
|
+
};
|
|
970
|
+
},
|
|
971
|
+
envKeys() {
|
|
972
|
+
return ["RESEND_API_KEY", "EMAIL_FROM"];
|
|
973
|
+
},
|
|
974
|
+
async apply(config) {
|
|
975
|
+
await writeFileEnsuringDir(join(config.targetDir, "src/lib/email.ts"), emailLib());
|
|
976
|
+
await writeFileEnsuringDir(join(config.targetDir, "src/emails/welcome.tsx"), welcomeEmailTemplate());
|
|
977
|
+
await writeFileEnsuringDir(join(config.targetDir, "src/routes/api/email/welcome.ts"), welcomeEmailEndpoint());
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
async function applyIntegrations(config) {
|
|
982
|
+
for (const id of config.integrations) await REGISTRY[id].apply(config);
|
|
983
|
+
await appendEnvExample(config);
|
|
984
|
+
}
|
|
985
|
+
function integrationDeps(config) {
|
|
986
|
+
const out = {};
|
|
987
|
+
for (const id of config.integrations) Object.assign(out, REGISTRY[id].deps());
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
async function appendEnvExample(config) {
|
|
991
|
+
if (config.integrations.length === 0) return;
|
|
992
|
+
const lines = [];
|
|
993
|
+
for (const id of config.integrations) {
|
|
994
|
+
const keys = REGISTRY[id].envKeys();
|
|
995
|
+
if (keys.length === 0) continue;
|
|
996
|
+
lines.push(`# ─── ${id} ───`);
|
|
997
|
+
for (const k of keys) lines.push(`${k}=`);
|
|
998
|
+
lines.push("");
|
|
999
|
+
}
|
|
1000
|
+
const envPath = join(config.targetDir, ".env.example");
|
|
1001
|
+
const existing = existsSync(envPath) ? await readFile(envPath, "utf-8") : "";
|
|
1002
|
+
await writeFile(envPath, existing ? `${existing.trimEnd()}\n\n${lines.join("\n")}` : lines.join("\n"));
|
|
1003
|
+
}
|
|
1004
|
+
async function writeFileEnsuringDir(path, content) {
|
|
1005
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1006
|
+
await writeFile(path, content);
|
|
1007
|
+
}
|
|
1008
|
+
function supabaseClient() {
|
|
1009
|
+
return `import { createClient, type SupabaseClient } from '@supabase/supabase-js'
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Server-side Supabase client. Uses the anon key by default; swap for the
|
|
1013
|
+
* service-role key inside trusted server contexts (route loaders that
|
|
1014
|
+
* need to bypass RLS) and pair with row-level policies in your Postgres
|
|
1015
|
+
* schema.
|
|
1016
|
+
*
|
|
1017
|
+
* The server reads SUPABASE_URL / SUPABASE_ANON_KEY from \`process.env\`.
|
|
1018
|
+
* For the browser bundle, expose the SAME values via \`publicEnv()\` so
|
|
1019
|
+
* client-side fetch / realtime subscriptions can connect.
|
|
1020
|
+
*/
|
|
1021
|
+
export const supabase: SupabaseClient = createClient(
|
|
1022
|
+
process.env.SUPABASE_URL ?? '',
|
|
1023
|
+
process.env.SUPABASE_ANON_KEY ?? '',
|
|
1024
|
+
{ auth: { persistSession: false, detectSessionInUrl: false } },
|
|
1025
|
+
)
|
|
1026
|
+
`;
|
|
1027
|
+
}
|
|
1028
|
+
function supabaseAuth() {
|
|
1029
|
+
return `import { supabase } from './supabase'
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Supabase-backed auth implementation. Mirrors the in-memory stub's
|
|
1033
|
+
* exported surface (signIn / signUp / getSession / signOut / SessionInfo)
|
|
1034
|
+
* so route guards don't change when swapping backends.
|
|
1035
|
+
*/
|
|
1036
|
+
|
|
1037
|
+
export interface SessionInfo {
|
|
1038
|
+
userId: string
|
|
1039
|
+
email: string
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
export async function signUp(
|
|
1043
|
+
email: string,
|
|
1044
|
+
password: string,
|
|
1045
|
+
): Promise<{ sessionId: string } | { error: string }> {
|
|
1046
|
+
const { data, error } = await supabase.auth.signUp({ email, password })
|
|
1047
|
+
if (error) return { error: error.message }
|
|
1048
|
+
if (!data.session) return { error: 'Email confirmation required — check your inbox.' }
|
|
1049
|
+
return { sessionId: data.session.access_token }
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export async function signIn(
|
|
1053
|
+
email: string,
|
|
1054
|
+
password: string,
|
|
1055
|
+
): Promise<{ sessionId: string } | { error: string }> {
|
|
1056
|
+
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
|
|
1057
|
+
if (error) return { error: error.message }
|
|
1058
|
+
return { sessionId: data.session.access_token }
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export async function getSession(sessionId: string | undefined): Promise<SessionInfo | null> {
|
|
1062
|
+
if (!sessionId) return null
|
|
1063
|
+
const { data, error } = await supabase.auth.getUser(sessionId)
|
|
1064
|
+
if (error || !data.user) return null
|
|
1065
|
+
return { userId: data.user.id, email: data.user.email ?? '' }
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export async function signOut(sessionId: string): Promise<void> {
|
|
1069
|
+
// Supabase tokens are JWTs; revocation is server-mediated. We invalidate
|
|
1070
|
+
// the access token by calling \`supabase.auth.admin.signOut(sessionId)\`
|
|
1071
|
+
// when the service-role key is available; otherwise fall back to a
|
|
1072
|
+
// client-side cookie clear (handled by the route).
|
|
1073
|
+
if (process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
1074
|
+
await supabase.auth.admin.signOut(sessionId)
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
`;
|
|
1078
|
+
}
|
|
1079
|
+
function supabaseDb() {
|
|
1080
|
+
return `import { supabase } from './supabase'
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Supabase-backed data layer. Mirrors the in-memory stub's exported
|
|
1084
|
+
* surface (User / Invoice / listUsers / listInvoices / invoiceById /
|
|
1085
|
+
* invoiceTotal) so dashboard routes don't change when swapping backends.
|
|
1086
|
+
*
|
|
1087
|
+
* Schema expected in your Supabase Postgres:
|
|
1088
|
+
*
|
|
1089
|
+
* create table public.users (
|
|
1090
|
+
* id uuid primary key,
|
|
1091
|
+
* email text not null,
|
|
1092
|
+
* name text not null,
|
|
1093
|
+
* role text not null check (role in ('admin','member')),
|
|
1094
|
+
* created_at timestamptz not null default now()
|
|
1095
|
+
* );
|
|
1096
|
+
*
|
|
1097
|
+
* create table public.invoices (
|
|
1098
|
+
* id text primary key,
|
|
1099
|
+
* number text not null,
|
|
1100
|
+
* customer jsonb not null,
|
|
1101
|
+
* items jsonb not null,
|
|
1102
|
+
* status text not null check (status in ('draft','pending','paid')),
|
|
1103
|
+
* issued_at timestamptz not null default now()
|
|
1104
|
+
* );
|
|
1105
|
+
*/
|
|
1106
|
+
|
|
1107
|
+
export interface User {
|
|
1108
|
+
id: string
|
|
1109
|
+
email: string
|
|
1110
|
+
name: string
|
|
1111
|
+
role: 'admin' | 'member'
|
|
1112
|
+
createdAt: Date
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export interface InvoiceItem {
|
|
1116
|
+
description: string
|
|
1117
|
+
qty: number
|
|
1118
|
+
unitPrice: number
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export interface Invoice {
|
|
1122
|
+
id: string
|
|
1123
|
+
number: string
|
|
1124
|
+
customer: { name: string; email: string; address: string }
|
|
1125
|
+
items: InvoiceItem[]
|
|
1126
|
+
status: 'draft' | 'pending' | 'paid'
|
|
1127
|
+
issuedAt: Date
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
export async function listUsers(): Promise<User[]> {
|
|
1131
|
+
const { data, error } = await supabase.from('users').select('*')
|
|
1132
|
+
if (error) throw error
|
|
1133
|
+
return data.map((row) => ({
|
|
1134
|
+
id: row.id,
|
|
1135
|
+
email: row.email,
|
|
1136
|
+
name: row.name,
|
|
1137
|
+
role: row.role,
|
|
1138
|
+
createdAt: new Date(row.created_at),
|
|
1139
|
+
}))
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
export async function listInvoices(): Promise<Invoice[]> {
|
|
1143
|
+
const { data, error } = await supabase.from('invoices').select('*')
|
|
1144
|
+
if (error) throw error
|
|
1145
|
+
return data.map(rowToInvoice)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export async function invoiceById(id: string): Promise<Invoice | undefined> {
|
|
1149
|
+
const { data, error } = await supabase.from('invoices').select('*').eq('id', id).maybeSingle()
|
|
1150
|
+
if (error) throw error
|
|
1151
|
+
return data ? rowToInvoice(data) : undefined
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
export function invoiceTotal(inv: Invoice): number {
|
|
1155
|
+
return inv.items.reduce((sum, i) => sum + i.qty * i.unitPrice, 0)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function rowToInvoice(row: any): Invoice {
|
|
1159
|
+
return {
|
|
1160
|
+
id: row.id,
|
|
1161
|
+
number: row.number,
|
|
1162
|
+
customer: row.customer,
|
|
1163
|
+
items: row.items,
|
|
1164
|
+
status: row.status,
|
|
1165
|
+
issuedAt: new Date(row.issued_at),
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
1170
|
+
function emailLib() {
|
|
1171
|
+
return `import { Resend } from 'resend'
|
|
1172
|
+
import { extractDocNode } from '@pyreon/document-primitives'
|
|
1173
|
+
import { render } from '@pyreon/document'
|
|
1174
|
+
import type { ComponentFn } from '@pyreon/core'
|
|
1175
|
+
|
|
1176
|
+
const resend = new Resend(process.env.RESEND_API_KEY)
|
|
1177
|
+
const FROM = process.env.EMAIL_FROM ?? 'noreply@example.com'
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Send an email rendered from a Pyreon \`@pyreon/document-primitives\`
|
|
1181
|
+
* template. The same template renders in the browser preview AND exports
|
|
1182
|
+
* to email HTML — that is the headline Pyreon angle: one component tree,
|
|
1183
|
+
* many output formats.
|
|
1184
|
+
*/
|
|
1185
|
+
export async function sendEmail<TProps>(opts: {
|
|
1186
|
+
to: string | string[]
|
|
1187
|
+
subject: string
|
|
1188
|
+
template: ComponentFn<TProps>
|
|
1189
|
+
data: TProps
|
|
1190
|
+
}): Promise<{ id: string } | { error: string }> {
|
|
1191
|
+
const node = extractDocNode(() => opts.template(opts.data))
|
|
1192
|
+
const html = (await render(node, 'email')) as string
|
|
1193
|
+
|
|
1194
|
+
const { data, error } = await resend.emails.send({
|
|
1195
|
+
from: FROM,
|
|
1196
|
+
to: opts.to,
|
|
1197
|
+
subject: opts.subject,
|
|
1198
|
+
html,
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
if (error) return { error: error.message }
|
|
1202
|
+
return { id: data?.id ?? 'unknown' }
|
|
1203
|
+
}
|
|
1204
|
+
`;
|
|
1205
|
+
}
|
|
1206
|
+
function welcomeEmailTemplate() {
|
|
1207
|
+
return `import {
|
|
1208
|
+
DocDocument,
|
|
1209
|
+
DocPage,
|
|
1210
|
+
DocSection,
|
|
1211
|
+
DocHeading,
|
|
1212
|
+
DocText,
|
|
1213
|
+
DocSpacer,
|
|
1214
|
+
} from '@pyreon/document-primitives'
|
|
1215
|
+
|
|
1216
|
+
export interface WelcomeEmailProps {
|
|
1217
|
+
name: string
|
|
1218
|
+
appUrl: string
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Welcome email template. Renders in the browser AND exports to email
|
|
1223
|
+
* HTML via \`@pyreon/document-primitives\` — the SAME component tree.
|
|
1224
|
+
*
|
|
1225
|
+
* Try it: in dev, visit \`/api/email/welcome?to=you@example.com\`. In
|
|
1226
|
+
* production, call \`sendEmail({ to, subject, template: WelcomeEmail,
|
|
1227
|
+
* data: { name, appUrl } })\` from any server route.
|
|
1228
|
+
*/
|
|
1229
|
+
export default function WelcomeEmail(props: WelcomeEmailProps) {
|
|
1230
|
+
return (
|
|
1231
|
+
<DocDocument title="Welcome" subject="Welcome to your new account">
|
|
1232
|
+
<DocPage>
|
|
1233
|
+
<DocSection>
|
|
1234
|
+
<DocHeading level="h1">Welcome, {props.name}.</DocHeading>
|
|
1235
|
+
</DocSection>
|
|
1236
|
+
|
|
1237
|
+
<DocSpacer />
|
|
1238
|
+
|
|
1239
|
+
<DocSection>
|
|
1240
|
+
<DocText>
|
|
1241
|
+
Your account is ready. The dashboard is the fastest way to get started — log
|
|
1242
|
+
in any time at:
|
|
1243
|
+
</DocText>
|
|
1244
|
+
<DocText>{props.appUrl}</DocText>
|
|
1245
|
+
</DocSection>
|
|
1246
|
+
|
|
1247
|
+
<DocSpacer />
|
|
1248
|
+
|
|
1249
|
+
<DocSection>
|
|
1250
|
+
<DocText>If you didn't create this account, ignore this email.</DocText>
|
|
1251
|
+
</DocSection>
|
|
1252
|
+
</DocPage>
|
|
1253
|
+
</DocDocument>
|
|
1254
|
+
)
|
|
1255
|
+
}
|
|
1256
|
+
`;
|
|
1257
|
+
}
|
|
1258
|
+
function welcomeEmailEndpoint() {
|
|
1259
|
+
return `import { sendEmail } from '../../../lib/email'
|
|
1260
|
+
import WelcomeEmail from '../../../emails/welcome'
|
|
1261
|
+
|
|
1262
|
+
export async function GET(request: Request) {
|
|
1263
|
+
const url = new URL(request.url)
|
|
1264
|
+
const to = url.searchParams.get('to')
|
|
1265
|
+
if (!to) {
|
|
1266
|
+
return new Response(JSON.stringify({ error: 'Missing ?to=' }), {
|
|
1267
|
+
status: 400,
|
|
1268
|
+
headers: { 'content-type': 'application/json' },
|
|
1269
|
+
})
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const appUrl = url.origin
|
|
1273
|
+
|
|
1274
|
+
const result = await sendEmail({
|
|
1275
|
+
to,
|
|
1276
|
+
subject: 'Welcome',
|
|
1277
|
+
template: WelcomeEmail,
|
|
1278
|
+
data: { name: to.split('@')[0] ?? 'friend', appUrl },
|
|
1279
|
+
})
|
|
1280
|
+
|
|
1281
|
+
return new Response(JSON.stringify(result), {
|
|
1282
|
+
headers: { 'content-type': 'application/json' },
|
|
1283
|
+
})
|
|
1284
|
+
}
|
|
1285
|
+
`;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
//#endregion
|
|
1289
|
+
//#region src/scaffold.ts
|
|
264
1290
|
async function scaffold(config) {
|
|
265
|
-
await cp(
|
|
1291
|
+
await cp(templateDir(config.template), config.targetDir, { recursive: true });
|
|
266
1292
|
await writeFile(join(config.targetDir, "package.json"), generatePackageJson(config));
|
|
267
1293
|
await writeFile(join(config.targetDir, "vite.config.ts"), generateViteConfig(config));
|
|
1294
|
+
await adapterFor(config.adapter).apply(config);
|
|
268
1295
|
await writeFile(join(config.targetDir, "src/entry-server.ts"), generateEntryServer(config));
|
|
269
1296
|
await writeFile(join(config.targetDir, "env.d.ts"), generateEnvDts(config));
|
|
270
1297
|
await writeFile(join(config.targetDir, ".gitignore"), "node_modules\ndist\n.DS_Store\n*.local\n.pyreon\n");
|
|
@@ -272,28 +1299,19 @@ async function scaffold(config) {
|
|
|
272
1299
|
$schema: "node_modules/@pyreon/lint/schema/pyreonlintrc.schema.json",
|
|
273
1300
|
preset: "recommended"
|
|
274
1301
|
}, null, 2) + "\n");
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
await removeIfExists(join(config.targetDir, "src/features"));
|
|
289
|
-
}
|
|
290
|
-
if (!config.features.includes("store")) await removeIfExists(join(config.targetDir, "src/stores"));
|
|
291
|
-
if (!config.features.includes("store")) {
|
|
292
|
-
const layoutPath = join(config.targetDir, "src/routes/_layout.tsx");
|
|
293
|
-
if (existsSync(layoutPath)) {
|
|
294
|
-
let layout = await readFile(layoutPath, "utf-8");
|
|
295
|
-
layout = layout.replace(/import .* from '\.\.\/stores\/app'\n/g, "").replace(/.*useAppStore.*\n/g, "").replace(/\s*<button[\s\S]*?sidebar-toggle[\s\S]*?<\/button>\n/g, "");
|
|
296
|
-
await writeFile(layoutPath, layout);
|
|
1302
|
+
await applyAiTools(config);
|
|
1303
|
+
await applyIntegrations(config);
|
|
1304
|
+
if (config.template === "app") {
|
|
1305
|
+
if (!config.features.includes("feature")) await removeIfExists(join(config.targetDir, "src/features"));
|
|
1306
|
+
if (!config.features.includes("feature") || !config.features.includes("forms")) await removeIfExists(join(config.targetDir, "src/routes/posts/new.tsx"));
|
|
1307
|
+
if (!config.features.includes("store")) {
|
|
1308
|
+
await removeIfExists(join(config.targetDir, "src/stores"));
|
|
1309
|
+
const layoutPath = join(config.targetDir, "src/routes/_layout.tsx");
|
|
1310
|
+
if (existsSync(layoutPath)) {
|
|
1311
|
+
let layout = await readFile(layoutPath, "utf-8");
|
|
1312
|
+
layout = layout.replace(/import .* from '\.\.\/stores\/app'\n/g, "").replace(/.*useAppStore.*\n/g, "").replace(/\s*<button[\s\S]*?sidebar-toggle[\s\S]*?<\/button>\n/g, "");
|
|
1313
|
+
await writeFile(layoutPath, layout);
|
|
1314
|
+
}
|
|
297
1315
|
}
|
|
298
1316
|
}
|
|
299
1317
|
}
|
|
@@ -312,34 +1330,32 @@ function generatePackageJson(config) {
|
|
|
312
1330
|
"@pyreon/server": pyreonVersion("@pyreon/server"),
|
|
313
1331
|
"@pyreon/zero": pyreonVersion("@pyreon/zero")
|
|
314
1332
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
} else {
|
|
327
|
-
const allDeps = /* @__PURE__ */ new Set();
|
|
328
|
-
for (const key of config.features) {
|
|
329
|
-
const feature = FEATURES[key];
|
|
330
|
-
if (feature) for (const dep of feature.deps) allDeps.add(dep);
|
|
331
|
-
}
|
|
332
|
-
for (const dep of allDeps) if (dep.startsWith("@pyreon/")) deps[dep] = pyreonVersion(dep);
|
|
333
|
-
else if (dep.startsWith("@tanstack/")) deps[dep] = dep.includes("query") ? "^5.90.0" : dep.includes("table") ? "^8.21.0" : "^3.13.0";
|
|
334
|
-
else if (dep === "zod") deps[dep] = "^4.0.0";
|
|
1333
|
+
const allFeatureDeps = /* @__PURE__ */ new Set();
|
|
1334
|
+
for (const key of config.features) {
|
|
1335
|
+
const feature = FEATURES[key];
|
|
1336
|
+
if (feature) for (const dep of feature.deps) allFeatureDeps.add(dep);
|
|
1337
|
+
}
|
|
1338
|
+
if (config.template === "app") {
|
|
1339
|
+
allFeatureDeps.add("@pyreon/query");
|
|
1340
|
+
allFeatureDeps.add("@tanstack/query-core");
|
|
1341
|
+
allFeatureDeps.add("@pyreon/store");
|
|
335
1342
|
}
|
|
1343
|
+
if (config.template === "dashboard") {
|
|
1344
|
+
allFeatureDeps.add("@pyreon/document-primitives");
|
|
1345
|
+
allFeatureDeps.add("@pyreon/document");
|
|
1346
|
+
allFeatureDeps.add("@pyreon/connector-document");
|
|
1347
|
+
}
|
|
1348
|
+
for (const dep of allFeatureDeps) if (dep.startsWith("@pyreon/")) deps[dep] = pyreonVersion(dep);
|
|
1349
|
+
else if (dep.startsWith("@tanstack/")) deps[dep] = dep.includes("query") ? "^5.90.0" : dep.includes("table") ? "^8.21.0" : "^3.13.0";
|
|
1350
|
+
else if (dep === "zod") deps[dep] = "^4.0.0";
|
|
1351
|
+
if (config.packageStrategy === "meta") deps["@pyreon/meta"] = pyreonVersion("@pyreon/meta");
|
|
336
1352
|
const devDeps = {
|
|
337
1353
|
"@pyreon/vite-plugin": pyreonVersion("@pyreon/vite-plugin"),
|
|
338
1354
|
"@pyreon/zero-cli": pyreonVersion("@pyreon/zero-cli"),
|
|
339
1355
|
typescript: "^6.0.2",
|
|
340
1356
|
vite: "^8.0.3"
|
|
341
1357
|
};
|
|
342
|
-
if (config.
|
|
1358
|
+
if (config.aiTools.includes("mcp")) devDeps["@pyreon/mcp"] = pyreonVersion("@pyreon/mcp");
|
|
343
1359
|
const compatPkgMap = {
|
|
344
1360
|
react: "@pyreon/react-compat",
|
|
345
1361
|
vue: "@pyreon/vue-compat",
|
|
@@ -348,6 +1364,7 @@ function generatePackageJson(config) {
|
|
|
348
1364
|
};
|
|
349
1365
|
if (config.compat !== "none" && compatPkgMap[config.compat]) deps[compatPkgMap[config.compat]] = pyreonVersion(compatPkgMap[config.compat]);
|
|
350
1366
|
if (config.lint) devDeps["@pyreon/lint"] = pyreonVersion("@pyreon/lint");
|
|
1367
|
+
for (const [name, version] of Object.entries(integrationDeps(config))) deps[name] = version === "workspace:^" ? pyreonVersion(name) : version;
|
|
351
1368
|
const scripts = {
|
|
352
1369
|
dev: "zero dev",
|
|
353
1370
|
build: "zero build",
|
|
@@ -369,20 +1386,25 @@ function generatePackageJson(config) {
|
|
|
369
1386
|
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
370
1387
|
}
|
|
371
1388
|
function generateViteConfig(config) {
|
|
1389
|
+
const modeMap = {
|
|
1390
|
+
"ssr-stream": `mode: 'ssr', ssr: { mode: 'stream' }`,
|
|
1391
|
+
"ssr-string": `mode: 'ssr'`,
|
|
1392
|
+
ssg: `mode: 'ssg'`,
|
|
1393
|
+
spa: `mode: 'spa'`
|
|
1394
|
+
};
|
|
1395
|
+
const pyreonOpts = config.compat !== "none" ? `{ compat: '${config.compat}' }` : "";
|
|
1396
|
+
const adapter = adapterFor(config.adapter);
|
|
1397
|
+
const adapterImport = adapter.viteFactory ? `\nimport { ${adapter.viteFactory} } from '@pyreon/zero/server'` : "";
|
|
1398
|
+
const adapterArg = adapter.viteFactory ? `, adapter: ${adapter.viteFactory}()` : "";
|
|
372
1399
|
return `import pyreon from '@pyreon/vite-plugin'
|
|
373
|
-
import zero from '@pyreon/zero/server'
|
|
1400
|
+
import zero from '@pyreon/zero/server'${adapterImport}
|
|
374
1401
|
import { fontPlugin } from '@pyreon/zero/font'
|
|
375
1402
|
import { seoPlugin } from '@pyreon/zero/seo'
|
|
376
1403
|
|
|
377
1404
|
export default {
|
|
378
1405
|
plugins: [
|
|
379
|
-
pyreon(${
|
|
380
|
-
zero({ ${{
|
|
381
|
-
"ssr-stream": `mode: 'ssr', ssr: { mode: 'stream' }`,
|
|
382
|
-
"ssr-string": `mode: 'ssr'`,
|
|
383
|
-
ssg: `mode: 'ssg'`,
|
|
384
|
-
spa: `mode: 'spa'`
|
|
385
|
-
}[config.renderMode]} }),
|
|
1406
|
+
pyreon(${pyreonOpts}),
|
|
1407
|
+
zero({ ${modeMap[config.renderMode]}${adapterArg} }),
|
|
386
1408
|
|
|
387
1409
|
// Google Fonts — self-hosted at build time, CDN in dev
|
|
388
1410
|
fontPlugin({
|
|
@@ -461,6 +1483,47 @@ async function removeIfExists(path) {
|
|
|
461
1483
|
const { rm } = await import("node:fs/promises");
|
|
462
1484
|
await rm(path, { recursive: true });
|
|
463
1485
|
}
|
|
1486
|
+
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/index.ts
|
|
1489
|
+
/**
|
|
1490
|
+
* Detect which bin alias the user invoked. The same package ships two
|
|
1491
|
+
* entry points: `create-pyreon-app` (canonical, discoverable via
|
|
1492
|
+
* `bunx create-pyreon-app`) and `create-zero` (back-compat alias for
|
|
1493
|
+
* users following older docs / `bun create @pyreon/zero` flow).
|
|
1494
|
+
*
|
|
1495
|
+
* The --help text echoes the alias the user actually typed so docs
|
|
1496
|
+
* links and copy-paste invocations stay consistent.
|
|
1497
|
+
*/
|
|
1498
|
+
function detectInvocation() {
|
|
1499
|
+
return (process.argv[1] ?? "").includes("create-pyreon-app") ? "create-pyreon-app" : "create-zero";
|
|
1500
|
+
}
|
|
1501
|
+
async function main() {
|
|
1502
|
+
const invokedAs = detectInvocation();
|
|
1503
|
+
let args;
|
|
1504
|
+
try {
|
|
1505
|
+
args = parseArgs(process.argv.slice(2));
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1508
|
+
process.exit(2);
|
|
1509
|
+
}
|
|
1510
|
+
if (args.help) {
|
|
1511
|
+
console.log(helpText(invokedAs));
|
|
1512
|
+
process.exit(0);
|
|
1513
|
+
}
|
|
1514
|
+
p.intro(invokedAs === "create-pyreon-app" ? "Create a new Pyreon project" : "Pyreon Zero");
|
|
1515
|
+
const config = await runPrompts(args);
|
|
1516
|
+
const s = p.spinner();
|
|
1517
|
+
s.start("Scaffolding project...");
|
|
1518
|
+
await scaffold(config);
|
|
1519
|
+
s.stop("Project created!");
|
|
1520
|
+
p.note([
|
|
1521
|
+
`cd ${config.name}`,
|
|
1522
|
+
"bun install",
|
|
1523
|
+
"bun run dev"
|
|
1524
|
+
].join("\n"), "Next steps");
|
|
1525
|
+
p.outro("Happy building!");
|
|
1526
|
+
}
|
|
464
1527
|
main().catch((err) => {
|
|
465
1528
|
console.error(err);
|
|
466
1529
|
process.exit(1);
|