create-pracht 0.2.5 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +28 -0
  2. package/package.json +1 -1
  3. package/src/index.js +332 -29
package/README.md CHANGED
@@ -16,7 +16,9 @@ npm run dev
16
16
  - Prompts for the target folder.
17
17
  - Detects the active package manager from the current environment.
18
18
  - Lets the user choose between the Node.js, Cloudflare, and Vercel adapters.
19
+ - Optionally wires up Tailwind CSS (`tailwindcss` + `@tailwindcss/vite`, a global stylesheet, and the shell import).
19
20
  - Scaffolds a minimal app with a route manifest or pages router, shell, home route, sample API route, runnable project README, and agent instructions.
21
+ - Initializes a git repository with an initial commit (skipped with `--no-git`, when git is unavailable, or when the target is already inside a repository).
20
22
  - `--dry-run` uses pinned fallback versions and does not require npm registry access.
21
23
 
22
24
  ## Usage
@@ -25,8 +27,22 @@ npm run dev
25
27
  node ./packages/start/bin/create-pracht.js
26
28
  node ./packages/start/bin/create-pracht.js my-app --adapter=node --skip-install
27
29
  node ./packages/start/bin/create-pracht.js my-app --adapter=vercel --skip-install
30
+ node ./packages/start/bin/create-pracht.js my-app --template=tailwind --yes
31
+ node ./packages/start/bin/create-pracht.js my-app --adapter=node --no-tailwind --no-git --yes
28
32
  ```
29
33
 
34
+ ## Options
35
+
36
+ - `--adapter=node|cf|vercel` — choose the hosting adapter (default: node).
37
+ - `--router=manifest|pages` — choose the routing system (default: manifest).
38
+ - `--template=minimal|tailwind` — non-interactive template selection; `minimal` is the default output, `tailwind` is minimal plus Tailwind CSS wiring.
39
+ - `--tailwind` / `--no-tailwind` — enable or disable Tailwind CSS without going through the prompt.
40
+ - `--no-git` — skip `git init` and the initial commit.
41
+ - `--skip-install` — skip dependency installation.
42
+ - `--yes`, `-y` — accept defaults (node adapter, manifest router, no Tailwind) and skip all prompts.
43
+ - `--json` — output a JSON summary instead of prose.
44
+ - `--dry-run` — list the files that would be created without writing anything.
45
+
30
46
  ## Generated Files
31
47
 
32
48
  - `package.json`
@@ -35,6 +51,16 @@ node ./packages/start/bin/create-pracht.js my-app --adapter=vercel --skip-instal
35
51
  - `src/routes/home.tsx`
36
52
  - `src/shells/public.tsx`
37
53
  - `src/api/health.ts`
54
+ - `.gitignore`
55
+
56
+ Node scaffolds also include:
57
+
58
+ - `Dockerfile` — multi-stage build (install → build → slim runtime) that runs `node dist/server/server.js`
59
+ - `.dockerignore`
60
+
61
+ Tailwind scaffolds also include:
62
+
63
+ - `src/styles/global.css` — the Tailwind entry stylesheet, imported by the shell
38
64
 
39
65
  Cloudflare scaffolds also include:
40
66
 
@@ -47,10 +73,12 @@ Cloudflare scaffolds also include:
47
73
 
48
74
  Node starters also include:
49
75
 
76
+ - `preview` -> `pracht preview`
50
77
  - `start` -> `node dist/server/server.js`
51
78
 
52
79
  Cloudflare starters also include:
53
80
 
81
+ - `preview` -> `pracht preview`
54
82
  - `deploy` -> `pracht build && wrangler deploy`
55
83
 
56
84
  Vercel starters also include:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pracht",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Interactive and scriptable starter CLI for creating full-stack Preact apps with Pracht.",
5
5
  "keywords": [
6
6
  "pracht",
package/src/index.js CHANGED
@@ -18,6 +18,8 @@ const FALLBACK_VERSION_RANGES = {
18
18
  "@pracht/cli": "^1.3.1",
19
19
  "@pracht/core": "^0.5.0",
20
20
  "@pracht/vite-plugin": "^0.3.2",
21
+ "@tailwindcss/vite": "^4.1.0",
22
+ tailwindcss: "^4.1.0",
21
23
  vercel: "latest",
22
24
  };
23
25
 
@@ -68,12 +70,19 @@ export async function run(argv = process.argv.slice(2)) {
68
70
  const dir = options.dir ?? (options.yes ? DEFAULT_DIRECTORY : null);
69
71
  const adapterId = options.adapter ?? (options.yes ? "node" : null);
70
72
  const router = options.router ?? (options.yes ? "manifest" : null);
73
+ const tailwind = options.tailwind ?? (options.yes ? false : null);
71
74
 
72
75
  let resolvedDir = dir;
73
76
  let resolvedAdapter = adapterId;
74
77
  let resolvedRouter = router;
78
+ let resolvedTailwind = tailwind;
75
79
 
76
- if (resolvedDir == null || resolvedAdapter == null || resolvedRouter == null) {
80
+ if (
81
+ resolvedDir == null ||
82
+ resolvedAdapter == null ||
83
+ resolvedRouter == null ||
84
+ resolvedTailwind == null
85
+ ) {
77
86
  const readline = createInterface({
78
87
  input: process.stdin,
79
88
  output: process.stdout,
@@ -83,6 +92,7 @@ export async function run(argv = process.argv.slice(2)) {
83
92
  resolvedDir = resolvedDir ?? (await promptForDirectory(readline));
84
93
  resolvedAdapter = resolvedAdapter ?? (await promptForAdapter(readline));
85
94
  resolvedRouter = resolvedRouter ?? (await promptForRouter(readline));
95
+ resolvedTailwind = resolvedTailwind ?? (await promptForTailwind(readline));
86
96
  } finally {
87
97
  readline.close();
88
98
  }
@@ -99,6 +109,7 @@ export async function run(argv = process.argv.slice(2)) {
99
109
  projectName: toPackageName(basename(targetDir)),
100
110
  resolveRemoteVersions: false,
101
111
  router: resolvedRouter,
112
+ tailwind: resolvedTailwind,
102
113
  });
103
114
 
104
115
  const fileList = Object.keys(files).sort();
@@ -111,6 +122,7 @@ export async function run(argv = process.argv.slice(2)) {
111
122
  dryRun: true,
112
123
  files: fileList,
113
124
  router: resolvedRouter,
125
+ tailwind: resolvedTailwind,
114
126
  }),
115
127
  );
116
128
  } else {
@@ -128,6 +140,7 @@ export async function run(argv = process.argv.slice(2)) {
128
140
  adapter: ADAPTERS[resolvedAdapter],
129
141
  packageManager,
130
142
  router: resolvedRouter,
143
+ tailwind: resolvedTailwind,
131
144
  targetDir,
132
145
  });
133
146
 
@@ -138,6 +151,23 @@ export async function run(argv = process.argv.slice(2)) {
138
151
  installSucceeded = await installDependencies(targetDir, packageManager);
139
152
  }
140
153
 
154
+ let gitInitialized = false;
155
+ if (options.git) {
156
+ const gitResult = await initGitRepository(targetDir);
157
+ gitInitialized = gitResult.initialized;
158
+
159
+ if (gitResult.initialized) {
160
+ log("");
161
+ log("Initialized a git repository with an initial commit.");
162
+ } else if (gitResult.reason === "existing-repo") {
163
+ log("");
164
+ log("Skipped git init — the target directory is already inside a git repository.");
165
+ } else if (gitResult.reason === "git-not-found") {
166
+ log("");
167
+ log("Skipped git init — git is not available on this machine.");
168
+ }
169
+ }
170
+
141
171
  if (options.json) {
142
172
  const files = await buildProjectFiles({
143
173
  adapter: ADAPTERS[resolvedAdapter],
@@ -145,6 +175,7 @@ export async function run(argv = process.argv.slice(2)) {
145
175
  projectName: toPackageName(basename(targetDir)),
146
176
  resolveRemoteVersions: false,
147
177
  router: resolvedRouter,
178
+ tailwind: resolvedTailwind,
148
179
  });
149
180
 
150
181
  console.log(
@@ -152,8 +183,10 @@ export async function run(argv = process.argv.slice(2)) {
152
183
  adapter: resolvedAdapter,
153
184
  directory: resolvedDir,
154
185
  files: Object.keys(files).sort(),
186
+ gitInitialized,
155
187
  installed: options.skipInstall ? false : installSucceeded,
156
188
  router: resolvedRouter,
189
+ tailwind: resolvedTailwind,
157
190
  }),
158
191
  );
159
192
  } else {
@@ -167,13 +200,20 @@ export async function run(argv = process.argv.slice(2)) {
167
200
  }
168
201
  }
169
202
 
170
- export async function scaffoldProject({ adapter, packageManager, router = "manifest", targetDir }) {
203
+ export async function scaffoldProject({
204
+ adapter,
205
+ packageManager,
206
+ router = "manifest",
207
+ tailwind = false,
208
+ targetDir,
209
+ }) {
171
210
  const packageName = toPackageName(basename(targetDir));
172
211
  const files = await buildProjectFiles({
173
212
  adapter,
174
213
  packageManager,
175
214
  projectName: packageName,
176
215
  router,
216
+ tailwind,
177
217
  });
178
218
 
179
219
  await mkdir(targetDir, { recursive: true });
@@ -207,9 +247,11 @@ export function parseArgs(argv) {
207
247
  adapter: undefined,
208
248
  dir: undefined,
209
249
  dryRun: false,
250
+ git: true,
210
251
  json: false,
211
252
  router: undefined,
212
253
  skipInstall: false,
254
+ tailwind: undefined,
213
255
  yes: false,
214
256
  };
215
257
 
@@ -219,6 +261,32 @@ export function parseArgs(argv) {
219
261
  continue;
220
262
  }
221
263
 
264
+ if (arg === "--tailwind") {
265
+ options.tailwind = true;
266
+ continue;
267
+ }
268
+
269
+ if (arg === "--no-tailwind") {
270
+ options.tailwind = false;
271
+ continue;
272
+ }
273
+
274
+ if (arg === "--no-git") {
275
+ options.git = false;
276
+ continue;
277
+ }
278
+
279
+ if (arg.startsWith("--template=")) {
280
+ const value = normalizeTemplate(arg.slice("--template=".length));
281
+ if (!value) {
282
+ throw new ValidationError(
283
+ `Invalid template: ${arg.slice("--template=".length)}. Use minimal or tailwind.`,
284
+ );
285
+ }
286
+ options.tailwind = value === "tailwind";
287
+ continue;
288
+ }
289
+
222
290
  if (arg === "--yes" || arg === "-y") {
223
291
  options.yes = true;
224
292
  continue;
@@ -322,6 +390,33 @@ async function promptForRouter(readline) {
322
390
  }
323
391
  }
324
392
 
393
+ async function promptForTailwind(readline) {
394
+ while (true) {
395
+ const answer = await readline.question("Use Tailwind CSS? (y/N): ");
396
+ const normalized = normalizeYesNo(answer.trim() || "no");
397
+
398
+ if (normalized != null) {
399
+ return normalized;
400
+ }
401
+
402
+ console.log("Answer y/yes or n/no.");
403
+ }
404
+ }
405
+
406
+ function normalizeYesNo(value) {
407
+ const normalized = value.toLowerCase();
408
+
409
+ if (normalized === "y" || normalized === "yes") {
410
+ return true;
411
+ }
412
+
413
+ if (normalized === "n" || normalized === "no") {
414
+ return false;
415
+ }
416
+
417
+ return null;
418
+ }
419
+
325
420
  async function ensureTargetDirectory(targetDir) {
326
421
  const error = await validateTargetDirectory(targetDir);
327
422
 
@@ -348,6 +443,20 @@ async function validateTargetDirectory(targetDir) {
348
443
  return null;
349
444
  }
350
445
 
446
+ function normalizeTemplate(value) {
447
+ const normalized = value.toLowerCase();
448
+
449
+ if (normalized === "minimal") {
450
+ return "minimal";
451
+ }
452
+
453
+ if (normalized === "tailwind") {
454
+ return "tailwind";
455
+ }
456
+
457
+ return null;
458
+ }
459
+
351
460
  function normalizeRouter(value) {
352
461
  const normalized = value.toLowerCase();
353
462
 
@@ -406,6 +515,7 @@ async function buildProjectFiles({
406
515
  projectName,
407
516
  resolveRemoteVersions = true,
408
517
  router,
518
+ tailwind = false,
409
519
  }) {
410
520
  const packagesToResolve = [
411
521
  "@pracht/cli",
@@ -416,26 +526,33 @@ async function buildProjectFiles({
416
526
  if (adapter.id === "vercel") {
417
527
  packagesToResolve.push("vercel");
418
528
  }
529
+ if (tailwind) {
530
+ packagesToResolve.push("tailwindcss", "@tailwindcss/vite");
531
+ }
419
532
 
420
533
  const versions = await resolveVersions(packagesToResolve, { remote: resolveRemoteVersions });
421
534
 
422
535
  const files = {
423
- ".gitignore": "dist\nnode_modules\n.wrangler\n.vercel\n",
424
- "README.md": createReadme({ adapter, packageManager, projectName, router }),
425
- "package.json": createPackageJson({ adapter, projectName, versions }),
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 }),
538
+ "package.json": createPackageJson({ adapter, projectName, tailwind, versions }),
426
539
  "src/api/health.ts": createHealthRoute(adapter),
427
- "vite.config.ts": createViteConfig(adapter, router),
540
+ "vite.config.ts": createViteConfig(adapter, router, tailwind),
428
541
  "tsconfig.json": createBaseTSConfig(adapter),
429
- "AGENTS.md": createAgentInstructions({ adapter, packageManager, router }),
542
+ "AGENTS.md": createAgentInstructions({ adapter, packageManager, router, tailwind }),
430
543
  };
431
544
 
432
545
  if (router === "pages") {
433
- files["src/pages/_app.tsx"] = createShellFile(projectName);
546
+ files["src/pages/_app.tsx"] = createShellFile(projectName, tailwind);
434
547
  files["src/pages/index.tsx"] = createPagesHomeRoute(adapter);
435
548
  } else {
436
549
  files["src/routes.ts"] = createRoutesFile();
437
550
  files["src/routes/home.tsx"] = createHomeRoute(adapter);
438
- files["src/shells/public.tsx"] = createShellFile(projectName);
551
+ files["src/shells/public.tsx"] = createShellFile(projectName, tailwind);
552
+ }
553
+
554
+ if (tailwind) {
555
+ files["src/styles/global.css"] = '@import "tailwindcss";\n';
439
556
  }
440
557
 
441
558
  if (adapter.id === "cloudflare") {
@@ -443,16 +560,22 @@ async function buildProjectFiles({
443
560
  files["src/env.d.ts"] = createCloudflareEnvDeclaration();
444
561
  }
445
562
 
563
+ if (adapter.id === "node") {
564
+ files["Dockerfile"] = createDockerfile(packageManager);
565
+ files[".dockerignore"] = createDockerignore();
566
+ }
567
+
446
568
  return files;
447
569
  }
448
570
 
449
- function createPackageJson({ adapter, projectName, versions }) {
571
+ function createPackageJson({ adapter, projectName, tailwind, versions }) {
450
572
  const scripts = {
451
573
  build: "pracht build",
452
574
  dev: "pracht dev",
453
575
  };
454
576
 
455
577
  if (adapter.id === "node") {
578
+ scripts.preview = "pracht preview";
456
579
  scripts.start = "node dist/server/server.js";
457
580
  }
458
581
 
@@ -466,6 +589,7 @@ function createPackageJson({ adapter, projectName, versions }) {
466
589
 
467
590
  if (adapter.id === "cloudflare") {
468
591
  scripts.deploy = "pracht build && wrangler deploy";
592
+ scripts.preview = "pracht preview";
469
593
  devDependencies.wrangler = "^4.81.0";
470
594
  }
471
595
 
@@ -474,6 +598,11 @@ function createPackageJson({ adapter, projectName, versions }) {
474
598
  devDependencies.vercel = versions["vercel"];
475
599
  }
476
600
 
601
+ if (tailwind) {
602
+ devDependencies["@tailwindcss/vite"] = versions["@tailwindcss/vite"];
603
+ devDependencies.tailwindcss = versions["tailwindcss"];
604
+ }
605
+
477
606
  return `${JSON.stringify(
478
607
  {
479
608
  dependencies: {
@@ -492,7 +621,7 @@ function createPackageJson({ adapter, projectName, versions }) {
492
621
  )}\n`;
493
622
  }
494
623
 
495
- function createViteConfig(adapter, router) {
624
+ function createViteConfig(adapter, router, tailwind) {
496
625
  const ADAPTER_IMPORTS = {
497
626
  node: { fn: "nodeAdapter", pkg: "@pracht/adapter-node" },
498
627
  cloudflare: { fn: "cloudflareAdapter", pkg: "@pracht/adapter-cloudflare" },
@@ -506,16 +635,23 @@ function createViteConfig(adapter, router) {
506
635
  ? `{ pagesDir: "/src/pages", adapter: ${info.fn}() }`
507
636
  : `{ adapter: ${info.fn}() }`;
508
637
 
509
- return [
638
+ const plugins = tailwind
639
+ ? `[pracht(${prachtOptions}), tailwindcss()]`
640
+ : `[pracht(${prachtOptions})]`;
641
+
642
+ const lines = [
510
643
  'import { defineConfig } from "vite";',
511
644
  'import { pracht } from "@pracht/vite-plugin";',
512
645
  `import { ${info.fn} } from "${info.pkg}";`,
513
- "",
514
- "export default defineConfig({",
515
- ` plugins: [pracht(${prachtOptions})],`,
516
- "});",
517
- "",
518
- ].join("\n");
646
+ ];
647
+
648
+ if (tailwind) {
649
+ lines.push('import tailwindcss from "@tailwindcss/vite";');
650
+ }
651
+
652
+ lines.push("", "export default defineConfig({", ` plugins: ${plugins},`, "});", "");
653
+
654
+ return lines.join("\n");
519
655
  }
520
656
 
521
657
  function createRoutesFile() {
@@ -534,9 +670,15 @@ function createRoutesFile() {
534
670
  ].join("\n");
535
671
  }
536
672
 
537
- function createShellFile(projectName) {
673
+ function createShellFile(projectName, tailwind = false) {
674
+ const lines = ['import type { ShellProps } from "@pracht/core";'];
675
+
676
+ if (tailwind) {
677
+ lines.push('import "../styles/global.css";');
678
+ }
679
+
538
680
  return [
539
- 'import type { ShellProps } from "@pracht/core";',
681
+ ...lines,
540
682
  "",
541
683
  "export function Shell({ children }: ShellProps) {",
542
684
  " return (",
@@ -705,7 +847,80 @@ function createCloudflareEnvDeclaration() {
705
847
  ].join("\n");
706
848
  }
707
849
 
708
- function createAgentInstructions({ adapter, packageManager, router }) {
850
+ function createDockerfile(packageManager) {
851
+ const COMMANDS = {
852
+ npm: {
853
+ build: "npm run build",
854
+ install: "npm install",
855
+ lockfile: "package-lock.json*",
856
+ prune: "npm prune --omit=dev",
857
+ setup: null,
858
+ },
859
+ pnpm: {
860
+ build: "pnpm build",
861
+ install: "pnpm install",
862
+ lockfile: "pnpm-lock.yaml*",
863
+ prune: "pnpm prune --prod",
864
+ setup: "corepack enable pnpm",
865
+ },
866
+ yarn: {
867
+ build: "yarn build",
868
+ install: "yarn install",
869
+ lockfile: "yarn.lock*",
870
+ prune: "yarn install --production --ignore-scripts --prefer-offline",
871
+ setup: "corepack enable yarn",
872
+ },
873
+ };
874
+
875
+ // The runtime image ships Node.js, so bun scaffolds fall back to npm inside Docker.
876
+ const commands = COMMANDS[packageManager] ?? COMMANDS.npm;
877
+
878
+ const lines = ["# syntax=docker/dockerfile:1", "", "FROM node:22-alpine AS base", "WORKDIR /app"];
879
+
880
+ if (commands.setup) {
881
+ lines.push(`RUN ${commands.setup}`);
882
+ }
883
+
884
+ lines.push(
885
+ "",
886
+ "FROM base AS deps",
887
+ `COPY package.json ${commands.lockfile} ./`,
888
+ `RUN ${commands.install}`,
889
+ "",
890
+ "FROM deps AS build",
891
+ "COPY . .",
892
+ `RUN ${commands.build}`,
893
+ `RUN ${commands.prune}`,
894
+ "",
895
+ "FROM node:22-alpine AS runtime",
896
+ "WORKDIR /app",
897
+ "ENV NODE_ENV=production",
898
+ "ENV PORT=3000",
899
+ "COPY --from=build /app/package.json ./package.json",
900
+ "COPY --from=build /app/node_modules ./node_modules",
901
+ "COPY --from=build /app/dist ./dist",
902
+ "EXPOSE 3000",
903
+ 'CMD ["node", "dist/server/server.js"]',
904
+ "",
905
+ );
906
+
907
+ return lines.join("\n");
908
+ }
909
+
910
+ function createDockerignore() {
911
+ return [
912
+ "node_modules",
913
+ "dist",
914
+ ".git",
915
+ ".env*",
916
+ "!.env.example",
917
+ "Dockerfile",
918
+ ".dockerignore",
919
+ "",
920
+ ].join("\n");
921
+ }
922
+
923
+ function createAgentInstructions({ adapter, packageManager, router, tailwind }) {
709
924
  const runCmd = packageManager === "npm" ? "npm run" : packageManager;
710
925
 
711
926
  const lines = [
@@ -717,6 +932,10 @@ function createAgentInstructions({ adapter, packageManager, router }) {
717
932
  `- \`${runCmd} build\` — production build`,
718
933
  ];
719
934
 
935
+ if (adapter.id === "node" || adapter.id === "cloudflare") {
936
+ lines.push(`- \`${runCmd} preview\` — build and serve the production build locally`);
937
+ }
938
+
720
939
  if (adapter.id === "node") {
721
940
  lines.push(`- \`${runCmd} start\` — run the built server`);
722
941
  }
@@ -756,6 +975,14 @@ function createAgentInstructions({ adapter, packageManager, router }) {
756
975
  lines.push("- `src/api/` — API route handlers");
757
976
  lines.push(`- \`vite.config.ts\` — Vite config with the ${adapter.label} adapter`);
758
977
 
978
+ if (tailwind) {
979
+ lines.push("- `src/styles/global.css` — Tailwind CSS entry stylesheet, imported by the shell");
980
+ }
981
+
982
+ if (adapter.id === "node") {
983
+ lines.push("- `Dockerfile` — multi-stage container build that runs the built server");
984
+ }
985
+
759
986
  if (adapter.id === "cloudflare") {
760
987
  lines.push("- `wrangler.jsonc` — Cloudflare Workers configuration");
761
988
  lines.push("- `src/env.d.ts` — TypeScript types for Cloudflare bindings");
@@ -766,9 +993,10 @@ function createAgentInstructions({ adapter, packageManager, router }) {
766
993
  return lines.join("\n");
767
994
  }
768
995
 
769
- function createReadme({ adapter, packageManager, projectName, router }) {
996
+ function createReadme({ adapter, packageManager, projectName, router, tailwind }) {
770
997
  const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
771
998
  const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
999
+ const previewCommand = packageManager === "npm" ? "npm run preview" : `${packageManager} preview`;
772
1000
  const startCommand = packageManager === "npm" ? "npm run start" : `${packageManager} start`;
773
1001
  const deployCommand = packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
774
1002
 
@@ -784,10 +1012,12 @@ function createReadme({ adapter, packageManager, projectName, router }) {
784
1012
  ];
785
1013
 
786
1014
  if (adapter.id === "node") {
1015
+ lines.push(`- \`${previewCommand}\``);
787
1016
  lines.push(`- \`${startCommand}\``);
788
1017
  }
789
1018
 
790
1019
  if (adapter.id === "cloudflare") {
1020
+ lines.push(`- \`${previewCommand}\``);
791
1021
  lines.push(`- \`${deployCommand}\``);
792
1022
  lines.push("");
793
1023
  lines.push(
@@ -816,9 +1046,79 @@ function createReadme({ adapter, packageManager, projectName, router }) {
816
1046
 
817
1047
  lines.push("- `src/api/health.ts` is a sample API route.");
818
1048
 
1049
+ if (tailwind) {
1050
+ lines.push("- `src/styles/global.css` is the Tailwind CSS entry, imported by the shell.");
1051
+ }
1052
+
1053
+ if (adapter.id === "node") {
1054
+ lines.push("");
1055
+ lines.push("## Docker");
1056
+ lines.push("");
1057
+ lines.push("A multi-stage `Dockerfile` builds the app and runs the Node server:");
1058
+ lines.push("");
1059
+ lines.push("```bash");
1060
+ lines.push(`docker build -t ${projectName} .`);
1061
+ lines.push(`docker run -p 3000:3000 ${projectName}`);
1062
+ lines.push("```");
1063
+ }
1064
+
819
1065
  return `${lines.join("\n")}\n`;
820
1066
  }
821
1067
 
1068
+ export async function initGitRepository(targetDir) {
1069
+ if (!(await execCommand("git", ["--version"]))) {
1070
+ return { initialized: false, reason: "git-not-found" };
1071
+ }
1072
+
1073
+ if (await execCommand("git", ["rev-parse", "--is-inside-work-tree"], targetDir)) {
1074
+ return { initialized: false, reason: "existing-repo" };
1075
+ }
1076
+
1077
+ if (!(await execCommand("git", ["init"], targetDir))) {
1078
+ return { initialized: false, reason: "init-failed" };
1079
+ }
1080
+
1081
+ if (!(await execCommand("git", ["add", "-A"], targetDir))) {
1082
+ return { initialized: false, reason: "commit-failed" };
1083
+ }
1084
+
1085
+ // Fall back to a scoped identity when the user has no git identity configured,
1086
+ // so the initial commit still succeeds (e.g. on fresh machines or CI).
1087
+ const hasIdentity = await execCommand("git", ["config", "user.email"], targetDir);
1088
+ const identityArgs = hasIdentity
1089
+ ? []
1090
+ : ["-c", "user.name=create-pracht", "-c", "user.email=create-pracht@localhost"];
1091
+
1092
+ const committed = await execCommand(
1093
+ "git",
1094
+ [...identityArgs, "commit", "-m", "Initial commit from create-pracht"],
1095
+ targetDir,
1096
+ );
1097
+
1098
+ if (!committed) {
1099
+ return { initialized: false, reason: "commit-failed" };
1100
+ }
1101
+
1102
+ return { initialized: true };
1103
+ }
1104
+
1105
+ function execCommand(command, args, cwd) {
1106
+ return new Promise((resolveExec) => {
1107
+ const child = spawn(command, args, {
1108
+ cwd,
1109
+ stdio: "ignore",
1110
+ });
1111
+
1112
+ child.on("close", (code) => {
1113
+ resolveExec(code === 0);
1114
+ });
1115
+
1116
+ child.on("error", () => {
1117
+ resolveExec(false);
1118
+ });
1119
+ });
1120
+ }
1121
+
822
1122
  async function installDependencies(targetDir, packageManager) {
823
1123
  const args = packageManager === "yarn" ? ["install"] : ["install"];
824
1124
 
@@ -868,13 +1168,16 @@ Usage:
868
1168
  create-pracht [directory] [options]
869
1169
 
870
1170
  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
1171
+ --adapter=node|cf|vercel Choose hosting adapter (default: node)
1172
+ --router=manifest|pages Choose routing system (default: manifest)
1173
+ --template=minimal|tailwind Choose starter template (minimal, or minimal + Tailwind CSS)
1174
+ --tailwind / --no-tailwind Enable or disable Tailwind CSS wiring (default: prompt)
1175
+ --no-git Skip git init and the initial commit
1176
+ --skip-install Skip dependency installation
1177
+ --yes, -y Accept defaults, skip all prompts
1178
+ --json Output JSON summary instead of prose
1179
+ --dry-run Show which files would be created without writing
1180
+ -h, --help Show this help message
878
1181
  `);
879
1182
  }
880
1183