designagent-cli 0.1.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/README.md +52 -0
- package/dist/index.js +3108 -0
- package/dist/skills/a11y-check/SKILL.md +43 -0
- package/dist/skills/component-audit/SKILL.md +35 -0
- package/dist/skills/copywriting/SKILL.md +36 -0
- package/dist/skills/design-review/SKILL.md +37 -0
- package/dist/skills/design-to-code/SKILL.md +39 -0
- package/dist/skills/redlines/SKILL.md +37 -0
- package/dist/skills/token-namer/SKILL.md +33 -0
- package/dist/skills/token-sync/SKILL.md +32 -0
- package/dist/templates/claude-md/base.md +95 -0
- package/dist/templates/claude-md/compose.md +21 -0
- package/dist/templates/claude-md/monorepo.md +9 -0
- package/dist/templates/claude-md/react.md +25 -0
- package/dist/templates/claude-md/swiftui.md +22 -0
- package/dist/templates/design-md/custom.md +138 -0
- package/dist/templates/design-md/shadcn.md +142 -0
- package/dist/templates/design-md/tailwind.md +149 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import pc4 from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/commands/init.ts
|
|
8
|
+
import { basename as basename5 } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
cancel,
|
|
11
|
+
confirm,
|
|
12
|
+
group,
|
|
13
|
+
intro,
|
|
14
|
+
isCancel,
|
|
15
|
+
multiselect,
|
|
16
|
+
note,
|
|
17
|
+
outro,
|
|
18
|
+
select,
|
|
19
|
+
spinner,
|
|
20
|
+
text
|
|
21
|
+
} from "@clack/prompts";
|
|
22
|
+
import pc from "picocolors";
|
|
23
|
+
|
|
24
|
+
// ../scanner/dist/readers/css-vars.js
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
26
|
+
import { relative } from "node:path";
|
|
27
|
+
import postcss from "postcss";
|
|
28
|
+
|
|
29
|
+
// ../scanner/dist/util/walk.js
|
|
30
|
+
import { readdirSync } from "node:fs";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
var IGNORE_DIRS = new Set([
|
|
33
|
+
"node_modules",
|
|
34
|
+
".git",
|
|
35
|
+
"dist",
|
|
36
|
+
"build",
|
|
37
|
+
"out",
|
|
38
|
+
".next",
|
|
39
|
+
".turbo",
|
|
40
|
+
".cache",
|
|
41
|
+
"coverage",
|
|
42
|
+
".designagent",
|
|
43
|
+
"vendor",
|
|
44
|
+
"Pods"
|
|
45
|
+
]);
|
|
46
|
+
function walk(root, exts, maxFiles = 8000) {
|
|
47
|
+
const extSet = new Set(exts.map((e) => e.toLowerCase()));
|
|
48
|
+
const out = [];
|
|
49
|
+
const stack = [root];
|
|
50
|
+
while (stack.length > 0 && out.length < maxFiles) {
|
|
51
|
+
const dir = stack.pop();
|
|
52
|
+
if (dir === undefined)
|
|
53
|
+
break;
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
63
|
+
continue;
|
|
64
|
+
stack.push(join(dir, entry.name));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const dot = entry.name.lastIndexOf(".");
|
|
68
|
+
if (dot <= 0)
|
|
69
|
+
continue;
|
|
70
|
+
const ext = entry.name.slice(dot).toLowerCase();
|
|
71
|
+
if (extSet.has(ext))
|
|
72
|
+
out.push(join(dir, entry.name));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ../scanner/dist/util/color.js
|
|
79
|
+
function normalizeHex(input) {
|
|
80
|
+
const m = input.trim().toLowerCase().match(/^#?([0-9a-f]{3,8})$/);
|
|
81
|
+
if (!m)
|
|
82
|
+
return null;
|
|
83
|
+
let h = m[1];
|
|
84
|
+
if (h.length === 3) {
|
|
85
|
+
h = h.split("").map((c) => c + c).join("");
|
|
86
|
+
} else if (h.length === 4) {
|
|
87
|
+
h = h.slice(0, 3).split("").map((c) => c + c).join("");
|
|
88
|
+
} else if (h.length === 8) {
|
|
89
|
+
h = h.slice(0, 6);
|
|
90
|
+
} else if (h.length !== 6) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return "#" + h;
|
|
94
|
+
}
|
|
95
|
+
function hexToRgb(hex) {
|
|
96
|
+
const h = hex.slice(1);
|
|
97
|
+
return {
|
|
98
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
99
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
100
|
+
b: parseInt(h.slice(4, 6), 16)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function srgbToLinear(channel) {
|
|
104
|
+
const c = channel / 255;
|
|
105
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
106
|
+
}
|
|
107
|
+
function rgbToLab({ r, g, b }) {
|
|
108
|
+
const R = srgbToLinear(r);
|
|
109
|
+
const G = srgbToLinear(g);
|
|
110
|
+
const B = srgbToLinear(b);
|
|
111
|
+
let x = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
|
|
112
|
+
const y = R * 0.2126 + G * 0.7152 + B * 0.0722;
|
|
113
|
+
let z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 1.08883;
|
|
114
|
+
const f = (t) => t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
115
|
+
const fx = f(x);
|
|
116
|
+
const fy = f(y);
|
|
117
|
+
const fz = f(z);
|
|
118
|
+
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
119
|
+
}
|
|
120
|
+
function deltaE(a, b) {
|
|
121
|
+
return Math.sqrt((a.L - b.L) ** 2 + (a.a - b.a) ** 2 + (a.b - b.b) ** 2);
|
|
122
|
+
}
|
|
123
|
+
function relativeLuminance({ r, g, b }) {
|
|
124
|
+
return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
|
|
125
|
+
}
|
|
126
|
+
function contrastRatio(a, b) {
|
|
127
|
+
const la = relativeLuminance(a);
|
|
128
|
+
const lb = relativeLuminance(b);
|
|
129
|
+
const hi = Math.max(la, lb);
|
|
130
|
+
const lo = Math.min(la, lb);
|
|
131
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
132
|
+
}
|
|
133
|
+
function saturation({ r, g, b }) {
|
|
134
|
+
const rn = r / 255;
|
|
135
|
+
const gn = g / 255;
|
|
136
|
+
const bn = b / 255;
|
|
137
|
+
const max = Math.max(rn, gn, bn);
|
|
138
|
+
const min = Math.min(rn, gn, bn);
|
|
139
|
+
if (max === min)
|
|
140
|
+
return 0;
|
|
141
|
+
const l = (max + min) / 2;
|
|
142
|
+
const d = max - min;
|
|
143
|
+
return l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
144
|
+
}
|
|
145
|
+
function readableOn(bg) {
|
|
146
|
+
const white = contrastRatio(bg, { r: 255, g: 255, b: 255 });
|
|
147
|
+
const black = contrastRatio(bg, { r: 17, g: 17, b: 17 });
|
|
148
|
+
return white >= black ? "#ffffff" : "#111111";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ../scanner/dist/readers/css-vars.js
|
|
152
|
+
var HEX_RE = /#[0-9a-fA-F]{3,8}\b/;
|
|
153
|
+
var PX_RE = /^(-?\d*\.?\d+)px$/;
|
|
154
|
+
var REM_RE = /^(-?\d*\.?\d+)rem$/;
|
|
155
|
+
function toPx(value) {
|
|
156
|
+
const v = value.trim();
|
|
157
|
+
const px = v.match(PX_RE);
|
|
158
|
+
if (px)
|
|
159
|
+
return { px: parseFloat(px[1]), raw: v };
|
|
160
|
+
const rem = v.match(REM_RE);
|
|
161
|
+
if (rem)
|
|
162
|
+
return { px: parseFloat(rem[1]) * 16, raw: v };
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function lineOf(decl) {
|
|
166
|
+
return decl.source?.start?.line ?? 0;
|
|
167
|
+
}
|
|
168
|
+
function readCssVars(root) {
|
|
169
|
+
const files = walk(root, [".css", ".pcss"]);
|
|
170
|
+
const colors = [];
|
|
171
|
+
const dimensions = [];
|
|
172
|
+
const typography = [];
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
let css;
|
|
175
|
+
try {
|
|
176
|
+
css = readFileSync(file, "utf8");
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let rootNode;
|
|
181
|
+
try {
|
|
182
|
+
rootNode = postcss.parse(css, { from: file });
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const rel = relative(root, file);
|
|
187
|
+
rootNode.walkDecls((decl) => {
|
|
188
|
+
const prop = decl.prop;
|
|
189
|
+
const value = decl.value;
|
|
190
|
+
const line = lineOf(decl);
|
|
191
|
+
const isVar = prop.startsWith("--");
|
|
192
|
+
const name = isVar ? prop.slice(2) : undefined;
|
|
193
|
+
const hexMatch = value.match(HEX_RE);
|
|
194
|
+
if (hexMatch) {
|
|
195
|
+
const hex = normalizeHex(hexMatch[0]);
|
|
196
|
+
if (hex) {
|
|
197
|
+
colors.push({
|
|
198
|
+
hex,
|
|
199
|
+
source: { file: rel, line },
|
|
200
|
+
origin: "css-var",
|
|
201
|
+
name
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (isVar) {
|
|
206
|
+
const dim = toPx(value);
|
|
207
|
+
if (dim && dim.px !== 0) {
|
|
208
|
+
const lname2 = (name ?? "").toLowerCase();
|
|
209
|
+
const category = lname2.includes("radius") || lname2.includes("rounded") ? "radius" : lname2.includes("spac") || lname2.includes("gap") || lname2.includes("size") ? "spacing" : "spacing";
|
|
210
|
+
dimensions.push({
|
|
211
|
+
px: dim.px,
|
|
212
|
+
raw: dim.raw,
|
|
213
|
+
source: { file: rel, line },
|
|
214
|
+
origin: "css-var",
|
|
215
|
+
category,
|
|
216
|
+
name
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const lname = (name ?? "").toLowerCase();
|
|
220
|
+
if (lname.startsWith("font") || lname.includes("text")) {
|
|
221
|
+
typography.push({
|
|
222
|
+
name: name ?? prop,
|
|
223
|
+
fontSize: PX_RE.test(value) || REM_RE.test(value) ? value : undefined,
|
|
224
|
+
fontFamily: lname.includes("family") ? value : undefined,
|
|
225
|
+
source: { file: rel, line }
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return { colors, dimensions, typography };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ../scanner/dist/readers/tailwind.js
|
|
235
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
236
|
+
import { join as join2, relative as relative2 } from "node:path";
|
|
237
|
+
var CONFIG_NAMES = [
|
|
238
|
+
"tailwind.config.js",
|
|
239
|
+
"tailwind.config.ts",
|
|
240
|
+
"tailwind.config.cjs",
|
|
241
|
+
"tailwind.config.mjs"
|
|
242
|
+
];
|
|
243
|
+
var PX_RE2 = /^(-?\d*\.?\d+)px$/;
|
|
244
|
+
var REM_RE2 = /^(-?\d*\.?\d+)rem$/;
|
|
245
|
+
function toPx2(value) {
|
|
246
|
+
const px = value.match(PX_RE2);
|
|
247
|
+
if (px)
|
|
248
|
+
return parseFloat(px[1]);
|
|
249
|
+
const rem = value.match(REM_RE2);
|
|
250
|
+
if (rem)
|
|
251
|
+
return parseFloat(rem[1]) * 16;
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function lineOfIndex(text, index) {
|
|
255
|
+
let line = 1;
|
|
256
|
+
for (let i = 0;i < index && i < text.length; i++) {
|
|
257
|
+
if (text[i] === `
|
|
258
|
+
`)
|
|
259
|
+
line++;
|
|
260
|
+
}
|
|
261
|
+
return line;
|
|
262
|
+
}
|
|
263
|
+
function findSectionRanges(text, sectionRe) {
|
|
264
|
+
const ranges = [];
|
|
265
|
+
let m;
|
|
266
|
+
sectionRe.lastIndex = 0;
|
|
267
|
+
while ((m = sectionRe.exec(text)) !== null) {
|
|
268
|
+
const open = text.indexOf("{", m.index);
|
|
269
|
+
if (open === -1)
|
|
270
|
+
continue;
|
|
271
|
+
let depth = 0;
|
|
272
|
+
let close = -1;
|
|
273
|
+
for (let i = open;i < text.length; i++) {
|
|
274
|
+
if (text[i] === "{")
|
|
275
|
+
depth++;
|
|
276
|
+
else if (text[i] === "}") {
|
|
277
|
+
depth--;
|
|
278
|
+
if (depth === 0) {
|
|
279
|
+
close = i;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (close !== -1)
|
|
285
|
+
ranges.push([open, close]);
|
|
286
|
+
}
|
|
287
|
+
return ranges;
|
|
288
|
+
}
|
|
289
|
+
function readTailwind(root) {
|
|
290
|
+
let configFile = null;
|
|
291
|
+
for (const name of CONFIG_NAMES) {
|
|
292
|
+
const candidate = join2(root, name);
|
|
293
|
+
if (existsSync(candidate)) {
|
|
294
|
+
configFile = candidate;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const empty = {
|
|
299
|
+
found: false,
|
|
300
|
+
configFile: null,
|
|
301
|
+
colors: [],
|
|
302
|
+
dimensions: [],
|
|
303
|
+
typography: []
|
|
304
|
+
};
|
|
305
|
+
if (!configFile)
|
|
306
|
+
return empty;
|
|
307
|
+
let text;
|
|
308
|
+
try {
|
|
309
|
+
text = readFileSync2(configFile, "utf8");
|
|
310
|
+
} catch {
|
|
311
|
+
return empty;
|
|
312
|
+
}
|
|
313
|
+
const rel = relative2(root, configFile);
|
|
314
|
+
const colors = [];
|
|
315
|
+
const dimensions = [];
|
|
316
|
+
const typography = [];
|
|
317
|
+
const hexRe = /#[0-9a-fA-F]{3,8}\b/g;
|
|
318
|
+
let m;
|
|
319
|
+
while ((m = hexRe.exec(text)) !== null) {
|
|
320
|
+
const hex = normalizeHex(m[0]);
|
|
321
|
+
if (hex) {
|
|
322
|
+
colors.push({
|
|
323
|
+
hex,
|
|
324
|
+
source: { file: rel, line: lineOfIndex(text, m.index) },
|
|
325
|
+
origin: "tailwind"
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const dimRe = /['"]?([\w-]+)['"]?\s*:\s*['"](-?\d*\.?\d+(?:px|rem))['"]/g;
|
|
330
|
+
const radiusSectionRe = /\b(borderRadius|radius|rounded)\s*:/gi;
|
|
331
|
+
const radiusRanges = findSectionRanges(text, radiusSectionRe);
|
|
332
|
+
while ((m = dimRe.exec(text)) !== null) {
|
|
333
|
+
const px = toPx2(m[2]);
|
|
334
|
+
if (px === null || px === 0)
|
|
335
|
+
continue;
|
|
336
|
+
const key = m[1].toLowerCase();
|
|
337
|
+
const inRadiusSection = radiusRanges.some(([start, end]) => m.index > start && m.index < end);
|
|
338
|
+
const category = key.includes("round") || key.includes("radius") || inRadiusSection ? "radius" : "spacing";
|
|
339
|
+
dimensions.push({
|
|
340
|
+
px,
|
|
341
|
+
raw: m[2],
|
|
342
|
+
source: { file: rel, line: lineOfIndex(text, m.index) },
|
|
343
|
+
origin: "tailwind",
|
|
344
|
+
category,
|
|
345
|
+
name: m[1]
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const line = (i) => lineOfIndex(text, i);
|
|
349
|
+
for (const [start, end] of findSectionRanges(text, /\bfontSize\s*:/gi)) {
|
|
350
|
+
const body = text.slice(start, end);
|
|
351
|
+
const re = /['"]?([\w-]+)['"]?\s*:\s*\[?\s*['"](-?\d*\.?\d+(?:px|rem|em))['"]/g;
|
|
352
|
+
let mm;
|
|
353
|
+
while ((mm = re.exec(body)) !== null) {
|
|
354
|
+
typography.push({
|
|
355
|
+
name: mm[1],
|
|
356
|
+
fontSize: mm[2],
|
|
357
|
+
origin: "tailwind",
|
|
358
|
+
source: { file: rel, line: line(start + mm.index) }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const [start, end] of findSectionRanges(text, /\bfontFamily\s*:/gi)) {
|
|
363
|
+
const body = text.slice(start, end);
|
|
364
|
+
const re = /['"]?([\w-]+)['"]?\s*:\s*\[?\s*['"]([^'"]+)['"]/g;
|
|
365
|
+
let mm;
|
|
366
|
+
while ((mm = re.exec(body)) !== null) {
|
|
367
|
+
typography.push({
|
|
368
|
+
name: `font-${mm[1]}`,
|
|
369
|
+
fontFamily: mm[2],
|
|
370
|
+
origin: "tailwind",
|
|
371
|
+
source: { file: rel, line: line(start + mm.index) }
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const [start, end] of findSectionRanges(text, /\bfontWeight\s*:/gi)) {
|
|
376
|
+
const body = text.slice(start, end);
|
|
377
|
+
const re = /['"]?([\w-]+)['"]?\s*:\s*['"]?(\d{3})['"]?/g;
|
|
378
|
+
let mm;
|
|
379
|
+
while ((mm = re.exec(body)) !== null) {
|
|
380
|
+
typography.push({
|
|
381
|
+
name: `weight-${mm[1]}`,
|
|
382
|
+
fontWeight: Number(mm[2]),
|
|
383
|
+
origin: "tailwind",
|
|
384
|
+
source: { file: rel, line: line(start + mm.index) }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { found: true, configFile, colors, dimensions, typography };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ../scanner/dist/readers/jsx-tsx.js
|
|
392
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
393
|
+
import { basename, relative as relative3 } from "node:path";
|
|
394
|
+
|
|
395
|
+
// ../scanner/dist/util/typography.js
|
|
396
|
+
var TAILWIND_FONT_SIZES = {
|
|
397
|
+
xs: "0.75rem",
|
|
398
|
+
sm: "0.875rem",
|
|
399
|
+
base: "1rem",
|
|
400
|
+
lg: "1.125rem",
|
|
401
|
+
xl: "1.25rem",
|
|
402
|
+
"2xl": "1.5rem",
|
|
403
|
+
"3xl": "1.875rem",
|
|
404
|
+
"4xl": "2.25rem",
|
|
405
|
+
"5xl": "3rem",
|
|
406
|
+
"6xl": "3.75rem",
|
|
407
|
+
"7xl": "4.5rem",
|
|
408
|
+
"8xl": "6rem",
|
|
409
|
+
"9xl": "8rem"
|
|
410
|
+
};
|
|
411
|
+
var TAILWIND_FONT_WEIGHTS = {
|
|
412
|
+
thin: 100,
|
|
413
|
+
extralight: 200,
|
|
414
|
+
light: 300,
|
|
415
|
+
normal: 400,
|
|
416
|
+
medium: 500,
|
|
417
|
+
semibold: 600,
|
|
418
|
+
bold: 700,
|
|
419
|
+
extrabold: 800,
|
|
420
|
+
black: 900
|
|
421
|
+
};
|
|
422
|
+
function fontSizeToPx(value) {
|
|
423
|
+
if (!value)
|
|
424
|
+
return null;
|
|
425
|
+
const v = value.trim();
|
|
426
|
+
let m = v.match(/^(-?\d*\.?\d+)rem$/i);
|
|
427
|
+
if (m)
|
|
428
|
+
return parseFloat(m[1]) * 16;
|
|
429
|
+
m = v.match(/^(-?\d*\.?\d+)px$/i);
|
|
430
|
+
if (m)
|
|
431
|
+
return parseFloat(m[1]);
|
|
432
|
+
m = v.match(/^(-?\d*\.?\d+)em$/i);
|
|
433
|
+
if (m)
|
|
434
|
+
return parseFloat(m[1]) * 16;
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ../scanner/dist/readers/jsx-tsx.js
|
|
439
|
+
var HEX_RE2 = /#[0-9a-fA-F]{3,8}\b/g;
|
|
440
|
+
var PX_STR_RE = /['"](-?\d*\.?\d+)px['"]/g;
|
|
441
|
+
function componentName(text, file) {
|
|
442
|
+
const fn = text.match(/export\s+(?:default\s+)?function\s+([A-Z]\w+)/);
|
|
443
|
+
if (fn)
|
|
444
|
+
return fn[1];
|
|
445
|
+
const cn = text.match(/export\s+(?:default\s+)?const\s+([A-Z]\w+)\s*[:=]/);
|
|
446
|
+
if (cn)
|
|
447
|
+
return cn[1];
|
|
448
|
+
const base = basename(file).replace(/\.(tsx|jsx)$/, "");
|
|
449
|
+
return /^[A-Z]/.test(base) ? base : null;
|
|
450
|
+
}
|
|
451
|
+
function lineOfIndex2(text, index) {
|
|
452
|
+
let line = 1;
|
|
453
|
+
for (let i = 0;i < index && i < text.length; i++) {
|
|
454
|
+
if (text[i] === `
|
|
455
|
+
`)
|
|
456
|
+
line++;
|
|
457
|
+
}
|
|
458
|
+
return line;
|
|
459
|
+
}
|
|
460
|
+
var SIZE_KEYS = Object.keys(TAILWIND_FONT_SIZES).map((k) => k.replace(/[^a-z0-9]/g, "\\$&")).join("|");
|
|
461
|
+
var TEXT_SIZE_RE = new RegExp(`\\btext-(${SIZE_KEYS})\\b`, "g");
|
|
462
|
+
var WEIGHT_KEYS = Object.keys(TAILWIND_FONT_WEIGHTS).join("|");
|
|
463
|
+
var FONT_WEIGHT_RE = new RegExp(`\\bfont-(${WEIGHT_KEYS})\\b`, "g");
|
|
464
|
+
var INLINE_SIZE_RE = /fontSize\s*:\s*['"]?(\d*\.?\d+(?:px|rem|em)?)['"]?/g;
|
|
465
|
+
var INLINE_WEIGHT_RE = /fontWeight\s*:\s*['"]?(\d{3}|bold|normal)['"]?/g;
|
|
466
|
+
var INLINE_FAMILY_RE = /fontFamily\s*:\s*['"]([^'"]+)['"]/g;
|
|
467
|
+
function readJsxTsx(root) {
|
|
468
|
+
const files = walk(root, [".tsx", ".jsx"]);
|
|
469
|
+
const colors = [];
|
|
470
|
+
const dimensions = [];
|
|
471
|
+
const components = [];
|
|
472
|
+
const typoAgg = new Map;
|
|
473
|
+
const bump = (key, entry) => {
|
|
474
|
+
const existing = typoAgg.get(key);
|
|
475
|
+
if (existing)
|
|
476
|
+
existing.count = (existing.count ?? 1) + 1;
|
|
477
|
+
else
|
|
478
|
+
typoAgg.set(key, { ...entry, count: 1 });
|
|
479
|
+
};
|
|
480
|
+
for (const file of files) {
|
|
481
|
+
let text;
|
|
482
|
+
try {
|
|
483
|
+
text = readFileSync3(file, "utf8");
|
|
484
|
+
} catch {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const rel = relative3(root, file);
|
|
488
|
+
const name = componentName(text, file);
|
|
489
|
+
if (name)
|
|
490
|
+
components.push({ name, file: rel });
|
|
491
|
+
let t;
|
|
492
|
+
TEXT_SIZE_RE.lastIndex = 0;
|
|
493
|
+
while ((t = TEXT_SIZE_RE.exec(text)) !== null) {
|
|
494
|
+
const step = t[1];
|
|
495
|
+
bump(`size:${step}`, {
|
|
496
|
+
name: `text-${step}`,
|
|
497
|
+
fontSize: TAILWIND_FONT_SIZES[step],
|
|
498
|
+
origin: "source",
|
|
499
|
+
source: { file: rel, line: lineOfIndex2(text, t.index) }
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
FONT_WEIGHT_RE.lastIndex = 0;
|
|
503
|
+
while ((t = FONT_WEIGHT_RE.exec(text)) !== null) {
|
|
504
|
+
const w = t[1];
|
|
505
|
+
bump(`weight:${w}`, {
|
|
506
|
+
name: `font-${w}`,
|
|
507
|
+
fontWeight: TAILWIND_FONT_WEIGHTS[w],
|
|
508
|
+
origin: "source",
|
|
509
|
+
source: { file: rel, line: lineOfIndex2(text, t.index) }
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
INLINE_SIZE_RE.lastIndex = 0;
|
|
513
|
+
while ((t = INLINE_SIZE_RE.exec(text)) !== null) {
|
|
514
|
+
const raw = /[a-z%]/i.test(t[1]) ? t[1] : `${t[1]}px`;
|
|
515
|
+
bump(`inline-size:${raw}`, {
|
|
516
|
+
name: `size-${raw}`,
|
|
517
|
+
fontSize: raw,
|
|
518
|
+
origin: "source",
|
|
519
|
+
source: { file: rel, line: lineOfIndex2(text, t.index) }
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
INLINE_WEIGHT_RE.lastIndex = 0;
|
|
523
|
+
while ((t = INLINE_WEIGHT_RE.exec(text)) !== null) {
|
|
524
|
+
const w = t[1] === "bold" ? 700 : t[1] === "normal" ? 400 : Number(t[1]);
|
|
525
|
+
bump(`inline-weight:${w}`, {
|
|
526
|
+
name: `weight-${w}`,
|
|
527
|
+
fontWeight: w,
|
|
528
|
+
origin: "source",
|
|
529
|
+
source: { file: rel, line: lineOfIndex2(text, t.index) }
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
INLINE_FAMILY_RE.lastIndex = 0;
|
|
533
|
+
while ((t = INLINE_FAMILY_RE.exec(text)) !== null) {
|
|
534
|
+
const family = t[1].split(",")[0].trim();
|
|
535
|
+
if (!family)
|
|
536
|
+
continue;
|
|
537
|
+
bump(`inline-family:${family}`, {
|
|
538
|
+
name: `font-${family.toLowerCase().replace(/\s+/g, "-")}`,
|
|
539
|
+
fontFamily: family,
|
|
540
|
+
origin: "source",
|
|
541
|
+
source: { file: rel, line: lineOfIndex2(text, t.index) }
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
let m;
|
|
545
|
+
HEX_RE2.lastIndex = 0;
|
|
546
|
+
while ((m = HEX_RE2.exec(text)) !== null) {
|
|
547
|
+
const hex = normalizeHex(m[0]);
|
|
548
|
+
if (hex) {
|
|
549
|
+
colors.push({
|
|
550
|
+
hex,
|
|
551
|
+
source: { file: rel, line: lineOfIndex2(text, m.index) },
|
|
552
|
+
origin: "source"
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
PX_STR_RE.lastIndex = 0;
|
|
557
|
+
while ((m = PX_STR_RE.exec(text)) !== null) {
|
|
558
|
+
const px = parseFloat(m[1]);
|
|
559
|
+
if (px === 0)
|
|
560
|
+
continue;
|
|
561
|
+
dimensions.push({
|
|
562
|
+
px,
|
|
563
|
+
raw: `${m[1]}px`,
|
|
564
|
+
source: { file: rel, line: lineOfIndex2(text, m.index) },
|
|
565
|
+
origin: "source",
|
|
566
|
+
category: "spacing"
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { colors, dimensions, typography: [...typoAgg.values()], components };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ../scanner/dist/readers/package-json.js
|
|
574
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
575
|
+
import { join as join3 } from "node:path";
|
|
576
|
+
var DESIGN_SYSTEMS = [
|
|
577
|
+
{ dep: "tailwindcss", name: "tailwind" },
|
|
578
|
+
{ dep: "@mui/material", name: "mui" },
|
|
579
|
+
{ dep: "@radix-ui/themes", name: "radix" },
|
|
580
|
+
{ dep: "@radix-ui/react-icons", name: "radix" },
|
|
581
|
+
{ dep: "@chakra-ui/react", name: "chakra" },
|
|
582
|
+
{ dep: "antd", name: "antd" }
|
|
583
|
+
];
|
|
584
|
+
function detectStack(root) {
|
|
585
|
+
let designSystem = null;
|
|
586
|
+
let tailwind = false;
|
|
587
|
+
const pkgPath = join3(root, "package.json");
|
|
588
|
+
if (existsSync2(pkgPath)) {
|
|
589
|
+
try {
|
|
590
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
591
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
592
|
+
if ("tailwindcss" in deps)
|
|
593
|
+
tailwind = true;
|
|
594
|
+
for (const { dep, name } of DESIGN_SYSTEMS) {
|
|
595
|
+
if (dep in deps) {
|
|
596
|
+
designSystem = name;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (designSystem === "tailwind" && (("class-variance-authority" in deps) || ("cmdk" in deps))) {
|
|
601
|
+
designSystem = "shadcn";
|
|
602
|
+
}
|
|
603
|
+
} catch {}
|
|
604
|
+
}
|
|
605
|
+
let canvas = null;
|
|
606
|
+
const mcpPath = join3(root, ".mcp.json");
|
|
607
|
+
if (existsSync2(mcpPath)) {
|
|
608
|
+
try {
|
|
609
|
+
const mcp = JSON.parse(readFileSync4(mcpPath, "utf8"));
|
|
610
|
+
const servers = Object.keys(mcp.mcpServers ?? {});
|
|
611
|
+
if (servers.includes("figma"))
|
|
612
|
+
canvas = "figma";
|
|
613
|
+
else if (servers.includes("pencil"))
|
|
614
|
+
canvas = "pencil";
|
|
615
|
+
} catch {}
|
|
616
|
+
}
|
|
617
|
+
return { tailwind, designSystem, canvas };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ../scanner/dist/readers/theme.js
|
|
621
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
622
|
+
import { basename as basename2, relative as relative4 } from "node:path";
|
|
623
|
+
|
|
624
|
+
// ../scanner/dist/util/dimension.js
|
|
625
|
+
var PX_RE3 = /^(-?\d*\.?\d+)px$/i;
|
|
626
|
+
var REM_RE3 = /^(-?\d*\.?\d+)rem$/i;
|
|
627
|
+
var DP_RE = /^(-?\d*\.?\d+)dp$/i;
|
|
628
|
+
var PT_RE = /^(-?\d*\.?\d+)pt$/i;
|
|
629
|
+
function toPx3(value) {
|
|
630
|
+
const v = value.trim();
|
|
631
|
+
const px = v.match(PX_RE3);
|
|
632
|
+
if (px)
|
|
633
|
+
return { px: parseFloat(px[1]), raw: v };
|
|
634
|
+
const rem = v.match(REM_RE3);
|
|
635
|
+
if (rem)
|
|
636
|
+
return { px: parseFloat(rem[1]) * 16, raw: v };
|
|
637
|
+
const dp = v.match(DP_RE);
|
|
638
|
+
if (dp)
|
|
639
|
+
return { px: parseFloat(dp[1]), raw: v };
|
|
640
|
+
const pt = v.match(PT_RE);
|
|
641
|
+
if (pt)
|
|
642
|
+
return { px: parseFloat(pt[1]), raw: v };
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
function dimensionCategory(name) {
|
|
646
|
+
const n = name.toLowerCase();
|
|
647
|
+
return n.includes("radius") || n.includes("round") || n.includes("corner") ? "radius" : "spacing";
|
|
648
|
+
}
|
|
649
|
+
function lineOfIndex3(text, index) {
|
|
650
|
+
let line = 1;
|
|
651
|
+
for (let i = 0;i < index && i < text.length; i++) {
|
|
652
|
+
if (text[i] === `
|
|
653
|
+
`)
|
|
654
|
+
line++;
|
|
655
|
+
}
|
|
656
|
+
return line;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ../scanner/dist/readers/theme.js
|
|
660
|
+
function isThemeFile(file) {
|
|
661
|
+
const b = basename2(file).toLowerCase();
|
|
662
|
+
return /(^|[.-])theme\.(t|j)sx?$/.test(b) || b === "theme.ts" || b === "theme.js";
|
|
663
|
+
}
|
|
664
|
+
function readTheme(root) {
|
|
665
|
+
const candidates = walk(root, [".ts", ".js", ".tsx", ".jsx", ".mjs", ".cjs"]).filter(isThemeFile);
|
|
666
|
+
if (candidates.length === 0)
|
|
667
|
+
return { found: false, colors: [] };
|
|
668
|
+
const colors = [];
|
|
669
|
+
const hexRe = /#[0-9a-fA-F]{3,8}\b/g;
|
|
670
|
+
const keyRe = /([a-zA-Z][\w-]*)\s*:\s*$/;
|
|
671
|
+
for (const file of candidates) {
|
|
672
|
+
let text;
|
|
673
|
+
try {
|
|
674
|
+
text = readFileSync5(file, "utf8");
|
|
675
|
+
} catch {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const rel = relative4(root, file);
|
|
679
|
+
let m;
|
|
680
|
+
hexRe.lastIndex = 0;
|
|
681
|
+
while ((m = hexRe.exec(text)) !== null) {
|
|
682
|
+
const hex = normalizeHex(m[0]);
|
|
683
|
+
if (!hex)
|
|
684
|
+
continue;
|
|
685
|
+
const before = text.slice(Math.max(0, m.index - 40), m.index);
|
|
686
|
+
const km = before.match(keyRe);
|
|
687
|
+
const name = km ? km[1] : undefined;
|
|
688
|
+
colors.push({
|
|
689
|
+
hex,
|
|
690
|
+
source: { file: rel, line: lineOfIndex3(text, m.index) },
|
|
691
|
+
origin: "theme",
|
|
692
|
+
name: name && name !== "main" && name !== "default" ? name : undefined
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return { found: colors.length > 0, colors };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ../scanner/dist/readers/scss.js
|
|
700
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
701
|
+
import { relative as relative5 } from "node:path";
|
|
702
|
+
function isTypographyVar(name) {
|
|
703
|
+
const n = name.toLowerCase();
|
|
704
|
+
return n.includes("font") || n.includes("text") || n.includes("leading") || n.includes("line-height") || n.includes("tracking") || n.includes("letter-spacing");
|
|
705
|
+
}
|
|
706
|
+
function readScss(root) {
|
|
707
|
+
const files = walk(root, [".scss", ".sass"]);
|
|
708
|
+
const colors = [];
|
|
709
|
+
const dimensions = [];
|
|
710
|
+
const typography = [];
|
|
711
|
+
const varRe = /\$([\w-]+)\s*:\s*([^;\n]+)/g;
|
|
712
|
+
const hexRe = /#[0-9a-fA-F]{3,8}\b/;
|
|
713
|
+
for (const file of files) {
|
|
714
|
+
let text;
|
|
715
|
+
try {
|
|
716
|
+
text = readFileSync6(file, "utf8");
|
|
717
|
+
} catch {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const rel = relative5(root, file);
|
|
721
|
+
let m;
|
|
722
|
+
varRe.lastIndex = 0;
|
|
723
|
+
while ((m = varRe.exec(text)) !== null) {
|
|
724
|
+
const name = m[1];
|
|
725
|
+
const value = m[2].trim();
|
|
726
|
+
const line = lineOfIndex3(text, m.index);
|
|
727
|
+
const hexMatch = value.match(hexRe);
|
|
728
|
+
if (hexMatch) {
|
|
729
|
+
const hex = normalizeHex(hexMatch[0]);
|
|
730
|
+
if (hex)
|
|
731
|
+
colors.push({ hex, source: { file: rel, line }, origin: "scss", name });
|
|
732
|
+
}
|
|
733
|
+
if (isTypographyVar(name)) {
|
|
734
|
+
const n = name.toLowerCase();
|
|
735
|
+
const dim2 = toPx3(value);
|
|
736
|
+
const weight = /^\d{3}$/.test(value) ? Number(value) : undefined;
|
|
737
|
+
typography.push({
|
|
738
|
+
name,
|
|
739
|
+
fontSize: n.includes("size") && dim2 ? dim2.raw : undefined,
|
|
740
|
+
fontFamily: n.includes("family") ? value.replace(/['"]/g, "") : undefined,
|
|
741
|
+
fontWeight: n.includes("weight") ? weight : undefined,
|
|
742
|
+
lineHeight: n.includes("leading") || n.includes("line-height") ? value : undefined,
|
|
743
|
+
origin: "scss",
|
|
744
|
+
source: { file: rel, line }
|
|
745
|
+
});
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
const dim = toPx3(value);
|
|
749
|
+
if (dim && dim.px !== 0) {
|
|
750
|
+
dimensions.push({
|
|
751
|
+
px: dim.px,
|
|
752
|
+
raw: dim.raw,
|
|
753
|
+
source: { file: rel, line },
|
|
754
|
+
origin: "scss",
|
|
755
|
+
category: dimensionCategory(name),
|
|
756
|
+
name
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { colors, dimensions, typography };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ../scanner/dist/readers/swift.js
|
|
765
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
766
|
+
import { relative as relative6 } from "node:path";
|
|
767
|
+
function channelToHex(n) {
|
|
768
|
+
const v = n <= 1 ? Math.round(n * 255) : Math.round(n);
|
|
769
|
+
return Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0");
|
|
770
|
+
}
|
|
771
|
+
function readSwift(root) {
|
|
772
|
+
const files = walk(root, [".swift"]);
|
|
773
|
+
const colors = [];
|
|
774
|
+
const hexRe = /#[0-9a-fA-F]{3,8}\b/g;
|
|
775
|
+
const rgbRe = /(?:UI)?Color\(\s*red:\s*([\d.]+)\s*,\s*green:\s*([\d.]+)\s*,\s*blue:\s*([\d.]+)/g;
|
|
776
|
+
for (const file of files) {
|
|
777
|
+
let text;
|
|
778
|
+
try {
|
|
779
|
+
text = readFileSync7(file, "utf8");
|
|
780
|
+
} catch {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const rel = relative6(root, file);
|
|
784
|
+
let m;
|
|
785
|
+
hexRe.lastIndex = 0;
|
|
786
|
+
while ((m = hexRe.exec(text)) !== null) {
|
|
787
|
+
const hex = normalizeHex(m[0]);
|
|
788
|
+
if (hex)
|
|
789
|
+
colors.push({ hex, source: { file: rel, line: lineOfIndex3(text, m.index) }, origin: "swift" });
|
|
790
|
+
}
|
|
791
|
+
rgbRe.lastIndex = 0;
|
|
792
|
+
while ((m = rgbRe.exec(text)) !== null) {
|
|
793
|
+
const hex = "#" + channelToHex(parseFloat(m[1])) + channelToHex(parseFloat(m[2])) + channelToHex(parseFloat(m[3]));
|
|
794
|
+
colors.push({ hex, source: { file: rel, line: lineOfIndex3(text, m.index) }, origin: "swift" });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { colors };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ../scanner/dist/readers/kotlin.js
|
|
801
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
802
|
+
import { relative as relative7 } from "node:path";
|
|
803
|
+
function readKotlin(root) {
|
|
804
|
+
const files = walk(root, [".kt", ".kts"]);
|
|
805
|
+
const colors = [];
|
|
806
|
+
const dimensions = [];
|
|
807
|
+
const colorRe = /Color\(\s*0x([0-9a-fA-F]{6,8})\s*\)/g;
|
|
808
|
+
const dpRe = /(\d+(?:\.\d+)?)\.dp\b/g;
|
|
809
|
+
for (const file of files) {
|
|
810
|
+
let text;
|
|
811
|
+
try {
|
|
812
|
+
text = readFileSync8(file, "utf8");
|
|
813
|
+
} catch {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const rel = relative7(root, file);
|
|
817
|
+
let m;
|
|
818
|
+
colorRe.lastIndex = 0;
|
|
819
|
+
while ((m = colorRe.exec(text)) !== null) {
|
|
820
|
+
let h = m[1].toLowerCase();
|
|
821
|
+
if (h.length === 8)
|
|
822
|
+
h = h.slice(2);
|
|
823
|
+
if (h.length === 6) {
|
|
824
|
+
colors.push({
|
|
825
|
+
hex: "#" + h,
|
|
826
|
+
source: { file: rel, line: lineOfIndex3(text, m.index) },
|
|
827
|
+
origin: "kotlin"
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
dpRe.lastIndex = 0;
|
|
832
|
+
while ((m = dpRe.exec(text)) !== null) {
|
|
833
|
+
const px = parseFloat(m[1]);
|
|
834
|
+
if (px === 0)
|
|
835
|
+
continue;
|
|
836
|
+
dimensions.push({
|
|
837
|
+
px,
|
|
838
|
+
raw: `${m[1]}dp`,
|
|
839
|
+
source: { file: rel, line: lineOfIndex3(text, m.index) },
|
|
840
|
+
origin: "kotlin",
|
|
841
|
+
category: "spacing"
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return { colors, dimensions };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// ../scanner/dist/readers/storybook.js
|
|
849
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
850
|
+
import { basename as basename3, relative as relative8 } from "node:path";
|
|
851
|
+
function isStoryFile(file) {
|
|
852
|
+
return /\.stories\.(t|j)sx?$/.test(basename3(file));
|
|
853
|
+
}
|
|
854
|
+
function readStorybook(root) {
|
|
855
|
+
const files = walk(root, [".tsx", ".jsx", ".ts", ".js"]).filter(isStoryFile);
|
|
856
|
+
const components = [];
|
|
857
|
+
for (const file of files) {
|
|
858
|
+
let text;
|
|
859
|
+
try {
|
|
860
|
+
text = readFileSync9(file, "utf8");
|
|
861
|
+
} catch {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const rel = relative8(root, file);
|
|
865
|
+
const titleM = text.match(/title\s*:\s*['"]([^'"]+)['"]/);
|
|
866
|
+
const compM = text.match(/component\s*:\s*([A-Z]\w+)/);
|
|
867
|
+
const name = compM ? compM[1] : titleM ? titleM[1].split("/").pop().trim() : basename3(file).replace(/\.stories\.(t|j)sx?$/, "");
|
|
868
|
+
const variants = [];
|
|
869
|
+
const storyRe = /export\s+const\s+([A-Z]\w+)\s*[:=]/g;
|
|
870
|
+
let m;
|
|
871
|
+
while ((m = storyRe.exec(text)) !== null) {
|
|
872
|
+
if (m[1] !== "default")
|
|
873
|
+
variants.push(m[1]);
|
|
874
|
+
}
|
|
875
|
+
components.push({ name, file: rel, variants: variants.length ? variants : undefined });
|
|
876
|
+
}
|
|
877
|
+
return { components };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ../scanner/dist/readers/tokens-json.js
|
|
881
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
882
|
+
import { basename as basename4, relative as relative9 } from "node:path";
|
|
883
|
+
function isTokenFile(file) {
|
|
884
|
+
const b = basename4(file).toLowerCase();
|
|
885
|
+
if (b === "package.json" || b === "tsconfig.json" || b.endsWith(".lock.json"))
|
|
886
|
+
return false;
|
|
887
|
+
return /tokens?(\.[\w-]+)?\.json$/.test(b) || /[\\/]tokens?[\\/]/.test(file.toLowerCase());
|
|
888
|
+
}
|
|
889
|
+
function collectLeaves(node, path, out) {
|
|
890
|
+
if (node === null || typeof node !== "object")
|
|
891
|
+
return;
|
|
892
|
+
const obj = node;
|
|
893
|
+
const value = obj.$value ?? obj.value;
|
|
894
|
+
if (value !== undefined && (typeof value === "string" || typeof value === "number")) {
|
|
895
|
+
out.push({
|
|
896
|
+
path,
|
|
897
|
+
value: String(value),
|
|
898
|
+
type: typeof obj.$type === "string" ? obj.$type : undefined
|
|
899
|
+
});
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
903
|
+
if (k.startsWith("$"))
|
|
904
|
+
continue;
|
|
905
|
+
collectLeaves(v, [...path, k], out);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function readTokensJson(root) {
|
|
909
|
+
const files = walk(root, [".json"]).filter(isTokenFile);
|
|
910
|
+
const colors = [];
|
|
911
|
+
const dimensions = [];
|
|
912
|
+
const typography = [];
|
|
913
|
+
let found = false;
|
|
914
|
+
for (const file of files) {
|
|
915
|
+
let parsed;
|
|
916
|
+
try {
|
|
917
|
+
parsed = JSON.parse(readFileSync10(file, "utf8"));
|
|
918
|
+
} catch {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const rel = relative9(root, file);
|
|
922
|
+
const leaves = [];
|
|
923
|
+
collectLeaves(parsed, [], leaves);
|
|
924
|
+
if (leaves.length === 0)
|
|
925
|
+
continue;
|
|
926
|
+
for (const leaf of leaves) {
|
|
927
|
+
const name = leaf.path.join("-");
|
|
928
|
+
const type = leaf.type?.toLowerCase();
|
|
929
|
+
const hex = normalizeHex(leaf.value);
|
|
930
|
+
const isColor = type === "color" || !type && hex !== null && /^#/.test(leaf.value.trim());
|
|
931
|
+
if (isColor && hex) {
|
|
932
|
+
colors.push({ hex, source: { file: rel, line: 0 }, origin: "tokens", name });
|
|
933
|
+
found = true;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const dim = toPx3(leaf.value);
|
|
937
|
+
const isDim = type === "dimension" || type === "spacing" || !type && dim !== null;
|
|
938
|
+
if (isDim && dim && dim.px !== 0) {
|
|
939
|
+
dimensions.push({
|
|
940
|
+
px: dim.px,
|
|
941
|
+
raw: dim.raw,
|
|
942
|
+
source: { file: rel, line: 0 },
|
|
943
|
+
origin: "tokens",
|
|
944
|
+
category: dimensionCategory(name),
|
|
945
|
+
name
|
|
946
|
+
});
|
|
947
|
+
found = true;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (type === "fontfamily" || type === "typography" || /font/i.test(name)) {
|
|
951
|
+
typography.push({ name, fontFamily: leaf.value, origin: "tokens", source: { file: rel, line: 0 } });
|
|
952
|
+
found = true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return { found, colors, dimensions, typography };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ../scanner/dist/analyzers/colors.js
|
|
960
|
+
var DUPLICATE_THRESHOLD = 5;
|
|
961
|
+
function analyzeColors(raw) {
|
|
962
|
+
const exact = new Map;
|
|
963
|
+
for (const c of raw) {
|
|
964
|
+
const e = exact.get(c.hex) ?? { count: 0, names: new Map };
|
|
965
|
+
e.count++;
|
|
966
|
+
if (c.name)
|
|
967
|
+
e.names.set(c.name, (e.names.get(c.name) ?? 0) + 1);
|
|
968
|
+
exact.set(c.hex, e);
|
|
969
|
+
}
|
|
970
|
+
const sorted = [...exact.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
971
|
+
const buckets = [];
|
|
972
|
+
for (const [hex, { count, names }] of sorted) {
|
|
973
|
+
const lab = rgbToLab(hexToRgb(hex));
|
|
974
|
+
const hit = buckets.find((b) => deltaE(b.lab, lab) < DUPLICATE_THRESHOLD);
|
|
975
|
+
if (hit) {
|
|
976
|
+
hit.count += count;
|
|
977
|
+
hit.members.set(hex, count);
|
|
978
|
+
for (const [n, v] of names)
|
|
979
|
+
hit.names.set(n, (hit.names.get(n) ?? 0) + v);
|
|
980
|
+
} else {
|
|
981
|
+
buckets.push({
|
|
982
|
+
hex,
|
|
983
|
+
count,
|
|
984
|
+
members: new Map([[hex, count]]),
|
|
985
|
+
lab,
|
|
986
|
+
names: new Map(names)
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
buckets.sort((a, b) => b.count - a.count);
|
|
991
|
+
const clusters = buckets.map((b, i) => {
|
|
992
|
+
const { name, named } = nameCluster(b, i);
|
|
993
|
+
return {
|
|
994
|
+
hex: b.hex,
|
|
995
|
+
count: b.count,
|
|
996
|
+
members: [...b.members.keys()],
|
|
997
|
+
name,
|
|
998
|
+
named
|
|
999
|
+
};
|
|
1000
|
+
});
|
|
1001
|
+
return { clusters, rawCount: exact.size };
|
|
1002
|
+
}
|
|
1003
|
+
function nameCluster(b, index) {
|
|
1004
|
+
if (b.names.size > 0) {
|
|
1005
|
+
const top = [...b.names.entries()].sort((x, y) => y[1] - x[1])[0][0];
|
|
1006
|
+
const slug = top.toLowerCase().replace(/^--/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1007
|
+
if (slug && /^[a-z]/.test(slug))
|
|
1008
|
+
return { name: slug, named: true };
|
|
1009
|
+
}
|
|
1010
|
+
const rgb = hexToRgb(b.hex);
|
|
1011
|
+
const lum = (rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114) / 255;
|
|
1012
|
+
const sat = saturation(rgb);
|
|
1013
|
+
if (sat < 0.12) {
|
|
1014
|
+
if (lum > 0.85)
|
|
1015
|
+
return { name: "surface", named: false };
|
|
1016
|
+
if (lum < 0.2)
|
|
1017
|
+
return { name: "on-surface", named: false };
|
|
1018
|
+
return { name: "neutral", named: false };
|
|
1019
|
+
}
|
|
1020
|
+
const role = ["primary", "secondary", "tertiary"][index] ?? `accent-${index}`;
|
|
1021
|
+
return { name: role, named: false };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ../scanner/dist/analyzers/spacing.js
|
|
1025
|
+
var DEFINITION_ORIGINS = new Set(["tailwind", "css-var", "scss", "tokens", "theme"]);
|
|
1026
|
+
function isDefinition(d) {
|
|
1027
|
+
return DEFINITION_ORIGINS.has(d.origin);
|
|
1028
|
+
}
|
|
1029
|
+
function uniqueRounded(dims, min, max) {
|
|
1030
|
+
return [
|
|
1031
|
+
...new Set(dims.map((d) => Math.round(d.px)).filter((v) => v >= min && v <= max))
|
|
1032
|
+
].sort((a, b) => a - b);
|
|
1033
|
+
}
|
|
1034
|
+
function analyzeSpacing(dims) {
|
|
1035
|
+
const spacing = dims.filter((d) => d.category === "spacing");
|
|
1036
|
+
const defined = uniqueRounded(spacing.filter(isDefinition), 1, 256);
|
|
1037
|
+
if (defined.length > 0) {
|
|
1038
|
+
return withGrid(defined);
|
|
1039
|
+
}
|
|
1040
|
+
const used = uniqueRounded(spacing, 1, 128);
|
|
1041
|
+
const result = withGrid(used);
|
|
1042
|
+
if (used.length >= 3 && result.grid !== null) {
|
|
1043
|
+
const onGrid = used.filter((v) => v % result.grid === 0);
|
|
1044
|
+
if (onGrid.length >= 3)
|
|
1045
|
+
return { values: onGrid, grid: result.grid, outliers: [] };
|
|
1046
|
+
}
|
|
1047
|
+
return { values: [], grid: null, outliers: [] };
|
|
1048
|
+
}
|
|
1049
|
+
function withGrid(values) {
|
|
1050
|
+
if (values.length === 0)
|
|
1051
|
+
return { values: [], grid: null, outliers: [] };
|
|
1052
|
+
let grid = null;
|
|
1053
|
+
for (const base of [8, 4]) {
|
|
1054
|
+
const onGrid = values.filter((v) => v % base === 0).length;
|
|
1055
|
+
if (onGrid / values.length >= 0.6) {
|
|
1056
|
+
grid = base;
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const outliers = grid ? values.filter((v) => v % grid !== 0) : [];
|
|
1061
|
+
return { values, grid, outliers };
|
|
1062
|
+
}
|
|
1063
|
+
function analyzeRadius(dims) {
|
|
1064
|
+
const radius = dims.filter((d) => d.category === "radius");
|
|
1065
|
+
const defined = uniqueRounded(radius.filter(isDefinition), 0, 9999);
|
|
1066
|
+
if (defined.length > 0)
|
|
1067
|
+
return { values: defined };
|
|
1068
|
+
return { values: uniqueRounded(radius, 0, 9999) };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// ../scanner/dist/analyzers/drift.js
|
|
1072
|
+
var MATCH_THRESHOLD = 5;
|
|
1073
|
+
function analyzeDrift(colors, dimensions, clusters, grid) {
|
|
1074
|
+
const drift = [];
|
|
1075
|
+
const clusterLabs = clusters.map((c) => ({
|
|
1076
|
+
name: c.name,
|
|
1077
|
+
lab: rgbToLab(hexToRgb(c.hex))
|
|
1078
|
+
}));
|
|
1079
|
+
for (const c of colors) {
|
|
1080
|
+
if (c.origin !== "source")
|
|
1081
|
+
continue;
|
|
1082
|
+
const lab = rgbToLab(hexToRgb(c.hex));
|
|
1083
|
+
let best = null;
|
|
1084
|
+
for (const cl of clusterLabs) {
|
|
1085
|
+
const d = deltaE(cl.lab, lab);
|
|
1086
|
+
if (!best || d < best.d)
|
|
1087
|
+
best = { name: cl.name, d };
|
|
1088
|
+
}
|
|
1089
|
+
drift.push({
|
|
1090
|
+
file: c.source.file,
|
|
1091
|
+
line: c.source.line,
|
|
1092
|
+
value: c.hex,
|
|
1093
|
+
kind: "color",
|
|
1094
|
+
suggestion: best && best.d < MATCH_THRESHOLD ? `colors.${best.name}` : undefined
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (grid) {
|
|
1098
|
+
for (const d of dimensions) {
|
|
1099
|
+
if (d.origin !== "source")
|
|
1100
|
+
continue;
|
|
1101
|
+
const px = Math.round(d.px);
|
|
1102
|
+
if (px > 0 && px % grid !== 0) {
|
|
1103
|
+
const nearest = Math.round(px / grid) * grid;
|
|
1104
|
+
drift.push({
|
|
1105
|
+
file: d.source.file,
|
|
1106
|
+
line: d.source.line,
|
|
1107
|
+
value: d.raw,
|
|
1108
|
+
kind: "spacing",
|
|
1109
|
+
suggestion: `${nearest}px (nearest ${grid}pt step)`
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
drift.sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1);
|
|
1115
|
+
return drift;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ../scanner/dist/analyzers/typography.js
|
|
1119
|
+
var DEFINITION_ORIGINS2 = new Set(["tailwind", "css-var", "scss", "tokens", "theme"]);
|
|
1120
|
+
function isDefinition2(t) {
|
|
1121
|
+
return t.origin !== undefined && DEFINITION_ORIGINS2.has(t.origin);
|
|
1122
|
+
}
|
|
1123
|
+
function dominant(values) {
|
|
1124
|
+
const tally = new Map;
|
|
1125
|
+
for (const { value, weight } of values) {
|
|
1126
|
+
tally.set(value, (tally.get(value) ?? 0) + weight);
|
|
1127
|
+
}
|
|
1128
|
+
let best;
|
|
1129
|
+
let bestN = 0;
|
|
1130
|
+
for (const [value, n] of tally) {
|
|
1131
|
+
if (n > bestN) {
|
|
1132
|
+
best = value;
|
|
1133
|
+
bestN = n;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return best;
|
|
1137
|
+
}
|
|
1138
|
+
var MAX_TOKENS = 24;
|
|
1139
|
+
function analyzeTypography(raw) {
|
|
1140
|
+
if (raw.length === 0)
|
|
1141
|
+
return [];
|
|
1142
|
+
const families = raw.filter((t) => t.fontFamily).map((t) => ({ value: t.fontFamily, weight: t.count ?? 1 }));
|
|
1143
|
+
const weights = raw.filter((t) => typeof t.fontWeight === "number").map((t) => ({ value: String(t.fontWeight), weight: t.count ?? 1 }));
|
|
1144
|
+
const dominantFamily = dominant(families);
|
|
1145
|
+
const dominantWeight = dominant(weights);
|
|
1146
|
+
const dominantWeightNum = dominantWeight ? Number(dominantWeight) : undefined;
|
|
1147
|
+
const byPx = new Map;
|
|
1148
|
+
const consider = [
|
|
1149
|
+
...raw.filter(isDefinition2),
|
|
1150
|
+
...raw.filter((t) => !isDefinition2(t))
|
|
1151
|
+
];
|
|
1152
|
+
for (const t of consider) {
|
|
1153
|
+
const px = fontSizeToPx(t.fontSize);
|
|
1154
|
+
if (px === null)
|
|
1155
|
+
continue;
|
|
1156
|
+
if (byPx.has(px))
|
|
1157
|
+
continue;
|
|
1158
|
+
byPx.set(px, {
|
|
1159
|
+
name: t.name,
|
|
1160
|
+
fontSize: t.fontSize,
|
|
1161
|
+
fontFamily: t.fontFamily ?? dominantFamily,
|
|
1162
|
+
fontWeight: t.fontWeight ?? dominantWeightNum,
|
|
1163
|
+
lineHeight: t.lineHeight,
|
|
1164
|
+
origin: t.origin
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
if (byPx.size > 0) {
|
|
1168
|
+
return [...byPx.entries()].sort((a, b) => b[0] - a[0]).slice(0, MAX_TOKENS).map(([, t]) => t);
|
|
1169
|
+
}
|
|
1170
|
+
const fallback = [];
|
|
1171
|
+
const seen = new Set;
|
|
1172
|
+
for (const t of raw) {
|
|
1173
|
+
if (!t.fontFamily && t.fontWeight === undefined && !t.lineHeight)
|
|
1174
|
+
continue;
|
|
1175
|
+
const key = `${t.fontFamily ?? ""}|${t.fontWeight ?? ""}|${t.lineHeight ?? ""}`;
|
|
1176
|
+
if (seen.has(key))
|
|
1177
|
+
continue;
|
|
1178
|
+
seen.add(key);
|
|
1179
|
+
fallback.push({
|
|
1180
|
+
name: t.name,
|
|
1181
|
+
fontFamily: t.fontFamily,
|
|
1182
|
+
fontWeight: t.fontWeight,
|
|
1183
|
+
lineHeight: t.lineHeight,
|
|
1184
|
+
origin: t.origin
|
|
1185
|
+
});
|
|
1186
|
+
if (fallback.length >= MAX_TOKENS)
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
return fallback;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// ../scanner/dist/output/design-md.js
|
|
1193
|
+
var SPACING_NAMES = ["xs", "sm", "md", "lg", "xl", "xxl", "xxxl"];
|
|
1194
|
+
var RADIUS_NAMES = ["sm", "md", "lg"];
|
|
1195
|
+
function safeName(name, fallback) {
|
|
1196
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1197
|
+
return /^[a-z]/.test(slug) ? slug : fallback;
|
|
1198
|
+
}
|
|
1199
|
+
function yamlColors(report) {
|
|
1200
|
+
const lines = ["colors:"];
|
|
1201
|
+
const used = new Set;
|
|
1202
|
+
const pick = (role) => report.colors.clusters.find((c) => c.name === role)?.hex;
|
|
1203
|
+
const primary = pick("primary") ?? report.colors.clusters[0]?.hex ?? "#1f2937";
|
|
1204
|
+
const surface = pick("surface") ?? "#ffffff";
|
|
1205
|
+
const onSurface = pick("on-surface") ?? "#111111";
|
|
1206
|
+
const emit = (name, hex) => {
|
|
1207
|
+
if (used.has(name))
|
|
1208
|
+
return;
|
|
1209
|
+
used.add(name);
|
|
1210
|
+
lines.push(` ${name}: "${hex}"`);
|
|
1211
|
+
};
|
|
1212
|
+
emit("primary", primary);
|
|
1213
|
+
emit("on-primary", readableOn(hexToRgb(primary)));
|
|
1214
|
+
const secondary = pick("secondary");
|
|
1215
|
+
if (secondary) {
|
|
1216
|
+
emit("secondary", secondary);
|
|
1217
|
+
emit("on-secondary", readableOn(hexToRgb(secondary)));
|
|
1218
|
+
}
|
|
1219
|
+
const tertiary = pick("tertiary");
|
|
1220
|
+
if (tertiary) {
|
|
1221
|
+
emit("tertiary", tertiary);
|
|
1222
|
+
emit("on-tertiary", readableOn(hexToRgb(tertiary)));
|
|
1223
|
+
}
|
|
1224
|
+
const neutral = pick("neutral");
|
|
1225
|
+
if (neutral)
|
|
1226
|
+
emit("neutral", neutral);
|
|
1227
|
+
emit("surface", surface);
|
|
1228
|
+
emit("on-surface", onSurface);
|
|
1229
|
+
emit("error", pick("error") ?? "#b91c1c");
|
|
1230
|
+
emit("on-error", "#ffffff");
|
|
1231
|
+
return lines;
|
|
1232
|
+
}
|
|
1233
|
+
function yamlSpacing(report) {
|
|
1234
|
+
const vals = report.spacing.values.length ? report.spacing.values : [4, 8, 16, 24, 32, 48];
|
|
1235
|
+
const chosen = vals.slice(0, SPACING_NAMES.length);
|
|
1236
|
+
const lines = ["spacing:"];
|
|
1237
|
+
chosen.forEach((v, i) => lines.push(` ${SPACING_NAMES[i]}: ${v}px`));
|
|
1238
|
+
return lines;
|
|
1239
|
+
}
|
|
1240
|
+
function yamlRounded(report) {
|
|
1241
|
+
const defaults = [4, 8, 12];
|
|
1242
|
+
const detected = report.radius.values.filter((v) => v > 0);
|
|
1243
|
+
const lines = ["rounded:", " none: 0px"];
|
|
1244
|
+
RADIUS_NAMES.slice(0, 3).forEach((name, i) => {
|
|
1245
|
+
const v = detected[i] ?? defaults[i];
|
|
1246
|
+
lines.push(` ${name}: ${v}px`);
|
|
1247
|
+
});
|
|
1248
|
+
lines.push(" full: 9999px");
|
|
1249
|
+
return lines;
|
|
1250
|
+
}
|
|
1251
|
+
function yamlTypography(report) {
|
|
1252
|
+
const scanned = report.typography.filter((t) => t.fontSize);
|
|
1253
|
+
if (scanned.length > 0) {
|
|
1254
|
+
const lines = ["typography:"];
|
|
1255
|
+
scanned.slice(0, 9).forEach((t, i) => {
|
|
1256
|
+
const nm = safeName(t.name, `type-${i + 1}`);
|
|
1257
|
+
const family = (t.fontFamily || "system-ui").split(",")[0].trim();
|
|
1258
|
+
const lh = typeof t.lineHeight === "number" ? t.lineHeight : typeof t.lineHeight === "string" && /^[\d.]/.test(t.lineHeight) ? t.lineHeight : 1.4;
|
|
1259
|
+
lines.push(` ${nm}:`);
|
|
1260
|
+
lines.push(` fontFamily: ${family}`);
|
|
1261
|
+
lines.push(` fontSize: ${t.fontSize}`);
|
|
1262
|
+
lines.push(` fontWeight: ${t.fontWeight ?? 400}`);
|
|
1263
|
+
lines.push(` lineHeight: ${lh}`);
|
|
1264
|
+
});
|
|
1265
|
+
return lines;
|
|
1266
|
+
}
|
|
1267
|
+
return [
|
|
1268
|
+
"typography:",
|
|
1269
|
+
" headline-lg:",
|
|
1270
|
+
" fontFamily: system-ui",
|
|
1271
|
+
" fontSize: 2.25rem",
|
|
1272
|
+
" fontWeight: 700",
|
|
1273
|
+
" lineHeight: 1.15",
|
|
1274
|
+
" headline-md:",
|
|
1275
|
+
" fontFamily: system-ui",
|
|
1276
|
+
" fontSize: 1.5rem",
|
|
1277
|
+
" fontWeight: 600",
|
|
1278
|
+
" lineHeight: 1.25",
|
|
1279
|
+
" body-md:",
|
|
1280
|
+
" fontFamily: system-ui",
|
|
1281
|
+
" fontSize: 1rem",
|
|
1282
|
+
" fontWeight: 400",
|
|
1283
|
+
" lineHeight: 1.6",
|
|
1284
|
+
" label-sm:",
|
|
1285
|
+
" fontFamily: system-ui",
|
|
1286
|
+
" fontSize: 0.75rem",
|
|
1287
|
+
" fontWeight: 500",
|
|
1288
|
+
" lineHeight: 1.3"
|
|
1289
|
+
];
|
|
1290
|
+
}
|
|
1291
|
+
function renderDesignMd(report, projectName) {
|
|
1292
|
+
const name = safeName(projectName, "scanned-system");
|
|
1293
|
+
const frontmatter = [
|
|
1294
|
+
"---",
|
|
1295
|
+
`name: ${name}`,
|
|
1296
|
+
"version: alpha",
|
|
1297
|
+
`description: Design tokens reverse-engineered from ${report.meta.filesScanned} files by DesignAgent.`,
|
|
1298
|
+
...yamlColors(report),
|
|
1299
|
+
...yamlTypography(report),
|
|
1300
|
+
...yamlSpacing(report),
|
|
1301
|
+
...yamlRounded(report),
|
|
1302
|
+
"components:",
|
|
1303
|
+
" button-primary:",
|
|
1304
|
+
' backgroundColor: "{colors.primary}"',
|
|
1305
|
+
' textColor: "{colors.on-primary}"',
|
|
1306
|
+
' rounded: "{rounded.md}"',
|
|
1307
|
+
" padding: 12px",
|
|
1308
|
+
" card:",
|
|
1309
|
+
' backgroundColor: "{colors.surface}"',
|
|
1310
|
+
' textColor: "{colors.on-surface}"',
|
|
1311
|
+
' rounded: "{rounded.lg}"',
|
|
1312
|
+
" padding: 16px",
|
|
1313
|
+
"---"
|
|
1314
|
+
].join(`
|
|
1315
|
+
`);
|
|
1316
|
+
const gridNote = report.spacing.grid ? `Detected an ${report.spacing.grid}pt spacing grid${report.spacing.outliers.length ? `, with ${report.spacing.outliers.length} off-grid outlier(s).` : "."}` : "No consistent spacing grid detected — values are listed as found.";
|
|
1317
|
+
const body = [
|
|
1318
|
+
"",
|
|
1319
|
+
"## Overview",
|
|
1320
|
+
"",
|
|
1321
|
+
`Reverse-engineered from your codebase: ${report.colors.raw} unique colors collapsed into ${report.colors.clusters.length} tokens, ${report.spacing.values.length} spacing values, ${report.components.length} components. ${report.drift.length} hardcoded value(s) flagged as drift.`,
|
|
1322
|
+
"",
|
|
1323
|
+
"## Colors",
|
|
1324
|
+
"",
|
|
1325
|
+
"Role-based palette clustered from real usage. Near-duplicates were merged.",
|
|
1326
|
+
"",
|
|
1327
|
+
"## Typography",
|
|
1328
|
+
"",
|
|
1329
|
+
"Starter type scale — refine to match your actual fonts.",
|
|
1330
|
+
"",
|
|
1331
|
+
"## Layout",
|
|
1332
|
+
"",
|
|
1333
|
+
gridNote,
|
|
1334
|
+
"",
|
|
1335
|
+
"## Elevation & Depth",
|
|
1336
|
+
"",
|
|
1337
|
+
"Keep elevation minimal; rely on surface and border contrast.",
|
|
1338
|
+
"",
|
|
1339
|
+
"## Components",
|
|
1340
|
+
"",
|
|
1341
|
+
"Token-composed primitives inferred from your component inventory.",
|
|
1342
|
+
"",
|
|
1343
|
+
"## Do's and Don'ts",
|
|
1344
|
+
"",
|
|
1345
|
+
"- **Do** replace hardcoded values flagged in the drift report with these tokens.",
|
|
1346
|
+
"- **Don't** introduce off-grid spacing or one-off colors.",
|
|
1347
|
+
""
|
|
1348
|
+
].join(`
|
|
1349
|
+
`);
|
|
1350
|
+
return frontmatter + `
|
|
1351
|
+
` + body;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// ../scanner/dist/index.js
|
|
1355
|
+
function mergeComponents(primary, stories) {
|
|
1356
|
+
const byName = new Map;
|
|
1357
|
+
for (const c of primary)
|
|
1358
|
+
if (!byName.has(c.name))
|
|
1359
|
+
byName.set(c.name, { ...c });
|
|
1360
|
+
for (const s of stories) {
|
|
1361
|
+
const existing = byName.get(s.name);
|
|
1362
|
+
if (existing) {
|
|
1363
|
+
if (s.variants?.length) {
|
|
1364
|
+
existing.variants = [...new Set([...existing.variants ?? [], ...s.variants])];
|
|
1365
|
+
}
|
|
1366
|
+
} else {
|
|
1367
|
+
byName.set(s.name, { ...s });
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return [...byName.values()];
|
|
1371
|
+
}
|
|
1372
|
+
function scan(root, options = {}) {
|
|
1373
|
+
const tailwind = readTailwind(root);
|
|
1374
|
+
const css = readCssVars(root);
|
|
1375
|
+
const scss = readScss(root);
|
|
1376
|
+
const theme = readTheme(root);
|
|
1377
|
+
const swift = readSwift(root);
|
|
1378
|
+
const kotlin = readKotlin(root);
|
|
1379
|
+
const tokens = readTokensJson(root);
|
|
1380
|
+
const jsx = readJsxTsx(root);
|
|
1381
|
+
const stories = readStorybook(root);
|
|
1382
|
+
const detected = detectStack(root);
|
|
1383
|
+
const colors = [
|
|
1384
|
+
...tailwind.colors,
|
|
1385
|
+
...css.colors,
|
|
1386
|
+
...scss.colors,
|
|
1387
|
+
...theme.colors,
|
|
1388
|
+
...tokens.colors,
|
|
1389
|
+
...swift.colors,
|
|
1390
|
+
...kotlin.colors,
|
|
1391
|
+
...jsx.colors
|
|
1392
|
+
];
|
|
1393
|
+
const dimensions = [
|
|
1394
|
+
...tailwind.dimensions,
|
|
1395
|
+
...css.dimensions,
|
|
1396
|
+
...scss.dimensions,
|
|
1397
|
+
...tokens.dimensions,
|
|
1398
|
+
...kotlin.dimensions,
|
|
1399
|
+
...jsx.dimensions
|
|
1400
|
+
];
|
|
1401
|
+
const rawTypography = [
|
|
1402
|
+
...tailwind.typography,
|
|
1403
|
+
...css.typography,
|
|
1404
|
+
...scss.typography,
|
|
1405
|
+
...tokens.typography,
|
|
1406
|
+
...jsx.typography
|
|
1407
|
+
];
|
|
1408
|
+
const { clusters, rawCount } = analyzeColors(colors);
|
|
1409
|
+
const spacing = analyzeSpacing(dimensions);
|
|
1410
|
+
const radius = analyzeRadius(dimensions);
|
|
1411
|
+
const typography = analyzeTypography(rawTypography);
|
|
1412
|
+
const drift = analyzeDrift(colors, dimensions, clusters, spacing.grid);
|
|
1413
|
+
const components = mergeComponents(jsx.components, stories.components);
|
|
1414
|
+
const fileSet = new Set;
|
|
1415
|
+
for (const c of colors)
|
|
1416
|
+
fileSet.add(c.source.file);
|
|
1417
|
+
for (const d of dimensions)
|
|
1418
|
+
fileSet.add(d.source.file);
|
|
1419
|
+
for (const comp of components)
|
|
1420
|
+
fileSet.add(comp.file);
|
|
1421
|
+
if (detected.tailwind || tailwind.found)
|
|
1422
|
+
detected.tailwind = true;
|
|
1423
|
+
return {
|
|
1424
|
+
meta: {
|
|
1425
|
+
root,
|
|
1426
|
+
filesScanned: fileSet.size,
|
|
1427
|
+
generatedAt: options.now ?? null
|
|
1428
|
+
},
|
|
1429
|
+
colors: { raw: rawCount, clusters },
|
|
1430
|
+
spacing,
|
|
1431
|
+
radius,
|
|
1432
|
+
typography,
|
|
1433
|
+
components,
|
|
1434
|
+
drift,
|
|
1435
|
+
detected
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/scaffold.ts
|
|
1440
|
+
import {
|
|
1441
|
+
copyFileSync,
|
|
1442
|
+
existsSync as existsSync5,
|
|
1443
|
+
mkdirSync as mkdirSync2,
|
|
1444
|
+
readFileSync as readFileSync12,
|
|
1445
|
+
renameSync,
|
|
1446
|
+
writeFileSync
|
|
1447
|
+
} from "node:fs";
|
|
1448
|
+
import { join as join6 } from "node:path";
|
|
1449
|
+
|
|
1450
|
+
// src/types.ts
|
|
1451
|
+
var FRAMEWORK_LABELS = {
|
|
1452
|
+
react: "React",
|
|
1453
|
+
vue: "Vue",
|
|
1454
|
+
svelte: "Svelte",
|
|
1455
|
+
"react-native": "React Native",
|
|
1456
|
+
swiftui: "SwiftUI",
|
|
1457
|
+
compose: "Jetpack Compose",
|
|
1458
|
+
none: "None / other"
|
|
1459
|
+
};
|
|
1460
|
+
var DESIGN_SYSTEM_LABELS = {
|
|
1461
|
+
tailwind: "Tailwind",
|
|
1462
|
+
shadcn: "shadcn/ui",
|
|
1463
|
+
custom: "Custom"
|
|
1464
|
+
};
|
|
1465
|
+
var CANVAS_LABELS = {
|
|
1466
|
+
figma: "Figma",
|
|
1467
|
+
pencil: "Pencil",
|
|
1468
|
+
openpencil: "OpenPencil",
|
|
1469
|
+
none: "None / code only"
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// src/templates.ts
|
|
1473
|
+
import { readFileSync as readFileSync11, existsSync as existsSync3 } from "node:fs";
|
|
1474
|
+
import { dirname, join as join4, resolve } from "node:path";
|
|
1475
|
+
import { fileURLToPath } from "node:url";
|
|
1476
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
1477
|
+
function resolveTemplatesDir() {
|
|
1478
|
+
const candidates = [join4(here, "templates"), resolve(here, "..", "templates")];
|
|
1479
|
+
for (const dir of candidates) {
|
|
1480
|
+
if (existsSync3(dir))
|
|
1481
|
+
return dir;
|
|
1482
|
+
}
|
|
1483
|
+
return candidates[0];
|
|
1484
|
+
}
|
|
1485
|
+
var TEMPLATES_DIR = resolveTemplatesDir();
|
|
1486
|
+
function readTemplate(relativePath) {
|
|
1487
|
+
return readFileSync11(join4(TEMPLATES_DIR, relativePath), "utf8");
|
|
1488
|
+
}
|
|
1489
|
+
function fill(template, vars) {
|
|
1490
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => (key in vars) ? vars[key] : match);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/generators/claude-md.ts
|
|
1494
|
+
var FRAMEWORK_TEMPLATES = {
|
|
1495
|
+
react: "react",
|
|
1496
|
+
swiftui: "swiftui",
|
|
1497
|
+
compose: "compose"
|
|
1498
|
+
};
|
|
1499
|
+
function canvasBlock(config) {
|
|
1500
|
+
switch (config.canvas) {
|
|
1501
|
+
case "figma":
|
|
1502
|
+
return [
|
|
1503
|
+
`Source of truth: **Figma**${config.figmaUrl ? ` — ${config.figmaUrl}` : ""}.`,
|
|
1504
|
+
"",
|
|
1505
|
+
"- Inspect components and variables via the Figma Dev Mode MCP (configured in `.mcp.json`).",
|
|
1506
|
+
"- Use the `token-sync` skill to pull Figma variables into `DESIGN.md`. Treat Figma as the upstream for tokens.",
|
|
1507
|
+
"- When code and Figma disagree on a token, sync `DESIGN.md` from Figma rather than hand-editing values."
|
|
1508
|
+
].join(`
|
|
1509
|
+
`);
|
|
1510
|
+
case "pencil":
|
|
1511
|
+
case "openpencil":
|
|
1512
|
+
return [
|
|
1513
|
+
`Source of truth: **${CANVAS_LABELS[config.canvas]}** (\`.pen\` files).`,
|
|
1514
|
+
"",
|
|
1515
|
+
"- Tokens live in the canvas's `.pen` JSON. Use the `token-sync` skill to extract them into `DESIGN.md`.",
|
|
1516
|
+
"- Regenerate `DESIGN.md` from the canvas instead of editing token values by hand."
|
|
1517
|
+
].join(`
|
|
1518
|
+
`);
|
|
1519
|
+
case "none":
|
|
1520
|
+
default:
|
|
1521
|
+
return [
|
|
1522
|
+
"No external canvas. `DESIGN.md` is the source of truth for tokens.",
|
|
1523
|
+
"",
|
|
1524
|
+
"- Evolve the design system directly in `DESIGN.md` and keep it linting clean.",
|
|
1525
|
+
"- You (Claude) act as the designer here — make confident, token-driven decisions and record the notable ones in `DECISIONS.md`."
|
|
1526
|
+
].join(`
|
|
1527
|
+
`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
function generateClaudeMd(config) {
|
|
1531
|
+
const frameworkLabels = config.frameworks.map((f) => FRAMEWORK_LABELS[f]).join(", ");
|
|
1532
|
+
const vars = {
|
|
1533
|
+
PROJECT_NAME: config.projectName,
|
|
1534
|
+
CANVAS: CANVAS_LABELS[config.canvas],
|
|
1535
|
+
DESIGN_SYSTEM: DESIGN_SYSTEM_LABELS[config.designSystem],
|
|
1536
|
+
FRAMEWORKS: frameworkLabels || "None",
|
|
1537
|
+
CANVAS_BLOCK: canvasBlock(config)
|
|
1538
|
+
};
|
|
1539
|
+
const parts = [fill(readTemplate("claude-md/base.md"), vars)];
|
|
1540
|
+
for (const framework of config.frameworks) {
|
|
1541
|
+
const templateName = FRAMEWORK_TEMPLATES[framework];
|
|
1542
|
+
if (templateName) {
|
|
1543
|
+
parts.push(fill(readTemplate(`claude-md/${templateName}.md`), vars));
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (config.frameworks.filter((f) => f !== "none").length > 1) {
|
|
1547
|
+
parts.push(fill(readTemplate("claude-md/monorepo.md"), vars));
|
|
1548
|
+
}
|
|
1549
|
+
return parts.join(`
|
|
1550
|
+
|
|
1551
|
+
---
|
|
1552
|
+
|
|
1553
|
+
`) + `
|
|
1554
|
+
`;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/generators/design-md.ts
|
|
1558
|
+
function generateDesignMd(config) {
|
|
1559
|
+
const starter = readTemplate(`design-md/${config.designSystem}.md`);
|
|
1560
|
+
return fill(starter, { PROJECT_NAME: config.projectName });
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/generators/decisions-md.ts
|
|
1564
|
+
function generateDecisionsMd(config, today) {
|
|
1565
|
+
const frameworks = config.frameworks.map((f) => FRAMEWORK_LABELS[f]).join(", ") || "None";
|
|
1566
|
+
return `# Decisions — ${config.projectName}
|
|
1567
|
+
|
|
1568
|
+
> The *why* behind the design system. Tokens live in \`DESIGN.md\`; conventions live in \`CLAUDE.md\`; this file records the reasoning so Claude (and you) keep context over time.
|
|
1569
|
+
|
|
1570
|
+
## How to use this log
|
|
1571
|
+
Add an entry whenever you make a design decision that future-you would otherwise have to reverse-engineer: a token choice, a tradeoff, a deviation from a default, a deliberate inconsistency. Newest at the top.
|
|
1572
|
+
|
|
1573
|
+
---
|
|
1574
|
+
|
|
1575
|
+
## ${today} — Initialized with designagent
|
|
1576
|
+
|
|
1577
|
+
- **Canvas:** ${CANVAS_LABELS[config.canvas]}${config.figmaUrl ? ` (${config.figmaUrl})` : ""}
|
|
1578
|
+
- **Design system:** ${DESIGN_SYSTEM_LABELS[config.designSystem]}
|
|
1579
|
+
- **Framework(s):** ${frameworks}
|
|
1580
|
+
|
|
1581
|
+
Set up the three-file design context: \`DESIGN.md\` (tokens), \`CLAUDE.md\` (intelligence), \`DECISIONS.md\` (this log). Starting tokens are the ${DESIGN_SYSTEM_LABELS[config.designSystem]} defaults — replace with brand values and record notable choices here.
|
|
1582
|
+
`;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/generators/mcp-config.ts
|
|
1586
|
+
function generateMcpConfig(config) {
|
|
1587
|
+
const mcpServers = {};
|
|
1588
|
+
switch (config.canvas) {
|
|
1589
|
+
case "figma":
|
|
1590
|
+
mcpServers.figma = {
|
|
1591
|
+
command: "npx",
|
|
1592
|
+
args: ["-y", "figma-developer-mcp", "--stdio"],
|
|
1593
|
+
env: { FIGMA_API_KEY: "${FIGMA_API_KEY}" }
|
|
1594
|
+
};
|
|
1595
|
+
break;
|
|
1596
|
+
case "pencil":
|
|
1597
|
+
mcpServers.pencil = {
|
|
1598
|
+
command: "npx",
|
|
1599
|
+
args: ["-y", "pencil-mcp"]
|
|
1600
|
+
};
|
|
1601
|
+
break;
|
|
1602
|
+
case "openpencil":
|
|
1603
|
+
case "none":
|
|
1604
|
+
default:
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
return JSON.stringify({ mcpServers }, null, 2) + `
|
|
1608
|
+
`;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/lint.ts
|
|
1612
|
+
import { spawnSync } from "node:child_process";
|
|
1613
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1614
|
+
function resolveCliEntry() {
|
|
1615
|
+
try {
|
|
1616
|
+
const url = import.meta.resolve("@google/design.md");
|
|
1617
|
+
return fileURLToPath2(url);
|
|
1618
|
+
} catch {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
function lintDesignMd(filePath) {
|
|
1623
|
+
const cliEntry = resolveCliEntry();
|
|
1624
|
+
if (!cliEntry) {
|
|
1625
|
+
return {
|
|
1626
|
+
ok: false,
|
|
1627
|
+
skipped: true,
|
|
1628
|
+
output: "@google/design.md not found — skipping lint."
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const result = spawnSync(process.execPath, [cliEntry, "lint", filePath], {
|
|
1632
|
+
encoding: "utf8"
|
|
1633
|
+
});
|
|
1634
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
1635
|
+
return { ok: result.status === 0, skipped: false, output };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/skills.ts
|
|
1639
|
+
import { cpSync, existsSync as existsSync4, mkdirSync, readdirSync as readdirSync2 } from "node:fs";
|
|
1640
|
+
import { homedir } from "node:os";
|
|
1641
|
+
import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
|
|
1642
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1643
|
+
var here2 = dirname2(fileURLToPath3(import.meta.url));
|
|
1644
|
+
function resolveSkillsDir() {
|
|
1645
|
+
const candidates = [
|
|
1646
|
+
join5(here2, "skills"),
|
|
1647
|
+
resolve2(here2, "..", "..", "skills")
|
|
1648
|
+
];
|
|
1649
|
+
return candidates.find((dir) => existsSync4(dir)) ?? null;
|
|
1650
|
+
}
|
|
1651
|
+
function skillsInstallDir() {
|
|
1652
|
+
return join5(homedir(), ".designagent", "skills");
|
|
1653
|
+
}
|
|
1654
|
+
function installSkills() {
|
|
1655
|
+
const src = resolveSkillsDir();
|
|
1656
|
+
if (!src)
|
|
1657
|
+
return [];
|
|
1658
|
+
const dest = skillsInstallDir();
|
|
1659
|
+
mkdirSync(dest, { recursive: true });
|
|
1660
|
+
const installed = [];
|
|
1661
|
+
for (const entry of readdirSync2(src, { withFileTypes: true })) {
|
|
1662
|
+
if (!entry.isDirectory())
|
|
1663
|
+
continue;
|
|
1664
|
+
cpSync(join5(src, entry.name), join5(dest, entry.name), { recursive: true });
|
|
1665
|
+
installed.push(entry.name);
|
|
1666
|
+
}
|
|
1667
|
+
return installed.sort();
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// src/scaffold.ts
|
|
1671
|
+
var AUGMENT_MARKER = "<!-- designagent:begin -->";
|
|
1672
|
+
function normalizeOptions(opts) {
|
|
1673
|
+
return typeof opts === "string" ? { today: opts } : opts;
|
|
1674
|
+
}
|
|
1675
|
+
function scaffold(config, cwd, optsOrToday) {
|
|
1676
|
+
const { today, designMd: designMdOverride, mode = "fresh" } = normalizeOptions(optsOrToday);
|
|
1677
|
+
const designMd = designMdOverride ?? generateDesignMd(config);
|
|
1678
|
+
const claudeMd = generateClaudeMd(config);
|
|
1679
|
+
const decisionsMd = generateDecisionsMd(config, today);
|
|
1680
|
+
const mcpConfig = generateMcpConfig(config);
|
|
1681
|
+
const designMdPath = join6(cwd, "DESIGN.md");
|
|
1682
|
+
const claudeMdPath = join6(cwd, "CLAUDE.md");
|
|
1683
|
+
const decisionsMdPath = join6(cwd, "DECISIONS.md");
|
|
1684
|
+
const mcpPath = join6(cwd, ".mcp.json");
|
|
1685
|
+
const backedUp = [];
|
|
1686
|
+
let claudeAugmented = false;
|
|
1687
|
+
if (mode === "fresh") {
|
|
1688
|
+
const backupDir = join6(cwd, ".designagent", "backup", today);
|
|
1689
|
+
for (const [path, name] of [
|
|
1690
|
+
[designMdPath, "DESIGN.md"],
|
|
1691
|
+
[claudeMdPath, "CLAUDE.md"],
|
|
1692
|
+
[decisionsMdPath, "DECISIONS.md"]
|
|
1693
|
+
]) {
|
|
1694
|
+
if (existsSync5(path)) {
|
|
1695
|
+
mkdirSync2(backupDir, { recursive: true });
|
|
1696
|
+
copyFileSync(path, join6(backupDir, name));
|
|
1697
|
+
backedUp.push(name);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
writeFileSync(designMdPath, designMd, "utf8");
|
|
1701
|
+
writeFileSync(claudeMdPath, claudeMd, "utf8");
|
|
1702
|
+
writeFileSync(decisionsMdPath, decisionsMd, "utf8");
|
|
1703
|
+
} else {
|
|
1704
|
+
if (!existsSync5(designMdPath))
|
|
1705
|
+
writeFileSync(designMdPath, designMd, "utf8");
|
|
1706
|
+
if (!existsSync5(decisionsMdPath))
|
|
1707
|
+
writeFileSync(decisionsMdPath, decisionsMd, "utf8");
|
|
1708
|
+
if (!existsSync5(claudeMdPath)) {
|
|
1709
|
+
writeFileSync(claudeMdPath, claudeMd, "utf8");
|
|
1710
|
+
} else {
|
|
1711
|
+
const current = readFileSync12(claudeMdPath, "utf8");
|
|
1712
|
+
if (!current.includes(AUGMENT_MARKER)) {
|
|
1713
|
+
const block = `
|
|
1714
|
+
|
|
1715
|
+
${AUGMENT_MARKER}
|
|
1716
|
+
${claudeMd}
|
|
1717
|
+
<!-- designagent:end -->
|
|
1718
|
+
`;
|
|
1719
|
+
writeFileSync(claudeMdPath, current + block, "utf8");
|
|
1720
|
+
claudeAugmented = true;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
let mcpWritten = false;
|
|
1725
|
+
if (mcpConfig) {
|
|
1726
|
+
const shouldWrite = mode === "fresh" || !existsSync5(mcpPath);
|
|
1727
|
+
if (shouldWrite) {
|
|
1728
|
+
writeFileSync(mcpPath, mcpConfig, "utf8");
|
|
1729
|
+
mcpWritten = true;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
const configDir = join6(cwd, ".designagent");
|
|
1733
|
+
mkdirSync2(configDir, { recursive: true });
|
|
1734
|
+
writeFileSync(join6(configDir, "config.json"), JSON.stringify(config, null, 2) + `
|
|
1735
|
+
`, "utf8");
|
|
1736
|
+
const lint = lintDesignMd(designMdPath);
|
|
1737
|
+
const installedSkills = installSkills();
|
|
1738
|
+
return {
|
|
1739
|
+
designMdPath,
|
|
1740
|
+
mcpWritten,
|
|
1741
|
+
lint,
|
|
1742
|
+
installedSkills,
|
|
1743
|
+
backedUp,
|
|
1744
|
+
claudeAugmented
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
function migrateLowercaseDesignMd(cwd) {
|
|
1748
|
+
const lower = join6(cwd, "design.md");
|
|
1749
|
+
if (!existsSync5(lower))
|
|
1750
|
+
return null;
|
|
1751
|
+
const bak = join6(cwd, ".design.md.bak");
|
|
1752
|
+
renameSync(lower, bak);
|
|
1753
|
+
return bak;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/detect.ts
|
|
1757
|
+
import { readdirSync as readdirSync3 } from "node:fs";
|
|
1758
|
+
function detectExisting(cwd) {
|
|
1759
|
+
let entries = [];
|
|
1760
|
+
try {
|
|
1761
|
+
entries = readdirSync3(cwd, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
1762
|
+
} catch {}
|
|
1763
|
+
const names = new Set(entries);
|
|
1764
|
+
const claudeMd = names.has("CLAUDE.md");
|
|
1765
|
+
const designMd = names.has("DESIGN.md");
|
|
1766
|
+
const designMdLower = !designMd && names.has("design.md");
|
|
1767
|
+
const decisionsMd = names.has("DECISIONS.md");
|
|
1768
|
+
const mcp = names.has(".mcp.json");
|
|
1769
|
+
return {
|
|
1770
|
+
claudeMd,
|
|
1771
|
+
designMd,
|
|
1772
|
+
designMdLower,
|
|
1773
|
+
decisionsMd,
|
|
1774
|
+
mcp,
|
|
1775
|
+
hasAny: claudeMd || designMd || designMdLower || decisionsMd,
|
|
1776
|
+
migratable: designMdLower
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/commands/init.ts
|
|
1781
|
+
function bail() {
|
|
1782
|
+
cancel("Setup cancelled.");
|
|
1783
|
+
process.exit(0);
|
|
1784
|
+
}
|
|
1785
|
+
function validateFigmaUrl(value) {
|
|
1786
|
+
if (!value)
|
|
1787
|
+
return "Paste a Figma file URL.";
|
|
1788
|
+
if (!/figma\.com\/(file|design)\//.test(value)) {
|
|
1789
|
+
return "That doesn't look like a Figma file URL (expected figma.com/design/... or /file/...).";
|
|
1790
|
+
}
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
var SKILLS_FOR_SUMMARY = ["design-to-code", "a11y-check", "token-sync"];
|
|
1794
|
+
function mapDetectedDesignSystem(detected) {
|
|
1795
|
+
if (detected === "tailwind" || detected === "shadcn")
|
|
1796
|
+
return detected;
|
|
1797
|
+
return "custom";
|
|
1798
|
+
}
|
|
1799
|
+
function mapDetectedCanvas(detected) {
|
|
1800
|
+
if (detected === "figma" || detected === "pencil")
|
|
1801
|
+
return detected;
|
|
1802
|
+
return "none";
|
|
1803
|
+
}
|
|
1804
|
+
function scanSummary(report) {
|
|
1805
|
+
const { colors, spacing, radius, typography, components, drift, detected } = report;
|
|
1806
|
+
const gridLine = spacing.grid ? `${spacing.values.length} (${spacing.values.length - spacing.outliers.length} on ${spacing.grid}pt grid, ${spacing.outliers.length} off)` : `${spacing.values.length}`;
|
|
1807
|
+
const lines = [
|
|
1808
|
+
`Colors found ${colors.raw} ${pc.dim(`(${colors.clusters.length} tokens after clustering)`)}`,
|
|
1809
|
+
`Typography found ${typography.length}`,
|
|
1810
|
+
`Spacing values ${gridLine}`,
|
|
1811
|
+
`Border radius ${radius.values.length}`,
|
|
1812
|
+
`Components found ${components.length}`,
|
|
1813
|
+
drift.length > 0 ? pc.yellow(`Hardcoded values ${drift.length} ← drift detected`) : pc.green("Hardcoded values 0")
|
|
1814
|
+
];
|
|
1815
|
+
if (detected.designSystem)
|
|
1816
|
+
lines.push(pc.dim(`Design system ${detected.designSystem} detected`));
|
|
1817
|
+
if (detected.canvas)
|
|
1818
|
+
lines.push(pc.dim(`Canvas ${detected.canvas} config found`));
|
|
1819
|
+
return lines.join(`
|
|
1820
|
+
`);
|
|
1821
|
+
}
|
|
1822
|
+
async function init() {
|
|
1823
|
+
console.log("");
|
|
1824
|
+
intro(pc.bold(pc.cyan("Design Agent")) + pc.dim(" — reading your codebase"));
|
|
1825
|
+
const cwd = process.cwd();
|
|
1826
|
+
const existing = detectExisting(cwd);
|
|
1827
|
+
let mode = "fresh";
|
|
1828
|
+
let doMigrate = false;
|
|
1829
|
+
if (existing.hasAny) {
|
|
1830
|
+
const found = [
|
|
1831
|
+
existing.claudeMd && "CLAUDE.md",
|
|
1832
|
+
existing.designMd && "DESIGN.md",
|
|
1833
|
+
existing.designMdLower && "design.md (non-standard)",
|
|
1834
|
+
existing.decisionsMd && "DECISIONS.md"
|
|
1835
|
+
].filter(Boolean);
|
|
1836
|
+
note(found.join(`
|
|
1837
|
+
`), "Existing files detected");
|
|
1838
|
+
const choice = await select({
|
|
1839
|
+
message: "What should DesignAgent do?",
|
|
1840
|
+
options: [
|
|
1841
|
+
{
|
|
1842
|
+
value: "augment",
|
|
1843
|
+
label: "Augment",
|
|
1844
|
+
hint: "add DesignAgent without overwriting anything"
|
|
1845
|
+
},
|
|
1846
|
+
...existing.migratable ? [
|
|
1847
|
+
{
|
|
1848
|
+
value: "migrate",
|
|
1849
|
+
label: "Migrate",
|
|
1850
|
+
hint: "convert design.md → DESIGN.md (keeps a .bak)"
|
|
1851
|
+
}
|
|
1852
|
+
] : [],
|
|
1853
|
+
{
|
|
1854
|
+
value: "fresh",
|
|
1855
|
+
label: "Fresh",
|
|
1856
|
+
hint: "start clean (backs up originals first)"
|
|
1857
|
+
},
|
|
1858
|
+
{ value: "cancel", label: "Cancel" }
|
|
1859
|
+
],
|
|
1860
|
+
initialValue: "augment"
|
|
1861
|
+
});
|
|
1862
|
+
if (isCancel(choice) || choice === "cancel")
|
|
1863
|
+
bail();
|
|
1864
|
+
if (choice === "migrate") {
|
|
1865
|
+
doMigrate = true;
|
|
1866
|
+
mode = "fresh";
|
|
1867
|
+
} else {
|
|
1868
|
+
mode = choice;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
const scanSpin = spinner();
|
|
1872
|
+
scanSpin.start("Scanning your codebase");
|
|
1873
|
+
const report = scan(cwd);
|
|
1874
|
+
scanSpin.stop(`Scanned — ${report.colors.raw} colors, ${report.components.length} components, ${report.drift.length} drift`);
|
|
1875
|
+
note(scanSummary(report), "What we found");
|
|
1876
|
+
const answers = await group({
|
|
1877
|
+
canvas: () => select({
|
|
1878
|
+
message: "What's your canvas?",
|
|
1879
|
+
options: [
|
|
1880
|
+
{ value: "figma", label: "Figma" },
|
|
1881
|
+
{ value: "pencil", label: "Pencil" },
|
|
1882
|
+
{ value: "openpencil", label: "OpenPencil" },
|
|
1883
|
+
{ value: "none", label: "None / code only" }
|
|
1884
|
+
],
|
|
1885
|
+
initialValue: mapDetectedCanvas(report.detected.canvas)
|
|
1886
|
+
}),
|
|
1887
|
+
figmaUrl: ({ results }) => results.canvas === "figma" ? text({
|
|
1888
|
+
message: "Paste your Figma file URL",
|
|
1889
|
+
placeholder: "https://figma.com/design/…",
|
|
1890
|
+
validate: validateFigmaUrl
|
|
1891
|
+
}) : Promise.resolve(undefined),
|
|
1892
|
+
designSystem: () => select({
|
|
1893
|
+
message: "What's your design system?",
|
|
1894
|
+
options: [
|
|
1895
|
+
{ value: "tailwind", label: "Tailwind" },
|
|
1896
|
+
{ value: "shadcn", label: "shadcn/ui" },
|
|
1897
|
+
{ value: "custom", label: "Custom" }
|
|
1898
|
+
],
|
|
1899
|
+
initialValue: mapDetectedDesignSystem(report.detected.designSystem)
|
|
1900
|
+
}),
|
|
1901
|
+
frameworks: () => multiselect({
|
|
1902
|
+
message: "Component framework? (space to select, enter to confirm)",
|
|
1903
|
+
options: [
|
|
1904
|
+
{ value: "react", label: "React", hint: "Web" },
|
|
1905
|
+
{ value: "vue", label: "Vue", hint: "Web" },
|
|
1906
|
+
{ value: "svelte", label: "Svelte", hint: "Web" },
|
|
1907
|
+
{ value: "react-native", label: "React Native", hint: "Cross-platform" },
|
|
1908
|
+
{ value: "swiftui", label: "SwiftUI", hint: "Native mobile" },
|
|
1909
|
+
{ value: "compose", label: "Jetpack Compose", hint: "Native mobile" },
|
|
1910
|
+
{ value: "none", label: "None / other" }
|
|
1911
|
+
],
|
|
1912
|
+
initialValues: ["react"],
|
|
1913
|
+
required: true
|
|
1914
|
+
}),
|
|
1915
|
+
projectName: () => text({
|
|
1916
|
+
message: "Project name?",
|
|
1917
|
+
placeholder: basename5(cwd),
|
|
1918
|
+
defaultValue: basename5(cwd)
|
|
1919
|
+
})
|
|
1920
|
+
}, { onCancel: bail });
|
|
1921
|
+
const config = {
|
|
1922
|
+
projectName: answers.projectName || basename5(cwd),
|
|
1923
|
+
canvas: answers.canvas,
|
|
1924
|
+
figmaUrl: answers.figmaUrl ?? null,
|
|
1925
|
+
designSystem: answers.designSystem,
|
|
1926
|
+
frameworks: answers.frameworks
|
|
1927
|
+
};
|
|
1928
|
+
const willWriteDesignMd = mode === "fresh" || !existing.designMd;
|
|
1929
|
+
const scanHasTokens = report.colors.clusters.length > 0;
|
|
1930
|
+
let designMdOverride;
|
|
1931
|
+
if (willWriteDesignMd && scanHasTokens) {
|
|
1932
|
+
const useScan = await confirm({
|
|
1933
|
+
message: `Generate DESIGN.md from what we found? ${pc.dim(`(${report.colors.clusters.length} tokens, ${report.drift.length} drift)`)}`,
|
|
1934
|
+
initialValue: true
|
|
1935
|
+
});
|
|
1936
|
+
if (isCancel(useScan))
|
|
1937
|
+
bail();
|
|
1938
|
+
if (useScan)
|
|
1939
|
+
designMdOverride = renderDesignMd(report, config.projectName);
|
|
1940
|
+
}
|
|
1941
|
+
if (doMigrate)
|
|
1942
|
+
migrateLowercaseDesignMd(cwd);
|
|
1943
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1944
|
+
const s = spinner();
|
|
1945
|
+
s.start("Building your design system");
|
|
1946
|
+
const result = scaffold(config, cwd, {
|
|
1947
|
+
today,
|
|
1948
|
+
mode,
|
|
1949
|
+
designMd: designMdOverride
|
|
1950
|
+
});
|
|
1951
|
+
s.stop("Design system ready");
|
|
1952
|
+
const lintLine = result.lint.skipped ? pc.yellow("DESIGN.md written (lint skipped — @google/design.md not installed)") : result.lint.ok ? pc.green(`DESIGN.md ${designMdOverride ? "reverse-engineered, linted clean" : "linted clean"}`) : pc.red("DESIGN.md written, but lint reported issues (see below)");
|
|
1953
|
+
const skillList = (result.installedSkills.length ? result.installedSkills : SKILLS_FOR_SUMMARY).join(", ");
|
|
1954
|
+
const claudeLine = result.claudeAugmented ? pc.green("CLAUDE.md augmented (existing file preserved)") : pc.green(`CLAUDE.md ${config.frameworks.length ? frameworkSummary(config.frameworks) : "design intelligence"}`);
|
|
1955
|
+
note([
|
|
1956
|
+
lintLine,
|
|
1957
|
+
claudeLine,
|
|
1958
|
+
pc.green("DECISIONS.md created"),
|
|
1959
|
+
result.mcpWritten ? pc.green(`.mcp.json ${config.canvas} MCP configured`) : pc.dim(".mcp.json not changed"),
|
|
1960
|
+
pc.green(`Skills ${skillList}`),
|
|
1961
|
+
result.backedUp.length ? pc.dim(`Backed up ${result.backedUp.join(", ")} → .designagent/backup/${today}/`) : ""
|
|
1962
|
+
].filter(Boolean).join(`
|
|
1963
|
+
`), "Files");
|
|
1964
|
+
if (!result.lint.ok && !result.lint.skipped && result.lint.output) {
|
|
1965
|
+
note(pc.dim(result.lint.output), "Lint output");
|
|
1966
|
+
}
|
|
1967
|
+
outro(pc.bold("You're agent-ready.") + pc.dim(" Start Claude Code and build with design intelligence."));
|
|
1968
|
+
}
|
|
1969
|
+
function frameworkSummary(frameworks) {
|
|
1970
|
+
const real = frameworks.filter((f) => f !== "none");
|
|
1971
|
+
if (real.length === 0)
|
|
1972
|
+
return "tokens only";
|
|
1973
|
+
if (real.length === 1)
|
|
1974
|
+
return real[0];
|
|
1975
|
+
return `${real.length} targets`;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/commands/docs.ts
|
|
1979
|
+
import { mkdirSync as mkdirSync3, watch, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1980
|
+
import { join as join7 } from "node:path";
|
|
1981
|
+
import { spawn } from "node:child_process";
|
|
1982
|
+
import { platform } from "node:os";
|
|
1983
|
+
import pc2 from "picocolors";
|
|
1984
|
+
|
|
1985
|
+
// ../docs/dist/styles.js
|
|
1986
|
+
var STYLES = `
|
|
1987
|
+
:root {
|
|
1988
|
+
--bg: #0b0c0e;
|
|
1989
|
+
--panel: #14161a;
|
|
1990
|
+
--panel-2: #1a1d22;
|
|
1991
|
+
--border: #262a31;
|
|
1992
|
+
--text: #e8eaed;
|
|
1993
|
+
--text-2: #9aa1ab;
|
|
1994
|
+
--text-3: #6b7280;
|
|
1995
|
+
--accent: #6ea8fe;
|
|
1996
|
+
--good: #4ade80;
|
|
1997
|
+
--warn: #fbbf24;
|
|
1998
|
+
--bad: #f87171;
|
|
1999
|
+
--radius: 12px;
|
|
2000
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
2001
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
2002
|
+
}
|
|
2003
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2004
|
+
body { font-family: var(--sans); background: var(--bg); color: var(--text); line-height: 1.55; }
|
|
2005
|
+
.wrap { max-width: 960px; margin: 0 auto; padding: 48px 24px 96px; }
|
|
2006
|
+
|
|
2007
|
+
header.hero { margin-bottom: 8px; }
|
|
2008
|
+
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--text-3); }
|
|
2009
|
+
h1 { font-size: 28px; font-weight: 650; margin: 6px 0 4px; }
|
|
2010
|
+
.hero-sub { color: var(--text-2); font-size: 14px; }
|
|
2011
|
+
|
|
2012
|
+
.stats { display: flex; flex-wrap: wrap; gap: 8px; margin: 24px 0 8px; }
|
|
2013
|
+
.stat { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; min-width: 92px; }
|
|
2014
|
+
.stat-num { font-size: 20px; font-weight: 650; }
|
|
2015
|
+
.stat-label { font-size: 11px; color: var(--text-3); text-transform: uppercase; letter-spacing: .05em; }
|
|
2016
|
+
.stat.warn .stat-num { color: var(--warn); }
|
|
2017
|
+
|
|
2018
|
+
nav.toc { position: sticky; top: 0; background: rgba(11,12,14,.85); backdrop-filter: blur(8px); display: flex; gap: 4px; flex-wrap: wrap; padding: 12px 0; margin: 16px 0 8px; border-bottom: 1px solid var(--border); z-index: 10; }
|
|
2019
|
+
nav.toc a { font-size: 13px; color: var(--text-2); text-decoration: none; padding: 4px 10px; border-radius: 8px; }
|
|
2020
|
+
nav.toc a:hover { background: var(--panel-2); color: var(--text); }
|
|
2021
|
+
|
|
2022
|
+
section { padding: 32px 0; border-bottom: 1px solid var(--border); }
|
|
2023
|
+
section:last-child { border-bottom: none; }
|
|
2024
|
+
h2 { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
|
|
2025
|
+
h3 { font-size: 14px; font-weight: 600; margin: 20px 0 10px; color: var(--text-2); }
|
|
2026
|
+
h3 .count { color: var(--text-3); font-weight: 500; }
|
|
2027
|
+
.section-sub { color: var(--text-2); font-size: 13px; margin-bottom: 16px; }
|
|
2028
|
+
.empty { color: var(--text-3); font-size: 14px; padding: 16px 0; }
|
|
2029
|
+
.empty.good { color: var(--good); }
|
|
2030
|
+
.muted { color: var(--text-3); }
|
|
2031
|
+
code { font-family: var(--mono); font-size: 12px; background: var(--panel-2); padding: 1px 6px; border-radius: 5px; }
|
|
2032
|
+
.badge { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; background: var(--panel-2); color: var(--text-3); padding: 1px 5px; border-radius: 4px; vertical-align: middle; }
|
|
2033
|
+
.badge.warn { background: rgba(251,191,36,.15); color: var(--warn); }
|
|
2034
|
+
|
|
2035
|
+
/* Colors */
|
|
2036
|
+
.swatch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
|
|
2037
|
+
.swatch { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
2038
|
+
.swatch-chip { height: 88px; display: flex; align-items: flex-end; padding: 8px; }
|
|
2039
|
+
.swatch-ratio { font-size: 11px; font-family: var(--mono); opacity: .9; }
|
|
2040
|
+
.swatch-ratio em { font-style: normal; font-weight: 600; }
|
|
2041
|
+
.swatch-meta { padding: 10px 12px; }
|
|
2042
|
+
.swatch-name { font-family: var(--mono); font-size: 12px; font-weight: 600; }
|
|
2043
|
+
.swatch-hex { font-family: var(--mono); font-size: 12px; color: var(--text-2); text-transform: uppercase; }
|
|
2044
|
+
.swatch-count { font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
2045
|
+
.swatch-dupes { font-size: 10px; color: var(--warn); margin-top: 4px; }
|
|
2046
|
+
|
|
2047
|
+
/* Drift */
|
|
2048
|
+
.drift-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
2049
|
+
.drift-table th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
|
2050
|
+
.drift-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
2051
|
+
.drift-line { color: var(--text-3); }
|
|
2052
|
+
.drift-chip { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; border: 1px solid var(--border); }
|
|
2053
|
+
.suggest { color: var(--good); background: rgba(74,222,128,.1); }
|
|
2054
|
+
|
|
2055
|
+
/* Spacing ruler */
|
|
2056
|
+
.ruler { display: flex; flex-direction: column; gap: 6px; }
|
|
2057
|
+
.ruler-row { display: grid; grid-template-columns: 120px 1fr; align-items: center; gap: 12px; }
|
|
2058
|
+
.ruler-label { font-family: var(--mono); font-size: 12px; color: var(--text-2); }
|
|
2059
|
+
.ruler-track { background: var(--panel); border-radius: 6px; height: 18px; overflow: hidden; }
|
|
2060
|
+
.ruler-bar { height: 100%; background: var(--accent); border-radius: 6px; }
|
|
2061
|
+
.ruler-row.off .ruler-bar { background: var(--warn); }
|
|
2062
|
+
|
|
2063
|
+
/* Typography */
|
|
2064
|
+
.type-list { display: flex; flex-direction: column; gap: 4px; }
|
|
2065
|
+
.type-row { display: grid; grid-template-columns: 80px 1fr; align-items: center; gap: 16px; padding: 10px 12px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; }
|
|
2066
|
+
.type-preview { font-size: 28px; line-height: 1; text-align: center; }
|
|
2067
|
+
.type-name { font-family: var(--mono); font-size: 13px; font-weight: 600; }
|
|
2068
|
+
.type-props { font-size: 12px; color: var(--text-2); }
|
|
2069
|
+
|
|
2070
|
+
/* Components */
|
|
2071
|
+
.comp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; }
|
|
2072
|
+
.comp { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; }
|
|
2073
|
+
.comp-name { font-size: 14px; font-weight: 600; }
|
|
2074
|
+
.comp-file { font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
2075
|
+
.comp-file code { background: none; padding: 0; }
|
|
2076
|
+
|
|
2077
|
+
footer { margin-top: 48px; color: var(--text-3); font-size: 12px; text-align: center; }
|
|
2078
|
+
footer a { color: var(--text-2); }
|
|
2079
|
+
`;
|
|
2080
|
+
|
|
2081
|
+
// ../docs/dist/util.js
|
|
2082
|
+
function esc(value) {
|
|
2083
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2084
|
+
}
|
|
2085
|
+
function hexToRgb2(hex) {
|
|
2086
|
+
const h = hex.replace("#", "");
|
|
2087
|
+
return {
|
|
2088
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
2089
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
2090
|
+
b: parseInt(h.slice(4, 6), 16)
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function srgbToLinear2(c) {
|
|
2094
|
+
const v = c / 255;
|
|
2095
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
2096
|
+
}
|
|
2097
|
+
function relativeLuminance2(hex) {
|
|
2098
|
+
const { r, g, b } = hexToRgb2(hex);
|
|
2099
|
+
return 0.2126 * srgbToLinear2(r) + 0.7152 * srgbToLinear2(g) + 0.0722 * srgbToLinear2(b);
|
|
2100
|
+
}
|
|
2101
|
+
function contrastRatio2(a, b) {
|
|
2102
|
+
const la = relativeLuminance2(a);
|
|
2103
|
+
const lb = relativeLuminance2(b);
|
|
2104
|
+
const hi = Math.max(la, lb);
|
|
2105
|
+
const lo = Math.min(la, lb);
|
|
2106
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
2107
|
+
}
|
|
2108
|
+
function readableText(bgHex) {
|
|
2109
|
+
return contrastRatio2(bgHex, "#ffffff") >= contrastRatio2(bgHex, "#111111") ? "#ffffff" : "#111111";
|
|
2110
|
+
}
|
|
2111
|
+
function wcagRating(ratio) {
|
|
2112
|
+
if (ratio >= 7)
|
|
2113
|
+
return { label: "AAA", pass: true };
|
|
2114
|
+
if (ratio >= 4.5)
|
|
2115
|
+
return { label: "AA", pass: true };
|
|
2116
|
+
if (ratio >= 3)
|
|
2117
|
+
return { label: "AA large", pass: false };
|
|
2118
|
+
return { label: "Fail", pass: false };
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// ../docs/dist/sections/colors.js
|
|
2122
|
+
function renderColors(report) {
|
|
2123
|
+
const clusters = report.colors.clusters;
|
|
2124
|
+
if (clusters.length === 0) {
|
|
2125
|
+
return section("colors", "Colors", `<p class="empty">No colors found in the codebase.</p>`);
|
|
2126
|
+
}
|
|
2127
|
+
const cards = clusters.map((c) => {
|
|
2128
|
+
const text2 = readableText(c.hex);
|
|
2129
|
+
const ratio = contrastRatio2(c.hex, text2);
|
|
2130
|
+
const rating = wcagRating(ratio);
|
|
2131
|
+
const dupes = c.members.length > 1 ? `<div class="swatch-dupes" title="Near-duplicates merged into this token">+${c.members.length - 1} near-duplicate${c.members.length - 1 > 1 ? "s" : ""}</div>` : "";
|
|
2132
|
+
return `
|
|
2133
|
+
<div class="swatch">
|
|
2134
|
+
<div class="swatch-chip" style="background:${esc(c.hex)};color:${text2}">
|
|
2135
|
+
<span class="swatch-ratio" title="Contrast of ${text2 === "#ffffff" ? "white" : "black"} text">${ratio.toFixed(1)}:1 <em>${rating.label}</em></span>
|
|
2136
|
+
</div>
|
|
2137
|
+
<div class="swatch-meta">
|
|
2138
|
+
<div class="swatch-name">colors.${esc(c.name)}${c.named ? "" : ' <span class="badge">auto</span>'}</div>
|
|
2139
|
+
<div class="swatch-hex">${esc(c.hex)}</div>
|
|
2140
|
+
<div class="swatch-count">${c.count} use${c.count === 1 ? "" : "s"}</div>
|
|
2141
|
+
${dupes}
|
|
2142
|
+
</div>
|
|
2143
|
+
</div>`;
|
|
2144
|
+
}).join("");
|
|
2145
|
+
const summary = `<p class="section-sub">${report.colors.raw} unique colors → ${clusters.length} tokens after clustering near-duplicates.</p>`;
|
|
2146
|
+
return section("colors", "Colors", summary + `<div class="swatch-grid">${cards}</div>`);
|
|
2147
|
+
}
|
|
2148
|
+
function section(id, title, body) {
|
|
2149
|
+
return `<section id="${id}"><h2>${title}</h2>${body}</section>`;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// ../docs/dist/sections/drift.js
|
|
2153
|
+
function renderDrift(report) {
|
|
2154
|
+
const drift = report.drift;
|
|
2155
|
+
if (drift.length === 0) {
|
|
2156
|
+
return section2("drift", "Drift report", `<p class="empty good">✓ No hardcoded values detected. Everything references a token.</p>`);
|
|
2157
|
+
}
|
|
2158
|
+
const colorDrift = drift.filter((d) => d.kind === "color");
|
|
2159
|
+
const spacingDrift = drift.filter((d) => d.kind === "spacing");
|
|
2160
|
+
const rows = (items) => items.map((d) => `
|
|
2161
|
+
<tr>
|
|
2162
|
+
<td class="drift-loc"><code>${esc(d.file)}</code><span class="drift-line">:${d.line}</span></td>
|
|
2163
|
+
<td class="drift-val">${d.kind === "color" ? `<span class="drift-chip" style="background:${esc(d.value)}"></span>` : ""}<code>${esc(d.value)}</code></td>
|
|
2164
|
+
<td class="drift-fix">${d.suggestion ? `<code class="suggest">${esc(d.suggestion)}</code>` : `<span class="muted">no close token — consider adding one</span>`}</td>
|
|
2165
|
+
</tr>`).join("");
|
|
2166
|
+
const table = (label, items) => items.length ? `<h3>${label} <span class="count">${items.length}</span></h3>
|
|
2167
|
+
<table class="drift-table">
|
|
2168
|
+
<thead><tr><th>Location</th><th>Hardcoded value</th><th>Suggested token</th></tr></thead>
|
|
2169
|
+
<tbody>${rows(items)}</tbody>
|
|
2170
|
+
</table>` : "";
|
|
2171
|
+
const summary = `<p class="section-sub"><strong>${drift.length}</strong> hardcoded value${drift.length === 1 ? "" : "s"} that should reference a token — ${colorDrift.length} color, ${spacingDrift.length} spacing.</p>`;
|
|
2172
|
+
return section2("drift", "Drift report", summary + table("Colors", colorDrift) + table("Spacing", spacingDrift));
|
|
2173
|
+
}
|
|
2174
|
+
function section2(id, title, body) {
|
|
2175
|
+
return `<section id="${id}"><h2>${title}</h2>${body}</section>`;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// ../docs/dist/sections/spacing.js
|
|
2179
|
+
function renderSpacing(report) {
|
|
2180
|
+
const { values, grid, outliers } = report.spacing;
|
|
2181
|
+
if (values.length === 0) {
|
|
2182
|
+
return section3("spacing", "Spacing", `<p class="empty">No spacing scale detected.</p>`);
|
|
2183
|
+
}
|
|
2184
|
+
const outlierSet = new Set(outliers);
|
|
2185
|
+
const max = Math.max(...values, 1);
|
|
2186
|
+
const bars = values.map((v) => {
|
|
2187
|
+
const off = outlierSet.has(v);
|
|
2188
|
+
const width = Math.max(2, Math.round(v / max * 100));
|
|
2189
|
+
return `
|
|
2190
|
+
<div class="ruler-row${off ? " off" : ""}">
|
|
2191
|
+
<div class="ruler-label">${v}px${off ? ' <span class="badge warn">off-grid</span>' : ""}</div>
|
|
2192
|
+
<div class="ruler-track"><div class="ruler-bar" style="width:${width}%"></div></div>
|
|
2193
|
+
</div>`;
|
|
2194
|
+
}).join("");
|
|
2195
|
+
const gridNote = grid ? `<p class="section-sub">Detected an <strong>${grid}pt grid</strong>${outliers.length ? ` — ${outliers.length} value${outliers.length === 1 ? "" : "s"} off the grid.` : " — all values aligned."}</p>` : `<p class="section-sub">No consistent grid detected.</p>`;
|
|
2196
|
+
return section3("spacing", "Spacing", gridNote + `<div class="ruler">${bars}</div>`);
|
|
2197
|
+
}
|
|
2198
|
+
function section3(id, title, body) {
|
|
2199
|
+
return `<section id="${id}"><h2>${title}</h2>${body}</section>`;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// ../docs/dist/sections/typography.js
|
|
2203
|
+
function renderTypography(report) {
|
|
2204
|
+
const type = report.typography;
|
|
2205
|
+
if (type.length === 0) {
|
|
2206
|
+
return section4("typography", "Typography", `<p class="empty">No typography tokens detected in the codebase.</p>`);
|
|
2207
|
+
}
|
|
2208
|
+
const rows = type.map((t) => {
|
|
2209
|
+
const size = t.fontSize ?? "—";
|
|
2210
|
+
const family = t.fontFamily ?? "inherit";
|
|
2211
|
+
const preview = t.fontSize && /^\d/.test(t.fontSize) ? `<div class="type-preview" style="font-size:${esc(t.fontSize)};font-family:${esc(family)}">Ag</div>` : `<div class="type-preview muted">Ag</div>`;
|
|
2212
|
+
return `
|
|
2213
|
+
<div class="type-row">
|
|
2214
|
+
${preview}
|
|
2215
|
+
<div class="type-meta">
|
|
2216
|
+
<div class="type-name">${esc(t.name)}</div>
|
|
2217
|
+
<div class="type-props"><code>${esc(size)}</code>${t.fontFamily ? ` · ${esc(t.fontFamily)}` : ""}${t.fontWeight ? ` · ${t.fontWeight}` : ""}</div>
|
|
2218
|
+
</div>
|
|
2219
|
+
</div>`;
|
|
2220
|
+
}).join("");
|
|
2221
|
+
return section4("typography", "Typography", `<p class="section-sub">${type.length} type token${type.length === 1 ? "" : "s"} found.</p><div class="type-list">${rows}</div>`);
|
|
2222
|
+
}
|
|
2223
|
+
function section4(id, title, body) {
|
|
2224
|
+
return `<section id="${id}"><h2>${title}</h2>${body}</section>`;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
// ../docs/dist/sections/components.js
|
|
2228
|
+
function renderComponents(report) {
|
|
2229
|
+
const components = report.components;
|
|
2230
|
+
if (components.length === 0) {
|
|
2231
|
+
return section5("components", "Components", `<p class="empty">No components found.</p>`);
|
|
2232
|
+
}
|
|
2233
|
+
const seen = new Map;
|
|
2234
|
+
for (const c of components)
|
|
2235
|
+
if (!seen.has(c.name))
|
|
2236
|
+
seen.set(c.name, c.file);
|
|
2237
|
+
const items = [...seen.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([name, file]) => `
|
|
2238
|
+
<div class="comp">
|
|
2239
|
+
<div class="comp-name">${esc(name)}</div>
|
|
2240
|
+
<div class="comp-file"><code>${esc(file)}</code></div>
|
|
2241
|
+
</div>`).join("");
|
|
2242
|
+
return section5("components", "Components", `<p class="section-sub">${seen.size} component${seen.size === 1 ? "" : "s"} inventoried.</p><div class="comp-grid">${items}</div>`);
|
|
2243
|
+
}
|
|
2244
|
+
function section5(id, title, body) {
|
|
2245
|
+
return `<section id="${id}"><h2>${title}</h2>${body}</section>`;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// ../docs/dist/renderer.js
|
|
2249
|
+
function renderDocs(report, options) {
|
|
2250
|
+
const { projectName, generatedAt } = options;
|
|
2251
|
+
const stat = (num, label, warn = false) => `<div class="stat${warn ? " warn" : ""}"><div class="stat-num">${esc(String(num))}</div><div class="stat-label">${esc(label)}</div></div>`;
|
|
2252
|
+
const stats = [
|
|
2253
|
+
stat(report.colors.clusters.length, "color tokens"),
|
|
2254
|
+
stat(report.spacing.values.length, "spacing"),
|
|
2255
|
+
stat(report.typography.length, "type"),
|
|
2256
|
+
stat(report.components.length, "components"),
|
|
2257
|
+
stat(report.drift.length, "drift", report.drift.length > 0)
|
|
2258
|
+
].join("");
|
|
2259
|
+
const toc = [
|
|
2260
|
+
["colors", "Colors"],
|
|
2261
|
+
["drift", "Drift"],
|
|
2262
|
+
["spacing", "Spacing"],
|
|
2263
|
+
["typography", "Typography"],
|
|
2264
|
+
["components", "Components"]
|
|
2265
|
+
].map(([id, label]) => `<a href="#${id}">${label}</a>`).join("");
|
|
2266
|
+
const sections = [
|
|
2267
|
+
renderColors(report),
|
|
2268
|
+
renderDrift(report),
|
|
2269
|
+
renderSpacing(report),
|
|
2270
|
+
renderTypography(report),
|
|
2271
|
+
renderComponents(report)
|
|
2272
|
+
].join(`
|
|
2273
|
+
`);
|
|
2274
|
+
const dateLine = generatedAt ? `<span class="hero-sub">Generated ${esc(generatedAt)}</span>` : "";
|
|
2275
|
+
const reloadMeta = options.autoReloadSeconds && options.autoReloadSeconds > 0 ? `
|
|
2276
|
+
<meta http-equiv="refresh" content="${Math.round(options.autoReloadSeconds)}">` : "";
|
|
2277
|
+
return `<!doctype html>
|
|
2278
|
+
<html lang="en">
|
|
2279
|
+
<head>
|
|
2280
|
+
<meta charset="utf-8">
|
|
2281
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">${reloadMeta}
|
|
2282
|
+
<title>${esc(projectName)} — Design System</title>
|
|
2283
|
+
<style>${STYLES}</style>
|
|
2284
|
+
</head>
|
|
2285
|
+
<body>
|
|
2286
|
+
<div class="wrap">
|
|
2287
|
+
<header class="hero">
|
|
2288
|
+
<div class="eyebrow">DesignAgent · reverse-engineered</div>
|
|
2289
|
+
<h1>${esc(projectName)} — Design System</h1>
|
|
2290
|
+
<p class="hero-sub">Pulled from your codebase. ${dateLine}</p>
|
|
2291
|
+
<div class="stats">${stats}</div>
|
|
2292
|
+
</header>
|
|
2293
|
+
<nav class="toc">${toc}</nav>
|
|
2294
|
+
<main>
|
|
2295
|
+
${sections}
|
|
2296
|
+
</main>
|
|
2297
|
+
<footer>
|
|
2298
|
+
Generated by <a href="https://designagent.dev">DesignAgent</a> from <code>DESIGN.md</code> + codebase scan.
|
|
2299
|
+
</footer>
|
|
2300
|
+
</div>
|
|
2301
|
+
</body>
|
|
2302
|
+
</html>
|
|
2303
|
+
`;
|
|
2304
|
+
}
|
|
2305
|
+
// src/commands/docs.ts
|
|
2306
|
+
function buildDocs(cwd, outPath, autoReloadSeconds) {
|
|
2307
|
+
const report = scan(cwd);
|
|
2308
|
+
const projectName = cwd.split("/").filter(Boolean).pop() ?? "project";
|
|
2309
|
+
const html = renderDocs(report, { projectName, autoReloadSeconds });
|
|
2310
|
+
writeFileSync2(outPath, html, "utf8");
|
|
2311
|
+
return {
|
|
2312
|
+
drift: report.drift.length,
|
|
2313
|
+
tokens: report.colors.clusters.length,
|
|
2314
|
+
components: report.components.length
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
function openInBrowser(path) {
|
|
2318
|
+
const os = platform();
|
|
2319
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
2320
|
+
try {
|
|
2321
|
+
const child = spawn(cmd, [path], { stdio: "ignore", detached: true, shell: os === "win32" });
|
|
2322
|
+
child.on("error", () => {});
|
|
2323
|
+
child.unref();
|
|
2324
|
+
} catch {}
|
|
2325
|
+
}
|
|
2326
|
+
function docs(cwd, options = {}) {
|
|
2327
|
+
let outPath;
|
|
2328
|
+
if (options.exportStatic) {
|
|
2329
|
+
const dir = join7(cwd, "designagent-docs");
|
|
2330
|
+
mkdirSync3(dir, { recursive: true });
|
|
2331
|
+
outPath = join7(dir, "index.html");
|
|
2332
|
+
} else {
|
|
2333
|
+
const dir = join7(cwd, ".designagent");
|
|
2334
|
+
mkdirSync3(dir, { recursive: true });
|
|
2335
|
+
outPath = join7(dir, "docs.html");
|
|
2336
|
+
}
|
|
2337
|
+
const reloadSeconds = options.watch ? 2 : undefined;
|
|
2338
|
+
const stats = buildDocs(cwd, outPath, reloadSeconds);
|
|
2339
|
+
console.log(pc2.green("✓ ") + `Visual docs written to ${pc2.bold(outPath)}
|
|
2340
|
+
` + pc2.dim(` ${stats.tokens} color tokens · ${stats.components} components · ${stats.drift} drift`));
|
|
2341
|
+
if (!options.noOpen && !options.exportStatic)
|
|
2342
|
+
openInBrowser(outPath);
|
|
2343
|
+
if (options.watch) {
|
|
2344
|
+
console.log(pc2.dim(" watching for changes — Ctrl-C to stop"));
|
|
2345
|
+
let timer = null;
|
|
2346
|
+
const rebuild = () => {
|
|
2347
|
+
if (timer)
|
|
2348
|
+
clearTimeout(timer);
|
|
2349
|
+
timer = setTimeout(() => {
|
|
2350
|
+
try {
|
|
2351
|
+
const s = buildDocs(cwd, outPath, reloadSeconds);
|
|
2352
|
+
console.log(pc2.dim(` ↻ regenerated — ${s.tokens} tokens · ${s.components} components · ${s.drift} drift`));
|
|
2353
|
+
} catch {}
|
|
2354
|
+
}, 150);
|
|
2355
|
+
};
|
|
2356
|
+
try {
|
|
2357
|
+
watch(cwd, { recursive: true }, (_event, filename) => {
|
|
2358
|
+
if (!filename)
|
|
2359
|
+
return;
|
|
2360
|
+
const f = filename.toString();
|
|
2361
|
+
if (f.includes(".designagent") || f.includes("designagent-docs") || f.includes("node_modules"))
|
|
2362
|
+
return;
|
|
2363
|
+
rebuild();
|
|
2364
|
+
});
|
|
2365
|
+
} catch {
|
|
2366
|
+
console.log(pc2.yellow(" (recursive watch unsupported on this platform)"));
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return outPath;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// src/commands/studio.ts
|
|
2373
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2374
|
+
import { join as join9 } from "node:path";
|
|
2375
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2376
|
+
import { platform as platform2 } from "node:os";
|
|
2377
|
+
import pc3 from "picocolors";
|
|
2378
|
+
|
|
2379
|
+
// ../studio/dist/model.js
|
|
2380
|
+
import { basename as basename6 } from "node:path";
|
|
2381
|
+
|
|
2382
|
+
// ../studio/dist/discovery/components.js
|
|
2383
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
2384
|
+
import { join as join8 } from "node:path";
|
|
2385
|
+
|
|
2386
|
+
// ../studio/dist/discovery/props.js
|
|
2387
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2388
|
+
import { Project } from "ts-morph";
|
|
2389
|
+
function literalUnion(typeText) {
|
|
2390
|
+
if (!typeText.includes("|"))
|
|
2391
|
+
return [];
|
|
2392
|
+
const matches = typeText.match(/'[^']*'|"[^"]*"/g);
|
|
2393
|
+
if (!matches)
|
|
2394
|
+
return [];
|
|
2395
|
+
const parts = typeText.split("|").map((p) => p.trim());
|
|
2396
|
+
if (matches.length < parts.length)
|
|
2397
|
+
return [];
|
|
2398
|
+
return matches.map((m) => m.slice(1, -1));
|
|
2399
|
+
}
|
|
2400
|
+
function propsFromMembers(decl) {
|
|
2401
|
+
const props = [];
|
|
2402
|
+
const variants = [];
|
|
2403
|
+
const members = "getProperties" in decl && typeof decl.getProperties === "function" ? decl.getProperties() : [];
|
|
2404
|
+
for (const m of members) {
|
|
2405
|
+
let name;
|
|
2406
|
+
let typeText = "unknown";
|
|
2407
|
+
let required = true;
|
|
2408
|
+
let description;
|
|
2409
|
+
try {
|
|
2410
|
+
name = m.getName();
|
|
2411
|
+
const node = m.getTypeNode();
|
|
2412
|
+
typeText = node ? node.getText() : m.getType().getText();
|
|
2413
|
+
required = !m.hasQuestionToken();
|
|
2414
|
+
const docs2 = m.getJsDocs();
|
|
2415
|
+
if (docs2.length) {
|
|
2416
|
+
const d = docs2[docs2.length - 1].getDescription().trim();
|
|
2417
|
+
if (d)
|
|
2418
|
+
description = d;
|
|
2419
|
+
}
|
|
2420
|
+
} catch {
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
props.push({ name, type: typeText, required, description });
|
|
2424
|
+
const values = literalUnion(typeText);
|
|
2425
|
+
if (values.length >= 2)
|
|
2426
|
+
variants.push({ prop: name, values });
|
|
2427
|
+
}
|
|
2428
|
+
return { props, variants };
|
|
2429
|
+
}
|
|
2430
|
+
function discoverProps(absFilePath, componentName2) {
|
|
2431
|
+
if (!existsSync6(absFilePath))
|
|
2432
|
+
return { props: [], variants: [] };
|
|
2433
|
+
let source;
|
|
2434
|
+
try {
|
|
2435
|
+
const project = new Project({
|
|
2436
|
+
useInMemoryFileSystem: false,
|
|
2437
|
+
skipAddingFilesFromTsConfig: true,
|
|
2438
|
+
compilerOptions: { allowJs: true, jsx: 4 }
|
|
2439
|
+
});
|
|
2440
|
+
source = project.addSourceFileAtPath(absFilePath);
|
|
2441
|
+
} catch {
|
|
2442
|
+
return { props: [], variants: [] };
|
|
2443
|
+
}
|
|
2444
|
+
try {
|
|
2445
|
+
const wanted = `${componentName2}Props`;
|
|
2446
|
+
const iface = source.getInterface(wanted) ?? source.getInterfaces().find((i) => /props$/i.test(i.getName()));
|
|
2447
|
+
if (iface) {
|
|
2448
|
+
const out = propsFromMembers(iface);
|
|
2449
|
+
const docs2 = iface.getJsDocs();
|
|
2450
|
+
if (docs2.length) {
|
|
2451
|
+
const d = docs2[docs2.length - 1].getDescription().trim();
|
|
2452
|
+
if (d)
|
|
2453
|
+
out.description = d;
|
|
2454
|
+
}
|
|
2455
|
+
return out;
|
|
2456
|
+
}
|
|
2457
|
+
const alias = source.getTypeAlias(wanted) ?? source.getTypeAliases().find((t) => /props$/i.test(t.getName()));
|
|
2458
|
+
if (alias) {
|
|
2459
|
+
const props = [];
|
|
2460
|
+
const variants = [];
|
|
2461
|
+
const body = alias.getTypeNode()?.getText() ?? "";
|
|
2462
|
+
const memberRe = /([a-zA-Z_$][\w$]*)\s*(\?)?\s*:\s*([^;\n}]+)/g;
|
|
2463
|
+
let m;
|
|
2464
|
+
while ((m = memberRe.exec(body)) !== null) {
|
|
2465
|
+
const name = m[1];
|
|
2466
|
+
const required = !m[2];
|
|
2467
|
+
const typeText = m[3].trim();
|
|
2468
|
+
props.push({ name, type: typeText, required });
|
|
2469
|
+
const values = literalUnion(typeText);
|
|
2470
|
+
if (values.length >= 2)
|
|
2471
|
+
variants.push({ prop: name, values });
|
|
2472
|
+
}
|
|
2473
|
+
return { props, variants };
|
|
2474
|
+
}
|
|
2475
|
+
} catch {}
|
|
2476
|
+
return { props: [], variants: [] };
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// ../studio/dist/discovery/components.js
|
|
2480
|
+
var COMPONENT_DIRS = ["components/", "src/components/", "ui/", "src/ui/", "app/components/"];
|
|
2481
|
+
function isComponentFile(file) {
|
|
2482
|
+
const f = file.replace(/\\/g, "/");
|
|
2483
|
+
return COMPONENT_DIRS.some((d) => f.includes(d)) || /\.(t|j)sx$/.test(f);
|
|
2484
|
+
}
|
|
2485
|
+
function escapeRe(s) {
|
|
2486
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2487
|
+
}
|
|
2488
|
+
function bareForms(name) {
|
|
2489
|
+
const base = name.replace(/^colors\.|^spacing\.|^rounded\./, "");
|
|
2490
|
+
const forms = new Set([base]);
|
|
2491
|
+
forms.add(base.replace(/^(color|colour|spacing|space|radius|rounded)-/, ""));
|
|
2492
|
+
return [...forms].filter(Boolean);
|
|
2493
|
+
}
|
|
2494
|
+
function findTokenRefs(fileText, tokenNames) {
|
|
2495
|
+
const refs = new Set;
|
|
2496
|
+
const UTIL = "(?:bg|text|border|fill|stroke|ring|p|m|px|py|pt|pb|pl|pr|mx|my|gap|space|rounded|w|h)";
|
|
2497
|
+
for (const name of tokenNames) {
|
|
2498
|
+
for (const bare of bareForms(name)) {
|
|
2499
|
+
if (!bare || /^\d+$/.test(bare) === false && bare.length < 2)
|
|
2500
|
+
continue;
|
|
2501
|
+
const e = escapeRe(bare);
|
|
2502
|
+
const patterns = [
|
|
2503
|
+
new RegExp(`var\\(--(?:color-|colour-|spacing-|space-|radius-|rounded-)?${e}\\b`),
|
|
2504
|
+
new RegExp(`${UTIL}-${e}\\b`)
|
|
2505
|
+
];
|
|
2506
|
+
if (patterns.some((re) => re.test(fileText))) {
|
|
2507
|
+
refs.add(name);
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
return [...refs].sort();
|
|
2513
|
+
}
|
|
2514
|
+
function discoverComponents(cwd, report) {
|
|
2515
|
+
const tokenNames = [
|
|
2516
|
+
...report.colors.clusters.map((c) => `colors.${c.name}`),
|
|
2517
|
+
...report.spacing.values.map((v) => `spacing.${v}`)
|
|
2518
|
+
];
|
|
2519
|
+
const driftByFile = new Map;
|
|
2520
|
+
for (const d of report.drift) {
|
|
2521
|
+
const list = driftByFile.get(d.file) ?? [];
|
|
2522
|
+
list.push(d);
|
|
2523
|
+
driftByFile.set(d.file, list);
|
|
2524
|
+
}
|
|
2525
|
+
const out = [];
|
|
2526
|
+
for (const c of report.components) {
|
|
2527
|
+
if (!isComponentFile(c.file))
|
|
2528
|
+
continue;
|
|
2529
|
+
const abs = join8(cwd, c.file);
|
|
2530
|
+
let fileText = "";
|
|
2531
|
+
try {
|
|
2532
|
+
fileText = readFileSync13(abs, "utf8");
|
|
2533
|
+
} catch {}
|
|
2534
|
+
const { props, variants, description } = discoverProps(abs, c.name);
|
|
2535
|
+
const tokenRefs = fileText ? findTokenRefs(fileText, tokenNames) : [];
|
|
2536
|
+
const drift = driftByFile.get(c.file) ?? [];
|
|
2537
|
+
const variantSets = [...variants];
|
|
2538
|
+
if (c.variants?.length)
|
|
2539
|
+
variantSets.push({ prop: "story", values: c.variants });
|
|
2540
|
+
out.push({
|
|
2541
|
+
name: c.name,
|
|
2542
|
+
file: c.file,
|
|
2543
|
+
description,
|
|
2544
|
+
props,
|
|
2545
|
+
variants: variantSets,
|
|
2546
|
+
tokenRefs,
|
|
2547
|
+
drift,
|
|
2548
|
+
stories: c.variants
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
const seen = new Map;
|
|
2552
|
+
for (const c of out)
|
|
2553
|
+
if (!seen.has(c.name))
|
|
2554
|
+
seen.set(c.name, c);
|
|
2555
|
+
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// ../studio/dist/model.js
|
|
2559
|
+
function buildStudioModel(cwd, options = {}) {
|
|
2560
|
+
const report = scan(cwd, { now: options.now ?? null });
|
|
2561
|
+
const components = discoverComponents(cwd, report);
|
|
2562
|
+
return {
|
|
2563
|
+
project: basename6(cwd) || "project",
|
|
2564
|
+
generatedAt: options.now ?? null,
|
|
2565
|
+
tokens: {
|
|
2566
|
+
colors: report.colors.clusters.map((c) => ({
|
|
2567
|
+
name: `colors.${c.name}`,
|
|
2568
|
+
hex: c.hex,
|
|
2569
|
+
count: c.count,
|
|
2570
|
+
members: c.members
|
|
2571
|
+
})),
|
|
2572
|
+
spacing: report.spacing,
|
|
2573
|
+
radius: report.radius.values,
|
|
2574
|
+
typography: report.typography.map((t) => ({
|
|
2575
|
+
name: t.name,
|
|
2576
|
+
fontSize: t.fontSize,
|
|
2577
|
+
fontFamily: t.fontFamily,
|
|
2578
|
+
fontWeight: t.fontWeight
|
|
2579
|
+
}))
|
|
2580
|
+
},
|
|
2581
|
+
components,
|
|
2582
|
+
drift: report.drift,
|
|
2583
|
+
detected: report.detected
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
// ../studio/dist/styles.js
|
|
2587
|
+
var STUDIO_STYLES = `
|
|
2588
|
+
:root{
|
|
2589
|
+
--bg:#0a0b0d; --panel:#121419; --panel-2:#181b21; --border:#23272f;
|
|
2590
|
+
--text:#e9ecf1; --text-2:#969db0; --text-3:#646b7a;
|
|
2591
|
+
--accent:#6ea8fe; --good:#56d364; --warn:#e3b341; --bad:#f4716a;
|
|
2592
|
+
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
|
|
2593
|
+
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
|
2594
|
+
}
|
|
2595
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2596
|
+
html,body{height:100%}
|
|
2597
|
+
body{font-family:var(--sans);background:var(--bg);color:var(--text);display:flex;height:100vh;overflow:hidden}
|
|
2598
|
+
|
|
2599
|
+
/* Sidebar */
|
|
2600
|
+
.sidebar{width:248px;flex-shrink:0;background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;padding:16px 0}
|
|
2601
|
+
.brand{padding:0 16px 14px;display:flex;align-items:baseline;gap:8px}
|
|
2602
|
+
.brand b{font-size:15px;font-weight:650}
|
|
2603
|
+
.brand span{font-size:11px;color:var(--text-3);font-family:var(--mono)}
|
|
2604
|
+
.filter{margin:0 12px 12px;width:calc(100% - 24px);background:var(--panel-2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;padding:7px 10px;font-family:var(--sans)}
|
|
2605
|
+
.filter::placeholder{color:var(--text-3)}
|
|
2606
|
+
.nav-group{font-size:10px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3);padding:14px 16px 6px}
|
|
2607
|
+
.nav-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 16px;font-size:13px;color:var(--text-2);cursor:pointer;border-left:2px solid transparent}
|
|
2608
|
+
.nav-item:hover{background:var(--panel-2);color:var(--text)}
|
|
2609
|
+
.nav-item.active{background:var(--panel-2);color:var(--text);border-left-color:var(--accent)}
|
|
2610
|
+
.nav-item .tag{font-size:10px;font-family:var(--mono);color:var(--text-3)}
|
|
2611
|
+
.nav-item .tag.bad{color:var(--bad)}
|
|
2612
|
+
|
|
2613
|
+
/* Main */
|
|
2614
|
+
.main{flex:1;overflow-y:auto;padding:32px 40px 80px}
|
|
2615
|
+
.main-head{margin-bottom:22px}
|
|
2616
|
+
.main-head h1{font-size:22px;font-weight:650;margin-bottom:3px}
|
|
2617
|
+
.main-head .sub{font-size:13px;color:var(--text-2)}
|
|
2618
|
+
.main-head .path{font-family:var(--mono);font-size:12px;color:var(--text-3)}
|
|
2619
|
+
h2.sec{font-size:12px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text-3);margin:26px 0 12px}
|
|
2620
|
+
.empty{color:var(--text-3);font-size:14px;padding:14px 0}
|
|
2621
|
+
.muted{color:var(--text-3)}
|
|
2622
|
+
code{font-family:var(--mono);font-size:12px;background:var(--panel-2);padding:1px 5px;border-radius:5px}
|
|
2623
|
+
.badge{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;padding:1px 6px;border-radius:5px;background:var(--panel-2);color:var(--text-3)}
|
|
2624
|
+
.badge.good{background:rgba(86,211,100,.14);color:var(--good)}
|
|
2625
|
+
.badge.bad{background:rgba(244,113,106,.14);color:var(--bad)}
|
|
2626
|
+
.badge.warn{background:rgba(227,179,65,.14);color:var(--warn)}
|
|
2627
|
+
|
|
2628
|
+
/* Swatches */
|
|
2629
|
+
.swatch-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px}
|
|
2630
|
+
.swatch{background:var(--panel);border:1px solid var(--border);border-radius:11px;overflow:hidden}
|
|
2631
|
+
.swatch .chip{height:78px;display:flex;align-items:flex-end;padding:8px}
|
|
2632
|
+
.swatch .chip .ratio{font-size:11px;font-family:var(--mono)}
|
|
2633
|
+
.swatch .meta{padding:9px 11px}
|
|
2634
|
+
.swatch .nm{font-family:var(--mono);font-size:12px;font-weight:600}
|
|
2635
|
+
.swatch .hx{font-family:var(--mono);font-size:12px;color:var(--text-2);text-transform:uppercase}
|
|
2636
|
+
.swatch .ct{font-size:11px;color:var(--text-3);margin-top:2px}
|
|
2637
|
+
|
|
2638
|
+
/* Ruler */
|
|
2639
|
+
.ruler{display:flex;flex-direction:column;gap:6px}
|
|
2640
|
+
.ruler-row{display:grid;grid-template-columns:120px 1fr;align-items:center;gap:12px}
|
|
2641
|
+
.ruler-row .lbl{font-family:var(--mono);font-size:12px;color:var(--text-2)}
|
|
2642
|
+
.ruler-row .track{background:var(--panel);border-radius:6px;height:18px;overflow:hidden}
|
|
2643
|
+
.ruler-row .bar{height:100%;background:var(--accent)}
|
|
2644
|
+
.ruler-row.off .bar{background:var(--warn)}
|
|
2645
|
+
|
|
2646
|
+
/* Type list */
|
|
2647
|
+
.type-row{display:grid;grid-template-columns:90px 1fr;gap:16px;align-items:center;padding:10px 12px;background:var(--panel);border:1px solid var(--border);border-radius:10px;margin-bottom:6px}
|
|
2648
|
+
.type-row .prev{font-size:26px;line-height:1;text-align:center}
|
|
2649
|
+
.type-row .nm{font-family:var(--mono);font-size:13px;font-weight:600}
|
|
2650
|
+
.type-row .pp{font-size:12px;color:var(--text-2)}
|
|
2651
|
+
|
|
2652
|
+
/* Component viewer */
|
|
2653
|
+
.tabs{display:flex;gap:4px;margin-bottom:16px;border-bottom:1px solid var(--border)}
|
|
2654
|
+
.tab{font-size:13px;color:var(--text-2);padding:8px 12px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
|
|
2655
|
+
.tab:hover{color:var(--text)}
|
|
2656
|
+
.tab.active{color:var(--text);border-bottom-color:var(--accent)}
|
|
2657
|
+
.panel-box{background:var(--panel);border:1px solid var(--border);border-radius:11px;padding:16px}
|
|
2658
|
+
table.props{width:100%;border-collapse:collapse;font-size:13px}
|
|
2659
|
+
table.props th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-3);padding:6px 10px;border-bottom:1px solid var(--border)}
|
|
2660
|
+
table.props td{padding:7px 10px;border-bottom:1px solid var(--border);vertical-align:top}
|
|
2661
|
+
table.props td .req{color:var(--bad);font-size:11px}
|
|
2662
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
2663
|
+
.chip-tok{font-family:var(--mono);font-size:12px;padding:3px 8px;border-radius:6px;background:rgba(86,211,100,.1);color:var(--good)}
|
|
2664
|
+
.chip-drift{font-family:var(--mono);font-size:12px;padding:3px 8px;border-radius:6px;background:rgba(244,113,106,.1);color:var(--bad)}
|
|
2665
|
+
.kv{display:flex;gap:8px;font-size:13px;padding:5px 0;border-bottom:1px solid var(--border)}
|
|
2666
|
+
.kv .k{width:160px;color:var(--text-3);font-family:var(--mono);font-size:12px}
|
|
2667
|
+
pre.code{background:var(--panel-2);border:1px solid var(--border);border-radius:9px;padding:12px;overflow:auto;font-family:var(--mono);font-size:12px;line-height:1.5;color:var(--text)}
|
|
2668
|
+
.drift-table{width:100%;border-collapse:collapse;font-size:13px}
|
|
2669
|
+
.drift-table th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-3);padding:6px 10px;border-bottom:1px solid var(--border)}
|
|
2670
|
+
.drift-table td{padding:7px 10px;border-bottom:1px solid var(--border)}
|
|
2671
|
+
.dot{display:inline-block;width:11px;height:11px;border-radius:3px;border:1px solid var(--border);vertical-align:-1px;margin-right:6px}
|
|
2672
|
+
.suggest{color:var(--good);font-family:var(--mono);font-size:12px}
|
|
2673
|
+
.variant-pills{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
|
|
2674
|
+
.vp{font-size:12px;padding:4px 10px;border-radius:7px;border:1px solid var(--border);color:var(--text-2);cursor:pointer;background:var(--panel)}
|
|
2675
|
+
.vp.active{border-color:var(--accent);color:var(--text)}
|
|
2676
|
+
`;
|
|
2677
|
+
|
|
2678
|
+
// ../studio/dist/client.js
|
|
2679
|
+
var STUDIO_CLIENT = String.raw`
|
|
2680
|
+
(function(){
|
|
2681
|
+
"use strict";
|
|
2682
|
+
var M = window.MODEL;
|
|
2683
|
+
var state = { view: "tokens:colors", componentTab: "preview", variant: null };
|
|
2684
|
+
|
|
2685
|
+
function el(tag, attrs, html){
|
|
2686
|
+
var e = document.createElement(tag);
|
|
2687
|
+
if (attrs) for (var k in attrs){ if(k==="class") e.className=attrs[k]; else e.setAttribute(k, attrs[k]); }
|
|
2688
|
+
if (html != null) e.innerHTML = html;
|
|
2689
|
+
return e;
|
|
2690
|
+
}
|
|
2691
|
+
function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g, function(c){ return {"&":"&","<":"<",">":">","\"":""","'":"'"}[c]; }); }
|
|
2692
|
+
function rgb(hex){ hex=hex.replace("#",""); return {r:parseInt(hex.slice(0,2),16),g:parseInt(hex.slice(2,4),16),b:parseInt(hex.slice(4,6),16)}; }
|
|
2693
|
+
function lin(c){ c/=255; return c<=0.04045?c/12.92:Math.pow((c+0.055)/1.055,2.4); }
|
|
2694
|
+
function lum(hex){ var c=rgb(hex); return 0.2126*lin(c.r)+0.7152*lin(c.g)+0.0722*lin(c.b); }
|
|
2695
|
+
function contrast(a,b){ var la=lum(a),lb=lum(b); var hi=Math.max(la,lb),lo=Math.min(la,lb); return (hi+0.05)/(lo+0.05); }
|
|
2696
|
+
function readable(hex){ return contrast(hex,"#ffffff")>=contrast(hex,"#111111")?"#ffffff":"#111111"; }
|
|
2697
|
+
function rating(r){ return r>=7?"AAA":r>=4.5?"AA":r>=3?"AA large":"Fail"; }
|
|
2698
|
+
|
|
2699
|
+
// ---- Sidebar ----
|
|
2700
|
+
function buildSidebar(){
|
|
2701
|
+
var sb = document.querySelector(".sidebar");
|
|
2702
|
+
sb.innerHTML = "";
|
|
2703
|
+
sb.appendChild(el("div",{class:"brand"},"<b>"+esc(M.project)+"</b><span>studio</span>"));
|
|
2704
|
+
var filter = el("input",{class:"filter",placeholder:"Filter components…"});
|
|
2705
|
+
filter.addEventListener("input", function(){ renderComponentNav(filter.value.toLowerCase()); });
|
|
2706
|
+
sb.appendChild(filter);
|
|
2707
|
+
|
|
2708
|
+
sb.appendChild(el("div",{class:"nav-group"},"Tokens"));
|
|
2709
|
+
[["tokens:colors","Colors", M.tokens.colors.length],
|
|
2710
|
+
["tokens:typography","Typography", M.tokens.typography.length],
|
|
2711
|
+
["tokens:spacing","Spacing", M.tokens.spacing.values.length]
|
|
2712
|
+
].forEach(function(it){ sb.appendChild(navItem(it[0],it[1],it[2])); });
|
|
2713
|
+
|
|
2714
|
+
sb.appendChild(el("div",{class:"nav-group"},"Components"));
|
|
2715
|
+
var holder = el("div",{class:"comp-nav"});
|
|
2716
|
+
sb.appendChild(holder);
|
|
2717
|
+
|
|
2718
|
+
sb.appendChild(el("div",{class:"nav-group"},"Report"));
|
|
2719
|
+
var driftBad = M.drift.length>0;
|
|
2720
|
+
var di = navItem("drift","Drift report", M.drift.length);
|
|
2721
|
+
if (driftBad) di.querySelector(".tag").className = "tag bad";
|
|
2722
|
+
sb.appendChild(di);
|
|
2723
|
+
|
|
2724
|
+
renderComponentNav("");
|
|
2725
|
+
}
|
|
2726
|
+
function navItem(view,label,count){
|
|
2727
|
+
var it = el("div",{class:"nav-item"+(state.view===view?" active":"")});
|
|
2728
|
+
it.innerHTML = "<span>"+esc(label)+"</span><span class='tag'>"+count+"</span>";
|
|
2729
|
+
it.addEventListener("click", function(){ state.view=view; state.variant=null; render(); });
|
|
2730
|
+
return it;
|
|
2731
|
+
}
|
|
2732
|
+
function renderComponentNav(q){
|
|
2733
|
+
var holder = document.querySelector(".comp-nav"); if(!holder) return;
|
|
2734
|
+
holder.innerHTML = "";
|
|
2735
|
+
M.components.filter(function(c){ return c.name.toLowerCase().indexOf(q)>=0; }).forEach(function(c){
|
|
2736
|
+
var view = "component:"+c.name;
|
|
2737
|
+
var it = el("div",{class:"nav-item"+(state.view===view?" active":"")});
|
|
2738
|
+
var tag = c.drift.length? "<span class='tag bad'>"+c.drift.length+"</span>" : "";
|
|
2739
|
+
it.innerHTML = "<span>"+esc(c.name)+"</span>"+tag;
|
|
2740
|
+
it.addEventListener("click", function(){ state.view=view; state.componentTab="preview"; state.variant=null; render(); });
|
|
2741
|
+
holder.appendChild(it);
|
|
2742
|
+
});
|
|
2743
|
+
if (!M.components.length) holder.appendChild(el("div",{class:"nav-item muted"},"<span>none found</span>"));
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// ---- Main ----
|
|
2747
|
+
function head(title, sub, path){
|
|
2748
|
+
var h = el("div",{class:"main-head"});
|
|
2749
|
+
h.innerHTML = "<h1>"+esc(title)+"</h1>"+(sub?"<div class='sub'>"+sub+"</div>":"")+(path?"<div class='path'>"+esc(path)+"</div>":"");
|
|
2750
|
+
return h;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
function viewColors(main){
|
|
2754
|
+
main.appendChild(head("Colors", M.tokens.colors.length+" tokens"));
|
|
2755
|
+
if(!M.tokens.colors.length){ main.appendChild(el("div",{class:"empty"},"No colors found.")); return; }
|
|
2756
|
+
var grid = el("div",{class:"swatch-grid"});
|
|
2757
|
+
M.tokens.colors.forEach(function(c){
|
|
2758
|
+
var t = readable(c.hex), r = contrast(c.hex,t);
|
|
2759
|
+
var dup = c.members.length>1? "<div class='ct' style='color:var(--warn)'>+"+(c.members.length-1)+" near-dup</div>":"";
|
|
2760
|
+
var s = el("div",{class:"swatch"});
|
|
2761
|
+
s.innerHTML = "<div class='chip' style='background:"+esc(c.hex)+";color:"+t+"'><span class='ratio'>"+r.toFixed(1)+":1 "+rating(r)+"</span></div>"+
|
|
2762
|
+
"<div class='meta'><div class='nm'>"+esc(c.name)+"</div><div class='hx'>"+esc(c.hex)+"</div><div class='ct'>"+c.count+" uses</div>"+dup+"</div>";
|
|
2763
|
+
grid.appendChild(s);
|
|
2764
|
+
});
|
|
2765
|
+
main.appendChild(grid);
|
|
2766
|
+
}
|
|
2767
|
+
function viewTypography(main){
|
|
2768
|
+
main.appendChild(head("Typography", M.tokens.typography.length+" tokens"));
|
|
2769
|
+
if(!M.tokens.typography.length){ main.appendChild(el("div",{class:"empty"},"No typography tokens found.")); return; }
|
|
2770
|
+
M.tokens.typography.forEach(function(t){
|
|
2771
|
+
var row = el("div",{class:"type-row"});
|
|
2772
|
+
var fs = t.fontSize && /^[0-9]/.test(t.fontSize) ? t.fontSize : "";
|
|
2773
|
+
var prev = fs ? "<div class='prev' style='font-size:"+esc(fs)+";font-family:"+esc(t.fontFamily||"inherit")+"'>Ag</div>" : "<div class='prev muted'>Ag</div>";
|
|
2774
|
+
row.innerHTML = prev+"<div><div class='nm'>"+esc(t.name)+"</div><div class='pp'>"+esc(t.fontSize||"—")+(t.fontFamily?" · "+esc(t.fontFamily):"")+(t.fontWeight?" · "+t.fontWeight:"")+"</div></div>";
|
|
2775
|
+
main.appendChild(row);
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function viewSpacing(main){
|
|
2779
|
+
var sp = M.tokens.spacing;
|
|
2780
|
+
main.appendChild(head("Spacing", sp.grid? sp.grid+"pt grid · "+sp.outliers.length+" off-grid":"no grid detected"));
|
|
2781
|
+
if(!sp.values.length){ main.appendChild(el("div",{class:"empty"},"No spacing scale detected.")); return; }
|
|
2782
|
+
var max = Math.max.apply(null, sp.values.concat([1]));
|
|
2783
|
+
var ruler = el("div",{class:"ruler"});
|
|
2784
|
+
sp.values.forEach(function(v){
|
|
2785
|
+
var off = sp.outliers.indexOf(v)>=0;
|
|
2786
|
+
var w = Math.max(2, Math.round(v/max*100));
|
|
2787
|
+
var row = el("div",{class:"ruler-row"+(off?" off":"")});
|
|
2788
|
+
row.innerHTML = "<div class='lbl'>"+v+"px"+(off?" <span class='badge warn'>off</span>":"")+"</div><div class='track'><div class='bar' style='width:"+w+"%'></div></div>";
|
|
2789
|
+
ruler.appendChild(row);
|
|
2790
|
+
});
|
|
2791
|
+
main.appendChild(ruler);
|
|
2792
|
+
}
|
|
2793
|
+
function viewDrift(main){
|
|
2794
|
+
main.appendChild(head("Drift report", M.drift.length+" hardcoded value"+(M.drift.length===1?"":"s")));
|
|
2795
|
+
if(!M.drift.length){ main.appendChild(el("div",{class:"empty"},"✓ No drift. Everything references a token.")); return; }
|
|
2796
|
+
var t = el("table",{class:"drift-table"});
|
|
2797
|
+
t.innerHTML = "<thead><tr><th>Location</th><th>Value</th><th>Suggested token</th></tr></thead>";
|
|
2798
|
+
var tb = el("tbody");
|
|
2799
|
+
M.drift.forEach(function(d){
|
|
2800
|
+
var chip = d.kind==="color"? "<span class='dot' style='background:"+esc(d.value)+"'></span>":"";
|
|
2801
|
+
var sug = d.suggestion? "<span class='suggest'>"+esc(d.suggestion)+"</span>":"<span class='muted'>—</span>";
|
|
2802
|
+
tb.appendChild(el("tr",null,"<td><code>"+esc(d.file)+"</code>:"+d.line+"</td><td>"+chip+"<code>"+esc(d.value)+"</code></td><td>"+sug+"</td>"));
|
|
2803
|
+
});
|
|
2804
|
+
t.appendChild(tb); main.appendChild(t);
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
function viewComponent(main, name){
|
|
2808
|
+
var c = M.components.filter(function(x){return x.name===name;})[0];
|
|
2809
|
+
if(!c){ main.appendChild(el("div",{class:"empty"},"Component not found.")); return; }
|
|
2810
|
+
main.appendChild(head(c.name, c.description? esc(c.description) : (c.props.length+" props · "+c.tokenRefs.length+" tokens · "+c.drift.length+" drift"), c.file));
|
|
2811
|
+
|
|
2812
|
+
var tabs = el("div",{class:"tabs"});
|
|
2813
|
+
[["preview","Preview"],["props","Props"],["tokens","Tokens"],["drift","Drift"],["spec","Spec"]].forEach(function(t){
|
|
2814
|
+
var tab = el("div",{class:"tab"+(state.componentTab===t[0]?" active":"")}, t[1]);
|
|
2815
|
+
tab.addEventListener("click", function(){ state.componentTab=t[0]; render(); });
|
|
2816
|
+
tabs.appendChild(tab);
|
|
2817
|
+
});
|
|
2818
|
+
main.appendChild(tabs);
|
|
2819
|
+
|
|
2820
|
+
var box = el("div");
|
|
2821
|
+
if (state.componentTab==="preview") renderPreview(box, c);
|
|
2822
|
+
else if (state.componentTab==="props") renderProps(box, c);
|
|
2823
|
+
else if (state.componentTab==="tokens") renderTokens(box, c);
|
|
2824
|
+
else if (state.componentTab==="drift") renderCompDrift(box, c);
|
|
2825
|
+
else if (state.componentTab==="spec") renderSpec(box, c);
|
|
2826
|
+
main.appendChild(box);
|
|
2827
|
+
}
|
|
2828
|
+
function renderPreview(box, c){
|
|
2829
|
+
if (c.variants.length){
|
|
2830
|
+
var pills = el("div",{class:"variant-pills"});
|
|
2831
|
+
c.variants.forEach(function(v){
|
|
2832
|
+
v.values.forEach(function(val){
|
|
2833
|
+
var key = v.prop+"="+val;
|
|
2834
|
+
var p = el("div",{class:"vp"+(state.variant===key?" active":"")}, esc(v.prop)+": "+esc(val));
|
|
2835
|
+
p.addEventListener("click", function(){ state.variant = state.variant===key?null:key; render(); });
|
|
2836
|
+
pills.appendChild(p);
|
|
2837
|
+
});
|
|
2838
|
+
});
|
|
2839
|
+
box.appendChild(pills);
|
|
2840
|
+
}
|
|
2841
|
+
var note = "Live component rendering needs a bundler (v2). For now Studio shows the API, token usage, drift, and an auto-spec.";
|
|
2842
|
+
var ex = el("div",{class:"panel-box"});
|
|
2843
|
+
var usage = "<"+c.name+(state.variant? " "+esc(state.variant.replace("=", "=\""))+"\"" : "")+" />";
|
|
2844
|
+
ex.innerHTML = "<div class='muted' style='font-size:12px;margin-bottom:10px'>"+note+"</div><pre class='code'>"+esc(usage)+"</pre>";
|
|
2845
|
+
box.appendChild(ex);
|
|
2846
|
+
}
|
|
2847
|
+
function renderProps(box, c){
|
|
2848
|
+
if(!c.props.length){ box.appendChild(el("div",{class:"empty"},"No typed props found.")); return; }
|
|
2849
|
+
var t = el("table",{class:"props"});
|
|
2850
|
+
t.innerHTML="<thead><tr><th>Prop</th><th>Type</th><th>Required</th><th>Notes</th></tr></thead>";
|
|
2851
|
+
var tb=el("tbody");
|
|
2852
|
+
c.props.forEach(function(p){
|
|
2853
|
+
tb.appendChild(el("tr",null,"<td><code>"+esc(p.name)+"</code></td><td><code>"+esc(p.type)+"</code></td><td>"+(p.required?"<span class='req'>required</span>":"<span class='muted'>optional</span>")+"</td><td class='muted'>"+(p.description?esc(p.description):"")+"</td>"));
|
|
2854
|
+
});
|
|
2855
|
+
t.appendChild(tb);
|
|
2856
|
+
var wrap = el("div",{class:"panel-box"}); wrap.appendChild(t); box.appendChild(wrap);
|
|
2857
|
+
}
|
|
2858
|
+
function renderTokens(box, c){
|
|
2859
|
+
var wrap = el("div",{class:"panel-box"});
|
|
2860
|
+
wrap.appendChild(el("h2",{class:"sec"},"Using correctly ("+c.tokenRefs.length+")"));
|
|
2861
|
+
if(c.tokenRefs.length){ var ch=el("div",{class:"chips"}); c.tokenRefs.forEach(function(t){ ch.appendChild(el("span",{class:"chip-tok"},esc(t))); }); wrap.appendChild(ch); }
|
|
2862
|
+
else wrap.appendChild(el("div",{class:"empty"},"No token references detected."));
|
|
2863
|
+
wrap.appendChild(el("h2",{class:"sec"},"Hardcoded / drift ("+c.drift.length+")"));
|
|
2864
|
+
if(c.drift.length){ var cd=el("div",{class:"chips"}); c.drift.forEach(function(d){ cd.appendChild(el("span",{class:"chip-drift"},esc(d.value)+(d.suggestion?" → "+esc(d.suggestion):""))); }); wrap.appendChild(cd); }
|
|
2865
|
+
else wrap.appendChild(el("div",{class:"empty good muted"},"✓ no drift in this component"));
|
|
2866
|
+
box.appendChild(wrap);
|
|
2867
|
+
}
|
|
2868
|
+
function renderCompDrift(box, c){
|
|
2869
|
+
if(!c.drift.length){ box.appendChild(el("div",{class:"empty"},"✓ No hardcoded values in this component.")); return; }
|
|
2870
|
+
var t=el("table",{class:"drift-table"});
|
|
2871
|
+
t.innerHTML="<thead><tr><th>Value</th><th>Line</th><th>Fix</th></tr></thead>";
|
|
2872
|
+
var tb=el("tbody");
|
|
2873
|
+
c.drift.forEach(function(d){
|
|
2874
|
+
var chip=d.kind==="color"?"<span class='dot' style='background:"+esc(d.value)+"'></span>":"";
|
|
2875
|
+
tb.appendChild(el("tr",null,"<td>"+chip+"<code>"+esc(d.value)+"</code></td><td class='muted'>"+esc(c.file)+":"+d.line+"</td><td>"+(d.suggestion?"<span class='suggest'>"+esc(d.suggestion)+"</span>":"<span class='muted'>add a token</span>")+"</td>"));
|
|
2876
|
+
});
|
|
2877
|
+
t.appendChild(tb); var w=el("div",{class:"panel-box"}); w.appendChild(t); box.appendChild(w);
|
|
2878
|
+
}
|
|
2879
|
+
function renderSpec(box, c){
|
|
2880
|
+
var w=el("div",{class:"panel-box"});
|
|
2881
|
+
var rows="";
|
|
2882
|
+
function kv(k,v){ return "<div class='kv'><div class='k'>"+esc(k)+"</div><div>"+v+"</div></div>"; }
|
|
2883
|
+
rows+=kv("Component","<code>"+esc(c.name)+"</code>");
|
|
2884
|
+
rows+=kv("Source","<code>"+esc(c.file)+"</code>");
|
|
2885
|
+
rows+=kv("Props", c.props.length? c.props.map(function(p){return "<code>"+esc(p.name)+"</code>";}).join(" ") : "<span class='muted'>none</span>");
|
|
2886
|
+
c.variants.forEach(function(v){ rows+=kv("Variant: "+v.prop, v.values.map(function(x){return "<code>"+esc(x)+"</code>";}).join(" ")); });
|
|
2887
|
+
rows+=kv("Tokens used", c.tokenRefs.length? c.tokenRefs.map(function(t){return "<span class='suggest'>"+esc(t)+"</span>";}).join(" ") : "<span class='muted'>none detected</span>");
|
|
2888
|
+
rows+=kv("Drift", c.drift.length? "<span class='badge bad'>"+c.drift.length+"</span>" : "<span class='badge good'>clean</span>");
|
|
2889
|
+
w.innerHTML=rows; box.appendChild(w);
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function render(){
|
|
2893
|
+
buildSidebar();
|
|
2894
|
+
var main = document.querySelector(".main"); main.innerHTML="";
|
|
2895
|
+
var v = state.view;
|
|
2896
|
+
if (v==="tokens:colors") viewColors(main);
|
|
2897
|
+
else if (v==="tokens:typography") viewTypography(main);
|
|
2898
|
+
else if (v==="tokens:spacing") viewSpacing(main);
|
|
2899
|
+
else if (v==="drift") viewDrift(main);
|
|
2900
|
+
else if (v.indexOf("component:")===0) viewComponent(main, v.slice("component:".length));
|
|
2901
|
+
else viewColors(main);
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// Live reload via SSE (watch mode only — endpoint may not exist).
|
|
2905
|
+
try {
|
|
2906
|
+
var es = new EventSource("/events");
|
|
2907
|
+
es.onmessage = function(){ location.reload(); };
|
|
2908
|
+
es.onerror = function(){ es.close(); };
|
|
2909
|
+
} catch(e){}
|
|
2910
|
+
|
|
2911
|
+
render();
|
|
2912
|
+
})();
|
|
2913
|
+
`;
|
|
2914
|
+
|
|
2915
|
+
// ../studio/dist/renderer.js
|
|
2916
|
+
function renderStudio(model) {
|
|
2917
|
+
const json = JSON.stringify(model).replace(/</g, "\\u003c");
|
|
2918
|
+
return `<!doctype html>
|
|
2919
|
+
<html lang="en">
|
|
2920
|
+
<head>
|
|
2921
|
+
<meta charset="utf-8">
|
|
2922
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2923
|
+
<title>${escapeHtml(model.project)} — DesignAgent Studio</title>
|
|
2924
|
+
<style>${STUDIO_STYLES}</style>
|
|
2925
|
+
</head>
|
|
2926
|
+
<body>
|
|
2927
|
+
<aside class="sidebar"></aside>
|
|
2928
|
+
<main class="main"></main>
|
|
2929
|
+
<script>window.MODEL=${json};</script>
|
|
2930
|
+
<script>${STUDIO_CLIENT}</script>
|
|
2931
|
+
</body>
|
|
2932
|
+
</html>
|
|
2933
|
+
`;
|
|
2934
|
+
}
|
|
2935
|
+
function escapeHtml(s) {
|
|
2936
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2937
|
+
}
|
|
2938
|
+
// ../studio/dist/server.js
|
|
2939
|
+
import { createServer } from "node:http";
|
|
2940
|
+
import { watch as watch2 } from "node:fs";
|
|
2941
|
+
var DEFAULT_PORT = 4321;
|
|
2942
|
+
function startStudioServer(cwd, options = {}) {
|
|
2943
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
2944
|
+
const clients = new Set;
|
|
2945
|
+
const server = createServer((req, res) => {
|
|
2946
|
+
if (req.url === "/events") {
|
|
2947
|
+
res.writeHead(200, {
|
|
2948
|
+
"content-type": "text/event-stream",
|
|
2949
|
+
"cache-control": "no-cache",
|
|
2950
|
+
connection: "keep-alive"
|
|
2951
|
+
});
|
|
2952
|
+
res.write(`: connected
|
|
2953
|
+
|
|
2954
|
+
`);
|
|
2955
|
+
clients.add(res);
|
|
2956
|
+
req.on("close", () => clients.delete(res));
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
const html = renderStudio(buildStudioModel(cwd));
|
|
2960
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
2961
|
+
res.end(html);
|
|
2962
|
+
});
|
|
2963
|
+
if (options.watch) {
|
|
2964
|
+
let timer = null;
|
|
2965
|
+
try {
|
|
2966
|
+
watch2(cwd, { recursive: true }, (_e, filename) => {
|
|
2967
|
+
if (!filename)
|
|
2968
|
+
return;
|
|
2969
|
+
const f = filename.toString();
|
|
2970
|
+
if (f.includes(".designagent") || f.includes("node_modules") || f.includes("designagent-studio"))
|
|
2971
|
+
return;
|
|
2972
|
+
if (timer)
|
|
2973
|
+
clearTimeout(timer);
|
|
2974
|
+
timer = setTimeout(() => {
|
|
2975
|
+
for (const c of clients)
|
|
2976
|
+
c.write(`data: reload
|
|
2977
|
+
|
|
2978
|
+
`);
|
|
2979
|
+
}, 150);
|
|
2980
|
+
});
|
|
2981
|
+
} catch {}
|
|
2982
|
+
}
|
|
2983
|
+
return new Promise((resolve3) => {
|
|
2984
|
+
server.listen(port, () => {
|
|
2985
|
+
resolve3({
|
|
2986
|
+
server,
|
|
2987
|
+
port,
|
|
2988
|
+
url: `http://localhost:${port}`,
|
|
2989
|
+
close: () => {
|
|
2990
|
+
for (const c of clients)
|
|
2991
|
+
c.end();
|
|
2992
|
+
server.close();
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
});
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
// src/commands/studio.ts
|
|
2999
|
+
function openInBrowser2(target) {
|
|
3000
|
+
const os = platform2();
|
|
3001
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
3002
|
+
try {
|
|
3003
|
+
const child = spawn2(cmd, [target], { stdio: "ignore", detached: true, shell: os === "win32" });
|
|
3004
|
+
child.on("error", () => {});
|
|
3005
|
+
child.unref();
|
|
3006
|
+
} catch {}
|
|
3007
|
+
}
|
|
3008
|
+
async function studio(cwd, options = {}) {
|
|
3009
|
+
if (options.exportStatic) {
|
|
3010
|
+
const model2 = buildStudioModel(cwd);
|
|
3011
|
+
const dir = join9(cwd, "designagent-studio");
|
|
3012
|
+
mkdirSync4(dir, { recursive: true });
|
|
3013
|
+
const out = join9(dir, "index.html");
|
|
3014
|
+
writeFileSync3(out, renderStudio(model2), "utf8");
|
|
3015
|
+
console.log(pc3.green("✓ ") + `Studio exported to ${pc3.bold(out)}
|
|
3016
|
+
` + pc3.dim(` ${model2.components.length} components · ${model2.tokens.colors.length} color tokens · ${model2.drift.length} drift`));
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const { url, close } = await startStudioServer(cwd, {
|
|
3020
|
+
port: options.port,
|
|
3021
|
+
watch: options.watch
|
|
3022
|
+
});
|
|
3023
|
+
const model = buildStudioModel(cwd);
|
|
3024
|
+
console.log(pc3.green("◆ ") + pc3.bold("DesignAgent Studio") + ` running at ${pc3.cyan(url)}
|
|
3025
|
+
` + pc3.dim(` ${model.components.length} components · ${model.tokens.colors.length} color tokens · ${model.drift.length} drift`) + (options.watch ? pc3.dim(`
|
|
3026
|
+
watching for changes — Ctrl-C to stop`) : pc3.dim(`
|
|
3027
|
+
Ctrl-C to stop`)));
|
|
3028
|
+
if (!options.noOpen)
|
|
3029
|
+
openInBrowser2(url);
|
|
3030
|
+
const shutdown = () => {
|
|
3031
|
+
close();
|
|
3032
|
+
process.exit(0);
|
|
3033
|
+
};
|
|
3034
|
+
process.on("SIGINT", shutdown);
|
|
3035
|
+
process.on("SIGTERM", shutdown);
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/index.ts
|
|
3039
|
+
var require2 = createRequire(import.meta.url);
|
|
3040
|
+
var { version } = require2("../package.json");
|
|
3041
|
+
var HELP = `
|
|
3042
|
+
${pc4.bold("designagent")} ${pc4.dim(`v${version}`)} — the design brain Claude Code needs.
|
|
3043
|
+
|
|
3044
|
+
${pc4.bold("Usage")}
|
|
3045
|
+
npx designagent <command>
|
|
3046
|
+
|
|
3047
|
+
${pc4.bold("Commands")}
|
|
3048
|
+
init Set up DESIGN.md, CLAUDE.md, and DECISIONS.md in this project
|
|
3049
|
+
docs Scan the codebase and open the visual design-system docs
|
|
3050
|
+
${pc4.dim("--export static HTML · --watch live-reload · --no-open")}
|
|
3051
|
+
studio Launch DesignAgent Studio — the AI-native Storybook alternative
|
|
3052
|
+
${pc4.dim("--export static HTML · --watch live-reload · --no-open · --port")}
|
|
3053
|
+
update Refresh an existing setup ${pc4.dim("(coming soon)")}
|
|
3054
|
+
help Show this help
|
|
3055
|
+
version Print the version
|
|
3056
|
+
|
|
3057
|
+
${pc4.dim("Docs: https://designagent.dev")}
|
|
3058
|
+
`;
|
|
3059
|
+
async function main() {
|
|
3060
|
+
const command = process.argv[2] ?? "init";
|
|
3061
|
+
switch (command) {
|
|
3062
|
+
case "init":
|
|
3063
|
+
await init();
|
|
3064
|
+
break;
|
|
3065
|
+
case "docs": {
|
|
3066
|
+
const args = process.argv.slice(3);
|
|
3067
|
+
docs(process.cwd(), {
|
|
3068
|
+
exportStatic: args.includes("--export"),
|
|
3069
|
+
noOpen: args.includes("--no-open"),
|
|
3070
|
+
watch: args.includes("--watch")
|
|
3071
|
+
});
|
|
3072
|
+
break;
|
|
3073
|
+
}
|
|
3074
|
+
case "studio": {
|
|
3075
|
+
const args = process.argv.slice(3);
|
|
3076
|
+
const portArg = args.find((a) => a.startsWith("--port="));
|
|
3077
|
+
await studio(process.cwd(), {
|
|
3078
|
+
exportStatic: args.includes("--export"),
|
|
3079
|
+
watch: args.includes("--watch"),
|
|
3080
|
+
noOpen: args.includes("--no-open"),
|
|
3081
|
+
port: portArg ? Number(portArg.split("=")[1]) : undefined
|
|
3082
|
+
});
|
|
3083
|
+
break;
|
|
3084
|
+
}
|
|
3085
|
+
case "update":
|
|
3086
|
+
console.log(pc4.yellow("`designagent update` is coming in a future release.") + `
|
|
3087
|
+
` + pc4.dim("For now, re-run ") + pc4.bold("npx designagent init") + pc4.dim(" (it will ask before overwriting your files)."));
|
|
3088
|
+
break;
|
|
3089
|
+
case "version":
|
|
3090
|
+
case "--version":
|
|
3091
|
+
case "-v":
|
|
3092
|
+
console.log(version);
|
|
3093
|
+
break;
|
|
3094
|
+
case "help":
|
|
3095
|
+
case "--help":
|
|
3096
|
+
case "-h":
|
|
3097
|
+
console.log(HELP);
|
|
3098
|
+
break;
|
|
3099
|
+
default:
|
|
3100
|
+
console.error(pc4.red(`Unknown command: ${command}`));
|
|
3101
|
+
console.log(HELP);
|
|
3102
|
+
process.exit(1);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
main().catch((err) => {
|
|
3106
|
+
console.error(pc4.red("designagent failed:"), err instanceof Error ? err.message : err);
|
|
3107
|
+
process.exit(1);
|
|
3108
|
+
});
|