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,181 @@
1
+ /**
2
+ * Prism โ€” Deploy Site to /opt/clone-architect-site
3
+ *
4
+ * 1. Gรฉnรจre public/index.html via generate-site.ts
5
+ * 2. Copie public/index.html โ†’ /opt/clone-architect-site/index.html
6
+ * 3. Copie chaque extractions/{domain}/showcase/index.html โ†’ /opt/clone-architect-site/brands/{domain}/index.html
7
+ * 4. Crรฉe /opt/clone-architect-site/404.html
8
+ * 5. Affiche le rรฉsumรฉ du dรฉploiement
9
+ *
10
+ * Usage: npx tsx scripts/deploy-site.ts
11
+ */
12
+
13
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+ import { execSync } from 'child_process';
16
+
17
+ const ROOT = process.cwd();
18
+ const DEPLOY_DIR = '/opt/clone-architect-site';
19
+ const CATALOG_PATH = join(ROOT, 'catalog', 'index.json');
20
+ const PUBLIC_INDEX = join(ROOT, 'public', 'index.html');
21
+
22
+ function writeWithSudo(targetPath: string, content: string): void {
23
+ // Try direct write first (paul owns /opt/clone-architect-site)
24
+ try {
25
+ const dir = dirname(targetPath);
26
+ mkdirSync(dir, { recursive: true });
27
+ writeFileSync(targetPath, content);
28
+ } catch {
29
+ // Fallback: sudo node write
30
+ const escaped = content.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
31
+ execSync(`sudo /usr/bin/node -e "
32
+ require('fs').mkdirSync(require('path').dirname('${targetPath}'), {recursive:true});
33
+ require('fs').writeFileSync('${targetPath}', \\\`${escaped}\\\`);
34
+ "`, { stdio: 'ignore' });
35
+ }
36
+ }
37
+
38
+ function copyWithSudo(src: string, dest: string): void {
39
+ try {
40
+ const dir = dirname(dest);
41
+ mkdirSync(dir, { recursive: true });
42
+ copyFileSync(src, dest);
43
+ } catch {
44
+ const dir = dirname(dest);
45
+ execSync(`sudo /usr/bin/node -e "
46
+ require('fs').mkdirSync('${dir}', {recursive:true});
47
+ require('fs').copyFileSync('${src}', '${dest}');
48
+ "`, { stdio: 'ignore' });
49
+ }
50
+ }
51
+
52
+ async function main() {
53
+ console.log('๐Ÿš€ Prism โ€” Deploy Site\n');
54
+
55
+ // Step 1 โ€” Regenerate public/index.html
56
+ console.log('๐Ÿ“„ Gรฉnรฉration public/index.html...');
57
+ execSync('npx tsx scripts/generate-site.ts', { cwd: ROOT, stdio: 'inherit' });
58
+
59
+ // Step 2 โ€” Ensure deploy dir exists
60
+ try {
61
+ mkdirSync(DEPLOY_DIR, { recursive: true });
62
+ } catch {
63
+ execSync(`sudo /usr/bin/node -e "require('fs').mkdirSync('${DEPLOY_DIR}', {recursive:true})"`, { stdio: 'ignore' });
64
+ }
65
+
66
+ // Step 2b โ€” Generate og-image.png (non-fatal: SEO/social-preview only, must NOT abort deploy).
67
+ // A transient Playwright screenshot failure here previously killed the whole deploy before
68
+ // showcases were copied. og-image regeneration is cosmetic โ€” the existing one is reused on failure.
69
+ console.log('\n๐ŸŽจ Gรฉnรฉration og-image.png...');
70
+ try {
71
+ execSync('npx tsx scripts/generate-og-image.ts', { cwd: ROOT, stdio: 'inherit' });
72
+ } catch (e) {
73
+ console.warn(` โš ๏ธ og-image generation failed (non-fatal, keeping existing): ${(e as Error).message.split('\n')[0]}`);
74
+ }
75
+
76
+ // Step 3 โ€” Copy index.html + og-image.png
77
+ console.log('\n๐Ÿ“‹ Copie index.html + og-image.png โ†’ /opt/clone-architect-site/...');
78
+ const indexContent = readFileSync(PUBLIC_INDEX, 'utf-8');
79
+ writeWithSudo(join(DEPLOY_DIR, 'index.html'), indexContent);
80
+ const ogPng = join(ROOT, 'public', 'og-image.png');
81
+ if (existsSync(ogPng)) copyWithSudo(ogPng, join(DEPLOY_DIR, 'og-image.png'));
82
+ console.log(' โœ… index.html + og-image.png dรฉployรฉs');
83
+
84
+ // Step 4 โ€” Copy showcase for each brand
85
+ console.log('\n๐ŸŽจ Dรฉploiement des showcases...');
86
+ const catalogRaw = readFileSync(CATALOG_PATH, 'utf-8');
87
+ const catalog = JSON.parse(catalogRaw);
88
+
89
+ let deployed = 0;
90
+ let skipped = 0;
91
+
92
+ for (const brand of catalog.brands) {
93
+ const domain = brand.domain;
94
+ const showcaseSrc = join(ROOT, 'extractions', domain, 'showcase', 'index.html');
95
+ const showcaseDest = join(DEPLOY_DIR, 'brands', domain, 'index.html');
96
+
97
+ if (existsSync(showcaseSrc)) {
98
+ copyWithSudo(showcaseSrc, showcaseDest);
99
+ deployed++;
100
+ } else {
101
+ skipped++;
102
+ }
103
+ }
104
+
105
+ console.log(` โœ… ${deployed} showcases dรฉployรฉs โ†’ /opt/clone-architect-site/brands/`);
106
+ if (skipped > 0) console.log(` โญ ${skipped} brands sans showcase (extraction incomplรจte)`);
107
+
108
+ // Step 4b โ€” Generate sitemap.xml (SEO indexation)
109
+ console.log('\n๐Ÿ—บ๏ธ Gรฉnรฉration sitemap.xml...');
110
+ const baseUrl = 'https://prism.ps-tools.dev';
111
+ const today = new Date().toISOString().slice(0, 10);
112
+ const sitemapUrls = [
113
+ `<url><loc>${baseUrl}/</loc><lastmod>${today}</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>`,
114
+ ];
115
+ for (const brand of catalog.brands) {
116
+ const domain = brand.domain;
117
+ const showcaseSrc = join(ROOT, 'extractions', domain, 'showcase', 'index.html');
118
+ if (existsSync(showcaseSrc)) {
119
+ sitemapUrls.push(`<url><loc>${baseUrl}/brands/${domain}/</loc><lastmod>${brand.extractedAt?.slice(0, 10) || today}</lastmod><changefreq>monthly</changefreq><priority>0.7</priority></url>`);
120
+ }
121
+ }
122
+ const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
123
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
124
+ ${sitemapUrls.join('\n')}
125
+ </urlset>`;
126
+ writeWithSudo(join(DEPLOY_DIR, 'sitemap.xml'), sitemapXml);
127
+ writeWithSudo(join(ROOT, 'public', 'sitemap.xml'), sitemapXml);
128
+ console.log(` โœ… sitemap.xml gรฉnรฉrรฉ (${sitemapUrls.length} URLs)`);
129
+
130
+ // Step 4c โ€” robots.txt
131
+ const robotsTxt = `User-agent: *
132
+ Allow: /
133
+ Disallow: /api/
134
+
135
+ Sitemap: ${baseUrl}/sitemap.xml
136
+ `;
137
+ writeWithSudo(join(DEPLOY_DIR, 'robots.txt'), robotsTxt);
138
+ writeWithSudo(join(ROOT, 'public', 'robots.txt'), robotsTxt);
139
+ console.log(` โœ… robots.txt dรฉployรฉ`);
140
+
141
+ // Step 5 โ€” Create 404.html
142
+ const notFoundHtml = `<!DOCTYPE html>
143
+ <html lang="en">
144
+ <head>
145
+ <meta charset="UTF-8">
146
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
147
+ <title>Not found โ€” Prism</title>
148
+ <style>
149
+ body { margin: 0; background: #0a0a0b; color: #e5e5e5; font-family: -apple-system, sans-serif;
150
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; }
151
+ .wrap { text-align: center; }
152
+ h1 { font-size: 4rem; margin: 0; color: #5e6ad2; }
153
+ p { color: #888; margin: 12px 0 32px; }
154
+ a { display: inline-block; padding: 10px 24px; background: #5e6ad2; color: #fff;
155
+ border-radius: 6px; text-decoration: none; font-size: 14px; }
156
+ a:hover { background: #4f5bbf; }
157
+ </style>
158
+ </head>
159
+ <body>
160
+ <div class="wrap">
161
+ <h1>404</h1>
162
+ <p>This brand hasn't been extracted yet.</p>
163
+ <a href="/">Browse 82 extractions โ†—</a>
164
+ </div>
165
+ </body>
166
+ </html>`;
167
+
168
+ writeWithSudo(join(DEPLOY_DIR, '404.html'), notFoundHtml);
169
+ writeWithSudo(join(ROOT, 'public', '404.html'), notFoundHtml);
170
+ console.log('\n๐Ÿ“„ 404.html crรฉรฉ');
171
+
172
+ // Step 6 โ€” Summary
173
+ console.log('\nโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
174
+ console.log('โœ… DEPLOY COMPLET');
175
+ console.log(` Site : https://prism.ps-tools.dev`);
176
+ console.log(` Brands : https://prism.ps-tools.dev/brands/linear.app/`);
177
+ console.log(` Total : ${deployed} showcases + index.html + 404.html`);
178
+ console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n');
179
+ }
180
+
181
+ main().catch(err => { console.error('โŒ Deploy error:', err); process.exit(1); });
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Prism โ€” Snapshot Diff Feature (unique moat vs getdesign.md)
3
+ *
4
+ * Compare two extractions of the same domain (at different dates) and report
5
+ * what changed in the design tokens: colors, typography, spacing, components.
6
+ *
7
+ * Use case: "What did Linear change in the last 3 months?"
8
+ *
9
+ * Usage:
10
+ * npx tsx scripts/diff-snapshots.ts <domain> # latest vs previous in archive/
11
+ * npx tsx scripts/diff-snapshots.ts <domain> <date1> <date2> # specific snapshots
12
+ * npx tsx scripts/diff-snapshots.ts --create <domain> # snapshot current state
13
+ */
14
+
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, copyFileSync } from 'fs';
16
+ import { join } from 'path';
17
+
18
+ const ROOT = process.cwd();
19
+ const ARCHIVE_DIR = join(ROOT, 'extractions', '_snapshots');
20
+
21
+ interface DesignTokens {
22
+ meta?: { extractedAt?: string; domain?: string };
23
+ colors?: {
24
+ background?: { primary?: string; secondary?: string; tertiary?: string };
25
+ text?: { primary?: string; secondary?: string; muted?: string };
26
+ accent?: any;
27
+ border?: any;
28
+ };
29
+ typography?: {
30
+ fontFamily?: { primary?: string; secondary?: string; mono?: string };
31
+ fontSize?: Record<string, string>;
32
+ fontWeight?: Record<string, string | number>;
33
+ };
34
+ spacing?: Record<string, string>;
35
+ borderRadius?: Record<string, string>;
36
+ shadows?: Record<string, string>;
37
+ }
38
+
39
+ function loadTokens(domain: string, snapshot?: string): DesignTokens | null {
40
+ let path: string;
41
+ if (snapshot) {
42
+ path = join(ARCHIVE_DIR, domain, snapshot, 'tokens.json');
43
+ } else {
44
+ path = join(ROOT, 'extractions', domain, 'tokens.json');
45
+ }
46
+ if (!existsSync(path)) return null;
47
+ return JSON.parse(readFileSync(path, 'utf-8'));
48
+ }
49
+
50
+ function listSnapshots(domain: string): string[] {
51
+ const dir = join(ARCHIVE_DIR, domain);
52
+ if (!existsSync(dir)) return [];
53
+ return readdirSync(dir).filter(d => /^\d{4}-\d{2}-\d{2}/.test(d)).sort();
54
+ }
55
+
56
+ function createSnapshot(domain: string): void {
57
+ const extractDir = join(ROOT, 'extractions', domain);
58
+ if (!existsSync(extractDir)) {
59
+ console.error(`โŒ Extraction not found: ${extractDir}`);
60
+ process.exit(1);
61
+ }
62
+ const date = new Date().toISOString().slice(0, 10);
63
+ const snapDir = join(ARCHIVE_DIR, domain, date);
64
+ mkdirSync(snapDir, { recursive: true });
65
+
66
+ // Copy tokens.json + DESIGN.md
67
+ for (const file of ['tokens.json', 'DESIGN.md', 'extraction-summary.json']) {
68
+ const src = join(extractDir, file);
69
+ if (existsSync(src)) copyFileSync(src, join(snapDir, file));
70
+ }
71
+ console.log(`๐Ÿ“ธ Snapshot created: ${snapDir}`);
72
+ }
73
+
74
+ function diffValue(label: string, a: any, b: any): string[] {
75
+ const lines: string[] = [];
76
+ if (a === b) return lines;
77
+ if (a === undefined || a === null) {
78
+ lines.push(` + ${label}: ${JSON.stringify(b)} (added)`);
79
+ } else if (b === undefined || b === null) {
80
+ lines.push(` - ${label}: ${JSON.stringify(a)} (removed)`);
81
+ } else {
82
+ lines.push(` ~ ${label}: ${JSON.stringify(a)} โ†’ ${JSON.stringify(b)}`);
83
+ }
84
+ return lines;
85
+ }
86
+
87
+ function diffObject(label: string, a: Record<string, any> = {}, b: Record<string, any> = {}): string[] {
88
+ const lines: string[] = [];
89
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
90
+ for (const k of keys) {
91
+ if (a[k] !== b[k]) {
92
+ lines.push(...diffValue(`${label}.${k}`, a[k], b[k]));
93
+ }
94
+ }
95
+ return lines;
96
+ }
97
+
98
+ function diffTokens(before: DesignTokens, after: DesignTokens): {
99
+ summary: { changed: number; added: number; removed: number };
100
+ lines: string[];
101
+ } {
102
+ const lines: string[] = [];
103
+
104
+ // Meta
105
+ const dateBefore = before.meta?.extractedAt?.slice(0, 10) || '?';
106
+ const dateAfter = after.meta?.extractedAt?.slice(0, 10) || '?';
107
+ lines.push(`\n๐Ÿ“… Comparison: ${dateBefore} โ†’ ${dateAfter}\n`);
108
+
109
+ // Colors
110
+ const colorChanges: string[] = [];
111
+ colorChanges.push(...diffObject('background', before.colors?.background, after.colors?.background));
112
+ colorChanges.push(...diffObject('text', before.colors?.text, after.colors?.text));
113
+ if (typeof before.colors?.accent === 'object' && typeof after.colors?.accent === 'object') {
114
+ colorChanges.push(...diffObject('accent', before.colors.accent, after.colors.accent));
115
+ }
116
+ if (typeof before.colors?.border === 'object' && typeof after.colors?.border === 'object') {
117
+ colorChanges.push(...diffObject('border', before.colors.border, after.colors.border));
118
+ }
119
+ if (colorChanges.length > 0) {
120
+ lines.push('๐ŸŽจ Colors:');
121
+ lines.push(...colorChanges);
122
+ }
123
+
124
+ // Typography
125
+ const typoChanges: string[] = [];
126
+ typoChanges.push(...diffObject('fontFamily', before.typography?.fontFamily, after.typography?.fontFamily));
127
+ typoChanges.push(...diffObject('fontSize', before.typography?.fontSize, after.typography?.fontSize));
128
+ typoChanges.push(...diffObject('fontWeight', before.typography?.fontWeight, after.typography?.fontWeight));
129
+ if (typoChanges.length > 0) {
130
+ lines.push('\n๐Ÿ”ค Typography:');
131
+ lines.push(...typoChanges);
132
+ }
133
+
134
+ // Spacing
135
+ const spacingChanges = diffObject('spacing', before.spacing, after.spacing);
136
+ if (spacingChanges.length > 0) {
137
+ lines.push('\n๐Ÿ“ Spacing:');
138
+ lines.push(...spacingChanges);
139
+ }
140
+
141
+ // Border radius
142
+ const radiusChanges = diffObject('borderRadius', before.borderRadius, after.borderRadius);
143
+ if (radiusChanges.length > 0) {
144
+ lines.push('\nโญ• Border radius:');
145
+ lines.push(...radiusChanges);
146
+ }
147
+
148
+ // Shadows
149
+ const shadowChanges = diffObject('shadow', before.shadows, after.shadows);
150
+ if (shadowChanges.length > 0) {
151
+ lines.push('\n๐Ÿ’ง Shadows:');
152
+ lines.push(...shadowChanges);
153
+ }
154
+
155
+ // Summary
156
+ let changed = 0, added = 0, removed = 0;
157
+ for (const l of lines) {
158
+ if (l.startsWith(' ~ ')) changed++;
159
+ else if (l.startsWith(' + ')) added++;
160
+ else if (l.startsWith(' - ')) removed++;
161
+ }
162
+
163
+ return {
164
+ summary: { changed, added, removed },
165
+ lines,
166
+ };
167
+ }
168
+
169
+ // โ”€โ”€ Cross-domain comparison (Design Diff killer feature) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
170
+
171
+ function compareDomains(domainA: string, domainB: string): void {
172
+ const tokensA = loadTokens(domainA);
173
+ const tokensB = loadTokens(domainB);
174
+
175
+ if (!tokensA) {
176
+ console.error(`โŒ No extraction for "${domainA}". Run: prism extract https://${domainA}`);
177
+ process.exit(1);
178
+ }
179
+ if (!tokensB) {
180
+ console.error(`โŒ No extraction for "${domainB}". Run: prism extract https://${domainB}`);
181
+ process.exit(1);
182
+ }
183
+
184
+ console.log(`\n๐Ÿ”ฌ Prism Design Diff โ€” Cross-brand comparison`);
185
+ console.log(` ${domainA} โ†” ${domainB}\n`);
186
+
187
+ const lines: string[] = [];
188
+
189
+ // Helper: color contrast label
190
+ function colorLabel(val: string | undefined): string {
191
+ return val ? val : 'โ€”';
192
+ }
193
+
194
+ // Colors table
195
+ lines.push('๐ŸŽจ COLORS\n');
196
+ const colorPairs = [
197
+ ['Background', tokensA.colors?.background?.primary, tokensB.colors?.background?.primary],
198
+ ['Background 2', tokensA.colors?.background?.secondary, tokensB.colors?.background?.secondary],
199
+ ['Text primary', tokensA.colors?.text?.primary, tokensB.colors?.text?.primary],
200
+ ['Text muted', tokensA.colors?.text?.muted, tokensB.colors?.text?.muted],
201
+ ['Accent', (tokensA.colors?.accent as any)?.primary, (tokensB.colors?.accent as any)?.primary],
202
+ ['Border', tokensA.colors?.border as string, tokensB.colors?.border as string],
203
+ ];
204
+ for (const [label, a, b] of colorPairs) {
205
+ const same = a === b ? ' โ† same' : '';
206
+ lines.push(` ${String(label).padEnd(14)} ${domainA.padEnd(20)} ${colorLabel(a as string)}`);
207
+ lines.push(` ${''.padEnd(14)} ${domainB.padEnd(20)} ${colorLabel(b as string)}${same}\n`);
208
+ }
209
+
210
+ // Typography table
211
+ lines.push('\n๐Ÿ”ค TYPOGRAPHY\n');
212
+ const typoPairs = [
213
+ ['Font body', tokensA.typography?.fontFamily?.primary, tokensB.typography?.fontFamily?.primary],
214
+ ['Font mono', tokensA.typography?.fontFamily?.mono, tokensB.typography?.fontFamily?.mono],
215
+ ['Size body', tokensA.typography?.fontSize?.base, tokensB.typography?.fontSize?.base],
216
+ ['Size heading', tokensA.typography?.fontSize?.xl, tokensB.typography?.fontSize?.xl],
217
+ ['Size display', tokensA.typography?.fontSize?.['3xl'], tokensB.typography?.fontSize?.['3xl']],
218
+ ['Weight bold', String(tokensA.typography?.fontWeight?.bold || ''), String(tokensB.typography?.fontWeight?.bold || '')],
219
+ ['Letter-spacing', (tokensA.typography as any)?.letterSpacing?.tight, (tokensB.typography as any)?.letterSpacing?.tight],
220
+ ];
221
+ for (const [label, a, b] of typoPairs) {
222
+ if (!a && !b) continue;
223
+ const delta = a !== b ? ' โ† differs' : '';
224
+ lines.push(` ${String(label).padEnd(16)} ${String(a || 'โ€”').padEnd(28)} ${String(b || 'โ€”')}${delta}`);
225
+ }
226
+
227
+ // Spacing table
228
+ lines.push('\n\n๐Ÿ“ SPACING\n');
229
+ const spacingKeys = ['sm', 'md', 'base', 'lg', 'xl'];
230
+ for (const k of spacingKeys) {
231
+ const a = (tokensA.spacing as any)?.[k];
232
+ const b = (tokensB.spacing as any)?.[k];
233
+ if (!a && !b) continue;
234
+ const delta = a !== b ? ' โ† differs' : '';
235
+ lines.push(` ${k.padEnd(6)} ${String(a || 'โ€”').padEnd(12)} ${String(b || 'โ€”')}${delta}`);
236
+ }
237
+
238
+ // Border radius
239
+ lines.push('\n\nโญ• BORDER RADIUS\n');
240
+ const radiusKeys = ['sm', 'md', 'lg', 'full'];
241
+ for (const k of radiusKeys) {
242
+ const a = (tokensA.borderRadius as any)?.[k];
243
+ const b = (tokensB.borderRadius as any)?.[k];
244
+ if (!a && !b) continue;
245
+ const delta = a !== b ? ' โ† differs' : '';
246
+ lines.push(` ${k.padEnd(6)} ${String(a || 'โ€”').padEnd(12)} ${String(b || 'โ€”')}${delta}`);
247
+ }
248
+
249
+ // Delta summary
250
+ let diffCount = 0;
251
+ for (const l of lines) { if (l.includes('โ† differs')) diffCount++; }
252
+
253
+ lines.push('\n');
254
+ lines.push(`\n${'โ•'.repeat(60)}`);
255
+ lines.push(`๐Ÿ“Š ${domainA} vs ${domainB}: ${diffCount} design differences found`);
256
+ lines.push(`${'โ•'.repeat(60)}\n`);
257
+
258
+ // Verdict
259
+ const aIsDark = (tokensA.meta as any)?.isDark ?? false;
260
+ const bIsDark = (tokensB.meta as any)?.isDark ?? false;
261
+ if (aIsDark !== bIsDark) {
262
+ lines.push(`๐Ÿ’ก Mode: ${domainA} is ${aIsDark ? 'dark' : 'light'}-native, ${domainB} is ${bIsDark ? 'dark' : 'light'}-native`);
263
+ }
264
+
265
+ console.log(lines.join('\n'));
266
+ }
267
+
268
+ async function main() {
269
+ const args = process.argv.slice(2);
270
+ const createMode = args.includes('--create');
271
+ const compareMode = args.includes('--compare') || (args.length === 2 && !args[0].startsWith('--') && !args[1].startsWith('--') && args[0].includes('.') && args[1].includes('.'));
272
+
273
+ if (createMode) {
274
+ const domain = args[args.indexOf('--create') + 1];
275
+ if (!domain) {
276
+ console.error('Usage: --create <domain>');
277
+ process.exit(1);
278
+ }
279
+ createSnapshot(domain);
280
+ return;
281
+ }
282
+
283
+ // Cross-domain comparison: prism diff linear.app notion.so
284
+ if (compareMode) {
285
+ const domainArgs = args.filter(a => !a.startsWith('--'));
286
+ if (domainArgs.length < 2) {
287
+ console.error('Usage: prism diff <domain-a> <domain-b>');
288
+ process.exit(1);
289
+ }
290
+ compareDomains(domainArgs[0], domainArgs[1]);
291
+ return;
292
+ }
293
+
294
+ const domain = args[0];
295
+ if (!domain) {
296
+ console.error(`Usage:
297
+ prism diff <domain-a> <domain-b> # compare two brands (Design Diff)
298
+ prism diff <domain> # current vs latest snapshot
299
+ prism diff <domain> <snap1> <snap2> # specific snapshots
300
+ prism diff --create <domain> # snapshot current state`);
301
+ process.exit(1);
302
+ }
303
+
304
+ const snapshots = listSnapshots(domain);
305
+ let before: DesignTokens | null = null;
306
+ let after: DesignTokens | null = null;
307
+ let beforeLabel = '?', afterLabel = '?';
308
+
309
+ if (args.length >= 3 && !args[1].includes('.') && !args[2].includes('.')) {
310
+ beforeLabel = args[1];
311
+ afterLabel = args[2];
312
+ before = loadTokens(domain, beforeLabel);
313
+ after = loadTokens(domain, afterLabel);
314
+ } else if (snapshots.length >= 1) {
315
+ beforeLabel = snapshots[0];
316
+ afterLabel = 'current';
317
+ before = loadTokens(domain, beforeLabel);
318
+ after = loadTokens(domain);
319
+ } else {
320
+ console.error(`โŒ No snapshots found for ${domain}. Create one with: prism diff --create ${domain}`);
321
+ process.exit(1);
322
+ }
323
+
324
+ if (!before || !after) {
325
+ console.error(`โŒ Missing tokens.json (before: ${!!before}, after: ${!!after})`);
326
+ process.exit(1);
327
+ }
328
+
329
+ console.log(`\n๐Ÿ” Prism โ€” Diff ${domain}`);
330
+ console.log(` ${beforeLabel} โ†’ ${afterLabel}\n`);
331
+
332
+ const diff = diffTokens(before, after);
333
+ console.log(diff.lines.join('\n'));
334
+
335
+ console.log(`\n${'โ•'.repeat(42)}`);
336
+ console.log(`๐Ÿ“Š Summary: ~${diff.summary.changed} changed ยท +${diff.summary.added} added ยท -${diff.summary.removed} removed`);
337
+ console.log(`${'โ•'.repeat(42)}\n`);
338
+ }
339
+
340
+ main().catch(err => { console.error(err); process.exit(1); });