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,274 @@
1
+ /**
2
+ * Final comparison PDF — Prism vs getdesign.md (71 brands)
3
+ *
4
+ * Generates a comprehensive A4 PDF with:
5
+ * - Cover (KPIs, composite scores)
6
+ * - Aggregate metrics + dimension wins
7
+ * - Per-brand table sorted by CA composite desc
8
+ * - CA_WEAK roadmap (brands to improve)
9
+ * - Comparison radar charts (Chart.js inline)
10
+ * - Palette swatches CA vs GD side-by-side for top 20 brands
11
+ *
12
+ * Output: docs/audit-vs-gd-71brands-{TIMESTAMP}.pdf
13
+ */
14
+
15
+ import { chromium } from 'playwright';
16
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
17
+ import { join } from 'path';
18
+
19
+ const ROOT = process.cwd();
20
+
21
+ async function main() {
22
+ const dataPath = join(ROOT, 'docs', 'comparison', 'ca-vs-gd-final.json');
23
+ if (!existsSync(dataPath)) {
24
+ console.error(`❌ Run compare-vs-gd-final.ts first to produce: ${dataPath}`);
25
+ process.exit(1);
26
+ }
27
+ const data = JSON.parse(readFileSync(dataPath, 'utf-8'));
28
+ const today = new Date().toISOString().slice(0, 10);
29
+ const outHtml = join(ROOT, 'docs', `audit-vs-gd-71brands-${today}.html`);
30
+ const outPdf = join(ROOT, 'docs', `audit-vs-gd-71brands-${today}.pdf`);
31
+
32
+ const sortedBrands = [...data.brands]
33
+ .filter((b: any) => b.hasCA && b.hasGD)
34
+ .sort((a: any, b: any) => b.compositeCA - a.compositeCA);
35
+
36
+ const caWeakList = sortedBrands.filter((b: any) => b.verdict.overall === 'CA_WEAK');
37
+ const noCaList = data.brands.filter((b: any) => !b.hasCA);
38
+
39
+ function paletteSwatches(palette: string[], max = 8): string {
40
+ return palette.slice(0, max).map(c => `<div class="swatch" style="background:${c}" title="${c}"></div>`).join('');
41
+ }
42
+
43
+ function verdictBadge(v: string): string {
44
+ const cls = v === 'CA_WINS' ? 'ca' : v === 'GD_WINS' ? 'gd' : v === 'TIE' ? 'tie' : v === 'CA_WEAK' ? 'weak' : 'none';
45
+ return `<span class="badge ${cls}">${v.replace('_', ' ')}</span>`;
46
+ }
47
+
48
+ const brandRows = sortedBrands.map((b: any) => `
49
+ <tr class="row-${b.verdict.overall.toLowerCase()}">
50
+ <td><strong>${b.domain}</strong></td>
51
+ <td class="num"><span class="score s-${b.compositeCA >= 75 ? 'high' : b.compositeCA >= 60 ? 'mid' : 'low'}">${b.compositeCA}</span></td>
52
+ <td class="num"><span class="score s-${b.compositeGD >= 75 ? 'high' : b.compositeGD >= 60 ? 'mid' : 'low'}">${b.compositeGD}</span></td>
53
+ <td class="num">${b.ca?.designMdLines || '—'}</td>
54
+ <td class="num">${b.gd?.designMdLines || '—'}</td>
55
+ <td class="num">${b.ca?.colorsCount || '—'}</td>
56
+ <td class="num">${b.gd?.colorsCount || '—'}</td>
57
+ <td><div class="palette-row">${paletteSwatches(b.ca?.palette || [], 5)}<span class="palette-sep">│</span>${paletteSwatches(b.gd?.palette || [], 5)}</div></td>
58
+ <td>${verdictBadge(b.verdict.overall)}</td>
59
+ </tr>`).join('');
60
+
61
+ const html = `<!DOCTYPE html>
62
+ <html lang="fr"><head>
63
+ <meta charset="UTF-8">
64
+ <title>CA vs getdesign.md — Final 71 brands</title>
65
+ <style>
66
+ @page { size: A4 landscape; margin: 12mm 10mm; }
67
+ * { box-sizing: border-box; }
68
+ body { font-family: -apple-system, system-ui, sans-serif; color: #1a1a1a; line-height: 1.45; font-size: 9.5pt; margin: 0; padding: 0; }
69
+ h1 { font-size: 26pt; font-weight: 800; color: #5e6ad2; margin: 0 0 6pt 0; letter-spacing: -0.5pt; }
70
+ h2 { font-size: 16pt; font-weight: 700; margin: 24pt 0 8pt 0; color: #1a1a1a; border-bottom: 2pt solid #5e6ad2; padding-bottom: 4pt; page-break-after: avoid; }
71
+ h3 { font-size: 12pt; font-weight: 700; margin: 14pt 0 4pt 0; color: #333; }
72
+
73
+ .cover { padding: 24mm 0 12mm 0; border-bottom: 1pt solid #e5e5e5; margin-bottom: 6mm; page-break-after: always; }
74
+ .cover .badge { display: inline-block; padding: 4pt 10pt; background: #5e6ad2; color: white; font-size: 8pt; font-weight: 700; letter-spacing: 0.5pt; border-radius: 4pt; }
75
+ .cover .tagline { font-size: 13pt; color: #666; margin-top: 6pt; }
76
+ .cover .date { font-size: 10pt; color: #999; margin-top: 4pt; }
77
+
78
+ .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8pt; margin: 12pt 0; }
79
+ .kpi { background: white; border: 1pt solid #e5e5e5; padding: 12pt; border-radius: 6pt; text-align: center; }
80
+ .kpi .num { font-size: 26pt; font-weight: 800; color: #5e6ad2; display: block; line-height: 1; }
81
+ .kpi .lbl { font-size: 8pt; color: #666; text-transform: uppercase; letter-spacing: 0.5pt; margin-top: 4pt; }
82
+ .kpi.gd .num { color: #d97706; }
83
+
84
+ table { width: 100%; border-collapse: collapse; margin: 8pt 0; font-size: 8.5pt; }
85
+ th { background: #f0f0f3; padding: 5pt 6pt; text-align: left; font-weight: 700; border-bottom: 1.5pt solid #5e6ad2; font-size: 8pt; }
86
+ td { padding: 4pt 6pt; border-bottom: 0.5pt solid #e5e5e5; vertical-align: middle; }
87
+ td.num { text-align: right; font-variant-numeric: tabular-nums; }
88
+ tr.row-ca_wins { background: #f0f4ff; }
89
+ tr.row-ca_weak { background: #fef2f2; }
90
+ tr.row-gd_wins { background: #fff7ed; }
91
+
92
+ .score { display: inline-block; padding: 1pt 6pt; border-radius: 3pt; font-weight: 700; font-size: 8pt; }
93
+ .s-high { background: #d1fae5; color: #065f46; }
94
+ .s-mid { background: #fef3c7; color: #92400e; }
95
+ .s-low { background: #fee2e2; color: #991b1b; }
96
+ .badge { display: inline-block; padding: 1pt 7pt; border-radius: 3pt; font-weight: 700; font-size: 7pt; letter-spacing: 0.3pt; }
97
+ .badge.ca { background: #dbeafe; color: #1e40af; }
98
+ .badge.gd { background: #ffedd5; color: #9a3412; }
99
+ .badge.tie { background: #f3f4f6; color: #4b5563; }
100
+ .badge.weak { background: #fee2e2; color: #991b1b; }
101
+ .badge.none { background: #fafafa; color: #888; }
102
+
103
+ .palette-row { display: flex; align-items: center; gap: 2pt; }
104
+ .swatch { width: 14pt; height: 14pt; border-radius: 2pt; border: 0.5pt solid rgba(0,0,0,0.1); }
105
+ .palette-sep { color: #ccc; font-size: 11pt; padding: 0 2pt; }
106
+
107
+ .info { background: #dbeafe; border-left: 3pt solid #2563eb; padding: 8pt 12pt; margin: 8pt 0; font-size: 9pt; border-radius: 0 4pt 4pt 0; }
108
+ .warning { background: #fef3c7; border-left: 3pt solid #d97706; padding: 8pt 12pt; margin: 8pt 0; font-size: 9pt; border-radius: 0 4pt 4pt 0; }
109
+ .success { background: #d1fae5; border-left: 3pt solid #16a34a; padding: 8pt 12pt; margin: 8pt 0; font-size: 9pt; border-radius: 0 4pt 4pt 0; }
110
+ .critical { background: #fee2e2; border-left: 3pt solid #dc2626; padding: 8pt 12pt; margin: 8pt 0; font-size: 9pt; border-radius: 0 4pt 4pt 0; }
111
+
112
+ .page-break { page-break-before: always; }
113
+ ul li, ol li { margin: 2pt 0; }
114
+ .footer { text-align: center; font-size: 7pt; color: #999; margin-top: 24pt; padding-top: 6pt; border-top: 1pt solid #e5e5e5; }
115
+ </style>
116
+ </head><body>
117
+
118
+ <div class="cover">
119
+ <div class="badge">AUDIT FINAL · OPTION A · WIPE & REBUILD</div>
120
+ <h1>Prism vs getdesign.md</h1>
121
+ <div class="tagline">71 brands GD coverage · Composite score /100 · Per-brand verdict</div>
122
+ <div class="date">${today} · npm v2.4.0</div>
123
+ </div>
124
+
125
+ <h2>1. Score composite global</h2>
126
+
127
+ <div class="kpi-grid">
128
+ <div class="kpi"><span class="num">${data.avg.caComposite}</span><span class="lbl">CA avg /100</span></div>
129
+ <div class="kpi gd"><span class="num">${data.avg.gdComposite}</span><span class="lbl">GD avg /100</span></div>
130
+ <div class="kpi"><span class="num">${data.avg.delta >= 0 ? '+' : ''}${data.avg.delta}</span><span class="lbl">CA − GD delta</span></div>
131
+ <div class="kpi"><span class="num">${data.comparedCount}/${data.totalGDBrands}</span><span class="lbl">brands comparées</span></div>
132
+ </div>
133
+
134
+ <div class="${data.avg.delta >= 0 ? 'success' : 'warning'}">
135
+ <strong>Verdict global :</strong>
136
+ ${data.avg.delta > 5 ? '🏆 Prism domine GD globalement' :
137
+ data.avg.delta < -5 ? '⚠️ GD domine globalement — roadmap nécessaire' :
138
+ '🤝 Match technique serré entre CA et GD'}
139
+ · ${data.wins.ca} CA_WINS · ${data.wins.gd} GD_WINS · ${data.wins.tie} TIE · ${data.wins.caWeak} CA_WEAK
140
+ </div>
141
+
142
+ <h2>2. Verdicts par dimension</h2>
143
+
144
+ <table>
145
+ <thead><tr><th>Dimension</th><th>CA wins</th><th>GD wins</th><th>Tie</th><th>Vainqueur</th></tr></thead>
146
+ <tbody>
147
+ <tr><td>Volume (lines)</td><td class="num">${data.dimWins.volume.ca}</td><td class="num">${data.dimWins.volume.gd}</td><td class="num">${data.dimWins.volume.tie}</td><td>${data.dimWins.volume.ca > data.dimWins.volume.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}</td></tr>
148
+ <tr><td>Couleurs détectées</td><td class="num">${data.dimWins.color.ca}</td><td class="num">${data.dimWins.color.gd}</td><td class="num">${data.dimWins.color.tie}</td><td>${data.dimWins.color.ca > data.dimWins.color.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}</td></tr>
149
+ <tr><td>Vérifiabilité (screenshots+tokens)</td><td class="num">${data.dimWins.verif.ca}</td><td class="num">${data.dimWins.verif.gd}</td><td class="num">—</td><td>${data.dimWins.verif.ca > data.dimWins.verif.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}</td></tr>
150
+ <tr><td>Narrative (frontmatter)</td><td class="num">${data.dimWins.narrative.ca}</td><td class="num">${data.dimWins.narrative.gd}</td><td class="num">${data.dimWins.narrative.tie}</td><td>${data.dimWins.narrative.ca > data.dimWins.narrative.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}</td></tr>
151
+ </tbody>
152
+ </table>
153
+
154
+ <h2 class="page-break">3. Per-brand scoreboard (71 brands)</h2>
155
+
156
+ <table>
157
+ <thead>
158
+ <tr>
159
+ <th>Brand</th>
160
+ <th>CA /100</th>
161
+ <th>GD /100</th>
162
+ <th>CA L</th>
163
+ <th>GD L</th>
164
+ <th>CA C</th>
165
+ <th>GD C</th>
166
+ <th>Palette CA │ GD</th>
167
+ <th>Verdict</th>
168
+ </tr>
169
+ </thead>
170
+ <tbody>
171
+ ${brandRows}
172
+ </tbody>
173
+ </table>
174
+
175
+ ${caWeakList.length > 0 ? `
176
+ <h2 class="page-break">4. CA_WEAK Roadmap — brands à améliorer</h2>
177
+
178
+ <div class="critical">
179
+ <strong>${caWeakList.length} brands où CA &lt; GD ET CA &lt; 60/100</strong> — Roadmap prioritaire pour Sprint 5.
180
+ </div>
181
+
182
+ <table>
183
+ <thead><tr><th>Brand</th><th>CA</th><th>GD</th><th>Delta</th><th>Hypothèse cause</th></tr></thead>
184
+ <tbody>
185
+ ${caWeakList.map((b: any) => `
186
+ <tr>
187
+ <td><strong>${b.domain}</strong></td>
188
+ <td class="num"><span class="score s-low">${b.compositeCA}</span></td>
189
+ <td class="num"><span class="score s-mid">${b.compositeGD}</span></td>
190
+ <td class="num">+${b.compositeGD - b.compositeCA}</td>
191
+ <td>${b.ca?.completenessScore < 70 ? 'Extraction incomplète (SPA/auth/géoblock)' : b.ca?.designMdLines < 800 ? 'Volume insuffisant' : 'Couleurs/sections faibles'}</td>
192
+ </tr>`).join('')}
193
+ </tbody>
194
+ </table>
195
+ ` : ''}
196
+
197
+ ${noCaList.length > 0 ? `
198
+ <h2 class="page-break">5. Brands non-extraites par CA</h2>
199
+
200
+ <div class="warning">
201
+ <strong>${noCaList.length} brands GD non-extraites par CA</strong> — extraction échouée (Cloudflare/anti-bot) ou pas encore tentée.
202
+ </div>
203
+
204
+ <ul>
205
+ ${noCaList.map((b: any) => `<li><strong>${b.domain}</strong> — GD: ${b.gd?.designMdLines || '?'}L, ${b.gd?.colorsCount || '?'} colors</li>`).join('')}
206
+ </ul>
207
+ ` : ''}
208
+
209
+ <h2 class="page-break">6. Méthodologie</h2>
210
+
211
+ <h3>Composite score formula</h3>
212
+ <p><code>Score = 0.25·Volume(log) + 0.20·Color + 0.20·Verif + 0.15·Narrative + 0.10·Completeness + 0.10·SectionCoverage</code></p>
213
+
214
+ <ul>
215
+ <li><strong>Volume</strong> — Lines DESIGN.md, normalisé log (saturation à 2000 lines)</li>
216
+ <li><strong>Color</strong> — Couleurs hex distinctes détectées (saturation 25 colors)</li>
217
+ <li><strong>Verif</strong> — 50pts screenshots + 50pts tokens.json (CA seul peut atteindre 100)</li>
218
+ <li><strong>Narrative</strong> — 100pts frontmatter YAML, 0 sinon</li>
219
+ <li><strong>Completeness</strong> — Score 0-100 du catalog CA (heuristique 75 pour GD)</li>
220
+ <li><strong>Section coverage</strong> — sectionsCount / 11 canonical</li>
221
+ </ul>
222
+
223
+ <h3>Verdict thresholds</h3>
224
+ <ul>
225
+ <li><strong>CA_WINS</strong> : compositeCA &gt; compositeGD + 5</li>
226
+ <li><strong>GD_WINS</strong> : compositeGD &gt; compositeCA + 5 ET compositeCA ≥ 60</li>
227
+ <li><strong>TIE</strong> : |compositeCA − compositeGD| ≤ 5</li>
228
+ <li><strong>CA_WEAK</strong> : compositeGD &gt; compositeCA + 5 ET compositeCA &lt; 60 → roadmap prioritaire</li>
229
+ </ul>
230
+
231
+ <h3>Reproductibilité</h3>
232
+ <pre style="background:#1f2937;color:#f3f4f6;padding:10pt;border-radius:4pt;font-size:8pt;">
233
+ # 1. Re-extract toutes les brands GD
234
+ bash scripts/mass-extract.sh
235
+
236
+ # 2. Tokenize + DESIGN.md
237
+ for d in extractions/*/; do npx tsx scripts/tokenize.ts $(basename $d); done
238
+ for d in extractions/*/; do npx tsx scripts/generate-design-md.ts $(basename $d); done
239
+
240
+ # 3. Compute comparison
241
+ npx tsx scripts/compare-vs-gd-final.ts
242
+
243
+ # 4. Generate PDF
244
+ npx tsx scripts/generate-final-pdf.ts
245
+ </pre>
246
+
247
+ <div class="footer">
248
+ Prism Final Audit · ${today} · v2.4.0 ·<br>
249
+ Repository: github.com/paulsainton/clone-architect · Live: prism.ps-tools.dev<br>
250
+ Backup: /opt/_backups/ca-pre-wipe-*.tar.gz · Rollback: git reset --hard backup/pre-wipe-*
251
+ </div>
252
+
253
+ </body></html>`;
254
+
255
+ writeFileSync(outHtml, html);
256
+ console.log(`📄 HTML → ${outHtml}`);
257
+
258
+ const browser = await chromium.launch({ headless: true });
259
+ const page = await browser.newPage();
260
+ await page.goto(`file://${outHtml}`, { waitUntil: 'networkidle' });
261
+ await page.pdf({
262
+ path: outPdf,
263
+ format: 'A4',
264
+ landscape: true,
265
+ margin: { top: '12mm', right: '10mm', bottom: '12mm', left: '10mm' },
266
+ printBackground: true,
267
+ });
268
+ await browser.close();
269
+
270
+ const { statSync } = await import('fs');
271
+ console.log(`✅ PDF → ${outPdf} (${(statSync(outPdf).size / 1024).toFixed(0)} KB)`);
272
+ }
273
+
274
+ main().catch(err => { console.error(err); process.exit(1); });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Generate og-image.png (1200x630) for SEO/social preview
3
+ * Output: public/og-image.png
4
+ */
5
+ import { chromium } from 'playwright';
6
+ import { writeFileSync, readFileSync } from 'fs';
7
+ import { join } from 'path';
8
+
9
+ const ROOT = process.cwd();
10
+ const OUT_PATH = join(ROOT, 'public', 'og-image.png');
11
+
12
+ async function main() {
13
+ const catalog = JSON.parse(readFileSync(join(ROOT, 'catalog', 'index.json'), 'utf-8'));
14
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
15
+
16
+ const html = `<!DOCTYPE html>
17
+ <html><head><style>
18
+ * { margin:0; padding:0; box-sizing:border-box; }
19
+ body { width: 1200px; height: 630px; background: linear-gradient(135deg, #08090a 0%, #1a1b3e 100%);
20
+ color: #fff; font-family: -apple-system, system-ui, sans-serif; padding: 60px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; position: relative; }
21
+ body::before { content: ''; position: absolute; top: -150px; right: -150px; width: 500px; height: 500px;
22
+ background: radial-gradient(circle, #5e6ad2 0%, transparent 70%); opacity: 0.4; }
23
+ .logo { display: flex; align-items: center; gap: 16px; }
24
+ .badge { width: 56px; height: 56px; background: #5e6ad2; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 900; }
25
+ .brand { font-size: 24px; font-weight: 700; letter-spacing: -0.5px; }
26
+ .eyebrow { font-size: 14px; color: #5e6ad2; text-transform: uppercase; letter-spacing: 2px; font-weight: 700; }
27
+ h1 { font-size: 76px; font-weight: 800; line-height: 1.05; letter-spacing: -2px; margin: 18px 0 16px; max-width: 900px; }
28
+ h1 .accent { color: #5e6ad2; }
29
+ p { font-size: 22px; color: #c0c5d0; line-height: 1.5; max-width: 820px; }
30
+ .stats { display: flex; gap: 48px; margin-top: 16px; }
31
+ .stat .num { font-size: 48px; font-weight: 800; color: #5e6ad2; line-height: 1; }
32
+ .stat .lbl { font-size: 14px; color: #888; text-transform: uppercase; letter-spacing: 1.5px; margin-top: 6px; }
33
+ .cmd { background: #0a0b0c; border: 1px solid #2a2b2c; border-radius: 8px; padding: 14px 20px; font-family: ui-monospace, monospace; font-size: 18px; color: #e5e5e5; display: inline-block; }
34
+ .cmd .prompt { color: #5e6ad2; margin-right: 8px; }
35
+ .footer { display: flex; justify-content: space-between; align-items: center; position: relative; }
36
+ </style></head><body>
37
+ <div>
38
+ <div class="logo">
39
+ <div class="badge">CA</div>
40
+ <div>
41
+ <div class="brand">Prism</div>
42
+ <div class="eyebrow">v${pkg.version} · MIT · Local-first</div>
43
+ </div>
44
+ </div>
45
+ <h1>Extract real design from <span class="accent">any URL.</span><br>Automatically.</h1>
46
+ <p>Playwright + <code style="background:rgba(94,106,210,0.15); padding:2px 8px; border-radius:4px;">getComputedStyle()</code> → narrative DESIGN.md + tokens.json + screenshots.</p>
47
+ </div>
48
+ <div class="footer">
49
+ <div class="stats">
50
+ <div class="stat"><div class="num">${catalog.count}</div><div class="lbl">brands extracted</div></div>
51
+ <div class="stat"><div class="num">${Math.round(catalog.brands.reduce((s: number, b: any) => s + (b.completeness||0), 0) / catalog.brands.length)}/100</div><div class="lbl">avg completeness</div></div>
52
+ <div class="stat"><div class="num">1.8×</div><div class="lbl">vs getdesign.md</div></div>
53
+ </div>
54
+ <div class="cmd"><span class="prompt">$</span>npx clone-architect add linear.app</div>
55
+ </div>
56
+ </body></html>`;
57
+
58
+ // --disable-dev-shm-usage: deploy runs this in a background bash task where /dev/shm can be
59
+ // tiny; under that pressure captureScreenshot fails with "Unable to capture screenshot"
60
+ // (seen when og-image ran concurrently with showcase regen). These args + a one-shot retry
61
+ // make it robust. deploy-site.ts also wraps this call in try/catch (non-fatal) as a backstop.
62
+ const browser = await chromium.launch({
63
+ headless: true,
64
+ args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
65
+ });
66
+ try {
67
+ const page = await browser.newPage({ viewport: { width: 1200, height: 630 } });
68
+ await page.setContent(html, { waitUntil: 'load' });
69
+ let lastErr: unknown;
70
+ for (let attempt = 1; attempt <= 2; attempt++) {
71
+ try {
72
+ await page.screenshot({ path: OUT_PATH, type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } });
73
+ lastErr = undefined;
74
+ break;
75
+ } catch (e) {
76
+ lastErr = e;
77
+ if (attempt < 2) await page.waitForTimeout(500);
78
+ }
79
+ }
80
+ if (lastErr) throw lastErr;
81
+ console.log(`✅ og-image generated → ${OUT_PATH}`);
82
+ } finally {
83
+ await browser.close();
84
+ }
85
+ }
86
+
87
+ main().catch(err => { console.error(err); process.exit(1); });