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
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Clone Architect — CLI entrypoint.
4
+ *
5
+ * Dispatches subcommands to the underlying TS scripts via tsx.
6
+ * Usage:
7
+ * clone-architect extract <url> Full pipeline: extract + analyze + tokenize + DESIGN.md
8
+ * clone-architect tokenize <domain> Re-tokenize an existing extraction
9
+ * clone-architect design <domain> Re-generate DESIGN.md for an existing extraction
10
+ * clone-architect compare <domain> Pixelmatch comparison vs original screenshots
11
+ * clone-architect bank <subcmd> Component bank (register/query/stats)
12
+ * clone-architect --help Show this help
13
+ * clone-architect --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
+ Clone Architect v${pkg.version}
41
+ Extract real design (computed CSS + screenshots + DESIGN.md) from any URL.
42
+
43
+ Usage:
44
+ clone-architect <command> [args]
45
+
46
+ Commands:
47
+ add <brand> Install a DESIGN.md + tokens.json into your project
48
+ Example: clone-architect add linear.app
49
+ Example: clone-architect add stripe.com
50
+
51
+ list List all available brands in the catalog
52
+ Example: clone-architect list
53
+
54
+ extract <url> Full pipeline: extract CSS + screenshots + tokens + DESIGN.md
55
+ Example: clone-architect 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: clone-architect 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: clone-architect diff linear.app
64
+
65
+ tokenize <domain> Re-tokenize an existing extraction
66
+ Example: clone-architect tokenize linear.app
67
+
68
+ design <domain> Re-generate DESIGN.md for an existing extraction
69
+ Example: clone-architect 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: clone-architect enrich linear.app
75
+
76
+ compare <domain> Pixelmatch comparison vs original screenshots
77
+ Example: clone-architect compare linear.app
78
+
79
+ bank <subcmd> Component bank (register/query/stats/show)
80
+ Example: clone-architect 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://github.com/' + (pkg.repository?.url || 'clone-architect/clone-architect').replace(/^git\\+|\\.git$/g, '')}
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: clone-architect add <brand>');
114
+ console.error('Example: clone-architect 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 \`clone-architect list\` to see all available brands.`);
138
+ console.error(`To extract a new URL: \`clone-architect 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 system:\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: clone-architect 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(`\nClone Architect 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: clone-architect add <brand>`);
199
+ console.log(`Extract new: clone-architect 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: clone-architect add ${pick.domain}`);
223
+ console.log(`View live: https://clone-architect.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: clone-architect search <keyword>');
232
+ console.error('Examples:');
233
+ console.error(' clone-architect search dark # find dark-mode brands');
234
+ console.error(' clone-architect search fintech # find Fintech category');
235
+ console.error(' clone-architect 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: clone-architect 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: clone-architect 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: clone-architect update <domain>');
276
+ console.error('Example: clone-architect 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
+ // Backup existing extraction before overwriting
284
+ if (existsSync(extractionDir)) {
285
+ // Use cp -r for backup
286
+ const cpResult = spawnSync('cp', ['-r', extractionDir, backupDir], { stdio: 'inherit' });
287
+ if (cpResult.status === 0) {
288
+ console.log(`šŸ“¦ Backed up existing extraction to ${backupDir}`);
289
+ } else {
290
+ console.warn(`āš ļø Could not create backup — proceeding without backup`);
291
+ }
292
+ }
293
+
294
+ console.log(`\nšŸ”„ Re-extracting ${domain}...`);
295
+ const url = domain.startsWith('http') ? domain : `https://${domain}`;
296
+
297
+ const localTsx2 = join(pkgRoot, 'node_modules', '.bin', 'tsx');
298
+ const tsxCmd2 = existsSync(localTsx2) ? localTsx2 : 'npx';
299
+ const tsxArgs2 = existsSync(localTsx2)
300
+ ? [join(pkgRoot, 'scripts/clone.ts'), url]
301
+ : ['tsx', join(pkgRoot, 'scripts/clone.ts'), url];
302
+
303
+ const result2 = spawnSync(tsxCmd2, tsxArgs2, { stdio: 'inherit', cwd: process.cwd() });
304
+ if (result2.error) {
305
+ console.error('Failed to run extraction:', result2.error.message);
306
+ process.exit(3);
307
+ }
308
+
309
+ console.log(`\nāœ… Update complete. Backup at: ${backupDir}`);
310
+ console.log(`Run \`clone-architect diff ${domain}\` to see what changed.`);
311
+ process.exit(result2.status ?? 0);
312
+ }
313
+
314
+ // ── `diff <domain>` — compare current DESIGN.md with latest backup ──
315
+ if (cmd === 'diff') {
316
+ const domain = args[0];
317
+ if (!domain) {
318
+ console.error('Usage: clone-architect diff <domain>');
319
+ console.error('Example: clone-architect diff linear.app');
320
+ process.exit(1);
321
+ }
322
+
323
+ const extractionDir = join(pkgRoot, 'extractions', domain);
324
+ const currentDesign = join(extractionDir, 'DESIGN.md');
325
+ const currentTokens = join(extractionDir, 'tokens.json');
326
+
327
+ if (!existsSync(currentDesign)) {
328
+ console.error(`No DESIGN.md found for ${domain}. Run: clone-architect extract https://${domain}`);
329
+ process.exit(1);
330
+ }
331
+
332
+ // Find most recent backup
333
+ const extractionsDir = join(pkgRoot, 'extractions');
334
+ const backupPrefix = domain + '.backup-';
335
+ let backups = [];
336
+ try {
337
+ const { readdirSync: rds } = await import('fs');
338
+ backups = rds(extractionsDir)
339
+ .filter(f => f.startsWith(backupPrefix))
340
+ .sort()
341
+ .reverse();
342
+ } catch { /* no backups */ }
343
+
344
+ if (backups.length === 0) {
345
+ console.log(`No backup found for ${domain}.`);
346
+ console.log(`Run \`clone-architect update ${domain}\` to create one.`);
347
+
348
+ // Still show metadata from current DESIGN.md
349
+ const content = readFileSync(currentDesign, 'utf-8');
350
+ const scoreMatch = content.match(/^completeness:\s*(\d+)/m);
351
+ const dateMatch = content.match(/^extracted_at:\s*"?([^"\n]+)"?/m);
352
+ if (scoreMatch || dateMatch) {
353
+ console.log(`\nCurrent extraction:`);
354
+ if (scoreMatch) console.log(` Completeness: ${scoreMatch[1]}/100`);
355
+ if (dateMatch) console.log(` Extracted: ${dateMatch[1]}`);
356
+ }
357
+ process.exit(0);
358
+ }
359
+
360
+ const latestBackup = join(extractionsDir, backups[0]);
361
+ const backupDesign = join(latestBackup, 'DESIGN.md');
362
+ const backupTokens = join(latestBackup, 'tokens.json');
363
+
364
+ console.log(`\nšŸ” Diffing ${domain}:`);
365
+ console.log(` Current: ${extractionDir}`);
366
+ console.log(` Backup: ${latestBackup}\n`);
367
+
368
+ // Token diff — compare accent, bg, font
369
+ if (existsSync(currentTokens) && existsSync(backupTokens)) {
370
+ try {
371
+ const curr = JSON.parse(readFileSync(currentTokens, 'utf-8'));
372
+ const prev = JSON.parse(readFileSync(backupTokens, 'utf-8'));
373
+
374
+ const diffs = [];
375
+ const COMPARE_PATHS = [
376
+ ['colors.background.primary', 'Background'],
377
+ ['colors.text.primary', 'Primary text'],
378
+ ['colors.accent.primary', 'Accent'],
379
+ ['colors.border', 'Border'],
380
+ ['typography.fontFamily.primary', 'Font'],
381
+ ];
382
+
383
+ for (const [path, label] of COMPARE_PATHS) {
384
+ const getPath = (obj, p) => p.split('.').reduce((o, k) => o?.[k], obj);
385
+ const currVal = getPath(curr, path);
386
+ const prevVal = getPath(prev, path);
387
+ if (currVal && prevVal && currVal !== prevVal) {
388
+ diffs.push(` ${label}: ${prevVal} → ${currVal}`);
389
+ }
390
+ }
391
+
392
+ if (diffs.length > 0) {
393
+ console.log('Token changes:');
394
+ diffs.forEach(d => console.log(d));
395
+ } else {
396
+ console.log('No token changes detected.');
397
+ }
398
+ } catch { /* ignore parse errors */ }
399
+ console.log('');
400
+ }
401
+
402
+ // DESIGN.md diff — show line count change and completeness diff
403
+ if (existsSync(backupDesign)) {
404
+ const currContent = readFileSync(currentDesign, 'utf-8');
405
+ const prevContent = readFileSync(backupDesign, 'utf-8');
406
+ const currLines = currContent.split('\n').length;
407
+ const prevLines = prevContent.split('\n').length;
408
+ const currScore = currContent.match(/^completeness:\s*(\d+)/m)?.[1];
409
+ const prevScore = prevContent.match(/^completeness:\s*(\d+)/m)?.[1];
410
+ const currDate = currContent.match(/^extracted_at:\s*"?([^"\n]+)"?/m)?.[1];
411
+ const prevDate = prevContent.match(/^extracted_at:\s*"?([^"\n]+)"?/m)?.[1];
412
+
413
+ console.log('DESIGN.md diff:');
414
+ console.log(` Lines: ${prevLines} → ${currLines} (${currLines >= prevLines ? '+' : ''}${currLines - prevLines})`);
415
+ if (currScore && prevScore) {
416
+ const diff = parseInt(currScore) - parseInt(prevScore);
417
+ console.log(` Completeness: ${prevScore}/100 → ${currScore}/100 (${diff >= 0 ? '+' : ''}${diff})`);
418
+ }
419
+ if (currDate && prevDate && currDate !== prevDate) {
420
+ console.log(` Extracted: ${prevDate.slice(0, 10)} → ${currDate.slice(0, 10)}`);
421
+ }
422
+
423
+ // Count section headers
424
+ const currSections = (currContent.match(/^## \d+\./gm) || []).length;
425
+ const prevSections = (prevContent.match(/^## \d+\./gm) || []).length;
426
+ if (currSections !== prevSections) {
427
+ console.log(` Sections: ${prevSections} → ${currSections}`);
428
+ }
429
+
430
+ // Run word diff via system diff if available
431
+ const diffResult = spawnSync('diff', ['--unified=0', backupDesign, currentDesign], { encoding: 'utf-8' });
432
+ if (diffResult.status !== 0 && diffResult.stdout) {
433
+ const diffLines = diffResult.stdout.split('\n');
434
+ const addedLines = diffLines.filter(l => l.startsWith('+') && !l.startsWith('+++'));
435
+ const removedLines = diffLines.filter(l => l.startsWith('-') && !l.startsWith('---'));
436
+ if (addedLines.length + removedLines.length > 0) {
437
+ console.log(`\n +${addedLines.length} lines added, -${removedLines.length} lines removed`);
438
+ }
439
+ }
440
+ console.log('');
441
+ }
442
+
443
+ process.exit(0);
444
+ }
445
+
446
+ const script = SCRIPTS[cmd];
447
+ if (!script) {
448
+ console.error(`Unknown command: "${cmd}"`);
449
+ console.error(`Run \`clone-architect --help\` for available commands.`);
450
+ process.exit(1);
451
+ }
452
+
453
+ const scriptPath = join(pkgRoot, script);
454
+ if (!existsSync(scriptPath)) {
455
+ console.error(`Internal error: script not found: ${scriptPath}`);
456
+ console.error(`This is a packaging bug — please report it.`);
457
+ process.exit(2);
458
+ }
459
+
460
+ // Resolve tsx binary — prefer local node_modules/.bin, fall back to npx tsx
461
+ const localTsx = join(pkgRoot, 'node_modules', '.bin', 'tsx');
462
+ const tsxCmd = existsSync(localTsx) ? localTsx : 'npx';
463
+ const tsxArgs = existsSync(localTsx) ? [scriptPath, ...args] : ['tsx', scriptPath, ...args];
464
+
465
+ const result = spawnSync(tsxCmd, tsxArgs, {
466
+ stdio: 'inherit',
467
+ cwd: process.cwd(),
468
+ });
469
+
470
+ if (result.error) {
471
+ console.error('Failed to invoke tsx:', result.error.message);
472
+ console.error('Make sure tsx is installed: npm install -g tsx');
473
+ process.exit(3);
474
+ }
475
+
476
+ process.exit(result.status ?? 0);