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
|
@@ -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);
|