create-pracht 0.3.0 → 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/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) {
@@ -20,7 +21,7 @@ const FALLBACK_VERSION_RANGES = {
20
21
  "@pracht/vite-plugin": "^0.3.2",
21
22
  "@tailwindcss/vite": "^4.1.0",
22
23
  tailwindcss: "^4.1.0",
23
- vercel: "latest",
24
+ vercel: "^56.5.0",
24
25
  };
25
26
 
26
27
  async function fetchLatestVersion(packageName) {
@@ -58,6 +59,12 @@ const ADAPTERS = {
58
59
 
59
60
  const DEFAULT_DIRECTORY = "pracht-app";
60
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
+
61
68
  export async function run(argv = process.argv.slice(2)) {
62
69
  const options = parseArgs(argv);
63
70
  const packageManager = getPackageManager();
@@ -71,17 +78,20 @@ export async function run(argv = process.argv.slice(2)) {
71
78
  const adapterId = options.adapter ?? (options.yes ? "node" : null);
72
79
  const router = options.router ?? (options.yes ? "manifest" : null);
73
80
  const tailwind = options.tailwind ?? (options.yes ? false : null);
81
+ const agentTools = options.agentTools ?? (options.yes ? true : null);
74
82
 
75
83
  let resolvedDir = dir;
76
84
  let resolvedAdapter = adapterId;
77
85
  let resolvedRouter = router;
78
86
  let resolvedTailwind = tailwind;
87
+ let resolvedAgentTools = agentTools;
79
88
 
80
89
  if (
81
90
  resolvedDir == null ||
82
91
  resolvedAdapter == null ||
83
92
  resolvedRouter == null ||
84
- resolvedTailwind == null
93
+ resolvedTailwind == null ||
94
+ resolvedAgentTools == null
85
95
  ) {
86
96
  const readline = createInterface({
87
97
  input: process.stdin,
@@ -93,6 +103,7 @@ export async function run(argv = process.argv.slice(2)) {
93
103
  resolvedAdapter = resolvedAdapter ?? (await promptForAdapter(readline));
94
104
  resolvedRouter = resolvedRouter ?? (await promptForRouter(readline));
95
105
  resolvedTailwind = resolvedTailwind ?? (await promptForTailwind(readline));
106
+ resolvedAgentTools = resolvedAgentTools ?? (await promptForAgentTools(readline));
96
107
  } finally {
97
108
  readline.close();
98
109
  }
@@ -105,6 +116,7 @@ export async function run(argv = process.argv.slice(2)) {
105
116
  if (options.dryRun) {
106
117
  const files = await buildProjectFiles({
107
118
  adapter: ADAPTERS[resolvedAdapter],
119
+ agentTools: resolvedAgentTools,
108
120
  packageManager,
109
121
  projectName: toPackageName(basename(targetDir)),
110
122
  resolveRemoteVersions: false,
@@ -118,6 +130,7 @@ export async function run(argv = process.argv.slice(2)) {
118
130
  console.log(
119
131
  JSON.stringify({
120
132
  adapter: resolvedAdapter,
133
+ agentTools: resolvedAgentTools,
121
134
  directory: resolvedDir,
122
135
  dryRun: true,
123
136
  files: fileList,
@@ -138,6 +151,7 @@ export async function run(argv = process.argv.slice(2)) {
138
151
 
139
152
  await scaffoldProject({
140
153
  adapter: ADAPTERS[resolvedAdapter],
154
+ agentTools: resolvedAgentTools,
141
155
  packageManager,
142
156
  router: resolvedRouter,
143
157
  tailwind: resolvedTailwind,
@@ -171,6 +185,7 @@ export async function run(argv = process.argv.slice(2)) {
171
185
  if (options.json) {
172
186
  const files = await buildProjectFiles({
173
187
  adapter: ADAPTERS[resolvedAdapter],
188
+ agentTools: resolvedAgentTools,
174
189
  packageManager,
175
190
  projectName: toPackageName(basename(targetDir)),
176
191
  resolveRemoteVersions: false,
@@ -181,6 +196,7 @@ export async function run(argv = process.argv.slice(2)) {
181
196
  console.log(
182
197
  JSON.stringify({
183
198
  adapter: resolvedAdapter,
199
+ agentTools: resolvedAgentTools,
184
200
  directory: resolvedDir,
185
201
  files: Object.keys(files).sort(),
186
202
  gitInitialized,
@@ -202,7 +218,9 @@ export async function run(argv = process.argv.slice(2)) {
202
218
 
203
219
  export async function scaffoldProject({
204
220
  adapter,
221
+ agentTools = true,
205
222
  packageManager,
223
+ resolveRemoteVersions = true,
206
224
  router = "manifest",
207
225
  tailwind = false,
208
226
  targetDir,
@@ -210,8 +228,10 @@ export async function scaffoldProject({
210
228
  const packageName = toPackageName(basename(targetDir));
211
229
  const files = await buildProjectFiles({
212
230
  adapter,
231
+ agentTools,
213
232
  packageManager,
214
233
  projectName: packageName,
234
+ resolveRemoteVersions,
215
235
  router,
216
236
  tailwind,
217
237
  });
@@ -245,6 +265,7 @@ export function getPackageManager(userAgent = process.env.npm_config_user_agent
245
265
  export function parseArgs(argv) {
246
266
  const options = {
247
267
  adapter: undefined,
268
+ agentTools: undefined,
248
269
  dir: undefined,
249
270
  dryRun: false,
250
271
  git: true,
@@ -276,6 +297,16 @@ export function parseArgs(argv) {
276
297
  continue;
277
298
  }
278
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
+
279
310
  if (arg.startsWith("--template=")) {
280
311
  const value = normalizeTemplate(arg.slice("--template=".length));
281
312
  if (!value) {
@@ -403,6 +434,19 @@ async function promptForTailwind(readline) {
403
434
  }
404
435
  }
405
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
+
406
450
  function normalizeYesNo(value) {
407
451
  const normalized = value.toLowerCase();
408
452
 
@@ -511,6 +555,7 @@ async function resolveVersions(packageNames, { remote = true } = {}) {
511
555
 
512
556
  async function buildProjectFiles({
513
557
  adapter,
558
+ agentTools = true,
514
559
  packageManager,
515
560
  projectName,
516
561
  resolveRemoteVersions = true,
@@ -533,13 +578,21 @@ async function buildProjectFiles({
533
578
  const versions = await resolveVersions(packagesToResolve, { remote: resolveRemoteVersions });
534
579
 
535
580
  const files = {
536
- ".gitignore": "dist\nnode_modules\n.wrangler\n.vercel\n.env*\n!.env.example\n.dev.vars\n",
537
- "README.md": createReadme({ adapter, packageManager, projectName, router, tailwind }),
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
+ }),
538
591
  "package.json": createPackageJson({ adapter, projectName, tailwind, versions }),
539
592
  "src/api/health.ts": createHealthRoute(adapter),
540
593
  "vite.config.ts": createViteConfig(adapter, router, tailwind),
541
594
  "tsconfig.json": createBaseTSConfig(adapter),
542
- "AGENTS.md": createAgentInstructions({ adapter, packageManager, router, tailwind }),
595
+ "AGENTS.md": createAgentInstructions({ adapter, agentTools, packageManager, router, tailwind }),
543
596
  };
544
597
 
545
598
  if (router === "pages") {
@@ -565,6 +618,45 @@ async function buildProjectFiles({
565
618
  files[".dockerignore"] = createDockerignore();
566
619
  }
567
620
 
621
+ if (agentTools) {
622
+ files[".mcp.json"] = createMcpConfig();
623
+ Object.assign(files, await readSkillFiles());
624
+ }
625
+
626
+ return files;
627
+ }
628
+
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
+
568
660
  return files;
569
661
  }
570
662
 
@@ -665,6 +757,13 @@ function createRoutesFile() {
665
757
  " routes: [",
666
758
  ' route("/", "./routes/home.tsx", { id: "home", render: "ssg", shell: "public" }),',
667
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
+ " // ],",
668
767
  "});",
669
768
  "",
670
769
  ].join("\n");
@@ -920,7 +1019,7 @@ function createDockerignore() {
920
1019
  ].join("\n");
921
1020
  }
922
1021
 
923
- function createAgentInstructions({ adapter, packageManager, router, tailwind }) {
1022
+ function createAgentInstructions({ adapter, agentTools, packageManager, router, tailwind }) {
924
1023
  const runCmd = packageManager === "npm" ? "npm run" : packageManager;
925
1024
 
926
1025
  const lines = [
@@ -954,6 +1053,12 @@ function createAgentInstructions({ adapter, packageManager, router, tailwind })
954
1053
  lines.push("- `pracht generate middleware --name auth` — add middleware");
955
1054
  lines.push("- `pracht generate api --path /health --methods GET` — add an API route");
956
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");
957
1062
 
958
1063
  lines.push("");
959
1064
  lines.push("## Project structure");
@@ -988,12 +1093,24 @@ function createAgentInstructions({ adapter, packageManager, router, tailwind })
988
1093
  lines.push("- `src/env.d.ts` — TypeScript types for Cloudflare bindings");
989
1094
  }
990
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
+
991
1108
  lines.push("");
992
1109
 
993
1110
  return lines.join("\n");
994
1111
  }
995
1112
 
996
- function createReadme({ adapter, packageManager, projectName, router, tailwind }) {
1113
+ function createReadme({ adapter, agentTools, packageManager, projectName, router, tailwind }) {
997
1114
  const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
998
1115
  const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
999
1116
  const previewCommand = packageManager === "npm" ? "npm run preview" : `${packageManager} preview`;
@@ -1050,6 +1167,25 @@ function createReadme({ adapter, packageManager, projectName, router, tailwind }
1050
1167
  lines.push("- `src/styles/global.css` is the Tailwind CSS entry, imported by the shell.");
1051
1168
  }
1052
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
+
1053
1189
  if (adapter.id === "node") {
1054
1190
  lines.push("");
1055
1191
  lines.push("## Docker");
@@ -1172,6 +1308,8 @@ Options:
1172
1308
  --router=manifest|pages Choose routing system (default: manifest)
1173
1309
  --template=minimal|tailwind Choose starter template (minimal, or minimal + Tailwind CSS)
1174
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)
1175
1313
  --no-git Skip git init and the initial commit
1176
1314
  --skip-install Skip dependency installation
1177
1315
  --yes, -y Accept defaults, skip all prompts