uifying 0.0.0-stage → 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.
@@ -0,0 +1,2036 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
5
+ import { join as join7, resolve as resolve2 } from "node:path";
6
+
7
+ // ../core/dist/util.js
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
10
+ import { dirname, extname, join, relative } from "node:path";
11
+ var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
12
+ "node_modules",
13
+ ".git",
14
+ ".next",
15
+ ".nuxt",
16
+ ".svelte-kit",
17
+ ".turbo",
18
+ ".cache",
19
+ ".vercel",
20
+ "dist",
21
+ "build",
22
+ "out",
23
+ "coverage",
24
+ ".uifying",
25
+ ".wrangler",
26
+ ".pnpm-store",
27
+ "public",
28
+ "test",
29
+ "tests",
30
+ "__tests__",
31
+ ".storybook",
32
+ "stories"
33
+ ]);
34
+ function toPosix(value2) {
35
+ return value2.split("\\").join("/");
36
+ }
37
+ function readText(file) {
38
+ return readFileSync(file, "utf8");
39
+ }
40
+ function readJson(file) {
41
+ return JSON.parse(readText(file));
42
+ }
43
+ function parseJsonc(file) {
44
+ const raw = readText(file).replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/,\s*([}\]])/g, "$1");
45
+ return JSON.parse(raw);
46
+ }
47
+ function writeText(file, content) {
48
+ mkdirSync(dirname(file), { recursive: true });
49
+ writeFileSync(file, content, "utf8");
50
+ }
51
+ function writeJson(file, value2) {
52
+ writeText(file, JSON.stringify(value2, null, 2) + "\n");
53
+ }
54
+ function ensureDir(dir) {
55
+ mkdirSync(dir, { recursive: true });
56
+ }
57
+ function walk(root, ignore = []) {
58
+ const ignored = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
59
+ const output = [];
60
+ const visit = (dir) => {
61
+ let entries;
62
+ try {
63
+ entries = readdirSync(dir);
64
+ } catch {
65
+ return;
66
+ }
67
+ entries.sort();
68
+ for (const entry of entries) {
69
+ if (output.length >= 5e3)
70
+ return;
71
+ const full = join(dir, entry);
72
+ let stat2;
73
+ try {
74
+ stat2 = statSync(full);
75
+ } catch {
76
+ continue;
77
+ }
78
+ if (stat2.isSymbolicLink())
79
+ continue;
80
+ if (stat2.isDirectory()) {
81
+ if (ignored.has(entry))
82
+ continue;
83
+ visit(full);
84
+ continue;
85
+ }
86
+ if (stat2.size > 15e5)
87
+ continue;
88
+ const rel = toPosix(relative(root, full));
89
+ if (rel.split("/").some((part) => ignored.has(part)))
90
+ continue;
91
+ if (/\.(test|spec|stories|story)\.[jt]sx?$/.test(rel))
92
+ continue;
93
+ output.push(rel);
94
+ }
95
+ };
96
+ visit(root);
97
+ return output;
98
+ }
99
+ function lineAt(source, index) {
100
+ const before = source.slice(0, Math.max(0, index));
101
+ const lines = before.split("\n");
102
+ return { line: lines.length, column: (lines[lines.length - 1]?.length ?? 0) + 1 };
103
+ }
104
+ function nowIso() {
105
+ return (/* @__PURE__ */ new Date()).toISOString();
106
+ }
107
+ function resolveCssValue(value2, variables, depth = 0) {
108
+ if (depth > 6)
109
+ return value2;
110
+ const next = value2.replace(/var\(\s*--([\w-]+)\s*(?:,\s*([^)]*))?\)/g, (_match, name, fallback) => {
111
+ return variables.get(name) ?? fallback ?? _match;
112
+ });
113
+ if (next === value2)
114
+ return value2;
115
+ return resolveCssValue(next, variables, depth + 1);
116
+ }
117
+ function numericPx(value2) {
118
+ const match = value2.trim().match(/^(-?\d+(?:\.\d+)?)(px|rem|em)?$/);
119
+ if (!match)
120
+ return null;
121
+ const number = Number(match[1]);
122
+ if (!Number.isFinite(number))
123
+ return null;
124
+ if (match[2] === "rem" || match[2] === "em")
125
+ return number * 16;
126
+ return number;
127
+ }
128
+ function detectPackageManager(root) {
129
+ if (existsSync(join(root, "pnpm-lock.yaml")))
130
+ return "pnpm";
131
+ if (existsSync(join(root, "yarn.lock")))
132
+ return "yarn";
133
+ if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")))
134
+ return "bun";
135
+ if (existsSync(join(root, "package-lock.json")))
136
+ return "npm";
137
+ return null;
138
+ }
139
+ function runGit(root, args) {
140
+ const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
141
+ if (result.status !== 0)
142
+ return "";
143
+ return result.stdout.trim();
144
+ }
145
+ var useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
146
+ var paint = (code, value2) => useColor ? `\x1B[${code}m${value2}\x1B[0m` : value2;
147
+ var color = {
148
+ red: (value2) => paint("31", value2),
149
+ yellow: (value2) => paint("33", value2),
150
+ green: (value2) => paint("32", value2),
151
+ cyan: (value2) => paint("36", value2),
152
+ dim: (value2) => paint("2", value2),
153
+ bold: (value2) => paint("1", value2)
154
+ };
155
+ function escapeHtml(value2) {
156
+ return value2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
157
+ }
158
+ function extensionless(file) {
159
+ return file.slice(0, file.length - extname(file).length);
160
+ }
161
+
162
+ // ../core/dist/scan.js
163
+ import { existsSync as existsSync2 } from "node:fs";
164
+ import { basename, join as join2 } from "node:path";
165
+ function scanProject(root, ignore = []) {
166
+ const packagePath = join2(root, "package.json");
167
+ if (!existsSync2(packagePath)) {
168
+ throw new Error("No package.json found. Run this from a React or Next.js project, or use uifying spec init for a new project.");
169
+ }
170
+ const pkg = readJson(packagePath);
171
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
172
+ const files = walk(root, ignore);
173
+ const cssFiles = files.filter((file) => file.endsWith(".css"));
174
+ const tsconfig = existsSync2(join2(root, "tsconfig.json")) ? safeJsonc(join2(root, "tsconfig.json")) : null;
175
+ const importAlias = detectImportAlias(tsconfig);
176
+ const tailwindConfig = files.find((file) => /(^|\/)tailwind\.config\.(ts|js|mjs|cjs)$/.test(file)) ?? null;
177
+ const globalsCss = pickGlobalsCss(root, cssFiles);
178
+ const tailwind = detectTailwind(deps, globalsCss ? readText(join2(root, globalsCss)) : "", tailwindConfig);
179
+ const componentDirs = ["src/components", "components", "src/ui", "ui"].filter((dir) => existsSync2(join2(root, dir)));
180
+ const pageDirs = ["app", "src/app", "pages", "src/pages"].filter((dir) => existsSync2(join2(root, dir)));
181
+ const raw = [];
182
+ const variables = /* @__PURE__ */ new Map();
183
+ if (globalsCss)
184
+ raw.push(...parseCssTokens(root, globalsCss, variables));
185
+ if (tailwindConfig)
186
+ raw.push(...parseTailwindConfig(root, tailwindConfig));
187
+ const tokens = normalizeTokens(dedupeTokens(raw), variables);
188
+ const components = scanComponents(root, files, importAlias).filter((component) => !isPageFile(component.path, pageDirs));
189
+ const patterns = scanPages(root, files, pageDirs, components);
190
+ const info = {
191
+ name: pkg.name ?? basename(root),
192
+ framework: deps.next ? "next" : deps.react ? "react" : "unknown",
193
+ typescript: Boolean(tsconfig) || files.some((file) => file.endsWith(".tsx") || file.endsWith(".ts")),
194
+ tailwind,
195
+ packageManager: detectPackageManager(root),
196
+ componentDirs,
197
+ pageDirs,
198
+ globalsCss,
199
+ tailwindConfig,
200
+ importAlias
201
+ };
202
+ return { info, tokens, components, patterns };
203
+ }
204
+ function emptyScan(root, name = basename(root)) {
205
+ const info = { name, framework: "unknown", typescript: existsSync2(join2(root, "tsconfig.json")), tailwind: null, packageManager: detectPackageManager(root), componentDirs: [], pageDirs: [], globalsCss: null, tailwindConfig: null, importAlias: null };
206
+ return { info, tokens: normalizeTokens(defaultRawTokens(), /* @__PURE__ */ new Map()), components: [], patterns: [] };
207
+ }
208
+ function safeJsonc(file) {
209
+ try {
210
+ return parseJsonc(file);
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ function detectImportAlias(tsconfig) {
216
+ if (!tsconfig || typeof tsconfig !== "object")
217
+ return null;
218
+ const paths = tsconfig.compilerOptions?.paths;
219
+ if (!paths)
220
+ return null;
221
+ for (const [pattern, targets] of Object.entries(paths)) {
222
+ const star = pattern.indexOf("*");
223
+ if (star < 0 || !targets?.[0])
224
+ continue;
225
+ const prefix = pattern.slice(0, star);
226
+ const target = targets[0].slice(0, targets[0].indexOf("*")).replace(/^\.\//, "").replace(/\/$/, "");
227
+ if (prefix && target !== void 0)
228
+ return { prefix, target };
229
+ }
230
+ return null;
231
+ }
232
+ function pickGlobalsCss(root, cssFiles) {
233
+ const preferred = ["app/globals.css", "src/app/globals.css", "src/styles/globals.css", "styles/globals.css", "app/global.css", "src/index.css", "src/styles/index.css"].find((file) => cssFiles.includes(file));
234
+ if (preferred)
235
+ return preferred;
236
+ for (const file of cssFiles) {
237
+ const body = readText(join2(root, file));
238
+ if (/--[\w-]+\s*:/.test(body) || /@tailwind\s|@import\s+["']tailwindcss["']/.test(body))
239
+ return file;
240
+ }
241
+ return cssFiles[0] ?? null;
242
+ }
243
+ function detectTailwind(deps, css2, config) {
244
+ const version = deps.tailwindcss ?? "";
245
+ if (/^\D*4\./.test(version) || /@import\s+["']tailwindcss["']/.test(css2) || /@theme\b/.test(css2))
246
+ return "v4";
247
+ if (/^\D*3\./.test(version) || config || /@tailwind\s/.test(css2))
248
+ return "v3";
249
+ return null;
250
+ }
251
+ function parseCssTokens(root, relPath, variables) {
252
+ const source = readText(join2(root, relPath));
253
+ const entries = [];
254
+ const regex = /--([\w-]+)\s*:\s*([^;]+);/g;
255
+ for (const match of source.matchAll(regex)) {
256
+ const value2 = match[2].trim().replace(/\s+/g, " ");
257
+ if (value2)
258
+ entries.push({ name: match[1], value: value2, index: match.index ?? 0 });
259
+ }
260
+ const rawMap = new Map(entries.map((entry) => [entry.name, entry.value]));
261
+ for (const [name, value2] of rawMap)
262
+ variables.set(name, resolveCssValue(value2, rawMap));
263
+ const tokens = entries.map((entry) => ({ name: entry.name, value: resolveCssValue(entry.value, rawMap), type: classifyToken(entry.name, entry.value), source: relPath + ":" + lineAt(source, entry.index).line, status: "confirmed" }));
264
+ const known = new Set(tokens.map((token) => token.value.toLowerCase()));
265
+ const seen = /* @__PURE__ */ new Set();
266
+ for (const match of source.matchAll(/#([0-9a-fA-F]{3,8})\b/g)) {
267
+ const value2 = "#" + match[1].toLowerCase();
268
+ if (known.has(value2) || seen.has(value2))
269
+ continue;
270
+ seen.add(value2);
271
+ tokens.push({ name: "literal." + value2, value: value2, type: "color", source: relPath + ":" + lineAt(source, match.index ?? 0).line, status: "candidate" });
272
+ }
273
+ return tokens;
274
+ }
275
+ function classifyToken(name, value2) {
276
+ const n = name.toLowerCase();
277
+ const v = value2.trim().toLowerCase();
278
+ if (/^(#|rgb|hsl|oklch|lab|color\()/.test(v) || /(color|colour|background|foreground|border|primary|brand|accent|success|warning|danger|muted|surface|text)/.test(n))
279
+ return "color";
280
+ if (/radius|rounded/.test(n))
281
+ return "radius";
282
+ if (/font-size|text-size|textsize/.test(n))
283
+ return "fontSize";
284
+ if (/font|family/.test(n))
285
+ return "font";
286
+ if (/space|spacing|gap|padding|margin/.test(n))
287
+ return "spacing";
288
+ if (/shadow|elevation/.test(n))
289
+ return "shadow";
290
+ return "other";
291
+ }
292
+ function parseTailwindConfig(root, relPath) {
293
+ const source = readText(join2(root, relPath));
294
+ const tokens = [];
295
+ const add = (section, type, prefix) => {
296
+ const body = extractSection(source, section);
297
+ if (!body)
298
+ return;
299
+ const object = parseObjectBody(body);
300
+ addObjectTokens(tokens, object, prefix, type, relPath);
301
+ };
302
+ add("colors", "color", "color");
303
+ add("borderRadius", "radius", "radius");
304
+ add("spacing", "spacing", "space");
305
+ add("fontFamily", "font", "font");
306
+ add("fontSize", "fontSize", "fontSize");
307
+ add("boxShadow", "shadow", "shadow");
308
+ return tokens;
309
+ }
310
+ function extractSection(source, key) {
311
+ const match = new RegExp("\\b" + key + "\\s*:\\s*\\{").exec(source);
312
+ if (!match)
313
+ return null;
314
+ const open = source.indexOf("{", match.index + match[0].length - 1);
315
+ if (open < 0)
316
+ return null;
317
+ const close = findMatching(source, open, "{", "}");
318
+ return close < 0 ? null : source.slice(open + 1, close);
319
+ }
320
+ function findMatching(source, open, openChar, closeChar) {
321
+ let depth = 0;
322
+ let quote = null;
323
+ for (let i = open; i < source.length; i++) {
324
+ const char = source[i];
325
+ if (quote) {
326
+ if (char === "\\") {
327
+ i++;
328
+ continue;
329
+ }
330
+ if (char === quote)
331
+ quote = null;
332
+ continue;
333
+ }
334
+ if (char === '"' || char === "'") {
335
+ quote = char;
336
+ continue;
337
+ }
338
+ if (char === openChar)
339
+ depth++;
340
+ if (char === closeChar) {
341
+ depth--;
342
+ if (depth === 0)
343
+ return i;
344
+ }
345
+ }
346
+ return -1;
347
+ }
348
+ function parseObjectBody(body) {
349
+ const result = {};
350
+ let i = 0;
351
+ while (i < body.length) {
352
+ while (i < body.length && /[\s,]/.test(body[i]))
353
+ i++;
354
+ if (i >= body.length)
355
+ break;
356
+ let key = "";
357
+ if (body[i] === '"' || body[i] === "'") {
358
+ const quote = body[i++];
359
+ while (i < body.length && body[i] !== quote)
360
+ key += body[i++];
361
+ i++;
362
+ } else {
363
+ while (i < body.length && body[i] !== ":" && body[i] !== ",")
364
+ key += body[i++];
365
+ }
366
+ key = key.trim();
367
+ while (i < body.length && /\s/.test(body[i]))
368
+ i++;
369
+ if (!key || body[i] !== ":") {
370
+ while (i < body.length && body[i] !== ",")
371
+ i++;
372
+ i++;
373
+ continue;
374
+ }
375
+ i++;
376
+ while (i < body.length && /\s/.test(body[i]))
377
+ i++;
378
+ if (body[i] === "{") {
379
+ const end = findMatching(body, i, "{", "}");
380
+ if (end < 0)
381
+ break;
382
+ result[key] = parseObjectBody(body.slice(i + 1, end));
383
+ i = end + 1;
384
+ } else if (body[i] === '"' || body[i] === "'") {
385
+ const quote = body[i++];
386
+ let value2 = "";
387
+ while (i < body.length && body[i] !== quote) {
388
+ if (body[i] === "\\")
389
+ i++;
390
+ value2 += body[i++];
391
+ }
392
+ i++;
393
+ result[key] = value2;
394
+ } else {
395
+ let value2 = "";
396
+ let depth = 0;
397
+ while (i < body.length) {
398
+ const char = body[i];
399
+ if (char === "[" || char === "(")
400
+ depth++;
401
+ if (char === "]" || char === ")")
402
+ depth = Math.max(0, depth - 1);
403
+ if (char === "," && depth === 0)
404
+ break;
405
+ value2 += char;
406
+ i++;
407
+ }
408
+ result[key] = value2.trim();
409
+ }
410
+ while (i < body.length && body[i] !== ",")
411
+ i++;
412
+ i++;
413
+ }
414
+ return result;
415
+ }
416
+ function addObjectTokens(tokens, object, prefix, type, source) {
417
+ const visit = (value2, current) => {
418
+ for (const [key, child] of Object.entries(value2)) {
419
+ const name = key === "DEFAULT" ? current : current ? current + "." + key : key;
420
+ if (typeof child === "object") {
421
+ visit(child, name);
422
+ continue;
423
+ }
424
+ const extracted = type === "color" ? extractColorValue(child) : type === "font" ? extractFontValue(child) : extractFirstValue(child);
425
+ if (extracted)
426
+ tokens.push({ name: prefix + "." + name, value: extracted, type, source, status: "confirmed" });
427
+ }
428
+ };
429
+ visit(object, "");
430
+ }
431
+ function extractColorValue(value2) {
432
+ const trimmed = value2.trim();
433
+ if (/^(#|rgb|hsl|oklch|lab|color\()/.test(trimmed))
434
+ return trimmed;
435
+ return trimmed.match(/['"]([^'"]+)['"]/)?.[1] ?? null;
436
+ }
437
+ function extractFontValue(value2) {
438
+ const quoted = [...value2.matchAll(/['"]([^'"]+)['"]/g)].map((match) => match[1]);
439
+ return quoted.length ? quoted.join(", ") : extractFirstValue(value2);
440
+ }
441
+ function extractFirstValue(value2) {
442
+ const quoted = value2.match(/['"]([^'"]+)['"]/);
443
+ if (quoted)
444
+ return quoted[1];
445
+ const trimmed = value2.trim();
446
+ return /^(\d|#|rgb|hsl|oklch)/.test(trimmed) ? trimmed : null;
447
+ }
448
+ function dedupeTokens(tokens) {
449
+ const seen = /* @__PURE__ */ new Set();
450
+ return tokens.filter((token) => {
451
+ const key = token.name + "|" + token.value;
452
+ if (seen.has(key))
453
+ return false;
454
+ seen.add(key);
455
+ return true;
456
+ });
457
+ }
458
+ function normalizeTokens(raw, variables) {
459
+ const colors = /* @__PURE__ */ new Map();
460
+ const radii = /* @__PURE__ */ new Map();
461
+ const spacing = /* @__PURE__ */ new Map();
462
+ const fonts = /* @__PURE__ */ new Map();
463
+ const fontSizes = /* @__PURE__ */ new Map();
464
+ const shadows = /* @__PURE__ */ new Map();
465
+ for (const token of raw) {
466
+ const value2 = resolveCssValue(token.value, variables);
467
+ if (token.type === "color" && token.status === "confirmed" && !token.name.startsWith("literal."))
468
+ colors.set(token.name.toLowerCase(), value2);
469
+ if (token.type === "radius")
470
+ radii.set(token.name.toLowerCase(), value2);
471
+ if (token.type === "spacing")
472
+ spacing.set(token.name.toLowerCase(), value2);
473
+ if (token.type === "font")
474
+ fonts.set(token.name.toLowerCase(), value2);
475
+ if (token.type === "fontSize")
476
+ fontSizes.set(token.name.toLowerCase(), value2);
477
+ if (token.type === "shadow")
478
+ shadows.set(token.name.toLowerCase(), value2);
479
+ }
480
+ const pick = (map, patterns, fallback) => {
481
+ for (const pattern of patterns)
482
+ for (const [name, value2] of map)
483
+ if (name.includes(pattern) && value2)
484
+ return value2;
485
+ return fallback;
486
+ };
487
+ const dim = (map, key, fallback) => {
488
+ for (const [name, value2] of map)
489
+ if (name.endsWith("-" + key) || name.endsWith("." + key) || name === key)
490
+ return value2;
491
+ return fallback;
492
+ };
493
+ const normalizedSpacing = {};
494
+ for (const [name, value2] of spacing) {
495
+ const key = name.split(/[-.]/).pop() ?? name;
496
+ if (numericPx(value2) !== null)
497
+ normalizedSpacing[key] = value2;
498
+ }
499
+ if (Object.keys(normalizedSpacing).length === 0)
500
+ Object.assign(normalizedSpacing, { "4": "4px", "8": "8px", "12": "12px", "16": "16px", "24": "24px", "32": "32px" });
501
+ const normalizedFontSizes = {};
502
+ for (const [name, value2] of fontSizes)
503
+ normalizedFontSizes[name.split(/[-.]/).pop() ?? name] = value2;
504
+ return {
505
+ colors: { background: pick(colors, ["background", "bg", "canvas"], "#f8fafc"), surface: pick(colors, ["surface", "card", "panel"], "#ffffff"), primary: pick(colors, ["primary", "brand", "accent"], "#2f5cff"), text: pick(colors, ["foreground", "text", "ink", "on-background"], "#0b1a35"), muted: pick(colors, ["muted", "secondary", "subtle"], "#5a7094"), border: pick(colors, ["border", "line", "divider"], "#e2e8f0"), success: pick(colors, ["success", "positive", "ok"], "#0c7354"), warning: pick(colors, ["warning", "warn", "caution"], "#946007"), danger: pick(colors, ["danger", "destructive", "error"], "#bd332e") },
506
+ radius: { sm: dim(radii, "sm", "4px"), md: dim(radii, "md", "8px"), lg: dim(radii, "lg", "12px") },
507
+ spacing: normalizedSpacing,
508
+ fonts: { sans: pick(fonts, ["sans", "body"], "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif"), serif: pick(fonts, ["serif"], "Georgia, serif"), mono: pick(fonts, ["mono", "code"], "ui-monospace, SFMono-Regular, Menlo, monospace") },
509
+ fontSizes: normalizedFontSizes,
510
+ shadows: Object.fromEntries(shadows),
511
+ raw
512
+ };
513
+ }
514
+ function defaultRawTokens() {
515
+ const source = "UIfying dashboard baseline";
516
+ const add = (name, value2, type) => ({ name, value: value2, type, source, status: "confirmed" });
517
+ return [
518
+ add("color.background", "#f8fafc", "color"),
519
+ add("color.surface", "#ffffff", "color"),
520
+ add("color.primary", "#2f5cff", "color"),
521
+ add("color.text", "#0b1a35", "color"),
522
+ add("color.muted", "#5a7094", "color"),
523
+ add("color.border", "#e2e8f0", "color"),
524
+ add("color.success", "#0c7354", "color"),
525
+ add("color.warning", "#946007", "color"),
526
+ add("color.danger", "#bd332e", "color"),
527
+ add("radius.sm", "4px", "radius"),
528
+ add("radius.md", "8px", "radius"),
529
+ add("radius.lg", "12px", "radius"),
530
+ add("space.4", "4px", "spacing"),
531
+ add("space.8", "8px", "spacing"),
532
+ add("space.12", "12px", "spacing"),
533
+ add("space.16", "16px", "spacing"),
534
+ add("space.24", "24px", "spacing"),
535
+ add("space.32", "32px", "spacing"),
536
+ add("font.sans", "ui-sans-serif, system-ui, sans-serif", "font"),
537
+ add("fontSize.sm", "12px", "fontSize"),
538
+ add("fontSize.base", "14px", "fontSize"),
539
+ add("fontSize.lg", "16px", "fontSize"),
540
+ add("fontSize.xl", "24px", "fontSize"),
541
+ add("shadow.card", "0 1px 2px rgba(15, 23, 42, 0.06)", "shadow")
542
+ ];
543
+ }
544
+ function scanComponents(root, files, alias) {
545
+ const components = [];
546
+ for (const file of files) {
547
+ if (!/\.(tsx|jsx)$/.test(file))
548
+ continue;
549
+ for (const component of extractComponents(file, readText(join2(root, file))))
550
+ components.push(component);
551
+ }
552
+ for (const component of components) {
553
+ const usages = [];
554
+ for (const file of files) {
555
+ if (!/\.(tsx|jsx|ts)$/.test(file) || file === component.path)
556
+ continue;
557
+ for (const imported of collectImports(readText(join2(root, file)))) {
558
+ if (imported.imported === component.name)
559
+ usages.push({ file, specifier: imported.specifier });
560
+ }
561
+ }
562
+ component.usedBy = [...new Set(usages.map((usage) => usage.file))];
563
+ const counts = /* @__PURE__ */ new Map();
564
+ for (const usage of usages)
565
+ counts.set(usage.specifier, (counts.get(usage.specifier) ?? 0) + 1);
566
+ component.importPath = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? fallbackImportPath(component.path, alias);
567
+ component.status = /(^|\/)ui\//.test(component.path) ? "approved" : "candidate";
568
+ }
569
+ return components.sort((a, b) => a.path.localeCompare(b.path) || a.name.localeCompare(b.name));
570
+ }
571
+ function extractComponents(file, content) {
572
+ const result = [];
573
+ const seen = /* @__PURE__ */ new Set();
574
+ for (const match of content.matchAll(/export\s+(default\s+)?(?:async\s+)?function\s+([A-Z][A-Za-z0-9_]*)\s*\(([^)]*)\)/g)) {
575
+ if (seen.has(match[2]))
576
+ continue;
577
+ if (!hasJsx(content.slice(match.index ?? 0, (match.index ?? 0) + 5e3)))
578
+ continue;
579
+ seen.add(match[2]);
580
+ result.push(makeComponent(match[2], file, match[1] ? "default" : "named", match[3], content));
581
+ }
582
+ for (const match of content.matchAll(/export\s+(default\s+)?const\s+([A-Z][A-Za-z0-9_]*)\s*=/g)) {
583
+ if (seen.has(match[2]))
584
+ continue;
585
+ if (!hasJsx(content.slice(match.index ?? 0, (match.index ?? 0) + 5e3)))
586
+ continue;
587
+ seen.add(match[2]);
588
+ result.push(makeComponent(match[2], file, match[1] ? "default" : "named", "", content));
589
+ }
590
+ return result;
591
+ }
592
+ function hasJsx(value2) {
593
+ return /<[A-Za-z][^>]*>/.test(value2) || /React\.createElement/.test(value2);
594
+ }
595
+ function makeComponent(name, file, exportKind, params, content) {
596
+ const props = [];
597
+ if (params.includes("{")) {
598
+ const inside = params.slice(params.indexOf("{") + 1, params.lastIndexOf("}"));
599
+ for (const part of inside.split(",")) {
600
+ const clean = part.trim().replace(/^\.\.\./, "").split(":")[0].split("=")[0].trim();
601
+ if (/^[A-Za-z_$][\w$]*$/.test(clean))
602
+ props.push(clean);
603
+ }
604
+ } else {
605
+ const typeName = params.split(":").pop()?.trim();
606
+ if (typeName) {
607
+ const typeRegex = new RegExp("(?:interface|type)\\s+" + typeName.replace(/[^\w]/g, "") + "[^\\{]*\\{([\\s\\S]*?)\\}");
608
+ const typeMatch = typeRegex.exec(content);
609
+ if (typeMatch)
610
+ for (const line of typeMatch[1].split("\n")) {
611
+ const prop = line.match(/^\s*([A-Za-z_$][\w$]*)\??\s*:/);
612
+ if (prop)
613
+ props.push(prop[1]);
614
+ }
615
+ }
616
+ }
617
+ return { name, path: file, exportKind, props: [...new Set(props)], variants: extractVariants(content), usedBy: [], status: "candidate" };
618
+ }
619
+ function extractVariants(source) {
620
+ const result = {};
621
+ const match = /cva\s*\(/.exec(source);
622
+ if (!match)
623
+ return result;
624
+ const open = source.indexOf("(", match.index);
625
+ const close = findMatching(source, open, "(", ")");
626
+ if (close < 0)
627
+ return result;
628
+ const call = source.slice(open + 1, close);
629
+ const variantsMatch = /variants\s*:\s*\{/.exec(call);
630
+ if (!variantsMatch)
631
+ return result;
632
+ const openBrace = call.indexOf("{", variantsMatch.index);
633
+ const closeBrace = findMatching(call, openBrace, "{", "}");
634
+ if (closeBrace < 0)
635
+ return result;
636
+ const groups = parseObjectBody(call.slice(openBrace + 1, closeBrace));
637
+ for (const [key, value2] of Object.entries(groups))
638
+ if (typeof value2 === "object")
639
+ result[key] = Object.keys(value2);
640
+ return result;
641
+ }
642
+ function collectImports(content) {
643
+ const result = [];
644
+ for (const match of content.matchAll(/import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"]/g)) {
645
+ const clause = match[1].trim();
646
+ const specifier = match[2];
647
+ const beforeBraces = clause.split("{")[0].trim();
648
+ if (beforeBraces && /^[A-Za-z_$][\w$]*$/.test(beforeBraces) && beforeBraces !== "*")
649
+ result.push({ imported: beforeBraces, specifier });
650
+ const named = clause.match(/\{([^}]*)\}/);
651
+ if (named)
652
+ for (const part of named[1].split(",")) {
653
+ const name = part.trim().split(/\s+as\s+/)[0].trim();
654
+ if (/^[A-Za-z_$][\w$]*$/.test(name))
655
+ result.push({ imported: name, specifier });
656
+ }
657
+ }
658
+ return result;
659
+ }
660
+ function fallbackImportPath(file, alias) {
661
+ const noExt = extensionless(file);
662
+ if (alias && noExt.startsWith(alias.target + "/"))
663
+ return alias.prefix + noExt.slice(alias.target.length + 1);
664
+ return noExt;
665
+ }
666
+ function scanPages(root, files, pageDirs, components) {
667
+ const pages = [];
668
+ for (const file of files) {
669
+ if (!isPageFile(file, pageDirs))
670
+ continue;
671
+ const content = readText(join2(root, file));
672
+ const tags = [...content.matchAll(/<([A-Z][A-Za-z0-9_]*)/g)].map((match) => match[1]);
673
+ pages.push({ path: file, type: classifyPage(file, content, tags), components: [...new Set(tags)].sort(), reference: false });
674
+ }
675
+ const preferred = pages.find((page) => page.type === "data-list" && /orders|users|list/.test(page.path)) ?? pages.find((page) => page.type === "data-list");
676
+ if (preferred)
677
+ preferred.reference = true;
678
+ return pages;
679
+ }
680
+ function isPageFile(file, pageDirs) {
681
+ if (!/\.(tsx|jsx)$/.test(file))
682
+ return false;
683
+ const base = basename(file);
684
+ if (base === "page.tsx" || base === "page.jsx")
685
+ return true;
686
+ if (pageDirs.some((dir) => file.startsWith(dir + "/"))) {
687
+ if (/\.(test|spec|stories)\./.test(file))
688
+ return false;
689
+ if (/(^|\/)(_app|_document|api)(\/|\.)/.test(file))
690
+ return false;
691
+ return true;
692
+ }
693
+ return false;
694
+ }
695
+ function classifyPage(file, content, tags) {
696
+ const path = file.toLowerCase();
697
+ if (/(login|signin|sign-in|register|auth)/.test(path))
698
+ return "auth";
699
+ if (/settings/.test(path))
700
+ return "settings";
701
+ if (/\[.*\]/.test(path) || /detail/.test(path))
702
+ return "detail";
703
+ if (tags.some((tag) => /DataTable|Table/.test(tag)) || /<table\b/.test(content))
704
+ return "data-list";
705
+ if (/dashboard/.test(path) || tags.some((tag) => /Chart|Stat|Kpi/.test(tag)))
706
+ return "dashboard";
707
+ if (tags.some((tag) => /Form|Input|Field/.test(tag)) || /<form\b/.test(content))
708
+ return "form";
709
+ return "page";
710
+ }
711
+
712
+ // ../core/dist/spec.js
713
+ import { existsSync as existsSync4 } from "node:fs";
714
+ import { join as join4 } from "node:path";
715
+
716
+ // ../core/dist/preview.js
717
+ function css(value2) {
718
+ return String(value2).replace(/[;{}<>"']/g, "").trim();
719
+ }
720
+ function renderPreview(spec) {
721
+ const c = spec.tokens.colors;
722
+ const r = spec.tokens.radius;
723
+ const font = spec.tokens.fonts.sans;
724
+ const statusLabel = spec.status === "approved" ? "APPROVED" : "DRAFT";
725
+ const statusClass = spec.status === "approved" ? "ok" : "draft";
726
+ const project = escapeHtml(spec.project.name);
727
+ const items = spec.items.map((item) => "<tr><td><code>" + escapeHtml(item.id) + "</code></td><td>" + escapeHtml(item.title) + "</td><td>" + item.status + "</td></tr>").join("");
728
+ const kpis = [
729
+ ["Revenue", "$48.2k", "+12.4%", [34, 48, 42, 58, 52, 71, 66, 84]],
730
+ ["Active users", "1,284", "+8.2%", [28, 36, 44, 40, 55, 58, 64, 72]],
731
+ ["Conversion", "3.8%", "+0.6%", [42, 38, 48, 45, 54, 51, 62, 66]],
732
+ ["Churn", "1.2%", "-0.3%", [72, 66, 68, 58, 54, 49, 44, 38]]
733
+ ].map(([label, value2, delta, bars]) => '<article class="u-kpi"><div class="u-kpi__top"><span>' + label + "</span><i></i></div><strong>" + value2 + '</strong><div class="u-kpi__meta"><em>' + delta + '</em><span>vs last month</span></div><div class="u-spark">' + bars.map((height) => '<i style="height:' + height + '%"></i>').join("") + "</div></article>").join("");
734
+ const rows = [
735
+ ["AM", "Avery Moss", "avery@northwind.dev", "Owner", "Active", "2m ago", true],
736
+ ["JL", "Jonas Lee", "jonas@northwind.dev", "Admin", "Active", "1h ago", false],
737
+ ["RK", "Rina Kato", "rina@northwind.dev", "Editor", "Invited", "3h ago", false],
738
+ ["TS", "Tom\xE1s Silva", "tomas@northwind.dev", "Viewer", "Active", "1d ago", false]
739
+ ].map(([initials, name, email, role, state, seen, selected]) => '<div class="u-tr' + (selected ? " is-selected" : "") + '"><span class="u-check">' + (selected ? "\u2713" : "") + '</span><span class="u-person"><b class="u-avatar">' + initials + "</b><span><strong>" + name + "</strong><small>" + email + '</small></span></span><span><span class="u-badge">' + role + '</span></span><span class="u-state"><i></i>' + state + '</span><span class="u-muted">' + seen + "</span></div>").join("");
740
+ const specRows = spec.items.map((item) => "<tr><td><code>" + escapeHtml(item.id) + "</code></td><td>" + escapeHtml(item.title) + "</td><td>" + item.rules.map(escapeHtml).join(" ") + "</td></tr>").join("");
741
+ const doc = [
742
+ "<!doctype html>",
743
+ '<html lang="en">',
744
+ "<head>",
745
+ '<meta charset="utf-8">',
746
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
747
+ '<meta name="robots" content="noindex, nofollow">',
748
+ "<title>" + project + " \u2014 UI spec preview</title>",
749
+ "<style>",
750
+ ":root{--u-bg:" + css(c.background) + ";--u-surface:" + css(c.surface) + ";--u-primary:" + css(c.primary) + ";--u-ink:" + css(c.text) + ";--u-muted:" + css(c.muted) + ";--u-border:" + css(c.border) + ";--u-ok:" + css(c.success) + ";--u-warn:" + css(c.warning) + ";--u-danger:" + css(c.danger) + ";--u-radius:" + css(r.md) + ";--u-radius-sm:" + css(r.sm) + ";--u-radius-lg:" + css(r.lg) + ";--u-font:" + css(font) + ";}",
751
+ "*{box-sizing:border-box}html{background:var(--u-bg);color-scheme:light}body{margin:0;background:var(--u-bg);color:var(--u-ink);font-family:var(--u-font);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}button{font:inherit;cursor:pointer}code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.86em}",
752
+ ".u-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 22px;border-bottom:1px solid var(--u-border);background:var(--u-surface);font-size:11px;color:var(--u-muted)}.u-bar__brand{display:flex;align-items:center;gap:10px;color:var(--u-ink);font-weight:650}.u-mark{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:var(--u-primary);color:#fff;font-size:9px;font-weight:700;letter-spacing:.02em}.u-status{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid var(--u-border);border-radius:999px;font-size:10px;letter-spacing:.05em;text-transform:uppercase;color:var(--u-muted)}.u-status i{width:5px;height:5px;border-radius:50%;background:var(--u-warn)}.u-status.ok i{background:var(--u-ok)}",
753
+ ".u-wrap{width:min(calc(100% - 32px),1240px);margin:0 auto;padding:28px 0 54px}.u-intro{max-width:720px;margin:0 auto 24px;text-align:center}.u-kicker{margin:0 0 8px;color:var(--u-primary);font-size:10px;font-weight:650;letter-spacing:.12em;text-transform:uppercase}.u-intro h1{margin:0;font-size:clamp(26px,4vw,42px);font-weight:650;letter-spacing:-.04em;line-height:1.08}.u-intro p{margin:10px auto 0;max-width:58ch;color:var(--u-muted);font-size:13px}.u-approve{display:inline-flex;align-items:center;gap:7px;margin-top:14px;padding:7px 10px;border:1px solid var(--u-border);border-radius:999px;background:var(--u-surface);font-size:10px;color:var(--u-muted)}.u-approve code{color:var(--u-ink)}",
754
+ ".u-shell{display:grid;grid-template-columns:220px minmax(0,1fr);overflow:hidden;border:1px solid var(--u-border);border-radius:16px;background:var(--u-surface);box-shadow:0 24px 60px -36px rgba(15,23,42,.35)}",
755
+ ".u-side{display:flex;flex-direction:column;padding:16px 12px;background:color-mix(in srgb,var(--u-primary) 5%,var(--u-surface));border-right:1px solid var(--u-border)}.u-brand{display:flex;align-items:center;gap:8px;padding:2px 8px;font-weight:700;letter-spacing:-.02em}.u-brand b{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;background:var(--u-ink);color:var(--u-surface);font-size:9px}.u-nav{display:grid;gap:2px;margin-top:22px}.u-nav span{padding:8px 10px;border-radius:7px;color:var(--u-muted);font-size:12px}.u-nav span.is-active{background:var(--u-surface);color:var(--u-ink);font-weight:650;box-shadow:0 1px 3px rgba(15,23,42,.08);border-left:2px solid var(--u-primary)}.u-nav small{display:block;margin:14px 10px 5px;color:var(--u-muted);font-size:9px;font-weight:600;letter-spacing:.1em;text-transform:uppercase}.u-side__foot{margin-top:auto;padding:10px;border:1px solid var(--u-border);border-radius:9px;background:var(--u-surface);font-size:10px;color:var(--u-muted)}.u-side__foot strong{display:block;margin-top:3px;color:var(--u-ink);font-size:14px}.u-side__meter{height:5px;margin-top:7px;border-radius:99px;background:var(--u-border);overflow:hidden}.u-side__meter i{display:block;width:62%;height:100%;background:var(--u-primary)}",
756
+ ".u-main{min-width:0;padding:16px 20px 22px}.u-top{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:10px;border-bottom:1px solid var(--u-border);color:var(--u-muted);font-size:10px}.u-top__right{display:flex;align-items:center;gap:8px}.u-search{padding:5px 9px;border:1px solid var(--u-border);border-radius:999px;color:var(--u-muted);font-size:10px;background:var(--u-bg)}.u-avatar{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:color-mix(in srgb,var(--u-primary) 14%,var(--u-surface));color:var(--u-primary);font-size:9px;font-weight:700;font-style:normal}.u-pagehead{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin:18px 0 14px}.u-pagehead h2{margin:0;font-size:24px;line-height:1.1;letter-spacing:-.04em}.u-pagehead p{margin:4px 0 0;color:var(--u-muted);font-size:11px}.u-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 10px;border:1px solid var(--u-primary);border-radius:7px;background:var(--u-primary);color:#fff;font-size:11px;font-weight:650}.u-btn--ghost{background:var(--u-surface);color:var(--u-muted);border-color:var(--u-border)}",
757
+ ".u-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.u-kpi{padding:11px;border:1px solid var(--u-border);border-radius:var(--u-radius);background:var(--u-surface)}.u-kpi__top{display:flex;align-items:center;justify-content:space-between;color:var(--u-muted);font-size:10px}.u-kpi__top i{width:6px;height:6px;border:1px solid var(--u-border)}.u-kpi strong{display:block;margin-top:5px;font-size:22px;font-weight:650;letter-spacing:-.04em;line-height:1}.u-kpi__meta{display:flex;align-items:center;gap:5px;margin-top:4px;color:var(--u-muted);font-size:9px}.u-kpi__meta em{color:var(--u-ok);font-style:normal;font-weight:650}.u-spark{display:flex;align-items:flex-end;gap:3px;height:24px;margin-top:7px}.u-spark i{flex:1;min-height:2px;border-radius:2px 2px 0 0;background:color-mix(in srgb,var(--u-primary) 55%,var(--u-surface))}.u-spark i:last-child{background:var(--u-primary)}",
758
+ ".u-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,.9fr);gap:10px;margin-top:10px}.u-card{min-width:0;padding:12px;border:1px solid var(--u-border);border-radius:var(--u-radius);background:var(--u-surface)}.u-card__head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.u-card__head h3{margin:0;font-size:12px;font-weight:650}.u-card__head span{color:var(--u-muted);font-size:9px}.u-chart{width:100%;height:150px;overflow:visible}.u-axis{display:flex;justify-content:space-between;margin-top:4px;color:var(--u-muted);font-size:8px}.u-legend{display:inline-flex;align-items:center;gap:4px;color:var(--u-muted);font-size:9px}.u-legend i{width:6px;height:6px;border-radius:2px;background:var(--u-primary)}.u-tip{position:relative;margin-top:-34px;margin-left:58%;width:max-content;padding:3px 5px;border-radius:4px;background:var(--u-ink);color:var(--u-surface);font-size:8px}",
759
+ ".u-tabs{display:flex;gap:8px;border-bottom:1px solid var(--u-border);margin-bottom:9px}.u-tabs span{padding:6px 2px;color:var(--u-muted);font-size:10px;border-bottom:2px solid transparent}.u-tabs span.is-active{color:var(--u-ink);border-bottom-color:var(--u-primary);font-weight:650}.u-table{border:1px solid var(--u-border);border-radius:var(--u-radius-sm);overflow:hidden}.u-tr{display:grid;grid-template-columns:14px minmax(0,1.4fr) .55fr .58fr .45fr;align-items:center;gap:7px;padding:7px 8px;border-top:1px solid var(--u-border);font-size:9px;color:var(--u-muted)}.u-tr:first-child{border-top:0}.u-tr.is-selected{background:color-mix(in srgb,var(--u-primary) 7%,var(--u-surface));color:var(--u-ink)}.u-check{display:inline-flex;align-items:center;justify-content:center;width:11px;height:11px;border:1px solid var(--u-border);border-radius:3px;color:var(--u-primary);font-size:8px}.u-tr.is-selected .u-check{border-color:var(--u-primary);background:var(--u-primary);color:#fff}.u-person{display:flex;align-items:center;gap:6px;min-width:0}.u-person>span{display:grid;min-width:0}.u-person strong{font-size:9px;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.u-person small{color:var(--u-muted);font-size:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.u-avatar{width:19px;height:19px;font-size:7px}.u-badge{display:inline-block;padding:2px 5px;border:1px solid var(--u-border);border-radius:4px;font-size:8px;color:var(--u-muted)}.u-state{display:inline-flex;align-items:center;gap:3px;font-size:8px}.u-state i{width:4px;height:4px;border-radius:50%;background:var(--u-ok)}.u-muted{color:var(--u-muted)}",
760
+ ".u-lower{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);gap:10px;margin-top:10px}.u-back{display:inline-flex;align-items:center;gap:5px;margin-bottom:7px;color:var(--u-primary);font-size:10px;font-weight:650}.u-crumb{color:var(--u-muted);font-size:9px}.u-subtitle{display:flex;align-items:center;gap:8px;margin-top:3px;font-size:17px;font-weight:650;letter-spacing:-.03em}.u-lines{display:grid;gap:6px;margin-top:9px}.u-lines label{color:var(--u-muted);font-size:8px;text-transform:uppercase;letter-spacing:.08em}.u-lines div{height:24px;border:1px solid var(--u-border);border-radius:5px;background:var(--u-bg)}.u-form{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:8px}.u-form .u-lines{margin:0}.u-modal{padding:0;overflow:hidden}.u-modal__head{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border-bottom:1px solid var(--u-border);font-size:12px;font-weight:650}.u-modal__body{display:grid;gap:8px;padding:12px}.u-modal__foot{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-top:1px solid var(--u-border);font-size:10px}.u-danger{color:var(--u-danger)}.u-modal__save{padding:5px 9px;border-radius:5px;background:var(--u-primary);color:#fff;font-size:10px;font-weight:650}",
761
+ ".u-states{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:10px}.u-state-card{min-height:108px;display:grid;align-content:center;justify-items:center;gap:5px;text-align:center;color:var(--u-muted);font-size:10px}.u-state-card b{color:var(--u-ink);font-size:11px}.u-state-box{width:34px;height:24px;border:1px dashed var(--u-border);border-radius:5px;background:var(--u-bg)}.u-skeleton{display:grid;gap:5px;width:70%}.u-skeleton i{height:6px;border-radius:99px;background:var(--u-border)}.u-skeleton i:last-child{width:62%}.u-error{width:26px;height:26px;display:grid;place-items:center;border-radius:50%;background:color-mix(in srgb,var(--u-danger) 12%,var(--u-surface));color:var(--u-danger);font-weight:700}",
762
+ ".u-index{margin-top:24px}.u-index h2{font-size:22px;letter-spacing:-.03em;margin:0 0 4px}.u-index p{margin:0 0 12px;color:var(--u-muted);font-size:12px}.u-index table{width:100%;border-collapse:collapse;border:1px solid var(--u-border);border-radius:10px;overflow:hidden;background:var(--u-surface);font-size:11px}.u-index th{padding:9px 10px;border-bottom:1px solid var(--u-border);color:var(--u-muted);font-size:9px;text-align:left;letter-spacing:.06em;text-transform:uppercase;font-weight:600}.u-index td{padding:9px 10px;border-bottom:1px solid var(--u-border);color:var(--u-muted);vertical-align:top}.u-index tr:last-child td{border-bottom:0}.u-index td:first-child{white-space:nowrap}.u-index code{color:var(--u-primary)}",
763
+ ".u-foot{margin:24px auto 0;max-width:820px;text-align:center;color:var(--u-muted);font-size:10px}.u-foot strong{color:var(--u-ink)}",
764
+ "@media(max-width:980px){.u-shell{grid-template-columns:64px minmax(0,1fr)}.u-side{padding:12px 8px}.u-brand span,.u-nav span:not(.is-active),.u-nav small,.u-side__foot{display:none}.u-nav span{display:flex;justify-content:center;padding:8px 0;font-size:0}.u-nav span::before{content:'';width:6px;height:6px;border:1px solid currentColor;border-radius:2px}.u-nav span.is-active{display:flex}.u-nav span.is-active::after{content:'';width:14px;height:2px;background:var(--u-primary)}}",
765
+ "@media(max-width:760px){.u-wrap{width:calc(100% - 20px);padding-top:18px}.u-shell{grid-template-columns:1fr}.u-side{flex-direction:row;align-items:center;gap:8px;border-right:0;border-bottom:1px solid var(--u-border);padding:8px 10px}.u-brand span{display:inline}.u-nav{display:flex;margin:0;gap:2px;overflow:auto}.u-nav span{display:block;padding:6px 7px;font-size:9px}.u-nav span::before{display:none}.u-nav small{display:none}.u-side__foot{display:none}.u-main{padding:12px}.u-kpis{grid-template-columns:1fr 1fr}.u-grid,.u-lower,.u-states{grid-template-columns:1fr}.u-pagehead h2{font-size:20px}.u-table{overflow-x:auto}.u-tr{min-width:520px}.u-index{overflow-x:auto}.u-index table{min-width:640px}.u-intro h1{font-size:26px}}",
766
+ "@media(max-width:460px){.u-kpis{grid-template-columns:1fr}.u-form{grid-template-columns:1fr}.u-bar{padding:10px 12px}.u-bar__brand span:last-child{display:none}}",
767
+ "</style>",
768
+ "</head>",
769
+ "<body>",
770
+ '<header class="u-bar"><div class="u-bar__brand"><span class="u-mark">UI</span><span>' + project + '</span><span>UI spec preview</span></div><span class="u-status ' + statusClass + '"><i></i>' + statusLabel + " \xB7 v" + escapeHtml(spec.specVersion) + "</span></header>",
771
+ '<main class="u-wrap">',
772
+ '<header class="u-intro"><p class="u-kicker">UI-only preview / ' + escapeHtml(spec.archetype) + "</p><h1>" + project + ' UI spec</h1><p>Navigation, selected states, cards, charts, dialogs, secondary pages, back buttons, forms, and data states. Static fixtures only; no API, auth, or mutations.</p><span class="u-approve"><code>npx uifying spec approve</code> when this looks right</span></header>',
773
+ '<div class="u-shell">',
774
+ '<aside class="u-side" id="nav"><div class="u-brand"><b>N</b><span>Northwind</span></div><nav class="u-nav"><small>Workspace</small><span class="is-active">Overview</span><span>Orders</span><span>Customers</span><span>Products</span><small>Settings</small><span>Team</span><span>Billing</span></nav><div class="u-side__foot">Seats<strong>12 / 20</strong><div class="u-side__meter"><i></i></div></div></aside>',
775
+ '<section class="u-main">',
776
+ '<div class="u-top"><span>Workspace / Overview</span><span class="u-top__right"><span class="u-search">Search</span><span class="u-avatar">AM</span></span></div>',
777
+ '<div class="u-pagehead"><div><h2>Overview</h2><p>Manage access, roles, and workspace health.</p></div><div><button class="u-btn">+ New report</button></div></div>',
778
+ '<div class="u-kpis" id="card">' + kpis + "</div>",
779
+ '<div class="u-grid"><section class="u-card" id="chart"><div class="u-card__head"><h3>Revenue</h3><span class="u-legend"><i></i>Net</span></div><svg class="u-chart" viewBox="0 0 560 150" preserveAspectRatio="none" aria-hidden="true"><defs><linearGradient id="u-fill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--u-primary)" stop-opacity=".22"/><stop offset="1" stop-color="var(--u-primary)" stop-opacity="0"/></linearGradient></defs><path d="M0 120H560M0 80H560M0 40H560" stroke="var(--u-border)" stroke-width="1"/><path d="M0 112 C40 104 58 108 88 82 S146 58 178 70 S226 98 260 62 S308 36 348 48 S408 22 442 32 S500 18 560 24 L560 150 L0 150 Z" fill="url(#u-fill)"/><path d="M0 112 C40 104 58 108 88 82 S146 58 178 70 S226 98 260 62 S308 36 348 48 S408 22 442 32 S500 18 560 24" fill="none" stroke="var(--u-primary)" stroke-width="2"/><circle cx="442" cy="32" r="4" fill="var(--u-surface)" stroke="var(--u-primary)" stroke-width="2"/></svg><div class="u-tip">$6.4k</div><div class="u-axis"><span>Mon</span><span>Wed</span><span>Fri</span><span>Sun</span></div></section>',
780
+ '<section class="u-card"><div class="u-card__head"><h3>Top channels</h3><span>Last 30 days</span></div><div class="u-lines"><label>Direct</label><div style="background:var(--u-primary);border-color:var(--u-primary);width:78%"></div><label>Organic</label><div style="width:61%"></div><label>Referral</label><div style="width:42%"></div></div><div style="margin-top:11px;color:var(--u-muted);font-size:9px">4 channels \xB7 12,840 visits</div></section></div>',
781
+ '<section class="u-card" id="selected" style="margin-top:10px"><div class="u-card__head"><h3>Recent members</h3><span>Selected row uses SEL-01</span></div><div class="u-tabs"><span class="is-active">All</span><span>Paid</span><span>Pending</span></div><div class="u-table">' + rows + "</div></section>",
782
+ "</section></div>",
783
+ '<div class="u-lower"><section class="u-card" id="page"><span class="u-back">\u2190 Back to orders</span><div class="u-crumb">Orders / #1042</div><div class="u-subtitle">Order #1042 <span class="u-badge">Paid</span></div><div class="u-tabs" style="margin-top:10px"><span class="is-active">Summary</span><span>Items</span><span>Activity</span></div><div class="u-form"><div class="u-lines"><label>Customer</label><div></div></div><div class="u-lines"><label>Status</label><div></div></div><div class="u-lines"><label>Total</label><div></div></div><div class="u-lines"><label>Created</label><div></div></div></div></section>',
784
+ '<section class="u-card u-modal" id="modal"><div class="u-modal__head">Edit user <span class="u-muted">\xD7</span></div><div class="u-modal__body"><div class="u-lines"><label>Role</label><div></div></div><div class="u-lines"><label>Status</label><div></div></div></div><div class="u-modal__foot"><span class="u-danger">Remove access</span><span class="u-modal__save">Save changes</span></div></section></div>',
785
+ '<div class="u-states" id="states"><section class="u-card u-state-card"><div class="u-state-box"></div><b>Empty</b><span>Nothing here yet. Use the same card shell.</span></section><section class="u-card u-state-card"><div class="u-skeleton"><i></i><i></i><i></i></div><b>Loading</b><span>Skeleton matches the final layout.</span></section><section class="u-card u-state-card"><div class="u-error">!</div><b>Error</b><span>Actionable message and retry.</span></section></div>',
786
+ '<section class="u-index" id="spec-index"><h2>Spec index</h2><p>Each rule is versioned and traceable. The checker cites these IDs.</p><table><thead><tr><th>ID</th><th>Rule</th><th>Definition</th></tr></thead><tbody>' + specRows + "</tbody></table></section>",
787
+ '<p class="u-foot"><strong>' + statusLabel + " spec v" + escapeHtml(spec.specVersion) + "</strong> \xB7 Generated by UIfying. Static fixtures, no API calls, auth, or mutations. Approve with <code>uifying spec approve</code>.</p>",
788
+ "</main>",
789
+ "</body>",
790
+ "</html>"
791
+ ];
792
+ return doc.join("\n");
793
+ }
794
+
795
+ // ../core/dist/skill.js
796
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
797
+ import { join as join3 } from "node:path";
798
+
799
+ // ../core/dist/skill-template.js
800
+ var SKILL_TEMPLATE = '---\nname: uifying\ndescription: Use when creating or changing UI in a project with a .uifying/ directory. Read the approved UI spec, retrieve task-scoped context, reuse existing components, and run the consistency checker after implementation.\n---\n\n<!-- uifying:managed:start -->\n# UIfying \u2014 UI consistency workflow\n\nThis project has UIfying installed.\n\n**Spec files:** `.uifying/spec.json` and `.uifying/spec.md`\n**Preview:** `.uifying/preview/index.html`\n\n## Before any UI work\n\n1. Read `.uifying/spec.json` and `.uifying/spec.md`.\n2. Run `uifying status`. If the spec is not approved, do not invent a new pattern. Show the preview with `uifying spec preview --serve` and ask the user to approve it with `uifying spec approve`.\n3. Retrieve task-scoped context:\n\n ~~~bash\n uifying context "<task description>" --json\n ~~~\n\n4. Reuse the returned components and import paths. Follow the returned spec IDs and tokens.\n5. If the task needs a pattern the spec does not cover, propose a spec change and a preview update first. Do not silently deviate.\n\n## After UI work\n\n1. Run the checker:\n\n ~~~bash\n uifying check --base <base-branch>\n ~~~\n\n2. Fix findings that have clear evidence. Do not delete or weaken a rule to make the check pass.\n3. Re-run the checker after fixes. Report remaining ambiguous findings to the user.\n4. Do not modify unrelated pages, global tokens, or the spec baseline without explicit approval.\n\n## Source of truth\n\n- The approved spec wins over current code for new UI.\n- Current code is evidence, not the destination.\n- The checker cites spec IDs, file paths, lines, and evidence.\n- No source upload and no account are required.\n\n## When a command is unavailable\n\nRead `.uifying/spec.json` directly, follow its items and tokens, and report that the CLI could not be run. Do not guess the approved pattern.\n<!-- uifying:managed:end -->\n';
801
+
802
+ // ../core/dist/skill.js
803
+ var START = "<!-- uifying:managed:start -->";
804
+ var END = "<!-- uifying:managed:end -->";
805
+ function renderSkill(spec) {
806
+ void spec;
807
+ return SKILL_TEMPLATE.endsWith("\n") ? SKILL_TEMPLATE : SKILL_TEMPLATE + "\n";
808
+ }
809
+ function codexBlock(spec) {
810
+ return renderSkill(spec).replace(/^---\n[\s\S]*?\n---\n+/, "");
811
+ }
812
+ function installSkill(root, agent, spec) {
813
+ const installed = [];
814
+ if (!agent)
815
+ return installed;
816
+ if (agent === "claude") {
817
+ const file = join3(root, ".claude", "skills", "uifying", "SKILL.md");
818
+ if (existsSync3(file)) {
819
+ const existing = readFileSync2(file, "utf8");
820
+ if (!existing.includes(START))
821
+ return installed;
822
+ }
823
+ ensureDir(join3(root, ".claude", "skills", "uifying"));
824
+ writeText(file, renderSkill(spec));
825
+ installed.push(file);
826
+ }
827
+ if (agent === "codex") {
828
+ const file = join3(root, "AGENTS.md");
829
+ const content = codexBlock(spec);
830
+ if (!existsSync3(file)) {
831
+ writeText(file, content);
832
+ installed.push(file);
833
+ } else {
834
+ const existing = readFileSync2(file, "utf8");
835
+ if (existing.includes(START) && existing.includes(END)) {
836
+ const before = existing.slice(0, existing.indexOf(START));
837
+ const after = existing.slice(existing.indexOf(END) + END.length);
838
+ writeText(file, before + content + after.replace(/^\n/, ""));
839
+ } else {
840
+ writeText(file, existing.replace(/\s*$/, "") + "\n\n" + content + "\n");
841
+ }
842
+ installed.push(file);
843
+ }
844
+ }
845
+ return installed;
846
+ }
847
+
848
+ // ../core/dist/spec.js
849
+ var SPEC_VERSION = "0.1.0";
850
+ function memoryPaths(root) {
851
+ const dir = join4(root, ".uifying");
852
+ const preview2 = join4(dir, "preview");
853
+ const skill = join4(dir, "skill");
854
+ return {
855
+ dir,
856
+ config: join4(dir, "config.json"),
857
+ spec: join4(dir, "spec.json"),
858
+ specMd: join4(dir, "spec.md"),
859
+ tokens: join4(dir, "tokens.json"),
860
+ components: join4(dir, "components.json"),
861
+ patterns: join4(dir, "patterns.json"),
862
+ evidence: join4(dir, "evidence.json"),
863
+ baseline: join4(dir, "baseline.json"),
864
+ preview: preview2,
865
+ previewFile: join4(preview2, "index.html"),
866
+ skill,
867
+ skillFile: join4(skill, "SKILL.md")
868
+ };
869
+ }
870
+ function readSpec(root) {
871
+ const paths = memoryPaths(root);
872
+ if (!existsSync4(paths.spec))
873
+ return null;
874
+ return readJson(paths.spec);
875
+ }
876
+ function readConfig(root) {
877
+ const paths = memoryPaths(root);
878
+ if (!existsSync4(paths.config))
879
+ return null;
880
+ return readJson(paths.config);
881
+ }
882
+ function buildSpec(scan, existing, options = {}) {
883
+ const archetype = detectArchetype(scan);
884
+ const now = nowIso();
885
+ const preserve = Boolean(existing && !options.force);
886
+ const status2 = preserve && existing ? existing.status : "draft";
887
+ const existingById = new Map((existing?.items ?? []).map((item) => [item.id, item]));
888
+ const generatedItems = archetype === "dashboard-admin" ? dashboardItems(scan) : appItems(scan);
889
+ const items = generatedItems.map((item) => {
890
+ const previous = existingById.get(item.id);
891
+ if (previous) {
892
+ return {
893
+ ...item,
894
+ rules: previous.rules?.length ? previous.rules : item.rules,
895
+ evidence: previous.evidence?.length ? previous.evidence : item.evidence,
896
+ status: previous.status
897
+ };
898
+ }
899
+ return { ...item, status: status2 };
900
+ });
901
+ const createdAt = preserve && existing ? existing.createdAt : now;
902
+ const specVersion = preserve && existing ? existing.specVersion : SPEC_VERSION;
903
+ const approvedAt = preserve && existing ? existing.approvedAt : void 0;
904
+ return {
905
+ schemaVersion: "1",
906
+ specVersion,
907
+ status: status2,
908
+ archetype,
909
+ createdAt,
910
+ updatedAt: now,
911
+ approvedAt,
912
+ project: scan.info,
913
+ tokens: scan.tokens,
914
+ components: scan.components,
915
+ patterns: scan.patterns,
916
+ items,
917
+ preview: "preview/index.html"
918
+ };
919
+ }
920
+ function writeMemory(root, spec, options = {}) {
921
+ const paths = memoryPaths(root);
922
+ ensureDir(paths.preview);
923
+ ensureDir(paths.skill);
924
+ const config = {
925
+ schemaVersion: "1",
926
+ projectRoot: ".",
927
+ ignore: options.ignore ?? readConfig(root)?.ignore ?? [],
928
+ componentDirs: spec.project.componentDirs,
929
+ pageDirs: spec.project.pageDirs,
930
+ agent: options.agent ?? readConfig(root)?.agent ?? null,
931
+ generatedAt: nowIso()
932
+ };
933
+ const baseline = {
934
+ specVersion: spec.specVersion,
935
+ status: spec.status,
936
+ approvedAt: spec.approvedAt,
937
+ authoritativeComponents: spec.components.filter((component) => component.status === "approved").map((component) => component.path),
938
+ goldenPages: spec.patterns.filter((page) => page.reference).map((page) => page.path),
939
+ exceptions: []
940
+ };
941
+ writeJson(paths.config, config);
942
+ writeJson(paths.spec, spec);
943
+ writeJson(paths.tokens, spec.tokens);
944
+ writeJson(paths.components, spec.components);
945
+ writeJson(paths.patterns, spec.patterns);
946
+ writeJson(paths.evidence, Object.fromEntries(spec.items.map((item) => [item.id, item.evidence])));
947
+ writeJson(paths.baseline, baseline);
948
+ writeText(paths.specMd, renderSpecMarkdown(spec));
949
+ writeText(paths.previewFile, renderPreview(spec));
950
+ writeText(paths.skillFile, renderSkill(spec));
951
+ return paths;
952
+ }
953
+ function approveMemory(root) {
954
+ const spec = readSpec(root);
955
+ if (!spec)
956
+ throw new Error("No .uifying/spec.json found. Run uifying init first.");
957
+ const approved = {
958
+ ...spec,
959
+ status: "approved",
960
+ approvedAt: nowIso(),
961
+ updatedAt: nowIso(),
962
+ items: spec.items.map((item) => ({ ...item, status: "approved" }))
963
+ };
964
+ const paths = writeMemory(root, approved, { agent: readConfig(root)?.agent ?? null });
965
+ writeText(paths.previewFile, renderPreview(approved));
966
+ return approved;
967
+ }
968
+ function renderSpecMarkdown(spec) {
969
+ const lines = [];
970
+ lines.push("# " + spec.project.name + " UI Spec");
971
+ lines.push("");
972
+ lines.push("**Status:** " + spec.status);
973
+ lines.push("**Spec version:** " + spec.specVersion);
974
+ lines.push("**Archetype:** " + spec.archetype);
975
+ lines.push("**Updated:** " + spec.updatedAt);
976
+ if (spec.approvedAt)
977
+ lines.push("**Approved:** " + spec.approvedAt);
978
+ lines.push("");
979
+ lines.push("> This file is generated by UIfying. The approved spec is the contract for new UI. The current code is evidence, not the specification.");
980
+ lines.push("");
981
+ lines.push("## Project");
982
+ lines.push("");
983
+ lines.push("| Key | Value |");
984
+ lines.push("|---|---|");
985
+ lines.push("| Framework | " + spec.project.framework + " |");
986
+ lines.push("| TypeScript | " + (spec.project.typescript ? "yes" : "no") + " |");
987
+ lines.push("| Tailwind | " + (spec.project.tailwind ?? "not detected") + " |");
988
+ lines.push("| Component dirs | " + (spec.project.componentDirs.join(", ") || "none") + " |");
989
+ lines.push("| Page dirs | " + (spec.project.pageDirs.join(", ") || "none") + " |");
990
+ lines.push("| Global CSS | " + (spec.project.globalsCss ?? "none") + " |");
991
+ lines.push("");
992
+ lines.push("## Foundations");
993
+ lines.push("");
994
+ lines.push("| Token | Value |");
995
+ lines.push("|---|---|");
996
+ for (const [key, value2] of Object.entries(spec.tokens.colors))
997
+ lines.push("| color." + key + " | " + value2 + " |");
998
+ for (const [key, value2] of Object.entries(spec.tokens.radius))
999
+ lines.push("| radius." + key + " | " + value2 + " |");
1000
+ for (const [key, value2] of Object.entries(spec.tokens.spacing))
1001
+ lines.push("| space." + key + " | " + value2 + " |");
1002
+ lines.push("| font.sans | " + spec.tokens.fonts.sans + " |");
1003
+ lines.push("");
1004
+ lines.push("## Components");
1005
+ lines.push("");
1006
+ if (spec.components.length === 0) {
1007
+ lines.push("No components indexed yet. The spec still defines the target UI, but new components should be added to the approved library.");
1008
+ } else {
1009
+ lines.push("| Component | Import | Props | Used by |");
1010
+ lines.push("|---|---|---|---|");
1011
+ for (const component of spec.components.slice(0, 40)) {
1012
+ lines.push("| " + component.name + " | " + (component.importPath ?? component.path) + " | " + (component.props.slice(0, 8).join(", ") || "-") + " | " + (component.usedBy.length ? component.usedBy.join(", ") : "-") + " |");
1013
+ }
1014
+ }
1015
+ lines.push("");
1016
+ lines.push("## Page patterns");
1017
+ lines.push("");
1018
+ if (spec.patterns.length === 0) {
1019
+ lines.push("No pages indexed yet.");
1020
+ } else {
1021
+ lines.push("| Page | Type | Reference |");
1022
+ lines.push("|---|---|---|");
1023
+ for (const page of spec.patterns)
1024
+ lines.push("| " + page.path + " | " + page.type + " | " + (page.reference ? "yes" : "no") + " |");
1025
+ }
1026
+ lines.push("");
1027
+ lines.push("## Spec rules");
1028
+ lines.push("");
1029
+ for (const item of spec.items) {
1030
+ lines.push("### " + item.id + " \u2014 " + item.title);
1031
+ lines.push("");
1032
+ lines.push("**Status:** " + item.status);
1033
+ lines.push("");
1034
+ for (const rule of item.rules)
1035
+ lines.push("- " + rule);
1036
+ if (item.evidence.length) {
1037
+ lines.push("");
1038
+ lines.push("**Evidence:** " + item.evidence.join("; "));
1039
+ }
1040
+ lines.push("");
1041
+ }
1042
+ lines.push("## Preview and approval");
1043
+ lines.push("");
1044
+ lines.push("Open the UI-only preview:");
1045
+ lines.push("");
1046
+ lines.push("~~~bash");
1047
+ lines.push("npx uifying spec preview");
1048
+ lines.push("~~~");
1049
+ lines.push("");
1050
+ lines.push("When the spec is right, approve the current version:");
1051
+ lines.push("");
1052
+ lines.push("~~~bash");
1053
+ lines.push("npx uifying spec approve");
1054
+ lines.push("~~~");
1055
+ lines.push("");
1056
+ lines.push("The checker only treats the approved spec as the contract. A new pattern requires a spec change and a new preview, not a checker bypass.");
1057
+ lines.push("");
1058
+ return lines.join("\n");
1059
+ }
1060
+ function detectArchetype(scan) {
1061
+ if (scan.patterns.some((page) => page.type === "dashboard" || page.type === "data-list"))
1062
+ return "dashboard-admin";
1063
+ if (scan.components.some((component) => /DataTable|Table|PageHeader|Sidebar/.test(component.name)))
1064
+ return "dashboard-admin";
1065
+ return scan.info.framework === "unknown" ? "dashboard-admin" : "app";
1066
+ }
1067
+ function findComponent(scan, names) {
1068
+ return scan.components.find((component) => names.some((name) => new RegExp(name, "i").test(component.name)));
1069
+ }
1070
+ function componentEvidence(scan, names) {
1071
+ const component = findComponent(scan, names);
1072
+ return component ? [component.path + " (" + component.name + ")"] : [];
1073
+ }
1074
+ function dashboardItems(scan) {
1075
+ const primary = scan.tokens.colors.primary;
1076
+ const surface = scan.tokens.colors.surface;
1077
+ const border = scan.tokens.colors.border;
1078
+ const md = scan.tokens.radius.md;
1079
+ const table = findComponent(scan, ["DataTable", "Table"]);
1080
+ const modal = findComponent(scan, ["Dialog", "Modal", "Sheet"]);
1081
+ const pageHeader = findComponent(scan, ["PageHeader", "Header"]);
1082
+ const back = findComponent(scan, ["BackButton", "Back"]);
1083
+ const rootEvidence = scan.info.globalsCss ? [scan.info.globalsCss] : ["UIfying dashboard baseline"];
1084
+ return [
1085
+ { id: "FOUND-01", section: "foundations", title: "Foundations", status: "draft", rules: ["Use the semantic colors, type ramp, spacing scale, and radius scale in this spec.", "Do not add one-off colors or arbitrary spacing values."], evidence: rootEvidence, previewRef: "preview#foundations" },
1086
+ { id: "NAV-01", section: "navigation", title: "Navigation and active state", status: "draft", rules: ["Desktop sidebar is 240px wide; collapsed rail is 64px below 1024px.", "Active item uses " + primary + " as an accent and a surface change.", "Active, hover, focus, and disabled states are visually distinct."], evidence: rootEvidence, previewRef: "preview#nav" },
1087
+ { id: "SEL-01", section: "selected-states", title: "Global selected states", status: "draft", rules: ["Nav active, table row selected, tab active, and filter chip active share one treatment.", "Selection is never communicated by color alone; use a mark, check, or border."], evidence: rootEvidence, previewRef: "preview#selected" },
1088
+ { id: "CARD-01", section: "surfaces", title: "Card and KPI recipe", status: "draft", rules: ["Cards use " + surface + " surface, " + border + " border, " + md + " radius, and 12-16px padding.", "KPI cards lead with the metric, then the label, then context or a sparkline.", "Card headers and actions use a consistent vertical rhythm."], evidence: componentEvidence(scan, ["Card", "Kpi", "Stat"]), previewRef: "preview#card" },
1089
+ { id: "CHART-01", section: "charts", title: "Chart recipe", status: "draft", rules: ["Use " + primary + " as the primary series; reserve success, warning, and danger for state.", "Hairline grid, minimal axes, one legend, and a tooltip.", "Every chart has loading, empty, and error states."], evidence: componentEvidence(scan, ["Chart", "Graph"]), previewRef: "preview#chart" },
1090
+ { id: "TABLE-01", section: "tables", title: "Table recipe", status: "draft", rules: [table ? "Reuse " + table.name + " from " + (table.importPath ?? table.path) + " before creating another table." : "Use a shared table primitive rather than a page-specific table.", "Header, row height, selected row, status badge, pagination, and empty state are part of the recipe.", "Row actions stay inside the table pattern."], evidence: componentEvidence(scan, ["DataTable", "Table"]), previewRef: "preview#table" },
1091
+ { id: "FORM-01", section: "forms", title: "Form recipe", status: "draft", rules: ["Labels sit above fields; help text and errors use the same reserved space.", "Use the shared Input, Select, and Button components.", "Form groups use the same spacing and column rhythm as the rest of the product."], evidence: componentEvidence(scan, ["Form", "Input", "Select"]), previewRef: "preview#form" },
1092
+ { id: "MODAL-01", section: "overlays", title: "Dialog recipe", status: "draft", rules: [modal ? "Reuse " + modal.name + " from " + (modal.importPath ?? modal.path) + " before building a new dialog." : "Use one shared dialog primitive.", "Dialog is 480px wide on desktop, full-screen on small screens, with a sticky footer.", "Destructive action is separated from the primary action."], evidence: componentEvidence(scan, ["Dialog", "Modal", "Sheet"]), previewRef: "preview#modal" },
1093
+ { id: "PAGE-02", section: "secondary-pages", title: "Secondary page shell", status: "draft", rules: [pageHeader ? "Use " + pageHeader.name + " for the page header." : "Secondary pages share one page-header recipe.", "Breadcrumb, title, actions, tabs, and content width follow the primary page shell.", "Content max width and responsive padding come from the layout recipe."], evidence: componentEvidence(scan, ["PageHeader", "Header"]), previewRef: "preview#page" },
1094
+ { id: "BACK-01", section: "secondary-pages", title: "Back button", status: "draft", rules: [back ? "Reuse " + back.name + " from " + (back.importPath ?? back.path) + "." : "Use one back-button treatment on secondary pages.", "Back sits top-left, before the breadcrumb; label and arrow are stable.", "Behavior is route-aware and does not use a generic history fallback without intent."], evidence: componentEvidence(scan, ["BackButton", "Back"]), previewRef: "preview#back" },
1095
+ { id: "STATE-01", section: "states", title: "Data states", status: "draft", rules: ["Every data surface defines empty, loading, error, and partial states.", "Skeletons match the final layout; do not use a spinner on first load if a skeleton is possible.", "Permission denied is a distinct state, not an empty result."], evidence: rootEvidence, previewRef: "preview#states" },
1096
+ { id: "RESP-01", section: "responsive", title: "Responsive rules", status: "draft", rules: ["Collapse the rail before the content becomes cramped.", "Stack KPI cards; let tables scroll horizontally rather than hiding columns.", "Dialogs become full-screen; back buttons remain reachable."], evidence: rootEvidence, previewRef: "preview#responsive" }
1097
+ ];
1098
+ }
1099
+ function appItems(scan) {
1100
+ const rootEvidence = scan.info.globalsCss ? [scan.info.globalsCss] : ["UIfying app baseline"];
1101
+ const modal = findComponent(scan, ["Dialog", "Modal", "Sheet"]);
1102
+ return [
1103
+ { id: "FOUND-01", section: "foundations", title: "Foundations", status: "draft", rules: ["Use the semantic colors, type ramp, spacing scale, and radius scale in this spec.", "Do not add one-off colors or arbitrary spacing values."], evidence: rootEvidence, previewRef: "preview#foundations" },
1104
+ { id: "SHELL-01", section: "shell", title: "App shell", status: "draft", rules: ["Navigation, page header, content width, and responsive behavior come from one shell recipe.", "Active and selected states are explicit."], evidence: rootEvidence, previewRef: "preview#shell" },
1105
+ { id: "CARD-01", section: "surfaces", title: "Card recipe", status: "draft", rules: ["Use one card surface, border, radius, and padding recipe."], evidence: rootEvidence, previewRef: "preview#card" },
1106
+ { id: "FORM-01", section: "forms", title: "Form recipe", status: "draft", rules: ["Labels above fields; reserved help and error space; shared inputs and buttons."], evidence: componentEvidence(scan, ["Form", "Input"]), previewRef: "preview#form" },
1107
+ { id: "MODAL-01", section: "overlays", title: "Dialog recipe", status: "draft", rules: [modal ? "Reuse " + modal.name + " from " + (modal.importPath ?? modal.path) + "." : "Use one shared dialog primitive.", "Dialog width, footer, and destructive action are consistent."], evidence: componentEvidence(scan, ["Dialog", "Modal", "Sheet"]), previewRef: "preview#modal" },
1108
+ { id: "STATE-01", section: "states", title: "Data states", status: "draft", rules: ["Every data surface defines empty, loading, error, and partial states."], evidence: rootEvidence, previewRef: "preview#states" },
1109
+ { id: "RESP-01", section: "responsive", title: "Responsive rules", status: "draft", rules: ["Stack content before it becomes cramped; keep primary actions reachable."], evidence: rootEvidence, previewRef: "preview#responsive" }
1110
+ ];
1111
+ }
1112
+
1113
+ // ../core/dist/context.js
1114
+ function buildContext(spec, task, limit = 8) {
1115
+ const text = task.toLowerCase();
1116
+ const words = text.split(/[^a-z0-9]+/).filter((word) => word.length > 2);
1117
+ const components = spec.components.map((component) => {
1118
+ let score = 0;
1119
+ const name = component.name.toLowerCase();
1120
+ const path = component.path.toLowerCase();
1121
+ for (const word of words) {
1122
+ if (name === word)
1123
+ score += 5;
1124
+ else if (name.includes(word) || word.includes(name))
1125
+ score += 2;
1126
+ if (path.includes(word))
1127
+ score += 1;
1128
+ if (component.props.some((prop) => prop.toLowerCase().includes(word)))
1129
+ score += 0.5;
1130
+ }
1131
+ if (component.usedBy.some((file) => words.some((word) => file.toLowerCase().includes(word))))
1132
+ score += 2;
1133
+ if (/(table|list|grid|user|order)/.test(text) && /(DataTable|Table|List)/i.test(component.name))
1134
+ score += 4;
1135
+ if (/(dialog|modal|popup|edit|user)/.test(text) && /(Dialog|Modal|Sheet)/i.test(component.name))
1136
+ score += 3;
1137
+ if (/(button|action|cta)/.test(text) && /Button/i.test(component.name))
1138
+ score += 2;
1139
+ if (/(page|header|title)/.test(text) && /(PageHeader|Header)/i.test(component.name))
1140
+ score += 2;
1141
+ if (/(card|kpi|stat|metric)/.test(text) && /(Card|Kpi|Stat|Metric)/i.test(component.name))
1142
+ score += 2;
1143
+ if (/(chart|graph|trend)/.test(text) && /(Chart|Graph)/i.test(component.name))
1144
+ score += 2;
1145
+ return {
1146
+ name: component.name,
1147
+ path: component.path,
1148
+ importPath: component.importPath,
1149
+ score: Math.round(score * 100) / 100,
1150
+ props: component.props,
1151
+ variants: component.variants,
1152
+ usedBy: component.usedBy
1153
+ };
1154
+ }).filter((component) => component.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
1155
+ const referencePage = pickReference(spec, text);
1156
+ const specItems = pickItems(spec.items, text);
1157
+ const note = components.length === 0 ? "No project component matched this task. Do not invent a component silently; check the spec or propose a new component." : void 0;
1158
+ return {
1159
+ task,
1160
+ specVersion: spec.specVersion,
1161
+ specStatus: spec.status,
1162
+ components,
1163
+ referencePage,
1164
+ specItems,
1165
+ tokens: {
1166
+ colors: spec.tokens.colors,
1167
+ radius: spec.tokens.radius,
1168
+ spacing: spec.tokens.spacing
1169
+ },
1170
+ note
1171
+ };
1172
+ }
1173
+ function pickReference(spec, text) {
1174
+ const scored = spec.patterns.map((page) => {
1175
+ let score = page.reference ? 2 : 0;
1176
+ if (/(list|table|user|order)/.test(text) && page.type === "data-list")
1177
+ score += 3;
1178
+ if (/(detail|edit|secondary)/.test(text) && page.type === "detail")
1179
+ score += 2;
1180
+ if (/(setting|form)/.test(text) && page.type === "settings")
1181
+ score += 2;
1182
+ if (page.path.toLowerCase().split(/[^a-z0-9]+/).some((part) => text.includes(part) && part.length > 2))
1183
+ score += 1;
1184
+ return { page, score };
1185
+ }).sort((a, b) => b.score - a.score);
1186
+ return scored[0]?.page.path;
1187
+ }
1188
+ function pickItems(items, text) {
1189
+ const priorities = ["FOUND-01"];
1190
+ if (/(nav|sidebar|menu)/.test(text))
1191
+ priorities.push("NAV-01");
1192
+ if (/(select|active|focus)/.test(text))
1193
+ priorities.push("SEL-01");
1194
+ if (/(table|list|user|order|data)/.test(text))
1195
+ priorities.push("TABLE-01", "STATE-01");
1196
+ if (/(form|input|field|search)/.test(text))
1197
+ priorities.push("FORM-01");
1198
+ if (/(dialog|modal|edit|popup)/.test(text))
1199
+ priorities.push("MODAL-01");
1200
+ if (/(card|kpi|stat|metric)/.test(text))
1201
+ priorities.push("CARD-01");
1202
+ if (/(chart|graph|trend)/.test(text))
1203
+ priorities.push("CHART-01");
1204
+ if (/(page|header|secondary|detail)/.test(text))
1205
+ priorities.push("PAGE-02", "BACK-01");
1206
+ if (/(state|empty|loading|error)/.test(text))
1207
+ priorities.push("STATE-01");
1208
+ priorities.push("RESP-01");
1209
+ const unique = [...new Set(priorities)];
1210
+ return unique.map((id) => items.find((item) => item.id === id)).filter((item) => Boolean(item)).slice(0, 8);
1211
+ }
1212
+ function formatContext(result) {
1213
+ const lines = [];
1214
+ lines.push("UI context for: " + result.task);
1215
+ lines.push("Spec: v" + result.specVersion + " (" + result.specStatus + ")");
1216
+ lines.push("");
1217
+ if (result.components.length === 0) {
1218
+ lines.push("Components: no match");
1219
+ } else {
1220
+ lines.push("Components");
1221
+ for (const component of result.components) {
1222
+ lines.push(" " + component.score.toFixed(2).padStart(5) + " " + component.name + " " + (component.importPath ?? component.path));
1223
+ if (component.props.length)
1224
+ lines.push(" props: " + component.props.slice(0, 8).join(", "));
1225
+ if (component.usedBy.length)
1226
+ lines.push(" used by: " + component.usedBy.slice(0, 3).join(", "));
1227
+ }
1228
+ }
1229
+ lines.push("");
1230
+ lines.push("Reference page: " + (result.referencePage ?? "none found"));
1231
+ lines.push("");
1232
+ lines.push("Spec rules");
1233
+ for (const item of result.specItems)
1234
+ lines.push(" " + item.id + " " + item.title);
1235
+ lines.push("");
1236
+ lines.push("Tokens");
1237
+ lines.push(" colors: " + Object.entries(result.tokens.colors).map(([key, value2]) => key + "=" + value2).join(" "));
1238
+ lines.push(" radius: " + Object.entries(result.tokens.radius).map(([key, value2]) => key + "=" + value2).join(" "));
1239
+ if (result.note) {
1240
+ lines.push("");
1241
+ lines.push("Note: " + result.note);
1242
+ }
1243
+ return lines.join("\n");
1244
+ }
1245
+
1246
+ // ../core/dist/check.js
1247
+ import { existsSync as existsSync5 } from "node:fs";
1248
+ import { join as join5 } from "node:path";
1249
+ var CHECKABLE = /\.(tsx|jsx|ts|js|vue|svelte|astro|css|html)$/;
1250
+ var SKIP_FILE = /(^|\/)(vite|next|tailwind|postcss|astro|svelte|nuxt|webpack|eslint|prettier|vitest|jest|playwright)\.config\.(ts|js|mjs|cjs)$/;
1251
+ var TOKEN_FILE = /(^|\/)(globals|tokens|theme|design-tokens)\.(css|ts|js|mjs)$/;
1252
+ function runCheck(root, spec, config, options = {}) {
1253
+ const approved = spec.status === "approved";
1254
+ const files = collectFiles(root, config, options);
1255
+ const ctx = {
1256
+ root,
1257
+ spec,
1258
+ config,
1259
+ approved,
1260
+ tokenSources: new Set([spec.project.globalsCss, spec.project.tailwindConfig].filter((value2) => Boolean(value2))),
1261
+ componentPaths: new Set(spec.components.map((component) => component.path)),
1262
+ componentDirs: config.componentDirs.length ? config.componentDirs : spec.project.componentDirs
1263
+ };
1264
+ const report = {
1265
+ schemaVersion: "1",
1266
+ specVersion: spec.specVersion,
1267
+ specStatus: spec.status,
1268
+ base: options.base,
1269
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1270
+ checkedFiles: files.length,
1271
+ unknown: 0,
1272
+ files: [],
1273
+ summary: { error: 0, warning: 0, info: 0 }
1274
+ };
1275
+ if (!approved) {
1276
+ const finding = {
1277
+ file: ".uifying/spec.json",
1278
+ line: 1,
1279
+ rule: "spec/approved-version",
1280
+ severity: "warning",
1281
+ message: "The UI spec is still a draft, so every finding is advisory.",
1282
+ evidence: { specVersion: spec.specVersion, source: ".uifying/spec.json" },
1283
+ suggestion: "Review the preview, then run: uifying spec approve"
1284
+ };
1285
+ report.files.push({ path: finding.file, findings: [finding] });
1286
+ report.summary.warning += 1;
1287
+ }
1288
+ for (const file of files) {
1289
+ let source;
1290
+ try {
1291
+ source = readText(join5(root, file));
1292
+ } catch {
1293
+ report.unknown += 1;
1294
+ continue;
1295
+ }
1296
+ const findings = [];
1297
+ if (!isTokenSource(file, ctx)) {
1298
+ findings.push(...checkColors(file, source, ctx));
1299
+ findings.push(...checkSpacing(file, source, ctx));
1300
+ }
1301
+ findings.push(...checkComponents(file, source, ctx));
1302
+ findings.push(...checkBackNavigation(file, source, ctx));
1303
+ if (findings.length) {
1304
+ const sorted = findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
1305
+ report.files.push({ path: file, findings: sorted });
1306
+ for (const finding of sorted)
1307
+ report.summary[finding.severity] += 1;
1308
+ }
1309
+ }
1310
+ report.files.sort((a, b) => a.path.localeCompare(b.path));
1311
+ return report;
1312
+ }
1313
+ function shouldFail(report, failOn = "error") {
1314
+ if (failOn === "none")
1315
+ return false;
1316
+ if (failOn === "warning")
1317
+ return report.summary.error + report.summary.warning > 0;
1318
+ return report.summary.error > 0;
1319
+ }
1320
+ function formatCheck(report) {
1321
+ const lines = [];
1322
+ lines.push("UI check spec v" + report.specVersion + " (" + report.specStatus + ")" + (report.base ? " base " + report.base : ""));
1323
+ lines.push("Checked " + report.checkedFiles + " files" + (report.unknown ? ", " + report.unknown + " unreadable" : ""));
1324
+ lines.push("");
1325
+ if (report.files.length === 0) {
1326
+ lines.push(color.green("No findings. The UI matches the approved spec."));
1327
+ return lines.join("\n");
1328
+ }
1329
+ for (const file of report.files) {
1330
+ lines.push(color.bold(file.path));
1331
+ for (const finding of file.findings) {
1332
+ const label = finding.severity === "error" ? color.red("error") : finding.severity === "warning" ? color.yellow("warn ") : color.dim("info ");
1333
+ lines.push(" " + label + " " + finding.line + ":" + (finding.column ?? 1) + " " + finding.message);
1334
+ lines.push(" " + color.dim(finding.rule + " " + finding.suggestion));
1335
+ }
1336
+ lines.push("");
1337
+ }
1338
+ const summary = report.summary;
1339
+ const paint2 = summary.error > 0 ? color.red : summary.warning > 0 ? color.yellow : color.green;
1340
+ lines.push(paint2(summary.error + " error, " + summary.warning + " warning, " + summary.info + " info"));
1341
+ return lines.join("\n");
1342
+ }
1343
+ function collectFiles(root, config, options) {
1344
+ let files;
1345
+ if (options.base && !options.all) {
1346
+ const valid = runGit(root, ["rev-parse", "--verify", options.base]);
1347
+ if (!valid)
1348
+ throw new Error("Unknown git base: " + options.base);
1349
+ const changed = runGit(root, ["diff", "--name-only", options.base]);
1350
+ const untracked = runGit(root, ["ls-files", "--others", "--exclude-standard"]);
1351
+ files = [...changed.split("\n"), ...untracked.split("\n")].map((file) => toPosix(file.trim())).filter(Boolean);
1352
+ } else {
1353
+ files = walk(root, config.ignore);
1354
+ }
1355
+ const dirs = [...config.componentDirs, ...config.pageDirs].map((dir) => dir.replace(/^\.\//, "").replace(/\/$/, ""));
1356
+ const seen = /* @__PURE__ */ new Set();
1357
+ return files.filter((file) => {
1358
+ if (seen.has(file))
1359
+ return false;
1360
+ if (!CHECKABLE.test(file))
1361
+ return false;
1362
+ if (file.endsWith(".d.ts"))
1363
+ return false;
1364
+ if (SKIP_FILE.test(file))
1365
+ return false;
1366
+ if (!existsSync5(join5(root, file)))
1367
+ return false;
1368
+ if (dirs.length > 0 && !dirs.some((dir) => file === dir || file.startsWith(dir + "/"))) {
1369
+ if (!TOKEN_FILE.test(file) || !file.startsWith("src/"))
1370
+ return false;
1371
+ }
1372
+ seen.add(file);
1373
+ return true;
1374
+ });
1375
+ }
1376
+ function isTokenSource(file, ctx) {
1377
+ if (ctx.tokenSources.has(file))
1378
+ return true;
1379
+ if (TOKEN_FILE.test(file))
1380
+ return true;
1381
+ return false;
1382
+ }
1383
+ function severity(ctx) {
1384
+ return ctx.approved ? "error" : "warning";
1385
+ }
1386
+ function normalizeColor(value2) {
1387
+ let next = value2.trim().toLowerCase().replace(/\s+/g, "");
1388
+ if (/^#[0-9a-f]{3}$/.test(next))
1389
+ next = "#" + next[1] + next[1] + next[2] + next[2] + next[3] + next[3];
1390
+ if (/^#[0-9a-f]{4}$/.test(next))
1391
+ next = "#" + next[1] + next[1] + next[2] + next[2] + next[3] + next[3] + next[4] + next[4];
1392
+ const rgb = next.match(/^rgba?\(([^)]+)\)$/);
1393
+ if (rgb) {
1394
+ const parts = rgb[1].split(",").slice(0, 3).map((part) => Number(part));
1395
+ if (parts.length === 3 && parts.every((part) => Number.isFinite(part)))
1396
+ return "rgb(" + parts.join(",") + ")";
1397
+ }
1398
+ return next;
1399
+ }
1400
+ function allowedColors(spec) {
1401
+ const allowed = /* @__PURE__ */ new Set();
1402
+ for (const value2 of Object.values(spec.tokens.colors)) {
1403
+ if (!value2)
1404
+ continue;
1405
+ allowed.add(normalizeColor(value2));
1406
+ for (const fallback of value2.matchAll(/#[0-9a-fA-F]{3,8}\b/g))
1407
+ allowed.add(normalizeColor(fallback[0]));
1408
+ for (const fallback of value2.matchAll(/rgba?\([^)]*\)/g))
1409
+ allowed.add(normalizeColor(fallback[0]));
1410
+ }
1411
+ return allowed;
1412
+ }
1413
+ function checkColors(file, source, ctx) {
1414
+ if (!Object.keys(ctx.spec.tokens.colors).length)
1415
+ return [];
1416
+ const allowed = allowedColors(ctx.spec);
1417
+ const findings = [];
1418
+ const seen = /* @__PURE__ */ new Set();
1419
+ const regex = /#[0-9a-fA-F]{8}\b|#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b|rgba?\([^)]*\)|hsla?\([^)]*\)/g;
1420
+ for (const match of source.matchAll(regex)) {
1421
+ const index = match.index ?? 0;
1422
+ const lineText = source.slice(source.lastIndexOf("\n", index - 1) + 1, source.indexOf("\n", index) === -1 ? source.length : source.indexOf("\n", index));
1423
+ if (/^\s*(\/\/|\*|<!--)/.test(lineText))
1424
+ continue;
1425
+ if (/url\([^)]*$/.test(source.slice(Math.max(0, index - 40), index)))
1426
+ continue;
1427
+ const value2 = match[0];
1428
+ const normalized = normalizeColor(value2);
1429
+ if (allowed.has(normalized))
1430
+ continue;
1431
+ if (normalized === "transparent" || normalized === "currentcolor")
1432
+ continue;
1433
+ const position = lineAt(source, index);
1434
+ const key = position.line + ":" + normalized;
1435
+ if (seen.has(key))
1436
+ continue;
1437
+ seen.add(key);
1438
+ findings.push({
1439
+ file,
1440
+ line: position.line,
1441
+ column: position.column,
1442
+ rule: "token/no-unknown-color",
1443
+ severity: severity(ctx),
1444
+ message: "Color " + value2 + " is not one of the spec color tokens.",
1445
+ evidence: { specId: "FOUND-01", specVersion: ctx.spec.specVersion, source: file, reference: value2 },
1446
+ suggestion: "Use a token such as " + tokenHint(ctx.spec) + " or add the color to the spec first."
1447
+ });
1448
+ }
1449
+ return findings;
1450
+ }
1451
+ function tokenHint(spec) {
1452
+ const preferred = ["primary", "accent", "text", "foreground"].find((name) => spec.tokens.colors[name]);
1453
+ if (preferred)
1454
+ return "var(--color-" + preferred + ")";
1455
+ const entry = Object.entries(spec.tokens.colors)[0];
1456
+ if (!entry)
1457
+ return "var(--color-primary)";
1458
+ return entry[0].startsWith("--") ? "var(" + entry[0] + ")" : entry[1];
1459
+ }
1460
+ function checkSpacing(file, source, ctx) {
1461
+ const allowed = /* @__PURE__ */ new Set([0]);
1462
+ for (const value2 of Object.values(ctx.spec.tokens.spacing)) {
1463
+ const px = numericPx(value2);
1464
+ if (px !== null)
1465
+ allowed.add(Math.round(px * 100) / 100);
1466
+ }
1467
+ if (allowed.size <= 1)
1468
+ return [];
1469
+ const findings = [];
1470
+ const seen = /* @__PURE__ */ new Set();
1471
+ const classRegex = /(?:^|[\s"'])(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-x|space-y)-\[(-?\d+(?:\.\d+)?)(px|rem)\]/g;
1472
+ const styleRegex = /(?:padding|margin|gap)(?:-[a-z]+)?\s*:\s*(-?\d+(?:\.\d+)?)(px|rem)/g;
1473
+ for (const regex of [classRegex, styleRegex]) {
1474
+ for (const match of source.matchAll(regex)) {
1475
+ const index = match.index ?? 0;
1476
+ const px = Number(match[1]) * (match[2] === "rem" ? 16 : 1);
1477
+ if (!Number.isFinite(px))
1478
+ continue;
1479
+ if ([...allowed].some((value2) => Math.abs(value2 - px) < 0.5))
1480
+ continue;
1481
+ const token = match[0].replace(/^[\s"']+/, "").trim();
1482
+ const position = lineAt(source, index);
1483
+ if (seen.has(position.line))
1484
+ continue;
1485
+ seen.add(position.line);
1486
+ findings.push({
1487
+ file,
1488
+ line: position.line,
1489
+ column: position.column,
1490
+ rule: "token/use-spacing-scale",
1491
+ severity: severity(ctx),
1492
+ message: "Spacing value " + token + " is off the spec spacing scale.",
1493
+ evidence: { specId: "RESP-01", specVersion: ctx.spec.specVersion, source: file, reference: token },
1494
+ suggestion: "Use a spacing token from the spec or add the value to the scale deliberately."
1495
+ });
1496
+ }
1497
+ }
1498
+ return findings;
1499
+ }
1500
+ function checkComponents(file, source, ctx) {
1501
+ const findings = [];
1502
+ if (isComponentPath(file, ctx))
1503
+ return findings;
1504
+ const rules = [
1505
+ { tag: /<button\b/, names: ["Button"], label: "button", rule: "component/prefer-existing", specId: "FOUND-01" },
1506
+ { tag: /<table\b/, names: ["DataTable", "Table"], label: "table", rule: "component/prefer-existing", specId: "TABLE-01" },
1507
+ { tag: /<dialog\b/, names: ["Dialog", "Modal", "Sheet"], label: "dialog", rule: "component/prefer-existing", specId: "MODAL-01" },
1508
+ { tag: /<input\b/, names: ["Input", "TextField"], label: "input", rule: "component/prefer-existing", specId: "FORM-01" },
1509
+ { tag: /<select\b/, names: ["Select", "Dropdown"], label: "select", rule: "component/prefer-existing", specId: "FORM-01" }
1510
+ ];
1511
+ for (const entry of rules) {
1512
+ const match = entry.tag.exec(source);
1513
+ if (!match)
1514
+ continue;
1515
+ const component = findComponent2(ctx.spec, entry.names);
1516
+ if (!component)
1517
+ continue;
1518
+ const position = lineAt(source, match.index);
1519
+ findings.push({
1520
+ file,
1521
+ line: position.line,
1522
+ column: position.column,
1523
+ rule: entry.rule,
1524
+ severity: severity(ctx),
1525
+ message: "Raw <" + entry.label + "> used while " + component.name + " exists in the project.",
1526
+ evidence: { specId: entry.specId, specVersion: ctx.spec.specVersion, source: component.path },
1527
+ suggestion: "Import " + (component.importPath ?? component.path) + " and use its existing variants."
1528
+ });
1529
+ }
1530
+ const modal = /<dialog\b|role\s*=\s*["']dialog["']/.exec(source);
1531
+ if (modal && !findComponent2(ctx.spec, ["Dialog", "Modal", "Sheet"])) {
1532
+ const specItem = ctx.spec.items.find((item) => item.id === "MODAL-01");
1533
+ if (specItem) {
1534
+ const position = lineAt(source, modal.index);
1535
+ findings.push({
1536
+ file,
1537
+ line: position.line,
1538
+ column: position.column,
1539
+ rule: "spec/use-modal-recipe",
1540
+ severity: severity(ctx),
1541
+ message: "Modal markup is not using the approved modal recipe.",
1542
+ evidence: { specId: "MODAL-01", specVersion: ctx.spec.specVersion, source: specItem.title },
1543
+ suggestion: "Use the modal shell from the spec: focus trap, Escape to close, title, body, and a single primary action."
1544
+ });
1545
+ }
1546
+ }
1547
+ void ctx.config;
1548
+ return findings;
1549
+ }
1550
+ function isComponentPath(file, ctx) {
1551
+ if (ctx.componentPaths.has(file))
1552
+ return true;
1553
+ return ctx.componentDirs.some((dir) => file.startsWith(dir.replace(/\/$/, "") + "/"));
1554
+ }
1555
+ function findComponent2(spec, names) {
1556
+ return spec.components.find((component) => names.includes(component.name)) ?? spec.components.find((component) => names.some((name) => component.name.endsWith(name)));
1557
+ }
1558
+ function checkBackNavigation(file, source, ctx) {
1559
+ const page = ctx.spec.patterns.find((pattern) => pattern.path === file);
1560
+ if (!page)
1561
+ return [];
1562
+ if (!["detail", "settings", "form"].includes(page.type))
1563
+ return [];
1564
+ if (/(ArrowLeft|ChevronLeft|router\.back|history\.back|Back to|←|href\s*=\s*["'][^"']*(settings|list|dashboard)[^"']*["'])/.test(source))
1565
+ return [];
1566
+ const item = ctx.spec.items.find((entry) => entry.id === "BACK-01" || entry.id === "PAGE-02");
1567
+ if (!item)
1568
+ return [];
1569
+ return [{
1570
+ file,
1571
+ line: 1,
1572
+ column: 1,
1573
+ rule: "spec/use-back-button",
1574
+ severity: severity(ctx),
1575
+ message: "Secondary page has no visible back navigation.",
1576
+ evidence: { specId: "BACK-01", specVersion: ctx.spec.specVersion, source: item.title },
1577
+ suggestion: "Add a back control that returns to the parent list or previous page."
1578
+ }];
1579
+ }
1580
+
1581
+ // ../core/dist/doctor.js
1582
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
1583
+ import { join as join6 } from "node:path";
1584
+ function runDoctor(root) {
1585
+ const checks = [];
1586
+ let spec = null;
1587
+ let config = null;
1588
+ let loadError = null;
1589
+ try {
1590
+ spec = readSpec(root);
1591
+ config = readConfig(root);
1592
+ } catch (error) {
1593
+ loadError = error instanceof Error ? error.message : String(error);
1594
+ }
1595
+ const paths = memoryPaths(root);
1596
+ const major = Number(process.versions.node.split(".")[0]);
1597
+ checks.push({
1598
+ label: "Node",
1599
+ status: Number.isFinite(major) && major >= 20 ? "ok" : "fail",
1600
+ detail: "v" + process.versions.node + (Number.isFinite(major) && major >= 20 ? "" : " (requires Node 20 or newer)")
1601
+ });
1602
+ checks.push({
1603
+ label: "Project",
1604
+ status: existsSync6(join6(root, "package.json")) ? "ok" : "warn",
1605
+ detail: existsSync6(join6(root, "package.json")) ? config ? "memory initialized" : "package.json found, memory not initialized" : "no package.json at this root"
1606
+ });
1607
+ if (loadError) {
1608
+ checks.push({
1609
+ label: "UI spec",
1610
+ status: "fail",
1611
+ detail: "could not read .uifying/spec.json: " + loadError
1612
+ });
1613
+ } else if (!spec) {
1614
+ checks.push({
1615
+ label: "UI spec",
1616
+ status: "fail",
1617
+ detail: "not found. Run: uifying spec init"
1618
+ });
1619
+ } else {
1620
+ checks.push({
1621
+ label: "UI spec",
1622
+ status: spec.status === "approved" ? "ok" : "warn",
1623
+ detail: "v" + spec.specVersion + " (" + spec.status + ")" + (spec.approvedAt ? ", approved " + spec.approvedAt.slice(0, 10) : "")
1624
+ });
1625
+ checks.push({
1626
+ label: "Tokens",
1627
+ status: Object.keys(spec.tokens.colors).length > 0 ? "ok" : "warn",
1628
+ detail: Object.keys(spec.tokens.colors).length + " colors, " + Object.keys(spec.tokens.spacing).length + " spacing values, " + Object.keys(spec.tokens.radius).length + " radii"
1629
+ });
1630
+ checks.push({
1631
+ label: "Components",
1632
+ status: spec.components.length > 0 ? "ok" : "warn",
1633
+ detail: spec.components.length + " detected, " + spec.components.filter((component) => component.status === "approved").length + " approved"
1634
+ });
1635
+ const previewFile = join6(paths.dir, spec.preview);
1636
+ checks.push({
1637
+ label: "Preview",
1638
+ status: existsSync6(previewFile) ? "ok" : "fail",
1639
+ detail: existsSync6(previewFile) ? spec.preview : "missing " + spec.preview + ". Run: uifying spec preview"
1640
+ });
1641
+ checks.push({
1642
+ label: "Agent skill",
1643
+ status: existsSync6(paths.skillFile) ? "ok" : "warn",
1644
+ detail: existsSync6(paths.skillFile) ? paths.skillFile : "missing. Run: uifying spec init"
1645
+ });
1646
+ const claude = join6(root, ".claude/skills/uifying/SKILL.md");
1647
+ const agents = join6(root, "AGENTS.md");
1648
+ let agentDetail = "no agent integration";
1649
+ let agentStatus = "warn";
1650
+ if (existsSync6(claude)) {
1651
+ agentStatus = "ok";
1652
+ agentDetail = ".claude/skills/uifying/SKILL.md";
1653
+ } else if (existsSync6(agents)) {
1654
+ const text = readFileSync3(agents, "utf8");
1655
+ if (/UIfying/i.test(text)) {
1656
+ agentStatus = "ok";
1657
+ agentDetail = "managed block in AGENTS.md";
1658
+ }
1659
+ }
1660
+ checks.push({ label: "Agent setup", status: agentStatus, detail: agentDetail });
1661
+ const framework = spec.project.framework === "unknown" ? "warn" : "ok";
1662
+ checks.push({
1663
+ label: "Framework",
1664
+ status: framework,
1665
+ detail: spec.project.framework + (spec.project.tailwind ? " + Tailwind " + spec.project.tailwind : "") + (spec.project.typescript ? " + TypeScript" : "")
1666
+ });
1667
+ }
1668
+ return {
1669
+ ok: checks.every((check2) => check2.status !== "fail"),
1670
+ root,
1671
+ checks
1672
+ };
1673
+ }
1674
+ function formatDoctor(report) {
1675
+ const lines = [];
1676
+ lines.push("UIfying doctor " + report.root);
1677
+ lines.push("");
1678
+ for (const check2 of report.checks) {
1679
+ const mark = check2.status === "ok" ? color.green("ok ") : check2.status === "warn" ? color.yellow("warn") : color.red("fail");
1680
+ lines.push(" " + mark + " " + check2.label.padEnd(12) + check2.detail);
1681
+ }
1682
+ lines.push("");
1683
+ lines.push(report.ok ? color.green("Ready.") : color.red("Blocking issues found."));
1684
+ return lines.join("\n");
1685
+ }
1686
+
1687
+ // ../core/dist/server.js
1688
+ import { createServer } from "node:http";
1689
+ import { readFile, stat } from "node:fs/promises";
1690
+ import { extname as extname2, resolve, sep } from "node:path";
1691
+ var MIME = {
1692
+ ".html": "text/html; charset=utf-8",
1693
+ ".css": "text/css; charset=utf-8",
1694
+ ".js": "text/javascript; charset=utf-8",
1695
+ ".json": "application/json; charset=utf-8",
1696
+ ".svg": "image/svg+xml",
1697
+ ".png": "image/png",
1698
+ ".jpg": "image/jpeg",
1699
+ ".jpeg": "image/jpeg",
1700
+ ".webp": "image/webp",
1701
+ ".woff2": "font/woff2",
1702
+ ".ico": "image/x-icon"
1703
+ };
1704
+ async function startPreviewServer(previewDir, options = {}) {
1705
+ const root = resolve(previewDir);
1706
+ const port = options.port ?? 4173;
1707
+ const host = options.host ?? "127.0.0.1";
1708
+ const server = createServer(async (request, response) => {
1709
+ if (request.method !== "GET" && request.method !== "HEAD") {
1710
+ response.writeHead(405, { Allow: "GET, HEAD" });
1711
+ response.end("Method not allowed");
1712
+ return;
1713
+ }
1714
+ let pathname = "/";
1715
+ try {
1716
+ pathname = decodeURIComponent(new URL(request.url ?? "/", "http://" + host).pathname);
1717
+ } catch {
1718
+ response.writeHead(400);
1719
+ response.end("Bad request");
1720
+ return;
1721
+ }
1722
+ if (pathname.includes("\0")) {
1723
+ response.writeHead(400);
1724
+ response.end("Bad request");
1725
+ return;
1726
+ }
1727
+ let target = resolve(root, "." + pathname);
1728
+ if (target !== root && !target.startsWith(root + sep)) {
1729
+ response.writeHead(403);
1730
+ response.end("Forbidden");
1731
+ return;
1732
+ }
1733
+ try {
1734
+ const info = await stat(target);
1735
+ if (info.isDirectory())
1736
+ target = resolve(target, "index.html");
1737
+ const body = await readFile(target);
1738
+ const headers = {
1739
+ "Content-Type": MIME[extname2(target).toLowerCase()] ?? "application/octet-stream",
1740
+ "Content-Length": String(body.byteLength),
1741
+ "Cache-Control": "no-store",
1742
+ "X-Content-Type-Options": "nosniff"
1743
+ };
1744
+ response.writeHead(200, headers);
1745
+ if (request.method === "HEAD")
1746
+ response.end();
1747
+ else
1748
+ response.end(body);
1749
+ } catch {
1750
+ const fallback = resolve(root, "index.html");
1751
+ try {
1752
+ const body = await readFile(fallback);
1753
+ response.writeHead(404, {
1754
+ "Content-Type": "text/html; charset=utf-8",
1755
+ "Content-Length": String(body.byteLength),
1756
+ "Cache-Control": "no-store"
1757
+ });
1758
+ response.end(body);
1759
+ } catch {
1760
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
1761
+ response.end("Not found");
1762
+ }
1763
+ }
1764
+ });
1765
+ await new Promise((resolvePromise, reject) => {
1766
+ server.once("error", reject);
1767
+ server.listen(port, host, () => {
1768
+ server.off("error", reject);
1769
+ resolvePromise();
1770
+ });
1771
+ });
1772
+ const address = server.address();
1773
+ const actualPort = typeof address === "object" && address ? address.port : port;
1774
+ return {
1775
+ url: "http://" + host + ":" + actualPort + "/",
1776
+ close: () => new Promise((resolvePromise, reject) => {
1777
+ server.close((error) => error ? reject(error) : resolvePromise());
1778
+ })
1779
+ };
1780
+ }
1781
+
1782
+ // src/index.ts
1783
+ var HELP = [
1784
+ "UIfying - a UI spec your coding agent can follow.",
1785
+ "",
1786
+ "Usage:",
1787
+ " uifying init [--agent claude|codex] [--force] [--name name]",
1788
+ " uifying spec init [options] Scan a project and write the draft spec",
1789
+ " uifying spec preview [--serve] Regenerate the UI-only preview",
1790
+ " uifying spec approve Approve the spec as the UI contract",
1791
+ " uifying preview [--serve] Serve the preview (rebuilds it first)",
1792
+ " uifying context <task> [--json] Retrieve components and spec rules for a task",
1793
+ " uifying check [--base ref] [--all] [--json]",
1794
+ " uifying doctor [--json] Check the local setup",
1795
+ " uifying status Show the current spec",
1796
+ "",
1797
+ "Options:",
1798
+ " --root <dir> Run against another project root",
1799
+ " --port <n> Preview server port (default 4173)",
1800
+ " --host <host> Preview server host (default 127.0.0.1)",
1801
+ " --fail-on <level> check: error (default), warning, or none",
1802
+ " --out <file> check: write the JSON report to a custom path",
1803
+ " --help Show this help",
1804
+ " --version Show the version",
1805
+ "",
1806
+ "Typical flow:",
1807
+ " uifying init",
1808
+ " uifying spec preview --serve",
1809
+ " uifying spec approve",
1810
+ ' uifying context "add a user table"',
1811
+ " uifying check --base main"
1812
+ ].join("\n");
1813
+ var VERSION = "0.1.0";
1814
+ try {
1815
+ const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
1816
+ if (pkg.version) VERSION = pkg.version;
1817
+ } catch {
1818
+ }
1819
+ function parseRoot(argv) {
1820
+ let root = process.cwd();
1821
+ const args = [];
1822
+ for (let i = 0; i < argv.length; i++) {
1823
+ const arg = argv[i];
1824
+ if (arg === "--root") {
1825
+ const next = argv[++i];
1826
+ if (next) root = resolve2(next);
1827
+ continue;
1828
+ }
1829
+ if (arg.startsWith("--root=")) {
1830
+ root = resolve2(arg.slice("--root=".length));
1831
+ continue;
1832
+ }
1833
+ args.push(arg);
1834
+ }
1835
+ return { root, args };
1836
+ }
1837
+ function has(args, name) {
1838
+ return args.includes(name);
1839
+ }
1840
+ function value(args, name) {
1841
+ const index = args.indexOf(name);
1842
+ if (index >= 0 && args[index + 1] && !args[index + 1].startsWith("--")) return args[index + 1];
1843
+ const inline = args.find((arg) => arg.startsWith(name + "="));
1844
+ return inline ? inline.slice(name.length + 1) : void 0;
1845
+ }
1846
+ function values(args, name) {
1847
+ const result = [];
1848
+ for (let i = 0; i < args.length; i++) {
1849
+ if (args[i] === name && args[i + 1] && !args[i + 1].startsWith("--")) result.push(args[i + 1]);
1850
+ if (args[i].startsWith(name + "=")) result.push(args[i].slice(name.length + 1));
1851
+ }
1852
+ return result;
1853
+ }
1854
+ function positional(args, skip) {
1855
+ return args.filter((arg, index) => index >= skip && !arg.startsWith("--") && !(index > 0 && args[index - 1].startsWith("--") && !args[index - 1].includes("=")));
1856
+ }
1857
+ function fail(message) {
1858
+ console.error(color.red("error") + " " + message);
1859
+ process.exit(1);
1860
+ }
1861
+ function ensureSpec(root) {
1862
+ const spec = readSpec(root);
1863
+ if (!spec) fail("No UI spec found. Run: uifying init");
1864
+ return spec;
1865
+ }
1866
+ async function init(root, args, label) {
1867
+ const agentRaw = value(args, "--agent");
1868
+ if (agentRaw && agentRaw !== "claude" && agentRaw !== "codex") fail("--agent must be claude or codex");
1869
+ const agent = agentRaw ?? readConfig(root)?.agent ?? null;
1870
+ const force = has(args, "--force");
1871
+ const name = value(args, "--name");
1872
+ const ignore = values(args, "--ignore");
1873
+ const existing = readSpec(root);
1874
+ const hasPackage = existsSync7(join7(root, "package.json"));
1875
+ const scan = hasPackage ? scanProject(root, [...readConfig(root)?.ignore ?? [], ...ignore]) : emptyScan(root, name);
1876
+ if (!hasPackage && !name) {
1877
+ console.log(color.yellow("warn") + " No package.json found. Using default dashboard tokens and an empty component set.");
1878
+ }
1879
+ const spec = buildSpec(scan, existing, { force, agent, ignore });
1880
+ writeMemory(root, spec, { force, agent, ignore });
1881
+ const installed = installSkill(root, agent, spec);
1882
+ const paths = memoryPaths(root);
1883
+ console.log("");
1884
+ console.log(color.green("UIfying initialized") + " " + spec.project.name);
1885
+ console.log(" Spec v" + spec.specVersion + " " + spec.status + " .uifying/spec.json");
1886
+ console.log(" Components " + spec.components.length + (spec.components.length ? " detected" : " found"));
1887
+ console.log(" Tokens " + Object.keys(spec.tokens.colors).length + " colors " + Object.keys(spec.tokens.spacing).length + " spacing");
1888
+ console.log(" Preview " + paths.previewFile);
1889
+ console.log(" Skill " + paths.skillFile);
1890
+ for (const file of installed) console.log(" Installed " + file);
1891
+ console.log("");
1892
+ console.log(color.bold("Next"));
1893
+ console.log(" 1. uifying spec preview --serve");
1894
+ console.log(" 2. Review and edit .uifying/spec.json or .uifying/spec.md");
1895
+ console.log(" 3. uifying spec approve");
1896
+ console.log("");
1897
+ void label;
1898
+ }
1899
+ async function preview(root, args, serveByDefault) {
1900
+ const spec = ensureSpec(root);
1901
+ const paths = memoryPaths(root);
1902
+ writeText(paths.previewFile, renderPreview(spec));
1903
+ console.log("Preview " + paths.previewFile);
1904
+ const serve = serveByDefault || has(args, "--serve");
1905
+ if (!serve) {
1906
+ console.log(color.dim("Serve it with: uifying preview"));
1907
+ return;
1908
+ }
1909
+ const port = Number(value(args, "--port") ?? 4173);
1910
+ if (!Number.isInteger(port) || port < 0 || port > 65535) fail("--port must be a number between 0 and 65535");
1911
+ const host = value(args, "--host") ?? "127.0.0.1";
1912
+ const server = await startPreviewServer(paths.preview, { port, host });
1913
+ console.log("");
1914
+ console.log(color.green("UI-only preview") + " " + server.url);
1915
+ console.log(color.dim("Static fixtures only. No API, auth, or mutations. Press Ctrl+C to stop."));
1916
+ const stop = async () => {
1917
+ await server.close().catch(() => void 0);
1918
+ process.exit(0);
1919
+ };
1920
+ process.once("SIGINT", stop);
1921
+ process.once("SIGTERM", stop);
1922
+ await new Promise(() => void 0);
1923
+ }
1924
+ function approve(root, args) {
1925
+ const approved = approveMemory(root);
1926
+ const installed = installSkill(root, readConfig(root)?.agent ?? null, approved);
1927
+ const paths = memoryPaths(root);
1928
+ console.log(color.green("Spec approved") + " v" + approved.specVersion);
1929
+ console.log(" " + paths.spec);
1930
+ console.log(" " + paths.specMd);
1931
+ console.log(" " + paths.previewFile);
1932
+ for (const file of installed) console.log(" " + file);
1933
+ console.log("");
1934
+ console.log(color.dim("New UI work must follow this spec. Existing code is not refactored automatically."));
1935
+ void args;
1936
+ }
1937
+ function context(root, args) {
1938
+ const spec = ensureSpec(root);
1939
+ const skip = args[0] === "context" ? 1 : 0;
1940
+ const task = positional(args, skip).join(" ").trim();
1941
+ if (!task) fail("Usage: uifying context <task>");
1942
+ const limitValue = value(args, "--limit");
1943
+ const limit = limitValue ? Number(limitValue) : 8;
1944
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) fail("--limit must be an integer between 1 and 50");
1945
+ const result = buildContext(spec, task, limit);
1946
+ if (has(args, "--json")) {
1947
+ console.log(JSON.stringify(result, null, 2));
1948
+ return;
1949
+ }
1950
+ console.log(formatContext(result));
1951
+ }
1952
+ function check(root, args) {
1953
+ const spec = ensureSpec(root);
1954
+ const config = readConfig(root);
1955
+ if (!config) fail("No .uifying/config.json found. Run: uifying init");
1956
+ const all = has(args, "--all");
1957
+ const base = value(args, "--base") ?? (all ? void 0 : detectBase(root));
1958
+ const failOnRaw = value(args, "--fail-on") ?? "error";
1959
+ if (failOnRaw !== "error" && failOnRaw !== "warning" && failOnRaw !== "none") fail("--fail-on must be error, warning, or none");
1960
+ const options = { base, all, failOn: failOnRaw };
1961
+ const report = runCheck(root, spec, config, options);
1962
+ const out = value(args, "--out") ?? join7(memoryPaths(root).dir, "check-report.json");
1963
+ writeJson(out, report);
1964
+ if (has(args, "--json")) {
1965
+ console.log(JSON.stringify(report, null, 2));
1966
+ } else {
1967
+ console.log(formatCheck(report));
1968
+ console.log(color.dim("Report: " + out));
1969
+ }
1970
+ if (shouldFail(report, failOnRaw)) process.exitCode = 1;
1971
+ }
1972
+ function detectBase(root) {
1973
+ for (const candidate of ["main", "master"]) {
1974
+ if (runGit(root, ["rev-parse", "--verify", candidate])) return candidate;
1975
+ }
1976
+ return void 0;
1977
+ }
1978
+ function doctor(root, args) {
1979
+ const report = runDoctor(root);
1980
+ if (has(args, "--json")) console.log(JSON.stringify(report, null, 2));
1981
+ else console.log(formatDoctor(report));
1982
+ if (!report.ok) process.exitCode = 1;
1983
+ }
1984
+ function status(root) {
1985
+ const spec = readSpec(root);
1986
+ if (!spec) {
1987
+ console.log("No UI spec found. Run: uifying init");
1988
+ process.exitCode = 1;
1989
+ return;
1990
+ }
1991
+ const paths = memoryPaths(root);
1992
+ console.log("Project " + spec.project.name + " (" + spec.project.framework + (spec.project.tailwind ? ", Tailwind " + spec.project.tailwind : "") + ")");
1993
+ console.log("Spec v" + spec.specVersion + " " + spec.status + (spec.approvedAt ? " approved " + spec.approvedAt.slice(0, 10) : ""));
1994
+ console.log("Memory " + paths.dir);
1995
+ console.log("Preview " + paths.previewFile + (existsSync7(paths.previewFile) ? "" : " (missing)"));
1996
+ console.log("Skill " + paths.skillFile + (existsSync7(paths.skillFile) ? "" : " (missing)"));
1997
+ console.log("Items " + spec.items.length + " spec rules, " + spec.components.length + " components, " + spec.patterns.length + " pages");
1998
+ }
1999
+ async function main() {
2000
+ const parsed = parseRoot(process.argv.slice(2));
2001
+ const args = parsed.args;
2002
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
2003
+ console.log(HELP);
2004
+ return;
2005
+ }
2006
+ if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
2007
+ console.log(VERSION);
2008
+ return;
2009
+ }
2010
+ const command = args[0];
2011
+ const root = parsed.root;
2012
+ if (command === "init") {
2013
+ await init(root, args.slice(1), "init");
2014
+ return;
2015
+ }
2016
+ if (command === "spec") {
2017
+ const sub = args[1] ?? "status";
2018
+ const rest = args.slice(2);
2019
+ if (sub === "init") return await init(root, rest, "spec init");
2020
+ if (sub === "preview") return await preview(root, rest, false);
2021
+ if (sub === "approve") return approve(root, rest);
2022
+ if (sub === "status") return status(root);
2023
+ fail("Unknown spec command: " + sub + ". Try: init, preview, approve, status");
2024
+ }
2025
+ if (command === "preview") return await preview(root, args.slice(1), true);
2026
+ if (command === "context") return context(root, args);
2027
+ if (command === "check") return check(root, args.slice(1));
2028
+ if (command === "doctor") return doctor(root, args.slice(1));
2029
+ if (command === "status") return status(root);
2030
+ fail("Unknown command: " + command + ". Run uifying --help");
2031
+ }
2032
+ main().catch((error) => {
2033
+ const message = error instanceof Error ? error.message : String(error);
2034
+ console.error(color.red("error") + " " + message);
2035
+ process.exit(1);
2036
+ });