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/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
- vercel: "latest",
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 (resolvedDir == null || resolvedAdapter == null || resolvedRouter == null) {
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({ adapter, packageManager, router = "manifest", targetDir }) {
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": "dist\nnode_modules\n.wrangler\n.vercel\n.env*\n!.env.example\n.dev.vars\n",
424
- "README.md": createReadme({ adapter, packageManager, projectName, router }),
425
- "package.json": createPackageJson({ adapter, projectName, versions }),
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 createPackageJson({ adapter, projectName, versions }) {
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
- return [
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
- "export default defineConfig({",
515
- ` plugins: [pracht(${prachtOptions})],`,
516
- "});",
517
- "",
518
- ].join("\n");
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
- 'import type { ShellProps } from "@pracht/core";',
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 createAgentInstructions({ adapter, packageManager, router }) {
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 Choose hosting adapter (default: node)
872
- --router=manifest|pages Choose routing system (default: manifest)
873
- --skip-install Skip dependency installation
874
- --yes, -y Accept defaults, skip all prompts
875
- --json Output JSON summary instead of prose
876
- --dry-run Show which files would be created without writing
877
- -h, --help Show this help message
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