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