uilint 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ createSpinner,
4
+ detectNextAppRouter,
5
+ findNextAppRouterProjects,
6
+ intro,
7
+ logError,
8
+ logInfo,
9
+ logSuccess,
10
+ logWarning,
11
+ note,
12
+ outro,
13
+ pc,
14
+ withSpinner
15
+ } from "./chunk-FRNXXIEM.js";
2
16
 
3
17
  // src/index.ts
4
18
  import { Command } from "commander";
5
19
 
6
20
  // src/commands/scan.ts
7
- import { dirname as dirname2, resolve as resolve2 } from "path";
21
+ import { dirname, resolve as resolve2 } from "path";
8
22
  import { existsSync as existsSync2, mkdirSync, statSync, writeFileSync } from "fs";
9
23
  import {
10
24
  createStyleSummary,
@@ -349,101 +363,6 @@ function maybeMs(ms) {
349
363
  return ms == null ? "n/a" : formatMs(ms);
350
364
  }
351
365
 
352
- // src/utils/prompts.ts
353
- import * as p from "@clack/prompts";
354
- import pc from "picocolors";
355
- import { readFileSync } from "fs";
356
- import { dirname, join as join2 } from "path";
357
- import { fileURLToPath } from "url";
358
- function getCLIVersion() {
359
- try {
360
- const __dirname3 = dirname(fileURLToPath(import.meta.url));
361
- const pkgPath = join2(__dirname3, "..", "..", "package.json");
362
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
363
- return pkg.version || "0.0.0";
364
- } catch {
365
- return "0.0.0";
366
- }
367
- }
368
- function intro2(title) {
369
- const version = getCLIVersion();
370
- const header = pc.bold(pc.cyan("\u25C6 UILint")) + pc.dim(` v${version}`);
371
- console.log();
372
- p.intro(title ? `${header} ${pc.dim("\xB7")} ${title}` : header);
373
- }
374
- function outro2(message) {
375
- p.outro(pc.green(message));
376
- }
377
- function cancel2(message = "Operation cancelled.") {
378
- p.cancel(pc.yellow(message));
379
- process.exit(0);
380
- }
381
- function handleCancel(value) {
382
- if (p.isCancel(value)) {
383
- cancel2();
384
- process.exit(0);
385
- }
386
- return value;
387
- }
388
- async function withSpinner(message, fn) {
389
- const s = p.spinner();
390
- s.start(message);
391
- try {
392
- const result = fn.length >= 1 ? await fn(s) : await fn();
393
- s.stop(pc.green("\u2713 ") + message);
394
- return result;
395
- } catch (error) {
396
- s.stop(pc.red("\u2717 ") + message);
397
- throw error;
398
- }
399
- }
400
- function createSpinner() {
401
- return p.spinner();
402
- }
403
- function note2(message, title) {
404
- p.note(message, title);
405
- }
406
- function logInfo(message) {
407
- p.log.info(message);
408
- }
409
- function logSuccess(message) {
410
- p.log.success(message);
411
- }
412
- function logWarning(message) {
413
- p.log.warn(message);
414
- }
415
- function logError(message) {
416
- p.log.error(message);
417
- }
418
- async function select2(options) {
419
- const result = await p.select({
420
- message: options.message,
421
- options: options.options,
422
- initialValue: options.initialValue
423
- });
424
- return handleCancel(result);
425
- }
426
- async function confirm2(options) {
427
- const result = await p.confirm({
428
- message: options.message,
429
- initialValue: options.initialValue ?? true
430
- });
431
- return handleCancel(result);
432
- }
433
- async function text2(options) {
434
- const result = await p.text(options);
435
- return handleCancel(result);
436
- }
437
- async function multiselect2(options) {
438
- const result = await p.multiselect({
439
- message: options.message,
440
- options: options.options,
441
- required: options.required,
442
- initialValues: options.initialValues
443
- });
444
- return handleCancel(result);
445
- }
446
-
447
366
  // src/utils/output.ts
448
367
  import chalk from "chalk";
449
368
  import {
@@ -460,9 +379,9 @@ function envTruthy(name) {
460
379
  if (!v) return false;
461
380
  return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
462
381
  }
463
- function preview(text3, maxLen) {
464
- if (text3.length <= maxLen) return text3;
465
- return text3.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text3.slice(-maxLen);
382
+ function preview(text, maxLen) {
383
+ if (text.length <= maxLen) return text;
384
+ return text.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text.slice(-maxLen);
466
385
  }
467
386
  function debugEnabled(options) {
468
387
  return Boolean(options.debug) || envTruthy("UILINT_DEBUG");
@@ -496,7 +415,7 @@ async function scan(options) {
496
415
  const dbgFull = debugFullEnabled(options);
497
416
  const dbgDump = debugDumpPath(options);
498
417
  if (!isJsonOutput) {
499
- intro2("Scan for UI Issues");
418
+ intro("Scan for UI Issues");
500
419
  }
501
420
  try {
502
421
  let snapshot;
@@ -597,7 +516,7 @@ async function scan(options) {
597
516
  } else {
598
517
  const startPath = snapshot.kind === "source" ? snapshot.inputPath : snapshot.kind === "dom" ? snapshot.inputPath : void 0;
599
518
  if (startPath) {
600
- styleguideLocation = findUILintStyleGuideUpwards(dirname2(startPath));
519
+ styleguideLocation = findUILintStyleGuideUpwards(dirname(startPath));
601
520
  }
602
521
  styleguideLocation = styleguideLocation ?? findStyleGuidePath(projectPath);
603
522
  if (styleguideLocation) {
@@ -611,12 +530,12 @@ async function scan(options) {
611
530
  } else if (!styleGuide) {
612
531
  if (!isJsonOutput) {
613
532
  logWarning("No styleguide found");
614
- note2(
533
+ note(
615
534
  [
616
535
  `Searched in: ${options.styleguide || projectPath}`,
617
536
  "",
618
537
  "Looked for:",
619
- ...STYLEGUIDE_PATHS.map((p2) => ` \u2022 ${p2}`),
538
+ ...STYLEGUIDE_PATHS.map((p) => ` \u2022 ${p}`),
620
539
  "",
621
540
  `Create ${pc.cyan(
622
541
  ".uilint/styleguide.md"
@@ -636,7 +555,7 @@ async function scan(options) {
636
555
  } else if (dbg && styleGuide) {
637
556
  debugLog(dbg, "Styleguide contents (preview)", preview(styleGuide, 800));
638
557
  }
639
- const tailwindSearchDir = (snapshot.kind === "source" || snapshot.kind === "dom") && snapshot.inputPath ? dirname2(snapshot.inputPath) : projectPath;
558
+ const tailwindSearchDir = (snapshot.kind === "source" || snapshot.kind === "dom") && snapshot.inputPath ? dirname(snapshot.inputPath) : projectPath;
640
559
  const tailwindTheme = readTailwindThemeTokens(tailwindSearchDir);
641
560
  const styleSummary = snapshot.kind === "dom" ? createStyleSummary(snapshot.snapshot.styles, {
642
561
  html: snapshot.snapshot.html,
@@ -702,7 +621,7 @@ async function scan(options) {
702
621
  const safeStamp = now.toISOString().replace(/[:.]/g, "-");
703
622
  const resolved = resolve2(process.cwd(), dbgDump);
704
623
  const dumpFile = resolved.endsWith(".json") || resolved.endsWith(".jsonl") ? resolved : resolve2(resolved, `scan-debug-${safeStamp}.json`);
705
- mkdirSync(dirname2(dumpFile), { recursive: true });
624
+ mkdirSync(dirname(dumpFile), { recursive: true });
706
625
  writeFileSync(
707
626
  dumpFile,
708
627
  JSON.stringify(
@@ -848,7 +767,7 @@ async function scan(options) {
848
767
  (firstAnswerNs ?? lastThinkingNs ?? analysisEndNs) - firstThinkingNs
849
768
  ) : null;
850
769
  const outputMs = firstAnswerNs && (lastAnswerNs || analysisEndNs) ? nsToMs((lastAnswerNs ?? analysisEndNs) - firstAnswerNs) : null;
851
- note2(
770
+ note(
852
771
  [
853
772
  `Prepare Ollama: ${formatMs(prepMs)}`,
854
773
  `Time to first token: ${maybeMs(ttftMs)}`,
@@ -898,7 +817,7 @@ async function scan(options) {
898
817
  }
899
818
 
900
819
  // src/commands/analyze.ts
901
- import { dirname as dirname3, resolve as resolve3 } from "path";
820
+ import { dirname as dirname2, resolve as resolve3 } from "path";
902
821
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
903
822
  import {
904
823
  buildSourceScanPrompt,
@@ -913,9 +832,9 @@ function envTruthy2(name) {
913
832
  if (!v) return false;
914
833
  return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
915
834
  }
916
- function preview2(text3, maxLen) {
917
- if (text3.length <= maxLen) return text3;
918
- return text3.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text3.slice(-maxLen);
835
+ function preview2(text, maxLen) {
836
+ if (text.length <= maxLen) return text;
837
+ return text.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text.slice(-maxLen);
919
838
  }
920
839
  function debugEnabled2(options) {
921
840
  return Boolean(options.debug) || envTruthy2("UILINT_DEBUG");
@@ -948,22 +867,22 @@ async function resolveStyleGuideForAnalyze(options) {
948
867
  return { content: options.styleGuide, path: null };
949
868
  }
950
869
  if (options.styleguidePath && typeof options.styleguidePath === "string") {
951
- const p2 = resolvePathSpecifier(options.styleguidePath, process.cwd());
952
- if (existsSync3(p2)) {
953
- return { content: await readStyleGuide2(p2), path: p2 };
870
+ const p = resolvePathSpecifier(options.styleguidePath, process.cwd());
871
+ if (existsSync3(p)) {
872
+ return { content: await readStyleGuide2(p), path: p };
954
873
  }
955
874
  return { content: null, path: null };
956
875
  }
957
876
  const env = process.env.UILINT_STYLEGUIDE_PATH;
958
877
  if (env && env.trim()) {
959
- const p2 = resolvePathSpecifier(env.trim(), process.cwd());
960
- if (existsSync3(p2)) {
961
- return { content: await readStyleGuide2(p2), path: p2 };
878
+ const p = resolvePathSpecifier(env.trim(), process.cwd());
879
+ if (existsSync3(p)) {
880
+ return { content: await readStyleGuide2(p), path: p };
962
881
  }
963
882
  }
964
883
  if (options.inputFile) {
965
884
  const absInput = resolvePathSpecifier(options.inputFile, process.cwd());
966
- const found = findUILintStyleGuideUpwards2(dirname3(absInput));
885
+ const found = findUILintStyleGuideUpwards2(dirname2(absInput));
967
886
  if (found) return { content: await readStyleGuide2(found), path: found };
968
887
  }
969
888
  const cwdPath = findStyleGuidePath2(process.cwd());
@@ -988,7 +907,7 @@ async function analyze(options) {
988
907
  const dbgFull = debugFullEnabled2(options);
989
908
  const dbgDump = debugDumpPath2(options);
990
909
  if (!isJsonOutput) {
991
- intro2("Analyze Source Code");
910
+ intro("Analyze Source Code");
992
911
  }
993
912
  try {
994
913
  debugLog2(dbg, "Input options", {
@@ -1082,7 +1001,7 @@ async function analyze(options) {
1082
1001
  const safeStamp = now.toISOString().replace(/[:.]/g, "-");
1083
1002
  const resolved = resolve3(process.cwd(), dbgDump);
1084
1003
  const dumpFile = resolved.endsWith(".json") || resolved.endsWith(".jsonl") ? resolved : resolve3(resolved, `analyze-debug-${safeStamp}.json`);
1085
- mkdirSync2(dirname3(dumpFile), { recursive: true });
1004
+ mkdirSync2(dirname2(dumpFile), { recursive: true });
1086
1005
  writeFileSync2(
1087
1006
  dumpFile,
1088
1007
  JSON.stringify(
@@ -1185,7 +1104,7 @@ async function analyze(options) {
1185
1104
  (firstAnswerNs ?? lastThinkingNs ?? analysisEndNs) - firstThinkingNs
1186
1105
  ) : null;
1187
1106
  const outputMs = firstAnswerNs && (lastAnswerNs || analysisEndNs) ? nsToMs((lastAnswerNs ?? analysisEndNs) - firstAnswerNs) : null;
1188
- note2(
1107
+ note(
1189
1108
  [
1190
1109
  `Prepare Ollama: ${formatMs(prepMs)}`,
1191
1110
  `Time to first token: ${maybeMs(ttftMs)}`,
@@ -1263,7 +1182,7 @@ async function readStdin2() {
1263
1182
  async function consistency(options) {
1264
1183
  const isJsonOutput = options.output === "json";
1265
1184
  if (!isJsonOutput) {
1266
- intro2("UI Consistency Analysis");
1185
+ intro("UI Consistency Analysis");
1267
1186
  }
1268
1187
  try {
1269
1188
  let inputJson = options.inputJson;
@@ -1353,7 +1272,7 @@ async function consistency(options) {
1353
1272
  }
1354
1273
 
1355
1274
  // src/commands/update.ts
1356
- import { dirname as dirname4, resolve as resolve4 } from "path";
1275
+ import { dirname as dirname3, resolve as resolve4 } from "path";
1357
1276
  import {
1358
1277
  createStyleSummary as createStyleSummary2,
1359
1278
  parseStyleGuide,
@@ -1368,13 +1287,13 @@ import {
1368
1287
  readTailwindThemeTokens as readTailwindThemeTokens2
1369
1288
  } from "uilint-core/node";
1370
1289
  async function update(options) {
1371
- intro2("Update Style Guide");
1290
+ intro("Update Style Guide");
1372
1291
  try {
1373
1292
  const projectPath = process.cwd();
1374
1293
  const styleGuidePath = options.styleguide || findStyleGuidePath3(projectPath);
1375
1294
  if (!styleGuidePath) {
1376
1295
  logError("No style guide found");
1377
- note2(
1296
+ note(
1378
1297
  `Create ${pc.cyan(
1379
1298
  ".uilint/styleguide.md"
1380
1299
  )} first (recommended: run ${pc.cyan("/genstyleguide")} in Cursor).`,
@@ -1397,7 +1316,7 @@ async function update(options) {
1397
1316
  process.exit(1);
1398
1317
  }
1399
1318
  logInfo(`Found ${pc.cyan(String(snapshot.elementCount))} elements`);
1400
- const tailwindSearchDir = options.inputFile ? dirname4(resolve4(projectPath, options.inputFile)) : projectPath;
1319
+ const tailwindSearchDir = options.inputFile ? dirname3(resolve4(projectPath, options.inputFile)) : projectPath;
1401
1320
  const tailwindTheme = readTailwindThemeTokens2(tailwindSearchDir);
1402
1321
  if (options.llm) {
1403
1322
  await withSpinner("Preparing Ollama", async () => {
@@ -1423,15 +1342,15 @@ async function update(options) {
1423
1342
  }
1424
1343
  return line;
1425
1344
  });
1426
- note2(
1345
+ note(
1427
1346
  suggestions.join("\n\n"),
1428
1347
  `Found ${result.issues.length} suggestion(s)`
1429
1348
  );
1430
1349
  logInfo("Edit the styleguide manually to apply these changes.");
1431
- outro2("Analysis complete");
1350
+ outro("Analysis complete");
1432
1351
  } else {
1433
1352
  logSuccess("Style guide is up to date!");
1434
- outro2("No changes needed");
1353
+ outro("No changes needed");
1435
1354
  }
1436
1355
  } else {
1437
1356
  const updatedContent = await withSpinner("Merging styles", async () => {
@@ -1457,14 +1376,14 @@ async function update(options) {
1457
1376
  });
1458
1377
  if (updatedContent === existingContent) {
1459
1378
  logSuccess("Style guide is already up to date!");
1460
- outro2("No changes needed");
1379
+ outro("No changes needed");
1461
1380
  return;
1462
1381
  }
1463
1382
  await withSpinner("Writing styleguide", async () => {
1464
1383
  await writeStyleGuide(styleGuidePath, updatedContent);
1465
1384
  });
1466
- note2(`Updated: ${pc.dim(styleGuidePath)}`, "Success");
1467
- outro2("Style guide updated!");
1385
+ note(`Updated: ${pc.dim(styleGuidePath)}`, "Success");
1386
+ outro("Style guide updated!");
1468
1387
  }
1469
1388
  } catch (error) {
1470
1389
  logError(error instanceof Error ? error.message : "Update failed");
@@ -1474,3631 +1393,226 @@ async function update(options) {
1474
1393
  await flushLangfuse();
1475
1394
  }
1476
1395
 
1477
- // src/commands/install.ts
1478
- import { join as join16 } from "path";
1479
- import { ruleRegistry as ruleRegistry2 } from "uilint-eslint";
1480
-
1481
- // src/commands/install/analyze.ts
1482
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
1483
- import { join as join8 } from "path";
1484
- import { findWorkspaceRoot as findWorkspaceRoot5 } from "uilint-core/node";
1396
+ // src/commands/serve.ts
1397
+ import { existsSync as existsSync5, statSync as statSync3, readdirSync, readFileSync } from "fs";
1398
+ import { createRequire } from "module";
1399
+ import { dirname as dirname5, resolve as resolve5, relative, join as join3, parse as parse2 } from "path";
1400
+ import { WebSocketServer, WebSocket } from "ws";
1401
+ import { watch } from "chokidar";
1402
+ import {
1403
+ findWorkspaceRoot as findWorkspaceRoot4,
1404
+ getVisionAnalyzer as getCoreVisionAnalyzer
1405
+ } from "uilint-core/node";
1485
1406
 
1486
- // src/utils/next-detect.ts
1487
- import { existsSync as existsSync4, readdirSync } from "fs";
1488
- import { join as join3 } from "path";
1489
- function fileExists(projectPath, relPath) {
1490
- return existsSync4(join3(projectPath, relPath));
1491
- }
1492
- function detectNextAppRouter(projectPath) {
1493
- const roots = ["app", join3("src", "app")];
1494
- const candidates = [];
1495
- let chosenRoot = null;
1496
- for (const root of roots) {
1497
- if (existsSync4(join3(projectPath, root))) {
1498
- chosenRoot = root;
1499
- break;
1407
+ // src/utils/vision-run.ts
1408
+ import { dirname as dirname4, join as join2, parse } from "path";
1409
+ import { existsSync as existsSync4, statSync as statSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
1410
+ import {
1411
+ ensureOllamaReady as ensureOllamaReady5,
1412
+ findStyleGuidePath as findStyleGuidePath4,
1413
+ findUILintStyleGuideUpwards as findUILintStyleGuideUpwards3,
1414
+ readStyleGuide as readStyleGuide4,
1415
+ VisionAnalyzer,
1416
+ UILINT_DEFAULT_VISION_MODEL
1417
+ } from "uilint-core/node";
1418
+ async function resolveVisionStyleGuide(args) {
1419
+ const projectPath = args.projectPath;
1420
+ const startDir = args.startDir ?? projectPath;
1421
+ if (args.styleguide) {
1422
+ const styleguideArg = resolvePathSpecifier(args.styleguide, projectPath);
1423
+ if (existsSync4(styleguideArg)) {
1424
+ const stat = statSync2(styleguideArg);
1425
+ if (stat.isFile()) {
1426
+ return {
1427
+ styleguideLocation: styleguideArg,
1428
+ styleGuide: await readStyleGuide4(styleguideArg)
1429
+ };
1430
+ }
1431
+ if (stat.isDirectory()) {
1432
+ const found = findStyleGuidePath4(styleguideArg);
1433
+ return {
1434
+ styleguideLocation: found,
1435
+ styleGuide: found ? await readStyleGuide4(found) : null
1436
+ };
1437
+ }
1500
1438
  }
1439
+ return { styleGuide: null, styleguideLocation: null };
1501
1440
  }
1502
- if (!chosenRoot) return null;
1503
- const entryCandidates = [
1504
- join3(chosenRoot, "layout.tsx"),
1505
- join3(chosenRoot, "layout.jsx"),
1506
- join3(chosenRoot, "layout.ts"),
1507
- join3(chosenRoot, "layout.js"),
1508
- // Fallbacks (less ideal, but can work):
1509
- join3(chosenRoot, "page.tsx"),
1510
- join3(chosenRoot, "page.jsx")
1511
- ];
1512
- for (const rel of entryCandidates) {
1513
- if (fileExists(projectPath, rel)) candidates.push(rel);
1514
- }
1441
+ const upwards = findUILintStyleGuideUpwards3(startDir);
1442
+ const fallback = upwards ?? findStyleGuidePath4(projectPath);
1515
1443
  return {
1516
- appRoot: chosenRoot,
1517
- appRootAbs: join3(projectPath, chosenRoot),
1518
- candidates
1444
+ styleguideLocation: fallback,
1445
+ styleGuide: fallback ? await readStyleGuide4(fallback) : null
1519
1446
  };
1520
1447
  }
1521
- var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
1522
- "node_modules",
1523
- ".git",
1524
- ".next",
1525
- "dist",
1526
- "build",
1527
- "out",
1528
- ".turbo",
1529
- ".vercel",
1530
- ".cursor",
1531
- "coverage",
1532
- ".uilint"
1533
- ]);
1534
- function findNextAppRouterProjects(rootDir, options) {
1535
- const maxDepth = options?.maxDepth ?? 4;
1536
- const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS;
1537
- const results = [];
1538
- const visited = /* @__PURE__ */ new Set();
1539
- function walk(dir, depth) {
1540
- if (depth > maxDepth) return;
1541
- if (visited.has(dir)) return;
1542
- visited.add(dir);
1543
- const detection = detectNextAppRouter(dir);
1544
- if (detection) {
1545
- results.push({ projectPath: dir, detection });
1546
- return;
1547
- }
1548
- let entries = [];
1549
- try {
1550
- entries = readdirSync(dir, { withFileTypes: true }).map((d) => ({
1551
- name: d.name,
1552
- isDirectory: d.isDirectory()
1553
- }));
1554
- } catch {
1555
- return;
1556
- }
1557
- for (const ent of entries) {
1558
- if (!ent.isDirectory) continue;
1559
- if (ignoreDirs.has(ent.name)) continue;
1560
- if (ent.name.startsWith(".") && ent.name !== ".") continue;
1561
- walk(join3(dir, ent.name), depth + 1);
1562
- }
1563
- }
1564
- walk(rootDir, 0);
1565
- return results;
1448
+ var ollamaReadyOnce = /* @__PURE__ */ new Map();
1449
+ async function ensureOllamaReadyCached(params) {
1450
+ const key = `${params.baseUrl}::${params.model}`;
1451
+ const existing = ollamaReadyOnce.get(key);
1452
+ if (existing) return existing;
1453
+ const p = ensureOllamaReady5({ model: params.model, baseUrl: params.baseUrl }).then(() => void 0).catch((e) => {
1454
+ ollamaReadyOnce.delete(key);
1455
+ throw e;
1456
+ });
1457
+ ollamaReadyOnce.set(key, p);
1458
+ return p;
1566
1459
  }
1567
-
1568
- // src/utils/vite-detect.ts
1569
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1570
- import { join as join4 } from "path";
1571
- var VITE_CONFIG_EXTS = [".ts", ".mjs", ".js", ".cjs"];
1572
- function findViteConfigFile(projectPath) {
1573
- for (const ext of VITE_CONFIG_EXTS) {
1574
- const rel = `vite.config${ext}`;
1575
- if (existsSync5(join4(projectPath, rel))) return rel;
1576
- }
1577
- return null;
1460
+ function writeVisionDebugDump(params) {
1461
+ const resolvedDirOrFile = resolvePathSpecifier(
1462
+ params.dumpPath,
1463
+ process.cwd()
1464
+ );
1465
+ const safeStamp = params.now.toISOString().replace(/[:.]/g, "-");
1466
+ const dumpFile = resolvedDirOrFile.endsWith(".json") || resolvedDirOrFile.endsWith(".jsonl") ? resolvedDirOrFile : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;
1467
+ mkdirSync3(dirname4(dumpFile), { recursive: true });
1468
+ writeFileSync3(
1469
+ dumpFile,
1470
+ JSON.stringify(
1471
+ {
1472
+ version: 1,
1473
+ timestamp: params.now.toISOString(),
1474
+ runtime: params.runtime,
1475
+ metadata: params.metadata ?? null,
1476
+ inputs: {
1477
+ imageBase64: params.includeSensitive ? params.inputs.imageBase64 : "(omitted; set debugDumpIncludeSensitive=true)",
1478
+ manifest: params.inputs.manifest,
1479
+ styleguideLocation: params.inputs.styleguideLocation,
1480
+ styleGuide: params.includeSensitive ? params.inputs.styleGuide : "(omitted; set debugDumpIncludeSensitive=true)"
1481
+ }
1482
+ },
1483
+ null,
1484
+ 2
1485
+ ),
1486
+ "utf-8"
1487
+ );
1488
+ return dumpFile;
1578
1489
  }
1579
- function looksLikeReactPackage(projectPath) {
1580
- try {
1581
- const pkgPath = join4(projectPath, "package.json");
1582
- if (!existsSync5(pkgPath)) return false;
1583
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1584
- const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1585
- return "react" in deps || "react-dom" in deps;
1586
- } catch {
1587
- return false;
1490
+ async function runVisionAnalysis(args) {
1491
+ const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;
1492
+ const baseUrl = args.baseUrl ?? "http://localhost:11434";
1493
+ let styleGuide = null;
1494
+ let styleguideLocation = null;
1495
+ if (args.styleGuide !== void 0) {
1496
+ styleGuide = args.styleGuide;
1497
+ styleguideLocation = args.styleguideLocation ?? null;
1498
+ } else {
1499
+ args.onPhase?.("Resolving styleguide...");
1500
+ const resolved = await resolveVisionStyleGuide({
1501
+ projectPath: args.projectPath,
1502
+ styleguide: args.styleguide,
1503
+ startDir: args.styleguideStartDir
1504
+ });
1505
+ styleGuide = resolved.styleGuide;
1506
+ styleguideLocation = resolved.styleguideLocation;
1588
1507
  }
1589
- }
1590
- function fileExists2(projectPath, relPath) {
1591
- return existsSync5(join4(projectPath, relPath));
1592
- }
1593
- function detectViteReact(projectPath) {
1594
- const configFile = findViteConfigFile(projectPath);
1595
- if (!configFile) return null;
1596
- if (!looksLikeReactPackage(projectPath)) return null;
1597
- const entryRoot = "src";
1598
- const candidates = [];
1599
- const entryCandidates = [
1600
- join4(entryRoot, "main.tsx"),
1601
- join4(entryRoot, "main.jsx"),
1602
- join4(entryRoot, "main.ts"),
1603
- join4(entryRoot, "main.js")
1604
- ];
1605
- for (const rel of entryCandidates) {
1606
- if (fileExists2(projectPath, rel)) candidates.push(rel);
1508
+ if (!args.skipEnsureOllama) {
1509
+ args.onPhase?.("Preparing Ollama...");
1510
+ await ensureOllamaReadyCached({ model: visionModel, baseUrl });
1607
1511
  }
1608
- const fallbackCandidates = [
1609
- join4(entryRoot, "App.tsx"),
1610
- join4(entryRoot, "App.jsx")
1611
- ];
1612
- for (const rel of fallbackCandidates) {
1613
- if (!candidates.includes(rel) && fileExists2(projectPath, rel)) {
1614
- candidates.push(rel);
1615
- }
1512
+ if (args.debugDump) {
1513
+ writeVisionDebugDump({
1514
+ dumpPath: args.debugDump,
1515
+ now: /* @__PURE__ */ new Date(),
1516
+ runtime: { visionModel, baseUrl },
1517
+ inputs: {
1518
+ imageBase64: args.imageBase64,
1519
+ manifest: args.manifest,
1520
+ styleguideLocation,
1521
+ styleGuide
1522
+ },
1523
+ includeSensitive: Boolean(args.debugDumpIncludeSensitive),
1524
+ metadata: args.debugDumpMetadata
1525
+ });
1616
1526
  }
1527
+ const analyzer = args.analyzer ?? new VisionAnalyzer({
1528
+ baseUrl: args.baseUrl,
1529
+ visionModel
1530
+ });
1531
+ args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);
1532
+ const result = await analyzer.analyzeScreenshot(
1533
+ args.imageBase64,
1534
+ args.manifest,
1535
+ {
1536
+ styleGuide,
1537
+ onProgress: args.onProgress
1538
+ }
1539
+ );
1540
+ args.onPhase?.(
1541
+ `Done (${result.issues.length} issues, ${result.analysisTime}ms)`
1542
+ );
1617
1543
  return {
1618
- configFile,
1619
- configFileAbs: join4(projectPath, configFile),
1620
- entryRoot,
1621
- candidates
1544
+ issues: result.issues,
1545
+ analysisTime: result.analysisTime,
1546
+ // Prompt is available in newer uilint-core versions; keep this resilient across versions.
1547
+ prompt: result.prompt,
1548
+ rawResponse: result.rawResponse,
1549
+ styleguideLocation,
1550
+ visionModel,
1551
+ baseUrl
1622
1552
  };
1623
1553
  }
1624
- var DEFAULT_IGNORE_DIRS2 = /* @__PURE__ */ new Set([
1625
- "node_modules",
1626
- ".git",
1627
- ".next",
1628
- "dist",
1629
- "build",
1630
- "out",
1631
- ".turbo",
1632
- ".vercel",
1633
- ".cursor",
1634
- "coverage",
1635
- ".uilint"
1636
- ]);
1637
- function findViteReactProjects(rootDir, options) {
1638
- const maxDepth = options?.maxDepth ?? 4;
1639
- const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS2;
1640
- const results = [];
1641
- const visited = /* @__PURE__ */ new Set();
1642
- function walk(dir, depth) {
1643
- if (depth > maxDepth) return;
1644
- if (visited.has(dir)) return;
1645
- visited.add(dir);
1646
- const detection = detectViteReact(dir);
1647
- if (detection) {
1648
- results.push({ projectPath: dir, detection });
1649
- return;
1650
- }
1651
- let entries = [];
1652
- try {
1653
- entries = readdirSync2(dir, { withFileTypes: true }).map((d) => ({
1654
- name: d.name,
1655
- isDirectory: d.isDirectory()
1656
- }));
1657
- } catch {
1658
- return;
1659
- }
1660
- for (const ent of entries) {
1661
- if (!ent.isDirectory) continue;
1662
- if (ignoreDirs.has(ent.name)) continue;
1663
- if (ent.name.startsWith(".") && ent.name !== ".") continue;
1664
- walk(join4(dir, ent.name), depth + 1);
1665
- }
1554
+ function writeVisionMarkdownReport(args) {
1555
+ const p = parse(args.imagePath);
1556
+ const outPath = args.outPath ?? join2(p.dir, `${p.name || p.base}.vision.md`);
1557
+ const lines = [];
1558
+ lines.push(`# UILint Vision Report`);
1559
+ lines.push(``);
1560
+ lines.push(`- Image: \`${p.base}\``);
1561
+ if (args.route) lines.push(`- Route: \`${args.route}\``);
1562
+ if (typeof args.timestamp === "number") {
1563
+ lines.push(`- Timestamp: \`${new Date(args.timestamp).toISOString()}\``);
1564
+ }
1565
+ if (args.visionModel) lines.push(`- Model: \`${args.visionModel}\``);
1566
+ if (args.baseUrl) lines.push(`- Ollama baseUrl: \`${args.baseUrl}\``);
1567
+ if (typeof args.analysisTimeMs === "number")
1568
+ lines.push(`- Analysis time: \`${args.analysisTimeMs}ms\``);
1569
+ lines.push(`- Generated: \`${(/* @__PURE__ */ new Date()).toISOString()}\``);
1570
+ lines.push(``);
1571
+ if (args.metadata && Object.keys(args.metadata).length > 0) {
1572
+ lines.push(`## Metadata`);
1573
+ lines.push(``);
1574
+ lines.push("```json");
1575
+ lines.push(JSON.stringify(args.metadata, null, 2));
1576
+ lines.push("```");
1577
+ lines.push(``);
1666
1578
  }
1667
- walk(rootDir, 0);
1668
- return results;
1579
+ lines.push(`## Prompt`);
1580
+ lines.push(``);
1581
+ lines.push("```text");
1582
+ lines.push((args.prompt ?? "").trim());
1583
+ lines.push("```");
1584
+ lines.push(``);
1585
+ lines.push(`## Raw Response`);
1586
+ lines.push(``);
1587
+ lines.push("```text");
1588
+ lines.push((args.rawResponse ?? "").trim());
1589
+ lines.push("```");
1590
+ lines.push(``);
1591
+ const content = lines.join("\n");
1592
+ mkdirSync3(dirname4(outPath), { recursive: true });
1593
+ writeFileSync3(outPath, content, "utf-8");
1594
+ return { outPath, content };
1669
1595
  }
1670
1596
 
1671
- // src/utils/package-detect.ts
1672
- import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "fs";
1673
- import { join as join5, relative } from "path";
1674
- var DEFAULT_IGNORE_DIRS3 = /* @__PURE__ */ new Set([
1675
- "node_modules",
1676
- ".git",
1677
- ".next",
1678
- "dist",
1679
- "build",
1680
- "out",
1681
- ".turbo",
1682
- ".vercel",
1683
- ".cursor",
1684
- "coverage",
1685
- ".uilint",
1686
- ".pnpm"
1687
- ]);
1688
- var ESLINT_CONFIG_FILES = [
1689
- "eslint.config.js",
1690
- "eslint.config.ts",
1691
- "eslint.config.mjs",
1692
- "eslint.config.cjs",
1693
- ".eslintrc.js",
1694
- ".eslintrc.cjs",
1695
- ".eslintrc.json",
1696
- ".eslintrc.yml",
1697
- ".eslintrc.yaml",
1698
- ".eslintrc"
1699
- ];
1700
- var FRONTEND_INDICATORS = [
1701
- "react",
1702
- "react-dom",
1703
- "next",
1704
- "vue",
1705
- "svelte",
1706
- "@angular/core",
1707
- "solid-js",
1708
- "preact"
1709
- ];
1710
- function isFrontendPackage(pkgJson) {
1711
- const deps = {
1712
- ...pkgJson.dependencies,
1713
- ...pkgJson.devDependencies
1714
- };
1715
- return FRONTEND_INDICATORS.some((pkg) => pkg in deps);
1597
+ // src/commands/serve.ts
1598
+ function pickAppRoot(params) {
1599
+ const { cwd, workspaceRoot } = params;
1600
+ if (detectNextAppRouter(cwd)) return cwd;
1601
+ const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
1602
+ if (matches.length === 0) return cwd;
1603
+ if (matches.length === 1) return matches[0].projectPath;
1604
+ const containing = matches.find(
1605
+ (m) => cwd === m.projectPath || cwd.startsWith(m.projectPath + "/")
1606
+ );
1607
+ if (containing) return containing.projectPath;
1608
+ return matches[0].projectPath;
1716
1609
  }
1717
- function isTypeScriptPackage(dir, pkgJson) {
1718
- if (existsSync6(join5(dir, "tsconfig.json"))) {
1719
- return true;
1720
- }
1721
- const deps = {
1722
- ...pkgJson.dependencies,
1723
- ...pkgJson.devDependencies
1724
- };
1725
- if ("typescript" in deps) {
1726
- return true;
1727
- }
1728
- for (const configFile of ESLINT_CONFIG_FILES) {
1729
- if (configFile.endsWith(".ts") && existsSync6(join5(dir, configFile))) {
1730
- return true;
1731
- }
1732
- }
1733
- return false;
1734
- }
1735
- function hasEslintConfig(dir) {
1736
- for (const file of ESLINT_CONFIG_FILES) {
1737
- if (existsSync6(join5(dir, file))) {
1738
- return true;
1739
- }
1740
- }
1741
- try {
1742
- const pkgPath = join5(dir, "package.json");
1743
- if (existsSync6(pkgPath)) {
1744
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1745
- if (pkg.eslintConfig) return true;
1746
- }
1747
- } catch {
1748
- }
1749
- return false;
1750
- }
1751
- function findPackages(rootDir, options) {
1752
- const maxDepth = options?.maxDepth ?? 5;
1753
- const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS3;
1754
- const results = [];
1755
- const visited = /* @__PURE__ */ new Set();
1756
- function processPackage(dir, isRoot) {
1757
- const pkgPath = join5(dir, "package.json");
1758
- if (!existsSync6(pkgPath)) return null;
1759
- try {
1760
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1761
- const name = pkg.name || relative(rootDir, dir) || ".";
1762
- return {
1763
- path: dir,
1764
- displayPath: relative(rootDir, dir) || ".",
1765
- name,
1766
- hasEslintConfig: hasEslintConfig(dir),
1767
- isFrontend: isFrontendPackage(pkg),
1768
- isRoot,
1769
- isTypeScript: isTypeScriptPackage(dir, pkg)
1770
- };
1771
- } catch {
1772
- return null;
1773
- }
1774
- }
1775
- function walk(dir, depth) {
1776
- if (depth > maxDepth) return;
1777
- if (visited.has(dir)) return;
1778
- visited.add(dir);
1779
- const pkg = processPackage(dir, depth === 0);
1780
- if (pkg) {
1781
- results.push(pkg);
1782
- }
1783
- let entries = [];
1784
- try {
1785
- entries = readdirSync3(dir, { withFileTypes: true }).map((d) => ({
1786
- name: d.name,
1787
- isDirectory: d.isDirectory()
1788
- }));
1789
- } catch {
1790
- return;
1791
- }
1792
- for (const ent of entries) {
1793
- if (!ent.isDirectory) continue;
1794
- if (ignoreDirs.has(ent.name)) continue;
1795
- if (ent.name.startsWith(".")) continue;
1796
- walk(join5(dir, ent.name), depth + 1);
1797
- }
1798
- }
1799
- walk(rootDir, 0);
1800
- return results.sort((a, b) => {
1801
- if (a.isRoot && !b.isRoot) return -1;
1802
- if (!a.isRoot && b.isRoot) return 1;
1803
- if (a.isFrontend && !b.isFrontend) return -1;
1804
- if (!a.isFrontend && b.isFrontend) return 1;
1805
- return a.displayPath.localeCompare(b.displayPath);
1806
- });
1807
- }
1808
- function formatPackageOption(pkg) {
1809
- const hints = [];
1810
- if (pkg.isRoot) hints.push("workspace root");
1811
- if (pkg.isFrontend) hints.push("frontend");
1812
- if (pkg.hasEslintConfig) hints.push("has ESLint config");
1813
- return {
1814
- value: pkg.path,
1815
- label: pkg.displayPath === "." ? pkg.name : `${pkg.name} (${pkg.displayPath})`,
1816
- hint: hints.length > 0 ? hints.join(", ") : void 0
1817
- };
1818
- }
1819
-
1820
- // src/utils/package-manager.ts
1821
- import { existsSync as existsSync7 } from "fs";
1822
- import { spawn } from "child_process";
1823
- import { dirname as dirname5, join as join6 } from "path";
1824
- function detectPackageManager(projectPath) {
1825
- let dir = projectPath;
1826
- for (; ; ) {
1827
- if (existsSync7(join6(dir, "pnpm-lock.yaml"))) return "pnpm";
1828
- if (existsSync7(join6(dir, "pnpm-workspace.yaml"))) return "pnpm";
1829
- if (existsSync7(join6(dir, "yarn.lock"))) return "yarn";
1830
- if (existsSync7(join6(dir, "bun.lockb"))) return "bun";
1831
- if (existsSync7(join6(dir, "bun.lock"))) return "bun";
1832
- if (existsSync7(join6(dir, "package-lock.json"))) return "npm";
1833
- const parent = dirname5(dir);
1834
- if (parent === dir) break;
1835
- dir = parent;
1836
- }
1837
- return "npm";
1838
- }
1839
- function spawnAsync(command, args, cwd) {
1840
- return new Promise((resolve7, reject) => {
1841
- const child = spawn(command, args, {
1842
- cwd,
1843
- stdio: "inherit",
1844
- shell: process.platform === "win32"
1845
- });
1846
- child.on("error", reject);
1847
- child.on("close", (code) => {
1848
- if (code === 0) resolve7();
1849
- else
1850
- reject(new Error(`${command} ${args.join(" ")} exited with ${code}`));
1851
- });
1852
- });
1853
- }
1854
- async function installDependencies(pm, projectPath, packages) {
1855
- if (!packages.length) return;
1856
- switch (pm) {
1857
- case "pnpm":
1858
- await spawnAsync("pnpm", ["add", ...packages], projectPath);
1859
- return;
1860
- case "yarn":
1861
- await spawnAsync("yarn", ["add", ...packages], projectPath);
1862
- return;
1863
- case "bun":
1864
- await spawnAsync("bun", ["add", ...packages], projectPath);
1865
- return;
1866
- case "npm":
1867
- default:
1868
- await spawnAsync("npm", ["install", "--save", ...packages], projectPath);
1869
- return;
1870
- }
1871
- }
1872
-
1873
- // src/utils/eslint-config-inject.ts
1874
- import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1875
- import { join as join7, relative as relative2, dirname as dirname6 } from "path";
1876
- import { parseExpression, parseModule, generateCode } from "magicast";
1877
- import { findWorkspaceRoot as findWorkspaceRoot4 } from "uilint-core/node";
1878
- var CONFIG_EXTENSIONS = [".ts", ".mjs", ".js", ".cjs"];
1879
- function findEslintConfigFile(projectPath) {
1880
- for (const ext of CONFIG_EXTENSIONS) {
1881
- const configPath = join7(projectPath, `eslint.config${ext}`);
1882
- if (existsSync8(configPath)) {
1883
- return configPath;
1884
- }
1885
- }
1886
- return null;
1887
- }
1888
- function getEslintConfigFilename(configPath) {
1889
- const parts = configPath.split("/");
1890
- return parts[parts.length - 1] || "eslint.config.mjs";
1891
- }
1892
- function isIdentifier(node, name) {
1893
- return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
1894
- }
1895
- function isStringLiteral(node) {
1896
- return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
1897
- }
1898
- function getObjectPropertyValue(obj, keyName) {
1899
- if (!obj || obj.type !== "ObjectExpression") return null;
1900
- for (const prop of obj.properties ?? []) {
1901
- if (!prop) continue;
1902
- if (prop.type === "ObjectProperty" || prop.type === "Property") {
1903
- const key = prop.key;
1904
- const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral(key) && key.value === keyName;
1905
- if (keyMatch) return prop.value;
1906
- }
1907
- }
1908
- return null;
1909
- }
1910
- function hasSpreadProperties(obj) {
1911
- if (!obj || obj.type !== "ObjectExpression") return false;
1912
- return (obj.properties ?? []).some(
1913
- (p2) => p2 && (p2.type === "SpreadElement" || p2.type === "SpreadProperty")
1914
- );
1915
- }
1916
- var IGNORED_AST_KEYS = /* @__PURE__ */ new Set([
1917
- "loc",
1918
- "start",
1919
- "end",
1920
- "extra",
1921
- "leadingComments",
1922
- "trailingComments",
1923
- "innerComments"
1924
- ]);
1925
- function normalizeAstForCompare(node) {
1926
- if (node === null) return null;
1927
- if (node === void 0) return void 0;
1928
- if (typeof node !== "object") return node;
1929
- if (Array.isArray(node)) return node.map(normalizeAstForCompare);
1930
- const out = {};
1931
- const keys = Object.keys(node).filter((k) => !IGNORED_AST_KEYS.has(k)).sort();
1932
- for (const k of keys) {
1933
- if (k.startsWith("$")) continue;
1934
- out[k] = normalizeAstForCompare(node[k]);
1935
- }
1936
- return out;
1937
- }
1938
- function astEquivalent(a, b) {
1939
- try {
1940
- return JSON.stringify(normalizeAstForCompare(a)) === JSON.stringify(normalizeAstForCompare(b));
1941
- } catch {
1942
- return false;
1943
- }
1944
- }
1945
- function collectUilintRuleIdsFromRulesObject(rulesObj) {
1946
- const ids = /* @__PURE__ */ new Set();
1947
- if (!rulesObj || rulesObj.type !== "ObjectExpression") return ids;
1948
- for (const prop of rulesObj.properties ?? []) {
1949
- if (!prop) continue;
1950
- if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
1951
- const key = prop.key;
1952
- if (!isStringLiteral(key)) continue;
1953
- const val = key.value;
1954
- if (typeof val !== "string") continue;
1955
- if (val.startsWith("uilint/")) {
1956
- ids.add(val.slice("uilint/".length));
1957
- }
1958
- }
1959
- return ids;
1960
- }
1961
- function findExportedConfigArrayExpression(mod) {
1962
- function unwrapExpression2(expr) {
1963
- let e = expr;
1964
- while (e) {
1965
- if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
1966
- e = e.expression;
1967
- continue;
1968
- }
1969
- if (e.type === "TSSatisfiesExpression") {
1970
- e = e.expression;
1971
- continue;
1972
- }
1973
- if (e.type === "ParenthesizedExpression") {
1974
- e = e.expression;
1975
- continue;
1976
- }
1977
- break;
1978
- }
1979
- return e;
1980
- }
1981
- function resolveTopLevelIdentifierToArrayExpr(program3, name) {
1982
- if (!program3 || program3.type !== "Program") return null;
1983
- for (const stmt of program3.body ?? []) {
1984
- if (stmt?.type !== "VariableDeclaration") continue;
1985
- for (const decl of stmt.declarations ?? []) {
1986
- const id = decl?.id;
1987
- if (!isIdentifier(id, name)) continue;
1988
- const init = unwrapExpression2(decl?.init);
1989
- if (!init) return null;
1990
- if (init.type === "ArrayExpression") return init;
1991
- if (init.type === "CallExpression" && isIdentifier(init.callee, "defineConfig") && unwrapExpression2(init.arguments?.[0])?.type === "ArrayExpression") {
1992
- return unwrapExpression2(init.arguments?.[0]);
1993
- }
1994
- return null;
1995
- }
1996
- }
1997
- return null;
1998
- }
1999
- const program2 = mod?.$ast;
2000
- if (program2 && program2.type === "Program") {
2001
- for (const stmt of program2.body ?? []) {
2002
- if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
2003
- const decl = unwrapExpression2(stmt.declaration);
2004
- if (!decl) break;
2005
- if (decl.type === "ArrayExpression") {
2006
- return { kind: "esm", arrayExpr: decl, program: program2 };
2007
- }
2008
- if (decl.type === "CallExpression" && isIdentifier(decl.callee, "defineConfig") && unwrapExpression2(decl.arguments?.[0])?.type === "ArrayExpression") {
2009
- return {
2010
- kind: "esm",
2011
- arrayExpr: unwrapExpression2(decl.arguments?.[0]),
2012
- program: program2
2013
- };
2014
- }
2015
- if (decl.type === "Identifier" && typeof decl.name === "string") {
2016
- const resolved = resolveTopLevelIdentifierToArrayExpr(
2017
- program2,
2018
- decl.name
2019
- );
2020
- if (resolved) return { kind: "esm", arrayExpr: resolved, program: program2 };
2021
- }
2022
- break;
2023
- }
2024
- }
2025
- if (!program2 || program2.type !== "Program") return null;
2026
- for (const stmt of program2.body ?? []) {
2027
- if (!stmt || stmt.type !== "ExpressionStatement") continue;
2028
- const expr = stmt.expression;
2029
- if (!expr || expr.type !== "AssignmentExpression") continue;
2030
- const left = expr.left;
2031
- const right = expr.right;
2032
- const isModuleExports = left?.type === "MemberExpression" && isIdentifier(left.object, "module") && isIdentifier(left.property, "exports");
2033
- if (!isModuleExports) continue;
2034
- if (right?.type === "ArrayExpression") {
2035
- return { kind: "cjs", arrayExpr: right, program: program2 };
2036
- }
2037
- if (right?.type === "CallExpression" && isIdentifier(right.callee, "defineConfig") && right.arguments?.[0]?.type === "ArrayExpression") {
2038
- return { kind: "cjs", arrayExpr: right.arguments[0], program: program2 };
2039
- }
2040
- if (right?.type === "Identifier" && typeof right.name === "string") {
2041
- const resolved = resolveTopLevelIdentifierToArrayExpr(
2042
- program2,
2043
- right.name
2044
- );
2045
- if (resolved) return { kind: "cjs", arrayExpr: resolved, program: program2 };
2046
- }
2047
- }
2048
- return null;
2049
- }
2050
- function collectConfiguredUilintRuleIdsFromConfigArray(arrayExpr) {
2051
- const ids = /* @__PURE__ */ new Set();
2052
- if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return ids;
2053
- for (const el of arrayExpr.elements ?? []) {
2054
- if (!el || el.type !== "ObjectExpression") continue;
2055
- const rules = getObjectPropertyValue(el, "rules");
2056
- for (const id of collectUilintRuleIdsFromRulesObject(rules)) ids.add(id);
2057
- }
2058
- return ids;
2059
- }
2060
- function findExistingUilintRulesObject(arrayExpr) {
2061
- if (!arrayExpr || arrayExpr.type !== "ArrayExpression") {
2062
- return { configObj: null, rulesObj: null, safeToMutate: false };
2063
- }
2064
- for (const el of arrayExpr.elements ?? []) {
2065
- if (!el || el.type !== "ObjectExpression") continue;
2066
- const plugins = getObjectPropertyValue(el, "plugins");
2067
- const rules = getObjectPropertyValue(el, "rules");
2068
- const hasUilintPlugin = plugins?.type === "ObjectExpression" && getObjectPropertyValue(plugins, "uilint") !== null;
2069
- const uilintIds = collectUilintRuleIdsFromRulesObject(rules);
2070
- const hasUilintRules = uilintIds.size > 0;
2071
- if (!hasUilintPlugin && !hasUilintRules) continue;
2072
- const safe = rules?.type === "ObjectExpression" && !hasSpreadProperties(rules);
2073
- return { configObj: el, rulesObj: rules, safeToMutate: safe };
2074
- }
2075
- return { configObj: null, rulesObj: null, safeToMutate: false };
2076
- }
2077
- function collectTopLevelBindings(program2) {
2078
- const names = /* @__PURE__ */ new Set();
2079
- if (!program2 || program2.type !== "Program") return names;
2080
- for (const stmt of program2.body ?? []) {
2081
- if (stmt?.type === "VariableDeclaration") {
2082
- for (const decl of stmt.declarations ?? []) {
2083
- const id = decl?.id;
2084
- if (id?.type === "Identifier" && typeof id.name === "string") {
2085
- names.add(id.name);
2086
- }
2087
- }
2088
- } else if (stmt?.type === "FunctionDeclaration") {
2089
- if (stmt.id?.type === "Identifier" && typeof stmt.id.name === "string") {
2090
- names.add(stmt.id.name);
2091
- }
2092
- }
2093
- }
2094
- return names;
2095
- }
2096
- function chooseUniqueIdentifier(base, used) {
2097
- if (!used.has(base)) return base;
2098
- let i = 2;
2099
- while (used.has(`${base}${i}`)) i++;
2100
- return `${base}${i}`;
2101
- }
2102
- function addLocalRuleImportsAst(mod, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
2103
- const importNames = /* @__PURE__ */ new Map();
2104
- let changed = false;
2105
- const configDir = dirname6(configPath);
2106
- const rulesDir = join7(rulesRoot, ".uilint", "rules");
2107
- const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
2108
- const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
2109
- const used = collectTopLevelBindings(mod.$ast);
2110
- for (const rule of selectedRules) {
2111
- const importName = chooseUniqueIdentifier(
2112
- `${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
2113
- used
2114
- );
2115
- importNames.set(rule.id, importName);
2116
- used.add(importName);
2117
- const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
2118
- mod.imports.$add({
2119
- imported: "default",
2120
- local: importName,
2121
- from: rulePath
2122
- });
2123
- changed = true;
2124
- }
2125
- return { importNames, changed };
2126
- }
2127
- function addLocalRuleRequiresAst(program2, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
2128
- const importNames = /* @__PURE__ */ new Map();
2129
- let changed = false;
2130
- if (!program2 || program2.type !== "Program") {
2131
- return { importNames, changed };
2132
- }
2133
- const configDir = dirname6(configPath);
2134
- const rulesDir = join7(rulesRoot, ".uilint", "rules");
2135
- const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
2136
- const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
2137
- const used = collectTopLevelBindings(program2);
2138
- for (const rule of selectedRules) {
2139
- const importName = chooseUniqueIdentifier(
2140
- `${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
2141
- used
2142
- );
2143
- importNames.set(rule.id, importName);
2144
- used.add(importName);
2145
- const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
2146
- const stmtMod = parseModule(
2147
- `const ${importName} = require("${rulePath}");`
2148
- );
2149
- const stmt = stmtMod.$ast.body?.[0];
2150
- if (stmt) {
2151
- let insertAt = 0;
2152
- const first = program2.body?.[0];
2153
- if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
2154
- insertAt = 1;
2155
- }
2156
- program2.body.splice(insertAt, 0, stmt);
2157
- changed = true;
2158
- }
2159
- }
2160
- return { importNames, changed };
2161
- }
2162
- function appendUilintConfigBlockToArray(arrayExpr, selectedRules, ruleImportNames) {
2163
- const pluginRulesCode = Array.from(ruleImportNames.entries()).map(([ruleId, importName]) => ` "${ruleId}": ${importName},`).join("\n");
2164
- const rulesPropsCode = selectedRules.map((r) => {
2165
- const ruleKey = `uilint/${r.id}`;
2166
- const valueCode = r.defaultOptions && r.defaultOptions.length > 0 ? `["${r.defaultSeverity}", ...${JSON.stringify(
2167
- r.defaultOptions,
2168
- null,
2169
- 2
2170
- )}]` : `"${r.defaultSeverity}"`;
2171
- return ` "${ruleKey}": ${valueCode},`;
2172
- }).join("\n");
2173
- const blockCode = `{
2174
- files: [
2175
- "src/**/*.{js,jsx,ts,tsx}",
2176
- "app/**/*.{js,jsx,ts,tsx}",
2177
- "pages/**/*.{js,jsx,ts,tsx}",
2178
- ],
2179
- plugins: {
2180
- uilint: {
2181
- rules: {
2182
- ${pluginRulesCode}
2183
- },
2184
- },
2185
- },
2186
- rules: {
2187
- ${rulesPropsCode}
2188
- },
2189
- }`;
2190
- const objExpr = parseExpression(blockCode).$ast;
2191
- arrayExpr.elements.push(objExpr);
2192
- }
2193
- function getUilintEslintConfigInfoFromSourceAst(source) {
2194
- try {
2195
- const mod = parseModule(source);
2196
- const found = findExportedConfigArrayExpression(mod);
2197
- if (!found) {
2198
- return {
2199
- error: "Could not locate an exported ESLint flat config array (expected `export default [...]`, `export default defineConfig([...])`, `module.exports = [...]`, or `module.exports = defineConfig([...])`)."
2200
- };
2201
- }
2202
- const configuredRuleIds = collectConfiguredUilintRuleIdsFromConfigArray(
2203
- found.arrayExpr
2204
- );
2205
- const existingUilint = findExistingUilintRulesObject(found.arrayExpr);
2206
- const configured = configuredRuleIds.size > 0 || existingUilint.configObj !== null;
2207
- return {
2208
- info: { configuredRuleIds, configured },
2209
- mod,
2210
- arrayExpr: found.arrayExpr,
2211
- kind: found.kind
2212
- };
2213
- } catch {
2214
- return {
2215
- error: "Unable to parse ESLint config as JavaScript. Please update it manually or simplify the config so it can be safely auto-modified."
2216
- };
2217
- }
2218
- }
2219
- function getUilintEslintConfigInfoFromSource(source) {
2220
- const ast = getUilintEslintConfigInfoFromSourceAst(source);
2221
- if ("error" in ast) {
2222
- const configuredRuleIds = extractConfiguredUilintRuleIds(source);
2223
- return {
2224
- configuredRuleIds,
2225
- configured: configuredRuleIds.size > 0
2226
- };
2227
- }
2228
- return ast.info;
2229
- }
2230
- function extractConfiguredUilintRuleIds(source) {
2231
- const ids = /* @__PURE__ */ new Set();
2232
- const re = /["']uilint\/([^"']+)["']\s*:/g;
2233
- for (const m of source.matchAll(re)) {
2234
- if (m[1]) ids.add(m[1]);
2235
- }
2236
- return ids;
2237
- }
2238
- function getMissingSelectedRules(selectedRules, configuredIds) {
2239
- return selectedRules.filter((r) => !configuredIds.has(r.id));
2240
- }
2241
- function buildDesiredRuleValueExpression(rule) {
2242
- if (rule.defaultOptions && rule.defaultOptions.length > 0) {
2243
- return `["${rule.defaultSeverity}", ...${JSON.stringify(
2244
- rule.defaultOptions,
2245
- null,
2246
- 2
2247
- )}]`;
2248
- }
2249
- return `"${rule.defaultSeverity}"`;
2250
- }
2251
- function collectUilintRuleValueNodesFromConfigArray(arrayExpr) {
2252
- const out = /* @__PURE__ */ new Map();
2253
- if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return out;
2254
- for (const el of arrayExpr.elements ?? []) {
2255
- if (!el || el.type !== "ObjectExpression") continue;
2256
- const rules = getObjectPropertyValue(el, "rules");
2257
- if (!rules || rules.type !== "ObjectExpression") continue;
2258
- for (const prop of rules.properties ?? []) {
2259
- if (!prop) continue;
2260
- if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
2261
- const key = prop.key;
2262
- if (!isStringLiteral(key)) continue;
2263
- const k = key.value;
2264
- if (typeof k !== "string" || !k.startsWith("uilint/")) continue;
2265
- const id = k.slice("uilint/".length);
2266
- if (!out.has(id)) out.set(id, prop.value);
2267
- }
2268
- }
2269
- return out;
2270
- }
2271
- function getRulesNeedingUpdate(selectedRules, configuredIds, arrayExpr) {
2272
- const existingVals = collectUilintRuleValueNodesFromConfigArray(arrayExpr);
2273
- return selectedRules.filter((r) => {
2274
- if (!configuredIds.has(r.id)) return false;
2275
- const existing = existingVals.get(r.id);
2276
- if (!existing) return true;
2277
- const desiredExpr = buildDesiredRuleValueExpression(r);
2278
- const desiredAst = parseExpression(desiredExpr).$ast;
2279
- return !astEquivalent(existing, desiredAst);
2280
- });
2281
- }
2282
- async function installEslintPlugin(opts) {
2283
- const configPath = findEslintConfigFile(opts.projectPath);
2284
- if (!configPath) {
2285
- return {
2286
- configFile: null,
2287
- modified: false,
2288
- missingRuleIds: [],
2289
- configured: false
2290
- };
2291
- }
2292
- const configFilename = getEslintConfigFilename(configPath);
2293
- const original = readFileSync4(configPath, "utf-8");
2294
- const isCommonJS = configPath.endsWith(".cjs");
2295
- const ast = getUilintEslintConfigInfoFromSourceAst(original);
2296
- if ("error" in ast) {
2297
- return {
2298
- configFile: configFilename,
2299
- modified: false,
2300
- missingRuleIds: [],
2301
- configured: false,
2302
- error: ast.error
2303
- };
2304
- }
2305
- const { info, mod, arrayExpr, kind } = ast;
2306
- const configuredIds = info.configuredRuleIds;
2307
- const missingRules = getMissingSelectedRules(
2308
- opts.selectedRules,
2309
- configuredIds
2310
- );
2311
- const rulesToUpdate = getRulesNeedingUpdate(
2312
- opts.selectedRules,
2313
- configuredIds,
2314
- arrayExpr
2315
- );
2316
- let rulesToApply = [];
2317
- if (!info.configured) {
2318
- rulesToApply = opts.selectedRules;
2319
- } else {
2320
- rulesToApply = [...missingRules, ...rulesToUpdate];
2321
- if (missingRules.length > 0 && !opts.force) {
2322
- const ok = await opts.confirmAddMissingRules?.(
2323
- configFilename,
2324
- missingRules
2325
- );
2326
- if (!ok) {
2327
- return {
2328
- configFile: configFilename,
2329
- modified: false,
2330
- missingRuleIds: missingRules.map((r) => r.id),
2331
- configured: true
2332
- };
2333
- }
2334
- }
2335
- }
2336
- if (rulesToApply.length === 0) {
2337
- return {
2338
- configFile: configFilename,
2339
- modified: false,
2340
- missingRuleIds: missingRules.map((r) => r.id),
2341
- configured: info.configured
2342
- };
2343
- }
2344
- let modifiedAst = false;
2345
- const localRulesDir = join7(opts.projectPath, ".uilint", "rules");
2346
- const workspaceRoot = findWorkspaceRoot4(opts.projectPath);
2347
- const workspaceRulesDir = join7(workspaceRoot, ".uilint", "rules");
2348
- const rulesRoot = existsSync8(localRulesDir) ? opts.projectPath : workspaceRoot;
2349
- let fileExtension = ".js";
2350
- if (rulesToApply.length > 0) {
2351
- const firstRulePath = join7(
2352
- rulesRoot,
2353
- ".uilint",
2354
- "rules",
2355
- `${rulesToApply[0].id}.ts`
2356
- );
2357
- if (existsSync8(firstRulePath)) {
2358
- fileExtension = ".ts";
2359
- }
2360
- }
2361
- let ruleImportNames;
2362
- if (kind === "esm") {
2363
- const result = addLocalRuleImportsAst(
2364
- mod,
2365
- rulesToApply,
2366
- configPath,
2367
- rulesRoot,
2368
- fileExtension
2369
- );
2370
- ruleImportNames = result.importNames;
2371
- if (result.changed) modifiedAst = true;
2372
- } else {
2373
- const result = addLocalRuleRequiresAst(
2374
- mod.$ast,
2375
- rulesToApply,
2376
- configPath,
2377
- rulesRoot,
2378
- fileExtension
2379
- );
2380
- ruleImportNames = result.importNames;
2381
- if (result.changed) modifiedAst = true;
2382
- }
2383
- if (ruleImportNames && ruleImportNames.size > 0) {
2384
- appendUilintConfigBlockToArray(arrayExpr, rulesToApply, ruleImportNames);
2385
- modifiedAst = true;
2386
- }
2387
- if (!info.configured) {
2388
- if (kind === "esm") {
2389
- mod.imports.$add({
2390
- imported: "createRule",
2391
- local: "createRule",
2392
- from: "uilint-eslint"
2393
- });
2394
- modifiedAst = true;
2395
- } else {
2396
- const stmtMod = parseModule(
2397
- `const { createRule } = require("uilint-eslint");`
2398
- );
2399
- const stmt = stmtMod.$ast.body?.[0];
2400
- if (stmt) {
2401
- let insertAt = 0;
2402
- const first = mod.$ast.body?.[0];
2403
- if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
2404
- insertAt = 1;
2405
- }
2406
- mod.$ast.body.splice(insertAt, 0, stmt);
2407
- modifiedAst = true;
2408
- }
2409
- }
2410
- }
2411
- const updated = modifiedAst ? generateCode(mod).code : original;
2412
- if (updated !== original) {
2413
- writeFileSync3(configPath, updated, "utf-8");
2414
- return {
2415
- configFile: configFilename,
2416
- modified: true,
2417
- missingRuleIds: missingRules.map((r) => r.id),
2418
- configured: getUilintEslintConfigInfoFromSource(updated).configured
2419
- };
2420
- }
2421
- return {
2422
- configFile: configFilename,
2423
- modified: false,
2424
- missingRuleIds: missingRules.map((r) => r.id),
2425
- configured: getUilintEslintConfigInfoFromSource(updated).configured
2426
- };
2427
- }
2428
-
2429
- // src/commands/install/analyze.ts
2430
- async function analyze2(projectPath = process.cwd()) {
2431
- const workspaceRoot = findWorkspaceRoot5(projectPath);
2432
- const packageManager = detectPackageManager(projectPath);
2433
- const cursorDir = join8(projectPath, ".cursor");
2434
- const cursorDirExists = existsSync9(cursorDir);
2435
- const styleguidePath = join8(projectPath, ".uilint", "styleguide.md");
2436
- const styleguideExists = existsSync9(styleguidePath);
2437
- const commandsDir = join8(cursorDir, "commands");
2438
- const genstyleguideExists = existsSync9(join8(commandsDir, "genstyleguide.md"));
2439
- const nextApps = [];
2440
- const directDetection = detectNextAppRouter(projectPath);
2441
- if (directDetection) {
2442
- nextApps.push({ projectPath, detection: directDetection });
2443
- } else {
2444
- const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
2445
- for (const match of matches) {
2446
- nextApps.push({
2447
- projectPath: match.projectPath,
2448
- detection: match.detection
2449
- });
2450
- }
2451
- }
2452
- const viteApps = [];
2453
- const directVite = detectViteReact(projectPath);
2454
- if (directVite) {
2455
- viteApps.push({ projectPath, detection: directVite });
2456
- } else {
2457
- const matches = findViteReactProjects(workspaceRoot, { maxDepth: 5 });
2458
- for (const match of matches) {
2459
- viteApps.push({
2460
- projectPath: match.projectPath,
2461
- detection: match.detection
2462
- });
2463
- }
2464
- }
2465
- const rawPackages = findPackages(workspaceRoot);
2466
- const packages = rawPackages.map((pkg) => {
2467
- const eslintConfigPath = findEslintConfigFile(pkg.path);
2468
- let eslintConfigFilename = null;
2469
- let hasRules = false;
2470
- let configuredRuleIds = [];
2471
- if (eslintConfigPath) {
2472
- eslintConfigFilename = getEslintConfigFilename(eslintConfigPath);
2473
- try {
2474
- const source = readFileSync5(eslintConfigPath, "utf-8");
2475
- const info = getUilintEslintConfigInfoFromSource(source);
2476
- hasRules = info.configuredRuleIds.size > 0;
2477
- configuredRuleIds = Array.from(info.configuredRuleIds);
2478
- } catch {
2479
- }
2480
- }
2481
- return {
2482
- ...pkg,
2483
- eslintConfigPath,
2484
- eslintConfigFilename,
2485
- hasUilintRules: hasRules,
2486
- configuredRuleIds
2487
- };
2488
- });
2489
- return {
2490
- projectPath,
2491
- workspaceRoot,
2492
- packageManager,
2493
- cursorDir: {
2494
- exists: cursorDirExists,
2495
- path: cursorDir
2496
- },
2497
- styleguide: {
2498
- exists: styleguideExists,
2499
- path: styleguidePath
2500
- },
2501
- commands: {
2502
- genstyleguide: genstyleguideExists
2503
- },
2504
- nextApps,
2505
- viteApps,
2506
- packages
2507
- };
2508
- }
2509
-
2510
- // src/commands/install/plan.ts
2511
- import { join as join11 } from "path";
2512
- import { createRequire as createRequire2 } from "module";
2513
-
2514
- // src/commands/install/constants.ts
2515
- var GENSTYLEGUIDE_COMMAND_MD = `# React Style Guide Generator
2516
-
2517
- Analyze the React UI codebase to produce a **prescriptive, semantic** style guide. Focus on consistency, intent, and relationships\u2014not specific values.
2518
-
2519
- ## Philosophy
2520
-
2521
- 1. **Identify the intended architecture** from the best patterns in use
2522
- 2. **Prescribe semantic rules** \u2014 about consistency and relationships, not pixels
2523
- 3. **Stay general** \u2014 "primary buttons should be visually consistent" not "buttons use px-4"
2524
- 4. **Focus on intent** \u2014 what should FEEL the same, not what values to use
2525
-
2526
- ## Analysis Steps
2527
-
2528
- ### 1. Detect the Stack
2529
- - Framework: Next.js (App Router? Pages?), Vite, CRA
2530
- - Component system: shadcn, MUI, Chakra, Radix, custom
2531
- - Styling: Tailwind, CSS Modules, styled-components
2532
- - Forms: react-hook-form, Formik, native
2533
- - State: React context, Zustand, Redux, Jotai
2534
-
2535
- ### 2. Identify Best Patterns
2536
- Examine the **best-written** components. Look at:
2537
- - \`components/ui/*\` \u2014 the design system
2538
- - Recently modified files \u2014 current standards
2539
- - Shared layouts \u2014 structural patterns
2540
-
2541
- ### 3. Infer Visual Hierarchy & Intent
2542
- Understand the design language:
2543
- - What distinguishes primary vs secondary actions?
2544
- - How is visual hierarchy established?
2545
- - What creates consistency across similar elements?
2546
-
2547
- ## Output Format
2548
-
2549
- Generate at \`<nextjs app root>/.uilint/styleguide.md\`:
2550
- \`\`\`yaml
2551
- # Stack
2552
- framework:
2553
- styling:
2554
- components:
2555
- component_path:
2556
- forms:
2557
-
2558
- # Component Usage (MUST use these)
2559
- use:
2560
- buttons:
2561
- inputs:
2562
- modals:
2563
- cards:
2564
- feedback:
2565
- icons:
2566
- links:
2567
-
2568
- # Semantic Rules (consistency & relationships)
2569
- semantics:
2570
- hierarchy:
2571
- - <e.g., "primary actions must be visually distinct from secondary">
2572
- - <e.g., "destructive actions should be visually cautionary">
2573
- - <e.g., "page titles should be visually heavier than section titles">
2574
- consistency:
2575
- - <e.g., "all primary buttons should share the same visual weight">
2576
- - <e.g., "form inputs should have uniform height and padding">
2577
- - <e.g., "card padding should be consistent across the app">
2578
- - <e.g., "interactive elements should have consistent hover/focus states">
2579
- spacing:
2580
- - <e.g., "use the spacing scale \u2014 no arbitrary values">
2581
- - <e.g., "related elements should be closer than unrelated">
2582
- - <e.g., "section spacing should be larger than element spacing">
2583
- layout:
2584
- - <e.g., "use gap for sibling spacing, not margin">
2585
- - <e.g., "containers should have consistent max-width and padding">
2586
-
2587
- # Patterns (structural, not values)
2588
- patterns:
2589
- forms: <e.g., "FormField + Controller + zod schema">
2590
- conditionals: <e.g., "cn() for class merging">
2591
- loading: <e.g., "Skeleton for content, Spinner for actions">
2592
- errors: <e.g., "ErrorBoundary at route, inline for forms">
2593
- responsive: <e.g., "mobile-first, standard breakpoints only">
2594
-
2595
- # Component Authoring
2596
- authoring:
2597
- - <e.g., "forwardRef for interactive components">
2598
- - <e.g., "variants via CVA or component props, not className overrides">
2599
- - <e.g., "extract when used 2+ times">
2600
- - <e.g., "'use client' only when needed">
2601
-
2602
- # Forbidden
2603
- forbidden:
2604
- - <e.g., "inline style={{}}">
2605
- - <e.g., "raw HTML elements when component exists">
2606
- - <e.g., "arbitrary values \u2014 use scale">
2607
- - <e.g., "className overrides that break visual consistency">
2608
- - <e.g., "one-off spacing that doesn't match siblings">
2609
-
2610
- # Legacy (if migration in progress)
2611
- legacy:
2612
- - <e.g., "old: CSS modules \u2192 new: Tailwind">
2613
- - <e.g., "old: Formik \u2192 new: react-hook-form">
2614
-
2615
- # Conventions
2616
- conventions:
2617
- -
2618
- -
2619
- -
2620
- \`\`\`
2621
-
2622
- ## Rules
2623
-
2624
- - **Semantic over specific**: "consistent padding" not "p-4"
2625
- - **Relationships over absolutes**: "heavier than" not "font-bold"
2626
- - **Intent over implementation**: "visually distinct" not "blue background"
2627
- - **Prescriptive**: Define target state, not current state
2628
- - **Terse**: No prose. Fragments and short phrases only.
2629
- - **Actionable**: Every rule should be human-verifiable
2630
- - **Omit if N/A**: Skip sections that don't apply
2631
- - **Max 5 items** per section \u2014 highest impact only
2632
- `;
2633
-
2634
- // src/utils/skill-loader.ts
2635
- import { readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2, existsSync as existsSync10 } from "fs";
2636
- import { join as join9, dirname as dirname7, relative as relative3 } from "path";
2637
- import { fileURLToPath as fileURLToPath2 } from "url";
2638
- var __filename = fileURLToPath2(import.meta.url);
2639
- var __dirname = dirname7(__filename);
2640
- function getSkillsDir() {
2641
- const devPath = join9(__dirname, "..", "..", "skills");
2642
- const prodPath = join9(__dirname, "..", "skills");
2643
- if (existsSync10(devPath)) {
2644
- return devPath;
2645
- }
2646
- if (existsSync10(prodPath)) {
2647
- return prodPath;
2648
- }
2649
- throw new Error(
2650
- "Could not find skills directory. This is a bug in uilint installation."
2651
- );
2652
- }
2653
- function collectFiles(dir, baseDir) {
2654
- const files = [];
2655
- const entries = readdirSync4(dir);
2656
- for (const entry of entries) {
2657
- const fullPath = join9(dir, entry);
2658
- const stat = statSync2(fullPath);
2659
- if (stat.isDirectory()) {
2660
- files.push(...collectFiles(fullPath, baseDir));
2661
- } else if (stat.isFile()) {
2662
- const relativePath = relative3(baseDir, fullPath);
2663
- const content = readFileSync6(fullPath, "utf-8");
2664
- files.push({ relativePath, content });
2665
- }
2666
- }
2667
- return files;
2668
- }
2669
- function loadSkill(name) {
2670
- const skillsDir = getSkillsDir();
2671
- const skillDir = join9(skillsDir, name);
2672
- if (!existsSync10(skillDir)) {
2673
- throw new Error(`Skill "${name}" not found in ${skillsDir}`);
2674
- }
2675
- const skillMdPath = join9(skillDir, "SKILL.md");
2676
- if (!existsSync10(skillMdPath)) {
2677
- throw new Error(`Skill "${name}" is missing SKILL.md`);
2678
- }
2679
- const files = collectFiles(skillDir, skillDir);
2680
- return { name, files };
2681
- }
2682
-
2683
- // src/utils/rule-loader.ts
2684
- import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
2685
- import { join as join10, dirname as dirname8 } from "path";
2686
- import { fileURLToPath as fileURLToPath3 } from "url";
2687
- import { createRequire } from "module";
2688
- var __filename2 = fileURLToPath3(import.meta.url);
2689
- var __dirname2 = dirname8(__filename2);
2690
- var require2 = createRequire(import.meta.url);
2691
- function findNodeModulesPackageRoot(pkgName, startDir) {
2692
- let dir = startDir;
2693
- while (true) {
2694
- const candidate = join10(dir, "node_modules", pkgName);
2695
- if (existsSync11(join10(candidate, "package.json"))) return candidate;
2696
- const parent = dirname8(dir);
2697
- if (parent === dir) break;
2698
- dir = parent;
2699
- }
2700
- return null;
2701
- }
2702
- function getUilintEslintPackageRoot() {
2703
- const fromCwd = findNodeModulesPackageRoot("uilint-eslint", process.cwd());
2704
- if (fromCwd) return fromCwd;
2705
- const fromHere = findNodeModulesPackageRoot("uilint-eslint", __dirname2);
2706
- if (fromHere) return fromHere;
2707
- try {
2708
- const entry = require2.resolve("uilint-eslint");
2709
- const entryDir = dirname8(entry);
2710
- return dirname8(entryDir);
2711
- } catch (e) {
2712
- const msg = e instanceof Error ? e.message : String(e);
2713
- throw new Error(
2714
- `Unable to locate uilint-eslint in node_modules (searched upwards from cwd and uilint's install path).
2715
- Resolver error: ${msg}
2716
- Fix: ensure uilint-eslint is installed in the target project (or workspace) and try again.`
2717
- );
2718
- }
2719
- }
2720
- function getUilintEslintSrcDir() {
2721
- const devPath = join10(
2722
- __dirname2,
2723
- "..",
2724
- "..",
2725
- "..",
2726
- "..",
2727
- "uilint-eslint",
2728
- "src"
2729
- );
2730
- if (existsSync11(devPath)) return devPath;
2731
- const pkgRoot = getUilintEslintPackageRoot();
2732
- const srcPath = join10(pkgRoot, "src");
2733
- if (existsSync11(srcPath)) return srcPath;
2734
- throw new Error(
2735
- 'Could not find uilint-eslint "src/" directory. If you are using a published install of uilint-eslint, ensure it includes source files, or run a JS-only rules install.'
2736
- );
2737
- }
2738
- function getUilintEslintDistDir() {
2739
- const devPath = join10(
2740
- __dirname2,
2741
- "..",
2742
- "..",
2743
- "..",
2744
- "..",
2745
- "uilint-eslint",
2746
- "dist"
2747
- );
2748
- if (existsSync11(devPath)) return devPath;
2749
- const pkgRoot = getUilintEslintPackageRoot();
2750
- const distPath = join10(pkgRoot, "dist");
2751
- if (existsSync11(distPath)) return distPath;
2752
- throw new Error(
2753
- 'Could not find uilint-eslint "dist/" directory. This is a bug in uilint installation.'
2754
- );
2755
- }
2756
- function transformRuleContent(content) {
2757
- let transformed = content;
2758
- transformed = transformed.replace(
2759
- /import\s+{\s*createRule\s*}\s+from\s+["']\.\.\/utils\/create-rule\.js["'];?/g,
2760
- 'import { createRule } from "uilint-eslint";'
2761
- );
2762
- transformed = transformed.replace(
2763
- /import\s+createRule\s+from\s+["']\.\.\/utils\/create-rule\.js["'];?/g,
2764
- 'import { createRule } from "uilint-eslint";'
2765
- );
2766
- transformed = transformed.replace(
2767
- /import\s+{([^}]+)}\s+from\s+["']\.\.\/utils\/([^"']+)\.js["'];?/g,
2768
- (match, imports, utilFile) => {
2769
- const utilsFromPackage = ["cache", "styleguide-loader", "import-graph"];
2770
- if (utilsFromPackage.includes(utilFile)) {
2771
- return `import {${imports}} from "uilint-eslint";`;
2772
- }
2773
- return match;
2774
- }
2775
- );
2776
- return transformed;
2777
- }
2778
- function loadRule(ruleId, options = { typescript: true }) {
2779
- const { typescript } = options;
2780
- const extension = typescript ? ".ts" : ".js";
2781
- if (typescript) {
2782
- const rulesDir = join10(getUilintEslintSrcDir(), "rules");
2783
- const implPath = join10(rulesDir, `${ruleId}.ts`);
2784
- const testPath = join10(rulesDir, `${ruleId}.test.ts`);
2785
- if (!existsSync11(implPath)) {
2786
- throw new Error(`Rule "${ruleId}" not found at ${implPath}`);
2787
- }
2788
- const rawContent = readFileSync7(implPath, "utf-8");
2789
- const transformedContent = transformRuleContent(rawContent);
2790
- const implementation = {
2791
- relativePath: `${ruleId}.ts`,
2792
- content: transformedContent
2793
- };
2794
- const test = existsSync11(testPath) ? {
2795
- relativePath: `${ruleId}.test.ts`,
2796
- content: transformRuleContent(readFileSync7(testPath, "utf-8"))
2797
- } : void 0;
2798
- return {
2799
- ruleId,
2800
- implementation,
2801
- test
2802
- };
2803
- } else {
2804
- const rulesDir = join10(getUilintEslintDistDir(), "rules");
2805
- const implPath = join10(rulesDir, `${ruleId}.js`);
2806
- if (!existsSync11(implPath)) {
2807
- throw new Error(
2808
- `Rule "${ruleId}" not found at ${implPath}. For JavaScript-only projects, uilint-eslint must be built to include compiled rule files in dist/rules/. If you're developing uilint-eslint, run 'pnpm build' in packages/uilint-eslint. If you're using a published package, ensure it includes the dist/ directory.`
2809
- );
2810
- }
2811
- const content = readFileSync7(implPath, "utf-8");
2812
- const implementation = {
2813
- relativePath: `${ruleId}.js`,
2814
- content
2815
- };
2816
- return {
2817
- ruleId,
2818
- implementation
2819
- };
2820
- }
2821
- }
2822
- function loadSelectedRules(ruleIds, options = { typescript: true }) {
2823
- return ruleIds.map((id) => loadRule(id, options));
2824
- }
2825
-
2826
- // src/commands/install/plan.ts
2827
- var require3 = createRequire2(import.meta.url);
2828
- function getSelfDependencyVersionRange(pkgName) {
2829
- try {
2830
- const pkgJson = require3("uilint/package.json");
2831
- const deps = pkgJson?.dependencies;
2832
- const optDeps = pkgJson?.optionalDependencies;
2833
- const peerDeps = pkgJson?.peerDependencies;
2834
- const v = deps?.[pkgName] ?? optDeps?.[pkgName] ?? peerDeps?.[pkgName];
2835
- return typeof v === "string" ? v : null;
2836
- } catch {
2837
- return null;
2838
- }
2839
- }
2840
- function toInstallSpecifier(pkgName) {
2841
- const range = getSelfDependencyVersionRange(pkgName);
2842
- if (!range) return pkgName;
2843
- if (range.startsWith("workspace:")) return pkgName;
2844
- if (range.startsWith("file:")) return pkgName;
2845
- if (range.startsWith("link:")) return pkgName;
2846
- return `${pkgName}@${range}`;
2847
- }
2848
- function createPlan(state, choices, options = {}) {
2849
- const actions = [];
2850
- const dependencies = [];
2851
- const { force = false } = options;
2852
- const { items } = choices;
2853
- const needsCursorDir = items.includes("genstyleguide") || items.includes("skill");
2854
- if (needsCursorDir && !state.cursorDir.exists) {
2855
- actions.push({
2856
- type: "create_directory",
2857
- path: state.cursorDir.path
2858
- });
2859
- }
2860
- if (items.includes("genstyleguide")) {
2861
- const commandsDir = join11(state.cursorDir.path, "commands");
2862
- actions.push({
2863
- type: "create_directory",
2864
- path: commandsDir
2865
- });
2866
- actions.push({
2867
- type: "create_file",
2868
- path: join11(commandsDir, "genstyleguide.md"),
2869
- content: GENSTYLEGUIDE_COMMAND_MD
2870
- });
2871
- }
2872
- if (items.includes("skill")) {
2873
- const skillsDir = join11(state.cursorDir.path, "skills");
2874
- actions.push({
2875
- type: "create_directory",
2876
- path: skillsDir
2877
- });
2878
- try {
2879
- const skill = loadSkill("ui-consistency-enforcer");
2880
- const skillDir = join11(skillsDir, skill.name);
2881
- actions.push({
2882
- type: "create_directory",
2883
- path: skillDir
2884
- });
2885
- for (const file of skill.files) {
2886
- const filePath = join11(skillDir, file.relativePath);
2887
- const fileDir = join11(
2888
- skillDir,
2889
- file.relativePath.split("/").slice(0, -1).join("/")
2890
- );
2891
- if (fileDir !== skillDir && file.relativePath.includes("/")) {
2892
- actions.push({
2893
- type: "create_directory",
2894
- path: fileDir
2895
- });
2896
- }
2897
- actions.push({
2898
- type: "create_file",
2899
- path: filePath,
2900
- content: file.content
2901
- });
2902
- }
2903
- } catch {
2904
- }
2905
- }
2906
- if (items.includes("next") && choices.next) {
2907
- const { projectPath, detection } = choices.next;
2908
- actions.push({
2909
- type: "install_next_routes",
2910
- projectPath,
2911
- appRoot: detection.appRoot
2912
- });
2913
- dependencies.push({
2914
- packagePath: projectPath,
2915
- packageManager: state.packageManager,
2916
- packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
2917
- });
2918
- actions.push({
2919
- type: "inject_react",
2920
- projectPath,
2921
- appRoot: detection.appRoot
2922
- });
2923
- actions.push({
2924
- type: "inject_next_config",
2925
- projectPath
2926
- });
2927
- }
2928
- if (items.includes("vite") && choices.vite) {
2929
- const { projectPath, detection } = choices.vite;
2930
- dependencies.push({
2931
- packagePath: projectPath,
2932
- packageManager: state.packageManager,
2933
- packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
2934
- });
2935
- actions.push({
2936
- type: "inject_react",
2937
- projectPath,
2938
- appRoot: detection.entryRoot,
2939
- mode: "vite"
2940
- });
2941
- actions.push({
2942
- type: "inject_vite_config",
2943
- projectPath
2944
- });
2945
- }
2946
- if (items.includes("eslint") && choices.eslint) {
2947
- const { packagePaths, selectedRules } = choices.eslint;
2948
- for (const pkgPath of packagePaths) {
2949
- const pkgInfo = state.packages.find((p2) => p2.path === pkgPath);
2950
- const rulesDir = join11(pkgPath, ".uilint", "rules");
2951
- actions.push({
2952
- type: "create_directory",
2953
- path: rulesDir
2954
- });
2955
- const isTypeScript = pkgInfo?.isTypeScript ?? true;
2956
- const ruleFiles = loadSelectedRules(
2957
- selectedRules.map((r) => r.id),
2958
- {
2959
- typescript: isTypeScript
2960
- }
2961
- );
2962
- for (const ruleFile of ruleFiles) {
2963
- actions.push({
2964
- type: "create_file",
2965
- path: join11(rulesDir, ruleFile.implementation.relativePath),
2966
- content: ruleFile.implementation.content
2967
- });
2968
- if (ruleFile.test && isTypeScript) {
2969
- actions.push({
2970
- type: "create_file",
2971
- path: join11(rulesDir, ruleFile.test.relativePath),
2972
- content: ruleFile.test.content
2973
- });
2974
- }
2975
- }
2976
- dependencies.push({
2977
- packagePath: pkgPath,
2978
- packageManager: state.packageManager,
2979
- packages: [toInstallSpecifier("uilint-eslint"), "typescript-eslint"]
2980
- });
2981
- if (pkgInfo?.eslintConfigPath) {
2982
- actions.push({
2983
- type: "inject_eslint",
2984
- packagePath: pkgPath,
2985
- configPath: pkgInfo.eslintConfigPath,
2986
- rules: selectedRules,
2987
- hasExistingRules: pkgInfo.hasUilintRules
2988
- });
2989
- }
2990
- }
2991
- const gitignorePath = join11(state.workspaceRoot, ".gitignore");
2992
- actions.push({
2993
- type: "append_to_file",
2994
- path: gitignorePath,
2995
- content: "\n# UILint cache\n.uilint/.cache\n",
2996
- ifNotContains: ".uilint/.cache"
2997
- });
2998
- }
2999
- return { actions, dependencies };
3000
- }
3001
-
3002
- // src/commands/install/execute.ts
3003
- import {
3004
- existsSync as existsSync16,
3005
- mkdirSync as mkdirSync3,
3006
- writeFileSync as writeFileSync7,
3007
- readFileSync as readFileSync11,
3008
- unlinkSync,
3009
- chmodSync
3010
- } from "fs";
3011
- import { dirname as dirname9 } from "path";
3012
-
3013
- // src/utils/react-inject.ts
3014
- import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
3015
- import { join as join12 } from "path";
3016
- import { parseModule as parseModule2, generateCode as generateCode2 } from "magicast";
3017
- function getDefaultCandidates(projectPath, appRoot) {
3018
- const viteMainCandidates = [
3019
- join12(appRoot, "main.tsx"),
3020
- join12(appRoot, "main.jsx"),
3021
- join12(appRoot, "main.ts"),
3022
- join12(appRoot, "main.js")
3023
- ];
3024
- const existingViteMain = viteMainCandidates.filter(
3025
- (rel) => existsSync12(join12(projectPath, rel))
3026
- );
3027
- if (existingViteMain.length > 0) return existingViteMain;
3028
- const viteAppCandidates = [join12(appRoot, "App.tsx"), join12(appRoot, "App.jsx")];
3029
- const existingViteApp = viteAppCandidates.filter(
3030
- (rel) => existsSync12(join12(projectPath, rel))
3031
- );
3032
- if (existingViteApp.length > 0) return existingViteApp;
3033
- const layoutCandidates = [
3034
- join12(appRoot, "layout.tsx"),
3035
- join12(appRoot, "layout.jsx"),
3036
- join12(appRoot, "layout.ts"),
3037
- join12(appRoot, "layout.js")
3038
- ];
3039
- const existingLayouts = layoutCandidates.filter(
3040
- (rel) => existsSync12(join12(projectPath, rel))
3041
- );
3042
- if (existingLayouts.length > 0) {
3043
- return existingLayouts;
3044
- }
3045
- const pageCandidates = [join12(appRoot, "page.tsx"), join12(appRoot, "page.jsx")];
3046
- return pageCandidates.filter((rel) => existsSync12(join12(projectPath, rel)));
3047
- }
3048
- function isUseClientDirective(stmt) {
3049
- return stmt?.type === "ExpressionStatement" && stmt.expression?.type === "StringLiteral" && stmt.expression.value === "use client";
3050
- }
3051
- function findImportDeclaration(program2, from) {
3052
- if (!program2 || program2.type !== "Program") return null;
3053
- for (const stmt of program2.body ?? []) {
3054
- if (stmt?.type !== "ImportDeclaration") continue;
3055
- if (stmt.source?.value === from) return stmt;
3056
- }
3057
- return null;
3058
- }
3059
- function walkAst(node, visit) {
3060
- if (!node || typeof node !== "object") return;
3061
- if (node.type) visit(node);
3062
- for (const key of Object.keys(node)) {
3063
- const v = node[key];
3064
- if (!v) continue;
3065
- if (Array.isArray(v)) {
3066
- for (const item of v) walkAst(item, visit);
3067
- } else if (typeof v === "object" && v.type) {
3068
- walkAst(v, visit);
3069
- }
3070
- }
3071
- }
3072
- function hasUILintDevtoolsJsx(program2) {
3073
- let found = false;
3074
- walkAst(program2, (node) => {
3075
- if (found) return;
3076
- if (node.type !== "JSXElement") return;
3077
- const name = node.openingElement?.name;
3078
- if (name?.type === "JSXIdentifier") {
3079
- if (name.name === "UILintProvider" || name.name === "uilint-devtools") {
3080
- found = true;
3081
- }
3082
- }
3083
- });
3084
- return found;
3085
- }
3086
- function addDevtoolsElementNextJs(program2) {
3087
- if (!program2 || program2.type !== "Program") return { changed: false };
3088
- if (hasUILintDevtoolsJsx(program2)) return { changed: false };
3089
- const devtoolsMod = parseModule2(
3090
- "const __uilint_devtools = (<uilint-devtools />);"
3091
- );
3092
- const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
3093
- if (!devtoolsJsx || devtoolsJsx.type !== "JSXElement")
3094
- return { changed: false };
3095
- let added = false;
3096
- walkAst(program2, (node) => {
3097
- if (added) return;
3098
- if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
3099
- const children = node.children ?? [];
3100
- const childrenIndex = children.findIndex(
3101
- (child) => child?.type === "JSXExpressionContainer" && child.expression?.type === "Identifier" && child.expression.name === "children"
3102
- );
3103
- if (childrenIndex === -1) return;
3104
- children.splice(childrenIndex + 1, 0, devtoolsJsx);
3105
- added = true;
3106
- });
3107
- if (!added) {
3108
- throw new Error("Could not find `{children}` in target file to add devtools.");
3109
- }
3110
- return { changed: true };
3111
- }
3112
- function addDevtoolsElementVite(program2) {
3113
- if (!program2 || program2.type !== "Program") return { changed: false };
3114
- if (hasUILintDevtoolsJsx(program2)) return { changed: false };
3115
- let added = false;
3116
- walkAst(program2, (node) => {
3117
- if (added) return;
3118
- if (node.type !== "CallExpression") return;
3119
- const callee = node.callee;
3120
- if (callee?.type !== "MemberExpression") return;
3121
- const prop = callee.property;
3122
- const isRender = prop?.type === "Identifier" && prop.name === "render" || prop?.type === "StringLiteral" && prop.value === "render" || prop?.type === "Literal" && prop.value === "render";
3123
- if (!isRender) return;
3124
- const arg0 = node.arguments?.[0];
3125
- if (!arg0) return;
3126
- if (arg0.type !== "JSXElement" && arg0.type !== "JSXFragment") return;
3127
- const devtoolsMod = parseModule2(
3128
- "const __uilint_devtools = (<uilint-devtools />);"
3129
- );
3130
- const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
3131
- if (!devtoolsJsx) return;
3132
- const fragmentMod = parseModule2(
3133
- "const __fragment = (<></>);"
3134
- );
3135
- const fragmentJsx = fragmentMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
3136
- if (!fragmentJsx) return;
3137
- fragmentJsx.children = [arg0, devtoolsJsx];
3138
- node.arguments[0] = fragmentJsx;
3139
- added = true;
3140
- });
3141
- if (!added) {
3142
- throw new Error(
3143
- "Could not find a `.render(<...>)` call to add devtools. Expected a React entry like `createRoot(...).render(<App />)`."
3144
- );
3145
- }
3146
- return { changed: true };
3147
- }
3148
- function ensureSideEffectImport(program2, from) {
3149
- if (!program2 || program2.type !== "Program") return { changed: false };
3150
- const existing = findImportDeclaration(program2, from);
3151
- if (existing) return { changed: false };
3152
- const importDecl = parseModule2(`import "${from}";`).$ast.body?.[0];
3153
- if (!importDecl) return { changed: false };
3154
- const body = program2.body ?? [];
3155
- let insertAt = 0;
3156
- while (insertAt < body.length && isUseClientDirective(body[insertAt])) {
3157
- insertAt++;
3158
- }
3159
- while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
3160
- insertAt++;
3161
- }
3162
- program2.body.splice(insertAt, 0, importDecl);
3163
- return { changed: true };
3164
- }
3165
- async function installReactUILintOverlay(opts) {
3166
- const candidates = getDefaultCandidates(opts.projectPath, opts.appRoot);
3167
- if (!candidates.length) {
3168
- throw new Error(
3169
- `No suitable entry files found under ${opts.appRoot} (expected Next.js layout/page or Vite main/App).`
3170
- );
3171
- }
3172
- let chosen;
3173
- if (candidates.length > 1 && opts.confirmFileChoice) {
3174
- chosen = await opts.confirmFileChoice(candidates);
3175
- } else {
3176
- chosen = candidates[0];
3177
- }
3178
- const absTarget = join12(opts.projectPath, chosen);
3179
- const original = readFileSync8(absTarget, "utf-8");
3180
- let mod;
3181
- try {
3182
- mod = parseModule2(original);
3183
- } catch {
3184
- throw new Error(
3185
- `Unable to parse ${chosen} as JavaScript/TypeScript. Please update it manually.`
3186
- );
3187
- }
3188
- const program2 = mod.$ast;
3189
- const hasDevtoolsImport = !!findImportDeclaration(program2, "uilint-react/devtools");
3190
- const hasOldImport = !!findImportDeclaration(program2, "uilint-react");
3191
- const alreadyConfigured = (hasDevtoolsImport || hasOldImport) && hasUILintDevtoolsJsx(program2);
3192
- let changed = false;
3193
- const importRes = ensureSideEffectImport(program2, "uilint-react/devtools");
3194
- if (importRes.changed) changed = true;
3195
- const mode = opts.mode ?? "next";
3196
- const addRes = mode === "vite" ? addDevtoolsElementVite(program2) : addDevtoolsElementNextJs(program2);
3197
- if (addRes.changed) changed = true;
3198
- const updated = changed ? generateCode2(mod).code : original;
3199
- const modified = updated !== original;
3200
- if (modified) {
3201
- writeFileSync4(absTarget, updated, "utf-8");
3202
- }
3203
- return {
3204
- targetFile: chosen,
3205
- modified,
3206
- alreadyConfigured: alreadyConfigured && !modified
3207
- };
3208
- }
3209
-
3210
- // src/utils/next-config-inject.ts
3211
- import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
3212
- import { join as join13 } from "path";
3213
- import { parseModule as parseModule3, generateCode as generateCode3 } from "magicast";
3214
- var CONFIG_EXTENSIONS2 = [".ts", ".mjs", ".js", ".cjs"];
3215
- function findNextConfigFile(projectPath) {
3216
- for (const ext of CONFIG_EXTENSIONS2) {
3217
- const configPath = join13(projectPath, `next.config${ext}`);
3218
- if (existsSync13(configPath)) {
3219
- return configPath;
3220
- }
3221
- }
3222
- return null;
3223
- }
3224
- function getNextConfigFilename(configPath) {
3225
- const parts = configPath.split("/");
3226
- return parts[parts.length - 1] || "next.config.ts";
3227
- }
3228
- function isIdentifier2(node, name) {
3229
- return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
3230
- }
3231
- function isStringLiteral2(node) {
3232
- return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
3233
- }
3234
- function ensureEsmWithJsxLocImport(program2) {
3235
- if (!program2 || program2.type !== "Program") return { changed: false };
3236
- const existing = (program2.body ?? []).find(
3237
- (s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin"
3238
- );
3239
- if (existing) {
3240
- const has = (existing.specifiers ?? []).some(
3241
- (sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "withJsxLoc" || sp.imported?.value === "withJsxLoc")
3242
- );
3243
- if (has) return { changed: false };
3244
- const spec = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0]?.specifiers?.[0];
3245
- if (!spec) return { changed: false };
3246
- existing.specifiers = [...existing.specifiers ?? [], spec];
3247
- return { changed: true };
3248
- }
3249
- const importDecl = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0];
3250
- if (!importDecl) return { changed: false };
3251
- const body = program2.body ?? [];
3252
- let insertAt = 0;
3253
- while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
3254
- insertAt++;
3255
- }
3256
- program2.body.splice(insertAt, 0, importDecl);
3257
- return { changed: true };
3258
- }
3259
- function ensureCjsWithJsxLocRequire(program2) {
3260
- if (!program2 || program2.type !== "Program") return { changed: false };
3261
- for (const stmt of program2.body ?? []) {
3262
- if (stmt?.type !== "VariableDeclaration") continue;
3263
- for (const decl of stmt.declarations ?? []) {
3264
- const init = decl?.init;
3265
- if (init?.type === "CallExpression" && isIdentifier2(init.callee, "require") && isStringLiteral2(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin") {
3266
- if (decl.id?.type === "ObjectPattern") {
3267
- const has = (decl.id.properties ?? []).some((p2) => {
3268
- if (p2?.type !== "ObjectProperty" && p2?.type !== "Property") return false;
3269
- return isIdentifier2(p2.key, "withJsxLoc");
3270
- });
3271
- if (has) return { changed: false };
3272
- const prop = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
3273
- if (!prop) return { changed: false };
3274
- decl.id.properties = [...decl.id.properties ?? [], prop];
3275
- return { changed: true };
3276
- }
3277
- return { changed: false };
3278
- }
3279
- }
3280
- }
3281
- const reqDecl = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0];
3282
- if (!reqDecl) return { changed: false };
3283
- program2.body.unshift(reqDecl);
3284
- return { changed: true };
3285
- }
3286
- function wrapEsmExportDefault(program2) {
3287
- if (!program2 || program2.type !== "Program") return { changed: false };
3288
- const exportDecl = (program2.body ?? []).find(
3289
- (s) => s?.type === "ExportDefaultDeclaration"
3290
- );
3291
- if (!exportDecl) return { changed: false };
3292
- const decl = exportDecl.declaration;
3293
- if (decl?.type === "CallExpression" && isIdentifier2(decl.callee, "withJsxLoc")) {
3294
- return { changed: false };
3295
- }
3296
- exportDecl.declaration = {
3297
- type: "CallExpression",
3298
- callee: { type: "Identifier", name: "withJsxLoc" },
3299
- arguments: [decl]
3300
- };
3301
- return { changed: true };
3302
- }
3303
- function wrapCjsModuleExports(program2) {
3304
- if (!program2 || program2.type !== "Program") return { changed: false };
3305
- for (const stmt of program2.body ?? []) {
3306
- if (!stmt || stmt.type !== "ExpressionStatement") continue;
3307
- const expr = stmt.expression;
3308
- if (!expr || expr.type !== "AssignmentExpression") continue;
3309
- const left = expr.left;
3310
- const right = expr.right;
3311
- const isModuleExports = left?.type === "MemberExpression" && isIdentifier2(left.object, "module") && isIdentifier2(left.property, "exports");
3312
- if (!isModuleExports) continue;
3313
- if (right?.type === "CallExpression" && isIdentifier2(right.callee, "withJsxLoc")) {
3314
- return { changed: false };
3315
- }
3316
- expr.right = {
3317
- type: "CallExpression",
3318
- callee: { type: "Identifier", name: "withJsxLoc" },
3319
- arguments: [right]
3320
- };
3321
- return { changed: true };
3322
- }
3323
- return { changed: false };
3324
- }
3325
- async function installJsxLocPlugin(opts) {
3326
- const configPath = findNextConfigFile(opts.projectPath);
3327
- if (!configPath) {
3328
- return { configFile: null, modified: false };
3329
- }
3330
- const configFilename = getNextConfigFilename(configPath);
3331
- const original = readFileSync9(configPath, "utf-8");
3332
- let mod;
3333
- try {
3334
- mod = parseModule3(original);
3335
- } catch {
3336
- return { configFile: configFilename, modified: false };
3337
- }
3338
- const program2 = mod.$ast;
3339
- const isCjs = configPath.endsWith(".cjs");
3340
- let changed = false;
3341
- if (isCjs) {
3342
- const reqRes = ensureCjsWithJsxLocRequire(program2);
3343
- if (reqRes.changed) changed = true;
3344
- const wrapRes = wrapCjsModuleExports(program2);
3345
- if (wrapRes.changed) changed = true;
3346
- } else {
3347
- const impRes = ensureEsmWithJsxLocImport(program2);
3348
- if (impRes.changed) changed = true;
3349
- const wrapRes = wrapEsmExportDefault(program2);
3350
- if (wrapRes.changed) changed = true;
3351
- }
3352
- const updated = changed ? generateCode3(mod).code : original;
3353
- if (updated !== original) {
3354
- writeFileSync5(configPath, updated, "utf-8");
3355
- return { configFile: configFilename, modified: true };
3356
- }
3357
- return { configFile: configFilename, modified: false };
3358
- }
3359
-
3360
- // src/utils/vite-config-inject.ts
3361
- import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
3362
- import { join as join14 } from "path";
3363
- import { parseModule as parseModule4, generateCode as generateCode4 } from "magicast";
3364
- var CONFIG_EXTENSIONS3 = [".ts", ".mjs", ".js", ".cjs"];
3365
- function findViteConfigFile2(projectPath) {
3366
- for (const ext of CONFIG_EXTENSIONS3) {
3367
- const configPath = join14(projectPath, `vite.config${ext}`);
3368
- if (existsSync14(configPath)) return configPath;
3369
- }
3370
- return null;
3371
- }
3372
- function getViteConfigFilename(configPath) {
3373
- const parts = configPath.split("/");
3374
- return parts[parts.length - 1] || "vite.config.ts";
3375
- }
3376
- function isIdentifier3(node, name) {
3377
- return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
3378
- }
3379
- function isStringLiteral3(node) {
3380
- return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
3381
- }
3382
- function unwrapExpression(expr) {
3383
- let e = expr;
3384
- while (e) {
3385
- if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
3386
- e = e.expression;
3387
- continue;
3388
- }
3389
- if (e.type === "TSSatisfiesExpression") {
3390
- e = e.expression;
3391
- continue;
3392
- }
3393
- if (e.type === "ParenthesizedExpression") {
3394
- e = e.expression;
3395
- continue;
3396
- }
3397
- break;
3398
- }
3399
- return e;
3400
- }
3401
- function findExportedConfigObjectExpression(mod) {
3402
- const program2 = mod?.$ast;
3403
- if (!program2 || program2.type !== "Program") return null;
3404
- for (const stmt of program2.body ?? []) {
3405
- if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
3406
- const decl = unwrapExpression(stmt.declaration);
3407
- if (!decl) break;
3408
- if (decl.type === "ObjectExpression") {
3409
- return { kind: "esm", objExpr: decl, program: program2 };
3410
- }
3411
- if (decl.type === "CallExpression" && isIdentifier3(decl.callee, "defineConfig") && unwrapExpression(decl.arguments?.[0])?.type === "ObjectExpression") {
3412
- return {
3413
- kind: "esm",
3414
- objExpr: unwrapExpression(decl.arguments?.[0]),
3415
- program: program2
3416
- };
3417
- }
3418
- break;
3419
- }
3420
- for (const stmt of program2.body ?? []) {
3421
- if (!stmt || stmt.type !== "ExpressionStatement") continue;
3422
- const expr = stmt.expression;
3423
- if (!expr || expr.type !== "AssignmentExpression") continue;
3424
- const left = expr.left;
3425
- const right = unwrapExpression(expr.right);
3426
- const isModuleExports = left?.type === "MemberExpression" && isIdentifier3(left.object, "module") && isIdentifier3(left.property, "exports");
3427
- if (!isModuleExports) continue;
3428
- if (right?.type === "ObjectExpression") {
3429
- return { kind: "cjs", objExpr: right, program: program2 };
3430
- }
3431
- if (right?.type === "CallExpression" && isIdentifier3(right.callee, "defineConfig") && unwrapExpression(right.arguments?.[0])?.type === "ObjectExpression") {
3432
- return {
3433
- kind: "cjs",
3434
- objExpr: unwrapExpression(right.arguments?.[0]),
3435
- program: program2
3436
- };
3437
- }
3438
- }
3439
- return null;
3440
- }
3441
- function getObjectProperty(obj, keyName) {
3442
- if (!obj || obj.type !== "ObjectExpression") return null;
3443
- for (const prop of obj.properties ?? []) {
3444
- if (!prop) continue;
3445
- if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
3446
- const key = prop.key;
3447
- const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral3(key) && key.value === keyName;
3448
- if (keyMatch) return prop;
3449
- }
3450
- return null;
3451
- }
3452
- function ensureEsmJsxLocImport(program2) {
3453
- if (!program2 || program2.type !== "Program") return { changed: false };
3454
- const existing = (program2.body ?? []).find(
3455
- (s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin/vite"
3456
- );
3457
- if (existing) {
3458
- const has = (existing.specifiers ?? []).some(
3459
- (sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "jsxLoc" || sp.imported?.value === "jsxLoc")
3460
- );
3461
- if (has) return { changed: false };
3462
- const spec = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0]?.specifiers?.[0];
3463
- if (!spec) return { changed: false };
3464
- existing.specifiers = [...existing.specifiers ?? [], spec];
3465
- return { changed: true };
3466
- }
3467
- const importDecl = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0];
3468
- if (!importDecl) return { changed: false };
3469
- const body = program2.body ?? [];
3470
- let insertAt = 0;
3471
- while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
3472
- insertAt++;
3473
- }
3474
- program2.body.splice(insertAt, 0, importDecl);
3475
- return { changed: true };
3476
- }
3477
- function ensureCjsJsxLocRequire(program2) {
3478
- if (!program2 || program2.type !== "Program") return { changed: false };
3479
- for (const stmt of program2.body ?? []) {
3480
- if (stmt?.type !== "VariableDeclaration") continue;
3481
- for (const decl of stmt.declarations ?? []) {
3482
- const init = decl?.init;
3483
- if (init?.type === "CallExpression" && isIdentifier3(init.callee, "require") && isStringLiteral3(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin/vite") {
3484
- if (decl.id?.type === "ObjectPattern") {
3485
- const has = (decl.id.properties ?? []).some((p2) => {
3486
- if (p2?.type !== "ObjectProperty" && p2?.type !== "Property") return false;
3487
- return isIdentifier3(p2.key, "jsxLoc");
3488
- });
3489
- if (has) return { changed: false };
3490
- const prop = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
3491
- if (!prop) return { changed: false };
3492
- decl.id.properties = [...decl.id.properties ?? [], prop];
3493
- return { changed: true };
3494
- }
3495
- return { changed: false };
3496
- }
3497
- }
3498
- }
3499
- const reqDecl = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0];
3500
- if (!reqDecl) return { changed: false };
3501
- program2.body.unshift(reqDecl);
3502
- return { changed: true };
3503
- }
3504
- function pluginsHasJsxLoc(arr) {
3505
- if (!arr || arr.type !== "ArrayExpression") return false;
3506
- for (const el of arr.elements ?? []) {
3507
- const e = unwrapExpression(el);
3508
- if (!e) continue;
3509
- if (e.type === "CallExpression" && isIdentifier3(e.callee, "jsxLoc")) return true;
3510
- }
3511
- return false;
3512
- }
3513
- function ensurePluginsContainsJsxLoc(configObj) {
3514
- const pluginsProp = getObjectProperty(configObj, "plugins");
3515
- if (!pluginsProp) {
3516
- const prop = parseModule4("export default { plugins: [jsxLoc()] };").$ast.body?.find((s) => s.type === "ExportDefaultDeclaration")?.declaration?.properties?.find((p2) => {
3517
- const k = p2?.key;
3518
- return k?.type === "Identifier" && k.name === "plugins" || isStringLiteral3(k) && k.value === "plugins";
3519
- });
3520
- if (!prop) return { changed: false };
3521
- configObj.properties = [...configObj.properties ?? [], prop];
3522
- return { changed: true };
3523
- }
3524
- const value = unwrapExpression(pluginsProp.value);
3525
- if (!value) return { changed: false };
3526
- if (value.type === "ArrayExpression") {
3527
- if (pluginsHasJsxLoc(value)) return { changed: false };
3528
- const jsxLocCall2 = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
3529
- if (!jsxLocCall2) return { changed: false };
3530
- value.elements.push(jsxLocCall2);
3531
- return { changed: true };
3532
- }
3533
- const jsxLocCall = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
3534
- if (!jsxLocCall) return { changed: false };
3535
- const spread = { type: "SpreadElement", argument: value };
3536
- pluginsProp.value = { type: "ArrayExpression", elements: [spread, jsxLocCall] };
3537
- return { changed: true };
3538
- }
3539
- async function installViteJsxLocPlugin(opts) {
3540
- const configPath = findViteConfigFile2(opts.projectPath);
3541
- if (!configPath) return { configFile: null, modified: false };
3542
- const configFilename = getViteConfigFilename(configPath);
3543
- const original = readFileSync10(configPath, "utf-8");
3544
- const isCjs = configPath.endsWith(".cjs");
3545
- let mod;
3546
- try {
3547
- mod = parseModule4(original);
3548
- } catch {
3549
- return { configFile: configFilename, modified: false };
3550
- }
3551
- const found = findExportedConfigObjectExpression(mod);
3552
- if (!found) return { configFile: configFilename, modified: false };
3553
- let changed = false;
3554
- if (isCjs) {
3555
- const reqRes = ensureCjsJsxLocRequire(found.program);
3556
- if (reqRes.changed) changed = true;
3557
- } else {
3558
- const impRes = ensureEsmJsxLocImport(found.program);
3559
- if (impRes.changed) changed = true;
3560
- }
3561
- const pluginsRes = ensurePluginsContainsJsxLoc(found.objExpr);
3562
- if (pluginsRes.changed) changed = true;
3563
- const updated = changed ? generateCode4(mod).code : original;
3564
- if (updated !== original) {
3565
- writeFileSync6(configPath, updated, "utf-8");
3566
- return { configFile: configFilename, modified: true };
3567
- }
3568
- return { configFile: configFilename, modified: false };
3569
- }
3570
-
3571
- // src/utils/next-routes.ts
3572
- import { existsSync as existsSync15 } from "fs";
3573
- import { mkdir, writeFile } from "fs/promises";
3574
- import { join as join15 } from "path";
3575
- var DEV_SOURCE_ROUTE_TS = `/**
3576
- * Dev-only API route for fetching source files
3577
- *
3578
- * This route allows the UILint overlay to fetch and display source code
3579
- * for components rendered on the page.
3580
- *
3581
- * Security:
3582
- * - Only available in development mode
3583
- * - Validates file path is within project root
3584
- * - Only allows specific file extensions
3585
- */
3586
-
3587
- import { NextRequest, NextResponse } from "next/server";
3588
- import { readFileSync, existsSync } from "fs";
3589
- import { resolve, relative, dirname, extname, sep } from "path";
3590
- import { fileURLToPath } from "url";
3591
-
3592
- export const runtime = "nodejs";
3593
-
3594
- // Allowed file extensions
3595
- const ALLOWED_EXTENSIONS = new Set([".tsx", ".ts", ".jsx", ".js", ".css"]);
3596
-
3597
- /**
3598
- * Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
3599
- *
3600
- * Why: In monorepos, process.cwd() might be the workspace root (it also has package.json),
3601
- * which would incorrectly store/read files under the wrong directory.
3602
- */
3603
- function findNextProjectRoot(): string {
3604
- // Prefer discovering via this route module's on-disk path.
3605
- // In Next, route code is executed from within ".next/server/...".
3606
- try {
3607
- const selfPath = fileURLToPath(import.meta.url);
3608
- const marker = sep + ".next" + sep;
3609
- const idx = selfPath.lastIndexOf(marker);
3610
- if (idx !== -1) {
3611
- return selfPath.slice(0, idx);
3612
- }
3613
- } catch {
3614
- // ignore
3615
- }
3616
-
3617
- // Fallback: walk up from cwd looking for .next/
3618
- let dir = process.cwd();
3619
- for (let i = 0; i < 20; i++) {
3620
- if (existsSync(resolve(dir, ".next"))) return dir;
3621
- const parent = dirname(dir);
3622
- if (parent === dir) break;
3623
- dir = parent;
3624
- }
3625
-
3626
- // Final fallback: cwd
3627
- return process.cwd();
3628
- }
3629
-
3630
- /**
3631
- * Validate that a path is within the allowed directory
3632
- */
3633
- function isPathWithinRoot(filePath: string, root: string): boolean {
3634
- const resolved = resolve(filePath);
3635
- const resolvedRoot = resolve(root);
3636
- return resolved.startsWith(resolvedRoot + "/") || resolved === resolvedRoot;
3637
- }
3638
-
3639
- /**
3640
- * Find workspace root by walking up looking for pnpm-workspace.yaml or .git
3641
- */
3642
- function findWorkspaceRoot(startDir: string): string {
3643
- let dir = startDir;
3644
- for (let i = 0; i < 10; i++) {
3645
- if (
3646
- existsSync(resolve(dir, "pnpm-workspace.yaml")) ||
3647
- existsSync(resolve(dir, ".git"))
3648
- ) {
3649
- return dir;
3650
- }
3651
- const parent = dirname(dir);
3652
- if (parent === dir) break;
3653
- dir = parent;
3654
- }
3655
- return startDir;
3656
- }
3657
-
3658
- export async function GET(request: NextRequest) {
3659
- // Block in production
3660
- if (process.env.NODE_ENV === "production") {
3661
- return NextResponse.json(
3662
- { error: "Not available in production" },
3663
- { status: 404 }
3664
- );
3665
- }
3666
-
3667
- const { searchParams } = new URL(request.url);
3668
- const filePath = searchParams.get("path");
3669
-
3670
- if (!filePath) {
3671
- return NextResponse.json(
3672
- { error: "Missing 'path' query parameter" },
3673
- { status: 400 }
3674
- );
3675
- }
3676
-
3677
- // Validate extension
3678
- const ext = extname(filePath).toLowerCase();
3679
- if (!ALLOWED_EXTENSIONS.has(ext)) {
3680
- return NextResponse.json(
3681
- { error: \`File extension '\${ext}' not allowed\` },
3682
- { status: 403 }
3683
- );
3684
- }
3685
-
3686
- // Find project root (prefer Next project root over workspace root)
3687
- const projectRoot = findNextProjectRoot();
3688
-
3689
- // Resolve the file path
3690
- const resolvedPath = resolve(filePath);
3691
-
3692
- // Security check: ensure path is within project root or workspace root
3693
- const workspaceRoot = findWorkspaceRoot(projectRoot);
3694
- const isWithinApp = isPathWithinRoot(resolvedPath, projectRoot);
3695
- const isWithinWorkspace = isPathWithinRoot(resolvedPath, workspaceRoot);
3696
-
3697
- if (!isWithinApp && !isWithinWorkspace) {
3698
- return NextResponse.json(
3699
- { error: "Path outside project directory" },
3700
- { status: 403 }
3701
- );
3702
- }
3703
-
3704
- // Check file exists
3705
- if (!existsSync(resolvedPath)) {
3706
- return NextResponse.json({ error: "File not found" }, { status: 404 });
3707
- }
3708
-
3709
- try {
3710
- const content = readFileSync(resolvedPath, "utf-8");
3711
- const relativePath = relative(workspaceRoot, resolvedPath);
3712
-
3713
- return NextResponse.json({
3714
- content,
3715
- relativePath,
3716
- projectRoot,
3717
- workspaceRoot,
3718
- });
3719
- } catch (error) {
3720
- console.error("[Dev Source API] Error reading file:", error);
3721
- return NextResponse.json({ error: "Failed to read file" }, { status: 500 });
3722
- }
3723
- }
3724
- `;
3725
- var SCREENSHOT_ROUTE_TS = `/**
3726
- * Dev-only API route for saving and retrieving vision analysis screenshots
3727
- *
3728
- * This route allows the UILint overlay to:
3729
- * - POST: Save screenshots and element manifests for vision analysis
3730
- * - GET: Retrieve screenshots or list available screenshots
3731
- *
3732
- * Security:
3733
- * - Only available in development mode
3734
- * - Saves to .uilint/screenshots/ directory within project
3735
- */
3736
-
3737
- import { NextRequest, NextResponse } from "next/server";
3738
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
3739
- import { resolve, join, dirname, basename, sep } from "path";
3740
- import { fileURLToPath } from "url";
3741
-
3742
- export const runtime = "nodejs";
3743
-
3744
- // Maximum screenshot size (10MB)
3745
- const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
3746
-
3747
- /**
3748
- * Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
3749
- */
3750
- function findNextProjectRoot(): string {
3751
- try {
3752
- const selfPath = fileURLToPath(import.meta.url);
3753
- const marker = sep + ".next" + sep;
3754
- const idx = selfPath.lastIndexOf(marker);
3755
- if (idx !== -1) {
3756
- return selfPath.slice(0, idx);
3757
- }
3758
- } catch {
3759
- // ignore
3760
- }
3761
-
3762
- let dir = process.cwd();
3763
- for (let i = 0; i < 20; i++) {
3764
- if (existsSync(resolve(dir, ".next"))) return dir;
3765
- const parent = dirname(dir);
3766
- if (parent === dir) break;
3767
- dir = parent;
3768
- }
3769
-
3770
- return process.cwd();
3771
- }
3772
-
3773
- /**
3774
- * Get the screenshots directory path, creating it if needed
3775
- */
3776
- function getScreenshotsDir(projectRoot: string): string {
3777
- const screenshotsDir = join(projectRoot, ".uilint", "screenshots");
3778
- if (!existsSync(screenshotsDir)) {
3779
- mkdirSync(screenshotsDir, { recursive: true });
3780
- }
3781
- return screenshotsDir;
3782
- }
3783
-
3784
- /**
3785
- * Validate filename to prevent path traversal
3786
- */
3787
- function isValidFilename(filename: string): boolean {
3788
- // Only allow alphanumeric, hyphens, underscores, and dots
3789
- // Must end with .png, .jpeg, .jpg, or .json
3790
- const validPattern = /^[a-zA-Z0-9_-]+\\.(png|jpeg|jpg|json)$/;
3791
- return validPattern.test(filename) && !filename.includes("..");
3792
- }
3793
-
3794
- /**
3795
- * POST: Save a screenshot and optionally its manifest
3796
- */
3797
- export async function POST(request: NextRequest) {
3798
- // Block in production
3799
- if (process.env.NODE_ENV === "production") {
3800
- return NextResponse.json(
3801
- { error: "Not available in production" },
3802
- { status: 404 }
3803
- );
3804
- }
3805
-
3806
- try {
3807
- const body = await request.json();
3808
- const { filename, imageData, manifest, analysisResult } = body;
3809
-
3810
- if (!filename) {
3811
- return NextResponse.json({ error: "Missing 'filename'" }, { status: 400 });
3812
- }
3813
-
3814
- // Validate filename
3815
- if (!isValidFilename(filename)) {
3816
- return NextResponse.json(
3817
- { error: "Invalid filename format" },
3818
- { status: 400 }
3819
- );
3820
- }
3821
-
3822
- // Allow "sidecar-only" updates (manifest/analysisResult) without re-sending image bytes.
3823
- const hasImageData = typeof imageData === "string" && imageData.length > 0;
3824
- const hasSidecar =
3825
- typeof manifest !== "undefined" || typeof analysisResult !== "undefined";
3826
-
3827
- if (!hasImageData && !hasSidecar) {
3828
- return NextResponse.json(
3829
- { error: "Nothing to save (provide imageData and/or manifest/analysisResult)" },
3830
- { status: 400 }
3831
- );
3832
- }
3833
-
3834
- // Check size (image only)
3835
- if (hasImageData && imageData.length > MAX_SCREENSHOT_SIZE) {
3836
- return NextResponse.json(
3837
- { error: "Screenshot too large (max 10MB)" },
3838
- { status: 413 }
3839
- );
3840
- }
3841
-
3842
- const projectRoot = findNextProjectRoot();
3843
- const screenshotsDir = getScreenshotsDir(projectRoot);
3844
-
3845
- const imagePath = join(screenshotsDir, filename);
3846
-
3847
- // Save the image (base64 data URL) if provided
3848
- if (hasImageData) {
3849
- const base64Data = imageData.includes(",")
3850
- ? imageData.split(",")[1]
3851
- : imageData;
3852
- writeFileSync(imagePath, Buffer.from(base64Data, "base64"));
3853
- }
3854
-
3855
- // Save manifest and analysis result as JSON sidecar
3856
- if (hasSidecar) {
3857
- const jsonFilename = filename.replace(/\\.(png|jpeg|jpg)$/, ".json");
3858
- const jsonPath = join(screenshotsDir, jsonFilename);
3859
-
3860
- // If a sidecar already exists, merge updates (lets us POST analysisResult later without re-sending image).
3861
- let existing: any = null;
3862
- if (existsSync(jsonPath)) {
3863
- try {
3864
- existing = JSON.parse(readFileSync(jsonPath, "utf-8"));
3865
- } catch {
3866
- existing = null;
3867
- }
3868
- }
3869
-
3870
- const routeFromAnalysis =
3871
- analysisResult && typeof analysisResult === "object"
3872
- ? (analysisResult as any).route
3873
- : undefined;
3874
- const issuesFromAnalysis =
3875
- analysisResult && typeof analysisResult === "object"
3876
- ? (analysisResult as any).issues
3877
- : undefined;
3878
-
3879
- const jsonData = {
3880
- ...(existing && typeof existing === "object" ? existing : {}),
3881
- timestamp: Date.now(),
3882
- filename,
3883
- screenshotFile: filename,
3884
- route:
3885
- typeof routeFromAnalysis === "string"
3886
- ? routeFromAnalysis
3887
- : (existing as any)?.route ?? null,
3888
- issues:
3889
- Array.isArray(issuesFromAnalysis)
3890
- ? issuesFromAnalysis
3891
- : (existing as any)?.issues ?? null,
3892
- manifest: typeof manifest === "undefined" ? existing?.manifest ?? null : manifest,
3893
- analysisResult:
3894
- typeof analysisResult === "undefined"
3895
- ? existing?.analysisResult ?? null
3896
- : analysisResult,
3897
- };
3898
- writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
3899
- }
3900
-
3901
- return NextResponse.json({
3902
- success: true,
3903
- path: imagePath,
3904
- projectRoot,
3905
- screenshotsDir,
3906
- });
3907
- } catch (error) {
3908
- console.error("[Screenshot API] Error saving screenshot:", error);
3909
- return NextResponse.json(
3910
- { error: "Failed to save screenshot" },
3911
- { status: 500 }
3912
- );
3913
- }
3914
- }
3915
-
3916
- /**
3917
- * GET: Retrieve a screenshot or list available screenshots
3918
- */
3919
- export async function GET(request: NextRequest) {
3920
- // Block in production
3921
- if (process.env.NODE_ENV === "production") {
3922
- return NextResponse.json(
3923
- { error: "Not available in production" },
3924
- { status: 404 }
3925
- );
3926
- }
3927
-
3928
- const { searchParams } = new URL(request.url);
3929
- const filename = searchParams.get("filename");
3930
- const list = searchParams.get("list");
3931
-
3932
- const projectRoot = findNextProjectRoot();
3933
- const screenshotsDir = getScreenshotsDir(projectRoot);
3934
-
3935
- // List mode: return all screenshots
3936
- if (list === "true") {
3937
- try {
3938
- const files = readdirSync(screenshotsDir);
3939
- const screenshots = files
3940
- .filter((f) => /\\.(png|jpeg|jpg)$/.test(f))
3941
- .map((f) => {
3942
- const jsonFile = f.replace(/\\.(png|jpeg|jpg)$/, ".json");
3943
- const jsonPath = join(screenshotsDir, jsonFile);
3944
- let metadata = null;
3945
- if (existsSync(jsonPath)) {
3946
- try {
3947
- metadata = JSON.parse(readFileSync(jsonPath, "utf-8"));
3948
- } catch {
3949
- // Ignore parse errors
3950
- }
3951
- }
3952
- return {
3953
- filename: f,
3954
- metadata,
3955
- };
3956
- })
3957
- .sort((a, b) => {
3958
- // Sort by timestamp descending (newest first)
3959
- const aTime = a.metadata?.timestamp || 0;
3960
- const bTime = b.metadata?.timestamp || 0;
3961
- return bTime - aTime;
3962
- });
3963
-
3964
- return NextResponse.json({ screenshots, projectRoot, screenshotsDir });
3965
- } catch (error) {
3966
- console.error("[Screenshot API] Error listing screenshots:", error);
3967
- return NextResponse.json(
3968
- { error: "Failed to list screenshots" },
3969
- { status: 500 }
3970
- );
3971
- }
3972
- }
3973
-
3974
- // Retrieve mode: get specific screenshot
3975
- if (!filename) {
3976
- return NextResponse.json(
3977
- { error: "Missing 'filename' parameter" },
3978
- { status: 400 }
3979
- );
3980
- }
3981
-
3982
- if (!isValidFilename(filename)) {
3983
- return NextResponse.json(
3984
- { error: "Invalid filename format" },
3985
- { status: 400 }
3986
- );
3987
- }
3988
-
3989
- const filePath = join(screenshotsDir, filename);
3990
-
3991
- if (!existsSync(filePath)) {
3992
- return NextResponse.json(
3993
- { error: "Screenshot not found" },
3994
- { status: 404 }
3995
- );
3996
- }
3997
-
3998
- try {
3999
- const content = readFileSync(filePath);
4000
-
4001
- // Determine content type
4002
- const ext = filename.split(".").pop()?.toLowerCase();
4003
- const contentType =
4004
- ext === "json"
4005
- ? "application/json"
4006
- : ext === "png"
4007
- ? "image/png"
4008
- : "image/jpeg";
4009
-
4010
- if (ext === "json") {
4011
- return NextResponse.json(JSON.parse(content.toString()));
4012
- }
4013
-
4014
- return new NextResponse(content, {
4015
- headers: {
4016
- "Content-Type": contentType,
4017
- "Cache-Control": "no-cache",
4018
- },
4019
- });
4020
- } catch (error) {
4021
- console.error("[Screenshot API] Error reading screenshot:", error);
4022
- return NextResponse.json(
4023
- { error: "Failed to read screenshot" },
4024
- { status: 500 }
4025
- );
4026
- }
4027
- }
4028
- `;
4029
- async function writeRouteFile(absPath, relPath, content, opts) {
4030
- if (existsSync15(absPath) && !opts.force) return;
4031
- await writeFile(absPath, content, "utf-8");
4032
- }
4033
- async function installNextUILintRoutes(opts) {
4034
- const baseRel = join15(opts.appRoot, "api", ".uilint");
4035
- const baseAbs = join15(opts.projectPath, baseRel);
4036
- await mkdir(join15(baseAbs, "source"), { recursive: true });
4037
- await writeRouteFile(
4038
- join15(baseAbs, "source", "route.ts"),
4039
- join15(baseRel, "source", "route.ts"),
4040
- DEV_SOURCE_ROUTE_TS,
4041
- opts
4042
- );
4043
- await mkdir(join15(baseAbs, "screenshots"), { recursive: true });
4044
- await writeRouteFile(
4045
- join15(baseAbs, "screenshots", "route.ts"),
4046
- join15(baseRel, "screenshots", "route.ts"),
4047
- SCREENSHOT_ROUTE_TS,
4048
- opts
4049
- );
4050
- }
4051
-
4052
- // src/commands/install/execute.ts
4053
- async function executeAction(action, options) {
4054
- const { dryRun = false } = options;
4055
- try {
4056
- switch (action.type) {
4057
- case "create_directory": {
4058
- if (dryRun) {
4059
- return {
4060
- action,
4061
- success: true,
4062
- wouldDo: `Create directory: ${action.path}`
4063
- };
4064
- }
4065
- if (!existsSync16(action.path)) {
4066
- mkdirSync3(action.path, { recursive: true });
4067
- }
4068
- return { action, success: true };
4069
- }
4070
- case "create_file": {
4071
- if (dryRun) {
4072
- return {
4073
- action,
4074
- success: true,
4075
- wouldDo: `Create file: ${action.path}${action.permissions ? ` (mode: ${action.permissions.toString(8)})` : ""}`
4076
- };
4077
- }
4078
- const dir = dirname9(action.path);
4079
- if (!existsSync16(dir)) {
4080
- mkdirSync3(dir, { recursive: true });
4081
- }
4082
- writeFileSync7(action.path, action.content, "utf-8");
4083
- if (action.permissions) {
4084
- chmodSync(action.path, action.permissions);
4085
- }
4086
- return { action, success: true };
4087
- }
4088
- case "merge_json": {
4089
- if (dryRun) {
4090
- return {
4091
- action,
4092
- success: true,
4093
- wouldDo: `Merge JSON into: ${action.path}`
4094
- };
4095
- }
4096
- let existing = {};
4097
- if (existsSync16(action.path)) {
4098
- try {
4099
- existing = JSON.parse(readFileSync11(action.path, "utf-8"));
4100
- } catch {
4101
- }
4102
- }
4103
- const merged = deepMerge(existing, action.merge);
4104
- const dir = dirname9(action.path);
4105
- if (!existsSync16(dir)) {
4106
- mkdirSync3(dir, { recursive: true });
4107
- }
4108
- writeFileSync7(action.path, JSON.stringify(merged, null, 2), "utf-8");
4109
- return { action, success: true };
4110
- }
4111
- case "delete_file": {
4112
- if (dryRun) {
4113
- return {
4114
- action,
4115
- success: true,
4116
- wouldDo: `Delete file: ${action.path}`
4117
- };
4118
- }
4119
- if (existsSync16(action.path)) {
4120
- unlinkSync(action.path);
4121
- }
4122
- return { action, success: true };
4123
- }
4124
- case "append_to_file": {
4125
- if (dryRun) {
4126
- return {
4127
- action,
4128
- success: true,
4129
- wouldDo: `Append to file: ${action.path}`
4130
- };
4131
- }
4132
- if (existsSync16(action.path)) {
4133
- const content = readFileSync11(action.path, "utf-8");
4134
- if (action.ifNotContains && content.includes(action.ifNotContains)) {
4135
- return { action, success: true };
4136
- }
4137
- writeFileSync7(action.path, content + action.content, "utf-8");
4138
- }
4139
- return { action, success: true };
4140
- }
4141
- case "inject_eslint": {
4142
- return await executeInjectEslint(action, options);
4143
- }
4144
- case "inject_react": {
4145
- return await executeInjectReact(action, options);
4146
- }
4147
- case "inject_next_config": {
4148
- return await executeInjectNextConfig(action, options);
4149
- }
4150
- case "inject_vite_config": {
4151
- return await executeInjectViteConfig(action, options);
4152
- }
4153
- case "install_next_routes": {
4154
- return await executeInstallNextRoutes(action, options);
4155
- }
4156
- default: {
4157
- const _exhaustive = action;
4158
- return {
4159
- action: _exhaustive,
4160
- success: false,
4161
- error: `Unknown action type`
4162
- };
4163
- }
4164
- }
4165
- } catch (error) {
4166
- return {
4167
- action,
4168
- success: false,
4169
- error: error instanceof Error ? error.message : String(error)
4170
- };
4171
- }
4172
- }
4173
- async function executeInjectEslint(action, options) {
4174
- const { dryRun = false } = options;
4175
- if (dryRun) {
4176
- return {
4177
- action,
4178
- success: true,
4179
- wouldDo: `Inject ESLint rules into: ${action.configPath}`
4180
- };
4181
- }
4182
- const result = await installEslintPlugin({
4183
- projectPath: action.packagePath,
4184
- selectedRules: action.rules,
4185
- force: !action.hasExistingRules,
4186
- // Don't force if already has rules
4187
- // Auto-confirm for execute phase (choices were made during planning)
4188
- confirmAddMissingRules: async () => true
4189
- });
4190
- return {
4191
- action,
4192
- success: result.configFile !== null && result.configured,
4193
- error: result.configFile === null ? "No ESLint config found" : result.configured ? void 0 : result.error ?? "Failed to configure uilint in ESLint config"
4194
- };
4195
- }
4196
- async function executeInjectReact(action, options) {
4197
- const { dryRun = false } = options;
4198
- if (dryRun) {
4199
- return {
4200
- action,
4201
- success: true,
4202
- wouldDo: `Inject <uilint-devtools /> into React app: ${action.projectPath}`
4203
- };
4204
- }
4205
- const result = await installReactUILintOverlay({
4206
- projectPath: action.projectPath,
4207
- appRoot: action.appRoot,
4208
- mode: action.mode,
4209
- force: false,
4210
- // Auto-select first choice for execute phase
4211
- confirmFileChoice: async (choices) => choices[0]
4212
- });
4213
- const success = result.modified || result.alreadyConfigured === true;
4214
- return {
4215
- action,
4216
- success,
4217
- error: success ? void 0 : "Failed to configure React overlay"
4218
- };
4219
- }
4220
- async function executeInjectViteConfig(action, options) {
4221
- const { dryRun = false } = options;
4222
- if (dryRun) {
4223
- return {
4224
- action,
4225
- success: true,
4226
- wouldDo: `Inject jsx-loc-plugin into vite.config: ${action.projectPath}`
4227
- };
4228
- }
4229
- const result = await installViteJsxLocPlugin({
4230
- projectPath: action.projectPath,
4231
- force: false
4232
- });
4233
- return {
4234
- action,
4235
- success: result.modified || result.configFile !== null,
4236
- error: result.configFile === null ? "No vite.config found" : void 0
4237
- };
4238
- }
4239
- async function executeInjectNextConfig(action, options) {
4240
- const { dryRun = false } = options;
4241
- if (dryRun) {
4242
- return {
4243
- action,
4244
- success: true,
4245
- wouldDo: `Inject jsx-loc-plugin into next.config: ${action.projectPath}`
4246
- };
4247
- }
4248
- const result = await installJsxLocPlugin({
4249
- projectPath: action.projectPath,
4250
- force: false
4251
- });
4252
- return {
4253
- action,
4254
- success: result.modified || result.configFile !== null,
4255
- error: result.configFile === null ? "No next.config found" : void 0
4256
- };
4257
- }
4258
- async function executeInstallNextRoutes(action, options) {
4259
- const { dryRun = false } = options;
4260
- if (dryRun) {
4261
- return {
4262
- action,
4263
- success: true,
4264
- wouldDo: `Install Next.js API routes: ${action.projectPath}`
4265
- };
4266
- }
4267
- await installNextUILintRoutes({
4268
- projectPath: action.projectPath,
4269
- appRoot: action.appRoot,
4270
- force: false
4271
- });
4272
- return { action, success: true };
4273
- }
4274
- function deepMerge(target, source) {
4275
- const result = { ...target };
4276
- for (const key of Object.keys(source)) {
4277
- const sourceVal = source[key];
4278
- const targetVal = target[key];
4279
- if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) {
4280
- result[key] = deepMerge(
4281
- targetVal,
4282
- sourceVal
4283
- );
4284
- } else {
4285
- result[key] = sourceVal;
4286
- }
4287
- }
4288
- return result;
4289
- }
4290
- function buildSummary(actionsPerformed, dependencyResults, items) {
4291
- const filesCreated = [];
4292
- const filesModified = [];
4293
- const filesDeleted = [];
4294
- const eslintTargets = [];
4295
- let nextApp;
4296
- let viteApp;
4297
- for (const result of actionsPerformed) {
4298
- if (!result.success) continue;
4299
- const { action } = result;
4300
- switch (action.type) {
4301
- case "create_file":
4302
- filesCreated.push(action.path);
4303
- break;
4304
- case "merge_json":
4305
- case "append_to_file":
4306
- filesModified.push(action.path);
4307
- break;
4308
- case "delete_file":
4309
- filesDeleted.push(action.path);
4310
- break;
4311
- case "inject_eslint":
4312
- filesModified.push(action.configPath);
4313
- eslintTargets.push({
4314
- displayName: action.packagePath,
4315
- configFile: action.configPath
4316
- });
4317
- break;
4318
- case "inject_react":
4319
- if (action.mode === "vite") {
4320
- viteApp = { entryRoot: action.appRoot };
4321
- } else {
4322
- nextApp = { appRoot: action.appRoot };
4323
- }
4324
- break;
4325
- case "install_next_routes":
4326
- nextApp = { appRoot: action.appRoot };
4327
- break;
4328
- }
4329
- }
4330
- const dependenciesInstalled = [];
4331
- for (const result of dependencyResults) {
4332
- if (result.success && !result.skipped) {
4333
- dependenciesInstalled.push({
4334
- packagePath: result.install.packagePath,
4335
- packages: result.install.packages
4336
- });
4337
- }
4338
- }
4339
- return {
4340
- installedItems: items,
4341
- filesCreated,
4342
- filesModified,
4343
- filesDeleted,
4344
- dependenciesInstalled,
4345
- eslintTargets,
4346
- nextApp,
4347
- viteApp
4348
- };
4349
- }
4350
- async function execute(plan, options = {}) {
4351
- const { dryRun = false, installDependencies: installDependencies2 = installDependencies } = options;
4352
- const actionsPerformed = [];
4353
- const dependencyResults = [];
4354
- for (const action of plan.actions) {
4355
- const result = await executeAction(action, options);
4356
- actionsPerformed.push(result);
4357
- }
4358
- for (const dep of plan.dependencies) {
4359
- if (dryRun) {
4360
- dependencyResults.push({
4361
- install: dep,
4362
- success: true,
4363
- skipped: true
4364
- });
4365
- continue;
4366
- }
4367
- try {
4368
- await installDependencies2(
4369
- dep.packageManager,
4370
- dep.packagePath,
4371
- dep.packages
4372
- );
4373
- dependencyResults.push({
4374
- install: dep,
4375
- success: true
4376
- });
4377
- } catch (error) {
4378
- dependencyResults.push({
4379
- install: dep,
4380
- success: false,
4381
- error: error instanceof Error ? error.message : String(error)
4382
- });
4383
- }
4384
- }
4385
- const actionsFailed = actionsPerformed.filter((r) => !r.success);
4386
- const depsFailed = dependencyResults.filter((r) => !r.success);
4387
- const success = actionsFailed.length === 0 && depsFailed.length === 0;
4388
- const items = [];
4389
- for (const result of actionsPerformed) {
4390
- if (!result.success) continue;
4391
- const { action } = result;
4392
- if (action.type === "create_file") {
4393
- if (action.path.includes("genstyleguide.md")) items.push("genstyleguide");
4394
- if (action.path.includes("/skills/") && action.path.includes("SKILL.md")) items.push("skill");
4395
- }
4396
- if (action.type === "inject_eslint") items.push("eslint");
4397
- if (action.type === "install_next_routes") items.push("next");
4398
- if (action.type === "inject_react") {
4399
- items.push(action.mode === "vite" ? "vite" : "next");
4400
- }
4401
- if (action.type === "inject_vite_config") items.push("vite");
4402
- }
4403
- const uniqueItems = [...new Set(items)];
4404
- const summary = buildSummary(
4405
- actionsPerformed,
4406
- dependencyResults,
4407
- uniqueItems
4408
- );
4409
- return {
4410
- success,
4411
- actionsPerformed,
4412
- dependencyResults,
4413
- summary
4414
- };
4415
- }
4416
-
4417
- // src/commands/install/prompter.ts
4418
- import { ruleRegistry } from "uilint-eslint";
4419
- var cliPrompter = {
4420
- async selectInstallItems() {
4421
- return multiselect2({
4422
- message: "What would you like to install?",
4423
- options: [
4424
- {
4425
- value: "eslint",
4426
- label: "ESLint plugin",
4427
- hint: "Installs uilint-eslint and configures eslint.config.*"
4428
- },
4429
- {
4430
- value: "next",
4431
- label: "UI overlay",
4432
- hint: "Installs routes + devtools (Alt+Click to inspect)"
4433
- },
4434
- {
4435
- value: "vite",
4436
- label: "UI overlay (Vite)",
4437
- hint: "Installs jsx-loc-plugin + devtools (Alt+Click to inspect)"
4438
- },
4439
- {
4440
- value: "genstyleguide",
4441
- label: "/genstyleguide command",
4442
- hint: "Adds .cursor/commands/genstyleguide.md"
4443
- },
4444
- {
4445
- value: "skill",
4446
- label: "UI Consistency Agent Skill",
4447
- hint: "Cursor agent skill for generating ESLint rules from UI patterns"
4448
- }
4449
- ],
4450
- required: true,
4451
- initialValues: ["eslint", "next", "genstyleguide", "skill"]
4452
- });
4453
- },
4454
- async selectNextApp(apps) {
4455
- const chosen = await select2({
4456
- message: "Which Next.js App Router project should UILint install into?",
4457
- options: apps.map((app) => ({
4458
- value: app.projectPath,
4459
- label: app.projectPath
4460
- })),
4461
- initialValue: apps[0].projectPath
4462
- });
4463
- return apps.find((a) => a.projectPath === chosen) || apps[0];
4464
- },
4465
- async selectViteApp(apps) {
4466
- const chosen = await select2({
4467
- message: "Which Vite + React project should UILint install into?",
4468
- options: apps.map((app) => ({
4469
- value: app.projectPath,
4470
- label: app.projectPath
4471
- })),
4472
- initialValue: apps[0].projectPath
4473
- });
4474
- return apps.find((a) => a.projectPath === chosen) || apps[0];
4475
- },
4476
- async selectEslintPackages(packages) {
4477
- if (packages.length === 1) {
4478
- const confirmed = await confirm2({
4479
- message: `Install ESLint plugin in ${pc.cyan(
4480
- packages[0].displayPath
4481
- )}?`,
4482
- initialValue: true
4483
- });
4484
- return confirmed ? [packages[0].path] : [];
4485
- }
4486
- const initialValues = packages.filter((p2) => p2.isFrontend).map((p2) => p2.path).slice(0, 1);
4487
- return multiselect2({
4488
- message: "Which packages should have ESLint plugin installed?",
4489
- options: packages.map(formatPackageOption),
4490
- required: false,
4491
- initialValues: initialValues.length > 0 ? initialValues : [packages[0].path]
4492
- });
4493
- },
4494
- async selectEslintRules() {
4495
- const selectedRuleIds = await multiselect2({
4496
- message: "Which rules would you like to enable?",
4497
- options: ruleRegistry.map((rule) => ({
4498
- value: rule.id,
4499
- label: rule.name,
4500
- hint: rule.description
4501
- })),
4502
- required: false,
4503
- initialValues: ruleRegistry.filter(
4504
- (r) => r.category === "static" || !r.requiresStyleguide
4505
- ).map((r) => r.id)
4506
- });
4507
- return ruleRegistry.filter(
4508
- (r) => selectedRuleIds.includes(r.id)
4509
- );
4510
- },
4511
- async selectEslintRuleSeverity() {
4512
- return select2({
4513
- message: "How strict should the selected ESLint rules be?",
4514
- options: [
4515
- {
4516
- value: "warn",
4517
- label: "Warn (recommended)",
4518
- hint: "Safer default while you dial in your styleguide + rules"
4519
- },
4520
- {
4521
- value: "error",
4522
- label: "Error (strict)",
4523
- hint: "Make selected rules fail CI"
4524
- },
4525
- {
4526
- value: "defaults",
4527
- label: "Use rule defaults",
4528
- hint: "Some rules are warn, some are error (as defined by uilint-eslint)"
4529
- }
4530
- ],
4531
- initialValue: "warn"
4532
- });
4533
- },
4534
- async confirmCustomizeRuleOptions() {
4535
- return confirm2({
4536
- message: "Customize individual rule options? (spacing scale, thresholds, etc.)",
4537
- initialValue: false
4538
- });
4539
- },
4540
- async configureRuleOptions(rule) {
4541
- if (!rule.optionSchema || rule.optionSchema.fields.length === 0) {
4542
- return void 0;
4543
- }
4544
- const options = {};
4545
- for (const field of rule.optionSchema.fields) {
4546
- const value = await promptForField(field, rule.name);
4547
- if (value !== void 0) {
4548
- options[field.key] = value;
4549
- }
4550
- }
4551
- return Object.keys(options).length > 0 ? options : void 0;
4552
- }
4553
- };
4554
- async function promptForField(field, ruleName) {
4555
- const message = `${pc.cyan(ruleName)} - ${field.label}`;
4556
- switch (field.type) {
4557
- case "text": {
4558
- const result = await text2({
4559
- message,
4560
- placeholder: field.placeholder,
4561
- defaultValue: typeof field.defaultValue === "string" ? field.defaultValue : Array.isArray(field.defaultValue) ? field.defaultValue.join(", ") : String(field.defaultValue)
4562
- });
4563
- if (field.key === "scale" && typeof result === "string") {
4564
- const scale = result.split(",").map((s) => parseFloat(s.trim())).filter((n) => !isNaN(n));
4565
- return scale.length > 0 ? scale : field.defaultValue;
4566
- }
4567
- return result || field.defaultValue;
4568
- }
4569
- case "number": {
4570
- const result = await text2({
4571
- message,
4572
- placeholder: field.placeholder || String(field.defaultValue),
4573
- defaultValue: String(field.defaultValue)
4574
- });
4575
- const num = parseFloat(result);
4576
- return isNaN(num) ? field.defaultValue : num;
4577
- }
4578
- case "boolean": {
4579
- return await confirm2({
4580
- message,
4581
- initialValue: Boolean(field.defaultValue)
4582
- });
4583
- }
4584
- case "select": {
4585
- if (!field.options) {
4586
- return field.defaultValue;
4587
- }
4588
- const stringOptions = field.options.map(
4589
- (opt) => ({
4590
- value: String(opt.value),
4591
- label: opt.label
4592
- })
4593
- );
4594
- const result = await select2({
4595
- message,
4596
- options: stringOptions,
4597
- initialValue: String(field.defaultValue)
4598
- });
4599
- const originalOpt = field.options.find(
4600
- (opt) => String(opt.value) === result
4601
- );
4602
- return originalOpt?.value ?? result;
4603
- }
4604
- case "multiselect": {
4605
- if (!field.options) {
4606
- return field.defaultValue;
4607
- }
4608
- const stringOptions = field.options.map(
4609
- (opt) => ({
4610
- value: String(opt.value),
4611
- label: opt.label
4612
- })
4613
- );
4614
- const result = await multiselect2({
4615
- message,
4616
- options: stringOptions,
4617
- initialValues: Array.isArray(field.defaultValue) ? field.defaultValue.map((v) => String(v)) : [String(field.defaultValue)]
4618
- });
4619
- return result.map((selected) => {
4620
- const originalOpt = field.options.find(
4621
- (opt) => String(opt.value) === selected
4622
- );
4623
- return originalOpt?.value ?? selected;
4624
- });
4625
- }
4626
- default:
4627
- return field.defaultValue;
4628
- }
4629
- }
4630
- async function gatherChoices(state, options, prompter) {
4631
- let items;
4632
- const hasExplicitFlags = options.genstyleguide !== void 0 || options.skill !== void 0 || options.routes !== void 0 || options.react !== void 0;
4633
- if (hasExplicitFlags || options.eslint) {
4634
- items = [];
4635
- if (options.genstyleguide) items.push("genstyleguide");
4636
- if (options.skill) items.push("skill");
4637
- if (options.routes || options.react) items.push("next");
4638
- if (options.eslint) items.push("eslint");
4639
- } else {
4640
- items = await prompter.selectInstallItems();
4641
- }
4642
- let nextChoices;
4643
- if (items.includes("next")) {
4644
- if (state.nextApps.length === 0) {
4645
- throw new Error(
4646
- "Could not find a Next.js App Router app root (expected app/ or src/app/). Run this from your Next.js project root."
4647
- );
4648
- } else if (state.nextApps.length === 1) {
4649
- nextChoices = {
4650
- projectPath: state.nextApps[0].projectPath,
4651
- detection: state.nextApps[0].detection
4652
- };
4653
- } else {
4654
- const selected = await prompter.selectNextApp(state.nextApps);
4655
- nextChoices = {
4656
- projectPath: selected.projectPath,
4657
- detection: selected.detection
4658
- };
4659
- }
4660
- }
4661
- let viteChoices;
4662
- if (items.includes("vite")) {
4663
- if (state.viteApps.length === 0) {
4664
- throw new Error(
4665
- "Could not find a Vite + React project (expected vite.config.* + react deps). Run this from your Vite project root."
4666
- );
4667
- } else if (state.viteApps.length === 1) {
4668
- viteChoices = {
4669
- projectPath: state.viteApps[0].projectPath,
4670
- detection: state.viteApps[0].detection
4671
- };
4672
- } else {
4673
- const selected = await prompter.selectViteApp(state.viteApps);
4674
- viteChoices = {
4675
- projectPath: selected.projectPath,
4676
- detection: selected.detection
4677
- };
4678
- }
4679
- }
4680
- let eslintChoices;
4681
- if (items.includes("eslint")) {
4682
- const packagesWithEslint = state.packages.filter(
4683
- (p2) => p2.eslintConfigPath !== null
4684
- );
4685
- if (packagesWithEslint.length === 0) {
4686
- throw new Error(
4687
- "No packages with eslint.config.{ts,mjs,js,cjs} found. Create an ESLint config first."
4688
- );
4689
- }
4690
- const packagePaths = await prompter.selectEslintPackages(
4691
- packagesWithEslint
4692
- );
4693
- if (packagePaths.length > 0) {
4694
- let selectedRules = await prompter.selectEslintRules();
4695
- const severity = await prompter.selectEslintRuleSeverity();
4696
- if (severity !== "defaults") {
4697
- selectedRules = selectedRules.map((rule) => ({
4698
- ...rule,
4699
- defaultSeverity: severity
4700
- }));
4701
- }
4702
- const hasConfigurableRules = selectedRules.some(
4703
- (r) => r.optionSchema && r.optionSchema.fields.length > 0
4704
- );
4705
- if (hasConfigurableRules) {
4706
- const customizeOptions = await prompter.confirmCustomizeRuleOptions();
4707
- if (customizeOptions) {
4708
- selectedRules = await configureRuleOptions(selectedRules, prompter);
4709
- }
4710
- }
4711
- eslintChoices = { packagePaths, selectedRules };
4712
- }
4713
- }
4714
- return {
4715
- items,
4716
- next: nextChoices,
4717
- vite: viteChoices,
4718
- eslint: eslintChoices
4719
- };
4720
- }
4721
- async function configureRuleOptions(rules, prompter) {
4722
- const configured = [];
4723
- for (const rule of rules) {
4724
- if (rule.optionSchema && rule.optionSchema.fields.length > 0) {
4725
- const options = await prompter.configureRuleOptions(rule);
4726
- if (options) {
4727
- const existingOptions = rule.defaultOptions && rule.defaultOptions.length > 0 ? rule.defaultOptions[0] : {};
4728
- configured.push({
4729
- ...rule,
4730
- defaultOptions: [{ ...existingOptions, ...options }]
4731
- });
4732
- } else {
4733
- configured.push(rule);
4734
- }
4735
- } else {
4736
- configured.push(rule);
4737
- }
4738
- }
4739
- return configured;
4740
- }
4741
-
4742
- // src/commands/install.ts
4743
- function displayResults(result) {
4744
- const { summary } = result;
4745
- const installedItems = [];
4746
- if (summary.installedItems.includes("genstyleguide")) {
4747
- installedItems.push(
4748
- `${pc.cyan("Command")} \u2192 .cursor/commands/genstyleguide.md`
4749
- );
4750
- }
4751
- if (summary.nextApp) {
4752
- installedItems.push(
4753
- `${pc.cyan("Next Routes")} \u2192 ${pc.dim(
4754
- join16(summary.nextApp.appRoot, "api/.uilint")
4755
- )}`
4756
- );
4757
- installedItems.push(
4758
- `${pc.cyan("Next Devtools")} \u2192 ${pc.dim("<uilint-devtools /> injected")}`
4759
- );
4760
- installedItems.push(
4761
- `${pc.cyan("JSX Loc Plugin")} \u2192 ${pc.dim(
4762
- "next.config wrapped with withJsxLoc"
4763
- )}`
4764
- );
4765
- }
4766
- if (summary.viteApp) {
4767
- installedItems.push(
4768
- `${pc.cyan("Vite Devtools")} \u2192 ${pc.dim("<uilint-devtools /> injected")}`
4769
- );
4770
- installedItems.push(
4771
- `${pc.cyan("JSX Loc Plugin")} \u2192 ${pc.dim(
4772
- "vite.config plugins patched with jsxLoc()"
4773
- )}`
4774
- );
4775
- }
4776
- if (summary.eslintTargets.length > 0) {
4777
- installedItems.push(
4778
- `${pc.cyan("ESLint Plugin")} \u2192 installed in ${summary.eslintTargets.length} package(s)`
4779
- );
4780
- for (let i = 0; i < summary.eslintTargets.length; i++) {
4781
- const isLast = i === summary.eslintTargets.length - 1;
4782
- const prefix = isLast ? "\u2514" : "\u251C";
4783
- installedItems.push(
4784
- ` ${pc.dim(prefix)} ${summary.eslintTargets[i].displayName}`
4785
- );
4786
- }
4787
- installedItems.push(`${pc.cyan("Available Rules")}:`);
4788
- for (let i = 0; i < ruleRegistry2.length; i++) {
4789
- const isLast = i === ruleRegistry2.length - 1;
4790
- const prefix = isLast ? "\u2514" : "\u251C";
4791
- const rule = ruleRegistry2[i];
4792
- const suffix = rule.id === "semantic" ? ` ${pc.dim("(LLM-powered)")}` : "";
4793
- installedItems.push(
4794
- ` ${pc.dim(prefix)} ${pc.cyan(`uilint/${rule.id}`)}${suffix}`
4795
- );
4796
- }
4797
- }
4798
- note2(installedItems.join("\n"), "Installed");
4799
- const steps = [];
4800
- const hasStyleguide = summary.filesCreated.some(
4801
- (f) => f.includes("styleguide.md")
4802
- );
4803
- if (!hasStyleguide) {
4804
- steps.push(`Create a styleguide: ${pc.cyan("/genstyleguide")}`);
4805
- }
4806
- if (summary.installedItems.includes("genstyleguide")) {
4807
- steps.push("Restart Cursor to load the new configuration");
4808
- }
4809
- if (summary.nextApp) {
4810
- steps.push(
4811
- "Run your Next.js dev server - use Alt+Click on any element to inspect"
4812
- );
4813
- }
4814
- if (summary.viteApp) {
4815
- steps.push(
4816
- "Run your Vite dev server - use Alt+Click on any element to inspect"
4817
- );
4818
- }
4819
- if (summary.eslintTargets.length > 0) {
4820
- steps.push(`Run ${pc.cyan("npx eslint src/")} to check for issues`);
4821
- steps.push(
4822
- `For real-time overlay integration, run ${pc.cyan(
4823
- "uilint serve"
4824
- )} alongside your dev server`
4825
- );
4826
- }
4827
- if (steps.length > 0) {
4828
- note2(steps.join("\n"), "Next Steps");
4829
- }
4830
- }
4831
- async function install(options = {}, prompter = cliPrompter, executeOptions = {}) {
4832
- const projectPath = process.cwd();
4833
- intro2("Setup Wizard");
4834
- logInfo("Analyzing project...");
4835
- const state = await analyze2(projectPath);
4836
- const choices = await gatherChoices(state, options, prompter);
4837
- if (choices.items.length === 0) {
4838
- logWarning("No items selected for installation");
4839
- outro2("Nothing to install");
4840
- return {
4841
- success: true,
4842
- actionsPerformed: [],
4843
- dependencyResults: [],
4844
- summary: {
4845
- installedItems: [],
4846
- filesCreated: [],
4847
- filesModified: [],
4848
- filesDeleted: [],
4849
- dependenciesInstalled: [],
4850
- eslintTargets: []
4851
- }
4852
- };
4853
- }
4854
- const plan = createPlan(state, choices, { force: options.force });
4855
- logInfo("Installing...");
4856
- const result = await withSpinner("Running installation", async () => {
4857
- return execute(plan, executeOptions);
4858
- });
4859
- const failedActions = result.actionsPerformed.filter((r) => !r.success);
4860
- const failedDeps = result.dependencyResults.filter((r) => !r.success);
4861
- if (failedActions.length > 0) {
4862
- for (const failed of failedActions) {
4863
- logWarning(`Failed: ${failed.action.type} - ${failed.error}`);
4864
- }
4865
- }
4866
- if (failedDeps.length > 0) {
4867
- for (const failed of failedDeps) {
4868
- logWarning(
4869
- `Failed to install dependencies in ${failed.install.packagePath}: ${failed.error}`
4870
- );
4871
- }
4872
- }
4873
- displayResults(result);
4874
- if (result.success) {
4875
- outro2("UILint installed successfully!");
4876
- } else {
4877
- outro2("UILint installation completed with some errors");
4878
- }
4879
- return result;
4880
- }
4881
-
4882
- // src/commands/serve.ts
4883
- import { existsSync as existsSync18, statSync as statSync4, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
4884
- import { createRequire as createRequire3 } from "module";
4885
- import { dirname as dirname11, resolve as resolve5, relative as relative4, join as join18, parse as parse2 } from "path";
4886
- import { WebSocketServer, WebSocket } from "ws";
4887
- import { watch } from "chokidar";
4888
- import {
4889
- findWorkspaceRoot as findWorkspaceRoot6,
4890
- getVisionAnalyzer as getCoreVisionAnalyzer
4891
- } from "uilint-core/node";
4892
-
4893
- // src/utils/vision-run.ts
4894
- import { dirname as dirname10, join as join17, parse } from "path";
4895
- import { existsSync as existsSync17, statSync as statSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync8 } from "fs";
4896
- import {
4897
- ensureOllamaReady as ensureOllamaReady5,
4898
- findStyleGuidePath as findStyleGuidePath4,
4899
- findUILintStyleGuideUpwards as findUILintStyleGuideUpwards3,
4900
- readStyleGuide as readStyleGuide4,
4901
- VisionAnalyzer,
4902
- UILINT_DEFAULT_VISION_MODEL
4903
- } from "uilint-core/node";
4904
- async function resolveVisionStyleGuide(args) {
4905
- const projectPath = args.projectPath;
4906
- const startDir = args.startDir ?? projectPath;
4907
- if (args.styleguide) {
4908
- const styleguideArg = resolvePathSpecifier(args.styleguide, projectPath);
4909
- if (existsSync17(styleguideArg)) {
4910
- const stat = statSync3(styleguideArg);
4911
- if (stat.isFile()) {
4912
- return {
4913
- styleguideLocation: styleguideArg,
4914
- styleGuide: await readStyleGuide4(styleguideArg)
4915
- };
4916
- }
4917
- if (stat.isDirectory()) {
4918
- const found = findStyleGuidePath4(styleguideArg);
4919
- return {
4920
- styleguideLocation: found,
4921
- styleGuide: found ? await readStyleGuide4(found) : null
4922
- };
4923
- }
4924
- }
4925
- return { styleGuide: null, styleguideLocation: null };
4926
- }
4927
- const upwards = findUILintStyleGuideUpwards3(startDir);
4928
- const fallback = upwards ?? findStyleGuidePath4(projectPath);
4929
- return {
4930
- styleguideLocation: fallback,
4931
- styleGuide: fallback ? await readStyleGuide4(fallback) : null
4932
- };
4933
- }
4934
- var ollamaReadyOnce = /* @__PURE__ */ new Map();
4935
- async function ensureOllamaReadyCached(params) {
4936
- const key = `${params.baseUrl}::${params.model}`;
4937
- const existing = ollamaReadyOnce.get(key);
4938
- if (existing) return existing;
4939
- const p2 = ensureOllamaReady5({ model: params.model, baseUrl: params.baseUrl }).then(() => void 0).catch((e) => {
4940
- ollamaReadyOnce.delete(key);
4941
- throw e;
4942
- });
4943
- ollamaReadyOnce.set(key, p2);
4944
- return p2;
4945
- }
4946
- function writeVisionDebugDump(params) {
4947
- const resolvedDirOrFile = resolvePathSpecifier(
4948
- params.dumpPath,
4949
- process.cwd()
4950
- );
4951
- const safeStamp = params.now.toISOString().replace(/[:.]/g, "-");
4952
- const dumpFile = resolvedDirOrFile.endsWith(".json") || resolvedDirOrFile.endsWith(".jsonl") ? resolvedDirOrFile : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;
4953
- mkdirSync4(dirname10(dumpFile), { recursive: true });
4954
- writeFileSync8(
4955
- dumpFile,
4956
- JSON.stringify(
4957
- {
4958
- version: 1,
4959
- timestamp: params.now.toISOString(),
4960
- runtime: params.runtime,
4961
- metadata: params.metadata ?? null,
4962
- inputs: {
4963
- imageBase64: params.includeSensitive ? params.inputs.imageBase64 : "(omitted; set debugDumpIncludeSensitive=true)",
4964
- manifest: params.inputs.manifest,
4965
- styleguideLocation: params.inputs.styleguideLocation,
4966
- styleGuide: params.includeSensitive ? params.inputs.styleGuide : "(omitted; set debugDumpIncludeSensitive=true)"
4967
- }
4968
- },
4969
- null,
4970
- 2
4971
- ),
4972
- "utf-8"
4973
- );
4974
- return dumpFile;
4975
- }
4976
- async function runVisionAnalysis(args) {
4977
- const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;
4978
- const baseUrl = args.baseUrl ?? "http://localhost:11434";
4979
- let styleGuide = null;
4980
- let styleguideLocation = null;
4981
- if (args.styleGuide !== void 0) {
4982
- styleGuide = args.styleGuide;
4983
- styleguideLocation = args.styleguideLocation ?? null;
4984
- } else {
4985
- args.onPhase?.("Resolving styleguide...");
4986
- const resolved = await resolveVisionStyleGuide({
4987
- projectPath: args.projectPath,
4988
- styleguide: args.styleguide,
4989
- startDir: args.styleguideStartDir
4990
- });
4991
- styleGuide = resolved.styleGuide;
4992
- styleguideLocation = resolved.styleguideLocation;
4993
- }
4994
- if (!args.skipEnsureOllama) {
4995
- args.onPhase?.("Preparing Ollama...");
4996
- await ensureOllamaReadyCached({ model: visionModel, baseUrl });
4997
- }
4998
- if (args.debugDump) {
4999
- writeVisionDebugDump({
5000
- dumpPath: args.debugDump,
5001
- now: /* @__PURE__ */ new Date(),
5002
- runtime: { visionModel, baseUrl },
5003
- inputs: {
5004
- imageBase64: args.imageBase64,
5005
- manifest: args.manifest,
5006
- styleguideLocation,
5007
- styleGuide
5008
- },
5009
- includeSensitive: Boolean(args.debugDumpIncludeSensitive),
5010
- metadata: args.debugDumpMetadata
5011
- });
5012
- }
5013
- const analyzer = args.analyzer ?? new VisionAnalyzer({
5014
- baseUrl: args.baseUrl,
5015
- visionModel
5016
- });
5017
- args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);
5018
- const result = await analyzer.analyzeScreenshot(
5019
- args.imageBase64,
5020
- args.manifest,
5021
- {
5022
- styleGuide,
5023
- onProgress: args.onProgress
5024
- }
5025
- );
5026
- args.onPhase?.(
5027
- `Done (${result.issues.length} issues, ${result.analysisTime}ms)`
5028
- );
5029
- return {
5030
- issues: result.issues,
5031
- analysisTime: result.analysisTime,
5032
- // Prompt is available in newer uilint-core versions; keep this resilient across versions.
5033
- prompt: result.prompt,
5034
- rawResponse: result.rawResponse,
5035
- styleguideLocation,
5036
- visionModel,
5037
- baseUrl
5038
- };
5039
- }
5040
- function writeVisionMarkdownReport(args) {
5041
- const p2 = parse(args.imagePath);
5042
- const outPath = args.outPath ?? join17(p2.dir, `${p2.name || p2.base}.vision.md`);
5043
- const lines = [];
5044
- lines.push(`# UILint Vision Report`);
5045
- lines.push(``);
5046
- lines.push(`- Image: \`${p2.base}\``);
5047
- if (args.route) lines.push(`- Route: \`${args.route}\``);
5048
- if (typeof args.timestamp === "number") {
5049
- lines.push(`- Timestamp: \`${new Date(args.timestamp).toISOString()}\``);
5050
- }
5051
- if (args.visionModel) lines.push(`- Model: \`${args.visionModel}\``);
5052
- if (args.baseUrl) lines.push(`- Ollama baseUrl: \`${args.baseUrl}\``);
5053
- if (typeof args.analysisTimeMs === "number")
5054
- lines.push(`- Analysis time: \`${args.analysisTimeMs}ms\``);
5055
- lines.push(`- Generated: \`${(/* @__PURE__ */ new Date()).toISOString()}\``);
5056
- lines.push(``);
5057
- if (args.metadata && Object.keys(args.metadata).length > 0) {
5058
- lines.push(`## Metadata`);
5059
- lines.push(``);
5060
- lines.push("```json");
5061
- lines.push(JSON.stringify(args.metadata, null, 2));
5062
- lines.push("```");
5063
- lines.push(``);
5064
- }
5065
- lines.push(`## Prompt`);
5066
- lines.push(``);
5067
- lines.push("```text");
5068
- lines.push((args.prompt ?? "").trim());
5069
- lines.push("```");
5070
- lines.push(``);
5071
- lines.push(`## Raw Response`);
5072
- lines.push(``);
5073
- lines.push("```text");
5074
- lines.push((args.rawResponse ?? "").trim());
5075
- lines.push("```");
5076
- lines.push(``);
5077
- const content = lines.join("\n");
5078
- mkdirSync4(dirname10(outPath), { recursive: true });
5079
- writeFileSync8(outPath, content, "utf-8");
5080
- return { outPath, content };
5081
- }
5082
-
5083
- // src/commands/serve.ts
5084
- function pickAppRoot(params) {
5085
- const { cwd, workspaceRoot } = params;
5086
- if (detectNextAppRouter(cwd)) return cwd;
5087
- const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
5088
- if (matches.length === 0) return cwd;
5089
- if (matches.length === 1) return matches[0].projectPath;
5090
- const containing = matches.find(
5091
- (m) => cwd === m.projectPath || cwd.startsWith(m.projectPath + "/")
5092
- );
5093
- if (containing) return containing.projectPath;
5094
- return matches[0].projectPath;
5095
- }
5096
- var cache = /* @__PURE__ */ new Map();
5097
- var eslintInstances = /* @__PURE__ */ new Map();
5098
- var visionAnalyzer = null;
5099
- function getVisionAnalyzerInstance() {
5100
- if (!visionAnalyzer) {
5101
- visionAnalyzer = getCoreVisionAnalyzer();
1610
+ var cache = /* @__PURE__ */ new Map();
1611
+ var eslintInstances = /* @__PURE__ */ new Map();
1612
+ var visionAnalyzer = null;
1613
+ function getVisionAnalyzerInstance() {
1614
+ if (!visionAnalyzer) {
1615
+ visionAnalyzer = getCoreVisionAnalyzer();
5102
1616
  }
5103
1617
  return visionAnalyzer;
5104
1618
  }
@@ -5111,7 +1625,7 @@ var resolvedPathCache = /* @__PURE__ */ new Map();
5111
1625
  var subscriptions = /* @__PURE__ */ new Map();
5112
1626
  var fileWatcher = null;
5113
1627
  var connectedClients = 0;
5114
- var localRequire = createRequire3(import.meta.url);
1628
+ var localRequire = createRequire(import.meta.url);
5115
1629
  function buildLineStarts(code) {
5116
1630
  const starts = [0];
5117
1631
  for (let i = 0; i < code.length; i++) {
@@ -5171,7 +1685,7 @@ function mapMessageToDataLoc(params) {
5171
1685
  }
5172
1686
  return void 0;
5173
1687
  }
5174
- var ESLINT_CONFIG_FILES2 = [
1688
+ var ESLINT_CONFIG_FILES = [
5175
1689
  // Flat config (ESLint v9+)
5176
1690
  "eslint.config.js",
5177
1691
  "eslint.config.mjs",
@@ -5188,24 +1702,24 @@ var ESLINT_CONFIG_FILES2 = [
5188
1702
  function findESLintCwd(startDir) {
5189
1703
  let dir = startDir;
5190
1704
  for (let i = 0; i < 30; i++) {
5191
- for (const cfg of ESLINT_CONFIG_FILES2) {
5192
- if (existsSync18(join18(dir, cfg))) return dir;
1705
+ for (const cfg of ESLINT_CONFIG_FILES) {
1706
+ if (existsSync5(join3(dir, cfg))) return dir;
5193
1707
  }
5194
- if (existsSync18(join18(dir, "package.json"))) return dir;
5195
- const parent = dirname11(dir);
1708
+ if (existsSync5(join3(dir, "package.json"))) return dir;
1709
+ const parent = dirname5(dir);
5196
1710
  if (parent === dir) break;
5197
1711
  dir = parent;
5198
1712
  }
5199
1713
  return startDir;
5200
1714
  }
5201
- function normalizePathSlashes(p2) {
5202
- return p2.replace(/\\/g, "/");
1715
+ function normalizePathSlashes(p) {
1716
+ return p.replace(/\\/g, "/");
5203
1717
  }
5204
1718
  function normalizeDataLocFilePath(absoluteFilePath, projectCwd) {
5205
1719
  const abs = normalizePathSlashes(resolve5(absoluteFilePath));
5206
1720
  const cwd = normalizePathSlashes(resolve5(projectCwd));
5207
1721
  if (abs === cwd || abs.startsWith(cwd + "/")) {
5208
- return normalizePathSlashes(relative4(cwd, abs));
1722
+ return normalizePathSlashes(relative(cwd, abs));
5209
1723
  }
5210
1724
  return abs;
5211
1725
  }
@@ -5217,27 +1731,27 @@ function resolveRequestedFilePath(filePath) {
5217
1731
  if (cached) return cached;
5218
1732
  const cwd = process.cwd();
5219
1733
  const fromCwd = resolve5(cwd, filePath);
5220
- if (existsSync18(fromCwd)) {
1734
+ if (existsSync5(fromCwd)) {
5221
1735
  resolvedPathCache.set(filePath, fromCwd);
5222
1736
  return fromCwd;
5223
1737
  }
5224
- const wsRoot = findWorkspaceRoot6(cwd);
1738
+ const wsRoot = findWorkspaceRoot4(cwd);
5225
1739
  const fromWs = resolve5(wsRoot, filePath);
5226
- if (existsSync18(fromWs)) {
1740
+ if (existsSync5(fromWs)) {
5227
1741
  resolvedPathCache.set(filePath, fromWs);
5228
1742
  return fromWs;
5229
1743
  }
5230
1744
  for (const top of ["apps", "packages"]) {
5231
- const base = join18(wsRoot, top);
5232
- if (!existsSync18(base)) continue;
1745
+ const base = join3(wsRoot, top);
1746
+ if (!existsSync5(base)) continue;
5233
1747
  try {
5234
- const entries = readdirSync5(base, { withFileTypes: true });
1748
+ const entries = readdirSync(base, { withFileTypes: true });
5235
1749
  for (const ent of entries) {
5236
1750
  if (!ent.isDirectory()) continue;
5237
- const p2 = resolve5(base, ent.name, filePath);
5238
- if (existsSync18(p2)) {
5239
- resolvedPathCache.set(filePath, p2);
5240
- return p2;
1751
+ const p = resolve5(base, ent.name, filePath);
1752
+ if (existsSync5(p)) {
1753
+ resolvedPathCache.set(filePath, p);
1754
+ return p;
5241
1755
  }
5242
1756
  }
5243
1757
  } catch {
@@ -5250,7 +1764,7 @@ async function getESLintForProject(projectCwd) {
5250
1764
  const cached = eslintInstances.get(projectCwd);
5251
1765
  if (cached) return cached;
5252
1766
  try {
5253
- const req = createRequire3(join18(projectCwd, "package.json"));
1767
+ const req = createRequire(join3(projectCwd, "package.json"));
5254
1768
  const mod = req("eslint");
5255
1769
  const ESLintCtor = mod?.ESLint ?? mod?.default?.ESLint ?? mod?.default ?? mod;
5256
1770
  if (!ESLintCtor) return null;
@@ -5263,13 +1777,13 @@ async function getESLintForProject(projectCwd) {
5263
1777
  }
5264
1778
  async function lintFile(filePath, onProgress) {
5265
1779
  const absolutePath = resolveRequestedFilePath(filePath);
5266
- if (!existsSync18(absolutePath)) {
1780
+ if (!existsSync5(absolutePath)) {
5267
1781
  onProgress(`File not found: ${pc.dim(absolutePath)}`);
5268
1782
  return [];
5269
1783
  }
5270
1784
  const mtimeMs = (() => {
5271
1785
  try {
5272
- return statSync4(absolutePath).mtimeMs;
1786
+ return statSync3(absolutePath).mtimeMs;
5273
1787
  } catch {
5274
1788
  return 0;
5275
1789
  }
@@ -5279,7 +1793,7 @@ async function lintFile(filePath, onProgress) {
5279
1793
  onProgress("Cache hit (unchanged)");
5280
1794
  return cached.issues;
5281
1795
  }
5282
- const fileDir = dirname11(absolutePath);
1796
+ const fileDir = dirname5(absolutePath);
5283
1797
  const projectCwd = findESLintCwd(fileDir);
5284
1798
  onProgress(`Resolving ESLint project... ${pc.dim(projectCwd)}`);
5285
1799
  const eslint = await getESLintForProject(projectCwd);
@@ -5302,7 +1816,7 @@ async function lintFile(filePath, onProgress) {
5302
1816
  let codeLength = 0;
5303
1817
  try {
5304
1818
  onProgress("Building JSX map...");
5305
- const code = readFileSync12(absolutePath, "utf-8");
1819
+ const code = readFileSync(absolutePath, "utf-8");
5306
1820
  codeLength = code.length;
5307
1821
  lineStarts = buildLineStarts(code);
5308
1822
  spans = buildJsxElementSpans(code, dataLocFile);
@@ -5384,9 +1898,9 @@ async function handleMessage(ws, data) {
5384
1898
  });
5385
1899
  const startedAt = Date.now();
5386
1900
  const resolved = resolveRequestedFilePath(filePath);
5387
- if (!existsSync18(resolved)) {
1901
+ if (!existsSync5(resolved)) {
5388
1902
  const cwd = process.cwd();
5389
- const wsRoot = findWorkspaceRoot6(cwd);
1903
+ const wsRoot = findWorkspaceRoot4(cwd);
5390
1904
  logWarning(
5391
1905
  [
5392
1906
  `${pc.dim("[ws]")} File not found for request`,
@@ -5546,14 +2060,14 @@ async function handleMessage(ws, data) {
5546
2060
  )}`
5547
2061
  );
5548
2062
  } else {
5549
- const screenshotsDir = join18(
2063
+ const screenshotsDir = join3(
5550
2064
  serverAppRootForVision,
5551
2065
  ".uilint",
5552
2066
  "screenshots"
5553
2067
  );
5554
- const imagePath = join18(screenshotsDir, screenshotFile);
2068
+ const imagePath = join3(screenshotsDir, screenshotFile);
5555
2069
  try {
5556
- if (!existsSync18(imagePath)) {
2070
+ if (!existsSync5(imagePath)) {
5557
2071
  logWarning(
5558
2072
  `Skipping vision report write: screenshot file not found ${pc.dim(
5559
2073
  imagePath
@@ -5661,7 +2175,7 @@ function handleFileChange(filePath) {
5661
2175
  async function serve(options) {
5662
2176
  const port = options.port || 9234;
5663
2177
  const cwd = process.cwd();
5664
- const wsRoot = findWorkspaceRoot6(cwd);
2178
+ const wsRoot = findWorkspaceRoot4(cwd);
5665
2179
  const appRoot = pickAppRoot({ cwd, workspaceRoot: wsRoot });
5666
2180
  serverAppRootForVision = appRoot;
5667
2181
  logInfo(`Workspace root: ${pc.dim(wsRoot)}`);
@@ -5714,11 +2228,11 @@ async function serve(options) {
5714
2228
  }
5715
2229
 
5716
2230
  // src/commands/vision.ts
5717
- import { dirname as dirname12, resolve as resolve6, join as join19 } from "path";
2231
+ import { dirname as dirname6, resolve as resolve6, join as join4 } from "path";
5718
2232
  import {
5719
- existsSync as existsSync19,
5720
- readFileSync as readFileSync13,
5721
- readdirSync as readdirSync6
2233
+ existsSync as existsSync6,
2234
+ readFileSync as readFileSync2,
2235
+ readdirSync as readdirSync2
5722
2236
  } from "fs";
5723
2237
  import {
5724
2238
  ensureOllamaReady as ensureOllamaReady6,
@@ -5730,9 +2244,9 @@ function envTruthy3(name) {
5730
2244
  if (!v) return false;
5731
2245
  return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
5732
2246
  }
5733
- function preview3(text3, maxLen) {
5734
- if (text3.length <= maxLen) return text3;
5735
- return text3.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text3.slice(-maxLen);
2247
+ function preview3(text, maxLen) {
2248
+ if (text.length <= maxLen) return text;
2249
+ return text.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text.slice(-maxLen);
5736
2250
  }
5737
2251
  function debugEnabled3(options) {
5738
2252
  return Boolean(options.debug) || envTruthy3("UILINT_DEBUG");
@@ -5763,33 +2277,33 @@ function debugLog3(enabled, message, obj) {
5763
2277
  function findScreenshotsDirUpwards(startDir) {
5764
2278
  let dir = startDir;
5765
2279
  for (let i = 0; i < 20; i++) {
5766
- const candidate = join19(dir, ".uilint", "screenshots");
5767
- if (existsSync19(candidate)) return candidate;
5768
- const parent = dirname12(dir);
2280
+ const candidate = join4(dir, ".uilint", "screenshots");
2281
+ if (existsSync6(candidate)) return candidate;
2282
+ const parent = dirname6(dir);
5769
2283
  if (parent === dir) break;
5770
2284
  dir = parent;
5771
2285
  }
5772
2286
  return null;
5773
2287
  }
5774
2288
  function listScreenshotSidecars(dirPath) {
5775
- if (!existsSync19(dirPath)) return [];
5776
- const entries = readdirSync6(dirPath).filter((f) => f.endsWith(".json")).map((f) => join19(dirPath, f));
2289
+ if (!existsSync6(dirPath)) return [];
2290
+ const entries = readdirSync2(dirPath).filter((f) => f.endsWith(".json")).map((f) => join4(dirPath, f));
5777
2291
  const out = [];
5778
- for (const p2 of entries) {
2292
+ for (const p of entries) {
5779
2293
  try {
5780
- const json = loadJsonFile(p2);
2294
+ const json = loadJsonFile(p);
5781
2295
  const issues = Array.isArray(json?.issues) ? json.issues : json?.analysisResult?.issues;
5782
2296
  out.push({
5783
- path: p2,
5784
- filename: json?.filename || json?.screenshotFile || p2.split("/").pop() || p2,
2297
+ path: p,
2298
+ filename: json?.filename || json?.screenshotFile || p.split("/").pop() || p,
5785
2299
  timestamp: typeof json?.timestamp === "number" ? json.timestamp : void 0,
5786
2300
  route: typeof json?.route === "string" ? json.route : void 0,
5787
2301
  issueCount: Array.isArray(issues) ? issues.length : void 0
5788
2302
  });
5789
2303
  } catch {
5790
2304
  out.push({
5791
- path: p2,
5792
- filename: p2.split("/").pop() || p2
2305
+ path: p,
2306
+ filename: p.split("/").pop() || p
5793
2307
  });
5794
2308
  }
5795
2309
  }
@@ -5802,11 +2316,11 @@ function listScreenshotSidecars(dirPath) {
5802
2316
  return out;
5803
2317
  }
5804
2318
  function readImageAsBase64(imagePath) {
5805
- const bytes = readFileSync13(imagePath);
2319
+ const bytes = readFileSync2(imagePath);
5806
2320
  return { base64: bytes.toString("base64"), sizeBytes: bytes.byteLength };
5807
2321
  }
5808
2322
  function loadJsonFile(filePath) {
5809
- const raw = readFileSync13(filePath, "utf-8");
2323
+ const raw = readFileSync2(filePath, "utf-8");
5810
2324
  return JSON.parse(raw);
5811
2325
  }
5812
2326
  function formatIssuesText(issues) {
@@ -5823,7 +2337,7 @@ async function vision(options) {
5823
2337
  const dbg = debugEnabled3(options);
5824
2338
  const dbgFull = debugFullEnabled3(options);
5825
2339
  const dbgDump = debugDumpPath3(options);
5826
- if (!isJsonOutput) intro2("Vision (Screenshot) Analysis");
2340
+ if (!isJsonOutput) intro("Vision (Screenshot) Analysis");
5827
2341
  try {
5828
2342
  const projectPath = process.cwd();
5829
2343
  if (options.list) {
@@ -5880,13 +2394,13 @@ async function vision(options) {
5880
2394
  await flushLangfuse();
5881
2395
  process.exit(1);
5882
2396
  }
5883
- if (imagePath && !existsSync19(imagePath)) {
2397
+ if (imagePath && !existsSync6(imagePath)) {
5884
2398
  throw new Error(`Image not found: ${imagePath}`);
5885
2399
  }
5886
- if (sidecarPath && !existsSync19(sidecarPath)) {
2400
+ if (sidecarPath && !existsSync6(sidecarPath)) {
5887
2401
  throw new Error(`Sidecar not found: ${sidecarPath}`);
5888
2402
  }
5889
- if (manifestFilePath && !existsSync19(manifestFilePath)) {
2403
+ if (manifestFilePath && !existsSync6(manifestFilePath)) {
5890
2404
  throw new Error(`Manifest file not found: ${manifestFilePath}`);
5891
2405
  }
5892
2406
  const sidecar = sidecarPath ? loadJsonFile(sidecarPath) : null;
@@ -5911,7 +2425,7 @@ async function vision(options) {
5911
2425
  const resolved = await resolveVisionStyleGuide({
5912
2426
  projectPath,
5913
2427
  styleguide: options.styleguide,
5914
- startDir: startPath ? dirname12(startPath) : projectPath
2428
+ startDir: startPath ? dirname6(startPath) : projectPath
5915
2429
  });
5916
2430
  styleGuide = resolved.styleGuide;
5917
2431
  styleguideLocation = resolved.styleguideLocation;
@@ -5921,12 +2435,12 @@ async function vision(options) {
5921
2435
  logSuccess(`Using styleguide: ${pc.dim(styleguideLocation)}`);
5922
2436
  } else if (!styleGuide && !isJsonOutput) {
5923
2437
  logWarning("No styleguide found");
5924
- note2(
2438
+ note(
5925
2439
  [
5926
2440
  `Searched in: ${options.styleguide || projectPath}`,
5927
2441
  "",
5928
2442
  "Looked for:",
5929
- ...STYLEGUIDE_PATHS2.map((p2) => ` \u2022 ${p2}`),
2443
+ ...STYLEGUIDE_PATHS2.map((p) => ` \u2022 ${p}`),
5930
2444
  "",
5931
2445
  `Create ${pc.cyan(
5932
2446
  ".uilint/styleguide.md"
@@ -5960,7 +2474,7 @@ async function vision(options) {
5960
2474
  const resolvedImagePath = imagePath || (() => {
5961
2475
  const screenshotFile = typeof sidecar?.screenshotFile === "string" ? sidecar.screenshotFile : typeof sidecar?.filename === "string" ? sidecar.filename : void 0;
5962
2476
  if (!screenshotFile) return null;
5963
- const baseDir = sidecarPath ? dirname12(sidecarPath) : projectPath;
2477
+ const baseDir = sidecarPath ? dirname6(sidecarPath) : projectPath;
5964
2478
  const abs = resolve6(baseDir, screenshotFile);
5965
2479
  return abs;
5966
2480
  })();
@@ -5969,7 +2483,7 @@ async function vision(options) {
5969
2483
  "No image path could be resolved. Provide --image or a sidecar with `screenshotFile`/`filename`."
5970
2484
  );
5971
2485
  }
5972
- if (!existsSync19(resolvedImagePath)) {
2486
+ if (!existsSync6(resolvedImagePath)) {
5973
2487
  throw new Error(`Image not found: ${resolvedImagePath}`);
5974
2488
  }
5975
2489
  const { base64, sizeBytes } = readImageAsBase64(resolvedImagePath);
@@ -6139,7 +2653,7 @@ async function vision(options) {
6139
2653
  (firstAnswerNs ?? lastThinkingNs ?? analysisEndNs) - firstThinkingNs
6140
2654
  ) : null;
6141
2655
  const outputMs = firstAnswerNs && (lastAnswerNs || analysisEndNs) ? nsToMs((lastAnswerNs ?? analysisEndNs) - firstAnswerNs) : null;
6142
- note2(
2656
+ note(
6143
2657
  [
6144
2658
  `Prepare Ollama: ${formatMs(prepMs)}`,
6145
2659
  `Time to first token: ${maybeMs(ttftMs)}`,
@@ -6197,9 +2711,9 @@ async function vision(options) {
6197
2711
  }
6198
2712
 
6199
2713
  // src/index.ts
6200
- import { readFileSync as readFileSync14 } from "fs";
6201
- import { dirname as dirname13, join as join20 } from "path";
6202
- import { fileURLToPath as fileURLToPath4 } from "url";
2714
+ import { readFileSync as readFileSync3 } from "fs";
2715
+ import { dirname as dirname7, join as join5 } from "path";
2716
+ import { fileURLToPath } from "url";
6203
2717
  function assertNodeVersion(minMajor) {
6204
2718
  const ver = process.versions.node || "";
6205
2719
  const majorStr = ver.split(".")[0] || "";
@@ -6213,17 +2727,17 @@ function assertNodeVersion(minMajor) {
6213
2727
  }
6214
2728
  assertNodeVersion(20);
6215
2729
  var program = new Command();
6216
- function getCLIVersion2() {
2730
+ function getCLIVersion() {
6217
2731
  try {
6218
- const __dirname3 = dirname13(fileURLToPath4(import.meta.url));
6219
- const pkgPath = join20(__dirname3, "..", "package.json");
6220
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
2732
+ const __dirname = dirname7(fileURLToPath(import.meta.url));
2733
+ const pkgPath = join5(__dirname, "..", "package.json");
2734
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
6221
2735
  return pkg.version || "0.0.0";
6222
2736
  } catch {
6223
2737
  return "0.0.0";
6224
2738
  }
6225
2739
  }
6226
- program.name("uilint").description("AI-powered UI consistency checker").version(getCLIVersion2());
2740
+ program.name("uilint").description("AI-powered UI consistency checker").version(getCLIVersion());
6227
2741
  program.command("analyze").description(
6228
2742
  "Analyze a source file/snippet for style issues (data-loc aware)"
6229
2743
  ).option("-f, --input-file <path>", "Path to a source file to analyze").option("--source-code <code>", "Source code to analyze (string)").option("--file-path <path>", "File path label shown in the prompt").option("--style-guide <text>", "Inline styleguide content to use").option("--styleguide-path <path>", "Path to a style guide file").option("--component-name <name>", "Component name for focused analysis").option(
@@ -6290,20 +2804,9 @@ program.command("update").description("Update existing style guide with new styl
6290
2804
  llm: options.llm
6291
2805
  });
6292
2806
  });
6293
- program.command("install").description("Install UILint integration").option("--force", "Overwrite existing configuration files").option("--genstyleguide", "Install /genstyleguide Cursor command").option("--eslint", "Install uilint-eslint plugin and configure ESLint").option(
6294
- "--routes",
6295
- "Back-compat: install Next.js overlay (routes + deps + inject)"
6296
- ).option(
6297
- "--react",
6298
- "Back-compat: install Next.js overlay (routes + deps + inject)"
6299
- ).action(async (options) => {
6300
- await install({
6301
- force: options.force,
6302
- genstyleguide: options.genstyleguide,
6303
- eslint: options.eslint,
6304
- routes: options.routes,
6305
- react: options.react
6306
- });
2807
+ program.command("install").description("Install UILint integration").option("--force", "Overwrite existing configuration files").action(async (options) => {
2808
+ const { installUI } = await import("./install-ui-KI7YHOVZ.js");
2809
+ await installUI({ force: options.force });
6307
2810
  });
6308
2811
  program.command("serve").description("Start WebSocket server for real-time UI linting").option("-p, --port <number>", "Port to listen on", "9234").action(async (options) => {
6309
2812
  await serve({