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