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.
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
- package/CHANGELOG.md +43 -0
- package/README.md +177 -6
- package/bin/design-extract.js +302 -92
- package/docs/superpowers/plans/2026-04-18-designlang-v7.md +1121 -0
- package/docs/superpowers/specs/2026-04-18-designlang-v7-design.md +150 -0
- package/package.json +13 -7
- package/src/config.js +59 -0
- package/src/crawler.js +297 -95
- package/src/extractors/a11y-remediation.js +47 -0
- package/src/extractors/animations.js +37 -5
- package/src/extractors/borders.js +40 -5
- package/src/extractors/component-clusters.js +39 -0
- package/src/extractors/components.js +77 -1
- package/src/extractors/css-health.js +151 -0
- package/src/extractors/gradients.js +25 -5
- package/src/extractors/scoring.js +20 -1
- package/src/extractors/semantic-regions.js +44 -0
- package/src/extractors/shadows.js +60 -17
- package/src/extractors/spacing.js +31 -2
- package/src/extractors/stack-fingerprint.js +88 -0
- package/src/extractors/variables.js +20 -1
- package/src/formatters/_token-ref.js +44 -0
- package/src/formatters/agent-rules.js +116 -0
- package/src/formatters/android-compose.js +164 -0
- package/src/formatters/dtcg-tokens.js +175 -0
- package/src/formatters/figma.js +66 -47
- package/src/formatters/flutter-dart.js +130 -0
- package/src/formatters/ios-swiftui.js +161 -0
- package/src/formatters/markdown.js +25 -0
- package/src/formatters/preview.js +65 -22
- package/src/formatters/svelte-theme.js +40 -0
- package/src/formatters/tailwind.js +57 -4
- package/src/formatters/theme.js +134 -0
- package/src/formatters/vue-theme.js +44 -0
- package/src/formatters/wordpress.js +267 -0
- package/src/history.js +8 -1
- package/src/index.js +76 -20
- package/src/mcp/resources.js +64 -0
- package/src/mcp/server.js +110 -0
- package/src/mcp/tools.js +149 -0
- package/src/utils.js +68 -0
- package/tests/cli.test.js +84 -0
- package/tests/extractors.test.js +792 -0
- package/tests/formatters.test.js +709 -0
- package/tests/mcp.test.js +68 -0
- package/tests/utils.test.js +413 -0
- package/website/app/globals.css +11 -11
package/bin/design-extract.js
CHANGED
|
@@ -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('
|
|
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(
|
|
99
|
+
const outDir = resolve(merged.out);
|
|
58
100
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 =
|
|
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${
|
|
116
|
+
spinner.text = `Crawling${merged.depth > 0 ? ` (depth: ${merged.depth})` : ''}...`;
|
|
68
117
|
// Parse auth options
|
|
69
|
-
const cookies =
|
|
118
|
+
const cookies = merged.cookie || [];
|
|
70
119
|
const headers = {};
|
|
71
|
-
if (
|
|
72
|
-
for (const h of
|
|
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:
|
|
80
|
-
height: parseInt(
|
|
81
|
-
wait:
|
|
82
|
-
dark:
|
|
83
|
-
depth:
|
|
84
|
-
screenshots:
|
|
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 (
|
|
141
|
+
if (merged.responsive || merged.full) {
|
|
92
142
|
spinner.text = 'Capturing responsive breakpoints...';
|
|
93
|
-
design.responsive = await captureResponsive(url, { wait:
|
|
143
|
+
design.responsive = await captureResponsive(url, { wait: merged.wait });
|
|
94
144
|
}
|
|
95
145
|
|
|
96
146
|
// Interaction state capture
|
|
97
|
-
if (
|
|
147
|
+
if (merged.interactions || merged.full) {
|
|
98
148
|
spinner.text = 'Capturing interaction states...';
|
|
99
|
-
design.interactions = await captureInteractions(url, { width:
|
|
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 (
|
|
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 (
|
|
172
|
+
if (merged.framework === 'react') {
|
|
116
173
|
files.push({ name: `${prefix}-theme.js`, content: formatReactTheme(design), label: 'React Theme' });
|
|
117
|
-
} else if (
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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();
|