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.
- package/CHANGELOG.md +292 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/bin/clone-architect.mjs +476 -0
- package/bin/prism.mjs +467 -0
- package/catalog/index.json +1155 -0
- package/extractions/airbnb.com/DESIGN.md +1068 -0
- package/extractions/airbnb.com/tokens.json +507 -0
- package/extractions/attio.com/DESIGN.md +1295 -0
- package/extractions/attio.com/tokens.json +438 -0
- package/extractions/auroxdashboard.com/DESIGN.md +724 -0
- package/extractions/auroxdashboard.com/tokens.json +195 -0
- package/extractions/careerexplorer.com/DESIGN.md +1178 -0
- package/extractions/careerexplorer.com/tokens.json +141 -0
- package/extractions/chance.co/DESIGN.md +1209 -0
- package/extractions/chance.co/tokens.json +160 -0
- package/extractions/choisis-ton-avenir.com/DESIGN.md +1265 -0
- package/extractions/choisis-ton-avenir.com/tokens.json +227 -0
- package/extractions/example.com/DESIGN.md +436 -0
- package/extractions/example.com/tokens.json +91 -0
- package/extractions/getdesign.md/DESIGN.md +1009 -0
- package/extractions/getdesign.md/tokens.json +219 -0
- package/extractions/github.com/DESIGN.md +1130 -0
- package/extractions/github.com/tokens.json +2092 -0
- package/extractions/hello-charly.com/DESIGN.md +1146 -0
- package/extractions/hello-charly.com/tokens.json +322 -0
- package/extractions/hyperliquid.xyz/DESIGN.md +779 -0
- package/extractions/hyperliquid.xyz/tokens.json +598 -0
- package/extractions/instagram.com/DESIGN.md +996 -0
- package/extractions/instagram.com/tokens.json +1240 -0
- package/extractions/jobirl.com/DESIGN.md +1160 -0
- package/extractions/jobirl.com/tokens.json +139 -0
- package/extractions/life360.com/DESIGN.md +1133 -0
- package/extractions/life360.com/tokens.json +491 -0
- package/extractions/lifesum.com/DESIGN.md +965 -0
- package/extractions/lifesum.com/tokens.json +170 -0
- package/extractions/linear.app/DESIGN.md +1301 -0
- package/extractions/linear.app/tokens.json +732 -0
- package/extractions/mavoie.org/DESIGN.md +1148 -0
- package/extractions/mavoie.org/tokens.json +128 -0
- package/extractions/miro.com/DESIGN.md +1237 -0
- package/extractions/miro.com/tokens.json +401 -0
- package/extractions/notion.so/DESIGN.md +1319 -0
- package/extractions/notion.so/tokens.json +906 -0
- package/extractions/onetonline.org/DESIGN.md +909 -0
- package/extractions/onetonline.org/tokens.json +280 -0
- package/extractions/posthog.com/DESIGN.md +1024 -0
- package/extractions/posthog.com/tokens.json +197 -0
- package/extractions/revolut.com/DESIGN.md +1080 -0
- package/extractions/revolut.com/tokens.json +401 -0
- package/extractions/stripe.com/DESIGN.md +1272 -0
- package/extractions/stripe.com/tokens.json +794 -0
- package/extractions/switchcollective.com/DESIGN.md +1040 -0
- package/extractions/switchcollective.com/tokens.json +98 -0
- package/extractions/truity.com/DESIGN.md +970 -0
- package/extractions/truity.com/tokens.json +166 -0
- package/extractions/uniquekicks.be/DESIGN.md +1171 -0
- package/extractions/uniquekicks.be/tokens.json +237 -0
- package/package.json +122 -0
- package/scripts/analyze.ts +281 -0
- package/scripts/bank-register.ts +379 -0
- package/scripts/bank.ts +374 -0
- package/scripts/browser-stealth.ts +189 -0
- package/scripts/clone.ts +198 -0
- package/scripts/compare-vs-gd-final.ts +273 -0
- package/scripts/compare-vs-gd.ts +269 -0
- package/scripts/compare.ts +405 -0
- package/scripts/deploy-site.ts +181 -0
- package/scripts/diff-snapshots.ts +340 -0
- package/scripts/enrich-catalog.ts +212 -0
- package/scripts/extract.ts +2038 -0
- package/scripts/extractors/advanced.ts +524 -0
- package/scripts/extractors/widgets.ts +711 -0
- package/scripts/generate-design-md.ts +5775 -0
- package/scripts/generate-final-pdf.ts +274 -0
- package/scripts/generate-og-image.ts +87 -0
- package/scripts/generate-showcase.ts +1588 -0
- package/scripts/generate-site.ts +847 -0
- package/scripts/mass-extract.sh +91 -0
- package/scripts/post-process-all.sh +55 -0
- package/scripts/regen-catalog.ts +203 -0
- package/scripts/shared/cache.ts +149 -0
- package/scripts/shared/css-helpers.ts +263 -0
- package/scripts/shared/logger.ts +57 -0
- package/scripts/shared/named-colors.ts +355 -0
- package/scripts/shared/types.ts +220 -0
- package/scripts/sync-catalog.ts +105 -0
- package/scripts/tokenize.ts +988 -0
- package/templates/layout-template.md +52 -0
- 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);
|