create-pracht 0.2.6 → 0.4.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 +38 -0
- package/package.json +3 -2
- package/skills/add-auth/SKILL.md +346 -0
- package/skills/add-db/SKILL.md +316 -0
- package/skills/add-i18n/SKILL.md +239 -0
- package/skills/add-observability/SKILL.md +270 -0
- package/skills/audit-a11y/SKILL.md +170 -0
- package/skills/audit-auth/SKILL.md +144 -0
- package/skills/audit-bundles/SKILL.md +167 -0
- package/skills/audit-csrf/SKILL.md +179 -0
- package/skills/audit-deps/SKILL.md +138 -0
- package/skills/audit-headers/SKILL.md +186 -0
- package/skills/audit-islands/SKILL.md +144 -0
- package/skills/audit-loaders/SKILL.md +135 -0
- package/skills/audit-redirects/SKILL.md +146 -0
- package/skills/audit-secrets/SKILL.md +150 -0
- package/skills/audit-seo/SKILL.md +164 -0
- package/skills/audit-shells/SKILL.md +142 -0
- package/skills/configure-isg/SKILL.md +172 -0
- package/skills/migrate-nextjs/SKILL.md +519 -0
- package/skills/pracht-debug/SKILL.md +146 -0
- package/skills/pracht-deploy/SKILL.md +208 -0
- package/skills/pracht-scaffold/SKILL.md +191 -0
- package/skills/pracht-test-api/SKILL.md +165 -0
- package/skills/pre-deploy/SKILL.md +164 -0
- package/skills/scaffold-e2e/SKILL.md +189 -0
- package/skills/scaffold-tests/SKILL.md +283 -0
- package/skills/tune-render-mode/SKILL.md +168 -0
- package/skills/typed-routes/SKILL.md +203 -0
- package/skills/upgrade-pracht/SKILL.md +151 -0
- package/src/index.js +472 -31
package/src/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { copyFile, mkdir, readdir, stat, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { copyFile, mkdir, readFile, readdir, stat, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
export class ValidationError extends Error {
|
|
8
9
|
constructor(message) {
|
|
@@ -18,7 +19,9 @@ const FALLBACK_VERSION_RANGES = {
|
|
|
18
19
|
"@pracht/cli": "^1.3.1",
|
|
19
20
|
"@pracht/core": "^0.5.0",
|
|
20
21
|
"@pracht/vite-plugin": "^0.3.2",
|
|
21
|
-
|
|
22
|
+
"@tailwindcss/vite": "^4.1.0",
|
|
23
|
+
tailwindcss: "^4.1.0",
|
|
24
|
+
vercel: "^56.5.0",
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
async function fetchLatestVersion(packageName) {
|
|
@@ -56,6 +59,12 @@ const ADAPTERS = {
|
|
|
56
59
|
|
|
57
60
|
const DEFAULT_DIRECTORY = "pracht-app";
|
|
58
61
|
|
|
62
|
+
const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
63
|
+
|
|
64
|
+
// The published package bundles a copy of the repo skills (see
|
|
65
|
+
// scripts/sync-skills.js); inside the monorepo we fall back to the source.
|
|
66
|
+
const SKILL_DIRS = [resolve(PACKAGE_ROOT, "skills"), resolve(PACKAGE_ROOT, "../../skills")];
|
|
67
|
+
|
|
59
68
|
export async function run(argv = process.argv.slice(2)) {
|
|
60
69
|
const options = parseArgs(argv);
|
|
61
70
|
const packageManager = getPackageManager();
|
|
@@ -68,12 +77,22 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
68
77
|
const dir = options.dir ?? (options.yes ? DEFAULT_DIRECTORY : null);
|
|
69
78
|
const adapterId = options.adapter ?? (options.yes ? "node" : null);
|
|
70
79
|
const router = options.router ?? (options.yes ? "manifest" : null);
|
|
80
|
+
const tailwind = options.tailwind ?? (options.yes ? false : null);
|
|
81
|
+
const agentTools = options.agentTools ?? (options.yes ? true : null);
|
|
71
82
|
|
|
72
83
|
let resolvedDir = dir;
|
|
73
84
|
let resolvedAdapter = adapterId;
|
|
74
85
|
let resolvedRouter = router;
|
|
86
|
+
let resolvedTailwind = tailwind;
|
|
87
|
+
let resolvedAgentTools = agentTools;
|
|
75
88
|
|
|
76
|
-
if (
|
|
89
|
+
if (
|
|
90
|
+
resolvedDir == null ||
|
|
91
|
+
resolvedAdapter == null ||
|
|
92
|
+
resolvedRouter == null ||
|
|
93
|
+
resolvedTailwind == null ||
|
|
94
|
+
resolvedAgentTools == null
|
|
95
|
+
) {
|
|
77
96
|
const readline = createInterface({
|
|
78
97
|
input: process.stdin,
|
|
79
98
|
output: process.stdout,
|
|
@@ -83,6 +102,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
83
102
|
resolvedDir = resolvedDir ?? (await promptForDirectory(readline));
|
|
84
103
|
resolvedAdapter = resolvedAdapter ?? (await promptForAdapter(readline));
|
|
85
104
|
resolvedRouter = resolvedRouter ?? (await promptForRouter(readline));
|
|
105
|
+
resolvedTailwind = resolvedTailwind ?? (await promptForTailwind(readline));
|
|
106
|
+
resolvedAgentTools = resolvedAgentTools ?? (await promptForAgentTools(readline));
|
|
86
107
|
} finally {
|
|
87
108
|
readline.close();
|
|
88
109
|
}
|
|
@@ -95,10 +116,12 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
95
116
|
if (options.dryRun) {
|
|
96
117
|
const files = await buildProjectFiles({
|
|
97
118
|
adapter: ADAPTERS[resolvedAdapter],
|
|
119
|
+
agentTools: resolvedAgentTools,
|
|
98
120
|
packageManager,
|
|
99
121
|
projectName: toPackageName(basename(targetDir)),
|
|
100
122
|
resolveRemoteVersions: false,
|
|
101
123
|
router: resolvedRouter,
|
|
124
|
+
tailwind: resolvedTailwind,
|
|
102
125
|
});
|
|
103
126
|
|
|
104
127
|
const fileList = Object.keys(files).sort();
|
|
@@ -107,10 +130,12 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
107
130
|
console.log(
|
|
108
131
|
JSON.stringify({
|
|
109
132
|
adapter: resolvedAdapter,
|
|
133
|
+
agentTools: resolvedAgentTools,
|
|
110
134
|
directory: resolvedDir,
|
|
111
135
|
dryRun: true,
|
|
112
136
|
files: fileList,
|
|
113
137
|
router: resolvedRouter,
|
|
138
|
+
tailwind: resolvedTailwind,
|
|
114
139
|
}),
|
|
115
140
|
);
|
|
116
141
|
} else {
|
|
@@ -126,8 +151,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
126
151
|
|
|
127
152
|
await scaffoldProject({
|
|
128
153
|
adapter: ADAPTERS[resolvedAdapter],
|
|
154
|
+
agentTools: resolvedAgentTools,
|
|
129
155
|
packageManager,
|
|
130
156
|
router: resolvedRouter,
|
|
157
|
+
tailwind: resolvedTailwind,
|
|
131
158
|
targetDir,
|
|
132
159
|
});
|
|
133
160
|
|
|
@@ -138,22 +165,44 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
138
165
|
installSucceeded = await installDependencies(targetDir, packageManager);
|
|
139
166
|
}
|
|
140
167
|
|
|
168
|
+
let gitInitialized = false;
|
|
169
|
+
if (options.git) {
|
|
170
|
+
const gitResult = await initGitRepository(targetDir);
|
|
171
|
+
gitInitialized = gitResult.initialized;
|
|
172
|
+
|
|
173
|
+
if (gitResult.initialized) {
|
|
174
|
+
log("");
|
|
175
|
+
log("Initialized a git repository with an initial commit.");
|
|
176
|
+
} else if (gitResult.reason === "existing-repo") {
|
|
177
|
+
log("");
|
|
178
|
+
log("Skipped git init — the target directory is already inside a git repository.");
|
|
179
|
+
} else if (gitResult.reason === "git-not-found") {
|
|
180
|
+
log("");
|
|
181
|
+
log("Skipped git init — git is not available on this machine.");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
141
185
|
if (options.json) {
|
|
142
186
|
const files = await buildProjectFiles({
|
|
143
187
|
adapter: ADAPTERS[resolvedAdapter],
|
|
188
|
+
agentTools: resolvedAgentTools,
|
|
144
189
|
packageManager,
|
|
145
190
|
projectName: toPackageName(basename(targetDir)),
|
|
146
191
|
resolveRemoteVersions: false,
|
|
147
192
|
router: resolvedRouter,
|
|
193
|
+
tailwind: resolvedTailwind,
|
|
148
194
|
});
|
|
149
195
|
|
|
150
196
|
console.log(
|
|
151
197
|
JSON.stringify({
|
|
152
198
|
adapter: resolvedAdapter,
|
|
199
|
+
agentTools: resolvedAgentTools,
|
|
153
200
|
directory: resolvedDir,
|
|
154
201
|
files: Object.keys(files).sort(),
|
|
202
|
+
gitInitialized,
|
|
155
203
|
installed: options.skipInstall ? false : installSucceeded,
|
|
156
204
|
router: resolvedRouter,
|
|
205
|
+
tailwind: resolvedTailwind,
|
|
157
206
|
}),
|
|
158
207
|
);
|
|
159
208
|
} else {
|
|
@@ -167,13 +216,24 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
167
216
|
}
|
|
168
217
|
}
|
|
169
218
|
|
|
170
|
-
export async function scaffoldProject({
|
|
219
|
+
export async function scaffoldProject({
|
|
220
|
+
adapter,
|
|
221
|
+
agentTools = true,
|
|
222
|
+
packageManager,
|
|
223
|
+
resolveRemoteVersions = true,
|
|
224
|
+
router = "manifest",
|
|
225
|
+
tailwind = false,
|
|
226
|
+
targetDir,
|
|
227
|
+
}) {
|
|
171
228
|
const packageName = toPackageName(basename(targetDir));
|
|
172
229
|
const files = await buildProjectFiles({
|
|
173
230
|
adapter,
|
|
231
|
+
agentTools,
|
|
174
232
|
packageManager,
|
|
175
233
|
projectName: packageName,
|
|
234
|
+
resolveRemoteVersions,
|
|
176
235
|
router,
|
|
236
|
+
tailwind,
|
|
177
237
|
});
|
|
178
238
|
|
|
179
239
|
await mkdir(targetDir, { recursive: true });
|
|
@@ -205,11 +265,14 @@ export function getPackageManager(userAgent = process.env.npm_config_user_agent
|
|
|
205
265
|
export function parseArgs(argv) {
|
|
206
266
|
const options = {
|
|
207
267
|
adapter: undefined,
|
|
268
|
+
agentTools: undefined,
|
|
208
269
|
dir: undefined,
|
|
209
270
|
dryRun: false,
|
|
271
|
+
git: true,
|
|
210
272
|
json: false,
|
|
211
273
|
router: undefined,
|
|
212
274
|
skipInstall: false,
|
|
275
|
+
tailwind: undefined,
|
|
213
276
|
yes: false,
|
|
214
277
|
};
|
|
215
278
|
|
|
@@ -219,6 +282,42 @@ export function parseArgs(argv) {
|
|
|
219
282
|
continue;
|
|
220
283
|
}
|
|
221
284
|
|
|
285
|
+
if (arg === "--tailwind") {
|
|
286
|
+
options.tailwind = true;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (arg === "--no-tailwind") {
|
|
291
|
+
options.tailwind = false;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (arg === "--no-git") {
|
|
296
|
+
options.git = false;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (arg === "--agent-tools") {
|
|
301
|
+
options.agentTools = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (arg === "--no-agent-tools") {
|
|
306
|
+
options.agentTools = false;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (arg.startsWith("--template=")) {
|
|
311
|
+
const value = normalizeTemplate(arg.slice("--template=".length));
|
|
312
|
+
if (!value) {
|
|
313
|
+
throw new ValidationError(
|
|
314
|
+
`Invalid template: ${arg.slice("--template=".length)}. Use minimal or tailwind.`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
options.tailwind = value === "tailwind";
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
222
321
|
if (arg === "--yes" || arg === "-y") {
|
|
223
322
|
options.yes = true;
|
|
224
323
|
continue;
|
|
@@ -322,6 +421,46 @@ async function promptForRouter(readline) {
|
|
|
322
421
|
}
|
|
323
422
|
}
|
|
324
423
|
|
|
424
|
+
async function promptForTailwind(readline) {
|
|
425
|
+
while (true) {
|
|
426
|
+
const answer = await readline.question("Use Tailwind CSS? (y/N): ");
|
|
427
|
+
const normalized = normalizeYesNo(answer.trim() || "no");
|
|
428
|
+
|
|
429
|
+
if (normalized != null) {
|
|
430
|
+
return normalized;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log("Answer y/yes or n/no.");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function promptForAgentTools(readline) {
|
|
438
|
+
while (true) {
|
|
439
|
+
const answer = await readline.question("Set up Claude Code skills + MCP? (Y/n): ");
|
|
440
|
+
const normalized = normalizeYesNo(answer.trim() || "yes");
|
|
441
|
+
|
|
442
|
+
if (normalized != null) {
|
|
443
|
+
return normalized;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log("Answer y/yes or n/no.");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function normalizeYesNo(value) {
|
|
451
|
+
const normalized = value.toLowerCase();
|
|
452
|
+
|
|
453
|
+
if (normalized === "y" || normalized === "yes") {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (normalized === "n" || normalized === "no") {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
325
464
|
async function ensureTargetDirectory(targetDir) {
|
|
326
465
|
const error = await validateTargetDirectory(targetDir);
|
|
327
466
|
|
|
@@ -348,6 +487,20 @@ async function validateTargetDirectory(targetDir) {
|
|
|
348
487
|
return null;
|
|
349
488
|
}
|
|
350
489
|
|
|
490
|
+
function normalizeTemplate(value) {
|
|
491
|
+
const normalized = value.toLowerCase();
|
|
492
|
+
|
|
493
|
+
if (normalized === "minimal") {
|
|
494
|
+
return "minimal";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (normalized === "tailwind") {
|
|
498
|
+
return "tailwind";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
351
504
|
function normalizeRouter(value) {
|
|
352
505
|
const normalized = value.toLowerCase();
|
|
353
506
|
|
|
@@ -402,10 +555,12 @@ async function resolveVersions(packageNames, { remote = true } = {}) {
|
|
|
402
555
|
|
|
403
556
|
async function buildProjectFiles({
|
|
404
557
|
adapter,
|
|
558
|
+
agentTools = true,
|
|
405
559
|
packageManager,
|
|
406
560
|
projectName,
|
|
407
561
|
resolveRemoteVersions = true,
|
|
408
562
|
router,
|
|
563
|
+
tailwind = false,
|
|
409
564
|
}) {
|
|
410
565
|
const packagesToResolve = [
|
|
411
566
|
"@pracht/cli",
|
|
@@ -416,26 +571,41 @@ async function buildProjectFiles({
|
|
|
416
571
|
if (adapter.id === "vercel") {
|
|
417
572
|
packagesToResolve.push("vercel");
|
|
418
573
|
}
|
|
574
|
+
if (tailwind) {
|
|
575
|
+
packagesToResolve.push("tailwindcss", "@tailwindcss/vite");
|
|
576
|
+
}
|
|
419
577
|
|
|
420
578
|
const versions = await resolveVersions(packagesToResolve, { remote: resolveRemoteVersions });
|
|
421
579
|
|
|
422
580
|
const files = {
|
|
423
|
-
".gitignore":
|
|
424
|
-
|
|
425
|
-
"
|
|
581
|
+
".gitignore":
|
|
582
|
+
"dist\nnode_modules\n.wrangler\n.vercel\n.env*\n!.env.example\n.dev.vars\n# Keep .pracht/app-graph.json committed — it is the `pracht plan` snapshot.\n",
|
|
583
|
+
"README.md": createReadme({
|
|
584
|
+
adapter,
|
|
585
|
+
agentTools,
|
|
586
|
+
packageManager,
|
|
587
|
+
projectName,
|
|
588
|
+
router,
|
|
589
|
+
tailwind,
|
|
590
|
+
}),
|
|
591
|
+
"package.json": createPackageJson({ adapter, projectName, tailwind, versions }),
|
|
426
592
|
"src/api/health.ts": createHealthRoute(adapter),
|
|
427
|
-
"vite.config.ts": createViteConfig(adapter, router),
|
|
593
|
+
"vite.config.ts": createViteConfig(adapter, router, tailwind),
|
|
428
594
|
"tsconfig.json": createBaseTSConfig(adapter),
|
|
429
|
-
"AGENTS.md": createAgentInstructions({ adapter, packageManager, router }),
|
|
595
|
+
"AGENTS.md": createAgentInstructions({ adapter, agentTools, packageManager, router, tailwind }),
|
|
430
596
|
};
|
|
431
597
|
|
|
432
598
|
if (router === "pages") {
|
|
433
|
-
files["src/pages/_app.tsx"] = createShellFile(projectName);
|
|
599
|
+
files["src/pages/_app.tsx"] = createShellFile(projectName, tailwind);
|
|
434
600
|
files["src/pages/index.tsx"] = createPagesHomeRoute(adapter);
|
|
435
601
|
} else {
|
|
436
602
|
files["src/routes.ts"] = createRoutesFile();
|
|
437
603
|
files["src/routes/home.tsx"] = createHomeRoute(adapter);
|
|
438
|
-
files["src/shells/public.tsx"] = createShellFile(projectName);
|
|
604
|
+
files["src/shells/public.tsx"] = createShellFile(projectName, tailwind);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (tailwind) {
|
|
608
|
+
files["src/styles/global.css"] = '@import "tailwindcss";\n';
|
|
439
609
|
}
|
|
440
610
|
|
|
441
611
|
if (adapter.id === "cloudflare") {
|
|
@@ -443,16 +613,61 @@ async function buildProjectFiles({
|
|
|
443
613
|
files["src/env.d.ts"] = createCloudflareEnvDeclaration();
|
|
444
614
|
}
|
|
445
615
|
|
|
616
|
+
if (adapter.id === "node") {
|
|
617
|
+
files["Dockerfile"] = createDockerfile(packageManager);
|
|
618
|
+
files[".dockerignore"] = createDockerignore();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (agentTools) {
|
|
622
|
+
files[".mcp.json"] = createMcpConfig();
|
|
623
|
+
Object.assign(files, await readSkillFiles());
|
|
624
|
+
}
|
|
625
|
+
|
|
446
626
|
return files;
|
|
447
627
|
}
|
|
448
628
|
|
|
449
|
-
function
|
|
629
|
+
function createMcpConfig() {
|
|
630
|
+
return `${JSON.stringify(
|
|
631
|
+
{
|
|
632
|
+
mcpServers: {
|
|
633
|
+
pracht: {
|
|
634
|
+
command: "npx",
|
|
635
|
+
args: ["pracht", "mcp"],
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
null,
|
|
640
|
+
2,
|
|
641
|
+
)}\n`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function readSkillFiles() {
|
|
645
|
+
const skillsDir = SKILL_DIRS.find((dir) => existsSync(dir));
|
|
646
|
+
|
|
647
|
+
if (!skillsDir) {
|
|
648
|
+
return {};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const files = {};
|
|
652
|
+
for (const name of await readdir(skillsDir)) {
|
|
653
|
+
const skillFile = resolve(skillsDir, name, "SKILL.md");
|
|
654
|
+
if (!existsSync(skillFile)) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
files[`.claude/skills/${name}/SKILL.md`] = await readFile(skillFile, "utf-8");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return files;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function createPackageJson({ adapter, projectName, tailwind, versions }) {
|
|
450
664
|
const scripts = {
|
|
451
665
|
build: "pracht build",
|
|
452
666
|
dev: "pracht dev",
|
|
453
667
|
};
|
|
454
668
|
|
|
455
669
|
if (adapter.id === "node") {
|
|
670
|
+
scripts.preview = "pracht preview";
|
|
456
671
|
scripts.start = "node dist/server/server.js";
|
|
457
672
|
}
|
|
458
673
|
|
|
@@ -466,6 +681,7 @@ function createPackageJson({ adapter, projectName, versions }) {
|
|
|
466
681
|
|
|
467
682
|
if (adapter.id === "cloudflare") {
|
|
468
683
|
scripts.deploy = "pracht build && wrangler deploy";
|
|
684
|
+
scripts.preview = "pracht preview";
|
|
469
685
|
devDependencies.wrangler = "^4.81.0";
|
|
470
686
|
}
|
|
471
687
|
|
|
@@ -474,6 +690,11 @@ function createPackageJson({ adapter, projectName, versions }) {
|
|
|
474
690
|
devDependencies.vercel = versions["vercel"];
|
|
475
691
|
}
|
|
476
692
|
|
|
693
|
+
if (tailwind) {
|
|
694
|
+
devDependencies["@tailwindcss/vite"] = versions["@tailwindcss/vite"];
|
|
695
|
+
devDependencies.tailwindcss = versions["tailwindcss"];
|
|
696
|
+
}
|
|
697
|
+
|
|
477
698
|
return `${JSON.stringify(
|
|
478
699
|
{
|
|
479
700
|
dependencies: {
|
|
@@ -492,7 +713,7 @@ function createPackageJson({ adapter, projectName, versions }) {
|
|
|
492
713
|
)}\n`;
|
|
493
714
|
}
|
|
494
715
|
|
|
495
|
-
function createViteConfig(adapter, router) {
|
|
716
|
+
function createViteConfig(adapter, router, tailwind) {
|
|
496
717
|
const ADAPTER_IMPORTS = {
|
|
497
718
|
node: { fn: "nodeAdapter", pkg: "@pracht/adapter-node" },
|
|
498
719
|
cloudflare: { fn: "cloudflareAdapter", pkg: "@pracht/adapter-cloudflare" },
|
|
@@ -506,16 +727,23 @@ function createViteConfig(adapter, router) {
|
|
|
506
727
|
? `{ pagesDir: "/src/pages", adapter: ${info.fn}() }`
|
|
507
728
|
: `{ adapter: ${info.fn}() }`;
|
|
508
729
|
|
|
509
|
-
|
|
730
|
+
const plugins = tailwind
|
|
731
|
+
? `[pracht(${prachtOptions}), tailwindcss()]`
|
|
732
|
+
: `[pracht(${prachtOptions})]`;
|
|
733
|
+
|
|
734
|
+
const lines = [
|
|
510
735
|
'import { defineConfig } from "vite";',
|
|
511
736
|
'import { pracht } from "@pracht/vite-plugin";',
|
|
512
737
|
`import { ${info.fn} } from "${info.pkg}";`,
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
"
|
|
517
|
-
|
|
518
|
-
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
if (tailwind) {
|
|
741
|
+
lines.push('import tailwindcss from "@tailwindcss/vite";');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
lines.push("", "export default defineConfig({", ` plugins: ${plugins},`, "});", "");
|
|
745
|
+
|
|
746
|
+
return lines.join("\n");
|
|
519
747
|
}
|
|
520
748
|
|
|
521
749
|
function createRoutesFile() {
|
|
@@ -529,14 +757,27 @@ function createRoutesFile() {
|
|
|
529
757
|
" routes: [",
|
|
530
758
|
' route("/", "./routes/home.tsx", { id: "home", render: "ssg", shell: "public" }),',
|
|
531
759
|
" ],",
|
|
760
|
+
" // Custom 404 page — any module in ./routes, rendered when nothing matches:",
|
|
761
|
+
' // notFound: "./routes/not-found.tsx",',
|
|
762
|
+
" // Declarative invariants enforced by `pracht verify` — uncomment to use",
|
|
763
|
+
" // (add the helpers to the @pracht/core import):",
|
|
764
|
+
" // constraints: [",
|
|
765
|
+
' // requireHead("**"),',
|
|
766
|
+
" // ],",
|
|
532
767
|
"});",
|
|
533
768
|
"",
|
|
534
769
|
].join("\n");
|
|
535
770
|
}
|
|
536
771
|
|
|
537
|
-
function createShellFile(projectName) {
|
|
772
|
+
function createShellFile(projectName, tailwind = false) {
|
|
773
|
+
const lines = ['import type { ShellProps } from "@pracht/core";'];
|
|
774
|
+
|
|
775
|
+
if (tailwind) {
|
|
776
|
+
lines.push('import "../styles/global.css";');
|
|
777
|
+
}
|
|
778
|
+
|
|
538
779
|
return [
|
|
539
|
-
|
|
780
|
+
...lines,
|
|
540
781
|
"",
|
|
541
782
|
"export function Shell({ children }: ShellProps) {",
|
|
542
783
|
" return (",
|
|
@@ -705,7 +946,80 @@ function createCloudflareEnvDeclaration() {
|
|
|
705
946
|
].join("\n");
|
|
706
947
|
}
|
|
707
948
|
|
|
708
|
-
function
|
|
949
|
+
function createDockerfile(packageManager) {
|
|
950
|
+
const COMMANDS = {
|
|
951
|
+
npm: {
|
|
952
|
+
build: "npm run build",
|
|
953
|
+
install: "npm install",
|
|
954
|
+
lockfile: "package-lock.json*",
|
|
955
|
+
prune: "npm prune --omit=dev",
|
|
956
|
+
setup: null,
|
|
957
|
+
},
|
|
958
|
+
pnpm: {
|
|
959
|
+
build: "pnpm build",
|
|
960
|
+
install: "pnpm install",
|
|
961
|
+
lockfile: "pnpm-lock.yaml*",
|
|
962
|
+
prune: "pnpm prune --prod",
|
|
963
|
+
setup: "corepack enable pnpm",
|
|
964
|
+
},
|
|
965
|
+
yarn: {
|
|
966
|
+
build: "yarn build",
|
|
967
|
+
install: "yarn install",
|
|
968
|
+
lockfile: "yarn.lock*",
|
|
969
|
+
prune: "yarn install --production --ignore-scripts --prefer-offline",
|
|
970
|
+
setup: "corepack enable yarn",
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// The runtime image ships Node.js, so bun scaffolds fall back to npm inside Docker.
|
|
975
|
+
const commands = COMMANDS[packageManager] ?? COMMANDS.npm;
|
|
976
|
+
|
|
977
|
+
const lines = ["# syntax=docker/dockerfile:1", "", "FROM node:22-alpine AS base", "WORKDIR /app"];
|
|
978
|
+
|
|
979
|
+
if (commands.setup) {
|
|
980
|
+
lines.push(`RUN ${commands.setup}`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
lines.push(
|
|
984
|
+
"",
|
|
985
|
+
"FROM base AS deps",
|
|
986
|
+
`COPY package.json ${commands.lockfile} ./`,
|
|
987
|
+
`RUN ${commands.install}`,
|
|
988
|
+
"",
|
|
989
|
+
"FROM deps AS build",
|
|
990
|
+
"COPY . .",
|
|
991
|
+
`RUN ${commands.build}`,
|
|
992
|
+
`RUN ${commands.prune}`,
|
|
993
|
+
"",
|
|
994
|
+
"FROM node:22-alpine AS runtime",
|
|
995
|
+
"WORKDIR /app",
|
|
996
|
+
"ENV NODE_ENV=production",
|
|
997
|
+
"ENV PORT=3000",
|
|
998
|
+
"COPY --from=build /app/package.json ./package.json",
|
|
999
|
+
"COPY --from=build /app/node_modules ./node_modules",
|
|
1000
|
+
"COPY --from=build /app/dist ./dist",
|
|
1001
|
+
"EXPOSE 3000",
|
|
1002
|
+
'CMD ["node", "dist/server/server.js"]',
|
|
1003
|
+
"",
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
return lines.join("\n");
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function createDockerignore() {
|
|
1010
|
+
return [
|
|
1011
|
+
"node_modules",
|
|
1012
|
+
"dist",
|
|
1013
|
+
".git",
|
|
1014
|
+
".env*",
|
|
1015
|
+
"!.env.example",
|
|
1016
|
+
"Dockerfile",
|
|
1017
|
+
".dockerignore",
|
|
1018
|
+
"",
|
|
1019
|
+
].join("\n");
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function createAgentInstructions({ adapter, agentTools, packageManager, router, tailwind }) {
|
|
709
1023
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
710
1024
|
|
|
711
1025
|
const lines = [
|
|
@@ -717,6 +1031,10 @@ function createAgentInstructions({ adapter, packageManager, router }) {
|
|
|
717
1031
|
`- \`${runCmd} build\` — production build`,
|
|
718
1032
|
];
|
|
719
1033
|
|
|
1034
|
+
if (adapter.id === "node" || adapter.id === "cloudflare") {
|
|
1035
|
+
lines.push(`- \`${runCmd} preview\` — build and serve the production build locally`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
720
1038
|
if (adapter.id === "node") {
|
|
721
1039
|
lines.push(`- \`${runCmd} start\` — run the built server`);
|
|
722
1040
|
}
|
|
@@ -735,6 +1053,12 @@ function createAgentInstructions({ adapter, packageManager, router }) {
|
|
|
735
1053
|
lines.push("- `pracht generate middleware --name auth` — add middleware");
|
|
736
1054
|
lines.push("- `pracht generate api --path /health --methods GET` — add an API route");
|
|
737
1055
|
lines.push("- `pracht doctor` — check project health");
|
|
1056
|
+
lines.push("- `pracht verify` — enforce route and constraint invariants");
|
|
1057
|
+
lines.push(
|
|
1058
|
+
"- `pracht plan --write` — refresh the committed `.pracht/app-graph.json` snapshot after route changes",
|
|
1059
|
+
);
|
|
1060
|
+
lines.push("- `pracht report` — PR-ready markdown summary (plan diff, verify, budgets)");
|
|
1061
|
+
lines.push("- `pracht llms --write` — write an `llms.txt` authoring guide for coding agents");
|
|
738
1062
|
|
|
739
1063
|
lines.push("");
|
|
740
1064
|
lines.push("## Project structure");
|
|
@@ -756,19 +1080,40 @@ function createAgentInstructions({ adapter, packageManager, router }) {
|
|
|
756
1080
|
lines.push("- `src/api/` — API route handlers");
|
|
757
1081
|
lines.push(`- \`vite.config.ts\` — Vite config with the ${adapter.label} adapter`);
|
|
758
1082
|
|
|
1083
|
+
if (tailwind) {
|
|
1084
|
+
lines.push("- `src/styles/global.css` — Tailwind CSS entry stylesheet, imported by the shell");
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (adapter.id === "node") {
|
|
1088
|
+
lines.push("- `Dockerfile` — multi-stage container build that runs the built server");
|
|
1089
|
+
}
|
|
1090
|
+
|
|
759
1091
|
if (adapter.id === "cloudflare") {
|
|
760
1092
|
lines.push("- `wrangler.jsonc` — Cloudflare Workers configuration");
|
|
761
1093
|
lines.push("- `src/env.d.ts` — TypeScript types for Cloudflare bindings");
|
|
762
1094
|
}
|
|
763
1095
|
|
|
1096
|
+
if (agentTools) {
|
|
1097
|
+
lines.push("");
|
|
1098
|
+
lines.push("## Agent tooling");
|
|
1099
|
+
lines.push("");
|
|
1100
|
+
lines.push(
|
|
1101
|
+
"- `.claude/skills/` — pracht Claude Code skills (audits, scaffolds, testing, debugging); invoke with `/<skill-name>`",
|
|
1102
|
+
);
|
|
1103
|
+
lines.push(
|
|
1104
|
+
"- `.mcp.json` — registers the `pracht mcp` server so MCP clients can inspect the app graph, run doctor/verify, and scaffold natively",
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
764
1108
|
lines.push("");
|
|
765
1109
|
|
|
766
1110
|
return lines.join("\n");
|
|
767
1111
|
}
|
|
768
1112
|
|
|
769
|
-
function createReadme({ adapter, packageManager, projectName, router }) {
|
|
1113
|
+
function createReadme({ adapter, agentTools, packageManager, projectName, router, tailwind }) {
|
|
770
1114
|
const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
|
|
771
1115
|
const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
|
|
1116
|
+
const previewCommand = packageManager === "npm" ? "npm run preview" : `${packageManager} preview`;
|
|
772
1117
|
const startCommand = packageManager === "npm" ? "npm run start" : `${packageManager} start`;
|
|
773
1118
|
const deployCommand = packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
|
|
774
1119
|
|
|
@@ -784,10 +1129,12 @@ function createReadme({ adapter, packageManager, projectName, router }) {
|
|
|
784
1129
|
];
|
|
785
1130
|
|
|
786
1131
|
if (adapter.id === "node") {
|
|
1132
|
+
lines.push(`- \`${previewCommand}\``);
|
|
787
1133
|
lines.push(`- \`${startCommand}\``);
|
|
788
1134
|
}
|
|
789
1135
|
|
|
790
1136
|
if (adapter.id === "cloudflare") {
|
|
1137
|
+
lines.push(`- \`${previewCommand}\``);
|
|
791
1138
|
lines.push(`- \`${deployCommand}\``);
|
|
792
1139
|
lines.push("");
|
|
793
1140
|
lines.push(
|
|
@@ -816,9 +1163,98 @@ function createReadme({ adapter, packageManager, projectName, router }) {
|
|
|
816
1163
|
|
|
817
1164
|
lines.push("- `src/api/health.ts` is a sample API route.");
|
|
818
1165
|
|
|
1166
|
+
if (tailwind) {
|
|
1167
|
+
lines.push("- `src/styles/global.css` is the Tailwind CSS entry, imported by the shell.");
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (agentTools) {
|
|
1171
|
+
lines.push(
|
|
1172
|
+
"- `.claude/skills/` and `.mcp.json` wire up the pracht Claude Code skills and MCP server.",
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
lines.push("");
|
|
1177
|
+
lines.push("## Checks");
|
|
1178
|
+
lines.push("");
|
|
1179
|
+
lines.push(
|
|
1180
|
+
router === "pages"
|
|
1181
|
+
? "- `pracht verify` validates routes."
|
|
1182
|
+
: "- `pracht verify` validates routes and constraints.",
|
|
1183
|
+
);
|
|
1184
|
+
lines.push(
|
|
1185
|
+
"- `pracht plan --write` commits an app-graph snapshot to `.pracht/`; `pracht plan` diffs against it.",
|
|
1186
|
+
);
|
|
1187
|
+
lines.push("- `pracht report` prints a PR-ready summary of both.");
|
|
1188
|
+
|
|
1189
|
+
if (adapter.id === "node") {
|
|
1190
|
+
lines.push("");
|
|
1191
|
+
lines.push("## Docker");
|
|
1192
|
+
lines.push("");
|
|
1193
|
+
lines.push("A multi-stage `Dockerfile` builds the app and runs the Node server:");
|
|
1194
|
+
lines.push("");
|
|
1195
|
+
lines.push("```bash");
|
|
1196
|
+
lines.push(`docker build -t ${projectName} .`);
|
|
1197
|
+
lines.push(`docker run -p 3000:3000 ${projectName}`);
|
|
1198
|
+
lines.push("```");
|
|
1199
|
+
}
|
|
1200
|
+
|
|
819
1201
|
return `${lines.join("\n")}\n`;
|
|
820
1202
|
}
|
|
821
1203
|
|
|
1204
|
+
export async function initGitRepository(targetDir) {
|
|
1205
|
+
if (!(await execCommand("git", ["--version"]))) {
|
|
1206
|
+
return { initialized: false, reason: "git-not-found" };
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (await execCommand("git", ["rev-parse", "--is-inside-work-tree"], targetDir)) {
|
|
1210
|
+
return { initialized: false, reason: "existing-repo" };
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (!(await execCommand("git", ["init"], targetDir))) {
|
|
1214
|
+
return { initialized: false, reason: "init-failed" };
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (!(await execCommand("git", ["add", "-A"], targetDir))) {
|
|
1218
|
+
return { initialized: false, reason: "commit-failed" };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Fall back to a scoped identity when the user has no git identity configured,
|
|
1222
|
+
// so the initial commit still succeeds (e.g. on fresh machines or CI).
|
|
1223
|
+
const hasIdentity = await execCommand("git", ["config", "user.email"], targetDir);
|
|
1224
|
+
const identityArgs = hasIdentity
|
|
1225
|
+
? []
|
|
1226
|
+
: ["-c", "user.name=create-pracht", "-c", "user.email=create-pracht@localhost"];
|
|
1227
|
+
|
|
1228
|
+
const committed = await execCommand(
|
|
1229
|
+
"git",
|
|
1230
|
+
[...identityArgs, "commit", "-m", "Initial commit from create-pracht"],
|
|
1231
|
+
targetDir,
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
if (!committed) {
|
|
1235
|
+
return { initialized: false, reason: "commit-failed" };
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return { initialized: true };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function execCommand(command, args, cwd) {
|
|
1242
|
+
return new Promise((resolveExec) => {
|
|
1243
|
+
const child = spawn(command, args, {
|
|
1244
|
+
cwd,
|
|
1245
|
+
stdio: "ignore",
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
child.on("close", (code) => {
|
|
1249
|
+
resolveExec(code === 0);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
child.on("error", () => {
|
|
1253
|
+
resolveExec(false);
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
822
1258
|
async function installDependencies(targetDir, packageManager) {
|
|
823
1259
|
const args = packageManager === "yarn" ? ["install"] : ["install"];
|
|
824
1260
|
|
|
@@ -868,13 +1304,18 @@ Usage:
|
|
|
868
1304
|
create-pracht [directory] [options]
|
|
869
1305
|
|
|
870
1306
|
Options:
|
|
871
|
-
--adapter=node|cf|vercel
|
|
872
|
-
--router=manifest|pages
|
|
873
|
-
--
|
|
874
|
-
--
|
|
875
|
-
--
|
|
876
|
-
|
|
877
|
-
-
|
|
1307
|
+
--adapter=node|cf|vercel Choose hosting adapter (default: node)
|
|
1308
|
+
--router=manifest|pages Choose routing system (default: manifest)
|
|
1309
|
+
--template=minimal|tailwind Choose starter template (minimal, or minimal + Tailwind CSS)
|
|
1310
|
+
--tailwind / --no-tailwind Enable or disable Tailwind CSS wiring (default: prompt)
|
|
1311
|
+
--agent-tools / --no-agent-tools
|
|
1312
|
+
Seed Claude Code skills and a pracht MCP config (default: prompt, yes)
|
|
1313
|
+
--no-git Skip git init and the initial commit
|
|
1314
|
+
--skip-install Skip dependency installation
|
|
1315
|
+
--yes, -y Accept defaults, skip all prompts
|
|
1316
|
+
--json Output JSON summary instead of prose
|
|
1317
|
+
--dry-run Show which files would be created without writing
|
|
1318
|
+
-h, --help Show this help message
|
|
878
1319
|
`);
|
|
879
1320
|
}
|
|
880
1321
|
|