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