designlang 5.0.0 → 7.0.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 (51) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. package/CHANGELOG.md +43 -0
  6. package/README.md +177 -6
  7. package/bin/design-extract.js +302 -92
  8. package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
  9. package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
  10. package/package.json +13 -7
  11. package/src/config.js +59 -0
  12. package/src/crawler.js +297 -95
  13. package/src/extractors/a11y-remediation.js +47 -0
  14. package/src/extractors/animations.js +37 -5
  15. package/src/extractors/borders.js +40 -5
  16. package/src/extractors/component-clusters.js +39 -0
  17. package/src/extractors/components.js +77 -1
  18. package/src/extractors/css-health.js +151 -0
  19. package/src/extractors/gradients.js +25 -5
  20. package/src/extractors/scoring.js +20 -1
  21. package/src/extractors/semantic-regions.js +44 -0
  22. package/src/extractors/shadows.js +60 -17
  23. package/src/extractors/spacing.js +31 -2
  24. package/src/extractors/stack-fingerprint.js +88 -0
  25. package/src/extractors/variables.js +20 -1
  26. package/src/formatters/_token-ref.js +44 -0
  27. package/src/formatters/agent-rules.js +116 -0
  28. package/src/formatters/android-compose.js +164 -0
  29. package/src/formatters/dtcg-tokens.js +175 -0
  30. package/src/formatters/figma.js +66 -47
  31. package/src/formatters/flutter-dart.js +130 -0
  32. package/src/formatters/ios-swiftui.js +161 -0
  33. package/src/formatters/markdown.js +25 -0
  34. package/src/formatters/preview.js +65 -22
  35. package/src/formatters/svelte-theme.js +40 -0
  36. package/src/formatters/tailwind.js +57 -4
  37. package/src/formatters/theme.js +134 -0
  38. package/src/formatters/vue-theme.js +44 -0
  39. package/src/formatters/wordpress.js +267 -0
  40. package/src/history.js +8 -1
  41. package/src/index.js +76 -20
  42. package/src/mcp/resources.js +64 -0
  43. package/src/mcp/server.js +110 -0
  44. package/src/mcp/tools.js +149 -0
  45. package/src/utils.js +68 -0
  46. package/tests/cli.test.js +84 -0
  47. package/tests/extractors.test.js +792 -0
  48. package/tests/formatters.test.js +709 -0
  49. package/tests/mcp.test.js +68 -0
  50. package/tests/utils.test.js +413 -0
  51. package/website/app/globals.css +11 -11
@@ -8,11 +8,20 @@ import ora from 'ora';
8
8
  import { extractDesignLanguage } from '../src/index.js';
9
9
  import { formatMarkdown } from '../src/formatters/markdown.js';
10
10
  import { formatTokens } from '../src/formatters/tokens.js';
11
+ import { formatDtcgTokens } from '../src/formatters/dtcg-tokens.js';
11
12
  import { formatTailwind } from '../src/formatters/tailwind.js';
12
13
  import { formatCssVars } from '../src/formatters/css-vars.js';
13
14
  import { formatPreview } from '../src/formatters/preview.js';
14
15
  import { formatFigma } from '../src/formatters/figma.js';
15
16
  import { formatReactTheme, formatShadcnTheme } from '../src/formatters/theme.js';
17
+ import { formatWordPress, formatWordPressTheme } from '../src/formatters/wordpress.js';
18
+ import { formatIosSwiftUI } from '../src/formatters/ios-swiftui.js';
19
+ import { formatAndroidCompose } from '../src/formatters/android-compose.js';
20
+ import { formatFlutterDart } from '../src/formatters/flutter-dart.js';
21
+ import { formatVueTheme } from '../src/formatters/vue-theme.js';
22
+ import { formatSvelteTheme } from '../src/formatters/svelte-theme.js';
23
+ import { formatAgentRules } from '../src/formatters/agent-rules.js';
24
+ import { loadConfig, mergeConfig } from '../src/config.js';
16
25
  import { diffDesigns, formatDiffMarkdown, formatDiffHtml } from '../src/diff.js';
17
26
  import { saveSnapshot, getHistory, formatHistoryMarkdown } from '../src/history.js';
18
27
  import { captureResponsive } from '../src/extractors/responsive.js';
@@ -25,12 +34,20 @@ import { diffDarkMode } from '../src/darkdiff.js';
25
34
  import { applyDesign } from '../src/apply.js';
26
35
  import { nameFromUrl } from '../src/utils.js';
27
36
 
37
+ function validateUrl(url) {
38
+ try { new URL(url); } catch {
39
+ console.error(chalk.red(`\n Invalid URL: ${url}\n`));
40
+ console.error(chalk.gray(' Example: designlang https://example.com\n'));
41
+ process.exit(1);
42
+ }
43
+ }
44
+
28
45
  const program = new Command();
29
46
 
30
47
  program
31
48
  .name('designlang')
32
49
  .description('Extract the complete design language from any website')
33
- .version('5.0.0');
50
+ .version('6.0.0');
34
51
 
35
52
  // ── Main command: extract ──────────────────────────────────────
36
53
  program
@@ -43,60 +60,100 @@ program
43
60
  .option('--dark', 'also extract dark mode styles')
44
61
  .option('--depth <n>', 'number of internal pages to also crawl', parseInt, 0)
45
62
  .option('--screenshots', 'capture component screenshots')
46
- .option('--framework <type>', 'generate framework theme (react, shadcn)')
63
+ .option('--framework <type>', 'generate framework theme (react, shadcn, vue, svelte)')
47
64
  .option('--responsive', 'capture design at multiple breakpoints')
48
65
  .option('--interactions', 'capture hover/focus/active states')
49
66
  .option('--full', 'enable all extra captures (screenshots, responsive, interactions)')
50
67
  .option('--cookie <cookies...>', 'cookies for authenticated pages (name=value)')
51
68
  .option('--header <headers...>', 'custom headers (name:value)')
69
+ .option('--ignore <selectors...>', 'CSS selectors to remove before extraction')
70
+ .option('--tokens-legacy', 'Emit pre-v7 flat token JSON (backward compat)')
71
+ .option('--platforms <csv>', 'Additional platforms: web,ios,android,flutter,wordpress,all (web is always emitted)', 'web')
72
+ .option('--emit-agent-rules', 'Emit Cursor/Claude Code/generic agent rules')
73
+ .option('--json', 'output raw JSON to stdout (for CI/CD)')
74
+ .option('--json-pretty', 'output formatted JSON to stdout')
52
75
  .option('--no-history', 'skip saving to history')
53
76
  .option('--verbose', 'show detailed progress')
77
+ .option('-q, --quiet', 'suppress output except file paths')
54
78
  .action(async (url, opts) => {
55
79
  if (!url.startsWith('http')) url = `https://${url}`;
80
+
81
+ // Load config file and merge with CLI opts
82
+ const config = loadConfig();
83
+ const merged = mergeConfig(opts, config);
84
+
85
+ // Validate URL
86
+ validateUrl(url);
87
+
88
+ // Validate numeric options
89
+ if (isNaN(merged.width) || merged.width < 100) {
90
+ console.error(chalk.red('\n Invalid width. Must be >= 100\n'));
91
+ process.exit(1);
92
+ }
93
+ if (merged.depth < 0 || merged.depth > 50) {
94
+ console.error(chalk.red('\n Invalid depth. Must be 0-50\n'));
95
+ process.exit(1);
96
+ }
97
+
56
98
  const prefix = opts.name || nameFromUrl(url);
57
- const outDir = resolve(opts.out);
99
+ const outDir = resolve(merged.out);
58
100
 
59
- console.log('');
60
- console.log(chalk.bold(' designlang'));
61
- console.log(chalk.gray(` ${url}${opts.depth > 0 ? ` (+ ${opts.depth} pages)` : ''}`));
62
- console.log('');
101
+ const jsonMode = opts.json || opts.jsonPretty;
102
+ const startTime = Date.now();
103
+
104
+ if (!jsonMode && !opts.quiet) {
105
+ console.log('');
106
+ console.log(chalk.bold(' designlang'));
107
+ console.log(chalk.gray(` ${url}${merged.depth > 0 ? ` (+ ${merged.depth} pages)` : ''}`));
108
+ console.log('');
109
+ }
63
110
 
64
- const spinner = ora('Launching browser...').start();
111
+ const spinner = jsonMode || opts.quiet
112
+ ? { start() { return this; }, set text(v) {}, succeed() {}, fail() {}, info() {}, stop() {} }
113
+ : ora('Launching browser...').start();
65
114
 
66
115
  try {
67
- spinner.text = `Crawling${opts.depth > 0 ? ` (depth: ${opts.depth})` : ''}...`;
116
+ spinner.text = `Crawling${merged.depth > 0 ? ` (depth: ${merged.depth})` : ''}...`;
68
117
  // Parse auth options
69
- const cookies = opts.cookie || [];
118
+ const cookies = merged.cookie || [];
70
119
  const headers = {};
71
- if (opts.header) {
72
- for (const h of opts.header) {
120
+ if (merged.header) {
121
+ for (const h of merged.header) {
73
122
  const [name, ...rest] = h.split(':');
74
123
  if (name && rest.length) headers[name.trim()] = rest.join(':').trim();
75
124
  }
76
125
  }
77
126
 
78
127
  const design = await extractDesignLanguage(url, {
79
- width: opts.width,
80
- height: parseInt(opts.height) || 800,
81
- wait: opts.wait,
82
- dark: opts.dark,
83
- depth: opts.depth,
84
- screenshots: opts.screenshots || opts.full,
128
+ width: merged.width,
129
+ height: parseInt(merged.height) || 800,
130
+ wait: merged.wait,
131
+ dark: merged.dark,
132
+ depth: merged.depth,
133
+ screenshots: merged.screenshots || merged.full,
85
134
  outDir,
135
+ ignore: merged.ignore,
86
136
  cookies: cookies.length > 0 ? cookies : undefined,
87
137
  headers: Object.keys(headers).length > 0 ? headers : undefined,
88
138
  });
89
139
 
90
140
  // Responsive capture
91
- if (opts.responsive || opts.full) {
141
+ if (merged.responsive || merged.full) {
92
142
  spinner.text = 'Capturing responsive breakpoints...';
93
- design.responsive = await captureResponsive(url, { wait: opts.wait });
143
+ design.responsive = await captureResponsive(url, { wait: merged.wait });
94
144
  }
95
145
 
96
146
  // Interaction state capture
97
- if (opts.interactions || opts.full) {
147
+ if (merged.interactions || merged.full) {
98
148
  spinner.text = 'Capturing interaction states...';
99
- design.interactions = await captureInteractions(url, { width: opts.width, height: parseInt(opts.height) || 800, wait: opts.wait });
149
+ design.interactions = await captureInteractions(url, { width: merged.width, height: parseInt(merged.height) || 800, wait: merged.wait });
150
+ }
151
+
152
+ // JSON mode: output and exit
153
+ if (jsonMode) {
154
+ const output = opts.jsonPretty ? JSON.stringify(design, null, 2) : JSON.stringify(design);
155
+ process.stdout.write(output + '\n');
156
+ process.exit(0);
100
157
  }
101
158
 
102
159
  spinner.text = 'Generating outputs...';
@@ -104,7 +161,7 @@ program
104
161
 
105
162
  const files = [
106
163
  { name: `${prefix}-design-language.md`, content: formatMarkdown(design), label: 'Markdown (AI-optimized)' },
107
- { name: `${prefix}-design-tokens.json`, content: formatTokens(design), label: 'Design Tokens (W3C)' },
164
+ { name: `${prefix}-design-tokens.json`, content: merged.tokensLegacy ? formatTokens(design) : JSON.stringify(formatDtcgTokens(design), null, 2), label: merged.tokensLegacy ? 'Design Tokens (legacy)' : 'Design Tokens (DTCG v1)' },
108
165
  { name: `${prefix}-tailwind.config.js`, content: formatTailwind(design), label: 'Tailwind Config' },
109
166
  { name: `${prefix}-variables.css`, content: formatCssVars(design), label: 'CSS Variables' },
110
167
  { name: `${prefix}-preview.html`, content: formatPreview(design), label: 'Visual Preview' },
@@ -112,101 +169,206 @@ program
112
169
  ];
113
170
 
114
171
  // Framework-specific themes
115
- if (opts.framework === 'react') {
172
+ if (merged.framework === 'react') {
116
173
  files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
117
- } else if (opts.framework === 'shadcn') {
174
+ } else if (merged.framework === 'shadcn') {
118
175
  files.push({ name: `${prefix}-shadcn-theme.css`, content: formatShadcnTheme(design), label: 'shadcn/ui Theme' });
176
+ } else if (merged.framework === 'vue') {
177
+ files.push({ name: `${prefix}-vue-theme.js`, content: formatVueTheme(design), label: 'Vue/Vuetify Theme' });
178
+ } else if (merged.framework === 'svelte') {
179
+ files.push({ name: `${prefix}-svelte-theme.css`, content: formatSvelteTheme(design), label: 'Svelte Theme' });
119
180
  } else {
120
181
  // Generate both by default
121
182
  files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
122
183
  files.push({ name: `${prefix}-shadcn-theme.css`, content: formatShadcnTheme(design), label: 'shadcn/ui Theme' });
123
184
  }
124
185
 
186
+ // WordPress theme (always generated)
187
+ files.push({ name: `${prefix}-wordpress-theme.json`, content: formatWordPress(design), label: 'WordPress Theme' });
188
+
189
+ // MCP companion — the subset of `design` the MCP server serves when a
190
+ // user runs `designlang mcp --output-dir <dir>` later.
191
+ const mcpPayload = {
192
+ colors: { all: design.colors?.all || [] },
193
+ regions: design.regions || [],
194
+ componentClusters: design.componentClusters || [],
195
+ accessibility: { remediation: design.accessibility?.remediation || [] },
196
+ cssHealth: design.cssHealth || null,
197
+ };
198
+ files.push({ name: `${prefix}-mcp.json`, content: JSON.stringify(mcpPayload, null, 2), label: 'MCP companion' });
199
+
125
200
  for (const file of files) {
126
201
  writeFileSync(join(outDir, file.name), file.content, 'utf-8');
127
202
  }
128
203
 
204
+ // Multi-platform emission (v7.0). web is already emitted above.
205
+ const platforms = merged.platforms || ['web'];
206
+ const dtcgTokens = formatDtcgTokens(design);
207
+ const platformFiles = [];
208
+ if (platforms.includes('ios')) {
209
+ const dir = join(outDir, 'ios');
210
+ mkdirSync(dir, { recursive: true });
211
+ const path = join(dir, 'DesignTokens.swift');
212
+ writeFileSync(path, formatIosSwiftUI(dtcgTokens), 'utf-8');
213
+ platformFiles.push({ path, label: 'iOS SwiftUI' });
214
+ }
215
+ if (platforms.includes('android')) {
216
+ const dir = join(outDir, 'android');
217
+ mkdirSync(dir, { recursive: true });
218
+ const out = formatAndroidCompose(dtcgTokens);
219
+ for (const name of Object.keys(out)) {
220
+ const p = join(dir, name);
221
+ writeFileSync(p, out[name], 'utf-8');
222
+ platformFiles.push({ path: p, label: `Android (${name})` });
223
+ }
224
+ }
225
+ if (platforms.includes('flutter')) {
226
+ const dir = join(outDir, 'flutter');
227
+ mkdirSync(dir, { recursive: true });
228
+ const p = join(dir, 'design_tokens.dart');
229
+ writeFileSync(p, formatFlutterDart(dtcgTokens), 'utf-8');
230
+ platformFiles.push({ path: p, label: 'Flutter Dart' });
231
+ }
232
+ if (platforms.includes('wordpress')) {
233
+ const dir = join(outDir, 'wordpress-theme');
234
+ mkdirSync(dir, { recursive: true });
235
+ const out = formatWordPressTheme(dtcgTokens, design);
236
+ for (const name of Object.keys(out)) {
237
+ const p = join(dir, name);
238
+ mkdirSync(join(p, '..'), { recursive: true });
239
+ writeFileSync(p, out[name], 'utf-8');
240
+ platformFiles.push({ path: p, label: `WordPress (${name})` });
241
+ }
242
+ }
243
+
244
+ // Agent rules (opt-in, also enabled by --full)
245
+ if (merged.emitAgentRules || merged.full) {
246
+ const agentFiles = formatAgentRules({ design, tokens: dtcgTokens, url });
247
+ for (const rel of Object.keys(agentFiles)) {
248
+ const p = join(outDir, rel);
249
+ mkdirSync(join(p, '..'), { recursive: true });
250
+ writeFileSync(p, agentFiles[rel], 'utf-8');
251
+ platformFiles.push({ path: p, label: `Agent rules (${rel})` });
252
+ }
253
+ }
254
+
129
255
  // Save to history
130
256
  if (opts.history !== false) {
131
257
  const histInfo = saveSnapshot(design);
132
258
  if (opts.verbose) spinner.info(`Snapshot #${histInfo.snapshotCount} saved for ${histInfo.hostname}`);
133
259
  }
134
260
 
261
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
262
+
135
263
  spinner.succeed('Extraction complete!');
136
- console.log('');
137
- console.log(chalk.bold(' Output files:'));
138
- for (const file of files) {
139
- const size = Buffer.byteLength(file.content);
140
- const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
141
- console.log(` ${chalk.green('✓')} ${chalk.cyan(file.name)} ${chalk.gray(`(${sizeStr})`)} — ${file.label}`);
142
- }
143
- if (opts.screenshots && design.componentScreenshots && Object.keys(design.componentScreenshots).length > 0) {
144
- for (const [, info] of Object.entries(design.componentScreenshots)) {
145
- console.log(` ${chalk.green('✓')} ${chalk.cyan(info.path)} — ${info.label} screenshot`);
264
+
265
+ if (opts.quiet) {
266
+ // Quiet mode: only show file paths
267
+ for (const file of files) {
268
+ console.log(join(outDir, file.name));
146
269
  }
147
- }
148
- console.log('');
149
- console.log(chalk.gray(` Saved to ${outDir}`));
270
+ for (const pf of platformFiles) {
271
+ console.log(pf.path);
272
+ }
273
+ } else {
274
+ console.log('');
275
+ console.log(chalk.bold(' Output files:'));
276
+ for (const file of files) {
277
+ const size = Buffer.byteLength(file.content);
278
+ const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
279
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(file.name)} ${chalk.gray(`(${sizeStr})`)} — ${file.label}`);
280
+ }
281
+ for (const pf of platformFiles) {
282
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(pf.path)} — ${pf.label}`);
283
+ }
284
+ if (opts.screenshots && design.componentScreenshots && Object.keys(design.componentScreenshots).length > 0) {
285
+ for (const [, info] of Object.entries(design.componentScreenshots)) {
286
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(info.path)} — ${info.label} screenshot`);
287
+ }
288
+ }
289
+ console.log('');
290
+ console.log(chalk.gray(` Saved to ${outDir}`));
150
291
 
151
- // Summary
152
- console.log('');
153
- console.log(chalk.bold(' Summary:'));
154
- if (design.meta.pagesAnalyzed > 1) {
155
- console.log(` ${chalk.gray('Pages:')} ${design.meta.pagesAnalyzed} pages analyzed`);
156
- }
157
- console.log(` ${chalk.gray('Colors:')} ${design.colors.all.length} unique colors`);
158
- console.log(` ${chalk.gray('Fonts:')} ${design.typography.families.map(f => f.name).join(', ') || 'none detected'}`);
159
- console.log(` ${chalk.gray('Spacing:')} ${design.spacing.scale.length} values${design.spacing.base ? ` (base: ${design.spacing.base}px)` : ''}`);
160
- console.log(` ${chalk.gray('Shadows:')} ${design.shadows.values.length} unique shadows`);
161
- console.log(` ${chalk.gray('Radii:')} ${design.borders.radii.length} unique values`);
162
- console.log(` ${chalk.gray('Breakpoints:')} ${design.breakpoints.length} breakpoints`);
163
- console.log(` ${chalk.gray('Components:')} ${Object.keys(design.components).length} patterns detected`);
164
- console.log(` ${chalk.gray('CSS Vars:')} ${Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0)} custom properties`);
165
- if (design.layout) {
166
- console.log(` ${chalk.gray('Layout:')} ${design.layout.gridCount} grids, ${design.layout.flexCount} flex containers`);
167
- }
168
- if (design.responsive) {
169
- console.log(` ${chalk.gray('Responsive:')} ${design.responsive.viewports.length} viewports, ${design.responsive.changes.length} breakpoint changes`);
170
- }
171
- if (design.interactions) {
172
- const ic = design.interactions;
173
- const total = ic.buttons.length + ic.links.length + ic.inputs.length;
174
- console.log(` ${chalk.gray('Interactions:')} ${total} state changes captured`);
175
- }
176
- if (design.score) {
177
- const s = design.score;
178
- const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
179
- console.log(` ${chalk.gray('Design Score:')} ${gradeColor(`${s.overall}/100 (${s.grade})`)}${s.issues.length > 0 ? ` — ${s.issues.length} issues` : ''}`);
180
- }
292
+ // Summary
293
+ console.log('');
294
+ console.log(chalk.bold(' Summary:'));
295
+ if (design.meta.pagesAnalyzed > 1) {
296
+ console.log(` ${chalk.gray('Pages:')} ${design.meta.pagesAnalyzed} pages analyzed`);
297
+ }
298
+ console.log(` ${chalk.gray('Colors:')} ${design.colors.all.length} unique colors`);
299
+ console.log(` ${chalk.gray('Fonts:')} ${design.typography.families.map(f => f.name).join(', ') || 'none detected'}`);
300
+ console.log(` ${chalk.gray('Spacing:')} ${design.spacing.scale.length} values${design.spacing.base ? ` (base: ${design.spacing.base}px)` : ''}`);
301
+ console.log(` ${chalk.gray('Shadows:')} ${design.shadows.values.length} unique shadows`);
302
+ console.log(` ${chalk.gray('Radii:')} ${design.borders.radii.length} unique values`);
303
+ console.log(` ${chalk.gray('Breakpoints:')} ${design.breakpoints.length} breakpoints`);
304
+ console.log(` ${chalk.gray('Components:')} ${Object.keys(design.components).length} patterns detected`);
305
+ console.log(` ${chalk.gray('CSS Vars:')} ${Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0)} custom properties`);
306
+ if (design.layout) {
307
+ console.log(` ${chalk.gray('Layout:')} ${design.layout.gridCount} grids, ${design.layout.flexCount} flex containers`);
308
+ }
309
+ if (design.responsive) {
310
+ console.log(` ${chalk.gray('Responsive:')} ${design.responsive.viewports.length} viewports, ${design.responsive.changes.length} breakpoint changes`);
311
+ }
312
+ if (design.interactions) {
313
+ const ic = design.interactions;
314
+ const total = ic.buttons.length + ic.links.length + ic.inputs.length;
315
+ console.log(` ${chalk.gray('Interactions:')} ${total} state changes captured`);
316
+ }
317
+ if (design.score) {
318
+ const s = design.score;
319
+ const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
320
+ console.log(` ${chalk.gray('Design Score:')} ${gradeColor(`${s.overall}/100 (${s.grade})`)}${s.issues.length > 0 ? ` — ${s.issues.length} issues` : ''}`);
321
+ }
181
322
 
182
- // New v5 extractors
183
- if (design.gradients && design.gradients.count > 0) {
184
- console.log(` ${chalk.gray('Gradients:')} ${design.gradients.count} unique gradients`);
185
- }
186
- if (design.zIndex && design.zIndex.allValues.length > 0) {
187
- console.log(` ${chalk.gray('Z-Index:')} ${design.zIndex.allValues.length} layers${design.zIndex.issues.length > 0 ? ` (${design.zIndex.issues.length} issues)` : ''}`);
188
- }
189
- if (design.icons && design.icons.count > 0) {
190
- console.log(` ${chalk.gray('Icons:')} ${design.icons.count} SVG icons (${design.icons.dominantStyle || 'mixed'})`);
191
- }
192
- if (design.fonts && design.fonts.fonts.length > 0) {
193
- const sources = design.fonts.fonts.map(f => f.source).filter((v, i, a) => a.indexOf(v) === i);
194
- console.log(` ${chalk.gray('Font Files:')} ${design.fonts.fonts.length} fonts (${sources.join(', ')})`);
195
- }
196
- if (design.images && design.images.patterns.length > 0) {
197
- const total = design.images.patterns.reduce((s, p) => s + p.count, 0);
198
- console.log(` ${chalk.gray('Images:')} ${total} images, ${design.images.patterns.length} style patterns`);
199
- }
323
+ // Score change vs last snapshot
324
+ const history = getHistory(url);
325
+ if (history.length > 1 && design.score) {
326
+ const prev = history[history.length - 2];
327
+ if (prev.score !== undefined) {
328
+ const delta = design.score.overall - prev.score;
329
+ if (delta !== 0) {
330
+ const sign = delta > 0 ? '+' : '';
331
+ const color = delta > 0 ? chalk.green : chalk.red;
332
+ console.log(` ${chalk.gray('Score \u0394:')} ${color(`${sign}${delta} from last scan`)}`);
333
+ }
334
+ }
335
+ }
336
+
337
+ // New v5 extractors
338
+ if (design.gradients && design.gradients.count > 0) {
339
+ console.log(` ${chalk.gray('Gradients:')} ${design.gradients.count} unique gradients`);
340
+ }
341
+ if (design.zIndex && design.zIndex.allValues.length > 0) {
342
+ console.log(` ${chalk.gray('Z-Index:')} ${design.zIndex.allValues.length} layers${design.zIndex.issues.length > 0 ? ` (${design.zIndex.issues.length} issues)` : ''}`);
343
+ }
344
+ if (design.icons && design.icons.count > 0) {
345
+ console.log(` ${chalk.gray('Icons:')} ${design.icons.count} SVG icons (${design.icons.dominantStyle || 'mixed'})`);
346
+ }
347
+ if (design.fonts && design.fonts.fonts.length > 0) {
348
+ const sources = design.fonts.fonts.map(f => f.source).filter((v, i, a) => a.indexOf(v) === i);
349
+ console.log(` ${chalk.gray('Font Files:')} ${design.fonts.fonts.length} fonts (${sources.join(', ')})`);
350
+ }
351
+ if (design.images && design.images.patterns.length > 0) {
352
+ const total = design.images.patterns.reduce((s, p) => s + p.count, 0);
353
+ console.log(` ${chalk.gray('Images:')} ${total} images, ${design.images.patterns.length} style patterns`);
354
+ }
200
355
 
201
- // Accessibility summary
202
- if (design.accessibility) {
203
- const a = design.accessibility;
204
- const scoreColor = a.score >= 80 ? chalk.green : a.score >= 50 ? chalk.yellow : chalk.red;
205
- console.log(` ${chalk.gray('A11y:')} ${scoreColor(`${a.score}% WCAG score`)} (${a.failCount} failing pairs)`);
356
+ // Accessibility summary
357
+ if (design.accessibility) {
358
+ const a = design.accessibility;
359
+ const scoreColor = a.score >= 80 ? chalk.green : a.score >= 50 ? chalk.yellow : chalk.red;
360
+ console.log(` ${chalk.gray('A11y:')} ${scoreColor(`${a.score}% WCAG score`)} (${a.failCount} failing pairs)`);
361
+ }
362
+
363
+ console.log(chalk.gray(` Completed in ${duration}s`));
364
+ console.log('');
206
365
  }
207
- console.log('');
208
366
 
209
367
  } catch (err) {
368
+ if (jsonMode) {
369
+ process.stderr.write(JSON.stringify({ error: err.message }) + '\n');
370
+ process.exit(1);
371
+ }
210
372
  spinner.fail('Extraction failed');
211
373
  if (err.message.includes('playwright')) {
212
374
  console.error(chalk.red('\n Playwright is not installed.'));
@@ -227,6 +389,8 @@ program
227
389
  .action(async (urlA, urlB, opts) => {
228
390
  if (!urlA.startsWith('http')) urlA = `https://${urlA}`;
229
391
  if (!urlB.startsWith('http')) urlB = `https://${urlB}`;
392
+ validateUrl(urlA);
393
+ validateUrl(urlB);
230
394
 
231
395
  console.log('');
232
396
  console.log(chalk.bold(' designlang diff'));
@@ -283,6 +447,7 @@ program
283
447
  .description('View design history for a website')
284
448
  .action(async (url) => {
285
449
  if (!url.startsWith('http')) url = `https://${url}`;
450
+ validateUrl(url);
286
451
  const history = getHistory(url);
287
452
  console.log('');
288
453
  console.log(formatHistoryMarkdown(url, history));
@@ -341,6 +506,7 @@ program
341
506
  .option('-o, --out <dir>', 'directory with token files to update', '.')
342
507
  .action(async (url, opts) => {
343
508
  if (!url.startsWith('http')) url = `https://${url}`;
509
+ validateUrl(url);
344
510
 
345
511
  console.log('');
346
512
  console.log(chalk.bold(' designlang sync'));
@@ -387,6 +553,7 @@ program
387
553
  .option('-o, --out <dir>', 'output directory', './cloned-design')
388
554
  .action(async (url, opts) => {
389
555
  if (!url.startsWith('http')) url = `https://${url}`;
556
+ validateUrl(url);
390
557
 
391
558
  console.log('');
392
559
  console.log(chalk.bold(' designlang clone'));
@@ -425,6 +592,7 @@ program
425
592
  .option('--interval <minutes>', 'check interval in minutes', parseInt, 60)
426
593
  .action(async (url, opts) => {
427
594
  if (!url.startsWith('http')) url = `https://${url}`;
595
+ validateUrl(url);
428
596
  const intervalMs = (opts.interval || 60) * 60 * 1000;
429
597
 
430
598
  console.log('');
@@ -463,6 +631,7 @@ program
463
631
  .description('Score a website\'s design system quality')
464
632
  .action(async (url) => {
465
633
  if (!url.startsWith('http')) url = `https://${url}`;
634
+ validateUrl(url);
466
635
 
467
636
  const spinner = ora('Analyzing design...').start();
468
637
 
@@ -531,6 +700,7 @@ program
531
700
  .option('--header <headers...>', 'custom headers')
532
701
  .action(async (url, opts) => {
533
702
  if (!url.startsWith('http')) url = `https://${url}`;
703
+ validateUrl(url);
534
704
 
535
705
  console.log('');
536
706
  console.log(chalk.bold(' designlang apply'));
@@ -561,4 +731,44 @@ program
561
731
  }
562
732
  });
563
733
 
734
+ // ── Export command ─────────────────────────────────────────
735
+ program
736
+ .command('export <url>')
737
+ .description('Export raw design data in various formats')
738
+ .option('-f, --format <type>', 'output format (json, csv)', 'json')
739
+ .option('--pretty', 'pretty-print output')
740
+ .action(async (url, opts) => {
741
+ if (!url.startsWith('http')) url = `https://${url}`;
742
+ validateUrl(url);
743
+
744
+ try {
745
+ const design = await extractDesignLanguage(url);
746
+
747
+ if (opts.format === 'csv') {
748
+ // Export colors as CSV
749
+ const rows = ['hex,rgb_r,rgb_g,rgb_b,hsl_h,hsl_s,hsl_l,count,contexts'];
750
+ for (const c of design.colors.all) {
751
+ rows.push(`${c.hex},${c.rgb.r},${c.rgb.g},${c.rgb.b},${c.hsl.h},${c.hsl.s},${c.hsl.l},${c.count},"${c.contexts.join(';')}"`);
752
+ }
753
+ process.stdout.write(rows.join('\n') + '\n');
754
+ } else {
755
+ const output = opts.pretty ? JSON.stringify(design, null, 2) : JSON.stringify(design);
756
+ process.stdout.write(output + '\n');
757
+ }
758
+ } catch (err) {
759
+ process.stderr.write(`Error: ${err.message}\n`);
760
+ process.exit(1);
761
+ }
762
+ });
763
+
764
+ // ── MCP server command ─────────────────────────────────────
765
+ program
766
+ .command('mcp')
767
+ .description('Launch designlang MCP server over stdio (exposes latest extraction as resources + tools)')
768
+ .option('--output-dir <path>', 'Source extraction directory', './design-extract-output')
769
+ .action(async (opts) => {
770
+ const { run } = await import('../src/mcp/server.js');
771
+ await run(opts);
772
+ });
773
+
564
774
  program.parse();