prism-design 2.13.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 (90) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/LICENSE +21 -0
  3. package/README.md +203 -0
  4. package/bin/clone-architect.mjs +476 -0
  5. package/bin/prism.mjs +467 -0
  6. package/catalog/index.json +1155 -0
  7. package/extractions/airbnb.com/DESIGN.md +1068 -0
  8. package/extractions/airbnb.com/tokens.json +507 -0
  9. package/extractions/attio.com/DESIGN.md +1295 -0
  10. package/extractions/attio.com/tokens.json +438 -0
  11. package/extractions/auroxdashboard.com/DESIGN.md +724 -0
  12. package/extractions/auroxdashboard.com/tokens.json +195 -0
  13. package/extractions/careerexplorer.com/DESIGN.md +1178 -0
  14. package/extractions/careerexplorer.com/tokens.json +141 -0
  15. package/extractions/chance.co/DESIGN.md +1209 -0
  16. package/extractions/chance.co/tokens.json +160 -0
  17. package/extractions/choisis-ton-avenir.com/DESIGN.md +1265 -0
  18. package/extractions/choisis-ton-avenir.com/tokens.json +227 -0
  19. package/extractions/example.com/DESIGN.md +436 -0
  20. package/extractions/example.com/tokens.json +91 -0
  21. package/extractions/getdesign.md/DESIGN.md +1009 -0
  22. package/extractions/getdesign.md/tokens.json +219 -0
  23. package/extractions/github.com/DESIGN.md +1130 -0
  24. package/extractions/github.com/tokens.json +2092 -0
  25. package/extractions/hello-charly.com/DESIGN.md +1146 -0
  26. package/extractions/hello-charly.com/tokens.json +322 -0
  27. package/extractions/hyperliquid.xyz/DESIGN.md +779 -0
  28. package/extractions/hyperliquid.xyz/tokens.json +598 -0
  29. package/extractions/instagram.com/DESIGN.md +996 -0
  30. package/extractions/instagram.com/tokens.json +1240 -0
  31. package/extractions/jobirl.com/DESIGN.md +1160 -0
  32. package/extractions/jobirl.com/tokens.json +139 -0
  33. package/extractions/life360.com/DESIGN.md +1133 -0
  34. package/extractions/life360.com/tokens.json +491 -0
  35. package/extractions/lifesum.com/DESIGN.md +965 -0
  36. package/extractions/lifesum.com/tokens.json +170 -0
  37. package/extractions/linear.app/DESIGN.md +1301 -0
  38. package/extractions/linear.app/tokens.json +732 -0
  39. package/extractions/mavoie.org/DESIGN.md +1148 -0
  40. package/extractions/mavoie.org/tokens.json +128 -0
  41. package/extractions/miro.com/DESIGN.md +1237 -0
  42. package/extractions/miro.com/tokens.json +401 -0
  43. package/extractions/notion.so/DESIGN.md +1319 -0
  44. package/extractions/notion.so/tokens.json +906 -0
  45. package/extractions/onetonline.org/DESIGN.md +909 -0
  46. package/extractions/onetonline.org/tokens.json +280 -0
  47. package/extractions/posthog.com/DESIGN.md +1024 -0
  48. package/extractions/posthog.com/tokens.json +197 -0
  49. package/extractions/revolut.com/DESIGN.md +1080 -0
  50. package/extractions/revolut.com/tokens.json +401 -0
  51. package/extractions/stripe.com/DESIGN.md +1272 -0
  52. package/extractions/stripe.com/tokens.json +794 -0
  53. package/extractions/switchcollective.com/DESIGN.md +1040 -0
  54. package/extractions/switchcollective.com/tokens.json +98 -0
  55. package/extractions/truity.com/DESIGN.md +970 -0
  56. package/extractions/truity.com/tokens.json +166 -0
  57. package/extractions/uniquekicks.be/DESIGN.md +1171 -0
  58. package/extractions/uniquekicks.be/tokens.json +237 -0
  59. package/package.json +122 -0
  60. package/scripts/analyze.ts +281 -0
  61. package/scripts/bank-register.ts +379 -0
  62. package/scripts/bank.ts +374 -0
  63. package/scripts/browser-stealth.ts +189 -0
  64. package/scripts/clone.ts +198 -0
  65. package/scripts/compare-vs-gd-final.ts +273 -0
  66. package/scripts/compare-vs-gd.ts +269 -0
  67. package/scripts/compare.ts +405 -0
  68. package/scripts/deploy-site.ts +181 -0
  69. package/scripts/diff-snapshots.ts +340 -0
  70. package/scripts/enrich-catalog.ts +212 -0
  71. package/scripts/extract.ts +2038 -0
  72. package/scripts/extractors/advanced.ts +524 -0
  73. package/scripts/extractors/widgets.ts +711 -0
  74. package/scripts/generate-design-md.ts +5775 -0
  75. package/scripts/generate-final-pdf.ts +274 -0
  76. package/scripts/generate-og-image.ts +87 -0
  77. package/scripts/generate-showcase.ts +1588 -0
  78. package/scripts/generate-site.ts +847 -0
  79. package/scripts/mass-extract.sh +91 -0
  80. package/scripts/post-process-all.sh +55 -0
  81. package/scripts/regen-catalog.ts +203 -0
  82. package/scripts/shared/cache.ts +149 -0
  83. package/scripts/shared/css-helpers.ts +263 -0
  84. package/scripts/shared/logger.ts +57 -0
  85. package/scripts/shared/named-colors.ts +355 -0
  86. package/scripts/shared/types.ts +220 -0
  87. package/scripts/sync-catalog.ts +105 -0
  88. package/scripts/tokenize.ts +988 -0
  89. package/templates/layout-template.md +52 -0
  90. package/templates/tokens-template.json +34 -0
package/bin/prism.mjs ADDED
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Prism — CLI entrypoint.
4
+ *
5
+ * Dispatches subcommands to the underlying TS scripts via tsx.
6
+ * Usage:
7
+ * prism extract <url> Full pipeline: extract + analyze + tokenize + DESIGN.md
8
+ * prism tokenize <domain> Re-tokenize an existing extraction
9
+ * prism design <domain> Re-generate DESIGN.md for an existing extraction
10
+ * prism compare <domain> Pixelmatch comparison vs original screenshots
11
+ * prism bank <subcmd> Component bank (register/query/stats)
12
+ * prism --help Show this help
13
+ * prism --version Print version
14
+ */
15
+
16
+ import { spawnSync } from 'child_process';
17
+ import { fileURLToPath } from 'url';
18
+ import { dirname, join, resolve } from 'path';
19
+ import { readFileSync, existsSync, writeFileSync, copyFileSync, mkdirSync } from 'fs';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const pkgRoot = resolve(__dirname, '..');
24
+
25
+ const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf-8'));
26
+
27
+ const SCRIPTS = {
28
+ extract: 'scripts/clone.ts', // full pipeline (aliased)
29
+ clone: 'scripts/clone.ts', // full pipeline
30
+ tokenize: 'scripts/tokenize.ts',
31
+ design: 'scripts/generate-design-md.ts',
32
+ 'generate-design': 'scripts/generate-design-md.ts',
33
+ compare: 'scripts/compare.ts',
34
+ bank: 'scripts/bank.ts',
35
+ analyze: 'scripts/analyze.ts',
36
+ enrich: 'scripts/narrative-enricher.ts',
37
+ };
38
+
39
+ const HELP = `
40
+ Prism v${pkg.version}
41
+ Extract the real design DNA of any website — computed CSS, screenshots, DESIGN.md.
42
+
43
+ Usage:
44
+ prism <command> [args]
45
+
46
+ Commands:
47
+ add <brand> Install a DESIGN.md + tokens.json into your project
48
+ Example: prism add linear.app
49
+ Example: prism add stripe.com
50
+
51
+ list List all available brands in the catalog
52
+ Example: prism list
53
+
54
+ extract <url> Full pipeline: extract CSS + screenshots + tokens + DESIGN.md
55
+ Example: prism extract https://linear.app
56
+
57
+ update <domain> Re-extract a domain and regenerate DESIGN.md + tokens
58
+ Backs up the current extraction before overwriting
59
+ Example: prism update linear.app
60
+
61
+ diff <domain> Compare current DESIGN.md with the previous backup
62
+ Shows what changed in the last update
63
+ Example: prism diff linear.app
64
+
65
+ tokenize <domain> Re-tokenize an existing extraction
66
+ Example: prism tokenize linear.app
67
+
68
+ design <domain> Re-generate DESIGN.md for an existing extraction
69
+ Example: prism design linear.app
70
+
71
+ enrich <domain> Enrich DESIGN.md narrative via Claude API (WHY-level prose)
72
+ Requires: ANTHROPIC_API_KEY env var
73
+ Output: DESIGN.enriched.md (~$0.003/brand)
74
+ Example: prism enrich linear.app
75
+
76
+ compare <domain> Pixelmatch comparison vs original screenshots
77
+ Example: prism compare linear.app
78
+
79
+ bank <subcmd> Component bank (register/query/stats/show)
80
+ Example: prism bank stats
81
+
82
+ Options:
83
+ --help, -h Show this help
84
+ --version, -v Print version
85
+
86
+ Output:
87
+ All extractions are saved to ./extractions/<domain>/ containing:
88
+ raw-css.json Ground-truth getComputedStyle() dump
89
+ tokens.json Normalized design tokens
90
+ layout-analysis.md Structural analysis
91
+ DESIGN.md Narrative LLM-optimized design doc (10+ sections)
92
+ screenshots/ Desktop 1440px + Mobile 390px screenshots
93
+
94
+ Docs: ${pkg.homepage || 'https://prism.ps-tools.dev'}
95
+ `;
96
+
97
+ const [cmd, ...args] = process.argv.slice(2);
98
+
99
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
100
+ console.log(HELP);
101
+ process.exit(0);
102
+ }
103
+
104
+ if (cmd === '--version' || cmd === '-v') {
105
+ console.log(pkg.version);
106
+ process.exit(0);
107
+ }
108
+
109
+ // ── `add <brand>` — install DESIGN.md + tokens.json into cwd ──
110
+ if (cmd === 'add') {
111
+ const brand = args[0];
112
+ if (!brand) {
113
+ console.error('Usage: prism add <brand>');
114
+ console.error('Example: prism add linear.app');
115
+ process.exit(1);
116
+ }
117
+
118
+ const catalogIndex = join(pkgRoot, 'catalog', 'index.json');
119
+ if (!existsSync(catalogIndex)) {
120
+ console.error('Catalog not found. This may be a packaging issue.');
121
+ process.exit(1);
122
+ }
123
+
124
+ const catalog = JSON.parse(readFileSync(catalogIndex, 'utf-8'));
125
+ const brandLower = brand.toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
126
+
127
+ // Find brand in catalog (fuzzy: match domain or domain without .com/.app)
128
+ let found = catalog.brands.find(b => b.domain === brandLower);
129
+ if (!found) {
130
+ found = catalog.brands.find(b =>
131
+ b.domain.replace(/\.(com|app|io|dev|so|ai|co|net|org)$/, '') === brandLower.replace(/\.(com|app|io|dev|so|ai|co|net|org)$/, '')
132
+ );
133
+ }
134
+
135
+ if (!found) {
136
+ console.error(`Brand "${brand}" not found in catalog.`);
137
+ console.error(`Run \`prism list\` to see all available brands.`);
138
+ console.error(`To extract a new URL: \`prism extract https://${brand}\``);
139
+ process.exit(1);
140
+ }
141
+
142
+ // Source files — from catalog symlink or extractions/
143
+ const extractionDir = join(pkgRoot, 'extractions', found.domain);
144
+ const designSrc = join(extractionDir, 'DESIGN.md');
145
+ const tokensSrc = join(extractionDir, 'tokens.json');
146
+
147
+ const cwd = process.cwd();
148
+ const designDst = join(cwd, 'DESIGN.md');
149
+ const tokensDst = join(cwd, 'design-tokens.json');
150
+
151
+ let installed = [];
152
+
153
+ if (existsSync(designSrc)) {
154
+ copyFileSync(designSrc, designDst);
155
+ installed.push('DESIGN.md');
156
+ }
157
+
158
+ if (existsSync(tokensSrc)) {
159
+ copyFileSync(tokensSrc, tokensDst);
160
+ installed.push('design-tokens.json');
161
+ }
162
+
163
+ if (installed.length === 0) {
164
+ console.error(`Files for "${found.domain}" not found in package.`);
165
+ process.exit(1);
166
+ }
167
+
168
+ console.log(`\n🧬 Installed ${found.domain} design DNA:\n`);
169
+ for (const f of installed) console.log(` šŸ“„ ${f}`);
170
+ if (found.accent) console.log(`\n šŸŽØ Accent: ${found.accent}`);
171
+ console.log(`\nTell your AI: "Use DESIGN.md as reference before writing any UI."`);
172
+ console.log(`\nExtract a new site: prism extract <url>`);
173
+ console.log('');
174
+ process.exit(0);
175
+ }
176
+
177
+ // ── `list` — show all catalog brands ──
178
+ if (cmd === 'list') {
179
+ const catalogIndex = join(pkgRoot, 'catalog', 'index.json');
180
+ if (!existsSync(catalogIndex)) {
181
+ console.error('Catalog not found.');
182
+ process.exit(1);
183
+ }
184
+ const catalog = JSON.parse(readFileSync(catalogIndex, 'utf-8'));
185
+ // Convert rgb(r,g,b) accent to #hex for readable terminal output
186
+ function rgbToHexCli(rgb) {
187
+ const m = rgb.match(/rgba?\(\s*(\d+),\s*(\d+),\s*(\d+)/);
188
+ if (!m) return rgb;
189
+ return '#' + [m[1], m[2], m[3]].map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
190
+ }
191
+ console.log(`\n🧬 Prism Catalog — ${catalog.count} brands\n`);
192
+ for (const b of catalog.brands) {
193
+ const rawAccent = b.accent || '';
194
+ const accentHex = rawAccent.startsWith('rgb') ? rgbToHexCli(rawAccent) : rawAccent;
195
+ const accentDisplay = accentHex ? ` ${accentHex}` : '';
196
+ console.log(` ${b.domain}${accentDisplay}`);
197
+ }
198
+ console.log(`\nInstall: prism add <brand>`);
199
+ console.log(`Extract new: prism extract <url>\n`);
200
+ process.exit(0);
201
+ }
202
+
203
+ // ── `random` — pick a random brand from catalog (discovery / inspiration) ──
204
+ if (cmd === 'random') {
205
+ const catalogIndex = join(pkgRoot, 'catalog', 'index.json');
206
+ if (!existsSync(catalogIndex)) {
207
+ console.error('Catalog not found.');
208
+ process.exit(1);
209
+ }
210
+ const catalog = JSON.parse(readFileSync(catalogIndex, 'utf-8'));
211
+ const brands = catalog.brands || [];
212
+ if (brands.length === 0) {
213
+ console.error('Catalog is empty.');
214
+ process.exit(1);
215
+ }
216
+ const pick = brands[Math.floor(Math.random() * brands.length)];
217
+ console.log(`\nšŸŽ² ${pick.domain}`);
218
+ if (pick.description) console.log(` ${pick.description.slice(0, 200)}${pick.description.length > 200 ? '…' : ''}`);
219
+ if (pick.category) console.log(` Category: ${pick.category}`);
220
+ if (pick.completeness) console.log(` Score: ${pick.completeness}/100`);
221
+ if (pick.accent) console.log(` Accent: ${pick.accent}`);
222
+ console.log(`\nInstall: prism add ${pick.domain}`);
223
+ console.log(`View live: https://prism.ps-tools.dev/${pick.domain}\n`);
224
+ process.exit(0);
225
+ }
226
+
227
+ // ── `search <keyword>` — fuzzy search across catalog ──
228
+ if (cmd === 'search') {
229
+ const keyword = args[0];
230
+ if (!keyword) {
231
+ console.error('Usage: prism search <keyword>');
232
+ console.error('Examples:');
233
+ console.error(' prism search dark # find dark-mode brands');
234
+ console.error(' prism search fintech # find Fintech category');
235
+ console.error(' prism search inter # find brands using Inter font');
236
+ process.exit(1);
237
+ }
238
+ const catalogIndex = join(pkgRoot, 'catalog', 'index.json');
239
+ if (!existsSync(catalogIndex)) {
240
+ console.error('Catalog not found.');
241
+ process.exit(1);
242
+ }
243
+ const catalog = JSON.parse(readFileSync(catalogIndex, 'utf-8'));
244
+ const brands = catalog.brands || [];
245
+ const q = keyword.toLowerCase();
246
+ const matches = brands.filter(b => {
247
+ const haystack = [
248
+ b.domain || '',
249
+ b.category || '',
250
+ b.font || '',
251
+ b.description || '',
252
+ b.dark ? 'dark mode' : 'light mode',
253
+ ].join(' ').toLowerCase();
254
+ return haystack.includes(q);
255
+ });
256
+ if (matches.length === 0) {
257
+ console.log(`\nNo brands matching "${keyword}".\n`);
258
+ console.log(`Try: prism list # browse all ${brands.length} brands\n`);
259
+ process.exit(0);
260
+ }
261
+ console.log(`\nFound ${matches.length} brand(s) matching "${keyword}":\n`);
262
+ for (const b of matches) {
263
+ const tag = b.completeness ? ` (${b.completeness}/100)` : '';
264
+ console.log(` ${b.domain}${tag}`);
265
+ if (b.description) console.log(` ${b.description.slice(0, 120)}${b.description.length > 120 ? '…' : ''}`);
266
+ }
267
+ console.log(`\nInstall any: prism add <brand>\n`);
268
+ process.exit(0);
269
+ }
270
+
271
+ // ── `update <domain>` — re-extract + backup previous extraction ──
272
+ if (cmd === 'update') {
273
+ const domain = args[0];
274
+ if (!domain) {
275
+ console.error('Usage: prism update <domain>');
276
+ console.error('Example: prism update linear.app');
277
+ process.exit(1);
278
+ }
279
+
280
+ const extractionDir = join(pkgRoot, 'extractions', domain);
281
+ const backupDir = join(pkgRoot, 'extractions', domain + '.backup-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19));
282
+
283
+ if (existsSync(extractionDir)) {
284
+ const cpResult = spawnSync('cp', ['-r', extractionDir, backupDir], { stdio: 'inherit' });
285
+ if (cpResult.status === 0) {
286
+ console.log(`šŸ“¦ Backed up existing extraction to ${backupDir}`);
287
+ } else {
288
+ console.warn(`āš ļø Could not create backup — proceeding without backup`);
289
+ }
290
+ }
291
+
292
+ console.log(`\nšŸ”„ Re-extracting ${domain}...`);
293
+ const url = domain.startsWith('http') ? domain : `https://${domain}`;
294
+
295
+ const localTsx2 = join(pkgRoot, 'node_modules', '.bin', 'tsx');
296
+ const tsxCmd2 = existsSync(localTsx2) ? localTsx2 : 'npx';
297
+ const tsxArgs2 = existsSync(localTsx2)
298
+ ? [join(pkgRoot, 'scripts/clone.ts'), url]
299
+ : ['tsx', join(pkgRoot, 'scripts/clone.ts'), url];
300
+
301
+ const result2 = spawnSync(tsxCmd2, tsxArgs2, { stdio: 'inherit', cwd: process.cwd() });
302
+ if (result2.error) {
303
+ console.error('Failed to run extraction:', result2.error.message);
304
+ process.exit(3);
305
+ }
306
+
307
+ console.log(`\nāœ… Update complete. Backup at: ${backupDir}`);
308
+ console.log(`Run \`prism diff ${domain}\` to see what changed.`);
309
+ process.exit(result2.status ?? 0);
310
+ }
311
+
312
+ // ── `diff <domain>` — compare current DESIGN.md with latest backup ──
313
+ if (cmd === 'diff') {
314
+ const domain = args[0];
315
+ if (!domain) {
316
+ console.error('Usage: prism diff <domain>');
317
+ console.error('Example: prism diff linear.app');
318
+ process.exit(1);
319
+ }
320
+
321
+ const extractionDir = join(pkgRoot, 'extractions', domain);
322
+ const currentDesign = join(extractionDir, 'DESIGN.md');
323
+ const currentTokens = join(extractionDir, 'tokens.json');
324
+
325
+ if (!existsSync(currentDesign)) {
326
+ console.error(`No DESIGN.md found for ${domain}. Run: prism extract https://${domain}`);
327
+ process.exit(1);
328
+ }
329
+
330
+ const extractionsDir = join(pkgRoot, 'extractions');
331
+ const backupPrefix = domain + '.backup-';
332
+ let backups = [];
333
+ try {
334
+ const { readdirSync: rds } = await import('fs');
335
+ backups = rds(extractionsDir)
336
+ .filter(f => f.startsWith(backupPrefix))
337
+ .sort()
338
+ .reverse();
339
+ } catch { /* no backups */ }
340
+
341
+ if (backups.length === 0) {
342
+ console.log(`No backup found for ${domain}.`);
343
+ console.log(`Run \`prism update ${domain}\` to create one.`);
344
+
345
+ const content = readFileSync(currentDesign, 'utf-8');
346
+ const scoreMatch = content.match(/^completeness:\s*(\d+)/m);
347
+ const dateMatch = content.match(/^extracted_at:\s*"?([^"\n]+)"?/m);
348
+ if (scoreMatch || dateMatch) {
349
+ console.log(`\nCurrent extraction:`);
350
+ if (scoreMatch) console.log(` Completeness: ${scoreMatch[1]}/100`);
351
+ if (dateMatch) console.log(` Extracted: ${dateMatch[1]}`);
352
+ }
353
+ process.exit(0);
354
+ }
355
+
356
+ const latestBackup = join(extractionsDir, backups[0]);
357
+ const backupDesign = join(latestBackup, 'DESIGN.md');
358
+ const backupTokens = join(latestBackup, 'tokens.json');
359
+
360
+ console.log(`\nšŸ” Diffing ${domain}:`);
361
+ console.log(` Current: ${extractionDir}`);
362
+ console.log(` Backup: ${latestBackup}\n`);
363
+
364
+ if (existsSync(currentTokens) && existsSync(backupTokens)) {
365
+ try {
366
+ const curr = JSON.parse(readFileSync(currentTokens, 'utf-8'));
367
+ const prev = JSON.parse(readFileSync(backupTokens, 'utf-8'));
368
+
369
+ const diffs = [];
370
+ const COMPARE_PATHS = [
371
+ ['colors.background.primary', 'Background'],
372
+ ['colors.text.primary', 'Primary text'],
373
+ ['colors.accent.primary', 'Accent'],
374
+ ['colors.border', 'Border'],
375
+ ['typography.fontFamily.primary', 'Font'],
376
+ ];
377
+
378
+ for (const [path, label] of COMPARE_PATHS) {
379
+ const getPath = (obj, p) => p.split('.').reduce((o, k) => o?.[k], obj);
380
+ const currVal = getPath(curr, path);
381
+ const prevVal = getPath(prev, path);
382
+ if (currVal && prevVal && currVal !== prevVal) {
383
+ diffs.push(` ${label}: ${prevVal} → ${currVal}`);
384
+ }
385
+ }
386
+
387
+ if (diffs.length > 0) {
388
+ console.log('Token changes:');
389
+ diffs.forEach(d => console.log(d));
390
+ } else {
391
+ console.log('No token changes detected.');
392
+ }
393
+ } catch { /* ignore parse errors */ }
394
+ console.log('');
395
+ }
396
+
397
+ if (existsSync(backupDesign)) {
398
+ const currContent = readFileSync(currentDesign, 'utf-8');
399
+ const prevContent = readFileSync(backupDesign, 'utf-8');
400
+ const currLines = currContent.split('\n').length;
401
+ const prevLines = prevContent.split('\n').length;
402
+ const currScore = currContent.match(/^completeness:\s*(\d+)/m)?.[1];
403
+ const prevScore = prevContent.match(/^completeness:\s*(\d+)/m)?.[1];
404
+ const currDate = currContent.match(/^extracted_at:\s*"?([^"\n]+)"?/m)?.[1];
405
+ const prevDate = prevContent.match(/^extracted_at:\s*"?([^"\n]+)"?/m)?.[1];
406
+
407
+ console.log('DESIGN.md diff:');
408
+ console.log(` Lines: ${prevLines} → ${currLines} (${currLines >= prevLines ? '+' : ''}${currLines - prevLines})`);
409
+ if (currScore && prevScore) {
410
+ const diff = parseInt(currScore) - parseInt(prevScore);
411
+ console.log(` Completeness: ${prevScore}/100 → ${currScore}/100 (${diff >= 0 ? '+' : ''}${diff})`);
412
+ }
413
+ if (currDate && prevDate && currDate !== prevDate) {
414
+ console.log(` Extracted: ${prevDate.slice(0, 10)} → ${currDate.slice(0, 10)}`);
415
+ }
416
+
417
+ const currSections = (currContent.match(/^## \d+\./gm) || []).length;
418
+ const prevSections = (prevContent.match(/^## \d+\./gm) || []).length;
419
+ if (currSections !== prevSections) {
420
+ console.log(` Sections: ${prevSections} → ${currSections}`);
421
+ }
422
+
423
+ const diffResult = spawnSync('diff', ['--unified=0', backupDesign, currentDesign], { encoding: 'utf-8' });
424
+ if (diffResult.status !== 0 && diffResult.stdout) {
425
+ const diffLines = diffResult.stdout.split('\n');
426
+ const addedLines = diffLines.filter(l => l.startsWith('+') && !l.startsWith('+++'));
427
+ const removedLines = diffLines.filter(l => l.startsWith('-') && !l.startsWith('---'));
428
+ if (addedLines.length + removedLines.length > 0) {
429
+ console.log(`\n +${addedLines.length} lines added, -${removedLines.length} lines removed`);
430
+ }
431
+ }
432
+ console.log('');
433
+ }
434
+
435
+ process.exit(0);
436
+ }
437
+
438
+ const script = SCRIPTS[cmd];
439
+ if (!script) {
440
+ console.error(`Unknown command: "${cmd}"`);
441
+ console.error(`Run \`prism --help\` for available commands.`);
442
+ process.exit(1);
443
+ }
444
+
445
+ const scriptPath = join(pkgRoot, script);
446
+ if (!existsSync(scriptPath)) {
447
+ console.error(`Internal error: script not found: ${scriptPath}`);
448
+ console.error(`This is a packaging bug — please report it.`);
449
+ process.exit(2);
450
+ }
451
+
452
+ const localTsx = join(pkgRoot, 'node_modules', '.bin', 'tsx');
453
+ const tsxCmd = existsSync(localTsx) ? localTsx : 'npx';
454
+ const tsxArgs = existsSync(localTsx) ? [scriptPath, ...args] : ['tsx', scriptPath, ...args];
455
+
456
+ const result = spawnSync(tsxCmd, tsxArgs, {
457
+ stdio: 'inherit',
458
+ cwd: process.cwd(),
459
+ });
460
+
461
+ if (result.error) {
462
+ console.error('Failed to invoke tsx:', result.error.message);
463
+ console.error('Make sure tsx is installed: npm install -g tsx');
464
+ process.exit(3);
465
+ }
466
+
467
+ process.exit(result.status ?? 0);