create-pracht 0.3.0 → 0.4.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.
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: upgrade-pracht
3
+ version: 1.0.1
4
+ description: |
5
+ Upgrade the @pracht/* packages in an app safely: inventory installed
6
+ versions, read the changelogs between installed and target, map breaking
7
+ changes to actual usage in the codebase, apply the upgrade, and walk the
8
+ verification ladder (doctor, typegen, verify, build, tests).
9
+ Use when asked to "upgrade pracht", "update @pracht packages", "bump the
10
+ framework", "what changed in the new pracht version", or "is this pracht
11
+ upgrade safe".
12
+ allowed-tools:
13
+ - Bash
14
+ - Read
15
+ - Edit
16
+ - Grep
17
+ - Glob
18
+ - AskUserQuestion
19
+ ---
20
+
21
+ # Pracht Upgrade
22
+
23
+ Upgrade `@pracht/*` dependencies with the changelog read *before* the install,
24
+ not after the build breaks.
25
+
26
+ ## Step 1: Inventory
27
+
28
+ List every installed pracht package and its resolved version:
29
+
30
+ ```bash
31
+ pnpm list --depth 1 --json | grep -A2 '@pracht/' # or read package.json + lockfile
32
+ ```
33
+
34
+ The family: `@pracht/core`, `@pracht/cli`, `@pracht/vite-plugin`,
35
+ `@pracht/adapter-node`, `@pracht/adapter-cloudflare`, `@pracht/adapter-vercel`,
36
+ `@pracht/preact-ssr-precompile`, `@pracht/image`. Get the latest published
37
+ versions with `npm view <pkg> version`.
38
+
39
+ ## Step 2: Understand the versioning model
40
+
41
+ Pracht packages are **independently versioned** (the repo's changesets config
42
+ has empty `fixed`/`linked` groups) — `@pracht/core` can be at 0.9.x while
43
+ `@pracht/cli` is at 1.6.x. There is no "one framework version". Two
44
+ consequences:
45
+
46
+ 1. **Internal dependencies are pinned exact.** Published packages depend on
47
+ their siblings at exact versions (e.g. `@pracht/vite-plugin@0.5.0` depends
48
+ on `@pracht/core@0.9.0`, not a range). Upgrade the whole family in one
49
+ move; upgrading only one package can drag in a second copy of
50
+ `@pracht/core` and split the runtime.
51
+ 2. **Most packages are 0.x**, so under semver a *minor* bump may be breaking —
52
+ treat `### Minor Changes` entries on 0.x packages with the same care as
53
+ majors.
54
+
55
+ After any upgrade, confirm a single core resolution:
56
+
57
+ ```bash
58
+ pnpm why @pracht/core # exactly one version may appear
59
+ ```
60
+
61
+ ## Step 3: Read the changelogs between installed and target
62
+
63
+ Only `@pracht/cli` ships `CHANGELOG.md` in its npm tarball
64
+ (`node_modules/@pracht/cli/CHANGELOG.md`); the other packages publish `dist/`
65
+ only. Fetch their changelogs from the repo instead:
66
+
67
+ ```
68
+ https://raw.githubusercontent.com/JoviDeCroock/pracht/main/packages/<dir>/CHANGELOG.md
69
+ ```
70
+
71
+ | Package | Repo directory |
72
+ | ------- | -------------- |
73
+ | `@pracht/core` | `packages/framework` |
74
+ | `@pracht/cli` | `packages/cli` |
75
+ | `@pracht/vite-plugin` | `packages/vite-plugin` |
76
+ | `@pracht/adapter-node` / `-cloudflare` / `-vercel` | `packages/adapter-*` |
77
+ | `@pracht/preact-ssr-precompile` | `packages/preact-ssr-precompile` |
78
+ | `@pracht/image` | `packages/image` |
79
+
80
+ Changelogs are changesets-generated: `## X.Y.Z` sections containing
81
+ `### Major Changes` / `### Minor Changes` / `### Patch Changes`. Read every
82
+ section between the installed and target version of every installed package.
83
+
84
+ ## Step 4: Map changes onto this app
85
+
86
+ Classify each entry as **breaking** / **feature** / **fix**. For each breaking
87
+ (or 0.x minor) entry, grep the app for the APIs, exports, config options, and
88
+ generated-file shapes it names, and record: affected files, the migration the
89
+ changelog prescribes, and whether it can be applied mechanically. Also
90
+ re-check peer ranges after a major target bump — `@pracht/vite-plugin`
91
+ requires `vite` (^8), `@pracht/adapter-cloudflare` requires `vite` and
92
+ `wrangler` (^4.81), `@pracht/core` requires `preact` (^10) and
93
+ `preact-render-to-string` (^6).
94
+
95
+ Present the plan as a table:
96
+
97
+ | Package | Installed → Target | Breaking entries | App impact | Migration |
98
+ | ------- | ------------------ | ---------------- | ---------- | --------- |
99
+
100
+ ## Step 5: Confirm, then apply
101
+
102
+ Use `AskUserQuestion` before touching anything when breaking migrations are
103
+ required: confirm the target versions and which migrations to apply. Then:
104
+
105
+ ```bash
106
+ pnpm up '@pracht/core@<v>' '@pracht/cli@<v>' '@pracht/vite-plugin@<v>' <adapters...>
107
+ ```
108
+
109
+ Upgrade every installed `@pracht/*` package in the same command. Apply the
110
+ agreed code migrations with minimal diffs, one changelog entry at a time.
111
+
112
+ ## Step 6: Verification ladder
113
+
114
+ Run in order; stop and fix at the first failure:
115
+
116
+ ```bash
117
+ pracht doctor --json # wiring still valid
118
+ pracht typegen --check # generated route types up to date?
119
+ pracht typegen # regenerate if --check failed or routes changed
120
+ pracht verify --json # framework-aware checks
121
+ pracht build # full production build (budgets included)
122
+ pnpm test # the app's own suite
123
+ ```
124
+
125
+ `pracht doctor`, `verify`, and `typegen --check` exit non-zero on failure, so
126
+ they gate CI cleanly.
127
+
128
+ ## Step 7: Rollback note
129
+
130
+ If the ladder cannot be made green, roll back rather than shipping a
131
+ half-upgrade:
132
+
133
+ ```bash
134
+ git restore package.json pnpm-lock.yaml && pnpm install
135
+ git checkout -- <migrated files> # or revert the upgrade commit
136
+ ```
137
+
138
+ Because internal deps are exact-pinned, a *partial* rollback (one package
139
+ back, the rest forward) recreates the duplicate-core problem from Step 2 —
140
+ roll the whole family back together.
141
+
142
+ ## Rules
143
+
144
+ 1. Never mix `@pracht/*` versions from different release waves — upgrade and
145
+ roll back the family as a unit, and verify with `pnpm why @pracht/core`.
146
+ 2. Read changelogs before installing, not after something breaks.
147
+ 3. Never apply a breaking-change migration without explicit user confirmation
148
+ via `AskUserQuestion`.
149
+ 4. Treat 0.x minor bumps as potentially breaking.
150
+ 5. Do not hand-edit lockfiles; let the package manager resolve.
151
+
152
+ $ARGUMENTS
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
 
@@ -632,8 +724,8 @@ function createViteConfig(adapter, router, tailwind) {
632
724
 
633
725
  const prachtOptions =
634
726
  router === "pages"
635
- ? `{ pagesDir: "/src/pages", adapter: ${info.fn}() }`
636
- : `{ adapter: ${info.fn}() }`;
727
+ ? `{ pagesDir: "/src/pages", adapter: ${info.fn}(), llmsTxt: {} }`
728
+ : `{ adapter: ${info.fn}(), llmsTxt: {} }`;
637
729
 
638
730
  const plugins = tailwind
639
731
  ? `[pracht(${prachtOptions}), tailwindcss()]`
@@ -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