uilint 0.2.7 → 0.2.9

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,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ detectNextAppRouter,
4
+ findNextAppRouterProjects
5
+ } from "./chunk-RHTG6DUD.js";
2
6
 
3
7
  // src/index.ts
4
8
  import { Command } from "commander";
@@ -237,8 +241,8 @@ async function initializeLangfuseIfEnabled() {
237
241
  },
238
242
  { asType: "generation" }
239
243
  );
240
- await new Promise((resolve8) => {
241
- resolveTrace = resolve8;
244
+ await new Promise((resolve7) => {
245
+ resolveTrace = resolve7;
242
246
  });
243
247
  if (endData && generationRef) {
244
248
  const usageDetails = endData.usage ? Object.fromEntries(
@@ -357,8 +361,8 @@ import { dirname, join as join2 } from "path";
357
361
  import { fileURLToPath } from "url";
358
362
  function getCLIVersion() {
359
363
  try {
360
- const __dirname3 = dirname(fileURLToPath(import.meta.url));
361
- const pkgPath = join2(__dirname3, "..", "..", "package.json");
364
+ const __dirname = dirname(fileURLToPath(import.meta.url));
365
+ const pkgPath = join2(__dirname, "..", "..", "package.json");
362
366
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
363
367
  return pkg.version || "0.0.0";
364
368
  } catch {
@@ -374,17 +378,6 @@ function intro2(title) {
374
378
  function outro2(message) {
375
379
  p.outro(pc.green(message));
376
380
  }
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
381
  async function withSpinner(message, fn) {
389
382
  const s = p.spinner();
390
383
  s.start(message);
@@ -415,34 +408,6 @@ function logWarning(message) {
415
408
  function logError(message) {
416
409
  p.log.error(message);
417
410
  }
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
411
 
447
412
  // src/utils/output.ts
448
413
  import chalk from "chalk";
@@ -460,9 +425,9 @@ function envTruthy(name) {
460
425
  if (!v) return false;
461
426
  return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
462
427
  }
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);
428
+ function preview(text2, maxLen) {
429
+ if (text2.length <= maxLen) return text2;
430
+ return text2.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text2.slice(-maxLen);
466
431
  }
467
432
  function debugEnabled(options) {
468
433
  return Boolean(options.debug) || envTruthy("UILINT_DEBUG");
@@ -913,9 +878,9 @@ function envTruthy2(name) {
913
878
  if (!v) return false;
914
879
  return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
915
880
  }
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);
881
+ function preview2(text2, maxLen) {
882
+ if (text2.length <= maxLen) return text2;
883
+ return text2.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text2.slice(-maxLen);
919
884
  }
920
885
  function debugEnabled2(options) {
921
886
  return Boolean(options.debug) || envTruthy2("UILINT_DEBUG");
@@ -1249,14 +1214,14 @@ import {
1249
1214
  } from "uilint-core";
1250
1215
  import { ensureOllamaReady as ensureOllamaReady3 } from "uilint-core/node";
1251
1216
  async function readStdin2() {
1252
- return new Promise((resolve8) => {
1217
+ return new Promise((resolve7) => {
1253
1218
  let data = "";
1254
1219
  const rl = createInterface({ input: process.stdin });
1255
1220
  rl.on("line", (line) => {
1256
1221
  data += line;
1257
1222
  });
1258
1223
  rl.on("close", () => {
1259
- resolve8(data);
1224
+ resolve7(data);
1260
1225
  });
1261
1226
  });
1262
1227
  }
@@ -1474,4121 +1439,239 @@ async function update(options) {
1474
1439
  await flushLangfuse();
1475
1440
  }
1476
1441
 
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";
1442
+ // src/commands/serve.ts
1443
+ import { existsSync as existsSync5, statSync as statSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
1444
+ import { createRequire } from "module";
1445
+ import { dirname as dirname6, resolve as resolve5, relative, join as join4, parse as parse2 } from "path";
1446
+ import { WebSocketServer, WebSocket } from "ws";
1447
+ import { watch } from "chokidar";
1448
+ import {
1449
+ findWorkspaceRoot as findWorkspaceRoot4,
1450
+ getVisionAnalyzer as getCoreVisionAnalyzer
1451
+ } from "uilint-core/node";
1485
1452
 
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;
1453
+ // src/utils/vision-run.ts
1454
+ import { dirname as dirname5, join as join3, parse } from "path";
1455
+ import { existsSync as existsSync4, statSync as statSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
1456
+ import {
1457
+ ensureOllamaReady as ensureOllamaReady5,
1458
+ findStyleGuidePath as findStyleGuidePath4,
1459
+ findUILintStyleGuideUpwards as findUILintStyleGuideUpwards3,
1460
+ readStyleGuide as readStyleGuide4,
1461
+ VisionAnalyzer,
1462
+ UILINT_DEFAULT_VISION_MODEL
1463
+ } from "uilint-core/node";
1464
+ async function resolveVisionStyleGuide(args) {
1465
+ const projectPath = args.projectPath;
1466
+ const startDir = args.startDir ?? projectPath;
1467
+ if (args.styleguide) {
1468
+ const styleguideArg = resolvePathSpecifier(args.styleguide, projectPath);
1469
+ if (existsSync4(styleguideArg)) {
1470
+ const stat = statSync2(styleguideArg);
1471
+ if (stat.isFile()) {
1472
+ return {
1473
+ styleguideLocation: styleguideArg,
1474
+ styleGuide: await readStyleGuide4(styleguideArg)
1475
+ };
1476
+ }
1477
+ if (stat.isDirectory()) {
1478
+ const found = findStyleGuidePath4(styleguideArg);
1479
+ return {
1480
+ styleguideLocation: found,
1481
+ styleGuide: found ? await readStyleGuide4(found) : null
1482
+ };
1483
+ }
1500
1484
  }
1485
+ return { styleGuide: null, styleguideLocation: null };
1501
1486
  }
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
- }
1487
+ const upwards = findUILintStyleGuideUpwards3(startDir);
1488
+ const fallback = upwards ?? findStyleGuidePath4(projectPath);
1515
1489
  return {
1516
- appRoot: chosenRoot,
1517
- appRootAbs: join3(projectPath, chosenRoot),
1518
- candidates
1490
+ styleguideLocation: fallback,
1491
+ styleGuide: fallback ? await readStyleGuide4(fallback) : null
1519
1492
  };
1520
1493
  }
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;
1494
+ var ollamaReadyOnce = /* @__PURE__ */ new Map();
1495
+ async function ensureOllamaReadyCached(params) {
1496
+ const key = `${params.baseUrl}::${params.model}`;
1497
+ const existing = ollamaReadyOnce.get(key);
1498
+ if (existing) return existing;
1499
+ const p2 = ensureOllamaReady5({ model: params.model, baseUrl: params.baseUrl }).then(() => void 0).catch((e) => {
1500
+ ollamaReadyOnce.delete(key);
1501
+ throw e;
1502
+ });
1503
+ ollamaReadyOnce.set(key, p2);
1504
+ return p2;
1566
1505
  }
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;
1506
+ function writeVisionDebugDump(params) {
1507
+ const resolvedDirOrFile = resolvePathSpecifier(
1508
+ params.dumpPath,
1509
+ process.cwd()
1510
+ );
1511
+ const safeStamp = params.now.toISOString().replace(/[:.]/g, "-");
1512
+ const dumpFile = resolvedDirOrFile.endsWith(".json") || resolvedDirOrFile.endsWith(".jsonl") ? resolvedDirOrFile : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;
1513
+ mkdirSync3(dirname5(dumpFile), { recursive: true });
1514
+ writeFileSync3(
1515
+ dumpFile,
1516
+ JSON.stringify(
1517
+ {
1518
+ version: 1,
1519
+ timestamp: params.now.toISOString(),
1520
+ runtime: params.runtime,
1521
+ metadata: params.metadata ?? null,
1522
+ inputs: {
1523
+ imageBase64: params.includeSensitive ? params.inputs.imageBase64 : "(omitted; set debugDumpIncludeSensitive=true)",
1524
+ manifest: params.inputs.manifest,
1525
+ styleguideLocation: params.inputs.styleguideLocation,
1526
+ styleGuide: params.includeSensitive ? params.inputs.styleGuide : "(omitted; set debugDumpIncludeSensitive=true)"
1527
+ }
1528
+ },
1529
+ null,
1530
+ 2
1531
+ ),
1532
+ "utf-8"
1533
+ );
1534
+ return dumpFile;
1578
1535
  }
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;
1536
+ async function runVisionAnalysis(args) {
1537
+ const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;
1538
+ const baseUrl = args.baseUrl ?? "http://localhost:11434";
1539
+ let styleGuide = null;
1540
+ let styleguideLocation = null;
1541
+ if (args.styleGuide !== void 0) {
1542
+ styleGuide = args.styleGuide;
1543
+ styleguideLocation = args.styleguideLocation ?? null;
1544
+ } else {
1545
+ args.onPhase?.("Resolving styleguide...");
1546
+ const resolved = await resolveVisionStyleGuide({
1547
+ projectPath: args.projectPath,
1548
+ styleguide: args.styleguide,
1549
+ startDir: args.styleguideStartDir
1550
+ });
1551
+ styleGuide = resolved.styleGuide;
1552
+ styleguideLocation = resolved.styleguideLocation;
1588
1553
  }
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);
1554
+ if (!args.skipEnsureOllama) {
1555
+ args.onPhase?.("Preparing Ollama...");
1556
+ await ensureOllamaReadyCached({ model: visionModel, baseUrl });
1607
1557
  }
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
- }
1558
+ if (args.debugDump) {
1559
+ writeVisionDebugDump({
1560
+ dumpPath: args.debugDump,
1561
+ now: /* @__PURE__ */ new Date(),
1562
+ runtime: { visionModel, baseUrl },
1563
+ inputs: {
1564
+ imageBase64: args.imageBase64,
1565
+ manifest: args.manifest,
1566
+ styleguideLocation,
1567
+ styleGuide
1568
+ },
1569
+ includeSensitive: Boolean(args.debugDumpIncludeSensitive),
1570
+ metadata: args.debugDumpMetadata
1571
+ });
1616
1572
  }
1573
+ const analyzer = args.analyzer ?? new VisionAnalyzer({
1574
+ baseUrl: args.baseUrl,
1575
+ visionModel
1576
+ });
1577
+ args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);
1578
+ const result = await analyzer.analyzeScreenshot(
1579
+ args.imageBase64,
1580
+ args.manifest,
1581
+ {
1582
+ styleGuide,
1583
+ onProgress: args.onProgress
1584
+ }
1585
+ );
1586
+ args.onPhase?.(
1587
+ `Done (${result.issues.length} issues, ${result.analysisTime}ms)`
1588
+ );
1617
1589
  return {
1618
- configFile,
1619
- configFileAbs: join4(projectPath, configFile),
1620
- entryRoot,
1621
- candidates
1590
+ issues: result.issues,
1591
+ analysisTime: result.analysisTime,
1592
+ // Prompt is available in newer uilint-core versions; keep this resilient across versions.
1593
+ prompt: result.prompt,
1594
+ rawResponse: result.rawResponse,
1595
+ styleguideLocation,
1596
+ visionModel,
1597
+ baseUrl
1622
1598
  };
1623
1599
  }
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
- }
1600
+ function writeVisionMarkdownReport(args) {
1601
+ const p2 = parse(args.imagePath);
1602
+ const outPath = args.outPath ?? join3(p2.dir, `${p2.name || p2.base}.vision.md`);
1603
+ const lines = [];
1604
+ lines.push(`# UILint Vision Report`);
1605
+ lines.push(``);
1606
+ lines.push(`- Image: \`${p2.base}\``);
1607
+ if (args.route) lines.push(`- Route: \`${args.route}\``);
1608
+ if (typeof args.timestamp === "number") {
1609
+ lines.push(`- Timestamp: \`${new Date(args.timestamp).toISOString()}\``);
1610
+ }
1611
+ if (args.visionModel) lines.push(`- Model: \`${args.visionModel}\``);
1612
+ if (args.baseUrl) lines.push(`- Ollama baseUrl: \`${args.baseUrl}\``);
1613
+ if (typeof args.analysisTimeMs === "number")
1614
+ lines.push(`- Analysis time: \`${args.analysisTimeMs}ms\``);
1615
+ lines.push(`- Generated: \`${(/* @__PURE__ */ new Date()).toISOString()}\``);
1616
+ lines.push(``);
1617
+ if (args.metadata && Object.keys(args.metadata).length > 0) {
1618
+ lines.push(`## Metadata`);
1619
+ lines.push(``);
1620
+ lines.push("```json");
1621
+ lines.push(JSON.stringify(args.metadata, null, 2));
1622
+ lines.push("```");
1623
+ lines.push(``);
1666
1624
  }
1667
- walk(rootDir, 0);
1668
- return results;
1625
+ lines.push(`## Prompt`);
1626
+ lines.push(``);
1627
+ lines.push("```text");
1628
+ lines.push((args.prompt ?? "").trim());
1629
+ lines.push("```");
1630
+ lines.push(``);
1631
+ lines.push(`## Raw Response`);
1632
+ lines.push(``);
1633
+ lines.push("```text");
1634
+ lines.push((args.rawResponse ?? "").trim());
1635
+ lines.push("```");
1636
+ lines.push(``);
1637
+ const content = lines.join("\n");
1638
+ mkdirSync3(dirname5(outPath), { recursive: true });
1639
+ writeFileSync3(outPath, content, "utf-8");
1640
+ return { outPath, content };
1669
1641
  }
1670
1642
 
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);
1643
+ // src/commands/serve.ts
1644
+ function pickAppRoot(params) {
1645
+ const { cwd, workspaceRoot } = params;
1646
+ if (detectNextAppRouter(cwd)) return cwd;
1647
+ const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
1648
+ if (matches.length === 0) return cwd;
1649
+ if (matches.length === 1) return matches[0].projectPath;
1650
+ const containing = matches.find(
1651
+ (m) => cwd === m.projectPath || cwd.startsWith(m.projectPath + "/")
1652
+ );
1653
+ if (containing) return containing.projectPath;
1654
+ return matches[0].projectPath;
1716
1655
  }
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
- }
1656
+ var cache = /* @__PURE__ */ new Map();
1657
+ var eslintInstances = /* @__PURE__ */ new Map();
1658
+ var visionAnalyzer = null;
1659
+ function getVisionAnalyzerInstance() {
1660
+ if (!visionAnalyzer) {
1661
+ visionAnalyzer = getCoreVisionAnalyzer();
1732
1662
  }
1733
- return false;
1663
+ return visionAnalyzer;
1734
1664
  }
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((resolve8, 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) resolve8();
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
- var LEGACY_HOOK_FILES = ["uilint-validate.sh", "uilint-validate.js"];
2431
- function safeParseJson(filePath) {
2432
- try {
2433
- const content = readFileSync5(filePath, "utf-8");
2434
- return JSON.parse(content);
2435
- } catch {
2436
- return void 0;
2437
- }
2438
- }
2439
- async function analyze2(projectPath = process.cwd()) {
2440
- const workspaceRoot = findWorkspaceRoot5(projectPath);
2441
- const packageManager = detectPackageManager(projectPath);
2442
- const cursorDir = join8(projectPath, ".cursor");
2443
- const cursorDirExists = existsSync9(cursorDir);
2444
- const mcpPath = join8(cursorDir, "mcp.json");
2445
- const mcpExists = existsSync9(mcpPath);
2446
- const mcpConfig = mcpExists ? safeParseJson(mcpPath) : void 0;
2447
- const hooksPath = join8(cursorDir, "hooks.json");
2448
- const hooksExists = existsSync9(hooksPath);
2449
- const hooksConfig = hooksExists ? safeParseJson(hooksPath) : void 0;
2450
- const hooksDir = join8(cursorDir, "hooks");
2451
- const legacyPaths = [];
2452
- for (const legacyFile of LEGACY_HOOK_FILES) {
2453
- const legacyPath = join8(hooksDir, legacyFile);
2454
- if (existsSync9(legacyPath)) {
2455
- legacyPaths.push(legacyPath);
2456
- }
2457
- }
2458
- const styleguidePath = join8(projectPath, ".uilint", "styleguide.md");
2459
- const styleguideExists = existsSync9(styleguidePath);
2460
- const commandsDir = join8(cursorDir, "commands");
2461
- const genstyleguideExists = existsSync9(join8(commandsDir, "genstyleguide.md"));
2462
- const genrulesExists = existsSync9(join8(commandsDir, "genrules.md"));
2463
- const nextApps = [];
2464
- const directDetection = detectNextAppRouter(projectPath);
2465
- if (directDetection) {
2466
- nextApps.push({ projectPath, detection: directDetection });
2467
- } else {
2468
- const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
2469
- for (const match of matches) {
2470
- nextApps.push({
2471
- projectPath: match.projectPath,
2472
- detection: match.detection
2473
- });
2474
- }
2475
- }
2476
- const viteApps = [];
2477
- const directVite = detectViteReact(projectPath);
2478
- if (directVite) {
2479
- viteApps.push({ projectPath, detection: directVite });
2480
- } else {
2481
- const matches = findViteReactProjects(workspaceRoot, { maxDepth: 5 });
2482
- for (const match of matches) {
2483
- viteApps.push({
2484
- projectPath: match.projectPath,
2485
- detection: match.detection
2486
- });
2487
- }
2488
- }
2489
- const rawPackages = findPackages(workspaceRoot);
2490
- const packages = rawPackages.map((pkg) => {
2491
- const eslintConfigPath = findEslintConfigFile(pkg.path);
2492
- let eslintConfigFilename = null;
2493
- let hasRules = false;
2494
- let configuredRuleIds = [];
2495
- if (eslintConfigPath) {
2496
- eslintConfigFilename = getEslintConfigFilename(eslintConfigPath);
2497
- try {
2498
- const source = readFileSync5(eslintConfigPath, "utf-8");
2499
- const info = getUilintEslintConfigInfoFromSource(source);
2500
- hasRules = info.configuredRuleIds.size > 0;
2501
- configuredRuleIds = Array.from(info.configuredRuleIds);
2502
- } catch {
2503
- }
2504
- }
2505
- return {
2506
- ...pkg,
2507
- eslintConfigPath,
2508
- eslintConfigFilename,
2509
- hasUilintRules: hasRules,
2510
- configuredRuleIds
2511
- };
2512
- });
2513
- return {
2514
- projectPath,
2515
- workspaceRoot,
2516
- packageManager,
2517
- cursorDir: {
2518
- exists: cursorDirExists,
2519
- path: cursorDir
2520
- },
2521
- mcp: {
2522
- exists: mcpExists,
2523
- path: mcpPath,
2524
- config: mcpConfig
2525
- },
2526
- hooks: {
2527
- exists: hooksExists,
2528
- path: hooksPath,
2529
- config: hooksConfig,
2530
- hasLegacy: legacyPaths.length > 0,
2531
- legacyPaths
2532
- },
2533
- styleguide: {
2534
- exists: styleguideExists,
2535
- path: styleguidePath
2536
- },
2537
- commands: {
2538
- genstyleguide: genstyleguideExists,
2539
- genrules: genrulesExists
2540
- },
2541
- nextApps,
2542
- viteApps,
2543
- packages
2544
- };
2545
- }
2546
-
2547
- // src/commands/install/plan.ts
2548
- import { join as join11 } from "path";
2549
- import { createRequire as createRequire2 } from "module";
2550
-
2551
- // src/commands/install/constants.ts
2552
- var HOOKS_CONFIG = {
2553
- version: 1,
2554
- hooks: {
2555
- beforeSubmitPrompt: [{ command: ".cursor/hooks/uilint-session-start.sh" }],
2556
- afterFileEdit: [{ command: ".cursor/hooks/uilint-track.sh" }],
2557
- stop: [{ command: ".cursor/hooks/uilint-session-end.sh" }]
2558
- }
2559
- };
2560
- var MCP_CONFIG = {
2561
- mcpServers: {
2562
- uilint: {
2563
- command: "npx",
2564
- args: ["uilint-mcp"]
2565
- }
2566
- }
2567
- };
2568
- var LEGACY_HOOK_COMMANDS = [
2569
- ".cursor/hooks/uilint-validate.sh",
2570
- ".cursor/hooks/uilint-validate.js"
2571
- ];
2572
- var SESSION_START_SCRIPT = `#!/bin/bash
2573
- # UILint session start hook
2574
- # Clears tracked files at the start of each agent turn
2575
- #
2576
- # IMPORTANT: Cursor hooks communicate over stdio using JSON.
2577
- # - stdout must be JSON (Cursor will parse it)
2578
- # - stderr is for logs
2579
-
2580
- echo "[UILint] Session start - clearing tracked files" >&2
2581
-
2582
- # Prefer local monorepo build when developing UILint itself.
2583
- # Fall back to npx for normal consumers.
2584
- uilint() {
2585
- if [ -f "packages/uilint/dist/index.js" ]; then
2586
- node "packages/uilint/dist/index.js" "$@"
2587
- else
2588
- npx uilint "$@"
2589
- fi
2590
- }
2591
-
2592
- # Read JSON input from stdin (required by hook protocol)
2593
- cat > /dev/null
2594
-
2595
- # Clear session state
2596
- result=$(uilint session clear)
2597
- status=$?
2598
-
2599
- echo "[UILint] Clear exit: $status" >&2
2600
-
2601
- if [ $status -eq 0 ] && [ -n "$result" ]; then
2602
- echo "$result"
2603
- else
2604
- echo '{"cleared":false}'
2605
- fi
2606
-
2607
- exit 0
2608
- `;
2609
- var TRACK_SCRIPT = `#!/bin/bash
2610
- # UILint file tracking hook
2611
- # Tracks UI file edits for batch validation on agent stop
2612
- #
2613
- # IMPORTANT: Cursor hooks communicate over stdio using JSON.
2614
- # - stdout must be JSON (Cursor will parse it)
2615
- # - stderr is for logs
2616
-
2617
- out='{}'
2618
-
2619
- # Read JSON input from stdin
2620
- input=$(cat)
2621
-
2622
- echo "[UILint] afterFileEdit hook triggered" >&2
2623
-
2624
- # Prefer local monorepo build when developing UILint itself.
2625
- # Fall back to npx for normal consumers.
2626
- uilint() {
2627
- if [ -f "packages/uilint/dist/index.js" ]; then
2628
- node "packages/uilint/dist/index.js" "$@"
2629
- else
2630
- npx uilint "$@"
2631
- fi
2632
- }
2633
-
2634
- # Extract file_path using grep/sed (works without jq)
2635
- file_path=$(echo "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)"/\\1/')
2636
-
2637
- echo "[UILint] Extracted file_path: $file_path" >&2
2638
-
2639
- if [ -z "$file_path" ]; then
2640
- echo "[UILint] No file_path found in input, skipping" >&2
2641
- printf '%s\\n' "$out"
2642
- exit 0
2643
- fi
2644
-
2645
- # Track the file (session command filters for UI files internally)
2646
- echo "[UILint] Tracking file: $file_path" >&2
2647
- result=$(uilint session track "$file_path")
2648
- status=$?
2649
-
2650
- echo "[UILint] Track exit: $status" >&2
2651
-
2652
- if [ $status -eq 0 ] && [ -n "$result" ]; then
2653
- out="$result"
2654
- fi
2655
-
2656
- printf '%s\\n' "$out"
2657
- exit 0
2658
- `;
2659
- var SESSION_END_SCRIPT = `#!/bin/bash
2660
- # UILint session end hook
2661
- # Scans tracked markup files and returns followup_message for auto-fix
2662
- #
2663
- # IMPORTANT: Cursor hooks communicate over stdio using JSON.
2664
- # - stdout must be JSON (Cursor will parse it)
2665
- # - stderr is for logs
2666
-
2667
- echo "[UILint] Session end hook triggered" >&2
2668
-
2669
- # Read JSON input from stdin (contains status, loop_count)
2670
- input=$(cat)
2671
-
2672
- echo "[UILint] Stop input: $input" >&2
2673
-
2674
- # Prefer local monorepo build when developing UILint itself.
2675
- # Fall back to npx for normal consumers.
2676
- uilint() {
2677
- if [ -f "packages/uilint/dist/index.js" ]; then
2678
- node "packages/uilint/dist/index.js" "$@"
2679
- else
2680
- npx uilint "$@"
2681
- fi
2682
- }
2683
-
2684
- # Extract loop_count to prevent infinite loops
2685
- loop_count=$(echo "$input" | grep -o '"loop_count"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$')
2686
- loop_count=\${loop_count:-0}
2687
-
2688
- echo "[UILint] Loop count: $loop_count" >&2
2689
-
2690
- # Don't trigger followup if we've already looped 3+ times
2691
- if [ "$loop_count" -ge 3 ]; then
2692
- echo "[UILint] Max loops reached, skipping scan" >&2
2693
- echo '{}'
2694
- exit 0
2695
- fi
2696
-
2697
- # First check what files are tracked
2698
- echo "[UILint] Checking tracked files..." >&2
2699
- tracked=$(uilint session list)
2700
- echo "[UILint] Tracked files: $tracked" >&2
2701
-
2702
- # Run scan with --hook flag for direct JSON output
2703
- echo "[UILint] Running scan..." >&2
2704
- result=$(uilint session scan --hook)
2705
- status=$?
2706
-
2707
- echo "[UILint] Scan exit: $status" >&2
2708
-
2709
- if [ $status -eq 0 ] && [ -n "$result" ]; then
2710
- echo "$result"
2711
- else
2712
- echo '{}'
2713
- fi
2714
-
2715
- exit 0
2716
- `;
2717
- var GENSTYLEGUIDE_COMMAND_MD = `# React Style Guide Generator
2718
-
2719
- Analyze the React UI codebase to produce a **prescriptive, semantic** style guide. Focus on consistency, intent, and relationships\u2014not specific values.
2720
-
2721
- ## Philosophy
2722
-
2723
- 1. **Identify the intended architecture** from the best patterns in use
2724
- 2. **Prescribe semantic rules** \u2014 about consistency and relationships, not pixels
2725
- 3. **Stay general** \u2014 "primary buttons should be visually consistent" not "buttons use px-4"
2726
- 4. **Focus on intent** \u2014 what should FEEL the same, not what values to use
2727
-
2728
- ## Analysis Steps
2729
-
2730
- ### 1. Detect the Stack
2731
- - Framework: Next.js (App Router? Pages?), Vite, CRA
2732
- - Component system: shadcn, MUI, Chakra, Radix, custom
2733
- - Styling: Tailwind, CSS Modules, styled-components
2734
- - Forms: react-hook-form, Formik, native
2735
- - State: React context, Zustand, Redux, Jotai
2736
-
2737
- ### 2. Identify Best Patterns
2738
- Examine the **best-written** components. Look at:
2739
- - \`components/ui/*\` \u2014 the design system
2740
- - Recently modified files \u2014 current standards
2741
- - Shared layouts \u2014 structural patterns
2742
-
2743
- ### 3. Infer Visual Hierarchy & Intent
2744
- Understand the design language:
2745
- - What distinguishes primary vs secondary actions?
2746
- - How is visual hierarchy established?
2747
- - What creates consistency across similar elements?
2748
-
2749
- ## Output Format
2750
-
2751
- Generate at \`<nextjs app root>/.uilint/styleguide.md\`:
2752
- \`\`\`yaml
2753
- # Stack
2754
- framework:
2755
- styling:
2756
- components:
2757
- component_path:
2758
- forms:
2759
-
2760
- # Component Usage (MUST use these)
2761
- use:
2762
- buttons:
2763
- inputs:
2764
- modals:
2765
- cards:
2766
- feedback:
2767
- icons:
2768
- links:
2769
-
2770
- # Semantic Rules (consistency & relationships)
2771
- semantics:
2772
- hierarchy:
2773
- - <e.g., "primary actions must be visually distinct from secondary">
2774
- - <e.g., "destructive actions should be visually cautionary">
2775
- - <e.g., "page titles should be visually heavier than section titles">
2776
- consistency:
2777
- - <e.g., "all primary buttons should share the same visual weight">
2778
- - <e.g., "form inputs should have uniform height and padding">
2779
- - <e.g., "card padding should be consistent across the app">
2780
- - <e.g., "interactive elements should have consistent hover/focus states">
2781
- spacing:
2782
- - <e.g., "use the spacing scale \u2014 no arbitrary values">
2783
- - <e.g., "related elements should be closer than unrelated">
2784
- - <e.g., "section spacing should be larger than element spacing">
2785
- layout:
2786
- - <e.g., "use gap for sibling spacing, not margin">
2787
- - <e.g., "containers should have consistent max-width and padding">
2788
-
2789
- # Patterns (structural, not values)
2790
- patterns:
2791
- forms: <e.g., "FormField + Controller + zod schema">
2792
- conditionals: <e.g., "cn() for class merging">
2793
- loading: <e.g., "Skeleton for content, Spinner for actions">
2794
- errors: <e.g., "ErrorBoundary at route, inline for forms">
2795
- responsive: <e.g., "mobile-first, standard breakpoints only">
2796
-
2797
- # Component Authoring
2798
- authoring:
2799
- - <e.g., "forwardRef for interactive components">
2800
- - <e.g., "variants via CVA or component props, not className overrides">
2801
- - <e.g., "extract when used 2+ times">
2802
- - <e.g., "'use client' only when needed">
2803
-
2804
- # Forbidden
2805
- forbidden:
2806
- - <e.g., "inline style={{}}">
2807
- - <e.g., "raw HTML elements when component exists">
2808
- - <e.g., "arbitrary values \u2014 use scale">
2809
- - <e.g., "className overrides that break visual consistency">
2810
- - <e.g., "one-off spacing that doesn't match siblings">
2811
-
2812
- # Legacy (if migration in progress)
2813
- legacy:
2814
- - <e.g., "old: CSS modules \u2192 new: Tailwind">
2815
- - <e.g., "old: Formik \u2192 new: react-hook-form">
2816
-
2817
- # Conventions
2818
- conventions:
2819
- -
2820
- -
2821
- -
2822
- \`\`\`
2823
-
2824
- ## Rules
2825
-
2826
- - **Semantic over specific**: "consistent padding" not "p-4"
2827
- - **Relationships over absolutes**: "heavier than" not "font-bold"
2828
- - **Intent over implementation**: "visually distinct" not "blue background"
2829
- - **Prescriptive**: Define target state, not current state
2830
- - **Terse**: No prose. Fragments and short phrases only.
2831
- - **Actionable**: Every rule should be human-verifiable
2832
- - **Omit if N/A**: Skip sections that don't apply
2833
- - **Max 5 items** per section \u2014 highest impact only
2834
- `;
2835
- var GENRULES_COMMAND_MD = `# ESLint Rule Generator
2836
-
2837
- Generate custom ESLint rules from your UILint styleguide (\`.uilint/styleguide.md\`).
2838
-
2839
- ## Purpose
2840
-
2841
- Transform your semantic styleguide rules into concrete, enforceable ESLint rules that:
2842
- - Run automatically during development
2843
- - Integrate with your editor
2844
- - Catch issues before commit
2845
- - Provide actionable error messages
2846
-
2847
- ## Analysis Steps
2848
-
2849
- ### 1. Read the Styleguide
2850
-
2851
- Look at \`.uilint/styleguide.md\` for:
2852
- - **Component Usage** (\`use:\` section) - which components should be used
2853
- - **Forbidden** patterns - what to disallow
2854
- - **Semantic Rules** - spacing, consistency, hierarchy
2855
- - **Patterns** - form handling, conditionals, state management
2856
-
2857
- ### 2. Identify Rule Candidates
2858
-
2859
- Focus on rules that can be statically analyzed:
2860
- - Import patterns (e.g., "use Button from shadcn, not raw HTML button")
2861
- - Forbidden patterns (e.g., "no inline style={{}}")
2862
- - Component library mixing (e.g., "don't mix MUI and shadcn")
2863
- - Tailwind patterns (e.g., "no arbitrary values")
2864
-
2865
- ### 3. Generate Rule Files
2866
-
2867
- Create TypeScript ESLint rules in \`.uilint/rules/\`:
2868
-
2869
- \`\`\`typescript
2870
- // .uilint/rules/prefer-shadcn-button.ts
2871
- import { createRule } from 'uilint-eslint';
2872
-
2873
- export default createRule({
2874
- name: 'prefer-shadcn-button',
2875
- meta: {
2876
- type: 'problem',
2877
- docs: { description: 'Use Button from shadcn instead of raw <button>' },
2878
- messages: {
2879
- preferButton: 'Use <Button> from @/components/ui/button instead of raw <button>',
2880
- },
2881
- schema: [],
2882
- },
2883
- defaultOptions: [],
2884
- create(context) {
2885
- return {
2886
- JSXOpeningElement(node) {
2887
- if (node.name.type === 'JSXIdentifier' && node.name.name === 'button') {
2888
- context.report({ node, messageId: 'preferButton' });
2889
- }
2890
- },
2891
- };
2892
- },
2893
- });
2894
- \`\`\`
2895
-
2896
- ### 4. Generate ESLint Config
2897
-
2898
- Create or update \`eslint.config.js\` to include the generated rules:
2899
-
2900
- \`\`\`javascript
2901
- import uilint from 'uilint-eslint';
2902
- import preferShadcnButton from './.uilint/rules/prefer-shadcn-button.js';
2903
-
2904
- export default [
2905
- uilint.configs.recommended,
2906
- {
2907
- plugins: {
2908
- 'uilint-custom': {
2909
- rules: {
2910
- 'prefer-shadcn-button': preferShadcnButton,
2911
- },
2912
- },
2913
- },
2914
- rules: {
2915
- 'uilint-custom/prefer-shadcn-button': 'error',
2916
- },
2917
- },
2918
- ];
2919
- \`\`\`
2920
-
2921
- ## Output
2922
-
2923
- Generate in \`.uilint/rules/\`:
2924
- - One TypeScript file per rule
2925
- - An \`index.ts\` that exports all rules
2926
- - Update instructions for \`eslint.config.js\`
2927
-
2928
- ## Guidelines
2929
-
2930
- - **Focus on static analysis** - rules must work without runtime info
2931
- - **Clear error messages** - tell devs exactly what to do
2932
- - **No false positives** - better to miss issues than over-report
2933
- - **Performance** - rules run on every file, keep them fast
2934
- - **Minimal rules** - generate 3-5 high-impact rules, not dozens
2935
- `;
2936
-
2937
- // src/utils/skill-loader.ts
2938
- import { readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2, existsSync as existsSync10 } from "fs";
2939
- import { join as join9, dirname as dirname7, relative as relative3 } from "path";
2940
- import { fileURLToPath as fileURLToPath2 } from "url";
2941
- var __filename = fileURLToPath2(import.meta.url);
2942
- var __dirname = dirname7(__filename);
2943
- function getSkillsDir() {
2944
- const devPath = join9(__dirname, "..", "..", "skills");
2945
- const prodPath = join9(__dirname, "..", "skills");
2946
- if (existsSync10(devPath)) {
2947
- return devPath;
2948
- }
2949
- if (existsSync10(prodPath)) {
2950
- return prodPath;
2951
- }
2952
- throw new Error(
2953
- "Could not find skills directory. This is a bug in uilint installation."
2954
- );
2955
- }
2956
- function collectFiles(dir, baseDir) {
2957
- const files = [];
2958
- const entries = readdirSync4(dir);
2959
- for (const entry of entries) {
2960
- const fullPath = join9(dir, entry);
2961
- const stat = statSync2(fullPath);
2962
- if (stat.isDirectory()) {
2963
- files.push(...collectFiles(fullPath, baseDir));
2964
- } else if (stat.isFile()) {
2965
- const relativePath = relative3(baseDir, fullPath);
2966
- const content = readFileSync6(fullPath, "utf-8");
2967
- files.push({ relativePath, content });
2968
- }
2969
- }
2970
- return files;
2971
- }
2972
- function loadSkill(name) {
2973
- const skillsDir = getSkillsDir();
2974
- const skillDir = join9(skillsDir, name);
2975
- if (!existsSync10(skillDir)) {
2976
- throw new Error(`Skill "${name}" not found in ${skillsDir}`);
2977
- }
2978
- const skillMdPath = join9(skillDir, "SKILL.md");
2979
- if (!existsSync10(skillMdPath)) {
2980
- throw new Error(`Skill "${name}" is missing SKILL.md`);
2981
- }
2982
- const files = collectFiles(skillDir, skillDir);
2983
- return { name, files };
2984
- }
2985
-
2986
- // src/utils/rule-loader.ts
2987
- import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
2988
- import { join as join10, dirname as dirname8 } from "path";
2989
- import { fileURLToPath as fileURLToPath3 } from "url";
2990
- import { createRequire } from "module";
2991
- var __filename2 = fileURLToPath3(import.meta.url);
2992
- var __dirname2 = dirname8(__filename2);
2993
- var require2 = createRequire(import.meta.url);
2994
- function findNodeModulesPackageRoot(pkgName, startDir) {
2995
- let dir = startDir;
2996
- while (true) {
2997
- const candidate = join10(dir, "node_modules", pkgName);
2998
- if (existsSync11(join10(candidate, "package.json"))) return candidate;
2999
- const parent = dirname8(dir);
3000
- if (parent === dir) break;
3001
- dir = parent;
3002
- }
3003
- return null;
3004
- }
3005
- function getUilintEslintPackageRoot() {
3006
- const fromCwd = findNodeModulesPackageRoot("uilint-eslint", process.cwd());
3007
- if (fromCwd) return fromCwd;
3008
- const fromHere = findNodeModulesPackageRoot("uilint-eslint", __dirname2);
3009
- if (fromHere) return fromHere;
3010
- try {
3011
- const entry = require2.resolve("uilint-eslint");
3012
- const entryDir = dirname8(entry);
3013
- return dirname8(entryDir);
3014
- } catch (e) {
3015
- const msg = e instanceof Error ? e.message : String(e);
3016
- throw new Error(
3017
- `Unable to locate uilint-eslint in node_modules (searched upwards from cwd and uilint's install path).
3018
- Resolver error: ${msg}
3019
- Fix: ensure uilint-eslint is installed in the target project (or workspace) and try again.`
3020
- );
3021
- }
3022
- }
3023
- function getUilintEslintSrcDir() {
3024
- const devPath = join10(
3025
- __dirname2,
3026
- "..",
3027
- "..",
3028
- "..",
3029
- "..",
3030
- "uilint-eslint",
3031
- "src"
3032
- );
3033
- if (existsSync11(devPath)) return devPath;
3034
- const pkgRoot = getUilintEslintPackageRoot();
3035
- const srcPath = join10(pkgRoot, "src");
3036
- if (existsSync11(srcPath)) return srcPath;
3037
- throw new Error(
3038
- '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.'
3039
- );
3040
- }
3041
- function getUilintEslintDistDir() {
3042
- const devPath = join10(
3043
- __dirname2,
3044
- "..",
3045
- "..",
3046
- "..",
3047
- "..",
3048
- "uilint-eslint",
3049
- "dist"
3050
- );
3051
- if (existsSync11(devPath)) return devPath;
3052
- const pkgRoot = getUilintEslintPackageRoot();
3053
- const distPath = join10(pkgRoot, "dist");
3054
- if (existsSync11(distPath)) return distPath;
3055
- throw new Error(
3056
- 'Could not find uilint-eslint "dist/" directory. This is a bug in uilint installation.'
3057
- );
3058
- }
3059
- function transformRuleContent(content) {
3060
- let transformed = content;
3061
- transformed = transformed.replace(
3062
- /import\s+{\s*createRule\s*}\s+from\s+["']\.\.\/utils\/create-rule\.js["'];?/g,
3063
- 'import { createRule } from "uilint-eslint";'
3064
- );
3065
- transformed = transformed.replace(
3066
- /import\s+createRule\s+from\s+["']\.\.\/utils\/create-rule\.js["'];?/g,
3067
- 'import { createRule } from "uilint-eslint";'
3068
- );
3069
- transformed = transformed.replace(
3070
- /import\s+{([^}]+)}\s+from\s+["']\.\.\/utils\/([^"']+)\.js["'];?/g,
3071
- (match, imports, utilFile) => {
3072
- const utilsFromPackage = ["cache", "styleguide-loader", "import-graph"];
3073
- if (utilsFromPackage.includes(utilFile)) {
3074
- return `import {${imports}} from "uilint-eslint";`;
3075
- }
3076
- return match;
3077
- }
3078
- );
3079
- return transformed;
3080
- }
3081
- function loadRule(ruleId, options = { typescript: true }) {
3082
- const { typescript } = options;
3083
- const extension = typescript ? ".ts" : ".js";
3084
- if (typescript) {
3085
- const rulesDir = join10(getUilintEslintSrcDir(), "rules");
3086
- const implPath = join10(rulesDir, `${ruleId}.ts`);
3087
- const testPath = join10(rulesDir, `${ruleId}.test.ts`);
3088
- if (!existsSync11(implPath)) {
3089
- throw new Error(`Rule "${ruleId}" not found at ${implPath}`);
3090
- }
3091
- const rawContent = readFileSync7(implPath, "utf-8");
3092
- const transformedContent = transformRuleContent(rawContent);
3093
- const implementation = {
3094
- relativePath: `${ruleId}.ts`,
3095
- content: transformedContent
3096
- };
3097
- const test = existsSync11(testPath) ? {
3098
- relativePath: `${ruleId}.test.ts`,
3099
- content: transformRuleContent(readFileSync7(testPath, "utf-8"))
3100
- } : void 0;
3101
- return {
3102
- ruleId,
3103
- implementation,
3104
- test
3105
- };
3106
- } else {
3107
- const rulesDir = join10(getUilintEslintDistDir(), "rules");
3108
- const implPath = join10(rulesDir, `${ruleId}.js`);
3109
- if (!existsSync11(implPath)) {
3110
- throw new Error(
3111
- `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.`
3112
- );
3113
- }
3114
- const content = readFileSync7(implPath, "utf-8");
3115
- const implementation = {
3116
- relativePath: `${ruleId}.js`,
3117
- content
3118
- };
3119
- return {
3120
- ruleId,
3121
- implementation
3122
- };
3123
- }
3124
- }
3125
- function loadSelectedRules(ruleIds, options = { typescript: true }) {
3126
- return ruleIds.map((id) => loadRule(id, options));
3127
- }
3128
-
3129
- // src/commands/install/plan.ts
3130
- var require3 = createRequire2(import.meta.url);
3131
- function getSelfDependencyVersionRange(pkgName) {
3132
- try {
3133
- const pkgJson = require3("uilint/package.json");
3134
- const deps = pkgJson?.dependencies;
3135
- const optDeps = pkgJson?.optionalDependencies;
3136
- const peerDeps = pkgJson?.peerDependencies;
3137
- const v = deps?.[pkgName] ?? optDeps?.[pkgName] ?? peerDeps?.[pkgName];
3138
- return typeof v === "string" ? v : null;
3139
- } catch {
3140
- return null;
3141
- }
3142
- }
3143
- function toInstallSpecifier(pkgName) {
3144
- const range = getSelfDependencyVersionRange(pkgName);
3145
- if (!range) return pkgName;
3146
- if (range.startsWith("workspace:")) return pkgName;
3147
- if (range.startsWith("file:")) return pkgName;
3148
- if (range.startsWith("link:")) return pkgName;
3149
- return `${pkgName}@${range}`;
3150
- }
3151
- function mergeHooksConfig(existing, ours) {
3152
- const result = { ...existing };
3153
- for (const [hookName, hookArray] of Object.entries(result.hooks)) {
3154
- if (!Array.isArray(hookArray)) continue;
3155
- result.hooks[hookName] = hookArray.filter(
3156
- (h) => !LEGACY_HOOK_COMMANDS.includes(h.command)
3157
- );
3158
- }
3159
- for (const [hookName, ourHooks] of Object.entries(ours.hooks)) {
3160
- if (!Array.isArray(ourHooks)) continue;
3161
- const existingHooks = result.hooks[hookName] || [];
3162
- for (const ourHook of ourHooks) {
3163
- const alreadyExists = existingHooks.some(
3164
- (h) => h.command === ourHook.command
3165
- );
3166
- if (!alreadyExists) {
3167
- existingHooks.push(ourHook);
3168
- }
3169
- }
3170
- result.hooks[hookName] = existingHooks;
3171
- }
3172
- return result;
3173
- }
3174
- function createPlan(state, choices, options = {}) {
3175
- const actions = [];
3176
- const dependencies = [];
3177
- const { force = false } = options;
3178
- const { items } = choices;
3179
- const needsCursorDir = items.includes("mcp") || items.includes("hooks") || items.includes("genstyleguide") || items.includes("genrules") || items.includes("skill");
3180
- if (needsCursorDir && !state.cursorDir.exists) {
3181
- actions.push({
3182
- type: "create_directory",
3183
- path: state.cursorDir.path
3184
- });
3185
- }
3186
- if (items.includes("mcp")) {
3187
- if (!state.mcp.exists || force) {
3188
- actions.push({
3189
- type: "create_file",
3190
- path: state.mcp.path,
3191
- content: JSON.stringify(MCP_CONFIG, null, 2)
3192
- });
3193
- } else if (choices.mcpMerge) {
3194
- const merged = {
3195
- mcpServers: {
3196
- ...state.mcp.config?.mcpServers || {},
3197
- ...MCP_CONFIG.mcpServers
3198
- }
3199
- };
3200
- actions.push({
3201
- type: "create_file",
3202
- path: state.mcp.path,
3203
- content: JSON.stringify(merged, null, 2)
3204
- });
3205
- }
3206
- }
3207
- if (items.includes("hooks")) {
3208
- const hooksDir = join11(state.cursorDir.path, "hooks");
3209
- actions.push({
3210
- type: "create_directory",
3211
- path: hooksDir
3212
- });
3213
- for (const legacyPath of state.hooks.legacyPaths) {
3214
- actions.push({
3215
- type: "delete_file",
3216
- path: legacyPath
3217
- });
3218
- }
3219
- let finalHooksConfig;
3220
- if (!state.hooks.exists || force) {
3221
- finalHooksConfig = HOOKS_CONFIG;
3222
- } else if (choices.hooksMerge && state.hooks.config) {
3223
- finalHooksConfig = mergeHooksConfig(state.hooks.config, HOOKS_CONFIG);
3224
- } else {
3225
- finalHooksConfig = HOOKS_CONFIG;
3226
- }
3227
- actions.push({
3228
- type: "create_file",
3229
- path: state.hooks.path,
3230
- content: JSON.stringify(finalHooksConfig, null, 2)
3231
- });
3232
- actions.push({
3233
- type: "create_file",
3234
- path: join11(hooksDir, "uilint-session-start.sh"),
3235
- content: SESSION_START_SCRIPT,
3236
- permissions: 493
3237
- });
3238
- actions.push({
3239
- type: "create_file",
3240
- path: join11(hooksDir, "uilint-track.sh"),
3241
- content: TRACK_SCRIPT,
3242
- permissions: 493
3243
- });
3244
- actions.push({
3245
- type: "create_file",
3246
- path: join11(hooksDir, "uilint-session-end.sh"),
3247
- content: SESSION_END_SCRIPT,
3248
- permissions: 493
3249
- });
3250
- }
3251
- if (items.includes("genstyleguide")) {
3252
- const commandsDir = join11(state.cursorDir.path, "commands");
3253
- actions.push({
3254
- type: "create_directory",
3255
- path: commandsDir
3256
- });
3257
- actions.push({
3258
- type: "create_file",
3259
- path: join11(commandsDir, "genstyleguide.md"),
3260
- content: GENSTYLEGUIDE_COMMAND_MD
3261
- });
3262
- }
3263
- if (items.includes("genrules")) {
3264
- const commandsDir = join11(state.cursorDir.path, "commands");
3265
- actions.push({
3266
- type: "create_directory",
3267
- path: commandsDir
3268
- });
3269
- actions.push({
3270
- type: "create_file",
3271
- path: join11(commandsDir, "genrules.md"),
3272
- content: GENRULES_COMMAND_MD
3273
- });
3274
- }
3275
- if (items.includes("skill")) {
3276
- const skillsDir = join11(state.cursorDir.path, "skills");
3277
- actions.push({
3278
- type: "create_directory",
3279
- path: skillsDir
3280
- });
3281
- try {
3282
- const skill = loadSkill("ui-consistency-enforcer");
3283
- const skillDir = join11(skillsDir, skill.name);
3284
- actions.push({
3285
- type: "create_directory",
3286
- path: skillDir
3287
- });
3288
- for (const file of skill.files) {
3289
- const filePath = join11(skillDir, file.relativePath);
3290
- const fileDir = join11(
3291
- skillDir,
3292
- file.relativePath.split("/").slice(0, -1).join("/")
3293
- );
3294
- if (fileDir !== skillDir && file.relativePath.includes("/")) {
3295
- actions.push({
3296
- type: "create_directory",
3297
- path: fileDir
3298
- });
3299
- }
3300
- actions.push({
3301
- type: "create_file",
3302
- path: filePath,
3303
- content: file.content
3304
- });
3305
- }
3306
- } catch {
3307
- }
3308
- }
3309
- if (items.includes("next") && choices.next) {
3310
- const { projectPath, detection } = choices.next;
3311
- actions.push({
3312
- type: "install_next_routes",
3313
- projectPath,
3314
- appRoot: detection.appRoot
3315
- });
3316
- dependencies.push({
3317
- packagePath: projectPath,
3318
- packageManager: state.packageManager,
3319
- packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
3320
- });
3321
- actions.push({
3322
- type: "inject_react",
3323
- projectPath,
3324
- appRoot: detection.appRoot
3325
- });
3326
- actions.push({
3327
- type: "inject_next_config",
3328
- projectPath
3329
- });
3330
- }
3331
- if (items.includes("vite") && choices.vite) {
3332
- const { projectPath, detection } = choices.vite;
3333
- dependencies.push({
3334
- packagePath: projectPath,
3335
- packageManager: state.packageManager,
3336
- packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
3337
- });
3338
- actions.push({
3339
- type: "inject_react",
3340
- projectPath,
3341
- appRoot: detection.entryRoot,
3342
- mode: "vite"
3343
- });
3344
- actions.push({
3345
- type: "inject_vite_config",
3346
- projectPath
3347
- });
3348
- }
3349
- if (items.includes("eslint") && choices.eslint) {
3350
- const { packagePaths, selectedRules } = choices.eslint;
3351
- for (const pkgPath of packagePaths) {
3352
- const pkgInfo = state.packages.find((p2) => p2.path === pkgPath);
3353
- const rulesDir = join11(pkgPath, ".uilint", "rules");
3354
- actions.push({
3355
- type: "create_directory",
3356
- path: rulesDir
3357
- });
3358
- const isTypeScript = pkgInfo?.isTypeScript ?? true;
3359
- const ruleFiles = loadSelectedRules(
3360
- selectedRules.map((r) => r.id),
3361
- {
3362
- typescript: isTypeScript
3363
- }
3364
- );
3365
- for (const ruleFile of ruleFiles) {
3366
- actions.push({
3367
- type: "create_file",
3368
- path: join11(rulesDir, ruleFile.implementation.relativePath),
3369
- content: ruleFile.implementation.content
3370
- });
3371
- if (ruleFile.test && isTypeScript) {
3372
- actions.push({
3373
- type: "create_file",
3374
- path: join11(rulesDir, ruleFile.test.relativePath),
3375
- content: ruleFile.test.content
3376
- });
3377
- }
3378
- }
3379
- dependencies.push({
3380
- packagePath: pkgPath,
3381
- packageManager: state.packageManager,
3382
- packages: [toInstallSpecifier("uilint-eslint"), "typescript-eslint"]
3383
- });
3384
- if (pkgInfo?.eslintConfigPath) {
3385
- actions.push({
3386
- type: "inject_eslint",
3387
- packagePath: pkgPath,
3388
- configPath: pkgInfo.eslintConfigPath,
3389
- rules: selectedRules,
3390
- hasExistingRules: pkgInfo.hasUilintRules
3391
- });
3392
- }
3393
- }
3394
- const gitignorePath = join11(state.workspaceRoot, ".gitignore");
3395
- actions.push({
3396
- type: "append_to_file",
3397
- path: gitignorePath,
3398
- content: "\n# UILint cache\n.uilint/.cache\n",
3399
- ifNotContains: ".uilint/.cache"
3400
- });
3401
- }
3402
- return { actions, dependencies };
3403
- }
3404
-
3405
- // src/commands/install/execute.ts
3406
- import {
3407
- existsSync as existsSync16,
3408
- mkdirSync as mkdirSync3,
3409
- writeFileSync as writeFileSync7,
3410
- readFileSync as readFileSync11,
3411
- unlinkSync,
3412
- chmodSync
3413
- } from "fs";
3414
- import { dirname as dirname9 } from "path";
3415
-
3416
- // src/utils/react-inject.ts
3417
- import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
3418
- import { join as join12 } from "path";
3419
- import { parseModule as parseModule2, generateCode as generateCode2 } from "magicast";
3420
- function getDefaultCandidates(projectPath, appRoot) {
3421
- const viteMainCandidates = [
3422
- join12(appRoot, "main.tsx"),
3423
- join12(appRoot, "main.jsx"),
3424
- join12(appRoot, "main.ts"),
3425
- join12(appRoot, "main.js")
3426
- ];
3427
- const existingViteMain = viteMainCandidates.filter(
3428
- (rel) => existsSync12(join12(projectPath, rel))
3429
- );
3430
- if (existingViteMain.length > 0) return existingViteMain;
3431
- const viteAppCandidates = [join12(appRoot, "App.tsx"), join12(appRoot, "App.jsx")];
3432
- const existingViteApp = viteAppCandidates.filter(
3433
- (rel) => existsSync12(join12(projectPath, rel))
3434
- );
3435
- if (existingViteApp.length > 0) return existingViteApp;
3436
- const layoutCandidates = [
3437
- join12(appRoot, "layout.tsx"),
3438
- join12(appRoot, "layout.jsx"),
3439
- join12(appRoot, "layout.ts"),
3440
- join12(appRoot, "layout.js")
3441
- ];
3442
- const existingLayouts = layoutCandidates.filter(
3443
- (rel) => existsSync12(join12(projectPath, rel))
3444
- );
3445
- if (existingLayouts.length > 0) {
3446
- return existingLayouts;
3447
- }
3448
- const pageCandidates = [join12(appRoot, "page.tsx"), join12(appRoot, "page.jsx")];
3449
- return pageCandidates.filter((rel) => existsSync12(join12(projectPath, rel)));
3450
- }
3451
- function isUseClientDirective(stmt) {
3452
- return stmt?.type === "ExpressionStatement" && stmt.expression?.type === "StringLiteral" && stmt.expression.value === "use client";
3453
- }
3454
- function findImportDeclaration(program2, from) {
3455
- if (!program2 || program2.type !== "Program") return null;
3456
- for (const stmt of program2.body ?? []) {
3457
- if (stmt?.type !== "ImportDeclaration") continue;
3458
- if (stmt.source?.value === from) return stmt;
3459
- }
3460
- return null;
3461
- }
3462
- function walkAst(node, visit) {
3463
- if (!node || typeof node !== "object") return;
3464
- if (node.type) visit(node);
3465
- for (const key of Object.keys(node)) {
3466
- const v = node[key];
3467
- if (!v) continue;
3468
- if (Array.isArray(v)) {
3469
- for (const item of v) walkAst(item, visit);
3470
- } else if (typeof v === "object" && v.type) {
3471
- walkAst(v, visit);
3472
- }
3473
- }
3474
- }
3475
- function ensureNamedImport(program2, from, name) {
3476
- if (!program2 || program2.type !== "Program") return { changed: false };
3477
- const existing = findImportDeclaration(program2, from);
3478
- if (existing) {
3479
- const has = (existing.specifiers ?? []).some(
3480
- (s) => s?.type === "ImportSpecifier" && (s.imported?.name === name || s.imported?.value === name)
3481
- );
3482
- if (has) return { changed: false };
3483
- const spec = parseModule2(`import { ${name} } from "${from}";`).$ast.body?.[0]?.specifiers?.[0];
3484
- if (!spec) return { changed: false };
3485
- existing.specifiers = [...existing.specifiers ?? [], spec];
3486
- return { changed: true };
3487
- }
3488
- const importDecl = parseModule2(`import { ${name} } from "${from}";`).$ast.body?.[0];
3489
- if (!importDecl) return { changed: false };
3490
- const body = program2.body ?? [];
3491
- let insertAt = 0;
3492
- while (insertAt < body.length && isUseClientDirective(body[insertAt])) {
3493
- insertAt++;
3494
- }
3495
- while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
3496
- insertAt++;
3497
- }
3498
- program2.body.splice(insertAt, 0, importDecl);
3499
- return { changed: true };
3500
- }
3501
- function hasUILintProviderJsx(program2) {
3502
- let found = false;
3503
- walkAst(program2, (node) => {
3504
- if (found) return;
3505
- if (node.type !== "JSXElement") return;
3506
- const name = node.openingElement?.name;
3507
- if (name?.type === "JSXIdentifier" && name.name === "UILintProvider") {
3508
- found = true;
3509
- }
3510
- });
3511
- return found;
3512
- }
3513
- function wrapFirstChildrenExpressionWithProvider(program2) {
3514
- if (!program2 || program2.type !== "Program") return { changed: false };
3515
- if (hasUILintProviderJsx(program2)) return { changed: false };
3516
- const providerMod = parseModule2(
3517
- 'const __uilint_provider = (<UILintProvider enabled={process.env.NODE_ENV !== "production"}>{children}</UILintProvider>);'
3518
- );
3519
- const providerJsx = providerMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
3520
- if (!providerJsx || providerJsx.type !== "JSXElement")
3521
- return { changed: false };
3522
- let replaced = false;
3523
- walkAst(program2, (node) => {
3524
- if (replaced) return;
3525
- if (node.type === "JSXExpressionContainer" && node.expression?.type === "Identifier" && node.expression.name === "children") {
3526
- Object.keys(node).forEach((k) => delete node[k]);
3527
- Object.assign(node, providerJsx);
3528
- replaced = true;
3529
- }
3530
- });
3531
- if (!replaced) {
3532
- throw new Error("Could not find `{children}` in target file to wrap.");
3533
- }
3534
- return { changed: true };
3535
- }
3536
- function wrapFirstRenderCallArgumentWithProvider(program2) {
3537
- if (!program2 || program2.type !== "Program") return { changed: false };
3538
- if (hasUILintProviderJsx(program2)) return { changed: false };
3539
- const providerMod = parseModule2(
3540
- 'const __uilint_provider = (<UILintProvider enabled={process.env.NODE_ENV !== "production"}></UILintProvider>);'
3541
- );
3542
- const providerJsx = providerMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
3543
- if (!providerJsx || providerJsx.type !== "JSXElement")
3544
- return { changed: false };
3545
- providerJsx.children = providerJsx.children ?? [];
3546
- let wrapped = false;
3547
- walkAst(program2, (node) => {
3548
- if (wrapped) return;
3549
- if (node.type !== "CallExpression") return;
3550
- const callee = node.callee;
3551
- if (callee?.type !== "MemberExpression") return;
3552
- const prop = callee.property;
3553
- const isRender = prop?.type === "Identifier" && prop.name === "render" || prop?.type === "StringLiteral" && prop.value === "render" || prop?.type === "Literal" && prop.value === "render";
3554
- if (!isRender) return;
3555
- const arg0 = node.arguments?.[0];
3556
- if (!arg0) return;
3557
- if (arg0.type !== "JSXElement" && arg0.type !== "JSXFragment") return;
3558
- providerJsx.children = [arg0];
3559
- node.arguments[0] = providerJsx;
3560
- wrapped = true;
3561
- });
3562
- if (!wrapped) {
3563
- throw new Error(
3564
- "Could not find a `.render(<...>)` call to wrap. Expected a React entry like `createRoot(...).render(<App />)`."
3565
- );
3566
- }
3567
- return { changed: true };
3568
- }
3569
- async function installReactUILintOverlay(opts) {
3570
- const candidates = getDefaultCandidates(opts.projectPath, opts.appRoot);
3571
- if (!candidates.length) {
3572
- throw new Error(
3573
- `No suitable entry files found under ${opts.appRoot} (expected Next.js layout/page or Vite main/App).`
3574
- );
3575
- }
3576
- let chosen;
3577
- if (candidates.length > 1 && opts.confirmFileChoice) {
3578
- chosen = await opts.confirmFileChoice(candidates);
3579
- } else {
3580
- chosen = candidates[0];
3581
- }
3582
- const absTarget = join12(opts.projectPath, chosen);
3583
- const original = readFileSync8(absTarget, "utf-8");
3584
- let mod;
3585
- try {
3586
- mod = parseModule2(original);
3587
- } catch {
3588
- throw new Error(
3589
- `Unable to parse ${chosen} as JavaScript/TypeScript. Please update it manually.`
3590
- );
3591
- }
3592
- const program2 = mod.$ast;
3593
- const alreadyConfigured = !!findImportDeclaration(program2, "uilint-react") && hasUILintProviderJsx(program2);
3594
- let changed = false;
3595
- const importRes = ensureNamedImport(
3596
- program2,
3597
- "uilint-react",
3598
- "UILintProvider"
3599
- );
3600
- if (importRes.changed) changed = true;
3601
- const mode = opts.mode ?? "next";
3602
- const wrapRes = mode === "vite" ? wrapFirstRenderCallArgumentWithProvider(program2) : wrapFirstChildrenExpressionWithProvider(program2);
3603
- if (wrapRes.changed) changed = true;
3604
- const updated = changed ? generateCode2(mod).code : original;
3605
- const modified = updated !== original;
3606
- if (modified) {
3607
- writeFileSync4(absTarget, updated, "utf-8");
3608
- }
3609
- return {
3610
- targetFile: chosen,
3611
- modified,
3612
- alreadyConfigured: alreadyConfigured && !modified
3613
- };
3614
- }
3615
-
3616
- // src/utils/next-config-inject.ts
3617
- import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
3618
- import { join as join13 } from "path";
3619
- import { parseModule as parseModule3, generateCode as generateCode3 } from "magicast";
3620
- var CONFIG_EXTENSIONS2 = [".ts", ".mjs", ".js", ".cjs"];
3621
- function findNextConfigFile(projectPath) {
3622
- for (const ext of CONFIG_EXTENSIONS2) {
3623
- const configPath = join13(projectPath, `next.config${ext}`);
3624
- if (existsSync13(configPath)) {
3625
- return configPath;
3626
- }
3627
- }
3628
- return null;
3629
- }
3630
- function getNextConfigFilename(configPath) {
3631
- const parts = configPath.split("/");
3632
- return parts[parts.length - 1] || "next.config.ts";
3633
- }
3634
- function isIdentifier2(node, name) {
3635
- return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
3636
- }
3637
- function isStringLiteral2(node) {
3638
- return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
3639
- }
3640
- function ensureEsmWithJsxLocImport(program2) {
3641
- if (!program2 || program2.type !== "Program") return { changed: false };
3642
- const existing = (program2.body ?? []).find(
3643
- (s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin"
3644
- );
3645
- if (existing) {
3646
- const has = (existing.specifiers ?? []).some(
3647
- (sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "withJsxLoc" || sp.imported?.value === "withJsxLoc")
3648
- );
3649
- if (has) return { changed: false };
3650
- const spec = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0]?.specifiers?.[0];
3651
- if (!spec) return { changed: false };
3652
- existing.specifiers = [...existing.specifiers ?? [], spec];
3653
- return { changed: true };
3654
- }
3655
- const importDecl = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0];
3656
- if (!importDecl) return { changed: false };
3657
- const body = program2.body ?? [];
3658
- let insertAt = 0;
3659
- while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
3660
- insertAt++;
3661
- }
3662
- program2.body.splice(insertAt, 0, importDecl);
3663
- return { changed: true };
3664
- }
3665
- function ensureCjsWithJsxLocRequire(program2) {
3666
- if (!program2 || program2.type !== "Program") return { changed: false };
3667
- for (const stmt of program2.body ?? []) {
3668
- if (stmt?.type !== "VariableDeclaration") continue;
3669
- for (const decl of stmt.declarations ?? []) {
3670
- const init = decl?.init;
3671
- if (init?.type === "CallExpression" && isIdentifier2(init.callee, "require") && isStringLiteral2(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin") {
3672
- if (decl.id?.type === "ObjectPattern") {
3673
- const has = (decl.id.properties ?? []).some((p2) => {
3674
- if (p2?.type !== "ObjectProperty" && p2?.type !== "Property") return false;
3675
- return isIdentifier2(p2.key, "withJsxLoc");
3676
- });
3677
- if (has) return { changed: false };
3678
- const prop = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
3679
- if (!prop) return { changed: false };
3680
- decl.id.properties = [...decl.id.properties ?? [], prop];
3681
- return { changed: true };
3682
- }
3683
- return { changed: false };
3684
- }
3685
- }
3686
- }
3687
- const reqDecl = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0];
3688
- if (!reqDecl) return { changed: false };
3689
- program2.body.unshift(reqDecl);
3690
- return { changed: true };
3691
- }
3692
- function wrapEsmExportDefault(program2) {
3693
- if (!program2 || program2.type !== "Program") return { changed: false };
3694
- const exportDecl = (program2.body ?? []).find(
3695
- (s) => s?.type === "ExportDefaultDeclaration"
3696
- );
3697
- if (!exportDecl) return { changed: false };
3698
- const decl = exportDecl.declaration;
3699
- if (decl?.type === "CallExpression" && isIdentifier2(decl.callee, "withJsxLoc")) {
3700
- return { changed: false };
3701
- }
3702
- exportDecl.declaration = {
3703
- type: "CallExpression",
3704
- callee: { type: "Identifier", name: "withJsxLoc" },
3705
- arguments: [decl]
3706
- };
3707
- return { changed: true };
3708
- }
3709
- function wrapCjsModuleExports(program2) {
3710
- if (!program2 || program2.type !== "Program") return { changed: false };
3711
- for (const stmt of program2.body ?? []) {
3712
- if (!stmt || stmt.type !== "ExpressionStatement") continue;
3713
- const expr = stmt.expression;
3714
- if (!expr || expr.type !== "AssignmentExpression") continue;
3715
- const left = expr.left;
3716
- const right = expr.right;
3717
- const isModuleExports = left?.type === "MemberExpression" && isIdentifier2(left.object, "module") && isIdentifier2(left.property, "exports");
3718
- if (!isModuleExports) continue;
3719
- if (right?.type === "CallExpression" && isIdentifier2(right.callee, "withJsxLoc")) {
3720
- return { changed: false };
3721
- }
3722
- expr.right = {
3723
- type: "CallExpression",
3724
- callee: { type: "Identifier", name: "withJsxLoc" },
3725
- arguments: [right]
3726
- };
3727
- return { changed: true };
3728
- }
3729
- return { changed: false };
3730
- }
3731
- async function installJsxLocPlugin(opts) {
3732
- const configPath = findNextConfigFile(opts.projectPath);
3733
- if (!configPath) {
3734
- return { configFile: null, modified: false };
3735
- }
3736
- const configFilename = getNextConfigFilename(configPath);
3737
- const original = readFileSync9(configPath, "utf-8");
3738
- let mod;
3739
- try {
3740
- mod = parseModule3(original);
3741
- } catch {
3742
- return { configFile: configFilename, modified: false };
3743
- }
3744
- const program2 = mod.$ast;
3745
- const isCjs = configPath.endsWith(".cjs");
3746
- let changed = false;
3747
- if (isCjs) {
3748
- const reqRes = ensureCjsWithJsxLocRequire(program2);
3749
- if (reqRes.changed) changed = true;
3750
- const wrapRes = wrapCjsModuleExports(program2);
3751
- if (wrapRes.changed) changed = true;
3752
- } else {
3753
- const impRes = ensureEsmWithJsxLocImport(program2);
3754
- if (impRes.changed) changed = true;
3755
- const wrapRes = wrapEsmExportDefault(program2);
3756
- if (wrapRes.changed) changed = true;
3757
- }
3758
- const updated = changed ? generateCode3(mod).code : original;
3759
- if (updated !== original) {
3760
- writeFileSync5(configPath, updated, "utf-8");
3761
- return { configFile: configFilename, modified: true };
3762
- }
3763
- return { configFile: configFilename, modified: false };
3764
- }
3765
-
3766
- // src/utils/vite-config-inject.ts
3767
- import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
3768
- import { join as join14 } from "path";
3769
- import { parseModule as parseModule4, generateCode as generateCode4 } from "magicast";
3770
- var CONFIG_EXTENSIONS3 = [".ts", ".mjs", ".js", ".cjs"];
3771
- function findViteConfigFile2(projectPath) {
3772
- for (const ext of CONFIG_EXTENSIONS3) {
3773
- const configPath = join14(projectPath, `vite.config${ext}`);
3774
- if (existsSync14(configPath)) return configPath;
3775
- }
3776
- return null;
3777
- }
3778
- function getViteConfigFilename(configPath) {
3779
- const parts = configPath.split("/");
3780
- return parts[parts.length - 1] || "vite.config.ts";
3781
- }
3782
- function isIdentifier3(node, name) {
3783
- return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
3784
- }
3785
- function isStringLiteral3(node) {
3786
- return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
3787
- }
3788
- function unwrapExpression(expr) {
3789
- let e = expr;
3790
- while (e) {
3791
- if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
3792
- e = e.expression;
3793
- continue;
3794
- }
3795
- if (e.type === "TSSatisfiesExpression") {
3796
- e = e.expression;
3797
- continue;
3798
- }
3799
- if (e.type === "ParenthesizedExpression") {
3800
- e = e.expression;
3801
- continue;
3802
- }
3803
- break;
3804
- }
3805
- return e;
3806
- }
3807
- function findExportedConfigObjectExpression(mod) {
3808
- const program2 = mod?.$ast;
3809
- if (!program2 || program2.type !== "Program") return null;
3810
- for (const stmt of program2.body ?? []) {
3811
- if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
3812
- const decl = unwrapExpression(stmt.declaration);
3813
- if (!decl) break;
3814
- if (decl.type === "ObjectExpression") {
3815
- return { kind: "esm", objExpr: decl, program: program2 };
3816
- }
3817
- if (decl.type === "CallExpression" && isIdentifier3(decl.callee, "defineConfig") && unwrapExpression(decl.arguments?.[0])?.type === "ObjectExpression") {
3818
- return {
3819
- kind: "esm",
3820
- objExpr: unwrapExpression(decl.arguments?.[0]),
3821
- program: program2
3822
- };
3823
- }
3824
- break;
3825
- }
3826
- for (const stmt of program2.body ?? []) {
3827
- if (!stmt || stmt.type !== "ExpressionStatement") continue;
3828
- const expr = stmt.expression;
3829
- if (!expr || expr.type !== "AssignmentExpression") continue;
3830
- const left = expr.left;
3831
- const right = unwrapExpression(expr.right);
3832
- const isModuleExports = left?.type === "MemberExpression" && isIdentifier3(left.object, "module") && isIdentifier3(left.property, "exports");
3833
- if (!isModuleExports) continue;
3834
- if (right?.type === "ObjectExpression") {
3835
- return { kind: "cjs", objExpr: right, program: program2 };
3836
- }
3837
- if (right?.type === "CallExpression" && isIdentifier3(right.callee, "defineConfig") && unwrapExpression(right.arguments?.[0])?.type === "ObjectExpression") {
3838
- return {
3839
- kind: "cjs",
3840
- objExpr: unwrapExpression(right.arguments?.[0]),
3841
- program: program2
3842
- };
3843
- }
3844
- }
3845
- return null;
3846
- }
3847
- function getObjectProperty(obj, keyName) {
3848
- if (!obj || obj.type !== "ObjectExpression") return null;
3849
- for (const prop of obj.properties ?? []) {
3850
- if (!prop) continue;
3851
- if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
3852
- const key = prop.key;
3853
- const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral3(key) && key.value === keyName;
3854
- if (keyMatch) return prop;
3855
- }
3856
- return null;
3857
- }
3858
- function ensureEsmJsxLocImport(program2) {
3859
- if (!program2 || program2.type !== "Program") return { changed: false };
3860
- const existing = (program2.body ?? []).find(
3861
- (s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin/vite"
3862
- );
3863
- if (existing) {
3864
- const has = (existing.specifiers ?? []).some(
3865
- (sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "jsxLoc" || sp.imported?.value === "jsxLoc")
3866
- );
3867
- if (has) return { changed: false };
3868
- const spec = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0]?.specifiers?.[0];
3869
- if (!spec) return { changed: false };
3870
- existing.specifiers = [...existing.specifiers ?? [], spec];
3871
- return { changed: true };
3872
- }
3873
- const importDecl = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0];
3874
- if (!importDecl) return { changed: false };
3875
- const body = program2.body ?? [];
3876
- let insertAt = 0;
3877
- while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
3878
- insertAt++;
3879
- }
3880
- program2.body.splice(insertAt, 0, importDecl);
3881
- return { changed: true };
3882
- }
3883
- function ensureCjsJsxLocRequire(program2) {
3884
- if (!program2 || program2.type !== "Program") return { changed: false };
3885
- for (const stmt of program2.body ?? []) {
3886
- if (stmt?.type !== "VariableDeclaration") continue;
3887
- for (const decl of stmt.declarations ?? []) {
3888
- const init = decl?.init;
3889
- if (init?.type === "CallExpression" && isIdentifier3(init.callee, "require") && isStringLiteral3(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin/vite") {
3890
- if (decl.id?.type === "ObjectPattern") {
3891
- const has = (decl.id.properties ?? []).some((p2) => {
3892
- if (p2?.type !== "ObjectProperty" && p2?.type !== "Property") return false;
3893
- return isIdentifier3(p2.key, "jsxLoc");
3894
- });
3895
- if (has) return { changed: false };
3896
- const prop = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
3897
- if (!prop) return { changed: false };
3898
- decl.id.properties = [...decl.id.properties ?? [], prop];
3899
- return { changed: true };
3900
- }
3901
- return { changed: false };
3902
- }
3903
- }
3904
- }
3905
- const reqDecl = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0];
3906
- if (!reqDecl) return { changed: false };
3907
- program2.body.unshift(reqDecl);
3908
- return { changed: true };
3909
- }
3910
- function pluginsHasJsxLoc(arr) {
3911
- if (!arr || arr.type !== "ArrayExpression") return false;
3912
- for (const el of arr.elements ?? []) {
3913
- const e = unwrapExpression(el);
3914
- if (!e) continue;
3915
- if (e.type === "CallExpression" && isIdentifier3(e.callee, "jsxLoc")) return true;
3916
- }
3917
- return false;
3918
- }
3919
- function ensurePluginsContainsJsxLoc(configObj) {
3920
- const pluginsProp = getObjectProperty(configObj, "plugins");
3921
- if (!pluginsProp) {
3922
- const prop = parseModule4("export default { plugins: [jsxLoc()] };").$ast.body?.find((s) => s.type === "ExportDefaultDeclaration")?.declaration?.properties?.find((p2) => {
3923
- const k = p2?.key;
3924
- return k?.type === "Identifier" && k.name === "plugins" || isStringLiteral3(k) && k.value === "plugins";
3925
- });
3926
- if (!prop) return { changed: false };
3927
- configObj.properties = [...configObj.properties ?? [], prop];
3928
- return { changed: true };
3929
- }
3930
- const value = unwrapExpression(pluginsProp.value);
3931
- if (!value) return { changed: false };
3932
- if (value.type === "ArrayExpression") {
3933
- if (pluginsHasJsxLoc(value)) return { changed: false };
3934
- const jsxLocCall2 = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
3935
- if (!jsxLocCall2) return { changed: false };
3936
- value.elements.push(jsxLocCall2);
3937
- return { changed: true };
3938
- }
3939
- const jsxLocCall = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
3940
- if (!jsxLocCall) return { changed: false };
3941
- const spread = { type: "SpreadElement", argument: value };
3942
- pluginsProp.value = { type: "ArrayExpression", elements: [spread, jsxLocCall] };
3943
- return { changed: true };
3944
- }
3945
- async function installViteJsxLocPlugin(opts) {
3946
- const configPath = findViteConfigFile2(opts.projectPath);
3947
- if (!configPath) return { configFile: null, modified: false };
3948
- const configFilename = getViteConfigFilename(configPath);
3949
- const original = readFileSync10(configPath, "utf-8");
3950
- const isCjs = configPath.endsWith(".cjs");
3951
- let mod;
3952
- try {
3953
- mod = parseModule4(original);
3954
- } catch {
3955
- return { configFile: configFilename, modified: false };
3956
- }
3957
- const found = findExportedConfigObjectExpression(mod);
3958
- if (!found) return { configFile: configFilename, modified: false };
3959
- let changed = false;
3960
- if (isCjs) {
3961
- const reqRes = ensureCjsJsxLocRequire(found.program);
3962
- if (reqRes.changed) changed = true;
3963
- } else {
3964
- const impRes = ensureEsmJsxLocImport(found.program);
3965
- if (impRes.changed) changed = true;
3966
- }
3967
- const pluginsRes = ensurePluginsContainsJsxLoc(found.objExpr);
3968
- if (pluginsRes.changed) changed = true;
3969
- const updated = changed ? generateCode4(mod).code : original;
3970
- if (updated !== original) {
3971
- writeFileSync6(configPath, updated, "utf-8");
3972
- return { configFile: configFilename, modified: true };
3973
- }
3974
- return { configFile: configFilename, modified: false };
3975
- }
3976
-
3977
- // src/utils/next-routes.ts
3978
- import { existsSync as existsSync15 } from "fs";
3979
- import { mkdir, writeFile } from "fs/promises";
3980
- import { join as join15 } from "path";
3981
- var DEV_SOURCE_ROUTE_TS = `/**
3982
- * Dev-only API route for fetching source files
3983
- *
3984
- * This route allows the UILint overlay to fetch and display source code
3985
- * for components rendered on the page.
3986
- *
3987
- * Security:
3988
- * - Only available in development mode
3989
- * - Validates file path is within project root
3990
- * - Only allows specific file extensions
3991
- */
3992
-
3993
- import { NextRequest, NextResponse } from "next/server";
3994
- import { readFileSync, existsSync } from "fs";
3995
- import { resolve, relative, dirname, extname, sep } from "path";
3996
- import { fileURLToPath } from "url";
3997
-
3998
- export const runtime = "nodejs";
3999
-
4000
- // Allowed file extensions
4001
- const ALLOWED_EXTENSIONS = new Set([".tsx", ".ts", ".jsx", ".js", ".css"]);
4002
-
4003
- /**
4004
- * Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
4005
- *
4006
- * Why: In monorepos, process.cwd() might be the workspace root (it also has package.json),
4007
- * which would incorrectly store/read files under the wrong directory.
4008
- */
4009
- function findNextProjectRoot(): string {
4010
- // Prefer discovering via this route module's on-disk path.
4011
- // In Next, route code is executed from within ".next/server/...".
4012
- try {
4013
- const selfPath = fileURLToPath(import.meta.url);
4014
- const marker = sep + ".next" + sep;
4015
- const idx = selfPath.lastIndexOf(marker);
4016
- if (idx !== -1) {
4017
- return selfPath.slice(0, idx);
4018
- }
4019
- } catch {
4020
- // ignore
4021
- }
4022
-
4023
- // Fallback: walk up from cwd looking for .next/
4024
- let dir = process.cwd();
4025
- for (let i = 0; i < 20; i++) {
4026
- if (existsSync(resolve(dir, ".next"))) return dir;
4027
- const parent = dirname(dir);
4028
- if (parent === dir) break;
4029
- dir = parent;
4030
- }
4031
-
4032
- // Final fallback: cwd
4033
- return process.cwd();
4034
- }
4035
-
4036
- /**
4037
- * Validate that a path is within the allowed directory
4038
- */
4039
- function isPathWithinRoot(filePath: string, root: string): boolean {
4040
- const resolved = resolve(filePath);
4041
- const resolvedRoot = resolve(root);
4042
- return resolved.startsWith(resolvedRoot + "/") || resolved === resolvedRoot;
4043
- }
4044
-
4045
- /**
4046
- * Find workspace root by walking up looking for pnpm-workspace.yaml or .git
4047
- */
4048
- function findWorkspaceRoot(startDir: string): string {
4049
- let dir = startDir;
4050
- for (let i = 0; i < 10; i++) {
4051
- if (
4052
- existsSync(resolve(dir, "pnpm-workspace.yaml")) ||
4053
- existsSync(resolve(dir, ".git"))
4054
- ) {
4055
- return dir;
4056
- }
4057
- const parent = dirname(dir);
4058
- if (parent === dir) break;
4059
- dir = parent;
4060
- }
4061
- return startDir;
4062
- }
4063
-
4064
- export async function GET(request: NextRequest) {
4065
- // Block in production
4066
- if (process.env.NODE_ENV === "production") {
4067
- return NextResponse.json(
4068
- { error: "Not available in production" },
4069
- { status: 404 }
4070
- );
4071
- }
4072
-
4073
- const { searchParams } = new URL(request.url);
4074
- const filePath = searchParams.get("path");
4075
-
4076
- if (!filePath) {
4077
- return NextResponse.json(
4078
- { error: "Missing 'path' query parameter" },
4079
- { status: 400 }
4080
- );
4081
- }
4082
-
4083
- // Validate extension
4084
- const ext = extname(filePath).toLowerCase();
4085
- if (!ALLOWED_EXTENSIONS.has(ext)) {
4086
- return NextResponse.json(
4087
- { error: \`File extension '\${ext}' not allowed\` },
4088
- { status: 403 }
4089
- );
4090
- }
4091
-
4092
- // Find project root (prefer Next project root over workspace root)
4093
- const projectRoot = findNextProjectRoot();
4094
-
4095
- // Resolve the file path
4096
- const resolvedPath = resolve(filePath);
4097
-
4098
- // Security check: ensure path is within project root or workspace root
4099
- const workspaceRoot = findWorkspaceRoot(projectRoot);
4100
- const isWithinApp = isPathWithinRoot(resolvedPath, projectRoot);
4101
- const isWithinWorkspace = isPathWithinRoot(resolvedPath, workspaceRoot);
4102
-
4103
- if (!isWithinApp && !isWithinWorkspace) {
4104
- return NextResponse.json(
4105
- { error: "Path outside project directory" },
4106
- { status: 403 }
4107
- );
4108
- }
4109
-
4110
- // Check file exists
4111
- if (!existsSync(resolvedPath)) {
4112
- return NextResponse.json({ error: "File not found" }, { status: 404 });
4113
- }
4114
-
4115
- try {
4116
- const content = readFileSync(resolvedPath, "utf-8");
4117
- const relativePath = relative(workspaceRoot, resolvedPath);
4118
-
4119
- return NextResponse.json({
4120
- content,
4121
- relativePath,
4122
- projectRoot,
4123
- workspaceRoot,
4124
- });
4125
- } catch (error) {
4126
- console.error("[Dev Source API] Error reading file:", error);
4127
- return NextResponse.json({ error: "Failed to read file" }, { status: 500 });
4128
- }
4129
- }
4130
- `;
4131
- var SCREENSHOT_ROUTE_TS = `/**
4132
- * Dev-only API route for saving and retrieving vision analysis screenshots
4133
- *
4134
- * This route allows the UILint overlay to:
4135
- * - POST: Save screenshots and element manifests for vision analysis
4136
- * - GET: Retrieve screenshots or list available screenshots
4137
- *
4138
- * Security:
4139
- * - Only available in development mode
4140
- * - Saves to .uilint/screenshots/ directory within project
4141
- */
4142
-
4143
- import { NextRequest, NextResponse } from "next/server";
4144
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
4145
- import { resolve, join, dirname, basename, sep } from "path";
4146
- import { fileURLToPath } from "url";
4147
-
4148
- export const runtime = "nodejs";
4149
-
4150
- // Maximum screenshot size (10MB)
4151
- const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
4152
-
4153
- /**
4154
- * Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
4155
- */
4156
- function findNextProjectRoot(): string {
4157
- try {
4158
- const selfPath = fileURLToPath(import.meta.url);
4159
- const marker = sep + ".next" + sep;
4160
- const idx = selfPath.lastIndexOf(marker);
4161
- if (idx !== -1) {
4162
- return selfPath.slice(0, idx);
4163
- }
4164
- } catch {
4165
- // ignore
4166
- }
4167
-
4168
- let dir = process.cwd();
4169
- for (let i = 0; i < 20; i++) {
4170
- if (existsSync(resolve(dir, ".next"))) return dir;
4171
- const parent = dirname(dir);
4172
- if (parent === dir) break;
4173
- dir = parent;
4174
- }
4175
-
4176
- return process.cwd();
4177
- }
4178
-
4179
- /**
4180
- * Get the screenshots directory path, creating it if needed
4181
- */
4182
- function getScreenshotsDir(projectRoot: string): string {
4183
- const screenshotsDir = join(projectRoot, ".uilint", "screenshots");
4184
- if (!existsSync(screenshotsDir)) {
4185
- mkdirSync(screenshotsDir, { recursive: true });
4186
- }
4187
- return screenshotsDir;
4188
- }
4189
-
4190
- /**
4191
- * Validate filename to prevent path traversal
4192
- */
4193
- function isValidFilename(filename: string): boolean {
4194
- // Only allow alphanumeric, hyphens, underscores, and dots
4195
- // Must end with .png, .jpeg, .jpg, or .json
4196
- const validPattern = /^[a-zA-Z0-9_-]+\\.(png|jpeg|jpg|json)$/;
4197
- return validPattern.test(filename) && !filename.includes("..");
4198
- }
4199
-
4200
- /**
4201
- * POST: Save a screenshot and optionally its manifest
4202
- */
4203
- export async function POST(request: NextRequest) {
4204
- // Block in production
4205
- if (process.env.NODE_ENV === "production") {
4206
- return NextResponse.json(
4207
- { error: "Not available in production" },
4208
- { status: 404 }
4209
- );
4210
- }
4211
-
4212
- try {
4213
- const body = await request.json();
4214
- const { filename, imageData, manifest, analysisResult } = body;
4215
-
4216
- if (!filename) {
4217
- return NextResponse.json({ error: "Missing 'filename'" }, { status: 400 });
4218
- }
4219
-
4220
- // Validate filename
4221
- if (!isValidFilename(filename)) {
4222
- return NextResponse.json(
4223
- { error: "Invalid filename format" },
4224
- { status: 400 }
4225
- );
4226
- }
4227
-
4228
- // Allow "sidecar-only" updates (manifest/analysisResult) without re-sending image bytes.
4229
- const hasImageData = typeof imageData === "string" && imageData.length > 0;
4230
- const hasSidecar =
4231
- typeof manifest !== "undefined" || typeof analysisResult !== "undefined";
4232
-
4233
- if (!hasImageData && !hasSidecar) {
4234
- return NextResponse.json(
4235
- { error: "Nothing to save (provide imageData and/or manifest/analysisResult)" },
4236
- { status: 400 }
4237
- );
4238
- }
4239
-
4240
- // Check size (image only)
4241
- if (hasImageData && imageData.length > MAX_SCREENSHOT_SIZE) {
4242
- return NextResponse.json(
4243
- { error: "Screenshot too large (max 10MB)" },
4244
- { status: 413 }
4245
- );
4246
- }
4247
-
4248
- const projectRoot = findNextProjectRoot();
4249
- const screenshotsDir = getScreenshotsDir(projectRoot);
4250
-
4251
- const imagePath = join(screenshotsDir, filename);
4252
-
4253
- // Save the image (base64 data URL) if provided
4254
- if (hasImageData) {
4255
- const base64Data = imageData.includes(",")
4256
- ? imageData.split(",")[1]
4257
- : imageData;
4258
- writeFileSync(imagePath, Buffer.from(base64Data, "base64"));
4259
- }
4260
-
4261
- // Save manifest and analysis result as JSON sidecar
4262
- if (hasSidecar) {
4263
- const jsonFilename = filename.replace(/\\.(png|jpeg|jpg)$/, ".json");
4264
- const jsonPath = join(screenshotsDir, jsonFilename);
4265
-
4266
- // If a sidecar already exists, merge updates (lets us POST analysisResult later without re-sending image).
4267
- let existing: any = null;
4268
- if (existsSync(jsonPath)) {
4269
- try {
4270
- existing = JSON.parse(readFileSync(jsonPath, "utf-8"));
4271
- } catch {
4272
- existing = null;
4273
- }
4274
- }
4275
-
4276
- const routeFromAnalysis =
4277
- analysisResult && typeof analysisResult === "object"
4278
- ? (analysisResult as any).route
4279
- : undefined;
4280
- const issuesFromAnalysis =
4281
- analysisResult && typeof analysisResult === "object"
4282
- ? (analysisResult as any).issues
4283
- : undefined;
4284
-
4285
- const jsonData = {
4286
- ...(existing && typeof existing === "object" ? existing : {}),
4287
- timestamp: Date.now(),
4288
- filename,
4289
- screenshotFile: filename,
4290
- route:
4291
- typeof routeFromAnalysis === "string"
4292
- ? routeFromAnalysis
4293
- : (existing as any)?.route ?? null,
4294
- issues:
4295
- Array.isArray(issuesFromAnalysis)
4296
- ? issuesFromAnalysis
4297
- : (existing as any)?.issues ?? null,
4298
- manifest: typeof manifest === "undefined" ? existing?.manifest ?? null : manifest,
4299
- analysisResult:
4300
- typeof analysisResult === "undefined"
4301
- ? existing?.analysisResult ?? null
4302
- : analysisResult,
4303
- };
4304
- writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
4305
- }
4306
-
4307
- return NextResponse.json({
4308
- success: true,
4309
- path: imagePath,
4310
- projectRoot,
4311
- screenshotsDir,
4312
- });
4313
- } catch (error) {
4314
- console.error("[Screenshot API] Error saving screenshot:", error);
4315
- return NextResponse.json(
4316
- { error: "Failed to save screenshot" },
4317
- { status: 500 }
4318
- );
4319
- }
4320
- }
4321
-
4322
- /**
4323
- * GET: Retrieve a screenshot or list available screenshots
4324
- */
4325
- export async function GET(request: NextRequest) {
4326
- // Block in production
4327
- if (process.env.NODE_ENV === "production") {
4328
- return NextResponse.json(
4329
- { error: "Not available in production" },
4330
- { status: 404 }
4331
- );
4332
- }
4333
-
4334
- const { searchParams } = new URL(request.url);
4335
- const filename = searchParams.get("filename");
4336
- const list = searchParams.get("list");
4337
-
4338
- const projectRoot = findNextProjectRoot();
4339
- const screenshotsDir = getScreenshotsDir(projectRoot);
4340
-
4341
- // List mode: return all screenshots
4342
- if (list === "true") {
4343
- try {
4344
- const files = readdirSync(screenshotsDir);
4345
- const screenshots = files
4346
- .filter((f) => /\\.(png|jpeg|jpg)$/.test(f))
4347
- .map((f) => {
4348
- const jsonFile = f.replace(/\\.(png|jpeg|jpg)$/, ".json");
4349
- const jsonPath = join(screenshotsDir, jsonFile);
4350
- let metadata = null;
4351
- if (existsSync(jsonPath)) {
4352
- try {
4353
- metadata = JSON.parse(readFileSync(jsonPath, "utf-8"));
4354
- } catch {
4355
- // Ignore parse errors
4356
- }
4357
- }
4358
- return {
4359
- filename: f,
4360
- metadata,
4361
- };
4362
- })
4363
- .sort((a, b) => {
4364
- // Sort by timestamp descending (newest first)
4365
- const aTime = a.metadata?.timestamp || 0;
4366
- const bTime = b.metadata?.timestamp || 0;
4367
- return bTime - aTime;
4368
- });
4369
-
4370
- return NextResponse.json({ screenshots, projectRoot, screenshotsDir });
4371
- } catch (error) {
4372
- console.error("[Screenshot API] Error listing screenshots:", error);
4373
- return NextResponse.json(
4374
- { error: "Failed to list screenshots" },
4375
- { status: 500 }
4376
- );
4377
- }
4378
- }
4379
-
4380
- // Retrieve mode: get specific screenshot
4381
- if (!filename) {
4382
- return NextResponse.json(
4383
- { error: "Missing 'filename' parameter" },
4384
- { status: 400 }
4385
- );
4386
- }
4387
-
4388
- if (!isValidFilename(filename)) {
4389
- return NextResponse.json(
4390
- { error: "Invalid filename format" },
4391
- { status: 400 }
4392
- );
4393
- }
4394
-
4395
- const filePath = join(screenshotsDir, filename);
4396
-
4397
- if (!existsSync(filePath)) {
4398
- return NextResponse.json(
4399
- { error: "Screenshot not found" },
4400
- { status: 404 }
4401
- );
4402
- }
4403
-
4404
- try {
4405
- const content = readFileSync(filePath);
4406
-
4407
- // Determine content type
4408
- const ext = filename.split(".").pop()?.toLowerCase();
4409
- const contentType =
4410
- ext === "json"
4411
- ? "application/json"
4412
- : ext === "png"
4413
- ? "image/png"
4414
- : "image/jpeg";
4415
-
4416
- if (ext === "json") {
4417
- return NextResponse.json(JSON.parse(content.toString()));
4418
- }
4419
-
4420
- return new NextResponse(content, {
4421
- headers: {
4422
- "Content-Type": contentType,
4423
- "Cache-Control": "no-cache",
4424
- },
4425
- });
4426
- } catch (error) {
4427
- console.error("[Screenshot API] Error reading screenshot:", error);
4428
- return NextResponse.json(
4429
- { error: "Failed to read screenshot" },
4430
- { status: 500 }
4431
- );
4432
- }
4433
- }
4434
- `;
4435
- async function writeRouteFile(absPath, relPath, content, opts) {
4436
- if (existsSync15(absPath) && !opts.force) return;
4437
- await writeFile(absPath, content, "utf-8");
4438
- }
4439
- async function installNextUILintRoutes(opts) {
4440
- const baseRel = join15(opts.appRoot, "api", ".uilint");
4441
- const baseAbs = join15(opts.projectPath, baseRel);
4442
- await mkdir(join15(baseAbs, "source"), { recursive: true });
4443
- await writeRouteFile(
4444
- join15(baseAbs, "source", "route.ts"),
4445
- join15(baseRel, "source", "route.ts"),
4446
- DEV_SOURCE_ROUTE_TS,
4447
- opts
4448
- );
4449
- await mkdir(join15(baseAbs, "screenshots"), { recursive: true });
4450
- await writeRouteFile(
4451
- join15(baseAbs, "screenshots", "route.ts"),
4452
- join15(baseRel, "screenshots", "route.ts"),
4453
- SCREENSHOT_ROUTE_TS,
4454
- opts
4455
- );
4456
- }
4457
-
4458
- // src/commands/install/execute.ts
4459
- async function executeAction(action, options) {
4460
- const { dryRun = false } = options;
4461
- try {
4462
- switch (action.type) {
4463
- case "create_directory": {
4464
- if (dryRun) {
4465
- return {
4466
- action,
4467
- success: true,
4468
- wouldDo: `Create directory: ${action.path}`
4469
- };
4470
- }
4471
- if (!existsSync16(action.path)) {
4472
- mkdirSync3(action.path, { recursive: true });
4473
- }
4474
- return { action, success: true };
4475
- }
4476
- case "create_file": {
4477
- if (dryRun) {
4478
- return {
4479
- action,
4480
- success: true,
4481
- wouldDo: `Create file: ${action.path}${action.permissions ? ` (mode: ${action.permissions.toString(8)})` : ""}`
4482
- };
4483
- }
4484
- const dir = dirname9(action.path);
4485
- if (!existsSync16(dir)) {
4486
- mkdirSync3(dir, { recursive: true });
4487
- }
4488
- writeFileSync7(action.path, action.content, "utf-8");
4489
- if (action.permissions) {
4490
- chmodSync(action.path, action.permissions);
4491
- }
4492
- return { action, success: true };
4493
- }
4494
- case "merge_json": {
4495
- if (dryRun) {
4496
- return {
4497
- action,
4498
- success: true,
4499
- wouldDo: `Merge JSON into: ${action.path}`
4500
- };
4501
- }
4502
- let existing = {};
4503
- if (existsSync16(action.path)) {
4504
- try {
4505
- existing = JSON.parse(readFileSync11(action.path, "utf-8"));
4506
- } catch {
4507
- }
4508
- }
4509
- const merged = deepMerge(existing, action.merge);
4510
- const dir = dirname9(action.path);
4511
- if (!existsSync16(dir)) {
4512
- mkdirSync3(dir, { recursive: true });
4513
- }
4514
- writeFileSync7(action.path, JSON.stringify(merged, null, 2), "utf-8");
4515
- return { action, success: true };
4516
- }
4517
- case "delete_file": {
4518
- if (dryRun) {
4519
- return {
4520
- action,
4521
- success: true,
4522
- wouldDo: `Delete file: ${action.path}`
4523
- };
4524
- }
4525
- if (existsSync16(action.path)) {
4526
- unlinkSync(action.path);
4527
- }
4528
- return { action, success: true };
4529
- }
4530
- case "append_to_file": {
4531
- if (dryRun) {
4532
- return {
4533
- action,
4534
- success: true,
4535
- wouldDo: `Append to file: ${action.path}`
4536
- };
4537
- }
4538
- if (existsSync16(action.path)) {
4539
- const content = readFileSync11(action.path, "utf-8");
4540
- if (action.ifNotContains && content.includes(action.ifNotContains)) {
4541
- return { action, success: true };
4542
- }
4543
- writeFileSync7(action.path, content + action.content, "utf-8");
4544
- }
4545
- return { action, success: true };
4546
- }
4547
- case "inject_eslint": {
4548
- return await executeInjectEslint(action, options);
4549
- }
4550
- case "inject_react": {
4551
- return await executeInjectReact(action, options);
4552
- }
4553
- case "inject_next_config": {
4554
- return await executeInjectNextConfig(action, options);
4555
- }
4556
- case "inject_vite_config": {
4557
- return await executeInjectViteConfig(action, options);
4558
- }
4559
- case "install_next_routes": {
4560
- return await executeInstallNextRoutes(action, options);
4561
- }
4562
- default: {
4563
- const _exhaustive = action;
4564
- return {
4565
- action: _exhaustive,
4566
- success: false,
4567
- error: `Unknown action type`
4568
- };
4569
- }
4570
- }
4571
- } catch (error) {
4572
- return {
4573
- action,
4574
- success: false,
4575
- error: error instanceof Error ? error.message : String(error)
4576
- };
4577
- }
4578
- }
4579
- async function executeInjectEslint(action, options) {
4580
- const { dryRun = false } = options;
4581
- if (dryRun) {
4582
- return {
4583
- action,
4584
- success: true,
4585
- wouldDo: `Inject ESLint rules into: ${action.configPath}`
4586
- };
4587
- }
4588
- const result = await installEslintPlugin({
4589
- projectPath: action.packagePath,
4590
- selectedRules: action.rules,
4591
- force: !action.hasExistingRules,
4592
- // Don't force if already has rules
4593
- // Auto-confirm for execute phase (choices were made during planning)
4594
- confirmAddMissingRules: async () => true
4595
- });
4596
- return {
4597
- action,
4598
- success: result.configFile !== null && result.configured,
4599
- error: result.configFile === null ? "No ESLint config found" : result.configured ? void 0 : result.error ?? "Failed to configure uilint in ESLint config"
4600
- };
4601
- }
4602
- async function executeInjectReact(action, options) {
4603
- const { dryRun = false } = options;
4604
- if (dryRun) {
4605
- return {
4606
- action,
4607
- success: true,
4608
- wouldDo: `Inject UILintProvider into React app: ${action.projectPath}`
4609
- };
4610
- }
4611
- const result = await installReactUILintOverlay({
4612
- projectPath: action.projectPath,
4613
- appRoot: action.appRoot,
4614
- mode: action.mode,
4615
- force: false,
4616
- // Auto-select first choice for execute phase
4617
- confirmFileChoice: async (choices) => choices[0]
4618
- });
4619
- const success = result.modified || result.alreadyConfigured === true;
4620
- return {
4621
- action,
4622
- success,
4623
- error: success ? void 0 : "Failed to configure React overlay"
4624
- };
4625
- }
4626
- async function executeInjectViteConfig(action, options) {
4627
- const { dryRun = false } = options;
4628
- if (dryRun) {
4629
- return {
4630
- action,
4631
- success: true,
4632
- wouldDo: `Inject jsx-loc-plugin into vite.config: ${action.projectPath}`
4633
- };
4634
- }
4635
- const result = await installViteJsxLocPlugin({
4636
- projectPath: action.projectPath,
4637
- force: false
4638
- });
4639
- return {
4640
- action,
4641
- success: result.modified || result.configFile !== null,
4642
- error: result.configFile === null ? "No vite.config found" : void 0
4643
- };
4644
- }
4645
- async function executeInjectNextConfig(action, options) {
4646
- const { dryRun = false } = options;
4647
- if (dryRun) {
4648
- return {
4649
- action,
4650
- success: true,
4651
- wouldDo: `Inject jsx-loc-plugin into next.config: ${action.projectPath}`
4652
- };
4653
- }
4654
- const result = await installJsxLocPlugin({
4655
- projectPath: action.projectPath,
4656
- force: false
4657
- });
4658
- return {
4659
- action,
4660
- success: result.modified || result.configFile !== null,
4661
- error: result.configFile === null ? "No next.config found" : void 0
4662
- };
4663
- }
4664
- async function executeInstallNextRoutes(action, options) {
4665
- const { dryRun = false } = options;
4666
- if (dryRun) {
4667
- return {
4668
- action,
4669
- success: true,
4670
- wouldDo: `Install Next.js API routes: ${action.projectPath}`
4671
- };
4672
- }
4673
- await installNextUILintRoutes({
4674
- projectPath: action.projectPath,
4675
- appRoot: action.appRoot,
4676
- force: false
4677
- });
4678
- return { action, success: true };
4679
- }
4680
- function deepMerge(target, source) {
4681
- const result = { ...target };
4682
- for (const key of Object.keys(source)) {
4683
- const sourceVal = source[key];
4684
- const targetVal = target[key];
4685
- if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) {
4686
- result[key] = deepMerge(
4687
- targetVal,
4688
- sourceVal
4689
- );
4690
- } else {
4691
- result[key] = sourceVal;
4692
- }
4693
- }
4694
- return result;
4695
- }
4696
- function buildSummary(actionsPerformed, dependencyResults, items) {
4697
- const filesCreated = [];
4698
- const filesModified = [];
4699
- const filesDeleted = [];
4700
- const eslintTargets = [];
4701
- let nextApp;
4702
- let viteApp;
4703
- for (const result of actionsPerformed) {
4704
- if (!result.success) continue;
4705
- const { action } = result;
4706
- switch (action.type) {
4707
- case "create_file":
4708
- filesCreated.push(action.path);
4709
- break;
4710
- case "merge_json":
4711
- case "append_to_file":
4712
- filesModified.push(action.path);
4713
- break;
4714
- case "delete_file":
4715
- filesDeleted.push(action.path);
4716
- break;
4717
- case "inject_eslint":
4718
- filesModified.push(action.configPath);
4719
- eslintTargets.push({
4720
- displayName: action.packagePath,
4721
- configFile: action.configPath
4722
- });
4723
- break;
4724
- case "inject_react":
4725
- if (action.mode === "vite") {
4726
- viteApp = { entryRoot: action.appRoot };
4727
- } else {
4728
- nextApp = { appRoot: action.appRoot };
4729
- }
4730
- break;
4731
- case "install_next_routes":
4732
- nextApp = { appRoot: action.appRoot };
4733
- break;
4734
- }
4735
- }
4736
- const dependenciesInstalled = [];
4737
- for (const result of dependencyResults) {
4738
- if (result.success && !result.skipped) {
4739
- dependenciesInstalled.push({
4740
- packagePath: result.install.packagePath,
4741
- packages: result.install.packages
4742
- });
4743
- }
4744
- }
4745
- return {
4746
- installedItems: items,
4747
- filesCreated,
4748
- filesModified,
4749
- filesDeleted,
4750
- dependenciesInstalled,
4751
- eslintTargets,
4752
- nextApp,
4753
- viteApp
4754
- };
4755
- }
4756
- async function execute(plan, options = {}) {
4757
- const { dryRun = false, installDependencies: installDependencies2 = installDependencies } = options;
4758
- const actionsPerformed = [];
4759
- const dependencyResults = [];
4760
- for (const action of plan.actions) {
4761
- const result = await executeAction(action, options);
4762
- actionsPerformed.push(result);
4763
- }
4764
- for (const dep of plan.dependencies) {
4765
- if (dryRun) {
4766
- dependencyResults.push({
4767
- install: dep,
4768
- success: true,
4769
- skipped: true
4770
- });
4771
- continue;
4772
- }
4773
- try {
4774
- await installDependencies2(
4775
- dep.packageManager,
4776
- dep.packagePath,
4777
- dep.packages
4778
- );
4779
- dependencyResults.push({
4780
- install: dep,
4781
- success: true
4782
- });
4783
- } catch (error) {
4784
- dependencyResults.push({
4785
- install: dep,
4786
- success: false,
4787
- error: error instanceof Error ? error.message : String(error)
4788
- });
4789
- }
4790
- }
4791
- const actionsFailed = actionsPerformed.filter((r) => !r.success);
4792
- const depsFailed = dependencyResults.filter((r) => !r.success);
4793
- const success = actionsFailed.length === 0 && depsFailed.length === 0;
4794
- const items = [];
4795
- for (const result of actionsPerformed) {
4796
- if (!result.success) continue;
4797
- const { action } = result;
4798
- if (action.type === "create_file") {
4799
- if (action.path.includes("mcp.json")) items.push("mcp");
4800
- if (action.path.includes("hooks.json")) items.push("hooks");
4801
- if (action.path.includes("genstyleguide.md")) items.push("genstyleguide");
4802
- if (action.path.includes("genrules.md")) items.push("genrules");
4803
- if (action.path.includes("/skills/") && action.path.includes("SKILL.md")) items.push("skill");
4804
- }
4805
- if (action.type === "inject_eslint") items.push("eslint");
4806
- if (action.type === "install_next_routes") items.push("next");
4807
- if (action.type === "inject_react") {
4808
- items.push(action.mode === "vite" ? "vite" : "next");
4809
- }
4810
- if (action.type === "inject_vite_config") items.push("vite");
4811
- }
4812
- const uniqueItems = [...new Set(items)];
4813
- const summary = buildSummary(
4814
- actionsPerformed,
4815
- dependencyResults,
4816
- uniqueItems
4817
- );
4818
- return {
4819
- success,
4820
- actionsPerformed,
4821
- dependencyResults,
4822
- summary
4823
- };
4824
- }
4825
-
4826
- // src/commands/install/prompter.ts
4827
- import { ruleRegistry } from "uilint-eslint";
4828
- var cliPrompter = {
4829
- async selectInstallItems() {
4830
- return multiselect2({
4831
- message: "What would you like to install?",
4832
- options: [
4833
- {
4834
- value: "eslint",
4835
- label: "ESLint plugin",
4836
- hint: "Installs uilint-eslint and configures eslint.config.*"
4837
- },
4838
- {
4839
- value: "next",
4840
- label: "UI overlay",
4841
- hint: "Installs routes + UILintProvider (Alt+Click to inspect)"
4842
- },
4843
- {
4844
- value: "vite",
4845
- label: "UI overlay (Vite)",
4846
- hint: "Installs jsx-loc-plugin + UILintProvider (Alt+Click to inspect)"
4847
- },
4848
- {
4849
- value: "genstyleguide",
4850
- label: "/genstyleguide command",
4851
- hint: "Adds .cursor/commands/genstyleguide.md"
4852
- },
4853
- {
4854
- value: "mcp",
4855
- label: "MCP Server",
4856
- hint: "Recommended - works with any MCP-compatible agent"
4857
- },
4858
- {
4859
- value: "hooks",
4860
- label: "Cursor Hooks",
4861
- hint: "Auto-validates UI files when the agent stops"
4862
- },
4863
- {
4864
- value: "genrules",
4865
- label: "/genrules command",
4866
- hint: "Adds .cursor/commands/genrules.md for ESLint rule generation"
4867
- },
4868
- {
4869
- value: "skill",
4870
- label: "UI Consistency Agent Skill",
4871
- hint: "Cursor agent skill for generating ESLint rules from UI patterns"
4872
- }
4873
- ],
4874
- required: true,
4875
- initialValues: ["eslint", "next", "genstyleguide", "skill"]
4876
- });
4877
- },
4878
- async confirmMcpMerge() {
4879
- return confirm2({
4880
- message: `${pc.dim(
4881
- ".cursor/mcp.json"
4882
- )} already exists. Merge UILint config?`,
4883
- initialValue: true
4884
- });
4885
- },
4886
- async confirmHooksMerge() {
4887
- return confirm2({
4888
- message: `${pc.dim(
4889
- ".cursor/hooks.json"
4890
- )} already exists. Merge UILint hooks?`,
4891
- initialValue: true
4892
- });
4893
- },
4894
- async selectNextApp(apps) {
4895
- const chosen = await select2({
4896
- message: "Which Next.js App Router project should UILint install into?",
4897
- options: apps.map((app) => ({
4898
- value: app.projectPath,
4899
- label: app.projectPath
4900
- })),
4901
- initialValue: apps[0].projectPath
4902
- });
4903
- return apps.find((a) => a.projectPath === chosen) || apps[0];
4904
- },
4905
- async selectViteApp(apps) {
4906
- const chosen = await select2({
4907
- message: "Which Vite + React project should UILint install into?",
4908
- options: apps.map((app) => ({
4909
- value: app.projectPath,
4910
- label: app.projectPath
4911
- })),
4912
- initialValue: apps[0].projectPath
4913
- });
4914
- return apps.find((a) => a.projectPath === chosen) || apps[0];
4915
- },
4916
- async selectEslintPackages(packages) {
4917
- if (packages.length === 1) {
4918
- const confirmed = await confirm2({
4919
- message: `Install ESLint plugin in ${pc.cyan(
4920
- packages[0].displayPath
4921
- )}?`,
4922
- initialValue: true
4923
- });
4924
- return confirmed ? [packages[0].path] : [];
4925
- }
4926
- const initialValues = packages.filter((p2) => p2.isFrontend).map((p2) => p2.path).slice(0, 1);
4927
- return multiselect2({
4928
- message: "Which packages should have ESLint plugin installed?",
4929
- options: packages.map(formatPackageOption),
4930
- required: false,
4931
- initialValues: initialValues.length > 0 ? initialValues : [packages[0].path]
4932
- });
4933
- },
4934
- async selectEslintRules() {
4935
- const selectedRuleIds = await multiselect2({
4936
- message: "Which rules would you like to enable?",
4937
- options: ruleRegistry.map((rule) => ({
4938
- value: rule.id,
4939
- label: rule.name,
4940
- hint: rule.description
4941
- })),
4942
- required: false,
4943
- initialValues: ruleRegistry.filter(
4944
- (r) => r.category === "static" || !r.requiresStyleguide
4945
- ).map((r) => r.id)
4946
- });
4947
- return ruleRegistry.filter(
4948
- (r) => selectedRuleIds.includes(r.id)
4949
- );
4950
- },
4951
- async selectEslintRuleSeverity() {
4952
- return select2({
4953
- message: "How strict should the selected ESLint rules be?",
4954
- options: [
4955
- {
4956
- value: "warn",
4957
- label: "Warn (recommended)",
4958
- hint: "Safer default while you dial in your styleguide + rules"
4959
- },
4960
- {
4961
- value: "error",
4962
- label: "Error (strict)",
4963
- hint: "Make selected rules fail CI"
4964
- },
4965
- {
4966
- value: "defaults",
4967
- label: "Use rule defaults",
4968
- hint: "Some rules are warn, some are error (as defined by uilint-eslint)"
4969
- }
4970
- ],
4971
- initialValue: "warn"
4972
- });
4973
- },
4974
- async confirmCustomizeRuleOptions() {
4975
- return confirm2({
4976
- message: "Customize individual rule options? (spacing scale, thresholds, etc.)",
4977
- initialValue: false
4978
- });
4979
- },
4980
- async configureRuleOptions(rule) {
4981
- if (!rule.optionSchema || rule.optionSchema.fields.length === 0) {
4982
- return void 0;
4983
- }
4984
- const options = {};
4985
- for (const field of rule.optionSchema.fields) {
4986
- const value = await promptForField(field, rule.name);
4987
- if (value !== void 0) {
4988
- options[field.key] = value;
4989
- }
4990
- }
4991
- return Object.keys(options).length > 0 ? options : void 0;
4992
- }
4993
- };
4994
- async function promptForField(field, ruleName) {
4995
- const message = `${pc.cyan(ruleName)} - ${field.label}`;
4996
- switch (field.type) {
4997
- case "text": {
4998
- const result = await text2({
4999
- message,
5000
- placeholder: field.placeholder,
5001
- defaultValue: typeof field.defaultValue === "string" ? field.defaultValue : Array.isArray(field.defaultValue) ? field.defaultValue.join(", ") : String(field.defaultValue)
5002
- });
5003
- if (field.key === "scale" && typeof result === "string") {
5004
- const scale = result.split(",").map((s) => parseFloat(s.trim())).filter((n) => !isNaN(n));
5005
- return scale.length > 0 ? scale : field.defaultValue;
5006
- }
5007
- return result || field.defaultValue;
5008
- }
5009
- case "number": {
5010
- const result = await text2({
5011
- message,
5012
- placeholder: field.placeholder || String(field.defaultValue),
5013
- defaultValue: String(field.defaultValue)
5014
- });
5015
- const num = parseFloat(result);
5016
- return isNaN(num) ? field.defaultValue : num;
5017
- }
5018
- case "boolean": {
5019
- return await confirm2({
5020
- message,
5021
- initialValue: Boolean(field.defaultValue)
5022
- });
5023
- }
5024
- case "select": {
5025
- if (!field.options) {
5026
- return field.defaultValue;
5027
- }
5028
- const stringOptions = field.options.map(
5029
- (opt) => ({
5030
- value: String(opt.value),
5031
- label: opt.label
5032
- })
5033
- );
5034
- const result = await select2({
5035
- message,
5036
- options: stringOptions,
5037
- initialValue: String(field.defaultValue)
5038
- });
5039
- const originalOpt = field.options.find(
5040
- (opt) => String(opt.value) === result
5041
- );
5042
- return originalOpt?.value ?? result;
5043
- }
5044
- case "multiselect": {
5045
- if (!field.options) {
5046
- return field.defaultValue;
5047
- }
5048
- const stringOptions = field.options.map(
5049
- (opt) => ({
5050
- value: String(opt.value),
5051
- label: opt.label
5052
- })
5053
- );
5054
- const result = await multiselect2({
5055
- message,
5056
- options: stringOptions,
5057
- initialValues: Array.isArray(field.defaultValue) ? field.defaultValue.map((v) => String(v)) : [String(field.defaultValue)]
5058
- });
5059
- return result.map((selected) => {
5060
- const originalOpt = field.options.find(
5061
- (opt) => String(opt.value) === selected
5062
- );
5063
- return originalOpt?.value ?? selected;
5064
- });
5065
- }
5066
- default:
5067
- return field.defaultValue;
5068
- }
5069
- }
5070
- async function gatherChoices(state, options, prompter) {
5071
- let items;
5072
- const hasExplicitFlags = options.mcp !== void 0 || options.hooks !== void 0 || options.genstyleguide !== void 0 || options.genrules !== void 0 || options.skill !== void 0 || options.routes !== void 0 || options.react !== void 0;
5073
- if (hasExplicitFlags || options.eslint) {
5074
- items = [];
5075
- if (options.mcp) items.push("mcp");
5076
- if (options.hooks) items.push("hooks");
5077
- if (options.genstyleguide) items.push("genstyleguide");
5078
- if (options.genrules) items.push("genrules");
5079
- if (options.skill) items.push("skill");
5080
- if (options.routes || options.react) items.push("next");
5081
- if (options.eslint) items.push("eslint");
5082
- } else if (options.mode) {
5083
- items = [];
5084
- if (options.mode === "mcp" || options.mode === "both") items.push("mcp");
5085
- if (options.mode === "hooks" || options.mode === "both")
5086
- items.push("hooks");
5087
- items.push("genstyleguide");
5088
- } else {
5089
- items = await prompter.selectInstallItems();
5090
- }
5091
- let mcpMerge = true;
5092
- if (items.includes("mcp") && state.mcp.exists && !options.force) {
5093
- mcpMerge = await prompter.confirmMcpMerge();
5094
- }
5095
- let hooksMerge = true;
5096
- if (items.includes("hooks") && state.hooks.exists && !options.force) {
5097
- hooksMerge = await prompter.confirmHooksMerge();
5098
- }
5099
- let nextChoices;
5100
- if (items.includes("next")) {
5101
- if (state.nextApps.length === 0) {
5102
- throw new Error(
5103
- "Could not find a Next.js App Router app root (expected app/ or src/app/). Run this from your Next.js project root."
5104
- );
5105
- } else if (state.nextApps.length === 1) {
5106
- nextChoices = {
5107
- projectPath: state.nextApps[0].projectPath,
5108
- detection: state.nextApps[0].detection
5109
- };
5110
- } else {
5111
- const selected = await prompter.selectNextApp(state.nextApps);
5112
- nextChoices = {
5113
- projectPath: selected.projectPath,
5114
- detection: selected.detection
5115
- };
5116
- }
5117
- }
5118
- let viteChoices;
5119
- if (items.includes("vite")) {
5120
- if (state.viteApps.length === 0) {
5121
- throw new Error(
5122
- "Could not find a Vite + React project (expected vite.config.* + react deps). Run this from your Vite project root."
5123
- );
5124
- } else if (state.viteApps.length === 1) {
5125
- viteChoices = {
5126
- projectPath: state.viteApps[0].projectPath,
5127
- detection: state.viteApps[0].detection
5128
- };
5129
- } else {
5130
- const selected = await prompter.selectViteApp(state.viteApps);
5131
- viteChoices = {
5132
- projectPath: selected.projectPath,
5133
- detection: selected.detection
5134
- };
5135
- }
5136
- }
5137
- let eslintChoices;
5138
- if (items.includes("eslint")) {
5139
- const packagesWithEslint = state.packages.filter(
5140
- (p2) => p2.eslintConfigPath !== null
5141
- );
5142
- if (packagesWithEslint.length === 0) {
5143
- throw new Error(
5144
- "No packages with eslint.config.{ts,mjs,js,cjs} found. Create an ESLint config first."
5145
- );
5146
- }
5147
- const packagePaths = await prompter.selectEslintPackages(
5148
- packagesWithEslint
5149
- );
5150
- if (packagePaths.length > 0) {
5151
- let selectedRules = await prompter.selectEslintRules();
5152
- const severity = await prompter.selectEslintRuleSeverity();
5153
- if (severity !== "defaults") {
5154
- selectedRules = selectedRules.map((rule) => ({
5155
- ...rule,
5156
- defaultSeverity: severity
5157
- }));
5158
- }
5159
- const hasConfigurableRules = selectedRules.some(
5160
- (r) => r.optionSchema && r.optionSchema.fields.length > 0
5161
- );
5162
- if (hasConfigurableRules) {
5163
- const customizeOptions = await prompter.confirmCustomizeRuleOptions();
5164
- if (customizeOptions) {
5165
- selectedRules = await configureRuleOptions(selectedRules, prompter);
5166
- }
5167
- }
5168
- eslintChoices = { packagePaths, selectedRules };
5169
- }
5170
- }
5171
- return {
5172
- items,
5173
- mcpMerge,
5174
- hooksMerge,
5175
- next: nextChoices,
5176
- vite: viteChoices,
5177
- eslint: eslintChoices
5178
- };
5179
- }
5180
- async function configureRuleOptions(rules, prompter) {
5181
- const configured = [];
5182
- for (const rule of rules) {
5183
- if (rule.optionSchema && rule.optionSchema.fields.length > 0) {
5184
- const options = await prompter.configureRuleOptions(rule);
5185
- if (options) {
5186
- const existingOptions = rule.defaultOptions && rule.defaultOptions.length > 0 ? rule.defaultOptions[0] : {};
5187
- configured.push({
5188
- ...rule,
5189
- defaultOptions: [{ ...existingOptions, ...options }]
5190
- });
5191
- } else {
5192
- configured.push(rule);
5193
- }
5194
- } else {
5195
- configured.push(rule);
5196
- }
5197
- }
5198
- return configured;
5199
- }
5200
-
5201
- // src/commands/install.ts
5202
- function displayResults(result) {
5203
- const { summary } = result;
5204
- const installedItems = [];
5205
- if (summary.installedItems.includes("mcp")) {
5206
- installedItems.push(`${pc.cyan("MCP Server")} \u2192 .cursor/mcp.json`);
5207
- }
5208
- if (summary.installedItems.includes("hooks")) {
5209
- installedItems.push(`${pc.cyan("Hooks")} \u2192 .cursor/hooks.json`);
5210
- installedItems.push(` ${pc.dim("\u251C")} uilint-session-start.sh`);
5211
- installedItems.push(` ${pc.dim("\u251C")} uilint-track.sh`);
5212
- installedItems.push(` ${pc.dim("\u2514")} uilint-session-end.sh`);
5213
- }
5214
- if (summary.installedItems.includes("genstyleguide")) {
5215
- installedItems.push(
5216
- `${pc.cyan("Command")} \u2192 .cursor/commands/genstyleguide.md`
5217
- );
5218
- }
5219
- if (summary.installedItems.includes("genrules")) {
5220
- installedItems.push(`${pc.cyan("Command")} \u2192 .cursor/commands/genrules.md`);
5221
- }
5222
- if (summary.nextApp) {
5223
- installedItems.push(
5224
- `${pc.cyan("Next Routes")} \u2192 ${pc.dim(
5225
- join16(summary.nextApp.appRoot, "api/.uilint")
5226
- )}`
5227
- );
5228
- installedItems.push(
5229
- `${pc.cyan("Next Overlay")} \u2192 ${pc.dim("<UILintProvider> injected")}`
5230
- );
5231
- installedItems.push(
5232
- `${pc.cyan("JSX Loc Plugin")} \u2192 ${pc.dim(
5233
- "next.config wrapped with withJsxLoc"
5234
- )}`
5235
- );
5236
- }
5237
- if (summary.viteApp) {
5238
- installedItems.push(
5239
- `${pc.cyan("Vite Overlay")} \u2192 ${pc.dim("<UILintProvider> injected")}`
5240
- );
5241
- installedItems.push(
5242
- `${pc.cyan("JSX Loc Plugin")} \u2192 ${pc.dim(
5243
- "vite.config plugins patched with jsxLoc()"
5244
- )}`
5245
- );
5246
- }
5247
- if (summary.eslintTargets.length > 0) {
5248
- installedItems.push(
5249
- `${pc.cyan("ESLint Plugin")} \u2192 installed in ${summary.eslintTargets.length} package(s)`
5250
- );
5251
- for (let i = 0; i < summary.eslintTargets.length; i++) {
5252
- const isLast = i === summary.eslintTargets.length - 1;
5253
- const prefix = isLast ? "\u2514" : "\u251C";
5254
- installedItems.push(
5255
- ` ${pc.dim(prefix)} ${summary.eslintTargets[i].displayName}`
5256
- );
5257
- }
5258
- installedItems.push(`${pc.cyan("Available Rules")}:`);
5259
- for (let i = 0; i < ruleRegistry2.length; i++) {
5260
- const isLast = i === ruleRegistry2.length - 1;
5261
- const prefix = isLast ? "\u2514" : "\u251C";
5262
- const rule = ruleRegistry2[i];
5263
- const suffix = rule.id === "semantic" ? ` ${pc.dim("(LLM-powered)")}` : "";
5264
- installedItems.push(
5265
- ` ${pc.dim(prefix)} ${pc.cyan(`uilint/${rule.id}`)}${suffix}`
5266
- );
5267
- }
5268
- }
5269
- note2(installedItems.join("\n"), "Installed");
5270
- const steps = [];
5271
- const hasStyleguide = summary.filesCreated.some(
5272
- (f) => f.includes("styleguide.md")
5273
- );
5274
- if (!hasStyleguide) {
5275
- steps.push(`Create a styleguide: ${pc.cyan("/genstyleguide")}`);
5276
- }
5277
- if (summary.installedItems.includes("mcp") || summary.installedItems.includes("hooks") || summary.installedItems.includes("genstyleguide")) {
5278
- steps.push("Restart Cursor to load the new configuration");
5279
- }
5280
- if (summary.installedItems.includes("mcp")) {
5281
- steps.push(`The MCP server exposes: ${pc.dim("scan_file, scan_snippet")}`);
5282
- }
5283
- if (summary.installedItems.includes("hooks")) {
5284
- steps.push("Hooks will auto-validate UI files when the agent stops");
5285
- }
5286
- if (summary.nextApp) {
5287
- steps.push(
5288
- "Run your Next.js dev server - use Alt+Click on any element to inspect"
5289
- );
5290
- }
5291
- if (summary.viteApp) {
5292
- steps.push(
5293
- "Run your Vite dev server - use Alt+Click on any element to inspect"
5294
- );
5295
- }
5296
- if (summary.eslintTargets.length > 0) {
5297
- steps.push(`Run ${pc.cyan("npx eslint src/")} to check for issues`);
5298
- steps.push(
5299
- `For real-time overlay integration, run ${pc.cyan(
5300
- "uilint serve"
5301
- )} alongside your dev server`
5302
- );
5303
- }
5304
- if (steps.length > 0) {
5305
- note2(steps.join("\n"), "Next Steps");
5306
- }
5307
- }
5308
- async function install(options = {}, prompter = cliPrompter, executeOptions = {}) {
5309
- const projectPath = process.cwd();
5310
- intro2("Setup Wizard");
5311
- logInfo("Analyzing project...");
5312
- const state = await analyze2(projectPath);
5313
- const choices = await gatherChoices(state, options, prompter);
5314
- if (choices.items.length === 0) {
5315
- logWarning("No items selected for installation");
5316
- outro2("Nothing to install");
5317
- return {
5318
- success: true,
5319
- actionsPerformed: [],
5320
- dependencyResults: [],
5321
- summary: {
5322
- installedItems: [],
5323
- filesCreated: [],
5324
- filesModified: [],
5325
- filesDeleted: [],
5326
- dependenciesInstalled: [],
5327
- eslintTargets: []
5328
- }
5329
- };
5330
- }
5331
- const plan = createPlan(state, choices, { force: options.force });
5332
- logInfo("Installing...");
5333
- const result = await withSpinner("Running installation", async () => {
5334
- return execute(plan, executeOptions);
5335
- });
5336
- const failedActions = result.actionsPerformed.filter((r) => !r.success);
5337
- const failedDeps = result.dependencyResults.filter((r) => !r.success);
5338
- if (failedActions.length > 0) {
5339
- for (const failed of failedActions) {
5340
- logWarning(`Failed: ${failed.action.type} - ${failed.error}`);
5341
- }
5342
- }
5343
- if (failedDeps.length > 0) {
5344
- for (const failed of failedDeps) {
5345
- logWarning(
5346
- `Failed to install dependencies in ${failed.install.packagePath}: ${failed.error}`
5347
- );
5348
- }
5349
- }
5350
- displayResults(result);
5351
- if (result.success) {
5352
- outro2("UILint installed successfully!");
5353
- } else {
5354
- outro2("UILint installation completed with some errors");
5355
- }
5356
- return result;
5357
- }
5358
-
5359
- // src/commands/serve.ts
5360
- import { existsSync as existsSync18, statSync as statSync4, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
5361
- import { createRequire as createRequire3 } from "module";
5362
- import { dirname as dirname11, resolve as resolve5, relative as relative4, join as join18, parse as parse2 } from "path";
5363
- import { WebSocketServer, WebSocket } from "ws";
5364
- import { watch } from "chokidar";
5365
- import {
5366
- findWorkspaceRoot as findWorkspaceRoot6,
5367
- getVisionAnalyzer as getCoreVisionAnalyzer
5368
- } from "uilint-core/node";
5369
-
5370
- // src/utils/vision-run.ts
5371
- import { dirname as dirname10, join as join17, parse } from "path";
5372
- import { existsSync as existsSync17, statSync as statSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync8 } from "fs";
5373
- import {
5374
- ensureOllamaReady as ensureOllamaReady5,
5375
- findStyleGuidePath as findStyleGuidePath4,
5376
- findUILintStyleGuideUpwards as findUILintStyleGuideUpwards3,
5377
- readStyleGuide as readStyleGuide4,
5378
- VisionAnalyzer,
5379
- UILINT_DEFAULT_VISION_MODEL
5380
- } from "uilint-core/node";
5381
- async function resolveVisionStyleGuide(args) {
5382
- const projectPath = args.projectPath;
5383
- const startDir = args.startDir ?? projectPath;
5384
- if (args.styleguide) {
5385
- const styleguideArg = resolvePathSpecifier(args.styleguide, projectPath);
5386
- if (existsSync17(styleguideArg)) {
5387
- const stat = statSync3(styleguideArg);
5388
- if (stat.isFile()) {
5389
- return {
5390
- styleguideLocation: styleguideArg,
5391
- styleGuide: await readStyleGuide4(styleguideArg)
5392
- };
5393
- }
5394
- if (stat.isDirectory()) {
5395
- const found = findStyleGuidePath4(styleguideArg);
5396
- return {
5397
- styleguideLocation: found,
5398
- styleGuide: found ? await readStyleGuide4(found) : null
5399
- };
5400
- }
5401
- }
5402
- return { styleGuide: null, styleguideLocation: null };
5403
- }
5404
- const upwards = findUILintStyleGuideUpwards3(startDir);
5405
- const fallback = upwards ?? findStyleGuidePath4(projectPath);
5406
- return {
5407
- styleguideLocation: fallback,
5408
- styleGuide: fallback ? await readStyleGuide4(fallback) : null
5409
- };
5410
- }
5411
- var ollamaReadyOnce = /* @__PURE__ */ new Map();
5412
- async function ensureOllamaReadyCached(params) {
5413
- const key = `${params.baseUrl}::${params.model}`;
5414
- const existing = ollamaReadyOnce.get(key);
5415
- if (existing) return existing;
5416
- const p2 = ensureOllamaReady5({ model: params.model, baseUrl: params.baseUrl }).then(() => void 0).catch((e) => {
5417
- ollamaReadyOnce.delete(key);
5418
- throw e;
5419
- });
5420
- ollamaReadyOnce.set(key, p2);
5421
- return p2;
5422
- }
5423
- function writeVisionDebugDump(params) {
5424
- const resolvedDirOrFile = resolvePathSpecifier(
5425
- params.dumpPath,
5426
- process.cwd()
5427
- );
5428
- const safeStamp = params.now.toISOString().replace(/[:.]/g, "-");
5429
- const dumpFile = resolvedDirOrFile.endsWith(".json") || resolvedDirOrFile.endsWith(".jsonl") ? resolvedDirOrFile : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;
5430
- mkdirSync4(dirname10(dumpFile), { recursive: true });
5431
- writeFileSync8(
5432
- dumpFile,
5433
- JSON.stringify(
5434
- {
5435
- version: 1,
5436
- timestamp: params.now.toISOString(),
5437
- runtime: params.runtime,
5438
- metadata: params.metadata ?? null,
5439
- inputs: {
5440
- imageBase64: params.includeSensitive ? params.inputs.imageBase64 : "(omitted; set debugDumpIncludeSensitive=true)",
5441
- manifest: params.inputs.manifest,
5442
- styleguideLocation: params.inputs.styleguideLocation,
5443
- styleGuide: params.includeSensitive ? params.inputs.styleGuide : "(omitted; set debugDumpIncludeSensitive=true)"
5444
- }
5445
- },
5446
- null,
5447
- 2
5448
- ),
5449
- "utf-8"
5450
- );
5451
- return dumpFile;
5452
- }
5453
- async function runVisionAnalysis(args) {
5454
- const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;
5455
- const baseUrl = args.baseUrl ?? "http://localhost:11434";
5456
- let styleGuide = null;
5457
- let styleguideLocation = null;
5458
- if (args.styleGuide !== void 0) {
5459
- styleGuide = args.styleGuide;
5460
- styleguideLocation = args.styleguideLocation ?? null;
5461
- } else {
5462
- args.onPhase?.("Resolving styleguide...");
5463
- const resolved = await resolveVisionStyleGuide({
5464
- projectPath: args.projectPath,
5465
- styleguide: args.styleguide,
5466
- startDir: args.styleguideStartDir
5467
- });
5468
- styleGuide = resolved.styleGuide;
5469
- styleguideLocation = resolved.styleguideLocation;
5470
- }
5471
- if (!args.skipEnsureOllama) {
5472
- args.onPhase?.("Preparing Ollama...");
5473
- await ensureOllamaReadyCached({ model: visionModel, baseUrl });
5474
- }
5475
- if (args.debugDump) {
5476
- writeVisionDebugDump({
5477
- dumpPath: args.debugDump,
5478
- now: /* @__PURE__ */ new Date(),
5479
- runtime: { visionModel, baseUrl },
5480
- inputs: {
5481
- imageBase64: args.imageBase64,
5482
- manifest: args.manifest,
5483
- styleguideLocation,
5484
- styleGuide
5485
- },
5486
- includeSensitive: Boolean(args.debugDumpIncludeSensitive),
5487
- metadata: args.debugDumpMetadata
5488
- });
5489
- }
5490
- const analyzer = args.analyzer ?? new VisionAnalyzer({
5491
- baseUrl: args.baseUrl,
5492
- visionModel
5493
- });
5494
- args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);
5495
- const result = await analyzer.analyzeScreenshot(
5496
- args.imageBase64,
5497
- args.manifest,
5498
- {
5499
- styleGuide,
5500
- onProgress: args.onProgress
5501
- }
5502
- );
5503
- args.onPhase?.(
5504
- `Done (${result.issues.length} issues, ${result.analysisTime}ms)`
5505
- );
5506
- return {
5507
- issues: result.issues,
5508
- analysisTime: result.analysisTime,
5509
- // Prompt is available in newer uilint-core versions; keep this resilient across versions.
5510
- prompt: result.prompt,
5511
- rawResponse: result.rawResponse,
5512
- styleguideLocation,
5513
- visionModel,
5514
- baseUrl
5515
- };
5516
- }
5517
- function writeVisionMarkdownReport(args) {
5518
- const p2 = parse(args.imagePath);
5519
- const outPath = args.outPath ?? join17(p2.dir, `${p2.name || p2.base}.vision.md`);
5520
- const lines = [];
5521
- lines.push(`# UILint Vision Report`);
5522
- lines.push(``);
5523
- lines.push(`- Image: \`${p2.base}\``);
5524
- if (args.route) lines.push(`- Route: \`${args.route}\``);
5525
- if (typeof args.timestamp === "number") {
5526
- lines.push(`- Timestamp: \`${new Date(args.timestamp).toISOString()}\``);
5527
- }
5528
- if (args.visionModel) lines.push(`- Model: \`${args.visionModel}\``);
5529
- if (args.baseUrl) lines.push(`- Ollama baseUrl: \`${args.baseUrl}\``);
5530
- if (typeof args.analysisTimeMs === "number")
5531
- lines.push(`- Analysis time: \`${args.analysisTimeMs}ms\``);
5532
- lines.push(`- Generated: \`${(/* @__PURE__ */ new Date()).toISOString()}\``);
5533
- lines.push(``);
5534
- if (args.metadata && Object.keys(args.metadata).length > 0) {
5535
- lines.push(`## Metadata`);
5536
- lines.push(``);
5537
- lines.push("```json");
5538
- lines.push(JSON.stringify(args.metadata, null, 2));
5539
- lines.push("```");
5540
- lines.push(``);
5541
- }
5542
- lines.push(`## Prompt`);
5543
- lines.push(``);
5544
- lines.push("```text");
5545
- lines.push((args.prompt ?? "").trim());
5546
- lines.push("```");
5547
- lines.push(``);
5548
- lines.push(`## Raw Response`);
5549
- lines.push(``);
5550
- lines.push("```text");
5551
- lines.push((args.rawResponse ?? "").trim());
5552
- lines.push("```");
5553
- lines.push(``);
5554
- const content = lines.join("\n");
5555
- mkdirSync4(dirname10(outPath), { recursive: true });
5556
- writeFileSync8(outPath, content, "utf-8");
5557
- return { outPath, content };
5558
- }
5559
-
5560
- // src/commands/serve.ts
5561
- function pickAppRoot(params) {
5562
- const { cwd, workspaceRoot } = params;
5563
- if (detectNextAppRouter(cwd)) return cwd;
5564
- const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
5565
- if (matches.length === 0) return cwd;
5566
- if (matches.length === 1) return matches[0].projectPath;
5567
- const containing = matches.find(
5568
- (m) => cwd === m.projectPath || cwd.startsWith(m.projectPath + "/")
5569
- );
5570
- if (containing) return containing.projectPath;
5571
- return matches[0].projectPath;
5572
- }
5573
- var cache = /* @__PURE__ */ new Map();
5574
- var eslintInstances = /* @__PURE__ */ new Map();
5575
- var visionAnalyzer = null;
5576
- function getVisionAnalyzerInstance() {
5577
- if (!visionAnalyzer) {
5578
- visionAnalyzer = getCoreVisionAnalyzer();
5579
- }
5580
- return visionAnalyzer;
5581
- }
5582
- var serverAppRootForVision = process.cwd();
5583
- function isValidScreenshotFilename(filename) {
5584
- const validPattern = /^[a-zA-Z0-9_-]+\.(png|jpeg|jpg)$/;
5585
- return validPattern.test(filename) && !filename.includes("..");
1665
+ var serverAppRootForVision = process.cwd();
1666
+ function isValidScreenshotFilename(filename) {
1667
+ const validPattern = /^[a-zA-Z0-9_-]+\.(png|jpeg|jpg)$/;
1668
+ return validPattern.test(filename) && !filename.includes("..");
5586
1669
  }
5587
1670
  var resolvedPathCache = /* @__PURE__ */ new Map();
5588
1671
  var subscriptions = /* @__PURE__ */ new Map();
5589
1672
  var fileWatcher = null;
5590
1673
  var connectedClients = 0;
5591
- var localRequire = createRequire3(import.meta.url);
1674
+ var localRequire = createRequire(import.meta.url);
5592
1675
  function buildLineStarts(code) {
5593
1676
  const starts = [0];
5594
1677
  for (let i = 0; i < code.length; i++) {
@@ -5648,7 +1731,7 @@ function mapMessageToDataLoc(params) {
5648
1731
  }
5649
1732
  return void 0;
5650
1733
  }
5651
- var ESLINT_CONFIG_FILES2 = [
1734
+ var ESLINT_CONFIG_FILES = [
5652
1735
  // Flat config (ESLint v9+)
5653
1736
  "eslint.config.js",
5654
1737
  "eslint.config.mjs",
@@ -5665,11 +1748,11 @@ var ESLINT_CONFIG_FILES2 = [
5665
1748
  function findESLintCwd(startDir) {
5666
1749
  let dir = startDir;
5667
1750
  for (let i = 0; i < 30; i++) {
5668
- for (const cfg of ESLINT_CONFIG_FILES2) {
5669
- if (existsSync18(join18(dir, cfg))) return dir;
1751
+ for (const cfg of ESLINT_CONFIG_FILES) {
1752
+ if (existsSync5(join4(dir, cfg))) return dir;
5670
1753
  }
5671
- if (existsSync18(join18(dir, "package.json"))) return dir;
5672
- const parent = dirname11(dir);
1754
+ if (existsSync5(join4(dir, "package.json"))) return dir;
1755
+ const parent = dirname6(dir);
5673
1756
  if (parent === dir) break;
5674
1757
  dir = parent;
5675
1758
  }
@@ -5682,7 +1765,7 @@ function normalizeDataLocFilePath(absoluteFilePath, projectCwd) {
5682
1765
  const abs = normalizePathSlashes(resolve5(absoluteFilePath));
5683
1766
  const cwd = normalizePathSlashes(resolve5(projectCwd));
5684
1767
  if (abs === cwd || abs.startsWith(cwd + "/")) {
5685
- return normalizePathSlashes(relative4(cwd, abs));
1768
+ return normalizePathSlashes(relative(cwd, abs));
5686
1769
  }
5687
1770
  return abs;
5688
1771
  }
@@ -5694,25 +1777,25 @@ function resolveRequestedFilePath(filePath) {
5694
1777
  if (cached) return cached;
5695
1778
  const cwd = process.cwd();
5696
1779
  const fromCwd = resolve5(cwd, filePath);
5697
- if (existsSync18(fromCwd)) {
1780
+ if (existsSync5(fromCwd)) {
5698
1781
  resolvedPathCache.set(filePath, fromCwd);
5699
1782
  return fromCwd;
5700
1783
  }
5701
- const wsRoot = findWorkspaceRoot6(cwd);
1784
+ const wsRoot = findWorkspaceRoot4(cwd);
5702
1785
  const fromWs = resolve5(wsRoot, filePath);
5703
- if (existsSync18(fromWs)) {
1786
+ if (existsSync5(fromWs)) {
5704
1787
  resolvedPathCache.set(filePath, fromWs);
5705
1788
  return fromWs;
5706
1789
  }
5707
1790
  for (const top of ["apps", "packages"]) {
5708
- const base = join18(wsRoot, top);
5709
- if (!existsSync18(base)) continue;
1791
+ const base = join4(wsRoot, top);
1792
+ if (!existsSync5(base)) continue;
5710
1793
  try {
5711
- const entries = readdirSync5(base, { withFileTypes: true });
1794
+ const entries = readdirSync(base, { withFileTypes: true });
5712
1795
  for (const ent of entries) {
5713
1796
  if (!ent.isDirectory()) continue;
5714
1797
  const p2 = resolve5(base, ent.name, filePath);
5715
- if (existsSync18(p2)) {
1798
+ if (existsSync5(p2)) {
5716
1799
  resolvedPathCache.set(filePath, p2);
5717
1800
  return p2;
5718
1801
  }
@@ -5727,7 +1810,7 @@ async function getESLintForProject(projectCwd) {
5727
1810
  const cached = eslintInstances.get(projectCwd);
5728
1811
  if (cached) return cached;
5729
1812
  try {
5730
- const req = createRequire3(join18(projectCwd, "package.json"));
1813
+ const req = createRequire(join4(projectCwd, "package.json"));
5731
1814
  const mod = req("eslint");
5732
1815
  const ESLintCtor = mod?.ESLint ?? mod?.default?.ESLint ?? mod?.default ?? mod;
5733
1816
  if (!ESLintCtor) return null;
@@ -5740,13 +1823,13 @@ async function getESLintForProject(projectCwd) {
5740
1823
  }
5741
1824
  async function lintFile(filePath, onProgress) {
5742
1825
  const absolutePath = resolveRequestedFilePath(filePath);
5743
- if (!existsSync18(absolutePath)) {
1826
+ if (!existsSync5(absolutePath)) {
5744
1827
  onProgress(`File not found: ${pc.dim(absolutePath)}`);
5745
1828
  return [];
5746
1829
  }
5747
1830
  const mtimeMs = (() => {
5748
1831
  try {
5749
- return statSync4(absolutePath).mtimeMs;
1832
+ return statSync3(absolutePath).mtimeMs;
5750
1833
  } catch {
5751
1834
  return 0;
5752
1835
  }
@@ -5756,7 +1839,7 @@ async function lintFile(filePath, onProgress) {
5756
1839
  onProgress("Cache hit (unchanged)");
5757
1840
  return cached.issues;
5758
1841
  }
5759
- const fileDir = dirname11(absolutePath);
1842
+ const fileDir = dirname6(absolutePath);
5760
1843
  const projectCwd = findESLintCwd(fileDir);
5761
1844
  onProgress(`Resolving ESLint project... ${pc.dim(projectCwd)}`);
5762
1845
  const eslint = await getESLintForProject(projectCwd);
@@ -5779,7 +1862,7 @@ async function lintFile(filePath, onProgress) {
5779
1862
  let codeLength = 0;
5780
1863
  try {
5781
1864
  onProgress("Building JSX map...");
5782
- const code = readFileSync12(absolutePath, "utf-8");
1865
+ const code = readFileSync2(absolutePath, "utf-8");
5783
1866
  codeLength = code.length;
5784
1867
  lineStarts = buildLineStarts(code);
5785
1868
  spans = buildJsxElementSpans(code, dataLocFile);
@@ -5861,9 +1944,9 @@ async function handleMessage(ws, data) {
5861
1944
  });
5862
1945
  const startedAt = Date.now();
5863
1946
  const resolved = resolveRequestedFilePath(filePath);
5864
- if (!existsSync18(resolved)) {
1947
+ if (!existsSync5(resolved)) {
5865
1948
  const cwd = process.cwd();
5866
- const wsRoot = findWorkspaceRoot6(cwd);
1949
+ const wsRoot = findWorkspaceRoot4(cwd);
5867
1950
  logWarning(
5868
1951
  [
5869
1952
  `${pc.dim("[ws]")} File not found for request`,
@@ -6023,14 +2106,14 @@ async function handleMessage(ws, data) {
6023
2106
  )}`
6024
2107
  );
6025
2108
  } else {
6026
- const screenshotsDir = join18(
2109
+ const screenshotsDir = join4(
6027
2110
  serverAppRootForVision,
6028
2111
  ".uilint",
6029
2112
  "screenshots"
6030
2113
  );
6031
- const imagePath = join18(screenshotsDir, screenshotFile);
2114
+ const imagePath = join4(screenshotsDir, screenshotFile);
6032
2115
  try {
6033
- if (!existsSync18(imagePath)) {
2116
+ if (!existsSync5(imagePath)) {
6034
2117
  logWarning(
6035
2118
  `Skipping vision report write: screenshot file not found ${pc.dim(
6036
2119
  imagePath
@@ -6138,7 +2221,7 @@ function handleFileChange(filePath) {
6138
2221
  async function serve(options) {
6139
2222
  const port = options.port || 9234;
6140
2223
  const cwd = process.cwd();
6141
- const wsRoot = findWorkspaceRoot6(cwd);
2224
+ const wsRoot = findWorkspaceRoot4(cwd);
6142
2225
  const appRoot = pickAppRoot({ cwd, workspaceRoot: wsRoot });
6143
2226
  serverAppRootForVision = appRoot;
6144
2227
  logInfo(`Workspace root: ${pc.dim(wsRoot)}`);
@@ -6180,22 +2263,22 @@ async function serve(options) {
6180
2263
  `UILint WebSocket server running on ${pc.cyan(`ws://localhost:${port}`)}`
6181
2264
  );
6182
2265
  logInfo("Press Ctrl+C to stop");
6183
- await new Promise((resolve8) => {
2266
+ await new Promise((resolve7) => {
6184
2267
  process.on("SIGINT", () => {
6185
2268
  logInfo("Shutting down...");
6186
2269
  wss.close();
6187
2270
  fileWatcher?.close();
6188
- resolve8();
2271
+ resolve7();
6189
2272
  });
6190
2273
  });
6191
2274
  }
6192
2275
 
6193
2276
  // src/commands/vision.ts
6194
- import { dirname as dirname12, resolve as resolve6, join as join19 } from "path";
2277
+ import { dirname as dirname7, resolve as resolve6, join as join5 } from "path";
6195
2278
  import {
6196
- existsSync as existsSync19,
6197
- readFileSync as readFileSync13,
6198
- readdirSync as readdirSync6
2279
+ existsSync as existsSync6,
2280
+ readFileSync as readFileSync3,
2281
+ readdirSync as readdirSync2
6199
2282
  } from "fs";
6200
2283
  import {
6201
2284
  ensureOllamaReady as ensureOllamaReady6,
@@ -6207,9 +2290,9 @@ function envTruthy3(name) {
6207
2290
  if (!v) return false;
6208
2291
  return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
6209
2292
  }
6210
- function preview3(text3, maxLen) {
6211
- if (text3.length <= maxLen) return text3;
6212
- return text3.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text3.slice(-maxLen);
2293
+ function preview3(text2, maxLen) {
2294
+ if (text2.length <= maxLen) return text2;
2295
+ return text2.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text2.slice(-maxLen);
6213
2296
  }
6214
2297
  function debugEnabled3(options) {
6215
2298
  return Boolean(options.debug) || envTruthy3("UILINT_DEBUG");
@@ -6240,17 +2323,17 @@ function debugLog3(enabled, message, obj) {
6240
2323
  function findScreenshotsDirUpwards(startDir) {
6241
2324
  let dir = startDir;
6242
2325
  for (let i = 0; i < 20; i++) {
6243
- const candidate = join19(dir, ".uilint", "screenshots");
6244
- if (existsSync19(candidate)) return candidate;
6245
- const parent = dirname12(dir);
2326
+ const candidate = join5(dir, ".uilint", "screenshots");
2327
+ if (existsSync6(candidate)) return candidate;
2328
+ const parent = dirname7(dir);
6246
2329
  if (parent === dir) break;
6247
2330
  dir = parent;
6248
2331
  }
6249
2332
  return null;
6250
2333
  }
6251
2334
  function listScreenshotSidecars(dirPath) {
6252
- if (!existsSync19(dirPath)) return [];
6253
- const entries = readdirSync6(dirPath).filter((f) => f.endsWith(".json")).map((f) => join19(dirPath, f));
2335
+ if (!existsSync6(dirPath)) return [];
2336
+ const entries = readdirSync2(dirPath).filter((f) => f.endsWith(".json")).map((f) => join5(dirPath, f));
6254
2337
  const out = [];
6255
2338
  for (const p2 of entries) {
6256
2339
  try {
@@ -6279,11 +2362,11 @@ function listScreenshotSidecars(dirPath) {
6279
2362
  return out;
6280
2363
  }
6281
2364
  function readImageAsBase64(imagePath) {
6282
- const bytes = readFileSync13(imagePath);
2365
+ const bytes = readFileSync3(imagePath);
6283
2366
  return { base64: bytes.toString("base64"), sizeBytes: bytes.byteLength };
6284
2367
  }
6285
2368
  function loadJsonFile(filePath) {
6286
- const raw = readFileSync13(filePath, "utf-8");
2369
+ const raw = readFileSync3(filePath, "utf-8");
6287
2370
  return JSON.parse(raw);
6288
2371
  }
6289
2372
  function formatIssuesText(issues) {
@@ -6357,13 +2440,13 @@ async function vision(options) {
6357
2440
  await flushLangfuse();
6358
2441
  process.exit(1);
6359
2442
  }
6360
- if (imagePath && !existsSync19(imagePath)) {
2443
+ if (imagePath && !existsSync6(imagePath)) {
6361
2444
  throw new Error(`Image not found: ${imagePath}`);
6362
2445
  }
6363
- if (sidecarPath && !existsSync19(sidecarPath)) {
2446
+ if (sidecarPath && !existsSync6(sidecarPath)) {
6364
2447
  throw new Error(`Sidecar not found: ${sidecarPath}`);
6365
2448
  }
6366
- if (manifestFilePath && !existsSync19(manifestFilePath)) {
2449
+ if (manifestFilePath && !existsSync6(manifestFilePath)) {
6367
2450
  throw new Error(`Manifest file not found: ${manifestFilePath}`);
6368
2451
  }
6369
2452
  const sidecar = sidecarPath ? loadJsonFile(sidecarPath) : null;
@@ -6388,7 +2471,7 @@ async function vision(options) {
6388
2471
  const resolved = await resolveVisionStyleGuide({
6389
2472
  projectPath,
6390
2473
  styleguide: options.styleguide,
6391
- startDir: startPath ? dirname12(startPath) : projectPath
2474
+ startDir: startPath ? dirname7(startPath) : projectPath
6392
2475
  });
6393
2476
  styleGuide = resolved.styleGuide;
6394
2477
  styleguideLocation = resolved.styleguideLocation;
@@ -6437,7 +2520,7 @@ async function vision(options) {
6437
2520
  const resolvedImagePath = imagePath || (() => {
6438
2521
  const screenshotFile = typeof sidecar?.screenshotFile === "string" ? sidecar.screenshotFile : typeof sidecar?.filename === "string" ? sidecar.filename : void 0;
6439
2522
  if (!screenshotFile) return null;
6440
- const baseDir = sidecarPath ? dirname12(sidecarPath) : projectPath;
2523
+ const baseDir = sidecarPath ? dirname7(sidecarPath) : projectPath;
6441
2524
  const abs = resolve6(baseDir, screenshotFile);
6442
2525
  return abs;
6443
2526
  })();
@@ -6446,7 +2529,7 @@ async function vision(options) {
6446
2529
  "No image path could be resolved. Provide --image or a sidecar with `screenshotFile`/`filename`."
6447
2530
  );
6448
2531
  }
6449
- if (!existsSync19(resolvedImagePath)) {
2532
+ if (!existsSync6(resolvedImagePath)) {
6450
2533
  throw new Error(`Image not found: ${resolvedImagePath}`);
6451
2534
  }
6452
2535
  const { base64, sizeBytes } = readImageAsBase64(resolvedImagePath);
@@ -6673,185 +2756,10 @@ async function vision(options) {
6673
2756
  await flushLangfuse();
6674
2757
  }
6675
2758
 
6676
- // src/commands/session.ts
6677
- import { existsSync as existsSync20, readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
6678
- import { basename, dirname as dirname13, resolve as resolve7 } from "path";
6679
- import { createStyleSummary as createStyleSummary3 } from "uilint-core";
6680
- import {
6681
- ensureOllamaReady as ensureOllamaReady7,
6682
- parseCLIInput as parseCLIInput2,
6683
- readStyleGuideFromProject as readStyleGuideFromProject2,
6684
- readTailwindThemeTokens as readTailwindThemeTokens3
6685
- } from "uilint-core/node";
6686
- var SESSION_FILE = "/tmp/uilint-session.json";
6687
- var UI_FILE_EXTENSIONS = [".tsx", ".jsx", ".css", ".scss", ".module.css"];
6688
- function readSession() {
6689
- if (!existsSync20(SESSION_FILE)) {
6690
- return { files: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
6691
- }
6692
- try {
6693
- const content = readFileSync14(SESSION_FILE, "utf-8");
6694
- return JSON.parse(content);
6695
- } catch {
6696
- return { files: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
6697
- }
6698
- }
6699
- function writeSession(state) {
6700
- writeFileSync10(SESSION_FILE, JSON.stringify(state, null, 2), "utf-8");
6701
- }
6702
- function isUIFile(filePath) {
6703
- return UI_FILE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
6704
- }
6705
- function isScannableMarkupFile(filePath) {
6706
- return [".tsx", ".jsx", ".html", ".htm"].some(
6707
- (ext) => filePath.endsWith(ext)
6708
- );
6709
- }
6710
- async function sessionClear() {
6711
- if (existsSync20(SESSION_FILE)) {
6712
- unlinkSync2(SESSION_FILE);
6713
- }
6714
- console.log(JSON.stringify({ cleared: true }));
6715
- }
6716
- async function sessionTrack(filePath) {
6717
- if (!isUIFile(filePath)) {
6718
- console.log(
6719
- JSON.stringify({
6720
- tracked: false,
6721
- reason: "not_ui_file",
6722
- file: filePath,
6723
- message: `Skipped non-UI file: ${basename(filePath)}`
6724
- })
6725
- );
6726
- return;
6727
- }
6728
- const session = readSession();
6729
- const wasAlreadyTracked = session.files.includes(filePath);
6730
- if (!wasAlreadyTracked) {
6731
- session.files.push(filePath);
6732
- writeSession(session);
6733
- }
6734
- console.log(
6735
- JSON.stringify({
6736
- tracked: true,
6737
- file: filePath,
6738
- total: session.files.length,
6739
- newlyAdded: !wasAlreadyTracked,
6740
- message: wasAlreadyTracked ? `Already tracking: ${basename(filePath)} (${session.files.length} files total)` : `Now tracking: ${basename(filePath)} (${session.files.length} files total)`
6741
- })
6742
- );
6743
- }
6744
- async function sessionScan(options = {}) {
6745
- const session = readSession();
6746
- if (session.files.length === 0) {
6747
- if (options.hookFormat) {
6748
- console.log("{}");
6749
- } else {
6750
- const result = {
6751
- totalFiles: 0,
6752
- filesWithIssues: 0,
6753
- results: [],
6754
- followupMessage: null
6755
- };
6756
- console.log(JSON.stringify(result));
6757
- }
6758
- return;
6759
- }
6760
- const projectPath = process.cwd();
6761
- let styleGuide;
6762
- try {
6763
- styleGuide = await readStyleGuideFromProject2(projectPath);
6764
- } catch {
6765
- if (options.hookFormat) {
6766
- console.log("{}");
6767
- } else {
6768
- const result = {
6769
- totalFiles: session.files.length,
6770
- filesWithIssues: 0,
6771
- results: [],
6772
- followupMessage: null
6773
- };
6774
- console.log(JSON.stringify(result));
6775
- }
6776
- return;
6777
- }
6778
- await ensureOllamaReady7();
6779
- const client = await createLLMClient({});
6780
- const results = [];
6781
- for (const filePath of session.files) {
6782
- if (!existsSync20(filePath)) continue;
6783
- if (!isScannableMarkupFile(filePath)) continue;
6784
- try {
6785
- const absolutePath = resolve7(process.cwd(), filePath);
6786
- const htmlLike = readFileSync14(filePath, "utf-8");
6787
- const snapshot = parseCLIInput2(htmlLike);
6788
- const tailwindSearchDir = dirname13(absolutePath);
6789
- const tailwindTheme = readTailwindThemeTokens3(tailwindSearchDir);
6790
- const styleSummary = createStyleSummary3(snapshot.styles, {
6791
- html: snapshot.html,
6792
- tailwindTheme
6793
- });
6794
- const analysis = await client.analyzeStyles(styleSummary, styleGuide);
6795
- results.push({
6796
- file: filePath,
6797
- issues: analysis.issues
6798
- });
6799
- } catch {
6800
- continue;
6801
- }
6802
- }
6803
- const filesWithIssues = results.filter((r) => r.issues.length > 0);
6804
- let followupMessage = null;
6805
- if (filesWithIssues.length > 0) {
6806
- const issueLines = [];
6807
- for (const fileResult of filesWithIssues) {
6808
- const fileName = basename(fileResult.file);
6809
- for (const issue of fileResult.issues) {
6810
- const type = issue.type?.toUpperCase?.() ?? "ISSUE";
6811
- const detail = issue.currentValue && issue.expectedValue ? ` (${issue.currentValue} \u2192 ${issue.expectedValue})` : issue.currentValue ? ` (${issue.currentValue})` : "";
6812
- issueLines.push(`- ${fileName}: [${type}] ${issue.message}${detail}`);
6813
- if (issue.suggestion) {
6814
- issueLines.push(` Suggestion: ${issue.suggestion}`);
6815
- }
6816
- }
6817
- }
6818
- followupMessage = [
6819
- `UILint scan found UI consistency issues in ${filesWithIssues.length} file(s):`,
6820
- "",
6821
- ...issueLines,
6822
- "",
6823
- "See .uilint/styleguide.md for style rules. Please fix these issues."
6824
- ].join("\n");
6825
- }
6826
- if (options.hookFormat) {
6827
- if (followupMessage) {
6828
- console.log(JSON.stringify({ followup_message: followupMessage }));
6829
- } else {
6830
- console.log("{}");
6831
- }
6832
- } else {
6833
- const result = {
6834
- totalFiles: results.length,
6835
- filesWithIssues: filesWithIssues.length,
6836
- results,
6837
- followupMessage
6838
- };
6839
- console.log(JSON.stringify(result));
6840
- }
6841
- if (existsSync20(SESSION_FILE)) {
6842
- unlinkSync2(SESSION_FILE);
6843
- }
6844
- await flushLangfuse();
6845
- }
6846
- async function sessionList() {
6847
- const session = readSession();
6848
- console.log(JSON.stringify(session));
6849
- }
6850
-
6851
2759
  // src/index.ts
6852
- import { readFileSync as readFileSync15 } from "fs";
6853
- import { dirname as dirname14, join as join20 } from "path";
6854
- import { fileURLToPath as fileURLToPath4 } from "url";
2760
+ import { readFileSync as readFileSync4 } from "fs";
2761
+ import { dirname as dirname8, join as join6 } from "path";
2762
+ import { fileURLToPath as fileURLToPath2 } from "url";
6855
2763
  function assertNodeVersion(minMajor) {
6856
2764
  const ver = process.versions.node || "";
6857
2765
  const majorStr = ver.split(".")[0] || "";
@@ -6867,9 +2775,9 @@ assertNodeVersion(20);
6867
2775
  var program = new Command();
6868
2776
  function getCLIVersion2() {
6869
2777
  try {
6870
- const __dirname3 = dirname14(fileURLToPath4(import.meta.url));
6871
- const pkgPath = join20(__dirname3, "..", "package.json");
6872
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
2778
+ const __dirname = dirname8(fileURLToPath2(import.meta.url));
2779
+ const pkgPath = join6(__dirname, "..", "package.json");
2780
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
6873
2781
  return pkg.version || "0.0.0";
6874
2782
  } catch {
6875
2783
  return "0.0.0";
@@ -6942,30 +2850,9 @@ program.command("update").description("Update existing style guide with new styl
6942
2850
  llm: options.llm
6943
2851
  });
6944
2852
  });
6945
- program.command("install").description("Install UILint integration (MCP server and/or Cursor hooks)").option("--force", "Overwrite existing configuration files").option("--mcp", "Install MCP server integration (.cursor/mcp.json)").option("--hooks", "Install Cursor hooks integration (.cursor/hooks.json)").option("--genstyleguide", "Install /genstyleguide Cursor command").option(
6946
- "--genrules",
6947
- "Install /genrules Cursor command for ESLint rule generation"
6948
- ).option("--eslint", "Install uilint-eslint plugin and configure ESLint").option(
6949
- "--routes",
6950
- "Back-compat: install Next.js overlay (routes + deps + inject)"
6951
- ).option(
6952
- "--react",
6953
- "Back-compat: install Next.js overlay (routes + deps + inject)"
6954
- ).option(
6955
- "--mode <mode>",
6956
- "Integration mode: mcp, hooks, or both (skips interactive prompt)"
6957
- ).action(async (options) => {
6958
- await install({
6959
- force: options.force,
6960
- mode: options.mode,
6961
- mcp: options.mcp,
6962
- hooks: options.hooks,
6963
- genstyleguide: options.genstyleguide,
6964
- genrules: options.genrules,
6965
- eslint: options.eslint,
6966
- routes: options.routes,
6967
- react: options.react
6968
- });
2853
+ program.command("install").description("Install UILint integration").option("--force", "Overwrite existing configuration files").action(async (options) => {
2854
+ const { installUI } = await import("./install-ui-OEFHX4FG.js");
2855
+ await installUI({ force: options.force });
6969
2856
  });
6970
2857
  program.command("serve").description("Start WebSocket server for real-time UI linting").option("-p, --port <number>", "Port to listen on", "9234").action(async (options) => {
6971
2858
  await serve({
@@ -7006,20 +2893,5 @@ program.command("vision").description("Analyze a screenshot with Ollama vision m
7006
2893
  debugDump: options.debugDump
7007
2894
  });
7008
2895
  });
7009
- var sessionCmd = program.command("session").description(
7010
- "Manage file tracking for agentic sessions (used by Cursor hooks)"
7011
- );
7012
- sessionCmd.command("clear").description("Clear tracked files (called at start of agent turn)").action(async () => {
7013
- await sessionClear();
7014
- });
7015
- sessionCmd.command("track <file>").description("Track a file edit (called on each file edit)").action(async (file) => {
7016
- await sessionTrack(file);
7017
- });
7018
- sessionCmd.command("scan").description("Scan all tracked markup files (called on agent stop)").option("--hook", "Output in Cursor hook format (followup_message JSON only)").action(async (options) => {
7019
- await sessionScan({ hookFormat: options.hook });
7020
- });
7021
- sessionCmd.command("list").description("List tracked files (for debugging)").action(async () => {
7022
- await sessionList();
7023
- });
7024
2896
  program.parse();
7025
2897
  //# sourceMappingURL=index.js.map