create-pracht 0.1.0 → 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jovi De Croock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -43,7 +43,10 @@ Cloudflare scaffolds also include:
43
43
 
44
44
  - `dev` -> `pracht dev`
45
45
  - `build` -> `pracht build`
46
- - `preview` -> `pracht preview`
46
+
47
+ Node starters also include:
48
+
49
+ - `start` -> `node dist/server/server.js`
47
50
 
48
51
  Cloudflare starters also include:
49
52
 
@@ -52,9 +55,3 @@ Cloudflare starters also include:
52
55
  Vercel starters also include:
53
56
 
54
57
  - `deploy` -> `pracht build && vercel deploy --prebuilt`
55
-
56
- Node starters can be run in production with:
57
-
58
- ```bash
59
- node dist/server/server.js
60
- ```
@@ -3,7 +3,10 @@
3
3
  import { run } from "../src/index.js";
4
4
 
5
5
  run().catch((error) => {
6
- console.error("Failed to create a pracht app.");
7
- console.error(error instanceof Error ? error.message : error);
8
- process.exit(1);
6
+ const code = error && error.code === 2 ? 2 : 1;
7
+ console.error(code === 2 ? error.message : "Failed to create a pracht app.");
8
+ if (code !== 2) {
9
+ console.error(error instanceof Error ? error.message : error);
10
+ }
11
+ process.exit(code);
9
12
  });
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "create-pracht",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
+ "license": "MIT",
5
+ "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/start",
6
+ "bugs": {
7
+ "url": "https://github.com/JoviDeCroock/pracht/issues"
8
+ },
4
9
  "repository": {
5
10
  "type": "git",
6
11
  "url": "https://github.com/JoviDeCroock/pracht",
7
12
  "directory": "packages/start"
8
13
  },
9
- "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/start",
10
- "bugs": {
11
- "url": "https://github.com/JoviDeCroock/pracht/issues"
12
- },
13
14
  "bin": {
14
15
  "create-pracht": "./bin/create-pracht.js"
15
16
  },
package/src/index.js CHANGED
@@ -1,17 +1,20 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { mkdir, 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
6
 
7
+ export class ValidationError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.code = 2;
11
+ }
12
+ }
13
+
7
14
  async function fetchLatestVersion(packageName) {
8
- const res = await fetch(
9
- `https://registry.npmjs.org/${packageName}/latest`,
10
- );
15
+ const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
11
16
  if (!res.ok) {
12
- throw new Error(
13
- `Failed to fetch version for ${packageName}: ${res.statusText}`,
14
- );
17
+ throw new Error(`Failed to fetch version for ${packageName}: ${res.statusText}`);
15
18
  }
16
19
  const data = await res.json();
17
20
  return data.version;
@@ -19,7 +22,7 @@ async function fetchLatestVersion(packageName) {
19
22
 
20
23
  const ADAPTERS = {
21
24
  node: {
22
- description: "Node.js server with pracht preview",
25
+ description: "Node.js server with a generated server entry",
23
26
  id: "node",
24
27
  label: "Node.js",
25
28
  packageName: "@pracht/adapter-node",
@@ -46,47 +49,109 @@ const DEFAULT_DIRECTORY = "pracht-app";
46
49
  export async function run(argv = process.argv.slice(2)) {
47
50
  const options = parseArgs(argv);
48
51
  const packageManager = getPackageManager();
52
+ const log = options.json ? () => {} : console.log.bind(console);
49
53
 
50
- console.log("create-pracht");
51
- console.log(`Using ${packageManager} for this scaffold.`);
52
- console.log("");
54
+ log("create-pracht");
55
+ log(`Using ${packageManager} for this scaffold.`);
56
+ log("");
53
57
 
54
- const readline = createInterface({
55
- input: process.stdin,
56
- output: process.stdout,
57
- });
58
+ const dir = options.dir ?? (options.yes ? DEFAULT_DIRECTORY : null);
59
+ const adapterId = options.adapter ?? (options.yes ? "node" : null);
60
+ const router = options.router ?? (options.yes ? "manifest" : null);
58
61
 
59
- try {
60
- const dir = options.dir ?? (await promptForDirectory(readline));
61
- const adapterId = options.adapter ?? (await promptForAdapter(readline));
62
- const router = options.router ?? (await promptForRouter(readline));
63
- const targetDir = resolve(process.cwd(), dir);
62
+ let resolvedDir = dir;
63
+ let resolvedAdapter = adapterId;
64
+ let resolvedRouter = router;
64
65
 
65
- await ensureTargetDirectory(targetDir);
66
+ if (resolvedDir == null || resolvedAdapter == null || resolvedRouter == null) {
67
+ const readline = createInterface({
68
+ input: process.stdin,
69
+ output: process.stdout,
70
+ });
66
71
 
67
- await scaffoldProject({
68
- adapter: ADAPTERS[adapterId],
72
+ try {
73
+ resolvedDir = resolvedDir ?? (await promptForDirectory(readline));
74
+ resolvedAdapter = resolvedAdapter ?? (await promptForAdapter(readline));
75
+ resolvedRouter = resolvedRouter ?? (await promptForRouter(readline));
76
+ } finally {
77
+ readline.close();
78
+ }
79
+ }
80
+
81
+ const targetDir = resolve(process.cwd(), resolvedDir);
82
+
83
+ await ensureTargetDirectory(targetDir);
84
+
85
+ if (options.dryRun) {
86
+ const files = await buildProjectFiles({
87
+ adapter: ADAPTERS[resolvedAdapter],
69
88
  packageManager,
70
- router,
71
- targetDir,
89
+ projectName: toPackageName(basename(targetDir)),
90
+ router: resolvedRouter,
72
91
  });
73
92
 
74
- let installSucceeded = false;
75
- if (!options.skipInstall) {
76
- console.log("");
77
- console.log(`Installing dependencies with ${packageManager}...`);
78
- installSucceeded = await installDependencies(targetDir, packageManager);
93
+ const fileList = Object.keys(files).sort();
94
+
95
+ if (options.json) {
96
+ console.log(
97
+ JSON.stringify({
98
+ adapter: resolvedAdapter,
99
+ directory: resolvedDir,
100
+ dryRun: true,
101
+ files: fileList,
102
+ router: resolvedRouter,
103
+ }),
104
+ );
105
+ } else {
106
+ log("Dry run — the following files would be created:");
107
+ log("");
108
+ for (const file of fileList) {
109
+ log(` ${file}`);
110
+ }
79
111
  }
80
112
 
113
+ return;
114
+ }
115
+
116
+ await scaffoldProject({
117
+ adapter: ADAPTERS[resolvedAdapter],
118
+ packageManager,
119
+ router: resolvedRouter,
120
+ targetDir,
121
+ });
122
+
123
+ let installSucceeded = false;
124
+ if (!options.skipInstall) {
125
+ log("");
126
+ log(`Installing dependencies with ${packageManager}...`);
127
+ installSucceeded = await installDependencies(targetDir, packageManager);
128
+ }
129
+
130
+ if (options.json) {
131
+ const files = await buildProjectFiles({
132
+ adapter: ADAPTERS[resolvedAdapter],
133
+ packageManager,
134
+ projectName: toPackageName(basename(targetDir)),
135
+ router: resolvedRouter,
136
+ });
137
+
138
+ console.log(
139
+ JSON.stringify({
140
+ adapter: resolvedAdapter,
141
+ directory: resolvedDir,
142
+ files: Object.keys(files).sort(),
143
+ installed: options.skipInstall ? false : installSucceeded,
144
+ router: resolvedRouter,
145
+ }),
146
+ );
147
+ } else {
81
148
  printNextSteps({
82
- adapter: ADAPTERS[adapterId],
83
- dir,
149
+ adapter: ADAPTERS[resolvedAdapter],
150
+ dir: resolvedDir,
84
151
  installSucceeded,
85
152
  packageManager,
86
153
  skipInstall: options.skipInstall,
87
154
  });
88
- } finally {
89
- readline.close();
90
155
  }
91
156
  }
92
157
 
@@ -106,6 +171,8 @@ export async function scaffoldProject({ adapter, packageManager, router = "manif
106
171
  await mkdir(dirname(filePath), { recursive: true });
107
172
  await writeFile(filePath, content, "utf-8");
108
173
  }
174
+
175
+ await symlink("AGENTS.md", resolve(targetDir, "CLAUDE.md"));
109
176
  }
110
177
 
111
178
  export function getPackageManager(userAgent = process.env.npm_config_user_agent ?? "") {
@@ -119,8 +186,11 @@ export function parseArgs(argv) {
119
186
  const options = {
120
187
  adapter: undefined,
121
188
  dir: undefined,
189
+ dryRun: false,
190
+ json: false,
122
191
  router: undefined,
123
192
  skipInstall: false,
193
+ yes: false,
124
194
  };
125
195
 
126
196
  for (const arg of argv) {
@@ -129,13 +199,40 @@ export function parseArgs(argv) {
129
199
  continue;
130
200
  }
131
201
 
202
+ if (arg === "--yes" || arg === "-y") {
203
+ options.yes = true;
204
+ continue;
205
+ }
206
+
207
+ if (arg === "--json") {
208
+ options.json = true;
209
+ continue;
210
+ }
211
+
212
+ if (arg === "--dry-run") {
213
+ options.dryRun = true;
214
+ continue;
215
+ }
216
+
132
217
  if (arg.startsWith("--adapter=")) {
133
- options.adapter = normalizeAdapter(arg.slice("--adapter=".length));
218
+ const value = normalizeAdapter(arg.slice("--adapter=".length));
219
+ if (!value) {
220
+ throw new ValidationError(
221
+ `Invalid adapter: ${arg.slice("--adapter=".length)}. Use node, cf, or vercel.`,
222
+ );
223
+ }
224
+ options.adapter = value;
134
225
  continue;
135
226
  }
136
227
 
137
228
  if (arg.startsWith("--router=")) {
138
- options.router = normalizeRouter(arg.slice("--router=".length));
229
+ const value = normalizeRouter(arg.slice("--router=".length));
230
+ if (!value) {
231
+ throw new ValidationError(
232
+ `Invalid router: ${arg.slice("--router=".length)}. Use manifest or pages.`,
233
+ );
234
+ }
235
+ options.router = value;
139
236
  continue;
140
237
  }
141
238
 
@@ -209,7 +306,7 @@ async function ensureTargetDirectory(targetDir) {
209
306
  const error = await validateTargetDirectory(targetDir);
210
307
 
211
308
  if (error) {
212
- throw new Error(error);
309
+ throw new ValidationError(error);
213
310
  }
214
311
  }
215
312
 
@@ -270,10 +367,7 @@ function normalizeAdapter(value) {
270
367
 
271
368
  async function resolveVersions(packageNames) {
272
369
  const entries = await Promise.all(
273
- packageNames.map(async (name) => [
274
- name,
275
- `^${await fetchLatestVersion(name)}`,
276
- ]),
370
+ packageNames.map(async (name) => [name, `^${await fetchLatestVersion(name)}`]),
277
371
  );
278
372
  return Object.fromEntries(entries);
279
373
  }
@@ -297,6 +391,8 @@ async function buildProjectFiles({ adapter, packageManager, projectName, router
297
391
  "package.json": createPackageJson({ adapter, projectName, versions }),
298
392
  "src/api/health.ts": createHealthRoute(adapter),
299
393
  "vite.config.ts": createViteConfig(adapter, router),
394
+ "tsconfig.json": createBaseTSConfig(adapter),
395
+ "AGENTS.md": createAgentInstructions({ adapter, packageManager, router }),
300
396
  };
301
397
 
302
398
  if (router === "pages") {
@@ -320,9 +416,12 @@ function createPackageJson({ adapter, projectName, versions }) {
320
416
  const scripts = {
321
417
  build: "pracht build",
322
418
  dev: "pracht dev",
323
- preview: "pracht preview",
324
419
  };
325
420
 
421
+ if (adapter.id === "node") {
422
+ scripts.start = "node dist/server/server.js";
423
+ }
424
+
326
425
  const devDependencies = {
327
426
  "@pracht/cli": versions["@pracht/cli"],
328
427
  "@pracht/vite-plugin": versions["@pracht/vite-plugin"],
@@ -505,6 +604,17 @@ function createPagesHomeRoute(adapter) {
505
604
  ].join("\n");
506
605
  }
507
606
 
607
+ function createBaseTSConfig(_adapter) {
608
+ const config = {
609
+ compilerOptions: {
610
+ jsx: "react-jsx",
611
+ jsxImportSource: "preact",
612
+ lib: ["ES2022"],
613
+ },
614
+ };
615
+ return JSON.stringify(config, null, 4);
616
+ }
617
+
508
618
  function createHealthRoute(adapter) {
509
619
  return [
510
620
  "export function GET() {",
@@ -552,10 +662,71 @@ function createCloudflareEnvDeclaration() {
552
662
  ].join("\n");
553
663
  }
554
664
 
665
+ function createAgentInstructions({ adapter, packageManager, router }) {
666
+ const runCmd = packageManager === "npm" ? "npm run" : packageManager;
667
+
668
+ const lines = [
669
+ "# Pracht App",
670
+ "",
671
+ "## Commands",
672
+ "",
673
+ `- \`${runCmd} dev\` — start the dev server`,
674
+ `- \`${runCmd} build\` — production build`,
675
+ ];
676
+
677
+ if (adapter.id === "node") {
678
+ lines.push(`- \`${runCmd} start\` — run the built server`);
679
+ }
680
+
681
+ if (adapter.id === "cloudflare" || adapter.id === "vercel") {
682
+ lines.push(`- \`${runCmd} deploy\` — build and deploy`);
683
+ }
684
+
685
+ lines.push("");
686
+ lines.push("## Scaffolding");
687
+ lines.push("");
688
+ lines.push("Use the CLI to generate new files:");
689
+ lines.push("");
690
+ lines.push("- `pracht generate route <name>` — add a route");
691
+ lines.push("- `pracht generate shell <name>` — add a shell");
692
+ lines.push("- `pracht generate middleware <name>` — add middleware");
693
+ lines.push("- `pracht generate api <name>` — add an API route");
694
+ lines.push("- `pracht doctor` — check project health");
695
+
696
+ lines.push("");
697
+ lines.push("## Project structure");
698
+ lines.push("");
699
+
700
+ if (router === "pages") {
701
+ lines.push("This app uses **pages routing** (file-system based).");
702
+ lines.push("");
703
+ lines.push("- `src/pages/` — file-system routes (each file becomes a route)");
704
+ lines.push("- `src/pages/_app.tsx` — app shell (layout and head)");
705
+ } else {
706
+ lines.push("This app uses **manifest routing**.");
707
+ lines.push("");
708
+ lines.push("- `src/routes.ts` — route manifest (defines all routes and shells)");
709
+ lines.push("- `src/routes/` — route components and loaders");
710
+ lines.push("- `src/shells/` — shell components (layouts)");
711
+ }
712
+
713
+ lines.push("- `src/api/` — API route handlers");
714
+ lines.push(`- \`vite.config.ts\` — Vite config with the ${adapter.label} adapter`);
715
+
716
+ if (adapter.id === "cloudflare") {
717
+ lines.push("- `wrangler.jsonc` — Cloudflare Workers configuration");
718
+ lines.push("- `src/env.d.ts` — TypeScript types for Cloudflare bindings");
719
+ }
720
+
721
+ lines.push("");
722
+
723
+ return lines.join("\n");
724
+ }
725
+
555
726
  function createReadme({ adapter, packageManager, projectName, router }) {
556
727
  const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
557
728
  const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
558
- const previewCommand = packageManager === "npm" ? "npm run preview" : `${packageManager} preview`;
729
+ const startCommand = packageManager === "npm" ? "npm run start" : `${packageManager} start`;
559
730
  const deployCommand = packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
560
731
 
561
732
  const lines = [
@@ -567,9 +738,12 @@ function createReadme({ adapter, packageManager, projectName, router }) {
567
738
  "",
568
739
  `- \`${installCommand}\``,
569
740
  `- \`${devCommand}\``,
570
- `- \`${previewCommand}\``,
571
741
  ];
572
742
 
743
+ if (adapter.id === "node") {
744
+ lines.push(`- \`${startCommand}\``);
745
+ }
746
+
573
747
  if (adapter.id === "cloudflare") {
574
748
  lines.push(`- \`${deployCommand}\``);
575
749
  lines.push("");
@@ -648,7 +822,16 @@ function printHelp() {
648
822
  console.log(`create-pracht
649
823
 
650
824
  Usage:
651
- create-pracht [directory] [--adapter=node|cf|vercel] [--router=manifest|pages] [--skip-install]
825
+ create-pracht [directory] [options]
826
+
827
+ Options:
828
+ --adapter=node|cf|vercel Choose hosting adapter (default: node)
829
+ --router=manifest|pages Choose routing system (default: manifest)
830
+ --skip-install Skip dependency installation
831
+ --yes, -y Accept defaults, skip all prompts
832
+ --json Output JSON summary instead of prose
833
+ --dry-run Show which files would be created without writing
834
+ -h, --help Show this help message
652
835
  `);
653
836
  }
654
837