uilint 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2429 -382
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/skills/ui-consistency-enforcer/SKILL.md +435 -0
- package/skills/ui-consistency-enforcer/references/REGISTRY-ENTRY.md +163 -0
- package/skills/ui-consistency-enforcer/references/RULE-TEMPLATE.ts +253 -0
- package/skills/ui-consistency-enforcer/references/TEST-TEMPLATE.ts +496 -0
package/dist/index.js
CHANGED
|
@@ -237,8 +237,8 @@ async function initializeLangfuseIfEnabled() {
|
|
|
237
237
|
},
|
|
238
238
|
{ asType: "generation" }
|
|
239
239
|
);
|
|
240
|
-
await new Promise((
|
|
241
|
-
resolveTrace =
|
|
240
|
+
await new Promise((resolve8) => {
|
|
241
|
+
resolveTrace = resolve8;
|
|
242
242
|
});
|
|
243
243
|
if (endData && generationRef) {
|
|
244
244
|
const usageDetails = endData.usage ? Object.fromEntries(
|
|
@@ -329,6 +329,26 @@ function registerShutdownHandler() {
|
|
|
329
329
|
}
|
|
330
330
|
registerShutdownHandler();
|
|
331
331
|
|
|
332
|
+
// src/utils/timing.ts
|
|
333
|
+
function nsNow() {
|
|
334
|
+
return process.hrtime.bigint();
|
|
335
|
+
}
|
|
336
|
+
function nsToMs(ns) {
|
|
337
|
+
return Number(ns) / 1e6;
|
|
338
|
+
}
|
|
339
|
+
function formatMs(ms) {
|
|
340
|
+
if (!Number.isFinite(ms)) return "n/a";
|
|
341
|
+
if (ms < 1e3) return `${ms.toFixed(ms < 10 ? 2 : ms < 100 ? 1 : 0)}ms`;
|
|
342
|
+
const s = ms / 1e3;
|
|
343
|
+
if (s < 60) return `${s.toFixed(s < 10 ? 2 : 1)}s`;
|
|
344
|
+
const m = Math.floor(s / 60);
|
|
345
|
+
const rem = s - m * 60;
|
|
346
|
+
return `${m}m ${rem.toFixed(rem < 10 ? 1 : 0)}s`;
|
|
347
|
+
}
|
|
348
|
+
function maybeMs(ms) {
|
|
349
|
+
return ms == null ? "n/a" : formatMs(ms);
|
|
350
|
+
}
|
|
351
|
+
|
|
332
352
|
// src/utils/prompts.ts
|
|
333
353
|
import * as p from "@clack/prompts";
|
|
334
354
|
import pc from "picocolors";
|
|
@@ -337,8 +357,8 @@ import { dirname, join as join2 } from "path";
|
|
|
337
357
|
import { fileURLToPath } from "url";
|
|
338
358
|
function getCLIVersion() {
|
|
339
359
|
try {
|
|
340
|
-
const
|
|
341
|
-
const pkgPath = join2(
|
|
360
|
+
const __dirname3 = dirname(fileURLToPath(import.meta.url));
|
|
361
|
+
const pkgPath = join2(__dirname3, "..", "..", "package.json");
|
|
342
362
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
343
363
|
return pkg.version || "0.0.0";
|
|
344
364
|
} catch {
|
|
@@ -369,7 +389,7 @@ async function withSpinner(message, fn) {
|
|
|
369
389
|
const s = p.spinner();
|
|
370
390
|
s.start(message);
|
|
371
391
|
try {
|
|
372
|
-
const result = await fn();
|
|
392
|
+
const result = fn.length >= 1 ? await fn(s) : await fn();
|
|
373
393
|
s.stop(pc.green("\u2713 ") + message);
|
|
374
394
|
return result;
|
|
375
395
|
} catch (error) {
|
|
@@ -642,6 +662,7 @@ async function scan(options) {
|
|
|
642
662
|
} else if (dbg && styleSummary) {
|
|
643
663
|
debugLog(dbg, "Style summary (preview)", preview(styleSummary, 800));
|
|
644
664
|
}
|
|
665
|
+
const prepStartNs = nsNow();
|
|
645
666
|
if (!isJsonOutput) {
|
|
646
667
|
await withSpinner("Preparing Ollama", async () => {
|
|
647
668
|
await ensureOllamaReady();
|
|
@@ -649,6 +670,7 @@ async function scan(options) {
|
|
|
649
670
|
} else {
|
|
650
671
|
await ensureOllamaReady();
|
|
651
672
|
}
|
|
673
|
+
const prepEndNs = nsNow();
|
|
652
674
|
const client = await createLLMClient({});
|
|
653
675
|
let result;
|
|
654
676
|
const prompt = snapshot.kind === "dom" ? buildAnalysisPrompt(styleSummary ?? "", styleGuide) : buildSourceAnalysisPrompt(snapshot.source, styleGuide, {
|
|
@@ -764,7 +786,33 @@ async function scan(options) {
|
|
|
764
786
|
} else {
|
|
765
787
|
const s = createSpinner();
|
|
766
788
|
s.start("Analyzing with LLM");
|
|
767
|
-
|
|
789
|
+
let thinkingStarted = false;
|
|
790
|
+
const analysisStartNs = nsNow();
|
|
791
|
+
let firstTokenNs = null;
|
|
792
|
+
let firstThinkingNs = null;
|
|
793
|
+
let lastThinkingNs = null;
|
|
794
|
+
let firstAnswerNs = null;
|
|
795
|
+
let lastAnswerNs = null;
|
|
796
|
+
const onProgress = (latestLine, _fullResponse, delta, thinkingDelta) => {
|
|
797
|
+
const nowNs = nsNow();
|
|
798
|
+
if (!firstTokenNs && (thinkingDelta || delta)) firstTokenNs = nowNs;
|
|
799
|
+
if (thinkingDelta) {
|
|
800
|
+
if (!firstThinkingNs) firstThinkingNs = nowNs;
|
|
801
|
+
lastThinkingNs = nowNs;
|
|
802
|
+
}
|
|
803
|
+
if (delta) {
|
|
804
|
+
if (!firstAnswerNs) firstAnswerNs = nowNs;
|
|
805
|
+
lastAnswerNs = nowNs;
|
|
806
|
+
}
|
|
807
|
+
if (thinkingDelta) {
|
|
808
|
+
if (!thinkingStarted) {
|
|
809
|
+
thinkingStarted = true;
|
|
810
|
+
s.stop(pc.dim("[scan] streaming thinking:"));
|
|
811
|
+
process.stderr.write(pc.dim("Thinking:\n"));
|
|
812
|
+
}
|
|
813
|
+
process.stderr.write(thinkingDelta);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
768
816
|
const maxLen = 60;
|
|
769
817
|
const displayLine = latestLine.length > maxLen ? latestLine.slice(0, maxLen) + "\u2026" : latestLine;
|
|
770
818
|
s.message(`Analyzing: ${pc.dim(displayLine || "...")}`);
|
|
@@ -790,6 +838,29 @@ async function scan(options) {
|
|
|
790
838
|
}
|
|
791
839
|
);
|
|
792
840
|
s.stop(pc.green("\u2713 ") + "Analyzing with LLM");
|
|
841
|
+
if (process.stdout.isTTY) {
|
|
842
|
+
const analysisEndNs = nsNow();
|
|
843
|
+
const prepMs = nsToMs(prepEndNs - prepStartNs);
|
|
844
|
+
const totalMs = nsToMs(analysisEndNs - analysisStartNs);
|
|
845
|
+
const endToEndMs = nsToMs(analysisEndNs - prepStartNs);
|
|
846
|
+
const ttftMs = firstTokenNs ? nsToMs(firstTokenNs - analysisStartNs) : null;
|
|
847
|
+
const thinkingMs = firstThinkingNs && (firstAnswerNs || lastThinkingNs) ? nsToMs(
|
|
848
|
+
(firstAnswerNs ?? lastThinkingNs ?? analysisEndNs) - firstThinkingNs
|
|
849
|
+
) : null;
|
|
850
|
+
const outputMs = firstAnswerNs && (lastAnswerNs || analysisEndNs) ? nsToMs((lastAnswerNs ?? analysisEndNs) - firstAnswerNs) : null;
|
|
851
|
+
note2(
|
|
852
|
+
[
|
|
853
|
+
`Prepare Ollama: ${formatMs(prepMs)}`,
|
|
854
|
+
`Time to first token: ${maybeMs(ttftMs)}`,
|
|
855
|
+
`Thinking: ${maybeMs(thinkingMs)}`,
|
|
856
|
+
`Outputting: ${maybeMs(outputMs)}`,
|
|
857
|
+
`LLM total: ${formatMs(totalMs)}`,
|
|
858
|
+
`End-to-end: ${formatMs(endToEndMs)}`,
|
|
859
|
+
result?.analysisTime ? pc.dim(`(core analysisTime: ${formatMs(result.analysisTime)})`) : pc.dim("(core analysisTime: n/a)")
|
|
860
|
+
].join("\n"),
|
|
861
|
+
"Timings"
|
|
862
|
+
);
|
|
863
|
+
}
|
|
793
864
|
} catch (error) {
|
|
794
865
|
s.stop(pc.red("\u2717 ") + "Analyzing with LLM");
|
|
795
866
|
throw error;
|
|
@@ -970,6 +1041,7 @@ async function analyze(options) {
|
|
|
970
1041
|
} else if (resolvedStyle.path && !isJsonOutput) {
|
|
971
1042
|
logSuccess(`Using styleguide: ${pc.dim(resolvedStyle.path)}`);
|
|
972
1043
|
}
|
|
1044
|
+
const prepStartNs = nsNow();
|
|
973
1045
|
if (!isJsonOutput) {
|
|
974
1046
|
await withSpinner("Preparing Ollama", async () => {
|
|
975
1047
|
await ensureOllamaReady2();
|
|
@@ -977,6 +1049,7 @@ async function analyze(options) {
|
|
|
977
1049
|
} else {
|
|
978
1050
|
await ensureOllamaReady2();
|
|
979
1051
|
}
|
|
1052
|
+
const prepEndNs = nsNow();
|
|
980
1053
|
const client = await createLLMClient({});
|
|
981
1054
|
const promptContext = {
|
|
982
1055
|
filePath: options.filePath || (options.inputFile ? options.inputFile : void 0) || "component.tsx",
|
|
@@ -1065,17 +1138,65 @@ async function analyze(options) {
|
|
|
1065
1138
|
if (!isJsonOutput) {
|
|
1066
1139
|
const s = createSpinner();
|
|
1067
1140
|
s.start("Analyzing with LLM");
|
|
1141
|
+
let thinkingStarted = false;
|
|
1142
|
+
const analysisStartNs = nsNow();
|
|
1143
|
+
let firstTokenNs = null;
|
|
1144
|
+
let firstThinkingNs = null;
|
|
1145
|
+
let lastThinkingNs = null;
|
|
1146
|
+
let firstAnswerNs = null;
|
|
1147
|
+
let lastAnswerNs = null;
|
|
1068
1148
|
try {
|
|
1069
1149
|
responseText = await client.complete(prompt, {
|
|
1070
1150
|
json: true,
|
|
1071
1151
|
stream: true,
|
|
1072
|
-
onProgress: (latestLine) => {
|
|
1152
|
+
onProgress: (latestLine, _fullResponse, delta, thinkingDelta) => {
|
|
1153
|
+
const nowNs = nsNow();
|
|
1154
|
+
if (!firstTokenNs && (thinkingDelta || delta)) firstTokenNs = nowNs;
|
|
1155
|
+
if (thinkingDelta) {
|
|
1156
|
+
if (!firstThinkingNs) firstThinkingNs = nowNs;
|
|
1157
|
+
lastThinkingNs = nowNs;
|
|
1158
|
+
}
|
|
1159
|
+
if (delta) {
|
|
1160
|
+
if (!firstAnswerNs) firstAnswerNs = nowNs;
|
|
1161
|
+
lastAnswerNs = nowNs;
|
|
1162
|
+
}
|
|
1163
|
+
if (thinkingDelta) {
|
|
1164
|
+
if (!thinkingStarted) {
|
|
1165
|
+
thinkingStarted = true;
|
|
1166
|
+
s.stop(pc.dim("[analyze] streaming thinking:"));
|
|
1167
|
+
process.stderr.write(pc.dim("Thinking:\n"));
|
|
1168
|
+
}
|
|
1169
|
+
process.stderr.write(thinkingDelta);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1073
1172
|
const maxLen = 60;
|
|
1074
1173
|
const line = latestLine.length > maxLen ? latestLine.slice(0, maxLen) + "\u2026" : latestLine;
|
|
1075
1174
|
s.message(`Analyzing: ${pc.dim(line || "...")}`);
|
|
1076
1175
|
}
|
|
1077
1176
|
});
|
|
1078
1177
|
s.stop(pc.green("\u2713 ") + "Analyzing with LLM");
|
|
1178
|
+
if (process.stdout.isTTY) {
|
|
1179
|
+
const analysisEndNs = nsNow();
|
|
1180
|
+
const prepMs = nsToMs(prepEndNs - prepStartNs);
|
|
1181
|
+
const totalMs = nsToMs(analysisEndNs - analysisStartNs);
|
|
1182
|
+
const endToEndMs = nsToMs(analysisEndNs - prepStartNs);
|
|
1183
|
+
const ttftMs = firstTokenNs ? nsToMs(firstTokenNs - analysisStartNs) : null;
|
|
1184
|
+
const thinkingMs = firstThinkingNs && (firstAnswerNs || lastThinkingNs) ? nsToMs(
|
|
1185
|
+
(firstAnswerNs ?? lastThinkingNs ?? analysisEndNs) - firstThinkingNs
|
|
1186
|
+
) : null;
|
|
1187
|
+
const outputMs = firstAnswerNs && (lastAnswerNs || analysisEndNs) ? nsToMs((lastAnswerNs ?? analysisEndNs) - firstAnswerNs) : null;
|
|
1188
|
+
note2(
|
|
1189
|
+
[
|
|
1190
|
+
`Prepare Ollama: ${formatMs(prepMs)}`,
|
|
1191
|
+
`Time to first token: ${maybeMs(ttftMs)}`,
|
|
1192
|
+
`Thinking: ${maybeMs(thinkingMs)}`,
|
|
1193
|
+
`Outputting: ${maybeMs(outputMs)}`,
|
|
1194
|
+
`LLM total: ${formatMs(totalMs)}`,
|
|
1195
|
+
`End-to-end: ${formatMs(endToEndMs)}`
|
|
1196
|
+
].join("\n"),
|
|
1197
|
+
"Timings"
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1079
1200
|
} catch (e) {
|
|
1080
1201
|
s.stop(pc.red("\u2717 ") + "Analyzing with LLM");
|
|
1081
1202
|
throw e;
|
|
@@ -1128,14 +1249,14 @@ import {
|
|
|
1128
1249
|
} from "uilint-core";
|
|
1129
1250
|
import { ensureOllamaReady as ensureOllamaReady3 } from "uilint-core/node";
|
|
1130
1251
|
async function readStdin2() {
|
|
1131
|
-
return new Promise((
|
|
1252
|
+
return new Promise((resolve8) => {
|
|
1132
1253
|
let data = "";
|
|
1133
1254
|
const rl = createInterface({ input: process.stdin });
|
|
1134
1255
|
rl.on("line", (line) => {
|
|
1135
1256
|
data += line;
|
|
1136
1257
|
});
|
|
1137
1258
|
rl.on("close", () => {
|
|
1138
|
-
|
|
1259
|
+
resolve8(data);
|
|
1139
1260
|
});
|
|
1140
1261
|
});
|
|
1141
1262
|
}
|
|
@@ -1354,13 +1475,13 @@ async function update(options) {
|
|
|
1354
1475
|
}
|
|
1355
1476
|
|
|
1356
1477
|
// src/commands/install.ts
|
|
1357
|
-
import { join as
|
|
1478
|
+
import { join as join16 } from "path";
|
|
1358
1479
|
import { ruleRegistry as ruleRegistry2 } from "uilint-eslint";
|
|
1359
1480
|
|
|
1360
1481
|
// src/commands/install/analyze.ts
|
|
1361
|
-
import { existsSync as
|
|
1362
|
-
import { join as
|
|
1363
|
-
import { findWorkspaceRoot as
|
|
1482
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
1483
|
+
import { join as join8 } from "path";
|
|
1484
|
+
import { findWorkspaceRoot as findWorkspaceRoot5 } from "uilint-core/node";
|
|
1364
1485
|
|
|
1365
1486
|
// src/utils/next-detect.ts
|
|
1366
1487
|
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
@@ -1444,10 +1565,113 @@ function findNextAppRouterProjects(rootDir, options) {
|
|
|
1444
1565
|
return results;
|
|
1445
1566
|
}
|
|
1446
1567
|
|
|
1447
|
-
// src/utils/
|
|
1568
|
+
// src/utils/vite-detect.ts
|
|
1448
1569
|
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
1449
|
-
import { join as join4
|
|
1570
|
+
import { join as join4 } from "path";
|
|
1571
|
+
var VITE_CONFIG_EXTS = [".ts", ".mjs", ".js", ".cjs"];
|
|
1572
|
+
function findViteConfigFile(projectPath) {
|
|
1573
|
+
for (const ext of VITE_CONFIG_EXTS) {
|
|
1574
|
+
const rel = `vite.config${ext}`;
|
|
1575
|
+
if (existsSync5(join4(projectPath, rel))) return rel;
|
|
1576
|
+
}
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
function looksLikeReactPackage(projectPath) {
|
|
1580
|
+
try {
|
|
1581
|
+
const pkgPath = join4(projectPath, "package.json");
|
|
1582
|
+
if (!existsSync5(pkgPath)) return false;
|
|
1583
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
1584
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1585
|
+
return "react" in deps || "react-dom" in deps;
|
|
1586
|
+
} catch {
|
|
1587
|
+
return false;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
function fileExists2(projectPath, relPath) {
|
|
1591
|
+
return existsSync5(join4(projectPath, relPath));
|
|
1592
|
+
}
|
|
1593
|
+
function detectViteReact(projectPath) {
|
|
1594
|
+
const configFile = findViteConfigFile(projectPath);
|
|
1595
|
+
if (!configFile) return null;
|
|
1596
|
+
if (!looksLikeReactPackage(projectPath)) return null;
|
|
1597
|
+
const entryRoot = "src";
|
|
1598
|
+
const candidates = [];
|
|
1599
|
+
const entryCandidates = [
|
|
1600
|
+
join4(entryRoot, "main.tsx"),
|
|
1601
|
+
join4(entryRoot, "main.jsx"),
|
|
1602
|
+
join4(entryRoot, "main.ts"),
|
|
1603
|
+
join4(entryRoot, "main.js")
|
|
1604
|
+
];
|
|
1605
|
+
for (const rel of entryCandidates) {
|
|
1606
|
+
if (fileExists2(projectPath, rel)) candidates.push(rel);
|
|
1607
|
+
}
|
|
1608
|
+
const fallbackCandidates = [
|
|
1609
|
+
join4(entryRoot, "App.tsx"),
|
|
1610
|
+
join4(entryRoot, "App.jsx")
|
|
1611
|
+
];
|
|
1612
|
+
for (const rel of fallbackCandidates) {
|
|
1613
|
+
if (!candidates.includes(rel) && fileExists2(projectPath, rel)) {
|
|
1614
|
+
candidates.push(rel);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
configFile,
|
|
1619
|
+
configFileAbs: join4(projectPath, configFile),
|
|
1620
|
+
entryRoot,
|
|
1621
|
+
candidates
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1450
1624
|
var DEFAULT_IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
1625
|
+
"node_modules",
|
|
1626
|
+
".git",
|
|
1627
|
+
".next",
|
|
1628
|
+
"dist",
|
|
1629
|
+
"build",
|
|
1630
|
+
"out",
|
|
1631
|
+
".turbo",
|
|
1632
|
+
".vercel",
|
|
1633
|
+
".cursor",
|
|
1634
|
+
"coverage",
|
|
1635
|
+
".uilint"
|
|
1636
|
+
]);
|
|
1637
|
+
function findViteReactProjects(rootDir, options) {
|
|
1638
|
+
const maxDepth = options?.maxDepth ?? 4;
|
|
1639
|
+
const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS2;
|
|
1640
|
+
const results = [];
|
|
1641
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1642
|
+
function walk(dir, depth) {
|
|
1643
|
+
if (depth > maxDepth) return;
|
|
1644
|
+
if (visited.has(dir)) return;
|
|
1645
|
+
visited.add(dir);
|
|
1646
|
+
const detection = detectViteReact(dir);
|
|
1647
|
+
if (detection) {
|
|
1648
|
+
results.push({ projectPath: dir, detection });
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
let entries = [];
|
|
1652
|
+
try {
|
|
1653
|
+
entries = readdirSync2(dir, { withFileTypes: true }).map((d) => ({
|
|
1654
|
+
name: d.name,
|
|
1655
|
+
isDirectory: d.isDirectory()
|
|
1656
|
+
}));
|
|
1657
|
+
} catch {
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
for (const ent of entries) {
|
|
1661
|
+
if (!ent.isDirectory) continue;
|
|
1662
|
+
if (ignoreDirs.has(ent.name)) continue;
|
|
1663
|
+
if (ent.name.startsWith(".") && ent.name !== ".") continue;
|
|
1664
|
+
walk(join4(dir, ent.name), depth + 1);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
walk(rootDir, 0);
|
|
1668
|
+
return results;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// src/utils/package-detect.ts
|
|
1672
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
1673
|
+
import { join as join5, relative } from "path";
|
|
1674
|
+
var DEFAULT_IGNORE_DIRS3 = /* @__PURE__ */ new Set([
|
|
1451
1675
|
"node_modules",
|
|
1452
1676
|
".git",
|
|
1453
1677
|
".next",
|
|
@@ -1463,6 +1687,7 @@ var DEFAULT_IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
1463
1687
|
]);
|
|
1464
1688
|
var ESLINT_CONFIG_FILES = [
|
|
1465
1689
|
"eslint.config.js",
|
|
1690
|
+
"eslint.config.ts",
|
|
1466
1691
|
"eslint.config.mjs",
|
|
1467
1692
|
"eslint.config.cjs",
|
|
1468
1693
|
".eslintrc.js",
|
|
@@ -1489,16 +1714,34 @@ function isFrontendPackage(pkgJson) {
|
|
|
1489
1714
|
};
|
|
1490
1715
|
return FRONTEND_INDICATORS.some((pkg) => pkg in deps);
|
|
1491
1716
|
}
|
|
1717
|
+
function isTypeScriptPackage(dir, pkgJson) {
|
|
1718
|
+
if (existsSync6(join5(dir, "tsconfig.json"))) {
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
const deps = {
|
|
1722
|
+
...pkgJson.dependencies,
|
|
1723
|
+
...pkgJson.devDependencies
|
|
1724
|
+
};
|
|
1725
|
+
if ("typescript" in deps) {
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
for (const configFile of ESLINT_CONFIG_FILES) {
|
|
1729
|
+
if (configFile.endsWith(".ts") && existsSync6(join5(dir, configFile))) {
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1492
1735
|
function hasEslintConfig(dir) {
|
|
1493
1736
|
for (const file of ESLINT_CONFIG_FILES) {
|
|
1494
|
-
if (
|
|
1737
|
+
if (existsSync6(join5(dir, file))) {
|
|
1495
1738
|
return true;
|
|
1496
1739
|
}
|
|
1497
1740
|
}
|
|
1498
1741
|
try {
|
|
1499
|
-
const pkgPath =
|
|
1500
|
-
if (
|
|
1501
|
-
const pkg = JSON.parse(
|
|
1742
|
+
const pkgPath = join5(dir, "package.json");
|
|
1743
|
+
if (existsSync6(pkgPath)) {
|
|
1744
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
1502
1745
|
if (pkg.eslintConfig) return true;
|
|
1503
1746
|
}
|
|
1504
1747
|
} catch {
|
|
@@ -1507,14 +1750,14 @@ function hasEslintConfig(dir) {
|
|
|
1507
1750
|
}
|
|
1508
1751
|
function findPackages(rootDir, options) {
|
|
1509
1752
|
const maxDepth = options?.maxDepth ?? 5;
|
|
1510
|
-
const ignoreDirs = options?.ignoreDirs ??
|
|
1753
|
+
const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS3;
|
|
1511
1754
|
const results = [];
|
|
1512
1755
|
const visited = /* @__PURE__ */ new Set();
|
|
1513
1756
|
function processPackage(dir, isRoot) {
|
|
1514
|
-
const pkgPath =
|
|
1515
|
-
if (!
|
|
1757
|
+
const pkgPath = join5(dir, "package.json");
|
|
1758
|
+
if (!existsSync6(pkgPath)) return null;
|
|
1516
1759
|
try {
|
|
1517
|
-
const pkg = JSON.parse(
|
|
1760
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
1518
1761
|
const name = pkg.name || relative(rootDir, dir) || ".";
|
|
1519
1762
|
return {
|
|
1520
1763
|
path: dir,
|
|
@@ -1522,7 +1765,8 @@ function findPackages(rootDir, options) {
|
|
|
1522
1765
|
name,
|
|
1523
1766
|
hasEslintConfig: hasEslintConfig(dir),
|
|
1524
1767
|
isFrontend: isFrontendPackage(pkg),
|
|
1525
|
-
isRoot
|
|
1768
|
+
isRoot,
|
|
1769
|
+
isTypeScript: isTypeScriptPackage(dir, pkg)
|
|
1526
1770
|
};
|
|
1527
1771
|
} catch {
|
|
1528
1772
|
return null;
|
|
@@ -1538,7 +1782,7 @@ function findPackages(rootDir, options) {
|
|
|
1538
1782
|
}
|
|
1539
1783
|
let entries = [];
|
|
1540
1784
|
try {
|
|
1541
|
-
entries =
|
|
1785
|
+
entries = readdirSync3(dir, { withFileTypes: true }).map((d) => ({
|
|
1542
1786
|
name: d.name,
|
|
1543
1787
|
isDirectory: d.isDirectory()
|
|
1544
1788
|
}));
|
|
@@ -1549,7 +1793,7 @@ function findPackages(rootDir, options) {
|
|
|
1549
1793
|
if (!ent.isDirectory) continue;
|
|
1550
1794
|
if (ignoreDirs.has(ent.name)) continue;
|
|
1551
1795
|
if (ent.name.startsWith(".")) continue;
|
|
1552
|
-
walk(
|
|
1796
|
+
walk(join5(dir, ent.name), depth + 1);
|
|
1553
1797
|
}
|
|
1554
1798
|
}
|
|
1555
1799
|
walk(rootDir, 0);
|
|
@@ -1574,18 +1818,18 @@ function formatPackageOption(pkg) {
|
|
|
1574
1818
|
}
|
|
1575
1819
|
|
|
1576
1820
|
// src/utils/package-manager.ts
|
|
1577
|
-
import { existsSync as
|
|
1821
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1578
1822
|
import { spawn } from "child_process";
|
|
1579
|
-
import { dirname as dirname5, join as
|
|
1823
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
1580
1824
|
function detectPackageManager(projectPath) {
|
|
1581
1825
|
let dir = projectPath;
|
|
1582
1826
|
for (; ; ) {
|
|
1583
|
-
if (
|
|
1584
|
-
if (
|
|
1585
|
-
if (
|
|
1586
|
-
if (
|
|
1587
|
-
if (
|
|
1588
|
-
if (
|
|
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";
|
|
1589
1833
|
const parent = dirname5(dir);
|
|
1590
1834
|
if (parent === dir) break;
|
|
1591
1835
|
dir = parent;
|
|
@@ -1593,7 +1837,7 @@ function detectPackageManager(projectPath) {
|
|
|
1593
1837
|
return "npm";
|
|
1594
1838
|
}
|
|
1595
1839
|
function spawnAsync(command, args, cwd) {
|
|
1596
|
-
return new Promise((
|
|
1840
|
+
return new Promise((resolve8, reject) => {
|
|
1597
1841
|
const child = spawn(command, args, {
|
|
1598
1842
|
cwd,
|
|
1599
1843
|
stdio: "inherit",
|
|
@@ -1601,7 +1845,7 @@ function spawnAsync(command, args, cwd) {
|
|
|
1601
1845
|
});
|
|
1602
1846
|
child.on("error", reject);
|
|
1603
1847
|
child.on("close", (code) => {
|
|
1604
|
-
if (code === 0)
|
|
1848
|
+
if (code === 0) resolve8();
|
|
1605
1849
|
else
|
|
1606
1850
|
reject(new Error(`${command} ${args.join(" ")} exited with ${code}`));
|
|
1607
1851
|
});
|
|
@@ -1627,14 +1871,15 @@ async function installDependencies(pm, projectPath, packages) {
|
|
|
1627
1871
|
}
|
|
1628
1872
|
|
|
1629
1873
|
// src/utils/eslint-config-inject.ts
|
|
1630
|
-
import { existsSync as
|
|
1631
|
-
import { join as
|
|
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";
|
|
1632
1876
|
import { parseExpression, parseModule, generateCode } from "magicast";
|
|
1633
|
-
|
|
1877
|
+
import { findWorkspaceRoot as findWorkspaceRoot4 } from "uilint-core/node";
|
|
1878
|
+
var CONFIG_EXTENSIONS = [".ts", ".mjs", ".js", ".cjs"];
|
|
1634
1879
|
function findEslintConfigFile(projectPath) {
|
|
1635
1880
|
for (const ext of CONFIG_EXTENSIONS) {
|
|
1636
|
-
const configPath =
|
|
1637
|
-
if (
|
|
1881
|
+
const configPath = join7(projectPath, `eslint.config${ext}`);
|
|
1882
|
+
if (existsSync8(configPath)) {
|
|
1638
1883
|
return configPath;
|
|
1639
1884
|
}
|
|
1640
1885
|
}
|
|
@@ -1644,25 +1889,6 @@ function getEslintConfigFilename(configPath) {
|
|
|
1644
1889
|
const parts = configPath.split("/");
|
|
1645
1890
|
return parts[parts.length - 1] || "eslint.config.mjs";
|
|
1646
1891
|
}
|
|
1647
|
-
function hasUilintImport(source) {
|
|
1648
|
-
return source.includes('from "uilint-eslint"') || source.includes("from 'uilint-eslint'") || source.includes('require("uilint-eslint")') || source.includes("require('uilint-eslint')");
|
|
1649
|
-
}
|
|
1650
|
-
function hasUilintConfigsUsage(source) {
|
|
1651
|
-
return /\builint\s*\.\s*configs\s*\./.test(source);
|
|
1652
|
-
}
|
|
1653
|
-
function walkAst(node, visit) {
|
|
1654
|
-
if (!node || typeof node !== "object") return;
|
|
1655
|
-
visit(node);
|
|
1656
|
-
for (const key of Object.keys(node)) {
|
|
1657
|
-
const v = node[key];
|
|
1658
|
-
if (!v) continue;
|
|
1659
|
-
if (Array.isArray(v)) {
|
|
1660
|
-
for (const item of v) walkAst(item, visit);
|
|
1661
|
-
} else if (typeof v === "object" && v.type) {
|
|
1662
|
-
walkAst(v, visit);
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
1892
|
function isIdentifier(node, name) {
|
|
1667
1893
|
return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
|
|
1668
1894
|
}
|
|
@@ -1687,35 +1913,6 @@ function hasSpreadProperties(obj) {
|
|
|
1687
1913
|
(p2) => p2 && (p2.type === "SpreadElement" || p2.type === "SpreadProperty")
|
|
1688
1914
|
);
|
|
1689
1915
|
}
|
|
1690
|
-
var IGNORED_AST_KEYS = /* @__PURE__ */ new Set([
|
|
1691
|
-
"loc",
|
|
1692
|
-
"start",
|
|
1693
|
-
"end",
|
|
1694
|
-
"extra",
|
|
1695
|
-
"leadingComments",
|
|
1696
|
-
"trailingComments",
|
|
1697
|
-
"innerComments"
|
|
1698
|
-
]);
|
|
1699
|
-
function normalizeAstForCompare(node) {
|
|
1700
|
-
if (node === null) return null;
|
|
1701
|
-
if (node === void 0) return void 0;
|
|
1702
|
-
if (typeof node !== "object") return node;
|
|
1703
|
-
if (Array.isArray(node)) return node.map(normalizeAstForCompare);
|
|
1704
|
-
const out = {};
|
|
1705
|
-
const keys = Object.keys(node).filter((k) => !IGNORED_AST_KEYS.has(k)).sort();
|
|
1706
|
-
for (const k of keys) {
|
|
1707
|
-
if (k.startsWith("$")) continue;
|
|
1708
|
-
out[k] = normalizeAstForCompare(node[k]);
|
|
1709
|
-
}
|
|
1710
|
-
return out;
|
|
1711
|
-
}
|
|
1712
|
-
function astEquivalent(a, b) {
|
|
1713
|
-
try {
|
|
1714
|
-
return JSON.stringify(normalizeAstForCompare(a)) === JSON.stringify(normalizeAstForCompare(b));
|
|
1715
|
-
} catch {
|
|
1716
|
-
return false;
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
1916
|
function collectUilintRuleIdsFromRulesObject(rulesObj) {
|
|
1720
1917
|
const ids = /* @__PURE__ */ new Set();
|
|
1721
1918
|
if (!rulesObj || rulesObj.type !== "ObjectExpression") return ids;
|
|
@@ -1726,13 +1923,14 @@ function collectUilintRuleIdsFromRulesObject(rulesObj) {
|
|
|
1726
1923
|
if (!isStringLiteral(key)) continue;
|
|
1727
1924
|
const val = key.value;
|
|
1728
1925
|
if (typeof val !== "string") continue;
|
|
1729
|
-
if (
|
|
1730
|
-
|
|
1926
|
+
if (val.startsWith("uilint/")) {
|
|
1927
|
+
ids.add(val.slice("uilint/".length));
|
|
1928
|
+
}
|
|
1731
1929
|
}
|
|
1732
1930
|
return ids;
|
|
1733
1931
|
}
|
|
1734
1932
|
function findExportedConfigArrayExpression(mod) {
|
|
1735
|
-
function
|
|
1933
|
+
function unwrapExpression2(expr) {
|
|
1736
1934
|
let e = expr;
|
|
1737
1935
|
while (e) {
|
|
1738
1936
|
if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
|
|
@@ -1758,11 +1956,11 @@ function findExportedConfigArrayExpression(mod) {
|
|
|
1758
1956
|
for (const decl of stmt.declarations ?? []) {
|
|
1759
1957
|
const id = decl?.id;
|
|
1760
1958
|
if (!isIdentifier(id, name)) continue;
|
|
1761
|
-
const init =
|
|
1959
|
+
const init = unwrapExpression2(decl?.init);
|
|
1762
1960
|
if (!init) return null;
|
|
1763
1961
|
if (init.type === "ArrayExpression") return init;
|
|
1764
|
-
if (init.type === "CallExpression" && isIdentifier(init.callee, "defineConfig") &&
|
|
1765
|
-
return
|
|
1962
|
+
if (init.type === "CallExpression" && isIdentifier(init.callee, "defineConfig") && unwrapExpression2(init.arguments?.[0])?.type === "ArrayExpression") {
|
|
1963
|
+
return unwrapExpression2(init.arguments?.[0]);
|
|
1766
1964
|
}
|
|
1767
1965
|
return null;
|
|
1768
1966
|
}
|
|
@@ -1773,15 +1971,15 @@ function findExportedConfigArrayExpression(mod) {
|
|
|
1773
1971
|
if (program2 && program2.type === "Program") {
|
|
1774
1972
|
for (const stmt of program2.body ?? []) {
|
|
1775
1973
|
if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
|
|
1776
|
-
const decl =
|
|
1974
|
+
const decl = unwrapExpression2(stmt.declaration);
|
|
1777
1975
|
if (!decl) break;
|
|
1778
1976
|
if (decl.type === "ArrayExpression") {
|
|
1779
1977
|
return { kind: "esm", arrayExpr: decl, program: program2 };
|
|
1780
1978
|
}
|
|
1781
|
-
if (decl.type === "CallExpression" && isIdentifier(decl.callee, "defineConfig") &&
|
|
1979
|
+
if (decl.type === "CallExpression" && isIdentifier(decl.callee, "defineConfig") && unwrapExpression2(decl.arguments?.[0])?.type === "ArrayExpression") {
|
|
1782
1980
|
return {
|
|
1783
1981
|
kind: "esm",
|
|
1784
|
-
arrayExpr:
|
|
1982
|
+
arrayExpr: unwrapExpression2(decl.arguments?.[0]),
|
|
1785
1983
|
program: program2
|
|
1786
1984
|
};
|
|
1787
1985
|
}
|
|
@@ -1820,24 +2018,6 @@ function findExportedConfigArrayExpression(mod) {
|
|
|
1820
2018
|
}
|
|
1821
2019
|
return null;
|
|
1822
2020
|
}
|
|
1823
|
-
function findUsesUilintConfigs(program2) {
|
|
1824
|
-
let found = false;
|
|
1825
|
-
walkAst(program2, (n) => {
|
|
1826
|
-
if (found) return;
|
|
1827
|
-
if (n?.type === "MemberExpression") {
|
|
1828
|
-
const obj = n.object;
|
|
1829
|
-
const prop = n.property;
|
|
1830
|
-
if (isIdentifier(prop, "configs") && isIdentifier(obj, "uilint")) {
|
|
1831
|
-
found = true;
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
if (obj?.type === "MemberExpression" && isIdentifier(obj.object, "uilint") && isIdentifier(obj.property, "configs")) {
|
|
1835
|
-
found = true;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
});
|
|
1839
|
-
return found;
|
|
1840
|
-
}
|
|
1841
2021
|
function collectConfiguredUilintRuleIdsFromConfigArray(arrayExpr) {
|
|
1842
2022
|
const ids = /* @__PURE__ */ new Set();
|
|
1843
2023
|
if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return ids;
|
|
@@ -1890,69 +2070,68 @@ function chooseUniqueIdentifier(base, used) {
|
|
|
1890
2070
|
while (used.has(`${base}${i}`)) i++;
|
|
1891
2071
|
return `${base}${i}`;
|
|
1892
2072
|
}
|
|
1893
|
-
function
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
);
|
|
1898
|
-
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
const id = decl?.id;
|
|
1916
|
-
const init = decl?.init;
|
|
1917
|
-
if (!isIdentifier(id)) continue;
|
|
1918
|
-
if (init?.type === "CallExpression" && isIdentifier(init.callee, "require") && isStringLiteral(init.arguments?.[0]) && init.arguments[0].value === "uilint-eslint") {
|
|
1919
|
-
return id.name;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
2073
|
+
function addLocalRuleImportsAst(mod, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
|
|
2074
|
+
const importNames = /* @__PURE__ */ new Map();
|
|
2075
|
+
let changed = false;
|
|
2076
|
+
const configDir = dirname6(configPath);
|
|
2077
|
+
const rulesDir = join7(rulesRoot, ".uilint", "rules");
|
|
2078
|
+
const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
|
|
2079
|
+
const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
|
|
2080
|
+
const used = collectTopLevelBindings(mod.$ast);
|
|
2081
|
+
for (const rule of selectedRules) {
|
|
2082
|
+
const importName = chooseUniqueIdentifier(
|
|
2083
|
+
`${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
|
|
2084
|
+
used
|
|
2085
|
+
);
|
|
2086
|
+
importNames.set(rule.id, importName);
|
|
2087
|
+
used.add(importName);
|
|
2088
|
+
const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
|
|
2089
|
+
mod.imports.$add({
|
|
2090
|
+
imported: "default",
|
|
2091
|
+
local: importName,
|
|
2092
|
+
from: rulePath
|
|
2093
|
+
});
|
|
2094
|
+
changed = true;
|
|
1922
2095
|
}
|
|
1923
|
-
return
|
|
2096
|
+
return { importNames, changed };
|
|
1924
2097
|
}
|
|
1925
|
-
function
|
|
2098
|
+
function addLocalRuleRequiresAst(program2, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
|
|
2099
|
+
const importNames = /* @__PURE__ */ new Map();
|
|
2100
|
+
let changed = false;
|
|
1926
2101
|
if (!program2 || program2.type !== "Program") {
|
|
1927
|
-
return {
|
|
2102
|
+
return { importNames, changed };
|
|
1928
2103
|
}
|
|
1929
|
-
const
|
|
1930
|
-
|
|
2104
|
+
const configDir = dirname6(configPath);
|
|
2105
|
+
const rulesDir = join7(rulesRoot, ".uilint", "rules");
|
|
2106
|
+
const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
|
|
2107
|
+
const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
|
|
1931
2108
|
const used = collectTopLevelBindings(program2);
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
}
|
|
1955
|
-
|
|
2109
|
+
for (const rule of selectedRules) {
|
|
2110
|
+
const importName = chooseUniqueIdentifier(
|
|
2111
|
+
`${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
|
|
2112
|
+
used
|
|
2113
|
+
);
|
|
2114
|
+
importNames.set(rule.id, importName);
|
|
2115
|
+
used.add(importName);
|
|
2116
|
+
const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
|
|
2117
|
+
const stmtMod = parseModule(
|
|
2118
|
+
`const ${importName} = require("${rulePath}");`
|
|
2119
|
+
);
|
|
2120
|
+
const stmt = stmtMod.$ast.body?.[0];
|
|
2121
|
+
if (stmt) {
|
|
2122
|
+
let insertAt = 0;
|
|
2123
|
+
const first = program2.body?.[0];
|
|
2124
|
+
if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
|
|
2125
|
+
insertAt = 1;
|
|
2126
|
+
}
|
|
2127
|
+
program2.body.splice(insertAt, 0, stmt);
|
|
2128
|
+
changed = true;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return { importNames, changed };
|
|
2132
|
+
}
|
|
2133
|
+
function appendUilintConfigBlockToArray(arrayExpr, selectedRules, ruleImportNames) {
|
|
2134
|
+
const pluginRulesCode = Array.from(ruleImportNames.entries()).map(([ruleId, importName]) => ` "${ruleId}": ${importName},`).join("\n");
|
|
1956
2135
|
const rulesPropsCode = selectedRules.map((r) => {
|
|
1957
2136
|
const ruleKey = `uilint/${r.id}`;
|
|
1958
2137
|
const valueCode = r.defaultOptions && r.defaultOptions.length > 0 ? `["${r.defaultSeverity}", ...${JSON.stringify(
|
|
@@ -1968,7 +2147,13 @@ function appendUilintConfigBlockToArray(arrayExpr, selectedRules, uilintRef) {
|
|
|
1968
2147
|
"app/**/*.{js,jsx,ts,tsx}",
|
|
1969
2148
|
"pages/**/*.{js,jsx,ts,tsx}",
|
|
1970
2149
|
],
|
|
1971
|
-
plugins: {
|
|
2150
|
+
plugins: {
|
|
2151
|
+
uilint: {
|
|
2152
|
+
rules: {
|
|
2153
|
+
${pluginRulesCode}
|
|
2154
|
+
},
|
|
2155
|
+
},
|
|
2156
|
+
},
|
|
1972
2157
|
rules: {
|
|
1973
2158
|
${rulesPropsCode}
|
|
1974
2159
|
},
|
|
@@ -1985,14 +2170,13 @@ function getUilintEslintConfigInfoFromSourceAst(source) {
|
|
|
1985
2170
|
error: "Could not locate an exported ESLint flat config array (expected `export default [...]`, `export default defineConfig([...])`, `module.exports = [...]`, or `module.exports = defineConfig([...])`)."
|
|
1986
2171
|
};
|
|
1987
2172
|
}
|
|
1988
|
-
const usesUilintConfigs = findUsesUilintConfigs(found.program);
|
|
1989
2173
|
const configuredRuleIds = collectConfiguredUilintRuleIdsFromConfigArray(
|
|
1990
2174
|
found.arrayExpr
|
|
1991
2175
|
);
|
|
1992
2176
|
const existingUilint = findExistingUilintRulesObject(found.arrayExpr);
|
|
1993
|
-
const configured =
|
|
2177
|
+
const configured = configuredRuleIds.size > 0 || existingUilint.configObj !== null;
|
|
1994
2178
|
return {
|
|
1995
|
-
info: {
|
|
2179
|
+
info: { configuredRuleIds, configured },
|
|
1996
2180
|
mod,
|
|
1997
2181
|
arrayExpr: found.arrayExpr,
|
|
1998
2182
|
kind: found.kind
|
|
@@ -2007,11 +2191,9 @@ function getUilintEslintConfigInfoFromSource(source) {
|
|
|
2007
2191
|
const ast = getUilintEslintConfigInfoFromSourceAst(source);
|
|
2008
2192
|
if ("error" in ast) {
|
|
2009
2193
|
const configuredRuleIds = extractConfiguredUilintRuleIds(source);
|
|
2010
|
-
const usesUilintConfigs = hasUilintConfigsUsage(source);
|
|
2011
2194
|
return {
|
|
2012
|
-
usesUilintConfigs,
|
|
2013
2195
|
configuredRuleIds,
|
|
2014
|
-
configured:
|
|
2196
|
+
configured: configuredRuleIds.size > 0
|
|
2015
2197
|
};
|
|
2016
2198
|
}
|
|
2017
2199
|
return ast.info;
|
|
@@ -2041,7 +2223,7 @@ async function installEslintPlugin(opts) {
|
|
|
2041
2223
|
};
|
|
2042
2224
|
}
|
|
2043
2225
|
const configFilename = getEslintConfigFilename(configPath);
|
|
2044
|
-
const original =
|
|
2226
|
+
const original = readFileSync4(configPath, "utf-8");
|
|
2045
2227
|
const isCommonJS = configPath.endsWith(".cjs");
|
|
2046
2228
|
const ast = getUilintEslintConfigInfoFromSourceAst(original);
|
|
2047
2229
|
if ("error" in ast) {
|
|
@@ -2054,10 +2236,15 @@ async function installEslintPlugin(opts) {
|
|
|
2054
2236
|
};
|
|
2055
2237
|
}
|
|
2056
2238
|
const { info, mod, arrayExpr, kind } = ast;
|
|
2057
|
-
const usesUilintConfigs = info.usesUilintConfigs;
|
|
2058
2239
|
const configuredIds = info.configuredRuleIds;
|
|
2059
|
-
const missingRules =
|
|
2060
|
-
|
|
2240
|
+
const missingRules = getMissingSelectedRules(
|
|
2241
|
+
opts.selectedRules,
|
|
2242
|
+
configuredIds
|
|
2243
|
+
);
|
|
2244
|
+
const rulesToUpdate = getRulesNeedingUpdate(
|
|
2245
|
+
opts.selectedRules,
|
|
2246
|
+
configuredIds
|
|
2247
|
+
);
|
|
2061
2248
|
let rulesToApply = [];
|
|
2062
2249
|
if (!info.configured) {
|
|
2063
2250
|
rulesToApply = opts.selectedRules;
|
|
@@ -2078,47 +2265,79 @@ async function installEslintPlugin(opts) {
|
|
|
2078
2265
|
}
|
|
2079
2266
|
}
|
|
2080
2267
|
}
|
|
2268
|
+
if (rulesToApply.length === 0) {
|
|
2269
|
+
return {
|
|
2270
|
+
configFile: configFilename,
|
|
2271
|
+
modified: false,
|
|
2272
|
+
missingRuleIds: missingRules.map((r) => r.id),
|
|
2273
|
+
configured: info.configured
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2081
2276
|
let modifiedAst = false;
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (!newProp) continue;
|
|
2097
|
-
if (existingProp) {
|
|
2098
|
-
if (!astEquivalent(existingProp.value, newProp.value)) {
|
|
2099
|
-
existingProp.value = newProp.value;
|
|
2100
|
-
changedRules = true;
|
|
2101
|
-
}
|
|
2102
|
-
} else {
|
|
2103
|
-
props.push(newProp);
|
|
2104
|
-
changedRules = true;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
if (changedRules) modifiedAst = true;
|
|
2108
|
-
} else {
|
|
2109
|
-
const uilintRef = kind === "esm" ? ensureUilintImportAst(mod).local : ensureUilintRequireAst(mod.$ast).local;
|
|
2110
|
-
appendUilintConfigBlockToArray(arrayExpr, rulesToApply, uilintRef);
|
|
2111
|
-
modifiedAst = true;
|
|
2277
|
+
const localRulesDir = join7(opts.projectPath, ".uilint", "rules");
|
|
2278
|
+
const workspaceRoot = findWorkspaceRoot4(opts.projectPath);
|
|
2279
|
+
const workspaceRulesDir = join7(workspaceRoot, ".uilint", "rules");
|
|
2280
|
+
const rulesRoot = existsSync8(localRulesDir) ? opts.projectPath : workspaceRoot;
|
|
2281
|
+
let fileExtension = ".js";
|
|
2282
|
+
if (rulesToApply.length > 0) {
|
|
2283
|
+
const firstRulePath = join7(
|
|
2284
|
+
rulesRoot,
|
|
2285
|
+
".uilint",
|
|
2286
|
+
"rules",
|
|
2287
|
+
`${rulesToApply[0].id}.ts`
|
|
2288
|
+
);
|
|
2289
|
+
if (existsSync8(firstRulePath)) {
|
|
2290
|
+
fileExtension = ".ts";
|
|
2112
2291
|
}
|
|
2113
|
-
} else if (!info.configured && !usesUilintConfigs) {
|
|
2114
2292
|
}
|
|
2115
|
-
|
|
2293
|
+
let ruleImportNames;
|
|
2294
|
+
if (kind === "esm") {
|
|
2295
|
+
const result = addLocalRuleImportsAst(
|
|
2296
|
+
mod,
|
|
2297
|
+
rulesToApply,
|
|
2298
|
+
configPath,
|
|
2299
|
+
rulesRoot,
|
|
2300
|
+
fileExtension
|
|
2301
|
+
);
|
|
2302
|
+
ruleImportNames = result.importNames;
|
|
2303
|
+
if (result.changed) modifiedAst = true;
|
|
2304
|
+
} else {
|
|
2305
|
+
const result = addLocalRuleRequiresAst(
|
|
2306
|
+
mod.$ast,
|
|
2307
|
+
rulesToApply,
|
|
2308
|
+
configPath,
|
|
2309
|
+
rulesRoot,
|
|
2310
|
+
fileExtension
|
|
2311
|
+
);
|
|
2312
|
+
ruleImportNames = result.importNames;
|
|
2313
|
+
if (result.changed) modifiedAst = true;
|
|
2314
|
+
}
|
|
2315
|
+
if (ruleImportNames && ruleImportNames.size > 0) {
|
|
2316
|
+
appendUilintConfigBlockToArray(arrayExpr, rulesToApply, ruleImportNames);
|
|
2317
|
+
modifiedAst = true;
|
|
2318
|
+
}
|
|
2319
|
+
if (!info.configured) {
|
|
2116
2320
|
if (kind === "esm") {
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2321
|
+
mod.imports.$add({
|
|
2322
|
+
imported: "createRule",
|
|
2323
|
+
local: "createRule",
|
|
2324
|
+
from: "uilint-eslint"
|
|
2325
|
+
});
|
|
2326
|
+
modifiedAst = true;
|
|
2327
|
+
} else {
|
|
2328
|
+
const stmtMod = parseModule(
|
|
2329
|
+
`const { createRule } = require("uilint-eslint");`
|
|
2330
|
+
);
|
|
2331
|
+
const stmt = stmtMod.$ast.body?.[0];
|
|
2332
|
+
if (stmt) {
|
|
2333
|
+
let insertAt = 0;
|
|
2334
|
+
const first = mod.$ast.body?.[0];
|
|
2335
|
+
if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
|
|
2336
|
+
insertAt = 1;
|
|
2337
|
+
}
|
|
2338
|
+
mod.$ast.body.splice(insertAt, 0, stmt);
|
|
2339
|
+
modifiedAst = true;
|
|
2340
|
+
}
|
|
2122
2341
|
}
|
|
2123
2342
|
}
|
|
2124
2343
|
const updated = modifiedAst ? generateCode(mod).code : original;
|
|
@@ -2143,36 +2362,36 @@ async function installEslintPlugin(opts) {
|
|
|
2143
2362
|
var LEGACY_HOOK_FILES = ["uilint-validate.sh", "uilint-validate.js"];
|
|
2144
2363
|
function safeParseJson(filePath) {
|
|
2145
2364
|
try {
|
|
2146
|
-
const content =
|
|
2365
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
2147
2366
|
return JSON.parse(content);
|
|
2148
2367
|
} catch {
|
|
2149
2368
|
return void 0;
|
|
2150
2369
|
}
|
|
2151
2370
|
}
|
|
2152
2371
|
async function analyze2(projectPath = process.cwd()) {
|
|
2153
|
-
const workspaceRoot =
|
|
2372
|
+
const workspaceRoot = findWorkspaceRoot5(projectPath);
|
|
2154
2373
|
const packageManager = detectPackageManager(projectPath);
|
|
2155
|
-
const cursorDir =
|
|
2156
|
-
const cursorDirExists =
|
|
2157
|
-
const mcpPath =
|
|
2158
|
-
const mcpExists =
|
|
2374
|
+
const cursorDir = join8(projectPath, ".cursor");
|
|
2375
|
+
const cursorDirExists = existsSync9(cursorDir);
|
|
2376
|
+
const mcpPath = join8(cursorDir, "mcp.json");
|
|
2377
|
+
const mcpExists = existsSync9(mcpPath);
|
|
2159
2378
|
const mcpConfig = mcpExists ? safeParseJson(mcpPath) : void 0;
|
|
2160
|
-
const hooksPath =
|
|
2161
|
-
const hooksExists =
|
|
2379
|
+
const hooksPath = join8(cursorDir, "hooks.json");
|
|
2380
|
+
const hooksExists = existsSync9(hooksPath);
|
|
2162
2381
|
const hooksConfig = hooksExists ? safeParseJson(hooksPath) : void 0;
|
|
2163
|
-
const hooksDir =
|
|
2382
|
+
const hooksDir = join8(cursorDir, "hooks");
|
|
2164
2383
|
const legacyPaths = [];
|
|
2165
2384
|
for (const legacyFile of LEGACY_HOOK_FILES) {
|
|
2166
|
-
const legacyPath =
|
|
2167
|
-
if (
|
|
2385
|
+
const legacyPath = join8(hooksDir, legacyFile);
|
|
2386
|
+
if (existsSync9(legacyPath)) {
|
|
2168
2387
|
legacyPaths.push(legacyPath);
|
|
2169
2388
|
}
|
|
2170
2389
|
}
|
|
2171
|
-
const styleguidePath =
|
|
2172
|
-
const styleguideExists =
|
|
2173
|
-
const commandsDir =
|
|
2174
|
-
const genstyleguideExists =
|
|
2175
|
-
const genrulesExists =
|
|
2390
|
+
const styleguidePath = join8(projectPath, ".uilint", "styleguide.md");
|
|
2391
|
+
const styleguideExists = existsSync9(styleguidePath);
|
|
2392
|
+
const commandsDir = join8(cursorDir, "commands");
|
|
2393
|
+
const genstyleguideExists = existsSync9(join8(commandsDir, "genstyleguide.md"));
|
|
2394
|
+
const genrulesExists = existsSync9(join8(commandsDir, "genrules.md"));
|
|
2176
2395
|
const nextApps = [];
|
|
2177
2396
|
const directDetection = detectNextAppRouter(projectPath);
|
|
2178
2397
|
if (directDetection) {
|
|
@@ -2186,6 +2405,19 @@ async function analyze2(projectPath = process.cwd()) {
|
|
|
2186
2405
|
});
|
|
2187
2406
|
}
|
|
2188
2407
|
}
|
|
2408
|
+
const viteApps = [];
|
|
2409
|
+
const directVite = detectViteReact(projectPath);
|
|
2410
|
+
if (directVite) {
|
|
2411
|
+
viteApps.push({ projectPath, detection: directVite });
|
|
2412
|
+
} else {
|
|
2413
|
+
const matches = findViteReactProjects(workspaceRoot, { maxDepth: 5 });
|
|
2414
|
+
for (const match of matches) {
|
|
2415
|
+
viteApps.push({
|
|
2416
|
+
projectPath: match.projectPath,
|
|
2417
|
+
detection: match.detection
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2189
2421
|
const rawPackages = findPackages(workspaceRoot);
|
|
2190
2422
|
const packages = rawPackages.map((pkg) => {
|
|
2191
2423
|
const eslintConfigPath = findEslintConfigFile(pkg.path);
|
|
@@ -2195,9 +2427,9 @@ async function analyze2(projectPath = process.cwd()) {
|
|
|
2195
2427
|
if (eslintConfigPath) {
|
|
2196
2428
|
eslintConfigFilename = getEslintConfigFilename(eslintConfigPath);
|
|
2197
2429
|
try {
|
|
2198
|
-
const source =
|
|
2430
|
+
const source = readFileSync5(eslintConfigPath, "utf-8");
|
|
2199
2431
|
const info = getUilintEslintConfigInfoFromSource(source);
|
|
2200
|
-
hasRules = info.configuredRuleIds.size > 0
|
|
2432
|
+
hasRules = info.configuredRuleIds.size > 0;
|
|
2201
2433
|
configuredRuleIds = Array.from(info.configuredRuleIds);
|
|
2202
2434
|
} catch {
|
|
2203
2435
|
}
|
|
@@ -2239,13 +2471,14 @@ async function analyze2(projectPath = process.cwd()) {
|
|
|
2239
2471
|
genrules: genrulesExists
|
|
2240
2472
|
},
|
|
2241
2473
|
nextApps,
|
|
2474
|
+
viteApps,
|
|
2242
2475
|
packages
|
|
2243
2476
|
};
|
|
2244
2477
|
}
|
|
2245
2478
|
|
|
2246
2479
|
// src/commands/install/plan.ts
|
|
2247
|
-
import { join as
|
|
2248
|
-
import { createRequire } from "module";
|
|
2480
|
+
import { join as join11 } from "path";
|
|
2481
|
+
import { createRequire as createRequire2 } from "module";
|
|
2249
2482
|
|
|
2250
2483
|
// src/commands/install/constants.ts
|
|
2251
2484
|
var HOOKS_CONFIG = {
|
|
@@ -2633,11 +2866,179 @@ Generate in \`.uilint/rules/\`:
|
|
|
2633
2866
|
- **Minimal rules** - generate 3-5 high-impact rules, not dozens
|
|
2634
2867
|
`;
|
|
2635
2868
|
|
|
2636
|
-
// src/
|
|
2869
|
+
// src/utils/skill-loader.ts
|
|
2870
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2, existsSync as existsSync10 } from "fs";
|
|
2871
|
+
import { join as join9, dirname as dirname7, relative as relative3 } from "path";
|
|
2872
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2873
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
2874
|
+
var __dirname = dirname7(__filename);
|
|
2875
|
+
function getSkillsDir() {
|
|
2876
|
+
const devPath = join9(__dirname, "..", "..", "skills");
|
|
2877
|
+
const prodPath = join9(__dirname, "..", "skills");
|
|
2878
|
+
if (existsSync10(devPath)) {
|
|
2879
|
+
return devPath;
|
|
2880
|
+
}
|
|
2881
|
+
if (existsSync10(prodPath)) {
|
|
2882
|
+
return prodPath;
|
|
2883
|
+
}
|
|
2884
|
+
throw new Error(
|
|
2885
|
+
"Could not find skills directory. This is a bug in uilint installation."
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
function collectFiles(dir, baseDir) {
|
|
2889
|
+
const files = [];
|
|
2890
|
+
const entries = readdirSync4(dir);
|
|
2891
|
+
for (const entry of entries) {
|
|
2892
|
+
const fullPath = join9(dir, entry);
|
|
2893
|
+
const stat = statSync2(fullPath);
|
|
2894
|
+
if (stat.isDirectory()) {
|
|
2895
|
+
files.push(...collectFiles(fullPath, baseDir));
|
|
2896
|
+
} else if (stat.isFile()) {
|
|
2897
|
+
const relativePath = relative3(baseDir, fullPath);
|
|
2898
|
+
const content = readFileSync6(fullPath, "utf-8");
|
|
2899
|
+
files.push({ relativePath, content });
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return files;
|
|
2903
|
+
}
|
|
2904
|
+
function loadSkill(name) {
|
|
2905
|
+
const skillsDir = getSkillsDir();
|
|
2906
|
+
const skillDir = join9(skillsDir, name);
|
|
2907
|
+
if (!existsSync10(skillDir)) {
|
|
2908
|
+
throw new Error(`Skill "${name}" not found in ${skillsDir}`);
|
|
2909
|
+
}
|
|
2910
|
+
const skillMdPath = join9(skillDir, "SKILL.md");
|
|
2911
|
+
if (!existsSync10(skillMdPath)) {
|
|
2912
|
+
throw new Error(`Skill "${name}" is missing SKILL.md`);
|
|
2913
|
+
}
|
|
2914
|
+
const files = collectFiles(skillDir, skillDir);
|
|
2915
|
+
return { name, files };
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// src/utils/rule-loader.ts
|
|
2919
|
+
import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
|
|
2920
|
+
import { join as join10, dirname as dirname8 } from "path";
|
|
2921
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2922
|
+
import { createRequire } from "module";
|
|
2923
|
+
var __filename2 = fileURLToPath3(import.meta.url);
|
|
2924
|
+
var __dirname2 = dirname8(__filename2);
|
|
2637
2925
|
var require2 = createRequire(import.meta.url);
|
|
2926
|
+
function getUilintEslintPackageRoot() {
|
|
2927
|
+
const entry = require2.resolve("uilint-eslint");
|
|
2928
|
+
const entryDir = dirname8(entry);
|
|
2929
|
+
return dirname8(entryDir);
|
|
2930
|
+
}
|
|
2931
|
+
function getUilintEslintSrcDir() {
|
|
2932
|
+
const devPath = join10(
|
|
2933
|
+
__dirname2,
|
|
2934
|
+
"..",
|
|
2935
|
+
"..",
|
|
2936
|
+
"..",
|
|
2937
|
+
"..",
|
|
2938
|
+
"uilint-eslint",
|
|
2939
|
+
"src"
|
|
2940
|
+
);
|
|
2941
|
+
if (existsSync11(devPath)) return devPath;
|
|
2942
|
+
const pkgRoot = getUilintEslintPackageRoot();
|
|
2943
|
+
const srcPath = join10(pkgRoot, "src");
|
|
2944
|
+
if (existsSync11(srcPath)) return srcPath;
|
|
2945
|
+
throw new Error(
|
|
2946
|
+
'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.'
|
|
2947
|
+
);
|
|
2948
|
+
}
|
|
2949
|
+
function getUilintEslintDistDir() {
|
|
2950
|
+
const devPath = join10(
|
|
2951
|
+
__dirname2,
|
|
2952
|
+
"..",
|
|
2953
|
+
"..",
|
|
2954
|
+
"..",
|
|
2955
|
+
"..",
|
|
2956
|
+
"uilint-eslint",
|
|
2957
|
+
"dist"
|
|
2958
|
+
);
|
|
2959
|
+
if (existsSync11(devPath)) return devPath;
|
|
2960
|
+
const pkgRoot = getUilintEslintPackageRoot();
|
|
2961
|
+
const distPath = join10(pkgRoot, "dist");
|
|
2962
|
+
if (existsSync11(distPath)) return distPath;
|
|
2963
|
+
throw new Error(
|
|
2964
|
+
'Could not find uilint-eslint "dist/" directory. This is a bug in uilint installation.'
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
function transformRuleContent(content) {
|
|
2968
|
+
let transformed = content;
|
|
2969
|
+
transformed = transformed.replace(
|
|
2970
|
+
/import\s+{\s*createRule\s*}\s+from\s+["']\.\.\/utils\/create-rule\.js["'];?/g,
|
|
2971
|
+
'import { createRule } from "uilint-eslint";'
|
|
2972
|
+
);
|
|
2973
|
+
transformed = transformed.replace(
|
|
2974
|
+
/import\s+createRule\s+from\s+["']\.\.\/utils\/create-rule\.js["'];?/g,
|
|
2975
|
+
'import { createRule } from "uilint-eslint";'
|
|
2976
|
+
);
|
|
2977
|
+
transformed = transformed.replace(
|
|
2978
|
+
/import\s+{([^}]+)}\s+from\s+["']\.\.\/utils\/([^"']+)\.js["'];?/g,
|
|
2979
|
+
(match, imports, utilFile) => {
|
|
2980
|
+
const utilsFromPackage = ["cache", "styleguide-loader", "import-graph"];
|
|
2981
|
+
if (utilsFromPackage.includes(utilFile)) {
|
|
2982
|
+
return `import {${imports}} from "uilint-eslint";`;
|
|
2983
|
+
}
|
|
2984
|
+
return match;
|
|
2985
|
+
}
|
|
2986
|
+
);
|
|
2987
|
+
return transformed;
|
|
2988
|
+
}
|
|
2989
|
+
function loadRule(ruleId, options = { typescript: true }) {
|
|
2990
|
+
const { typescript } = options;
|
|
2991
|
+
const extension = typescript ? ".ts" : ".js";
|
|
2992
|
+
if (typescript) {
|
|
2993
|
+
const rulesDir = join10(getUilintEslintSrcDir(), "rules");
|
|
2994
|
+
const implPath = join10(rulesDir, `${ruleId}.ts`);
|
|
2995
|
+
const testPath = join10(rulesDir, `${ruleId}.test.ts`);
|
|
2996
|
+
if (!existsSync11(implPath)) {
|
|
2997
|
+
throw new Error(`Rule "${ruleId}" not found at ${implPath}`);
|
|
2998
|
+
}
|
|
2999
|
+
const rawContent = readFileSync7(implPath, "utf-8");
|
|
3000
|
+
const transformedContent = transformRuleContent(rawContent);
|
|
3001
|
+
const implementation = {
|
|
3002
|
+
relativePath: `${ruleId}.ts`,
|
|
3003
|
+
content: transformedContent
|
|
3004
|
+
};
|
|
3005
|
+
const test = existsSync11(testPath) ? {
|
|
3006
|
+
relativePath: `${ruleId}.test.ts`,
|
|
3007
|
+
content: transformRuleContent(readFileSync7(testPath, "utf-8"))
|
|
3008
|
+
} : void 0;
|
|
3009
|
+
return {
|
|
3010
|
+
ruleId,
|
|
3011
|
+
implementation,
|
|
3012
|
+
test
|
|
3013
|
+
};
|
|
3014
|
+
} else {
|
|
3015
|
+
const rulesDir = join10(getUilintEslintDistDir(), "rules");
|
|
3016
|
+
const implPath = join10(rulesDir, `${ruleId}.js`);
|
|
3017
|
+
if (!existsSync11(implPath)) {
|
|
3018
|
+
throw new Error(
|
|
3019
|
+
`Rule "${ruleId}" not found at ${implPath}. Make sure uilint-eslint has been built.`
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
const content = readFileSync7(implPath, "utf-8");
|
|
3023
|
+
const implementation = {
|
|
3024
|
+
relativePath: `${ruleId}.js`,
|
|
3025
|
+
content
|
|
3026
|
+
};
|
|
3027
|
+
return {
|
|
3028
|
+
ruleId,
|
|
3029
|
+
implementation
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
function loadSelectedRules(ruleIds, options = { typescript: true }) {
|
|
3034
|
+
return ruleIds.map((id) => loadRule(id, options));
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// src/commands/install/plan.ts
|
|
3038
|
+
var require3 = createRequire2(import.meta.url);
|
|
2638
3039
|
function getSelfDependencyVersionRange(pkgName) {
|
|
2639
3040
|
try {
|
|
2640
|
-
const pkgJson =
|
|
3041
|
+
const pkgJson = require3("uilint/package.json");
|
|
2641
3042
|
const deps = pkgJson?.dependencies;
|
|
2642
3043
|
const optDeps = pkgJson?.optionalDependencies;
|
|
2643
3044
|
const peerDeps = pkgJson?.peerDependencies;
|
|
@@ -2683,7 +3084,7 @@ function createPlan(state, choices, options = {}) {
|
|
|
2683
3084
|
const dependencies = [];
|
|
2684
3085
|
const { force = false } = options;
|
|
2685
3086
|
const { items } = choices;
|
|
2686
|
-
const needsCursorDir = items.includes("mcp") || items.includes("hooks") || items.includes("genstyleguide") || items.includes("genrules");
|
|
3087
|
+
const needsCursorDir = items.includes("mcp") || items.includes("hooks") || items.includes("genstyleguide") || items.includes("genrules") || items.includes("skill");
|
|
2687
3088
|
if (needsCursorDir && !state.cursorDir.exists) {
|
|
2688
3089
|
actions.push({
|
|
2689
3090
|
type: "create_directory",
|
|
@@ -2712,7 +3113,7 @@ function createPlan(state, choices, options = {}) {
|
|
|
2712
3113
|
}
|
|
2713
3114
|
}
|
|
2714
3115
|
if (items.includes("hooks")) {
|
|
2715
|
-
const hooksDir =
|
|
3116
|
+
const hooksDir = join11(state.cursorDir.path, "hooks");
|
|
2716
3117
|
actions.push({
|
|
2717
3118
|
type: "create_directory",
|
|
2718
3119
|
path: hooksDir
|
|
@@ -2738,58 +3139,92 @@ function createPlan(state, choices, options = {}) {
|
|
|
2738
3139
|
});
|
|
2739
3140
|
actions.push({
|
|
2740
3141
|
type: "create_file",
|
|
2741
|
-
path:
|
|
3142
|
+
path: join11(hooksDir, "uilint-session-start.sh"),
|
|
2742
3143
|
content: SESSION_START_SCRIPT,
|
|
2743
3144
|
permissions: 493
|
|
2744
3145
|
});
|
|
2745
3146
|
actions.push({
|
|
2746
3147
|
type: "create_file",
|
|
2747
|
-
path:
|
|
3148
|
+
path: join11(hooksDir, "uilint-track.sh"),
|
|
2748
3149
|
content: TRACK_SCRIPT,
|
|
2749
3150
|
permissions: 493
|
|
2750
3151
|
});
|
|
2751
3152
|
actions.push({
|
|
2752
3153
|
type: "create_file",
|
|
2753
|
-
path:
|
|
3154
|
+
path: join11(hooksDir, "uilint-session-end.sh"),
|
|
2754
3155
|
content: SESSION_END_SCRIPT,
|
|
2755
3156
|
permissions: 493
|
|
2756
3157
|
});
|
|
2757
3158
|
}
|
|
2758
3159
|
if (items.includes("genstyleguide")) {
|
|
2759
|
-
const commandsDir =
|
|
3160
|
+
const commandsDir = join11(state.cursorDir.path, "commands");
|
|
2760
3161
|
actions.push({
|
|
2761
3162
|
type: "create_directory",
|
|
2762
3163
|
path: commandsDir
|
|
2763
3164
|
});
|
|
2764
3165
|
actions.push({
|
|
2765
3166
|
type: "create_file",
|
|
2766
|
-
path:
|
|
3167
|
+
path: join11(commandsDir, "genstyleguide.md"),
|
|
2767
3168
|
content: GENSTYLEGUIDE_COMMAND_MD
|
|
2768
3169
|
});
|
|
2769
3170
|
}
|
|
2770
3171
|
if (items.includes("genrules")) {
|
|
2771
|
-
const commandsDir =
|
|
3172
|
+
const commandsDir = join11(state.cursorDir.path, "commands");
|
|
2772
3173
|
actions.push({
|
|
2773
3174
|
type: "create_directory",
|
|
2774
3175
|
path: commandsDir
|
|
2775
3176
|
});
|
|
2776
3177
|
actions.push({
|
|
2777
3178
|
type: "create_file",
|
|
2778
|
-
path:
|
|
3179
|
+
path: join11(commandsDir, "genrules.md"),
|
|
2779
3180
|
content: GENRULES_COMMAND_MD
|
|
2780
3181
|
});
|
|
2781
3182
|
}
|
|
2782
|
-
if (items.includes("
|
|
2783
|
-
const
|
|
3183
|
+
if (items.includes("skill")) {
|
|
3184
|
+
const skillsDir = join11(state.cursorDir.path, "skills");
|
|
2784
3185
|
actions.push({
|
|
2785
|
-
type: "
|
|
2786
|
-
|
|
2787
|
-
appRoot: detection.appRoot
|
|
3186
|
+
type: "create_directory",
|
|
3187
|
+
path: skillsDir
|
|
2788
3188
|
});
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
3189
|
+
try {
|
|
3190
|
+
const skill = loadSkill("ui-consistency-enforcer");
|
|
3191
|
+
const skillDir = join11(skillsDir, skill.name);
|
|
3192
|
+
actions.push({
|
|
3193
|
+
type: "create_directory",
|
|
3194
|
+
path: skillDir
|
|
3195
|
+
});
|
|
3196
|
+
for (const file of skill.files) {
|
|
3197
|
+
const filePath = join11(skillDir, file.relativePath);
|
|
3198
|
+
const fileDir = join11(
|
|
3199
|
+
skillDir,
|
|
3200
|
+
file.relativePath.split("/").slice(0, -1).join("/")
|
|
3201
|
+
);
|
|
3202
|
+
if (fileDir !== skillDir && file.relativePath.includes("/")) {
|
|
3203
|
+
actions.push({
|
|
3204
|
+
type: "create_directory",
|
|
3205
|
+
path: fileDir
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
actions.push({
|
|
3209
|
+
type: "create_file",
|
|
3210
|
+
path: filePath,
|
|
3211
|
+
content: file.content
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
} catch {
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
if (items.includes("next") && choices.next) {
|
|
3218
|
+
const { projectPath, detection } = choices.next;
|
|
3219
|
+
actions.push({
|
|
3220
|
+
type: "install_next_routes",
|
|
3221
|
+
projectPath,
|
|
3222
|
+
appRoot: detection.appRoot
|
|
3223
|
+
});
|
|
3224
|
+
dependencies.push({
|
|
3225
|
+
packagePath: projectPath,
|
|
3226
|
+
packageManager: state.packageManager,
|
|
3227
|
+
packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
|
|
2793
3228
|
});
|
|
2794
3229
|
actions.push({
|
|
2795
3230
|
type: "inject_react",
|
|
@@ -2801,10 +3236,57 @@ function createPlan(state, choices, options = {}) {
|
|
|
2801
3236
|
projectPath
|
|
2802
3237
|
});
|
|
2803
3238
|
}
|
|
3239
|
+
if (items.includes("vite") && choices.vite) {
|
|
3240
|
+
const { projectPath, detection } = choices.vite;
|
|
3241
|
+
dependencies.push({
|
|
3242
|
+
packagePath: projectPath,
|
|
3243
|
+
packageManager: state.packageManager,
|
|
3244
|
+
packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
|
|
3245
|
+
});
|
|
3246
|
+
actions.push({
|
|
3247
|
+
type: "inject_react",
|
|
3248
|
+
projectPath,
|
|
3249
|
+
appRoot: detection.entryRoot,
|
|
3250
|
+
mode: "vite"
|
|
3251
|
+
});
|
|
3252
|
+
actions.push({
|
|
3253
|
+
type: "inject_vite_config",
|
|
3254
|
+
projectPath
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
2804
3257
|
if (items.includes("eslint") && choices.eslint) {
|
|
2805
3258
|
const { packagePaths, selectedRules } = choices.eslint;
|
|
2806
3259
|
for (const pkgPath of packagePaths) {
|
|
2807
3260
|
const pkgInfo = state.packages.find((p2) => p2.path === pkgPath);
|
|
3261
|
+
const rulesDir = join11(pkgPath, ".uilint", "rules");
|
|
3262
|
+
actions.push({
|
|
3263
|
+
type: "create_directory",
|
|
3264
|
+
path: rulesDir
|
|
3265
|
+
});
|
|
3266
|
+
const isTypeScript = pkgInfo?.isTypeScript ?? true;
|
|
3267
|
+
try {
|
|
3268
|
+
const ruleFiles = loadSelectedRules(
|
|
3269
|
+
selectedRules.map((r) => r.id),
|
|
3270
|
+
{
|
|
3271
|
+
typescript: isTypeScript
|
|
3272
|
+
}
|
|
3273
|
+
);
|
|
3274
|
+
for (const ruleFile of ruleFiles) {
|
|
3275
|
+
actions.push({
|
|
3276
|
+
type: "create_file",
|
|
3277
|
+
path: join11(rulesDir, ruleFile.implementation.relativePath),
|
|
3278
|
+
content: ruleFile.implementation.content
|
|
3279
|
+
});
|
|
3280
|
+
if (ruleFile.test && isTypeScript) {
|
|
3281
|
+
actions.push({
|
|
3282
|
+
type: "create_file",
|
|
3283
|
+
path: join11(rulesDir, ruleFile.test.relativePath),
|
|
3284
|
+
content: ruleFile.test.content
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
} catch {
|
|
3289
|
+
}
|
|
2808
3290
|
dependencies.push({
|
|
2809
3291
|
packagePath: pkgPath,
|
|
2810
3292
|
packageManager: state.packageManager,
|
|
@@ -2820,7 +3302,7 @@ function createPlan(state, choices, options = {}) {
|
|
|
2820
3302
|
});
|
|
2821
3303
|
}
|
|
2822
3304
|
}
|
|
2823
|
-
const gitignorePath =
|
|
3305
|
+
const gitignorePath = join11(state.workspaceRoot, ".gitignore");
|
|
2824
3306
|
actions.push({
|
|
2825
3307
|
type: "append_to_file",
|
|
2826
3308
|
path: gitignorePath,
|
|
@@ -2833,34 +3315,49 @@ function createPlan(state, choices, options = {}) {
|
|
|
2833
3315
|
|
|
2834
3316
|
// src/commands/install/execute.ts
|
|
2835
3317
|
import {
|
|
2836
|
-
existsSync as
|
|
3318
|
+
existsSync as existsSync16,
|
|
2837
3319
|
mkdirSync as mkdirSync3,
|
|
2838
|
-
writeFileSync as
|
|
2839
|
-
readFileSync as
|
|
3320
|
+
writeFileSync as writeFileSync7,
|
|
3321
|
+
readFileSync as readFileSync11,
|
|
2840
3322
|
unlinkSync,
|
|
2841
3323
|
chmodSync
|
|
2842
3324
|
} from "fs";
|
|
2843
|
-
import { dirname as
|
|
3325
|
+
import { dirname as dirname9 } from "path";
|
|
2844
3326
|
|
|
2845
3327
|
// src/utils/react-inject.ts
|
|
2846
|
-
import { existsSync as
|
|
2847
|
-
import { join as
|
|
3328
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
3329
|
+
import { join as join12 } from "path";
|
|
2848
3330
|
import { parseModule as parseModule2, generateCode as generateCode2 } from "magicast";
|
|
2849
3331
|
function getDefaultCandidates(projectPath, appRoot) {
|
|
3332
|
+
const viteMainCandidates = [
|
|
3333
|
+
join12(appRoot, "main.tsx"),
|
|
3334
|
+
join12(appRoot, "main.jsx"),
|
|
3335
|
+
join12(appRoot, "main.ts"),
|
|
3336
|
+
join12(appRoot, "main.js")
|
|
3337
|
+
];
|
|
3338
|
+
const existingViteMain = viteMainCandidates.filter(
|
|
3339
|
+
(rel) => existsSync12(join12(projectPath, rel))
|
|
3340
|
+
);
|
|
3341
|
+
if (existingViteMain.length > 0) return existingViteMain;
|
|
3342
|
+
const viteAppCandidates = [join12(appRoot, "App.tsx"), join12(appRoot, "App.jsx")];
|
|
3343
|
+
const existingViteApp = viteAppCandidates.filter(
|
|
3344
|
+
(rel) => existsSync12(join12(projectPath, rel))
|
|
3345
|
+
);
|
|
3346
|
+
if (existingViteApp.length > 0) return existingViteApp;
|
|
2850
3347
|
const layoutCandidates = [
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
3348
|
+
join12(appRoot, "layout.tsx"),
|
|
3349
|
+
join12(appRoot, "layout.jsx"),
|
|
3350
|
+
join12(appRoot, "layout.ts"),
|
|
3351
|
+
join12(appRoot, "layout.js")
|
|
2855
3352
|
];
|
|
2856
3353
|
const existingLayouts = layoutCandidates.filter(
|
|
2857
|
-
(rel) =>
|
|
3354
|
+
(rel) => existsSync12(join12(projectPath, rel))
|
|
2858
3355
|
);
|
|
2859
3356
|
if (existingLayouts.length > 0) {
|
|
2860
3357
|
return existingLayouts;
|
|
2861
3358
|
}
|
|
2862
|
-
const pageCandidates = [
|
|
2863
|
-
return pageCandidates.filter((rel) =>
|
|
3359
|
+
const pageCandidates = [join12(appRoot, "page.tsx"), join12(appRoot, "page.jsx")];
|
|
3360
|
+
return pageCandidates.filter((rel) => existsSync12(join12(projectPath, rel)));
|
|
2864
3361
|
}
|
|
2865
3362
|
function isUseClientDirective(stmt) {
|
|
2866
3363
|
return stmt?.type === "ExpressionStatement" && stmt.expression?.type === "StringLiteral" && stmt.expression.value === "use client";
|
|
@@ -2873,16 +3370,16 @@ function findImportDeclaration(program2, from) {
|
|
|
2873
3370
|
}
|
|
2874
3371
|
return null;
|
|
2875
3372
|
}
|
|
2876
|
-
function
|
|
3373
|
+
function walkAst(node, visit) {
|
|
2877
3374
|
if (!node || typeof node !== "object") return;
|
|
2878
3375
|
if (node.type) visit(node);
|
|
2879
3376
|
for (const key of Object.keys(node)) {
|
|
2880
3377
|
const v = node[key];
|
|
2881
3378
|
if (!v) continue;
|
|
2882
3379
|
if (Array.isArray(v)) {
|
|
2883
|
-
for (const item of v)
|
|
3380
|
+
for (const item of v) walkAst(item, visit);
|
|
2884
3381
|
} else if (typeof v === "object" && v.type) {
|
|
2885
|
-
|
|
3382
|
+
walkAst(v, visit);
|
|
2886
3383
|
}
|
|
2887
3384
|
}
|
|
2888
3385
|
}
|
|
@@ -2914,7 +3411,7 @@ function ensureNamedImport(program2, from, name) {
|
|
|
2914
3411
|
}
|
|
2915
3412
|
function hasUILintProviderJsx(program2) {
|
|
2916
3413
|
let found = false;
|
|
2917
|
-
|
|
3414
|
+
walkAst(program2, (node) => {
|
|
2918
3415
|
if (found) return;
|
|
2919
3416
|
if (node.type !== "JSXElement") return;
|
|
2920
3417
|
const name = node.openingElement?.name;
|
|
@@ -2934,7 +3431,7 @@ function wrapFirstChildrenExpressionWithProvider(program2) {
|
|
|
2934
3431
|
if (!providerJsx || providerJsx.type !== "JSXElement")
|
|
2935
3432
|
return { changed: false };
|
|
2936
3433
|
let replaced = false;
|
|
2937
|
-
|
|
3434
|
+
walkAst(program2, (node) => {
|
|
2938
3435
|
if (replaced) return;
|
|
2939
3436
|
if (node.type === "JSXExpressionContainer" && node.expression?.type === "Identifier" && node.expression.name === "children") {
|
|
2940
3437
|
Object.keys(node).forEach((k) => delete node[k]);
|
|
@@ -2947,11 +3444,44 @@ function wrapFirstChildrenExpressionWithProvider(program2) {
|
|
|
2947
3444
|
}
|
|
2948
3445
|
return { changed: true };
|
|
2949
3446
|
}
|
|
3447
|
+
function wrapFirstRenderCallArgumentWithProvider(program2) {
|
|
3448
|
+
if (!program2 || program2.type !== "Program") return { changed: false };
|
|
3449
|
+
if (hasUILintProviderJsx(program2)) return { changed: false };
|
|
3450
|
+
const providerMod = parseModule2(
|
|
3451
|
+
'const __uilint_provider = (<UILintProvider enabled={process.env.NODE_ENV !== "production"}></UILintProvider>);'
|
|
3452
|
+
);
|
|
3453
|
+
const providerJsx = providerMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
|
|
3454
|
+
if (!providerJsx || providerJsx.type !== "JSXElement")
|
|
3455
|
+
return { changed: false };
|
|
3456
|
+
providerJsx.children = providerJsx.children ?? [];
|
|
3457
|
+
let wrapped = false;
|
|
3458
|
+
walkAst(program2, (node) => {
|
|
3459
|
+
if (wrapped) return;
|
|
3460
|
+
if (node.type !== "CallExpression") return;
|
|
3461
|
+
const callee = node.callee;
|
|
3462
|
+
if (callee?.type !== "MemberExpression") return;
|
|
3463
|
+
const prop = callee.property;
|
|
3464
|
+
const isRender = prop?.type === "Identifier" && prop.name === "render" || prop?.type === "StringLiteral" && prop.value === "render" || prop?.type === "Literal" && prop.value === "render";
|
|
3465
|
+
if (!isRender) return;
|
|
3466
|
+
const arg0 = node.arguments?.[0];
|
|
3467
|
+
if (!arg0) return;
|
|
3468
|
+
if (arg0.type !== "JSXElement" && arg0.type !== "JSXFragment") return;
|
|
3469
|
+
providerJsx.children = [arg0];
|
|
3470
|
+
node.arguments[0] = providerJsx;
|
|
3471
|
+
wrapped = true;
|
|
3472
|
+
});
|
|
3473
|
+
if (!wrapped) {
|
|
3474
|
+
throw new Error(
|
|
3475
|
+
"Could not find a `.render(<...>)` call to wrap. Expected a React entry like `createRoot(...).render(<App />)`."
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
return { changed: true };
|
|
3479
|
+
}
|
|
2950
3480
|
async function installReactUILintOverlay(opts) {
|
|
2951
3481
|
const candidates = getDefaultCandidates(opts.projectPath, opts.appRoot);
|
|
2952
3482
|
if (!candidates.length) {
|
|
2953
3483
|
throw new Error(
|
|
2954
|
-
`No suitable
|
|
3484
|
+
`No suitable entry files found under ${opts.appRoot} (expected Next.js layout/page or Vite main/App).`
|
|
2955
3485
|
);
|
|
2956
3486
|
}
|
|
2957
3487
|
let chosen;
|
|
@@ -2960,8 +3490,8 @@ async function installReactUILintOverlay(opts) {
|
|
|
2960
3490
|
} else {
|
|
2961
3491
|
chosen = candidates[0];
|
|
2962
3492
|
}
|
|
2963
|
-
const absTarget =
|
|
2964
|
-
const original =
|
|
3493
|
+
const absTarget = join12(opts.projectPath, chosen);
|
|
3494
|
+
const original = readFileSync8(absTarget, "utf-8");
|
|
2965
3495
|
let mod;
|
|
2966
3496
|
try {
|
|
2967
3497
|
mod = parseModule2(original);
|
|
@@ -2979,7 +3509,8 @@ async function installReactUILintOverlay(opts) {
|
|
|
2979
3509
|
"UILintProvider"
|
|
2980
3510
|
);
|
|
2981
3511
|
if (importRes.changed) changed = true;
|
|
2982
|
-
const
|
|
3512
|
+
const mode = opts.mode ?? "next";
|
|
3513
|
+
const wrapRes = mode === "vite" ? wrapFirstRenderCallArgumentWithProvider(program2) : wrapFirstChildrenExpressionWithProvider(program2);
|
|
2983
3514
|
if (wrapRes.changed) changed = true;
|
|
2984
3515
|
const updated = changed ? generateCode2(mod).code : original;
|
|
2985
3516
|
const modified = updated !== original;
|
|
@@ -2994,14 +3525,14 @@ async function installReactUILintOverlay(opts) {
|
|
|
2994
3525
|
}
|
|
2995
3526
|
|
|
2996
3527
|
// src/utils/next-config-inject.ts
|
|
2997
|
-
import { existsSync as
|
|
2998
|
-
import { join as
|
|
3528
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
3529
|
+
import { join as join13 } from "path";
|
|
2999
3530
|
import { parseModule as parseModule3, generateCode as generateCode3 } from "magicast";
|
|
3000
3531
|
var CONFIG_EXTENSIONS2 = [".ts", ".mjs", ".js", ".cjs"];
|
|
3001
3532
|
function findNextConfigFile(projectPath) {
|
|
3002
3533
|
for (const ext of CONFIG_EXTENSIONS2) {
|
|
3003
|
-
const configPath =
|
|
3004
|
-
if (
|
|
3534
|
+
const configPath = join13(projectPath, `next.config${ext}`);
|
|
3535
|
+
if (existsSync13(configPath)) {
|
|
3005
3536
|
return configPath;
|
|
3006
3537
|
}
|
|
3007
3538
|
}
|
|
@@ -3114,7 +3645,7 @@ async function installJsxLocPlugin(opts) {
|
|
|
3114
3645
|
return { configFile: null, modified: false };
|
|
3115
3646
|
}
|
|
3116
3647
|
const configFilename = getNextConfigFilename(configPath);
|
|
3117
|
-
const original =
|
|
3648
|
+
const original = readFileSync9(configPath, "utf-8");
|
|
3118
3649
|
let mod;
|
|
3119
3650
|
try {
|
|
3120
3651
|
mod = parseModule3(original);
|
|
@@ -3143,10 +3674,221 @@ async function installJsxLocPlugin(opts) {
|
|
|
3143
3674
|
return { configFile: configFilename, modified: false };
|
|
3144
3675
|
}
|
|
3145
3676
|
|
|
3677
|
+
// src/utils/vite-config-inject.ts
|
|
3678
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
3679
|
+
import { join as join14 } from "path";
|
|
3680
|
+
import { parseModule as parseModule4, generateCode as generateCode4 } from "magicast";
|
|
3681
|
+
var CONFIG_EXTENSIONS3 = [".ts", ".mjs", ".js", ".cjs"];
|
|
3682
|
+
function findViteConfigFile2(projectPath) {
|
|
3683
|
+
for (const ext of CONFIG_EXTENSIONS3) {
|
|
3684
|
+
const configPath = join14(projectPath, `vite.config${ext}`);
|
|
3685
|
+
if (existsSync14(configPath)) return configPath;
|
|
3686
|
+
}
|
|
3687
|
+
return null;
|
|
3688
|
+
}
|
|
3689
|
+
function getViteConfigFilename(configPath) {
|
|
3690
|
+
const parts = configPath.split("/");
|
|
3691
|
+
return parts[parts.length - 1] || "vite.config.ts";
|
|
3692
|
+
}
|
|
3693
|
+
function isIdentifier3(node, name) {
|
|
3694
|
+
return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
|
|
3695
|
+
}
|
|
3696
|
+
function isStringLiteral3(node) {
|
|
3697
|
+
return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
|
|
3698
|
+
}
|
|
3699
|
+
function unwrapExpression(expr) {
|
|
3700
|
+
let e = expr;
|
|
3701
|
+
while (e) {
|
|
3702
|
+
if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
|
|
3703
|
+
e = e.expression;
|
|
3704
|
+
continue;
|
|
3705
|
+
}
|
|
3706
|
+
if (e.type === "TSSatisfiesExpression") {
|
|
3707
|
+
e = e.expression;
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
if (e.type === "ParenthesizedExpression") {
|
|
3711
|
+
e = e.expression;
|
|
3712
|
+
continue;
|
|
3713
|
+
}
|
|
3714
|
+
break;
|
|
3715
|
+
}
|
|
3716
|
+
return e;
|
|
3717
|
+
}
|
|
3718
|
+
function findExportedConfigObjectExpression(mod) {
|
|
3719
|
+
const program2 = mod?.$ast;
|
|
3720
|
+
if (!program2 || program2.type !== "Program") return null;
|
|
3721
|
+
for (const stmt of program2.body ?? []) {
|
|
3722
|
+
if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
|
|
3723
|
+
const decl = unwrapExpression(stmt.declaration);
|
|
3724
|
+
if (!decl) break;
|
|
3725
|
+
if (decl.type === "ObjectExpression") {
|
|
3726
|
+
return { kind: "esm", objExpr: decl, program: program2 };
|
|
3727
|
+
}
|
|
3728
|
+
if (decl.type === "CallExpression" && isIdentifier3(decl.callee, "defineConfig") && unwrapExpression(decl.arguments?.[0])?.type === "ObjectExpression") {
|
|
3729
|
+
return {
|
|
3730
|
+
kind: "esm",
|
|
3731
|
+
objExpr: unwrapExpression(decl.arguments?.[0]),
|
|
3732
|
+
program: program2
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
break;
|
|
3736
|
+
}
|
|
3737
|
+
for (const stmt of program2.body ?? []) {
|
|
3738
|
+
if (!stmt || stmt.type !== "ExpressionStatement") continue;
|
|
3739
|
+
const expr = stmt.expression;
|
|
3740
|
+
if (!expr || expr.type !== "AssignmentExpression") continue;
|
|
3741
|
+
const left = expr.left;
|
|
3742
|
+
const right = unwrapExpression(expr.right);
|
|
3743
|
+
const isModuleExports = left?.type === "MemberExpression" && isIdentifier3(left.object, "module") && isIdentifier3(left.property, "exports");
|
|
3744
|
+
if (!isModuleExports) continue;
|
|
3745
|
+
if (right?.type === "ObjectExpression") {
|
|
3746
|
+
return { kind: "cjs", objExpr: right, program: program2 };
|
|
3747
|
+
}
|
|
3748
|
+
if (right?.type === "CallExpression" && isIdentifier3(right.callee, "defineConfig") && unwrapExpression(right.arguments?.[0])?.type === "ObjectExpression") {
|
|
3749
|
+
return {
|
|
3750
|
+
kind: "cjs",
|
|
3751
|
+
objExpr: unwrapExpression(right.arguments?.[0]),
|
|
3752
|
+
program: program2
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
function getObjectProperty(obj, keyName) {
|
|
3759
|
+
if (!obj || obj.type !== "ObjectExpression") return null;
|
|
3760
|
+
for (const prop of obj.properties ?? []) {
|
|
3761
|
+
if (!prop) continue;
|
|
3762
|
+
if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
|
|
3763
|
+
const key = prop.key;
|
|
3764
|
+
const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral3(key) && key.value === keyName;
|
|
3765
|
+
if (keyMatch) return prop;
|
|
3766
|
+
}
|
|
3767
|
+
return null;
|
|
3768
|
+
}
|
|
3769
|
+
function ensureEsmJsxLocImport(program2) {
|
|
3770
|
+
if (!program2 || program2.type !== "Program") return { changed: false };
|
|
3771
|
+
const existing = (program2.body ?? []).find(
|
|
3772
|
+
(s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin/vite"
|
|
3773
|
+
);
|
|
3774
|
+
if (existing) {
|
|
3775
|
+
const has = (existing.specifiers ?? []).some(
|
|
3776
|
+
(sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "jsxLoc" || sp.imported?.value === "jsxLoc")
|
|
3777
|
+
);
|
|
3778
|
+
if (has) return { changed: false };
|
|
3779
|
+
const spec = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0]?.specifiers?.[0];
|
|
3780
|
+
if (!spec) return { changed: false };
|
|
3781
|
+
existing.specifiers = [...existing.specifiers ?? [], spec];
|
|
3782
|
+
return { changed: true };
|
|
3783
|
+
}
|
|
3784
|
+
const importDecl = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0];
|
|
3785
|
+
if (!importDecl) return { changed: false };
|
|
3786
|
+
const body = program2.body ?? [];
|
|
3787
|
+
let insertAt = 0;
|
|
3788
|
+
while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
|
|
3789
|
+
insertAt++;
|
|
3790
|
+
}
|
|
3791
|
+
program2.body.splice(insertAt, 0, importDecl);
|
|
3792
|
+
return { changed: true };
|
|
3793
|
+
}
|
|
3794
|
+
function ensureCjsJsxLocRequire(program2) {
|
|
3795
|
+
if (!program2 || program2.type !== "Program") return { changed: false };
|
|
3796
|
+
for (const stmt of program2.body ?? []) {
|
|
3797
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
3798
|
+
for (const decl of stmt.declarations ?? []) {
|
|
3799
|
+
const init = decl?.init;
|
|
3800
|
+
if (init?.type === "CallExpression" && isIdentifier3(init.callee, "require") && isStringLiteral3(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin/vite") {
|
|
3801
|
+
if (decl.id?.type === "ObjectPattern") {
|
|
3802
|
+
const has = (decl.id.properties ?? []).some((p2) => {
|
|
3803
|
+
if (p2?.type !== "ObjectProperty" && p2?.type !== "Property") return false;
|
|
3804
|
+
return isIdentifier3(p2.key, "jsxLoc");
|
|
3805
|
+
});
|
|
3806
|
+
if (has) return { changed: false };
|
|
3807
|
+
const prop = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
|
|
3808
|
+
if (!prop) return { changed: false };
|
|
3809
|
+
decl.id.properties = [...decl.id.properties ?? [], prop];
|
|
3810
|
+
return { changed: true };
|
|
3811
|
+
}
|
|
3812
|
+
return { changed: false };
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
const reqDecl = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0];
|
|
3817
|
+
if (!reqDecl) return { changed: false };
|
|
3818
|
+
program2.body.unshift(reqDecl);
|
|
3819
|
+
return { changed: true };
|
|
3820
|
+
}
|
|
3821
|
+
function pluginsHasJsxLoc(arr) {
|
|
3822
|
+
if (!arr || arr.type !== "ArrayExpression") return false;
|
|
3823
|
+
for (const el of arr.elements ?? []) {
|
|
3824
|
+
const e = unwrapExpression(el);
|
|
3825
|
+
if (!e) continue;
|
|
3826
|
+
if (e.type === "CallExpression" && isIdentifier3(e.callee, "jsxLoc")) return true;
|
|
3827
|
+
}
|
|
3828
|
+
return false;
|
|
3829
|
+
}
|
|
3830
|
+
function ensurePluginsContainsJsxLoc(configObj) {
|
|
3831
|
+
const pluginsProp = getObjectProperty(configObj, "plugins");
|
|
3832
|
+
if (!pluginsProp) {
|
|
3833
|
+
const prop = parseModule4("export default { plugins: [jsxLoc()] };").$ast.body?.find((s) => s.type === "ExportDefaultDeclaration")?.declaration?.properties?.find((p2) => {
|
|
3834
|
+
const k = p2?.key;
|
|
3835
|
+
return k?.type === "Identifier" && k.name === "plugins" || isStringLiteral3(k) && k.value === "plugins";
|
|
3836
|
+
});
|
|
3837
|
+
if (!prop) return { changed: false };
|
|
3838
|
+
configObj.properties = [...configObj.properties ?? [], prop];
|
|
3839
|
+
return { changed: true };
|
|
3840
|
+
}
|
|
3841
|
+
const value = unwrapExpression(pluginsProp.value);
|
|
3842
|
+
if (!value) return { changed: false };
|
|
3843
|
+
if (value.type === "ArrayExpression") {
|
|
3844
|
+
if (pluginsHasJsxLoc(value)) return { changed: false };
|
|
3845
|
+
const jsxLocCall2 = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
|
|
3846
|
+
if (!jsxLocCall2) return { changed: false };
|
|
3847
|
+
value.elements.push(jsxLocCall2);
|
|
3848
|
+
return { changed: true };
|
|
3849
|
+
}
|
|
3850
|
+
const jsxLocCall = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
|
|
3851
|
+
if (!jsxLocCall) return { changed: false };
|
|
3852
|
+
const spread = { type: "SpreadElement", argument: value };
|
|
3853
|
+
pluginsProp.value = { type: "ArrayExpression", elements: [spread, jsxLocCall] };
|
|
3854
|
+
return { changed: true };
|
|
3855
|
+
}
|
|
3856
|
+
async function installViteJsxLocPlugin(opts) {
|
|
3857
|
+
const configPath = findViteConfigFile2(opts.projectPath);
|
|
3858
|
+
if (!configPath) return { configFile: null, modified: false };
|
|
3859
|
+
const configFilename = getViteConfigFilename(configPath);
|
|
3860
|
+
const original = readFileSync10(configPath, "utf-8");
|
|
3861
|
+
const isCjs = configPath.endsWith(".cjs");
|
|
3862
|
+
let mod;
|
|
3863
|
+
try {
|
|
3864
|
+
mod = parseModule4(original);
|
|
3865
|
+
} catch {
|
|
3866
|
+
return { configFile: configFilename, modified: false };
|
|
3867
|
+
}
|
|
3868
|
+
const found = findExportedConfigObjectExpression(mod);
|
|
3869
|
+
if (!found) return { configFile: configFilename, modified: false };
|
|
3870
|
+
let changed = false;
|
|
3871
|
+
if (isCjs) {
|
|
3872
|
+
const reqRes = ensureCjsJsxLocRequire(found.program);
|
|
3873
|
+
if (reqRes.changed) changed = true;
|
|
3874
|
+
} else {
|
|
3875
|
+
const impRes = ensureEsmJsxLocImport(found.program);
|
|
3876
|
+
if (impRes.changed) changed = true;
|
|
3877
|
+
}
|
|
3878
|
+
const pluginsRes = ensurePluginsContainsJsxLoc(found.objExpr);
|
|
3879
|
+
if (pluginsRes.changed) changed = true;
|
|
3880
|
+
const updated = changed ? generateCode4(mod).code : original;
|
|
3881
|
+
if (updated !== original) {
|
|
3882
|
+
writeFileSync6(configPath, updated, "utf-8");
|
|
3883
|
+
return { configFile: configFilename, modified: true };
|
|
3884
|
+
}
|
|
3885
|
+
return { configFile: configFilename, modified: false };
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3146
3888
|
// src/utils/next-routes.ts
|
|
3147
|
-
import { existsSync as
|
|
3889
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3148
3890
|
import { mkdir, writeFile } from "fs/promises";
|
|
3149
|
-
import { join as
|
|
3891
|
+
import { join as join15 } from "path";
|
|
3150
3892
|
var DEV_SOURCE_ROUTE_TS = `/**
|
|
3151
3893
|
* Dev-only API route for fetching source files
|
|
3152
3894
|
*
|
|
@@ -3161,7 +3903,8 @@ var DEV_SOURCE_ROUTE_TS = `/**
|
|
|
3161
3903
|
|
|
3162
3904
|
import { NextRequest, NextResponse } from "next/server";
|
|
3163
3905
|
import { readFileSync, existsSync } from "fs";
|
|
3164
|
-
import { resolve, relative, dirname, extname } from "path";
|
|
3906
|
+
import { resolve, relative, dirname, extname, sep } from "path";
|
|
3907
|
+
import { fileURLToPath } from "url";
|
|
3165
3908
|
|
|
3166
3909
|
export const runtime = "nodejs";
|
|
3167
3910
|
|
|
@@ -3169,23 +3912,36 @@ export const runtime = "nodejs";
|
|
|
3169
3912
|
const ALLOWED_EXTENSIONS = new Set([".tsx", ".ts", ".jsx", ".js", ".css"]);
|
|
3170
3913
|
|
|
3171
3914
|
/**
|
|
3172
|
-
*
|
|
3915
|
+
* Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
|
|
3916
|
+
*
|
|
3917
|
+
* Why: In monorepos, process.cwd() might be the workspace root (it also has package.json),
|
|
3918
|
+
* which would incorrectly store/read files under the wrong directory.
|
|
3173
3919
|
*/
|
|
3174
|
-
function
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
) {
|
|
3182
|
-
return
|
|
3920
|
+
function findNextProjectRoot(): string {
|
|
3921
|
+
// Prefer discovering via this route module's on-disk path.
|
|
3922
|
+
// In Next, route code is executed from within ".next/server/...".
|
|
3923
|
+
try {
|
|
3924
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
3925
|
+
const marker = sep + ".next" + sep;
|
|
3926
|
+
const idx = selfPath.lastIndexOf(marker);
|
|
3927
|
+
if (idx !== -1) {
|
|
3928
|
+
return selfPath.slice(0, idx);
|
|
3183
3929
|
}
|
|
3930
|
+
} catch {
|
|
3931
|
+
// ignore
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
// Fallback: walk up from cwd looking for .next/
|
|
3935
|
+
let dir = process.cwd();
|
|
3936
|
+
for (let i = 0; i < 20; i++) {
|
|
3937
|
+
if (existsSync(resolve(dir, ".next"))) return dir;
|
|
3184
3938
|
const parent = dirname(dir);
|
|
3185
3939
|
if (parent === dir) break;
|
|
3186
3940
|
dir = parent;
|
|
3187
3941
|
}
|
|
3188
|
-
|
|
3942
|
+
|
|
3943
|
+
// Final fallback: cwd
|
|
3944
|
+
return process.cwd();
|
|
3189
3945
|
}
|
|
3190
3946
|
|
|
3191
3947
|
/**
|
|
@@ -3244,8 +4000,8 @@ export async function GET(request: NextRequest) {
|
|
|
3244
4000
|
);
|
|
3245
4001
|
}
|
|
3246
4002
|
|
|
3247
|
-
// Find project root
|
|
3248
|
-
const projectRoot =
|
|
4003
|
+
// Find project root (prefer Next project root over workspace root)
|
|
4004
|
+
const projectRoot = findNextProjectRoot();
|
|
3249
4005
|
|
|
3250
4006
|
// Resolve the file path
|
|
3251
4007
|
const resolvedPath = resolve(filePath);
|
|
@@ -3274,6 +4030,8 @@ export async function GET(request: NextRequest) {
|
|
|
3274
4030
|
return NextResponse.json({
|
|
3275
4031
|
content,
|
|
3276
4032
|
relativePath,
|
|
4033
|
+
projectRoot,
|
|
4034
|
+
workspaceRoot,
|
|
3277
4035
|
});
|
|
3278
4036
|
} catch (error) {
|
|
3279
4037
|
console.error("[Dev Source API] Error reading file:", error);
|
|
@@ -3281,20 +4039,331 @@ export async function GET(request: NextRequest) {
|
|
|
3281
4039
|
}
|
|
3282
4040
|
}
|
|
3283
4041
|
`;
|
|
4042
|
+
var SCREENSHOT_ROUTE_TS = `/**
|
|
4043
|
+
* Dev-only API route for saving and retrieving vision analysis screenshots
|
|
4044
|
+
*
|
|
4045
|
+
* This route allows the UILint overlay to:
|
|
4046
|
+
* - POST: Save screenshots and element manifests for vision analysis
|
|
4047
|
+
* - GET: Retrieve screenshots or list available screenshots
|
|
4048
|
+
*
|
|
4049
|
+
* Security:
|
|
4050
|
+
* - Only available in development mode
|
|
4051
|
+
* - Saves to .uilint/screenshots/ directory within project
|
|
4052
|
+
*/
|
|
4053
|
+
|
|
4054
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4055
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
4056
|
+
import { resolve, join, dirname, basename, sep } from "path";
|
|
4057
|
+
import { fileURLToPath } from "url";
|
|
4058
|
+
|
|
4059
|
+
export const runtime = "nodejs";
|
|
4060
|
+
|
|
4061
|
+
// Maximum screenshot size (10MB)
|
|
4062
|
+
const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
|
|
4063
|
+
|
|
4064
|
+
/**
|
|
4065
|
+
* Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
|
|
4066
|
+
*/
|
|
4067
|
+
function findNextProjectRoot(): string {
|
|
4068
|
+
try {
|
|
4069
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
4070
|
+
const marker = sep + ".next" + sep;
|
|
4071
|
+
const idx = selfPath.lastIndexOf(marker);
|
|
4072
|
+
if (idx !== -1) {
|
|
4073
|
+
return selfPath.slice(0, idx);
|
|
4074
|
+
}
|
|
4075
|
+
} catch {
|
|
4076
|
+
// ignore
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
let dir = process.cwd();
|
|
4080
|
+
for (let i = 0; i < 20; i++) {
|
|
4081
|
+
if (existsSync(resolve(dir, ".next"))) return dir;
|
|
4082
|
+
const parent = dirname(dir);
|
|
4083
|
+
if (parent === dir) break;
|
|
4084
|
+
dir = parent;
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
return process.cwd();
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
/**
|
|
4091
|
+
* Get the screenshots directory path, creating it if needed
|
|
4092
|
+
*/
|
|
4093
|
+
function getScreenshotsDir(projectRoot: string): string {
|
|
4094
|
+
const screenshotsDir = join(projectRoot, ".uilint", "screenshots");
|
|
4095
|
+
if (!existsSync(screenshotsDir)) {
|
|
4096
|
+
mkdirSync(screenshotsDir, { recursive: true });
|
|
4097
|
+
}
|
|
4098
|
+
return screenshotsDir;
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
/**
|
|
4102
|
+
* Validate filename to prevent path traversal
|
|
4103
|
+
*/
|
|
4104
|
+
function isValidFilename(filename: string): boolean {
|
|
4105
|
+
// Only allow alphanumeric, hyphens, underscores, and dots
|
|
4106
|
+
// Must end with .png, .jpeg, .jpg, or .json
|
|
4107
|
+
const validPattern = /^[a-zA-Z0-9_-]+\\.(png|jpeg|jpg|json)$/;
|
|
4108
|
+
return validPattern.test(filename) && !filename.includes("..");
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
/**
|
|
4112
|
+
* POST: Save a screenshot and optionally its manifest
|
|
4113
|
+
*/
|
|
4114
|
+
export async function POST(request: NextRequest) {
|
|
4115
|
+
// Block in production
|
|
4116
|
+
if (process.env.NODE_ENV === "production") {
|
|
4117
|
+
return NextResponse.json(
|
|
4118
|
+
{ error: "Not available in production" },
|
|
4119
|
+
{ status: 404 }
|
|
4120
|
+
);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
try {
|
|
4124
|
+
const body = await request.json();
|
|
4125
|
+
const { filename, imageData, manifest, analysisResult } = body;
|
|
4126
|
+
|
|
4127
|
+
if (!filename) {
|
|
4128
|
+
return NextResponse.json({ error: "Missing 'filename'" }, { status: 400 });
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
// Validate filename
|
|
4132
|
+
if (!isValidFilename(filename)) {
|
|
4133
|
+
return NextResponse.json(
|
|
4134
|
+
{ error: "Invalid filename format" },
|
|
4135
|
+
{ status: 400 }
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// Allow "sidecar-only" updates (manifest/analysisResult) without re-sending image bytes.
|
|
4140
|
+
const hasImageData = typeof imageData === "string" && imageData.length > 0;
|
|
4141
|
+
const hasSidecar =
|
|
4142
|
+
typeof manifest !== "undefined" || typeof analysisResult !== "undefined";
|
|
4143
|
+
|
|
4144
|
+
if (!hasImageData && !hasSidecar) {
|
|
4145
|
+
return NextResponse.json(
|
|
4146
|
+
{ error: "Nothing to save (provide imageData and/or manifest/analysisResult)" },
|
|
4147
|
+
{ status: 400 }
|
|
4148
|
+
);
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
// Check size (image only)
|
|
4152
|
+
if (hasImageData && imageData.length > MAX_SCREENSHOT_SIZE) {
|
|
4153
|
+
return NextResponse.json(
|
|
4154
|
+
{ error: "Screenshot too large (max 10MB)" },
|
|
4155
|
+
{ status: 413 }
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
const projectRoot = findNextProjectRoot();
|
|
4160
|
+
const screenshotsDir = getScreenshotsDir(projectRoot);
|
|
4161
|
+
|
|
4162
|
+
const imagePath = join(screenshotsDir, filename);
|
|
4163
|
+
|
|
4164
|
+
// Save the image (base64 data URL) if provided
|
|
4165
|
+
if (hasImageData) {
|
|
4166
|
+
const base64Data = imageData.includes(",")
|
|
4167
|
+
? imageData.split(",")[1]
|
|
4168
|
+
: imageData;
|
|
4169
|
+
writeFileSync(imagePath, Buffer.from(base64Data, "base64"));
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
// Save manifest and analysis result as JSON sidecar
|
|
4173
|
+
if (hasSidecar) {
|
|
4174
|
+
const jsonFilename = filename.replace(/\\.(png|jpeg|jpg)$/, ".json");
|
|
4175
|
+
const jsonPath = join(screenshotsDir, jsonFilename);
|
|
4176
|
+
|
|
4177
|
+
// If a sidecar already exists, merge updates (lets us POST analysisResult later without re-sending image).
|
|
4178
|
+
let existing: any = null;
|
|
4179
|
+
if (existsSync(jsonPath)) {
|
|
4180
|
+
try {
|
|
4181
|
+
existing = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
4182
|
+
} catch {
|
|
4183
|
+
existing = null;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
const routeFromAnalysis =
|
|
4188
|
+
analysisResult && typeof analysisResult === "object"
|
|
4189
|
+
? (analysisResult as any).route
|
|
4190
|
+
: undefined;
|
|
4191
|
+
const issuesFromAnalysis =
|
|
4192
|
+
analysisResult && typeof analysisResult === "object"
|
|
4193
|
+
? (analysisResult as any).issues
|
|
4194
|
+
: undefined;
|
|
4195
|
+
|
|
4196
|
+
const jsonData = {
|
|
4197
|
+
...(existing && typeof existing === "object" ? existing : {}),
|
|
4198
|
+
timestamp: Date.now(),
|
|
4199
|
+
filename,
|
|
4200
|
+
screenshotFile: filename,
|
|
4201
|
+
route:
|
|
4202
|
+
typeof routeFromAnalysis === "string"
|
|
4203
|
+
? routeFromAnalysis
|
|
4204
|
+
: (existing as any)?.route ?? null,
|
|
4205
|
+
issues:
|
|
4206
|
+
Array.isArray(issuesFromAnalysis)
|
|
4207
|
+
? issuesFromAnalysis
|
|
4208
|
+
: (existing as any)?.issues ?? null,
|
|
4209
|
+
manifest: typeof manifest === "undefined" ? existing?.manifest ?? null : manifest,
|
|
4210
|
+
analysisResult:
|
|
4211
|
+
typeof analysisResult === "undefined"
|
|
4212
|
+
? existing?.analysisResult ?? null
|
|
4213
|
+
: analysisResult,
|
|
4214
|
+
};
|
|
4215
|
+
writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
return NextResponse.json({
|
|
4219
|
+
success: true,
|
|
4220
|
+
path: imagePath,
|
|
4221
|
+
projectRoot,
|
|
4222
|
+
screenshotsDir,
|
|
4223
|
+
});
|
|
4224
|
+
} catch (error) {
|
|
4225
|
+
console.error("[Screenshot API] Error saving screenshot:", error);
|
|
4226
|
+
return NextResponse.json(
|
|
4227
|
+
{ error: "Failed to save screenshot" },
|
|
4228
|
+
{ status: 500 }
|
|
4229
|
+
);
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
/**
|
|
4234
|
+
* GET: Retrieve a screenshot or list available screenshots
|
|
4235
|
+
*/
|
|
4236
|
+
export async function GET(request: NextRequest) {
|
|
4237
|
+
// Block in production
|
|
4238
|
+
if (process.env.NODE_ENV === "production") {
|
|
4239
|
+
return NextResponse.json(
|
|
4240
|
+
{ error: "Not available in production" },
|
|
4241
|
+
{ status: 404 }
|
|
4242
|
+
);
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
const { searchParams } = new URL(request.url);
|
|
4246
|
+
const filename = searchParams.get("filename");
|
|
4247
|
+
const list = searchParams.get("list");
|
|
4248
|
+
|
|
4249
|
+
const projectRoot = findNextProjectRoot();
|
|
4250
|
+
const screenshotsDir = getScreenshotsDir(projectRoot);
|
|
4251
|
+
|
|
4252
|
+
// List mode: return all screenshots
|
|
4253
|
+
if (list === "true") {
|
|
4254
|
+
try {
|
|
4255
|
+
const files = readdirSync(screenshotsDir);
|
|
4256
|
+
const screenshots = files
|
|
4257
|
+
.filter((f) => /\\.(png|jpeg|jpg)$/.test(f))
|
|
4258
|
+
.map((f) => {
|
|
4259
|
+
const jsonFile = f.replace(/\\.(png|jpeg|jpg)$/, ".json");
|
|
4260
|
+
const jsonPath = join(screenshotsDir, jsonFile);
|
|
4261
|
+
let metadata = null;
|
|
4262
|
+
if (existsSync(jsonPath)) {
|
|
4263
|
+
try {
|
|
4264
|
+
metadata = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
4265
|
+
} catch {
|
|
4266
|
+
// Ignore parse errors
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
return {
|
|
4270
|
+
filename: f,
|
|
4271
|
+
metadata,
|
|
4272
|
+
};
|
|
4273
|
+
})
|
|
4274
|
+
.sort((a, b) => {
|
|
4275
|
+
// Sort by timestamp descending (newest first)
|
|
4276
|
+
const aTime = a.metadata?.timestamp || 0;
|
|
4277
|
+
const bTime = b.metadata?.timestamp || 0;
|
|
4278
|
+
return bTime - aTime;
|
|
4279
|
+
});
|
|
4280
|
+
|
|
4281
|
+
return NextResponse.json({ screenshots, projectRoot, screenshotsDir });
|
|
4282
|
+
} catch (error) {
|
|
4283
|
+
console.error("[Screenshot API] Error listing screenshots:", error);
|
|
4284
|
+
return NextResponse.json(
|
|
4285
|
+
{ error: "Failed to list screenshots" },
|
|
4286
|
+
{ status: 500 }
|
|
4287
|
+
);
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
// Retrieve mode: get specific screenshot
|
|
4292
|
+
if (!filename) {
|
|
4293
|
+
return NextResponse.json(
|
|
4294
|
+
{ error: "Missing 'filename' parameter" },
|
|
4295
|
+
{ status: 400 }
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
if (!isValidFilename(filename)) {
|
|
4300
|
+
return NextResponse.json(
|
|
4301
|
+
{ error: "Invalid filename format" },
|
|
4302
|
+
{ status: 400 }
|
|
4303
|
+
);
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
const filePath = join(screenshotsDir, filename);
|
|
4307
|
+
|
|
4308
|
+
if (!existsSync(filePath)) {
|
|
4309
|
+
return NextResponse.json(
|
|
4310
|
+
{ error: "Screenshot not found" },
|
|
4311
|
+
{ status: 404 }
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
try {
|
|
4316
|
+
const content = readFileSync(filePath);
|
|
4317
|
+
|
|
4318
|
+
// Determine content type
|
|
4319
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
4320
|
+
const contentType =
|
|
4321
|
+
ext === "json"
|
|
4322
|
+
? "application/json"
|
|
4323
|
+
: ext === "png"
|
|
4324
|
+
? "image/png"
|
|
4325
|
+
: "image/jpeg";
|
|
4326
|
+
|
|
4327
|
+
if (ext === "json") {
|
|
4328
|
+
return NextResponse.json(JSON.parse(content.toString()));
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
return new NextResponse(content, {
|
|
4332
|
+
headers: {
|
|
4333
|
+
"Content-Type": contentType,
|
|
4334
|
+
"Cache-Control": "no-cache",
|
|
4335
|
+
},
|
|
4336
|
+
});
|
|
4337
|
+
} catch (error) {
|
|
4338
|
+
console.error("[Screenshot API] Error reading screenshot:", error);
|
|
4339
|
+
return NextResponse.json(
|
|
4340
|
+
{ error: "Failed to read screenshot" },
|
|
4341
|
+
{ status: 500 }
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
`;
|
|
3284
4346
|
async function writeRouteFile(absPath, relPath, content, opts) {
|
|
3285
|
-
if (
|
|
4347
|
+
if (existsSync15(absPath) && !opts.force) return;
|
|
3286
4348
|
await writeFile(absPath, content, "utf-8");
|
|
3287
4349
|
}
|
|
3288
4350
|
async function installNextUILintRoutes(opts) {
|
|
3289
|
-
const baseRel =
|
|
3290
|
-
const baseAbs =
|
|
3291
|
-
await mkdir(
|
|
4351
|
+
const baseRel = join15(opts.appRoot, "api", ".uilint");
|
|
4352
|
+
const baseAbs = join15(opts.projectPath, baseRel);
|
|
4353
|
+
await mkdir(join15(baseAbs, "source"), { recursive: true });
|
|
3292
4354
|
await writeRouteFile(
|
|
3293
|
-
|
|
3294
|
-
|
|
4355
|
+
join15(baseAbs, "source", "route.ts"),
|
|
4356
|
+
join15(baseRel, "source", "route.ts"),
|
|
3295
4357
|
DEV_SOURCE_ROUTE_TS,
|
|
3296
4358
|
opts
|
|
3297
4359
|
);
|
|
4360
|
+
await mkdir(join15(baseAbs, "screenshots"), { recursive: true });
|
|
4361
|
+
await writeRouteFile(
|
|
4362
|
+
join15(baseAbs, "screenshots", "route.ts"),
|
|
4363
|
+
join15(baseRel, "screenshots", "route.ts"),
|
|
4364
|
+
SCREENSHOT_ROUTE_TS,
|
|
4365
|
+
opts
|
|
4366
|
+
);
|
|
3298
4367
|
}
|
|
3299
4368
|
|
|
3300
4369
|
// src/commands/install/execute.ts
|
|
@@ -3310,7 +4379,7 @@ async function executeAction(action, options) {
|
|
|
3310
4379
|
wouldDo: `Create directory: ${action.path}`
|
|
3311
4380
|
};
|
|
3312
4381
|
}
|
|
3313
|
-
if (!
|
|
4382
|
+
if (!existsSync16(action.path)) {
|
|
3314
4383
|
mkdirSync3(action.path, { recursive: true });
|
|
3315
4384
|
}
|
|
3316
4385
|
return { action, success: true };
|
|
@@ -3323,11 +4392,11 @@ async function executeAction(action, options) {
|
|
|
3323
4392
|
wouldDo: `Create file: ${action.path}${action.permissions ? ` (mode: ${action.permissions.toString(8)})` : ""}`
|
|
3324
4393
|
};
|
|
3325
4394
|
}
|
|
3326
|
-
const dir =
|
|
3327
|
-
if (!
|
|
4395
|
+
const dir = dirname9(action.path);
|
|
4396
|
+
if (!existsSync16(dir)) {
|
|
3328
4397
|
mkdirSync3(dir, { recursive: true });
|
|
3329
4398
|
}
|
|
3330
|
-
|
|
4399
|
+
writeFileSync7(action.path, action.content, "utf-8");
|
|
3331
4400
|
if (action.permissions) {
|
|
3332
4401
|
chmodSync(action.path, action.permissions);
|
|
3333
4402
|
}
|
|
@@ -3342,18 +4411,18 @@ async function executeAction(action, options) {
|
|
|
3342
4411
|
};
|
|
3343
4412
|
}
|
|
3344
4413
|
let existing = {};
|
|
3345
|
-
if (
|
|
4414
|
+
if (existsSync16(action.path)) {
|
|
3346
4415
|
try {
|
|
3347
|
-
existing = JSON.parse(
|
|
4416
|
+
existing = JSON.parse(readFileSync11(action.path, "utf-8"));
|
|
3348
4417
|
} catch {
|
|
3349
4418
|
}
|
|
3350
4419
|
}
|
|
3351
4420
|
const merged = deepMerge(existing, action.merge);
|
|
3352
|
-
const dir =
|
|
3353
|
-
if (!
|
|
4421
|
+
const dir = dirname9(action.path);
|
|
4422
|
+
if (!existsSync16(dir)) {
|
|
3354
4423
|
mkdirSync3(dir, { recursive: true });
|
|
3355
4424
|
}
|
|
3356
|
-
|
|
4425
|
+
writeFileSync7(action.path, JSON.stringify(merged, null, 2), "utf-8");
|
|
3357
4426
|
return { action, success: true };
|
|
3358
4427
|
}
|
|
3359
4428
|
case "delete_file": {
|
|
@@ -3364,7 +4433,7 @@ async function executeAction(action, options) {
|
|
|
3364
4433
|
wouldDo: `Delete file: ${action.path}`
|
|
3365
4434
|
};
|
|
3366
4435
|
}
|
|
3367
|
-
if (
|
|
4436
|
+
if (existsSync16(action.path)) {
|
|
3368
4437
|
unlinkSync(action.path);
|
|
3369
4438
|
}
|
|
3370
4439
|
return { action, success: true };
|
|
@@ -3377,12 +4446,12 @@ async function executeAction(action, options) {
|
|
|
3377
4446
|
wouldDo: `Append to file: ${action.path}`
|
|
3378
4447
|
};
|
|
3379
4448
|
}
|
|
3380
|
-
if (
|
|
3381
|
-
const content =
|
|
4449
|
+
if (existsSync16(action.path)) {
|
|
4450
|
+
const content = readFileSync11(action.path, "utf-8");
|
|
3382
4451
|
if (action.ifNotContains && content.includes(action.ifNotContains)) {
|
|
3383
4452
|
return { action, success: true };
|
|
3384
4453
|
}
|
|
3385
|
-
|
|
4454
|
+
writeFileSync7(action.path, content + action.content, "utf-8");
|
|
3386
4455
|
}
|
|
3387
4456
|
return { action, success: true };
|
|
3388
4457
|
}
|
|
@@ -3395,6 +4464,9 @@ async function executeAction(action, options) {
|
|
|
3395
4464
|
case "inject_next_config": {
|
|
3396
4465
|
return await executeInjectNextConfig(action, options);
|
|
3397
4466
|
}
|
|
4467
|
+
case "inject_vite_config": {
|
|
4468
|
+
return await executeInjectViteConfig(action, options);
|
|
4469
|
+
}
|
|
3398
4470
|
case "install_next_routes": {
|
|
3399
4471
|
return await executeInstallNextRoutes(action, options);
|
|
3400
4472
|
}
|
|
@@ -3450,6 +4522,7 @@ async function executeInjectReact(action, options) {
|
|
|
3450
4522
|
const result = await installReactUILintOverlay({
|
|
3451
4523
|
projectPath: action.projectPath,
|
|
3452
4524
|
appRoot: action.appRoot,
|
|
4525
|
+
mode: action.mode,
|
|
3453
4526
|
force: false,
|
|
3454
4527
|
// Auto-select first choice for execute phase
|
|
3455
4528
|
confirmFileChoice: async (choices) => choices[0]
|
|
@@ -3461,6 +4534,25 @@ async function executeInjectReact(action, options) {
|
|
|
3461
4534
|
error: success ? void 0 : "Failed to configure React overlay"
|
|
3462
4535
|
};
|
|
3463
4536
|
}
|
|
4537
|
+
async function executeInjectViteConfig(action, options) {
|
|
4538
|
+
const { dryRun = false } = options;
|
|
4539
|
+
if (dryRun) {
|
|
4540
|
+
return {
|
|
4541
|
+
action,
|
|
4542
|
+
success: true,
|
|
4543
|
+
wouldDo: `Inject jsx-loc-plugin into vite.config: ${action.projectPath}`
|
|
4544
|
+
};
|
|
4545
|
+
}
|
|
4546
|
+
const result = await installViteJsxLocPlugin({
|
|
4547
|
+
projectPath: action.projectPath,
|
|
4548
|
+
force: false
|
|
4549
|
+
});
|
|
4550
|
+
return {
|
|
4551
|
+
action,
|
|
4552
|
+
success: result.modified || result.configFile !== null,
|
|
4553
|
+
error: result.configFile === null ? "No vite.config found" : void 0
|
|
4554
|
+
};
|
|
4555
|
+
}
|
|
3464
4556
|
async function executeInjectNextConfig(action, options) {
|
|
3465
4557
|
const { dryRun = false } = options;
|
|
3466
4558
|
if (dryRun) {
|
|
@@ -3518,6 +4610,7 @@ function buildSummary(actionsPerformed, dependencyResults, items) {
|
|
|
3518
4610
|
const filesDeleted = [];
|
|
3519
4611
|
const eslintTargets = [];
|
|
3520
4612
|
let nextApp;
|
|
4613
|
+
let viteApp;
|
|
3521
4614
|
for (const result of actionsPerformed) {
|
|
3522
4615
|
if (!result.success) continue;
|
|
3523
4616
|
const { action } = result;
|
|
@@ -3540,6 +4633,12 @@ function buildSummary(actionsPerformed, dependencyResults, items) {
|
|
|
3540
4633
|
});
|
|
3541
4634
|
break;
|
|
3542
4635
|
case "inject_react":
|
|
4636
|
+
if (action.mode === "vite") {
|
|
4637
|
+
viteApp = { entryRoot: action.appRoot };
|
|
4638
|
+
} else {
|
|
4639
|
+
nextApp = { appRoot: action.appRoot };
|
|
4640
|
+
}
|
|
4641
|
+
break;
|
|
3543
4642
|
case "install_next_routes":
|
|
3544
4643
|
nextApp = { appRoot: action.appRoot };
|
|
3545
4644
|
break;
|
|
@@ -3561,7 +4660,8 @@ function buildSummary(actionsPerformed, dependencyResults, items) {
|
|
|
3561
4660
|
filesDeleted,
|
|
3562
4661
|
dependenciesInstalled,
|
|
3563
4662
|
eslintTargets,
|
|
3564
|
-
nextApp
|
|
4663
|
+
nextApp,
|
|
4664
|
+
viteApp
|
|
3565
4665
|
};
|
|
3566
4666
|
}
|
|
3567
4667
|
async function execute(plan, options = {}) {
|
|
@@ -3611,11 +4711,14 @@ async function execute(plan, options = {}) {
|
|
|
3611
4711
|
if (action.path.includes("hooks.json")) items.push("hooks");
|
|
3612
4712
|
if (action.path.includes("genstyleguide.md")) items.push("genstyleguide");
|
|
3613
4713
|
if (action.path.includes("genrules.md")) items.push("genrules");
|
|
4714
|
+
if (action.path.includes("/skills/") && action.path.includes("SKILL.md")) items.push("skill");
|
|
3614
4715
|
}
|
|
3615
4716
|
if (action.type === "inject_eslint") items.push("eslint");
|
|
3616
|
-
if (action.type === "
|
|
3617
|
-
|
|
4717
|
+
if (action.type === "install_next_routes") items.push("next");
|
|
4718
|
+
if (action.type === "inject_react") {
|
|
4719
|
+
items.push(action.mode === "vite" ? "vite" : "next");
|
|
3618
4720
|
}
|
|
4721
|
+
if (action.type === "inject_vite_config") items.push("vite");
|
|
3619
4722
|
}
|
|
3620
4723
|
const uniqueItems = [...new Set(items)];
|
|
3621
4724
|
const summary = buildSummary(
|
|
@@ -3641,13 +4744,18 @@ var cliPrompter = {
|
|
|
3641
4744
|
{
|
|
3642
4745
|
value: "eslint",
|
|
3643
4746
|
label: "ESLint plugin",
|
|
3644
|
-
hint: "Installs uilint-eslint and configures eslint.config
|
|
4747
|
+
hint: "Installs uilint-eslint and configures eslint.config.*"
|
|
3645
4748
|
},
|
|
3646
4749
|
{
|
|
3647
4750
|
value: "next",
|
|
3648
4751
|
label: "UI overlay",
|
|
3649
4752
|
hint: "Installs routes + UILintProvider (Alt+Click to inspect)"
|
|
3650
4753
|
},
|
|
4754
|
+
{
|
|
4755
|
+
value: "vite",
|
|
4756
|
+
label: "UI overlay (Vite)",
|
|
4757
|
+
hint: "Installs jsx-loc-plugin + UILintProvider (Alt+Click to inspect)"
|
|
4758
|
+
},
|
|
3651
4759
|
{
|
|
3652
4760
|
value: "genstyleguide",
|
|
3653
4761
|
label: "/genstyleguide command",
|
|
@@ -3667,10 +4775,15 @@ var cliPrompter = {
|
|
|
3667
4775
|
value: "genrules",
|
|
3668
4776
|
label: "/genrules command",
|
|
3669
4777
|
hint: "Adds .cursor/commands/genrules.md for ESLint rule generation"
|
|
4778
|
+
},
|
|
4779
|
+
{
|
|
4780
|
+
value: "skill",
|
|
4781
|
+
label: "UI Consistency Agent Skill",
|
|
4782
|
+
hint: "Cursor agent skill for generating ESLint rules from UI patterns"
|
|
3670
4783
|
}
|
|
3671
4784
|
],
|
|
3672
4785
|
required: true,
|
|
3673
|
-
initialValues: ["eslint", "next", "genstyleguide"]
|
|
4786
|
+
initialValues: ["eslint", "next", "genstyleguide", "skill"]
|
|
3674
4787
|
});
|
|
3675
4788
|
},
|
|
3676
4789
|
async confirmMcpMerge() {
|
|
@@ -3700,6 +4813,17 @@ var cliPrompter = {
|
|
|
3700
4813
|
});
|
|
3701
4814
|
return apps.find((a) => a.projectPath === chosen) || apps[0];
|
|
3702
4815
|
},
|
|
4816
|
+
async selectViteApp(apps) {
|
|
4817
|
+
const chosen = await select2({
|
|
4818
|
+
message: "Which Vite + React project should UILint install into?",
|
|
4819
|
+
options: apps.map((app) => ({
|
|
4820
|
+
value: app.projectPath,
|
|
4821
|
+
label: app.projectPath
|
|
4822
|
+
})),
|
|
4823
|
+
initialValue: apps[0].projectPath
|
|
4824
|
+
});
|
|
4825
|
+
return apps.find((a) => a.projectPath === chosen) || apps[0];
|
|
4826
|
+
},
|
|
3703
4827
|
async selectEslintPackages(packages) {
|
|
3704
4828
|
if (packages.length === 1) {
|
|
3705
4829
|
const confirmed = await confirm2({
|
|
@@ -3856,13 +4980,14 @@ async function promptForField(field, ruleName) {
|
|
|
3856
4980
|
}
|
|
3857
4981
|
async function gatherChoices(state, options, prompter) {
|
|
3858
4982
|
let items;
|
|
3859
|
-
const hasExplicitFlags = options.mcp !== void 0 || options.hooks !== void 0 || options.genstyleguide !== void 0 || options.genrules !== void 0 || options.routes !== void 0 || options.react !== void 0;
|
|
4983
|
+
const hasExplicitFlags = options.mcp !== void 0 || options.hooks !== void 0 || options.genstyleguide !== void 0 || options.genrules !== void 0 || options.skill !== void 0 || options.routes !== void 0 || options.react !== void 0;
|
|
3860
4984
|
if (hasExplicitFlags || options.eslint) {
|
|
3861
4985
|
items = [];
|
|
3862
4986
|
if (options.mcp) items.push("mcp");
|
|
3863
4987
|
if (options.hooks) items.push("hooks");
|
|
3864
4988
|
if (options.genstyleguide) items.push("genstyleguide");
|
|
3865
4989
|
if (options.genrules) items.push("genrules");
|
|
4990
|
+
if (options.skill) items.push("skill");
|
|
3866
4991
|
if (options.routes || options.react) items.push("next");
|
|
3867
4992
|
if (options.eslint) items.push("eslint");
|
|
3868
4993
|
} else if (options.mode) {
|
|
@@ -3901,6 +5026,25 @@ async function gatherChoices(state, options, prompter) {
|
|
|
3901
5026
|
};
|
|
3902
5027
|
}
|
|
3903
5028
|
}
|
|
5029
|
+
let viteChoices;
|
|
5030
|
+
if (items.includes("vite")) {
|
|
5031
|
+
if (state.viteApps.length === 0) {
|
|
5032
|
+
throw new Error(
|
|
5033
|
+
"Could not find a Vite + React project (expected vite.config.* + react deps). Run this from your Vite project root."
|
|
5034
|
+
);
|
|
5035
|
+
} else if (state.viteApps.length === 1) {
|
|
5036
|
+
viteChoices = {
|
|
5037
|
+
projectPath: state.viteApps[0].projectPath,
|
|
5038
|
+
detection: state.viteApps[0].detection
|
|
5039
|
+
};
|
|
5040
|
+
} else {
|
|
5041
|
+
const selected = await prompter.selectViteApp(state.viteApps);
|
|
5042
|
+
viteChoices = {
|
|
5043
|
+
projectPath: selected.projectPath,
|
|
5044
|
+
detection: selected.detection
|
|
5045
|
+
};
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
3904
5048
|
let eslintChoices;
|
|
3905
5049
|
if (items.includes("eslint")) {
|
|
3906
5050
|
const packagesWithEslint = state.packages.filter(
|
|
@@ -3908,7 +5052,7 @@ async function gatherChoices(state, options, prompter) {
|
|
|
3908
5052
|
);
|
|
3909
5053
|
if (packagesWithEslint.length === 0) {
|
|
3910
5054
|
throw new Error(
|
|
3911
|
-
"No packages with eslint.config.{mjs,js,cjs} found. Create an ESLint config first."
|
|
5055
|
+
"No packages with eslint.config.{ts,mjs,js,cjs} found. Create an ESLint config first."
|
|
3912
5056
|
);
|
|
3913
5057
|
}
|
|
3914
5058
|
const packagePaths = await prompter.selectEslintPackages(
|
|
@@ -3940,6 +5084,7 @@ async function gatherChoices(state, options, prompter) {
|
|
|
3940
5084
|
mcpMerge,
|
|
3941
5085
|
hooksMerge,
|
|
3942
5086
|
next: nextChoices,
|
|
5087
|
+
vite: viteChoices,
|
|
3943
5088
|
eslint: eslintChoices
|
|
3944
5089
|
};
|
|
3945
5090
|
}
|
|
@@ -3988,7 +5133,7 @@ function displayResults(result) {
|
|
|
3988
5133
|
if (summary.nextApp) {
|
|
3989
5134
|
installedItems.push(
|
|
3990
5135
|
`${pc.cyan("Next Routes")} \u2192 ${pc.dim(
|
|
3991
|
-
|
|
5136
|
+
join16(summary.nextApp.appRoot, "api/.uilint")
|
|
3992
5137
|
)}`
|
|
3993
5138
|
);
|
|
3994
5139
|
installedItems.push(
|
|
@@ -4000,6 +5145,16 @@ function displayResults(result) {
|
|
|
4000
5145
|
)}`
|
|
4001
5146
|
);
|
|
4002
5147
|
}
|
|
5148
|
+
if (summary.viteApp) {
|
|
5149
|
+
installedItems.push(
|
|
5150
|
+
`${pc.cyan("Vite Overlay")} \u2192 ${pc.dim("<UILintProvider> injected")}`
|
|
5151
|
+
);
|
|
5152
|
+
installedItems.push(
|
|
5153
|
+
`${pc.cyan("JSX Loc Plugin")} \u2192 ${pc.dim(
|
|
5154
|
+
"vite.config plugins patched with jsxLoc()"
|
|
5155
|
+
)}`
|
|
5156
|
+
);
|
|
5157
|
+
}
|
|
4003
5158
|
if (summary.eslintTargets.length > 0) {
|
|
4004
5159
|
installedItems.push(
|
|
4005
5160
|
`${pc.cyan("ESLint Plugin")} \u2192 installed in ${summary.eslintTargets.length} package(s)`
|
|
@@ -4039,9 +5194,14 @@ function displayResults(result) {
|
|
|
4039
5194
|
if (summary.installedItems.includes("hooks")) {
|
|
4040
5195
|
steps.push("Hooks will auto-validate UI files when the agent stops");
|
|
4041
5196
|
}
|
|
4042
|
-
if (summary.nextApp) {
|
|
5197
|
+
if (summary.nextApp) {
|
|
5198
|
+
steps.push(
|
|
5199
|
+
"Run your Next.js dev server - use Alt+Click on any element to inspect"
|
|
5200
|
+
);
|
|
5201
|
+
}
|
|
5202
|
+
if (summary.viteApp) {
|
|
4043
5203
|
steps.push(
|
|
4044
|
-
"Run your
|
|
5204
|
+
"Run your Vite dev server - use Alt+Click on any element to inspect"
|
|
4045
5205
|
);
|
|
4046
5206
|
}
|
|
4047
5207
|
if (summary.eslintTargets.length > 0) {
|
|
@@ -4108,12 +5268,207 @@ async function install(options = {}, prompter = cliPrompter, executeOptions = {}
|
|
|
4108
5268
|
}
|
|
4109
5269
|
|
|
4110
5270
|
// src/commands/serve.ts
|
|
4111
|
-
import { existsSync as
|
|
4112
|
-
import { createRequire as
|
|
4113
|
-
import { dirname as
|
|
5271
|
+
import { existsSync as existsSync18, statSync as statSync4, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
|
|
5272
|
+
import { createRequire as createRequire3 } from "module";
|
|
5273
|
+
import { dirname as dirname11, resolve as resolve5, relative as relative4, join as join18, parse as parse2 } from "path";
|
|
4114
5274
|
import { WebSocketServer, WebSocket } from "ws";
|
|
4115
5275
|
import { watch } from "chokidar";
|
|
4116
|
-
import {
|
|
5276
|
+
import {
|
|
5277
|
+
findWorkspaceRoot as findWorkspaceRoot6,
|
|
5278
|
+
getVisionAnalyzer as getCoreVisionAnalyzer
|
|
5279
|
+
} from "uilint-core/node";
|
|
5280
|
+
|
|
5281
|
+
// src/utils/vision-run.ts
|
|
5282
|
+
import { dirname as dirname10, join as join17, parse } from "path";
|
|
5283
|
+
import { existsSync as existsSync17, statSync as statSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
5284
|
+
import {
|
|
5285
|
+
ensureOllamaReady as ensureOllamaReady5,
|
|
5286
|
+
findStyleGuidePath as findStyleGuidePath4,
|
|
5287
|
+
findUILintStyleGuideUpwards as findUILintStyleGuideUpwards3,
|
|
5288
|
+
readStyleGuide as readStyleGuide4,
|
|
5289
|
+
VisionAnalyzer,
|
|
5290
|
+
UILINT_DEFAULT_VISION_MODEL
|
|
5291
|
+
} from "uilint-core/node";
|
|
5292
|
+
async function resolveVisionStyleGuide(args) {
|
|
5293
|
+
const projectPath = args.projectPath;
|
|
5294
|
+
const startDir = args.startDir ?? projectPath;
|
|
5295
|
+
if (args.styleguide) {
|
|
5296
|
+
const styleguideArg = resolvePathSpecifier(args.styleguide, projectPath);
|
|
5297
|
+
if (existsSync17(styleguideArg)) {
|
|
5298
|
+
const stat = statSync3(styleguideArg);
|
|
5299
|
+
if (stat.isFile()) {
|
|
5300
|
+
return {
|
|
5301
|
+
styleguideLocation: styleguideArg,
|
|
5302
|
+
styleGuide: await readStyleGuide4(styleguideArg)
|
|
5303
|
+
};
|
|
5304
|
+
}
|
|
5305
|
+
if (stat.isDirectory()) {
|
|
5306
|
+
const found = findStyleGuidePath4(styleguideArg);
|
|
5307
|
+
return {
|
|
5308
|
+
styleguideLocation: found,
|
|
5309
|
+
styleGuide: found ? await readStyleGuide4(found) : null
|
|
5310
|
+
};
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
return { styleGuide: null, styleguideLocation: null };
|
|
5314
|
+
}
|
|
5315
|
+
const upwards = findUILintStyleGuideUpwards3(startDir);
|
|
5316
|
+
const fallback = upwards ?? findStyleGuidePath4(projectPath);
|
|
5317
|
+
return {
|
|
5318
|
+
styleguideLocation: fallback,
|
|
5319
|
+
styleGuide: fallback ? await readStyleGuide4(fallback) : null
|
|
5320
|
+
};
|
|
5321
|
+
}
|
|
5322
|
+
var ollamaReadyOnce = /* @__PURE__ */ new Map();
|
|
5323
|
+
async function ensureOllamaReadyCached(params) {
|
|
5324
|
+
const key = `${params.baseUrl}::${params.model}`;
|
|
5325
|
+
const existing = ollamaReadyOnce.get(key);
|
|
5326
|
+
if (existing) return existing;
|
|
5327
|
+
const p2 = ensureOllamaReady5({ model: params.model, baseUrl: params.baseUrl }).then(() => void 0).catch((e) => {
|
|
5328
|
+
ollamaReadyOnce.delete(key);
|
|
5329
|
+
throw e;
|
|
5330
|
+
});
|
|
5331
|
+
ollamaReadyOnce.set(key, p2);
|
|
5332
|
+
return p2;
|
|
5333
|
+
}
|
|
5334
|
+
function writeVisionDebugDump(params) {
|
|
5335
|
+
const resolvedDirOrFile = resolvePathSpecifier(
|
|
5336
|
+
params.dumpPath,
|
|
5337
|
+
process.cwd()
|
|
5338
|
+
);
|
|
5339
|
+
const safeStamp = params.now.toISOString().replace(/[:.]/g, "-");
|
|
5340
|
+
const dumpFile = resolvedDirOrFile.endsWith(".json") || resolvedDirOrFile.endsWith(".jsonl") ? resolvedDirOrFile : `${resolvedDirOrFile}/vision-debug-${safeStamp}.json`;
|
|
5341
|
+
mkdirSync4(dirname10(dumpFile), { recursive: true });
|
|
5342
|
+
writeFileSync8(
|
|
5343
|
+
dumpFile,
|
|
5344
|
+
JSON.stringify(
|
|
5345
|
+
{
|
|
5346
|
+
version: 1,
|
|
5347
|
+
timestamp: params.now.toISOString(),
|
|
5348
|
+
runtime: params.runtime,
|
|
5349
|
+
metadata: params.metadata ?? null,
|
|
5350
|
+
inputs: {
|
|
5351
|
+
imageBase64: params.includeSensitive ? params.inputs.imageBase64 : "(omitted; set debugDumpIncludeSensitive=true)",
|
|
5352
|
+
manifest: params.inputs.manifest,
|
|
5353
|
+
styleguideLocation: params.inputs.styleguideLocation,
|
|
5354
|
+
styleGuide: params.includeSensitive ? params.inputs.styleGuide : "(omitted; set debugDumpIncludeSensitive=true)"
|
|
5355
|
+
}
|
|
5356
|
+
},
|
|
5357
|
+
null,
|
|
5358
|
+
2
|
|
5359
|
+
),
|
|
5360
|
+
"utf-8"
|
|
5361
|
+
);
|
|
5362
|
+
return dumpFile;
|
|
5363
|
+
}
|
|
5364
|
+
async function runVisionAnalysis(args) {
|
|
5365
|
+
const visionModel = args.model || UILINT_DEFAULT_VISION_MODEL;
|
|
5366
|
+
const baseUrl = args.baseUrl ?? "http://localhost:11434";
|
|
5367
|
+
let styleGuide = null;
|
|
5368
|
+
let styleguideLocation = null;
|
|
5369
|
+
if (args.styleGuide !== void 0) {
|
|
5370
|
+
styleGuide = args.styleGuide;
|
|
5371
|
+
styleguideLocation = args.styleguideLocation ?? null;
|
|
5372
|
+
} else {
|
|
5373
|
+
args.onPhase?.("Resolving styleguide...");
|
|
5374
|
+
const resolved = await resolveVisionStyleGuide({
|
|
5375
|
+
projectPath: args.projectPath,
|
|
5376
|
+
styleguide: args.styleguide,
|
|
5377
|
+
startDir: args.styleguideStartDir
|
|
5378
|
+
});
|
|
5379
|
+
styleGuide = resolved.styleGuide;
|
|
5380
|
+
styleguideLocation = resolved.styleguideLocation;
|
|
5381
|
+
}
|
|
5382
|
+
if (!args.skipEnsureOllama) {
|
|
5383
|
+
args.onPhase?.("Preparing Ollama...");
|
|
5384
|
+
await ensureOllamaReadyCached({ model: visionModel, baseUrl });
|
|
5385
|
+
}
|
|
5386
|
+
if (args.debugDump) {
|
|
5387
|
+
writeVisionDebugDump({
|
|
5388
|
+
dumpPath: args.debugDump,
|
|
5389
|
+
now: /* @__PURE__ */ new Date(),
|
|
5390
|
+
runtime: { visionModel, baseUrl },
|
|
5391
|
+
inputs: {
|
|
5392
|
+
imageBase64: args.imageBase64,
|
|
5393
|
+
manifest: args.manifest,
|
|
5394
|
+
styleguideLocation,
|
|
5395
|
+
styleGuide
|
|
5396
|
+
},
|
|
5397
|
+
includeSensitive: Boolean(args.debugDumpIncludeSensitive),
|
|
5398
|
+
metadata: args.debugDumpMetadata
|
|
5399
|
+
});
|
|
5400
|
+
}
|
|
5401
|
+
const analyzer = args.analyzer ?? new VisionAnalyzer({
|
|
5402
|
+
baseUrl: args.baseUrl,
|
|
5403
|
+
visionModel
|
|
5404
|
+
});
|
|
5405
|
+
args.onPhase?.(`Analyzing ${args.manifest.length} elements...`);
|
|
5406
|
+
const result = await analyzer.analyzeScreenshot(
|
|
5407
|
+
args.imageBase64,
|
|
5408
|
+
args.manifest,
|
|
5409
|
+
{
|
|
5410
|
+
styleGuide,
|
|
5411
|
+
onProgress: args.onProgress
|
|
5412
|
+
}
|
|
5413
|
+
);
|
|
5414
|
+
args.onPhase?.(
|
|
5415
|
+
`Done (${result.issues.length} issues, ${result.analysisTime}ms)`
|
|
5416
|
+
);
|
|
5417
|
+
return {
|
|
5418
|
+
issues: result.issues,
|
|
5419
|
+
analysisTime: result.analysisTime,
|
|
5420
|
+
// Prompt is available in newer uilint-core versions; keep this resilient across versions.
|
|
5421
|
+
prompt: result.prompt,
|
|
5422
|
+
rawResponse: result.rawResponse,
|
|
5423
|
+
styleguideLocation,
|
|
5424
|
+
visionModel,
|
|
5425
|
+
baseUrl
|
|
5426
|
+
};
|
|
5427
|
+
}
|
|
5428
|
+
function writeVisionMarkdownReport(args) {
|
|
5429
|
+
const p2 = parse(args.imagePath);
|
|
5430
|
+
const outPath = args.outPath ?? join17(p2.dir, `${p2.name || p2.base}.vision.md`);
|
|
5431
|
+
const lines = [];
|
|
5432
|
+
lines.push(`# UILint Vision Report`);
|
|
5433
|
+
lines.push(``);
|
|
5434
|
+
lines.push(`- Image: \`${p2.base}\``);
|
|
5435
|
+
if (args.route) lines.push(`- Route: \`${args.route}\``);
|
|
5436
|
+
if (typeof args.timestamp === "number") {
|
|
5437
|
+
lines.push(`- Timestamp: \`${new Date(args.timestamp).toISOString()}\``);
|
|
5438
|
+
}
|
|
5439
|
+
if (args.visionModel) lines.push(`- Model: \`${args.visionModel}\``);
|
|
5440
|
+
if (args.baseUrl) lines.push(`- Ollama baseUrl: \`${args.baseUrl}\``);
|
|
5441
|
+
if (typeof args.analysisTimeMs === "number")
|
|
5442
|
+
lines.push(`- Analysis time: \`${args.analysisTimeMs}ms\``);
|
|
5443
|
+
lines.push(`- Generated: \`${(/* @__PURE__ */ new Date()).toISOString()}\``);
|
|
5444
|
+
lines.push(``);
|
|
5445
|
+
if (args.metadata && Object.keys(args.metadata).length > 0) {
|
|
5446
|
+
lines.push(`## Metadata`);
|
|
5447
|
+
lines.push(``);
|
|
5448
|
+
lines.push("```json");
|
|
5449
|
+
lines.push(JSON.stringify(args.metadata, null, 2));
|
|
5450
|
+
lines.push("```");
|
|
5451
|
+
lines.push(``);
|
|
5452
|
+
}
|
|
5453
|
+
lines.push(`## Prompt`);
|
|
5454
|
+
lines.push(``);
|
|
5455
|
+
lines.push("```text");
|
|
5456
|
+
lines.push((args.prompt ?? "").trim());
|
|
5457
|
+
lines.push("```");
|
|
5458
|
+
lines.push(``);
|
|
5459
|
+
lines.push(`## Raw Response`);
|
|
5460
|
+
lines.push(``);
|
|
5461
|
+
lines.push("```text");
|
|
5462
|
+
lines.push((args.rawResponse ?? "").trim());
|
|
5463
|
+
lines.push("```");
|
|
5464
|
+
lines.push(``);
|
|
5465
|
+
const content = lines.join("\n");
|
|
5466
|
+
mkdirSync4(dirname10(outPath), { recursive: true });
|
|
5467
|
+
writeFileSync8(outPath, content, "utf-8");
|
|
5468
|
+
return { outPath, content };
|
|
5469
|
+
}
|
|
5470
|
+
|
|
5471
|
+
// src/commands/serve.ts
|
|
4117
5472
|
function pickAppRoot(params) {
|
|
4118
5473
|
const { cwd, workspaceRoot } = params;
|
|
4119
5474
|
if (detectNextAppRouter(cwd)) return cwd;
|
|
@@ -4128,11 +5483,23 @@ function pickAppRoot(params) {
|
|
|
4128
5483
|
}
|
|
4129
5484
|
var cache = /* @__PURE__ */ new Map();
|
|
4130
5485
|
var eslintInstances = /* @__PURE__ */ new Map();
|
|
5486
|
+
var visionAnalyzer = null;
|
|
5487
|
+
function getVisionAnalyzerInstance() {
|
|
5488
|
+
if (!visionAnalyzer) {
|
|
5489
|
+
visionAnalyzer = getCoreVisionAnalyzer();
|
|
5490
|
+
}
|
|
5491
|
+
return visionAnalyzer;
|
|
5492
|
+
}
|
|
5493
|
+
var serverAppRootForVision = process.cwd();
|
|
5494
|
+
function isValidScreenshotFilename(filename) {
|
|
5495
|
+
const validPattern = /^[a-zA-Z0-9_-]+\.(png|jpeg|jpg)$/;
|
|
5496
|
+
return validPattern.test(filename) && !filename.includes("..");
|
|
5497
|
+
}
|
|
4131
5498
|
var resolvedPathCache = /* @__PURE__ */ new Map();
|
|
4132
5499
|
var subscriptions = /* @__PURE__ */ new Map();
|
|
4133
5500
|
var fileWatcher = null;
|
|
4134
5501
|
var connectedClients = 0;
|
|
4135
|
-
var localRequire =
|
|
5502
|
+
var localRequire = createRequire3(import.meta.url);
|
|
4136
5503
|
function buildLineStarts(code) {
|
|
4137
5504
|
const starts = [0];
|
|
4138
5505
|
for (let i = 0; i < code.length; i++) {
|
|
@@ -4146,8 +5513,8 @@ function offsetFromLineCol(lineStarts, line1, col0, codeLength) {
|
|
|
4146
5513
|
return Math.max(0, Math.min(codeLength, base + Math.max(0, col0)));
|
|
4147
5514
|
}
|
|
4148
5515
|
function buildJsxElementSpans(code, dataLocFile) {
|
|
4149
|
-
const { parse } = localRequire("@typescript-eslint/typescript-estree");
|
|
4150
|
-
const ast =
|
|
5516
|
+
const { parse: parse3 } = localRequire("@typescript-eslint/typescript-estree");
|
|
5517
|
+
const ast = parse3(code, {
|
|
4151
5518
|
loc: true,
|
|
4152
5519
|
range: true,
|
|
4153
5520
|
jsx: true,
|
|
@@ -4210,10 +5577,10 @@ function findESLintCwd(startDir) {
|
|
|
4210
5577
|
let dir = startDir;
|
|
4211
5578
|
for (let i = 0; i < 30; i++) {
|
|
4212
5579
|
for (const cfg of ESLINT_CONFIG_FILES2) {
|
|
4213
|
-
if (
|
|
5580
|
+
if (existsSync18(join18(dir, cfg))) return dir;
|
|
4214
5581
|
}
|
|
4215
|
-
if (
|
|
4216
|
-
const parent =
|
|
5582
|
+
if (existsSync18(join18(dir, "package.json"))) return dir;
|
|
5583
|
+
const parent = dirname11(dir);
|
|
4217
5584
|
if (parent === dir) break;
|
|
4218
5585
|
dir = parent;
|
|
4219
5586
|
}
|
|
@@ -4226,7 +5593,7 @@ function normalizeDataLocFilePath(absoluteFilePath, projectCwd) {
|
|
|
4226
5593
|
const abs = normalizePathSlashes(resolve5(absoluteFilePath));
|
|
4227
5594
|
const cwd = normalizePathSlashes(resolve5(projectCwd));
|
|
4228
5595
|
if (abs === cwd || abs.startsWith(cwd + "/")) {
|
|
4229
|
-
return normalizePathSlashes(
|
|
5596
|
+
return normalizePathSlashes(relative4(cwd, abs));
|
|
4230
5597
|
}
|
|
4231
5598
|
return abs;
|
|
4232
5599
|
}
|
|
@@ -4238,25 +5605,25 @@ function resolveRequestedFilePath(filePath) {
|
|
|
4238
5605
|
if (cached) return cached;
|
|
4239
5606
|
const cwd = process.cwd();
|
|
4240
5607
|
const fromCwd = resolve5(cwd, filePath);
|
|
4241
|
-
if (
|
|
5608
|
+
if (existsSync18(fromCwd)) {
|
|
4242
5609
|
resolvedPathCache.set(filePath, fromCwd);
|
|
4243
5610
|
return fromCwd;
|
|
4244
5611
|
}
|
|
4245
|
-
const wsRoot =
|
|
5612
|
+
const wsRoot = findWorkspaceRoot6(cwd);
|
|
4246
5613
|
const fromWs = resolve5(wsRoot, filePath);
|
|
4247
|
-
if (
|
|
5614
|
+
if (existsSync18(fromWs)) {
|
|
4248
5615
|
resolvedPathCache.set(filePath, fromWs);
|
|
4249
5616
|
return fromWs;
|
|
4250
5617
|
}
|
|
4251
5618
|
for (const top of ["apps", "packages"]) {
|
|
4252
|
-
const base =
|
|
4253
|
-
if (!
|
|
5619
|
+
const base = join18(wsRoot, top);
|
|
5620
|
+
if (!existsSync18(base)) continue;
|
|
4254
5621
|
try {
|
|
4255
|
-
const entries =
|
|
5622
|
+
const entries = readdirSync5(base, { withFileTypes: true });
|
|
4256
5623
|
for (const ent of entries) {
|
|
4257
5624
|
if (!ent.isDirectory()) continue;
|
|
4258
5625
|
const p2 = resolve5(base, ent.name, filePath);
|
|
4259
|
-
if (
|
|
5626
|
+
if (existsSync18(p2)) {
|
|
4260
5627
|
resolvedPathCache.set(filePath, p2);
|
|
4261
5628
|
return p2;
|
|
4262
5629
|
}
|
|
@@ -4271,7 +5638,7 @@ async function getESLintForProject(projectCwd) {
|
|
|
4271
5638
|
const cached = eslintInstances.get(projectCwd);
|
|
4272
5639
|
if (cached) return cached;
|
|
4273
5640
|
try {
|
|
4274
|
-
const req =
|
|
5641
|
+
const req = createRequire3(join18(projectCwd, "package.json"));
|
|
4275
5642
|
const mod = req("eslint");
|
|
4276
5643
|
const ESLintCtor = mod?.ESLint ?? mod?.default?.ESLint ?? mod?.default ?? mod;
|
|
4277
5644
|
if (!ESLintCtor) return null;
|
|
@@ -4284,13 +5651,13 @@ async function getESLintForProject(projectCwd) {
|
|
|
4284
5651
|
}
|
|
4285
5652
|
async function lintFile(filePath, onProgress) {
|
|
4286
5653
|
const absolutePath = resolveRequestedFilePath(filePath);
|
|
4287
|
-
if (!
|
|
5654
|
+
if (!existsSync18(absolutePath)) {
|
|
4288
5655
|
onProgress(`File not found: ${pc.dim(absolutePath)}`);
|
|
4289
5656
|
return [];
|
|
4290
5657
|
}
|
|
4291
5658
|
const mtimeMs = (() => {
|
|
4292
5659
|
try {
|
|
4293
|
-
return
|
|
5660
|
+
return statSync4(absolutePath).mtimeMs;
|
|
4294
5661
|
} catch {
|
|
4295
5662
|
return 0;
|
|
4296
5663
|
}
|
|
@@ -4300,7 +5667,7 @@ async function lintFile(filePath, onProgress) {
|
|
|
4300
5667
|
onProgress("Cache hit (unchanged)");
|
|
4301
5668
|
return cached.issues;
|
|
4302
5669
|
}
|
|
4303
|
-
const fileDir =
|
|
5670
|
+
const fileDir = dirname11(absolutePath);
|
|
4304
5671
|
const projectCwd = findESLintCwd(fileDir);
|
|
4305
5672
|
onProgress(`Resolving ESLint project... ${pc.dim(projectCwd)}`);
|
|
4306
5673
|
const eslint = await getESLintForProject(projectCwd);
|
|
@@ -4323,7 +5690,7 @@ async function lintFile(filePath, onProgress) {
|
|
|
4323
5690
|
let codeLength = 0;
|
|
4324
5691
|
try {
|
|
4325
5692
|
onProgress("Building JSX map...");
|
|
4326
|
-
const code =
|
|
5693
|
+
const code = readFileSync12(absolutePath, "utf-8");
|
|
4327
5694
|
codeLength = code.length;
|
|
4328
5695
|
lineStarts = buildLineStarts(code);
|
|
4329
5696
|
spans = buildJsxElementSpans(code, dataLocFile);
|
|
@@ -4392,6 +5759,7 @@ async function handleMessage(ws, data) {
|
|
|
4392
5759
|
message.filePath ?? "(all)"
|
|
4393
5760
|
)}`
|
|
4394
5761
|
);
|
|
5762
|
+
} else if (message.type === "vision:analyze") {
|
|
4395
5763
|
}
|
|
4396
5764
|
switch (message.type) {
|
|
4397
5765
|
case "lint:file": {
|
|
@@ -4404,9 +5772,9 @@ async function handleMessage(ws, data) {
|
|
|
4404
5772
|
});
|
|
4405
5773
|
const startedAt = Date.now();
|
|
4406
5774
|
const resolved = resolveRequestedFilePath(filePath);
|
|
4407
|
-
if (!
|
|
5775
|
+
if (!existsSync18(resolved)) {
|
|
4408
5776
|
const cwd = process.cwd();
|
|
4409
|
-
const wsRoot =
|
|
5777
|
+
const wsRoot = findWorkspaceRoot6(cwd);
|
|
4410
5778
|
logWarning(
|
|
4411
5779
|
[
|
|
4412
5780
|
`${pc.dim("[ws]")} File not found for request`,
|
|
@@ -4492,6 +5860,167 @@ async function handleMessage(ws, data) {
|
|
|
4492
5860
|
}
|
|
4493
5861
|
break;
|
|
4494
5862
|
}
|
|
5863
|
+
case "vision:analyze": {
|
|
5864
|
+
const {
|
|
5865
|
+
route,
|
|
5866
|
+
timestamp,
|
|
5867
|
+
screenshot,
|
|
5868
|
+
screenshotFile,
|
|
5869
|
+
manifest,
|
|
5870
|
+
requestId
|
|
5871
|
+
} = message;
|
|
5872
|
+
logInfo(
|
|
5873
|
+
`${pc.dim("[ws]")} ${pc.bold("vision:analyze")} ${pc.dim(route)}${requestId ? ` ${pc.dim(`(req ${requestId})`)}` : ""}`
|
|
5874
|
+
);
|
|
5875
|
+
sendMessage(ws, {
|
|
5876
|
+
type: "vision:progress",
|
|
5877
|
+
route,
|
|
5878
|
+
requestId,
|
|
5879
|
+
phase: "Starting vision analysis..."
|
|
5880
|
+
});
|
|
5881
|
+
const startedAt = Date.now();
|
|
5882
|
+
const analyzer = getVisionAnalyzerInstance();
|
|
5883
|
+
try {
|
|
5884
|
+
const screenshotBytes = typeof screenshot === "string" ? Buffer.byteLength(screenshot) : 0;
|
|
5885
|
+
const analyzerModel = typeof analyzer.getModel === "function" ? analyzer.getModel() : void 0;
|
|
5886
|
+
const analyzerBaseUrl = typeof analyzer.getBaseUrl === "function" ? analyzer.getBaseUrl() : void 0;
|
|
5887
|
+
logInfo(
|
|
5888
|
+
[
|
|
5889
|
+
`${pc.dim("[ws]")} ${pc.dim("vision")} details`,
|
|
5890
|
+
` route: ${pc.dim(route)}`,
|
|
5891
|
+
` requestId: ${pc.dim(requestId ?? "(none)")}`,
|
|
5892
|
+
` manifest: ${pc.dim(String(manifest.length))} element(s)`,
|
|
5893
|
+
` screenshot: ${pc.dim(
|
|
5894
|
+
screenshot ? `${Math.round(screenshotBytes / 1024)}kb` : "none"
|
|
5895
|
+
)}`,
|
|
5896
|
+
` screenshotFile: ${pc.dim(screenshotFile ?? "(none)")}`,
|
|
5897
|
+
` ollamaUrl: ${pc.dim(analyzerBaseUrl ?? "(default)")}`,
|
|
5898
|
+
` visionModel: ${pc.dim(analyzerModel ?? "(default)")}`
|
|
5899
|
+
].join("\n")
|
|
5900
|
+
);
|
|
5901
|
+
if (!screenshot) {
|
|
5902
|
+
sendMessage(ws, {
|
|
5903
|
+
type: "vision:result",
|
|
5904
|
+
route,
|
|
5905
|
+
issues: [],
|
|
5906
|
+
analysisTime: Date.now() - startedAt,
|
|
5907
|
+
error: "No screenshot provided for vision analysis",
|
|
5908
|
+
requestId
|
|
5909
|
+
});
|
|
5910
|
+
break;
|
|
5911
|
+
}
|
|
5912
|
+
const result = await runVisionAnalysis({
|
|
5913
|
+
imageBase64: screenshot,
|
|
5914
|
+
manifest,
|
|
5915
|
+
projectPath: serverAppRootForVision,
|
|
5916
|
+
// In the overlay/server context, default to upward search from app root.
|
|
5917
|
+
baseUrl: analyzerBaseUrl,
|
|
5918
|
+
model: analyzerModel,
|
|
5919
|
+
analyzer,
|
|
5920
|
+
onPhase: (phase) => {
|
|
5921
|
+
sendMessage(ws, {
|
|
5922
|
+
type: "vision:progress",
|
|
5923
|
+
route,
|
|
5924
|
+
requestId,
|
|
5925
|
+
phase
|
|
5926
|
+
});
|
|
5927
|
+
}
|
|
5928
|
+
});
|
|
5929
|
+
if (typeof screenshotFile === "string" && screenshotFile.length > 0) {
|
|
5930
|
+
if (!isValidScreenshotFilename(screenshotFile)) {
|
|
5931
|
+
logWarning(
|
|
5932
|
+
`Skipping vision report write: invalid screenshotFile ${pc.dim(
|
|
5933
|
+
screenshotFile
|
|
5934
|
+
)}`
|
|
5935
|
+
);
|
|
5936
|
+
} else {
|
|
5937
|
+
const screenshotsDir = join18(
|
|
5938
|
+
serverAppRootForVision,
|
|
5939
|
+
".uilint",
|
|
5940
|
+
"screenshots"
|
|
5941
|
+
);
|
|
5942
|
+
const imagePath = join18(screenshotsDir, screenshotFile);
|
|
5943
|
+
try {
|
|
5944
|
+
if (!existsSync18(imagePath)) {
|
|
5945
|
+
logWarning(
|
|
5946
|
+
`Skipping vision report write: screenshot file not found ${pc.dim(
|
|
5947
|
+
imagePath
|
|
5948
|
+
)}`
|
|
5949
|
+
);
|
|
5950
|
+
} else {
|
|
5951
|
+
const report = writeVisionMarkdownReport({
|
|
5952
|
+
imagePath,
|
|
5953
|
+
route,
|
|
5954
|
+
timestamp,
|
|
5955
|
+
visionModel: result.visionModel,
|
|
5956
|
+
baseUrl: result.baseUrl,
|
|
5957
|
+
analysisTimeMs: result.analysisTime,
|
|
5958
|
+
prompt: result.prompt ?? null,
|
|
5959
|
+
rawResponse: result.rawResponse ?? null,
|
|
5960
|
+
metadata: {
|
|
5961
|
+
screenshotFile: parse2(imagePath).base,
|
|
5962
|
+
appRoot: serverAppRootForVision,
|
|
5963
|
+
manifestElements: manifest.length,
|
|
5964
|
+
requestId: requestId ?? null
|
|
5965
|
+
}
|
|
5966
|
+
});
|
|
5967
|
+
logInfo(
|
|
5968
|
+
`${pc.dim("[ws]")} wrote vision report ${pc.dim(
|
|
5969
|
+
report.outPath
|
|
5970
|
+
)}`
|
|
5971
|
+
);
|
|
5972
|
+
}
|
|
5973
|
+
} catch (e) {
|
|
5974
|
+
logWarning(
|
|
5975
|
+
`Failed to write vision report for ${pc.dim(screenshotFile)}: ${e instanceof Error ? e.message : String(e)}`
|
|
5976
|
+
);
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
const elapsed = Date.now() - startedAt;
|
|
5981
|
+
logInfo(
|
|
5982
|
+
`${pc.dim("[ws]")} vision:analyze done ${pc.dim(route)} \u2192 ${pc.bold(
|
|
5983
|
+
`${result.issues.length}`
|
|
5984
|
+
)} issue(s) ${pc.dim(`(${elapsed}ms)`)}`
|
|
5985
|
+
);
|
|
5986
|
+
if (result.rawResponse) {
|
|
5987
|
+
logInfo(
|
|
5988
|
+
`${pc.dim("[ws]")} vision rawResponse ${pc.dim(
|
|
5989
|
+
`${result.rawResponse.length} chars`
|
|
5990
|
+
)}`
|
|
5991
|
+
);
|
|
5992
|
+
}
|
|
5993
|
+
sendMessage(ws, {
|
|
5994
|
+
type: "vision:result",
|
|
5995
|
+
route,
|
|
5996
|
+
issues: result.issues,
|
|
5997
|
+
analysisTime: result.analysisTime,
|
|
5998
|
+
requestId
|
|
5999
|
+
});
|
|
6000
|
+
} catch (error) {
|
|
6001
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6002
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
6003
|
+
logError(
|
|
6004
|
+
[
|
|
6005
|
+
`Vision analysis failed`,
|
|
6006
|
+
` route: ${route}`,
|
|
6007
|
+
` requestId: ${requestId ?? "(none)"}`,
|
|
6008
|
+
` error: ${errorMessage}`,
|
|
6009
|
+
stack ? ` stack:
|
|
6010
|
+
${stack}` : ""
|
|
6011
|
+
].filter(Boolean).join("\n")
|
|
6012
|
+
);
|
|
6013
|
+
sendMessage(ws, {
|
|
6014
|
+
type: "vision:result",
|
|
6015
|
+
route,
|
|
6016
|
+
issues: [],
|
|
6017
|
+
analysisTime: Date.now() - startedAt,
|
|
6018
|
+
error: errorMessage,
|
|
6019
|
+
requestId
|
|
6020
|
+
});
|
|
6021
|
+
}
|
|
6022
|
+
break;
|
|
6023
|
+
}
|
|
4495
6024
|
}
|
|
4496
6025
|
}
|
|
4497
6026
|
function handleDisconnect(ws) {
|
|
@@ -4520,8 +6049,9 @@ function handleFileChange(filePath) {
|
|
|
4520
6049
|
async function serve(options) {
|
|
4521
6050
|
const port = options.port || 9234;
|
|
4522
6051
|
const cwd = process.cwd();
|
|
4523
|
-
const wsRoot =
|
|
6052
|
+
const wsRoot = findWorkspaceRoot6(cwd);
|
|
4524
6053
|
const appRoot = pickAppRoot({ cwd, workspaceRoot: wsRoot });
|
|
6054
|
+
serverAppRootForVision = appRoot;
|
|
4525
6055
|
logInfo(`Workspace root: ${pc.dim(wsRoot)}`);
|
|
4526
6056
|
logInfo(`App root: ${pc.dim(appRoot)}`);
|
|
4527
6057
|
logInfo(`Server cwd: ${pc.dim(cwd)}`);
|
|
@@ -4561,22 +6091,505 @@ async function serve(options) {
|
|
|
4561
6091
|
`UILint WebSocket server running on ${pc.cyan(`ws://localhost:${port}`)}`
|
|
4562
6092
|
);
|
|
4563
6093
|
logInfo("Press Ctrl+C to stop");
|
|
4564
|
-
await new Promise((
|
|
6094
|
+
await new Promise((resolve8) => {
|
|
4565
6095
|
process.on("SIGINT", () => {
|
|
4566
6096
|
logInfo("Shutting down...");
|
|
4567
6097
|
wss.close();
|
|
4568
6098
|
fileWatcher?.close();
|
|
4569
|
-
|
|
6099
|
+
resolve8();
|
|
4570
6100
|
});
|
|
4571
6101
|
});
|
|
4572
6102
|
}
|
|
4573
6103
|
|
|
6104
|
+
// src/commands/vision.ts
|
|
6105
|
+
import { dirname as dirname12, resolve as resolve6, join as join19 } from "path";
|
|
6106
|
+
import {
|
|
6107
|
+
existsSync as existsSync19,
|
|
6108
|
+
readFileSync as readFileSync13,
|
|
6109
|
+
readdirSync as readdirSync6
|
|
6110
|
+
} from "fs";
|
|
6111
|
+
import {
|
|
6112
|
+
ensureOllamaReady as ensureOllamaReady6,
|
|
6113
|
+
STYLEGUIDE_PATHS as STYLEGUIDE_PATHS2,
|
|
6114
|
+
UILINT_DEFAULT_VISION_MODEL as UILINT_DEFAULT_VISION_MODEL2
|
|
6115
|
+
} from "uilint-core/node";
|
|
6116
|
+
function envTruthy3(name) {
|
|
6117
|
+
const v = process.env[name];
|
|
6118
|
+
if (!v) return false;
|
|
6119
|
+
return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
|
|
6120
|
+
}
|
|
6121
|
+
function preview3(text3, maxLen) {
|
|
6122
|
+
if (text3.length <= maxLen) return text3;
|
|
6123
|
+
return text3.slice(0, maxLen) + "\n\u2026<truncated>\u2026\n" + text3.slice(-maxLen);
|
|
6124
|
+
}
|
|
6125
|
+
function debugEnabled3(options) {
|
|
6126
|
+
return Boolean(options.debug) || envTruthy3("UILINT_DEBUG");
|
|
6127
|
+
}
|
|
6128
|
+
function debugFullEnabled3(options) {
|
|
6129
|
+
return Boolean(options.debugFull) || envTruthy3("UILINT_DEBUG_FULL");
|
|
6130
|
+
}
|
|
6131
|
+
function debugDumpPath3(options) {
|
|
6132
|
+
const v = options.debugDump ?? process.env.UILINT_DEBUG_DUMP;
|
|
6133
|
+
if (!v) return null;
|
|
6134
|
+
if (v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes") {
|
|
6135
|
+
return resolve6(process.cwd(), ".uilint");
|
|
6136
|
+
}
|
|
6137
|
+
return v;
|
|
6138
|
+
}
|
|
6139
|
+
function debugLog3(enabled, message, obj) {
|
|
6140
|
+
if (!enabled) return;
|
|
6141
|
+
if (obj === void 0) {
|
|
6142
|
+
console.error(pc.dim("[uilint:debug]"), message);
|
|
6143
|
+
} else {
|
|
6144
|
+
try {
|
|
6145
|
+
console.error(pc.dim("[uilint:debug]"), message, obj);
|
|
6146
|
+
} catch {
|
|
6147
|
+
console.error(pc.dim("[uilint:debug]"), message);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
function findScreenshotsDirUpwards(startDir) {
|
|
6152
|
+
let dir = startDir;
|
|
6153
|
+
for (let i = 0; i < 20; i++) {
|
|
6154
|
+
const candidate = join19(dir, ".uilint", "screenshots");
|
|
6155
|
+
if (existsSync19(candidate)) return candidate;
|
|
6156
|
+
const parent = dirname12(dir);
|
|
6157
|
+
if (parent === dir) break;
|
|
6158
|
+
dir = parent;
|
|
6159
|
+
}
|
|
6160
|
+
return null;
|
|
6161
|
+
}
|
|
6162
|
+
function listScreenshotSidecars(dirPath) {
|
|
6163
|
+
if (!existsSync19(dirPath)) return [];
|
|
6164
|
+
const entries = readdirSync6(dirPath).filter((f) => f.endsWith(".json")).map((f) => join19(dirPath, f));
|
|
6165
|
+
const out = [];
|
|
6166
|
+
for (const p2 of entries) {
|
|
6167
|
+
try {
|
|
6168
|
+
const json = loadJsonFile(p2);
|
|
6169
|
+
const issues = Array.isArray(json?.issues) ? json.issues : json?.analysisResult?.issues;
|
|
6170
|
+
out.push({
|
|
6171
|
+
path: p2,
|
|
6172
|
+
filename: json?.filename || json?.screenshotFile || p2.split("/").pop() || p2,
|
|
6173
|
+
timestamp: typeof json?.timestamp === "number" ? json.timestamp : void 0,
|
|
6174
|
+
route: typeof json?.route === "string" ? json.route : void 0,
|
|
6175
|
+
issueCount: Array.isArray(issues) ? issues.length : void 0
|
|
6176
|
+
});
|
|
6177
|
+
} catch {
|
|
6178
|
+
out.push({
|
|
6179
|
+
path: p2,
|
|
6180
|
+
filename: p2.split("/").pop() || p2
|
|
6181
|
+
});
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
out.sort((a, b) => {
|
|
6185
|
+
const at = a.timestamp ?? 0;
|
|
6186
|
+
const bt = b.timestamp ?? 0;
|
|
6187
|
+
if (at !== bt) return bt - at;
|
|
6188
|
+
return b.path.localeCompare(a.path);
|
|
6189
|
+
});
|
|
6190
|
+
return out;
|
|
6191
|
+
}
|
|
6192
|
+
function readImageAsBase64(imagePath) {
|
|
6193
|
+
const bytes = readFileSync13(imagePath);
|
|
6194
|
+
return { base64: bytes.toString("base64"), sizeBytes: bytes.byteLength };
|
|
6195
|
+
}
|
|
6196
|
+
function loadJsonFile(filePath) {
|
|
6197
|
+
const raw = readFileSync13(filePath, "utf-8");
|
|
6198
|
+
return JSON.parse(raw);
|
|
6199
|
+
}
|
|
6200
|
+
function formatIssuesText(issues) {
|
|
6201
|
+
if (issues.length === 0) return "No vision issues found.\n";
|
|
6202
|
+
return issues.map((i) => {
|
|
6203
|
+
const sev = i.severity || "info";
|
|
6204
|
+
const cat = i.category || "other";
|
|
6205
|
+
const where = i.dataLoc ? ` (${i.dataLoc})` : "";
|
|
6206
|
+
return `- [${sev}/${cat}] ${i.message}${where}`;
|
|
6207
|
+
}).join("\n") + "\n";
|
|
6208
|
+
}
|
|
6209
|
+
async function vision(options) {
|
|
6210
|
+
const isJsonOutput = options.output === "json";
|
|
6211
|
+
const dbg = debugEnabled3(options);
|
|
6212
|
+
const dbgFull = debugFullEnabled3(options);
|
|
6213
|
+
const dbgDump = debugDumpPath3(options);
|
|
6214
|
+
if (!isJsonOutput) intro2("Vision (Screenshot) Analysis");
|
|
6215
|
+
try {
|
|
6216
|
+
const projectPath = process.cwd();
|
|
6217
|
+
if (options.list) {
|
|
6218
|
+
const base = (options.screenshotsDir ? resolvePathSpecifier(options.screenshotsDir, projectPath) : null) || findScreenshotsDirUpwards(projectPath);
|
|
6219
|
+
if (!base) {
|
|
6220
|
+
if (isJsonOutput) {
|
|
6221
|
+
printJSON({ screenshotsDir: null, sidecars: [] });
|
|
6222
|
+
} else {
|
|
6223
|
+
logWarning(
|
|
6224
|
+
"No `.uilint/screenshots` directory found (walked up from cwd)."
|
|
6225
|
+
);
|
|
6226
|
+
}
|
|
6227
|
+
await flushLangfuse();
|
|
6228
|
+
return;
|
|
6229
|
+
}
|
|
6230
|
+
const sidecars = listScreenshotSidecars(base);
|
|
6231
|
+
if (isJsonOutput) {
|
|
6232
|
+
printJSON({ screenshotsDir: base, sidecars });
|
|
6233
|
+
} else {
|
|
6234
|
+
logInfo(`Screenshots dir: ${pc.dim(base)}`);
|
|
6235
|
+
if (sidecars.length === 0) {
|
|
6236
|
+
process.stdout.write("No sidecars found.\n");
|
|
6237
|
+
} else {
|
|
6238
|
+
process.stdout.write(
|
|
6239
|
+
sidecars.map((s, idx) => {
|
|
6240
|
+
const stamp = s.timestamp ? new Date(s.timestamp).toLocaleString() : "(no timestamp)";
|
|
6241
|
+
const route = s.route ? ` ${pc.dim(s.route)}` : "";
|
|
6242
|
+
const count = typeof s.issueCount === "number" ? ` ${pc.dim(`(${s.issueCount} issues)`)}` : "";
|
|
6243
|
+
return `${idx === 0 ? "*" : "-"} ${s.path}${pc.dim(
|
|
6244
|
+
` \u2014 ${stamp}`
|
|
6245
|
+
)}${route}${count}`;
|
|
6246
|
+
}).join("\n") + "\n"
|
|
6247
|
+
);
|
|
6248
|
+
process.stdout.write(
|
|
6249
|
+
pc.dim(
|
|
6250
|
+
`Tip: run \`uilint vision --sidecar <path>\` (the newest is marked with "*").
|
|
6251
|
+
`
|
|
6252
|
+
)
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
await flushLangfuse();
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6259
|
+
const imagePath = options.image ? resolvePathSpecifier(options.image, projectPath) : void 0;
|
|
6260
|
+
const sidecarPath = options.sidecar ? resolvePathSpecifier(options.sidecar, projectPath) : void 0;
|
|
6261
|
+
const manifestFilePath = options.manifestFile ? resolvePathSpecifier(options.manifestFile, projectPath) : void 0;
|
|
6262
|
+
if (!imagePath && !sidecarPath) {
|
|
6263
|
+
if (isJsonOutput) {
|
|
6264
|
+
printJSON({ error: "No input provided", issues: [] });
|
|
6265
|
+
} else {
|
|
6266
|
+
logError("No input provided. Use --image or --sidecar.");
|
|
6267
|
+
}
|
|
6268
|
+
await flushLangfuse();
|
|
6269
|
+
process.exit(1);
|
|
6270
|
+
}
|
|
6271
|
+
if (imagePath && !existsSync19(imagePath)) {
|
|
6272
|
+
throw new Error(`Image not found: ${imagePath}`);
|
|
6273
|
+
}
|
|
6274
|
+
if (sidecarPath && !existsSync19(sidecarPath)) {
|
|
6275
|
+
throw new Error(`Sidecar not found: ${sidecarPath}`);
|
|
6276
|
+
}
|
|
6277
|
+
if (manifestFilePath && !existsSync19(manifestFilePath)) {
|
|
6278
|
+
throw new Error(`Manifest file not found: ${manifestFilePath}`);
|
|
6279
|
+
}
|
|
6280
|
+
const sidecar = sidecarPath ? loadJsonFile(sidecarPath) : null;
|
|
6281
|
+
const routeLabel = options.route || (typeof sidecar?.route === "string" ? sidecar.route : void 0) || (sidecarPath ? `(from ${sidecarPath})` : "(unknown)");
|
|
6282
|
+
let manifest = null;
|
|
6283
|
+
if (options.manifestJson) {
|
|
6284
|
+
manifest = JSON.parse(options.manifestJson);
|
|
6285
|
+
} else if (manifestFilePath) {
|
|
6286
|
+
manifest = loadJsonFile(manifestFilePath);
|
|
6287
|
+
} else if (sidecar && Array.isArray(sidecar.manifest)) {
|
|
6288
|
+
manifest = sidecar.manifest;
|
|
6289
|
+
}
|
|
6290
|
+
if (!manifest || manifest.length === 0) {
|
|
6291
|
+
throw new Error(
|
|
6292
|
+
"No manifest provided. Supply --manifest-json, --manifest-file, or a sidecar JSON with a `manifest` array."
|
|
6293
|
+
);
|
|
6294
|
+
}
|
|
6295
|
+
let styleGuide = null;
|
|
6296
|
+
let styleguideLocation = null;
|
|
6297
|
+
const startPath = (imagePath ?? sidecarPath ?? manifestFilePath ?? void 0) || void 0;
|
|
6298
|
+
{
|
|
6299
|
+
const resolved = await resolveVisionStyleGuide({
|
|
6300
|
+
projectPath,
|
|
6301
|
+
styleguide: options.styleguide,
|
|
6302
|
+
startDir: startPath ? dirname12(startPath) : projectPath
|
|
6303
|
+
});
|
|
6304
|
+
styleGuide = resolved.styleGuide;
|
|
6305
|
+
styleguideLocation = resolved.styleguideLocation;
|
|
6306
|
+
}
|
|
6307
|
+
if (styleguideLocation && styleGuide) {
|
|
6308
|
+
if (!isJsonOutput)
|
|
6309
|
+
logSuccess(`Using styleguide: ${pc.dim(styleguideLocation)}`);
|
|
6310
|
+
} else if (!styleGuide && !isJsonOutput) {
|
|
6311
|
+
logWarning("No styleguide found");
|
|
6312
|
+
note2(
|
|
6313
|
+
[
|
|
6314
|
+
`Searched in: ${options.styleguide || projectPath}`,
|
|
6315
|
+
"",
|
|
6316
|
+
"Looked for:",
|
|
6317
|
+
...STYLEGUIDE_PATHS2.map((p2) => ` \u2022 ${p2}`),
|
|
6318
|
+
"",
|
|
6319
|
+
`Create ${pc.cyan(
|
|
6320
|
+
".uilint/styleguide.md"
|
|
6321
|
+
)} (recommended: run ${pc.cyan("/genstyleguide")} in Cursor).`
|
|
6322
|
+
].join("\n"),
|
|
6323
|
+
"Missing Styleguide"
|
|
6324
|
+
);
|
|
6325
|
+
}
|
|
6326
|
+
debugLog3(dbg, "Vision input (high-level)", {
|
|
6327
|
+
imagePath: imagePath ?? null,
|
|
6328
|
+
sidecarPath: sidecarPath ?? null,
|
|
6329
|
+
manifestFile: manifestFilePath ?? null,
|
|
6330
|
+
manifestElements: manifest.length,
|
|
6331
|
+
route: routeLabel,
|
|
6332
|
+
styleguideLocation,
|
|
6333
|
+
styleGuideLength: styleGuide ? styleGuide.length : 0
|
|
6334
|
+
});
|
|
6335
|
+
const visionModel = options.model || UILINT_DEFAULT_VISION_MODEL2;
|
|
6336
|
+
const prepStartNs = nsNow();
|
|
6337
|
+
if (!isJsonOutput) {
|
|
6338
|
+
await withSpinner("Preparing Ollama", async () => {
|
|
6339
|
+
await ensureOllamaReady6({
|
|
6340
|
+
model: visionModel,
|
|
6341
|
+
baseUrl: options.baseUrl
|
|
6342
|
+
});
|
|
6343
|
+
});
|
|
6344
|
+
} else {
|
|
6345
|
+
await ensureOllamaReady6({ model: visionModel, baseUrl: options.baseUrl });
|
|
6346
|
+
}
|
|
6347
|
+
const prepEndNs = nsNow();
|
|
6348
|
+
const resolvedImagePath = imagePath || (() => {
|
|
6349
|
+
const screenshotFile = typeof sidecar?.screenshotFile === "string" ? sidecar.screenshotFile : typeof sidecar?.filename === "string" ? sidecar.filename : void 0;
|
|
6350
|
+
if (!screenshotFile) return null;
|
|
6351
|
+
const baseDir = sidecarPath ? dirname12(sidecarPath) : projectPath;
|
|
6352
|
+
const abs = resolve6(baseDir, screenshotFile);
|
|
6353
|
+
return abs;
|
|
6354
|
+
})();
|
|
6355
|
+
if (!resolvedImagePath) {
|
|
6356
|
+
throw new Error(
|
|
6357
|
+
"No image path could be resolved. Provide --image or a sidecar with `screenshotFile`/`filename`."
|
|
6358
|
+
);
|
|
6359
|
+
}
|
|
6360
|
+
if (!existsSync19(resolvedImagePath)) {
|
|
6361
|
+
throw new Error(`Image not found: ${resolvedImagePath}`);
|
|
6362
|
+
}
|
|
6363
|
+
const { base64, sizeBytes } = readImageAsBase64(resolvedImagePath);
|
|
6364
|
+
debugLog3(dbg, "Image loaded", {
|
|
6365
|
+
imagePath: resolvedImagePath,
|
|
6366
|
+
sizeBytes,
|
|
6367
|
+
base64Length: base64.length
|
|
6368
|
+
});
|
|
6369
|
+
if (dbgFull && styleGuide) {
|
|
6370
|
+
debugLog3(dbg, "Styleguide (full)", styleGuide);
|
|
6371
|
+
} else if (dbg && styleGuide) {
|
|
6372
|
+
debugLog3(dbg, "Styleguide (preview)", preview3(styleGuide, 800));
|
|
6373
|
+
}
|
|
6374
|
+
let result = null;
|
|
6375
|
+
const analysisStartNs = nsNow();
|
|
6376
|
+
let firstTokenNs = null;
|
|
6377
|
+
let firstThinkingNs = null;
|
|
6378
|
+
let lastThinkingNs = null;
|
|
6379
|
+
let firstAnswerNs = null;
|
|
6380
|
+
let lastAnswerNs = null;
|
|
6381
|
+
if (isJsonOutput) {
|
|
6382
|
+
result = await runVisionAnalysis({
|
|
6383
|
+
imageBase64: base64,
|
|
6384
|
+
manifest,
|
|
6385
|
+
projectPath,
|
|
6386
|
+
styleGuide,
|
|
6387
|
+
styleguideLocation,
|
|
6388
|
+
baseUrl: options.baseUrl,
|
|
6389
|
+
model: visionModel,
|
|
6390
|
+
skipEnsureOllama: true,
|
|
6391
|
+
debugDump: dbgDump ?? void 0,
|
|
6392
|
+
debugDumpIncludeSensitive: dbgFull,
|
|
6393
|
+
debugDumpMetadata: {
|
|
6394
|
+
route: routeLabel,
|
|
6395
|
+
imagePath: resolvedImagePath,
|
|
6396
|
+
imageSizeBytes: sizeBytes,
|
|
6397
|
+
imageBase64Length: base64.length
|
|
6398
|
+
}
|
|
6399
|
+
});
|
|
6400
|
+
} else {
|
|
6401
|
+
if (options.stream) {
|
|
6402
|
+
let lastStatus = "";
|
|
6403
|
+
let printedAnyText = false;
|
|
6404
|
+
let inThinking = false;
|
|
6405
|
+
result = await runVisionAnalysis({
|
|
6406
|
+
imageBase64: base64,
|
|
6407
|
+
manifest,
|
|
6408
|
+
projectPath,
|
|
6409
|
+
styleGuide,
|
|
6410
|
+
styleguideLocation,
|
|
6411
|
+
baseUrl: options.baseUrl,
|
|
6412
|
+
model: visionModel,
|
|
6413
|
+
skipEnsureOllama: true,
|
|
6414
|
+
debugDump: dbgDump ?? void 0,
|
|
6415
|
+
debugDumpIncludeSensitive: dbgFull,
|
|
6416
|
+
debugDumpMetadata: {
|
|
6417
|
+
route: routeLabel,
|
|
6418
|
+
imagePath: resolvedImagePath,
|
|
6419
|
+
imageSizeBytes: sizeBytes,
|
|
6420
|
+
imageBase64Length: base64.length
|
|
6421
|
+
},
|
|
6422
|
+
onProgress: (latestLine, _fullResponse, delta, thinkingDelta) => {
|
|
6423
|
+
const nowNs = nsNow();
|
|
6424
|
+
if (!firstTokenNs && (thinkingDelta || delta)) firstTokenNs = nowNs;
|
|
6425
|
+
if (thinkingDelta) {
|
|
6426
|
+
if (!firstThinkingNs) firstThinkingNs = nowNs;
|
|
6427
|
+
lastThinkingNs = nowNs;
|
|
6428
|
+
}
|
|
6429
|
+
if (delta) {
|
|
6430
|
+
if (!firstAnswerNs) firstAnswerNs = nowNs;
|
|
6431
|
+
lastAnswerNs = nowNs;
|
|
6432
|
+
}
|
|
6433
|
+
if (thinkingDelta) {
|
|
6434
|
+
if (!printedAnyText) {
|
|
6435
|
+
printedAnyText = true;
|
|
6436
|
+
console.error(pc.dim("[vision] streaming:"));
|
|
6437
|
+
process.stderr.write(pc.dim("Thinking:\n"));
|
|
6438
|
+
inThinking = true;
|
|
6439
|
+
} else if (!inThinking) {
|
|
6440
|
+
process.stderr.write(pc.dim("\n\nThinking:\n"));
|
|
6441
|
+
inThinking = true;
|
|
6442
|
+
}
|
|
6443
|
+
process.stderr.write(thinkingDelta);
|
|
6444
|
+
return;
|
|
6445
|
+
}
|
|
6446
|
+
if (delta) {
|
|
6447
|
+
if (!printedAnyText) {
|
|
6448
|
+
printedAnyText = true;
|
|
6449
|
+
console.error(pc.dim("[vision] streaming:"));
|
|
6450
|
+
}
|
|
6451
|
+
if (inThinking) {
|
|
6452
|
+
process.stderr.write(pc.dim("\n\nAnswer:\n"));
|
|
6453
|
+
inThinking = false;
|
|
6454
|
+
}
|
|
6455
|
+
process.stderr.write(delta);
|
|
6456
|
+
return;
|
|
6457
|
+
}
|
|
6458
|
+
const line = (latestLine || "").trim();
|
|
6459
|
+
if (!line || line === lastStatus) return;
|
|
6460
|
+
lastStatus = line;
|
|
6461
|
+
console.error(pc.dim("[vision]"), line);
|
|
6462
|
+
}
|
|
6463
|
+
});
|
|
6464
|
+
} else {
|
|
6465
|
+
result = await withSpinner(
|
|
6466
|
+
"Analyzing screenshot with vision model",
|
|
6467
|
+
async (s) => {
|
|
6468
|
+
return await runVisionAnalysis({
|
|
6469
|
+
imageBase64: base64,
|
|
6470
|
+
manifest,
|
|
6471
|
+
projectPath,
|
|
6472
|
+
styleGuide,
|
|
6473
|
+
styleguideLocation,
|
|
6474
|
+
baseUrl: options.baseUrl,
|
|
6475
|
+
model: visionModel,
|
|
6476
|
+
skipEnsureOllama: true,
|
|
6477
|
+
debugDump: dbgDump ?? void 0,
|
|
6478
|
+
debugDumpIncludeSensitive: dbgFull,
|
|
6479
|
+
debugDumpMetadata: {
|
|
6480
|
+
route: routeLabel,
|
|
6481
|
+
imagePath: resolvedImagePath,
|
|
6482
|
+
imageSizeBytes: sizeBytes,
|
|
6483
|
+
imageBase64Length: base64.length
|
|
6484
|
+
},
|
|
6485
|
+
onProgress: (latestLine, _fullResponse, delta, thinkingDelta) => {
|
|
6486
|
+
const nowNs = nsNow();
|
|
6487
|
+
if (!firstTokenNs && (thinkingDelta || delta))
|
|
6488
|
+
firstTokenNs = nowNs;
|
|
6489
|
+
if (thinkingDelta) {
|
|
6490
|
+
if (!firstThinkingNs) firstThinkingNs = nowNs;
|
|
6491
|
+
lastThinkingNs = nowNs;
|
|
6492
|
+
}
|
|
6493
|
+
if (delta) {
|
|
6494
|
+
if (!firstAnswerNs) firstAnswerNs = nowNs;
|
|
6495
|
+
lastAnswerNs = nowNs;
|
|
6496
|
+
}
|
|
6497
|
+
const maxLen = 60;
|
|
6498
|
+
const displayLine = latestLine.length > maxLen ? latestLine.slice(0, maxLen) + "\u2026" : latestLine;
|
|
6499
|
+
s.message(`Analyzing: ${pc.dim(displayLine || "...")}`);
|
|
6500
|
+
}
|
|
6501
|
+
});
|
|
6502
|
+
}
|
|
6503
|
+
);
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
const analysisEndNs = nsNow();
|
|
6507
|
+
const issues = result?.issues ?? [];
|
|
6508
|
+
if (isJsonOutput) {
|
|
6509
|
+
printJSON({
|
|
6510
|
+
route: routeLabel,
|
|
6511
|
+
model: visionModel,
|
|
6512
|
+
issues,
|
|
6513
|
+
analysisTime: result?.analysisTime ?? 0,
|
|
6514
|
+
imagePath: resolvedImagePath,
|
|
6515
|
+
imageSizeBytes: sizeBytes
|
|
6516
|
+
});
|
|
6517
|
+
} else {
|
|
6518
|
+
logInfo(`Route: ${pc.dim(routeLabel)}`);
|
|
6519
|
+
logInfo(`Model: ${pc.dim(visionModel)}`);
|
|
6520
|
+
process.stdout.write(formatIssuesText(issues));
|
|
6521
|
+
if (process.stdout.isTTY) {
|
|
6522
|
+
const prepMs = nsToMs(prepEndNs - prepStartNs);
|
|
6523
|
+
const totalMs = nsToMs(analysisEndNs - analysisStartNs);
|
|
6524
|
+
const endToEndMs = nsToMs(analysisEndNs - prepStartNs);
|
|
6525
|
+
const ttftMs = firstTokenNs ? nsToMs(firstTokenNs - analysisStartNs) : null;
|
|
6526
|
+
const thinkingMs = firstThinkingNs && (firstAnswerNs || lastThinkingNs) ? nsToMs(
|
|
6527
|
+
(firstAnswerNs ?? lastThinkingNs ?? analysisEndNs) - firstThinkingNs
|
|
6528
|
+
) : null;
|
|
6529
|
+
const outputMs = firstAnswerNs && (lastAnswerNs || analysisEndNs) ? nsToMs((lastAnswerNs ?? analysisEndNs) - firstAnswerNs) : null;
|
|
6530
|
+
note2(
|
|
6531
|
+
[
|
|
6532
|
+
`Prepare Ollama: ${formatMs(prepMs)}`,
|
|
6533
|
+
`Time to first token: ${maybeMs(ttftMs)}`,
|
|
6534
|
+
`Thinking: ${maybeMs(thinkingMs)}`,
|
|
6535
|
+
`Outputting: ${maybeMs(outputMs)}`,
|
|
6536
|
+
`LLM total: ${formatMs(totalMs)}`,
|
|
6537
|
+
`End-to-end: ${formatMs(endToEndMs)}`,
|
|
6538
|
+
result?.analysisTime ? pc.dim(`(core analysisTime: ${formatMs(result.analysisTime)})`) : pc.dim("(core analysisTime: n/a)")
|
|
6539
|
+
].join("\n"),
|
|
6540
|
+
"Timings"
|
|
6541
|
+
);
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
try {
|
|
6545
|
+
writeVisionMarkdownReport({
|
|
6546
|
+
imagePath: resolvedImagePath,
|
|
6547
|
+
route: routeLabel,
|
|
6548
|
+
visionModel,
|
|
6549
|
+
baseUrl: options.baseUrl ?? "http://localhost:11434",
|
|
6550
|
+
analysisTimeMs: result?.analysisTime ?? 0,
|
|
6551
|
+
prompt: result?.prompt ?? null,
|
|
6552
|
+
rawResponse: result?.rawResponse ?? null,
|
|
6553
|
+
metadata: {
|
|
6554
|
+
imageSizeBytes: sizeBytes,
|
|
6555
|
+
styleguideLocation
|
|
6556
|
+
}
|
|
6557
|
+
});
|
|
6558
|
+
debugLog3(dbg, "Wrote .vision.md report alongside image");
|
|
6559
|
+
} catch (e) {
|
|
6560
|
+
debugLog3(
|
|
6561
|
+
dbg,
|
|
6562
|
+
"Failed to write .vision.md report",
|
|
6563
|
+
e instanceof Error ? e.message : e
|
|
6564
|
+
);
|
|
6565
|
+
}
|
|
6566
|
+
if (issues.length > 0) {
|
|
6567
|
+
await flushLangfuse();
|
|
6568
|
+
process.exit(1);
|
|
6569
|
+
}
|
|
6570
|
+
} catch (error) {
|
|
6571
|
+
if (options.output === "json") {
|
|
6572
|
+
printJSON({
|
|
6573
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
6574
|
+
issues: []
|
|
6575
|
+
});
|
|
6576
|
+
} else {
|
|
6577
|
+
logError(
|
|
6578
|
+
error instanceof Error ? error.message : "Vision analysis failed"
|
|
6579
|
+
);
|
|
6580
|
+
}
|
|
6581
|
+
await flushLangfuse();
|
|
6582
|
+
process.exit(1);
|
|
6583
|
+
}
|
|
6584
|
+
await flushLangfuse();
|
|
6585
|
+
}
|
|
6586
|
+
|
|
4574
6587
|
// src/commands/session.ts
|
|
4575
|
-
import { existsSync as
|
|
4576
|
-
import { basename, dirname as
|
|
6588
|
+
import { existsSync as existsSync20, readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
|
|
6589
|
+
import { basename, dirname as dirname13, resolve as resolve7 } from "path";
|
|
4577
6590
|
import { createStyleSummary as createStyleSummary3 } from "uilint-core";
|
|
4578
6591
|
import {
|
|
4579
|
-
ensureOllamaReady as
|
|
6592
|
+
ensureOllamaReady as ensureOllamaReady7,
|
|
4580
6593
|
parseCLIInput as parseCLIInput2,
|
|
4581
6594
|
readStyleGuideFromProject as readStyleGuideFromProject2,
|
|
4582
6595
|
readTailwindThemeTokens as readTailwindThemeTokens3
|
|
@@ -4584,18 +6597,18 @@ import {
|
|
|
4584
6597
|
var SESSION_FILE = "/tmp/uilint-session.json";
|
|
4585
6598
|
var UI_FILE_EXTENSIONS = [".tsx", ".jsx", ".css", ".scss", ".module.css"];
|
|
4586
6599
|
function readSession() {
|
|
4587
|
-
if (!
|
|
6600
|
+
if (!existsSync20(SESSION_FILE)) {
|
|
4588
6601
|
return { files: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4589
6602
|
}
|
|
4590
6603
|
try {
|
|
4591
|
-
const content =
|
|
6604
|
+
const content = readFileSync14(SESSION_FILE, "utf-8");
|
|
4592
6605
|
return JSON.parse(content);
|
|
4593
6606
|
} catch {
|
|
4594
6607
|
return { files: [], startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4595
6608
|
}
|
|
4596
6609
|
}
|
|
4597
6610
|
function writeSession(state) {
|
|
4598
|
-
|
|
6611
|
+
writeFileSync10(SESSION_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
4599
6612
|
}
|
|
4600
6613
|
function isUIFile(filePath) {
|
|
4601
6614
|
return UI_FILE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
@@ -4606,7 +6619,7 @@ function isScannableMarkupFile(filePath) {
|
|
|
4606
6619
|
);
|
|
4607
6620
|
}
|
|
4608
6621
|
async function sessionClear() {
|
|
4609
|
-
if (
|
|
6622
|
+
if (existsSync20(SESSION_FILE)) {
|
|
4610
6623
|
unlinkSync2(SESSION_FILE);
|
|
4611
6624
|
}
|
|
4612
6625
|
console.log(JSON.stringify({ cleared: true }));
|
|
@@ -4673,17 +6686,17 @@ async function sessionScan(options = {}) {
|
|
|
4673
6686
|
}
|
|
4674
6687
|
return;
|
|
4675
6688
|
}
|
|
4676
|
-
await
|
|
6689
|
+
await ensureOllamaReady7();
|
|
4677
6690
|
const client = await createLLMClient({});
|
|
4678
6691
|
const results = [];
|
|
4679
6692
|
for (const filePath of session.files) {
|
|
4680
|
-
if (!
|
|
6693
|
+
if (!existsSync20(filePath)) continue;
|
|
4681
6694
|
if (!isScannableMarkupFile(filePath)) continue;
|
|
4682
6695
|
try {
|
|
4683
|
-
const absolutePath =
|
|
4684
|
-
const htmlLike =
|
|
6696
|
+
const absolutePath = resolve7(process.cwd(), filePath);
|
|
6697
|
+
const htmlLike = readFileSync14(filePath, "utf-8");
|
|
4685
6698
|
const snapshot = parseCLIInput2(htmlLike);
|
|
4686
|
-
const tailwindSearchDir =
|
|
6699
|
+
const tailwindSearchDir = dirname13(absolutePath);
|
|
4687
6700
|
const tailwindTheme = readTailwindThemeTokens3(tailwindSearchDir);
|
|
4688
6701
|
const styleSummary = createStyleSummary3(snapshot.styles, {
|
|
4689
6702
|
html: snapshot.html,
|
|
@@ -4736,7 +6749,7 @@ async function sessionScan(options = {}) {
|
|
|
4736
6749
|
};
|
|
4737
6750
|
console.log(JSON.stringify(result));
|
|
4738
6751
|
}
|
|
4739
|
-
if (
|
|
6752
|
+
if (existsSync20(SESSION_FILE)) {
|
|
4740
6753
|
unlinkSync2(SESSION_FILE);
|
|
4741
6754
|
}
|
|
4742
6755
|
await flushLangfuse();
|
|
@@ -4747,9 +6760,9 @@ async function sessionList() {
|
|
|
4747
6760
|
}
|
|
4748
6761
|
|
|
4749
6762
|
// src/index.ts
|
|
4750
|
-
import { readFileSync as
|
|
4751
|
-
import { dirname as
|
|
4752
|
-
import { fileURLToPath as
|
|
6763
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
6764
|
+
import { dirname as dirname14, join as join20 } from "path";
|
|
6765
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4753
6766
|
function assertNodeVersion(minMajor) {
|
|
4754
6767
|
const ver = process.versions.node || "";
|
|
4755
6768
|
const majorStr = ver.split(".")[0] || "";
|
|
@@ -4765,9 +6778,9 @@ assertNodeVersion(20);
|
|
|
4765
6778
|
var program = new Command();
|
|
4766
6779
|
function getCLIVersion2() {
|
|
4767
6780
|
try {
|
|
4768
|
-
const
|
|
4769
|
-
const pkgPath =
|
|
4770
|
-
const pkg = JSON.parse(
|
|
6781
|
+
const __dirname3 = dirname14(fileURLToPath4(import.meta.url));
|
|
6782
|
+
const pkgPath = join20(__dirname3, "..", "package.json");
|
|
6783
|
+
const pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
|
|
4771
6784
|
return pkg.version || "0.0.0";
|
|
4772
6785
|
} catch {
|
|
4773
6786
|
return "0.0.0";
|
|
@@ -4870,6 +6883,40 @@ program.command("serve").description("Start WebSocket server for real-time UI li
|
|
|
4870
6883
|
port: parseInt(options.port, 10)
|
|
4871
6884
|
});
|
|
4872
6885
|
});
|
|
6886
|
+
program.command("vision").description("Analyze a screenshot with Ollama vision models (requires a manifest)").option("--list", "List available .uilint/screenshots sidecars and exit").option(
|
|
6887
|
+
"--screenshots-dir <path>",
|
|
6888
|
+
"Screenshots directory for --list (default: nearest .uilint/screenshots)"
|
|
6889
|
+
).option("--image <path>", "Path to a screenshot image (png/jpg)").option(
|
|
6890
|
+
"--sidecar <path>",
|
|
6891
|
+
"Path to a .uilint/screenshots/*.json sidecar (contains manifest + metadata)"
|
|
6892
|
+
).option("--manifest-file <path>", "Path to a manifest JSON file (array)").option("--manifest-json <json>", "Inline manifest JSON (array)").option("--route <route>", "Optional route label (e.g., /todos)").option(
|
|
6893
|
+
"-s, --styleguide <path>",
|
|
6894
|
+
"Path to style guide file OR project directory (falls back to upward search)"
|
|
6895
|
+
).option("-o, --output <format>", "Output format: text or json", "text").option("--model <name>", "Ollama vision model override", void 0).option("--base-url <url>", "Ollama base URL (default: http://localhost:11434)").option("--stream", "Stream model output/progress to stderr (text mode only)").option("--debug", "Enable debug logging (stderr)").option(
|
|
6896
|
+
"--debug-full",
|
|
6897
|
+
"Print full prompt/styleguide and include base64 in dumps (can be very large)"
|
|
6898
|
+
).option(
|
|
6899
|
+
"--debug-dump <path>",
|
|
6900
|
+
"Write full analysis payload dump to JSON file (or directory to auto-name)"
|
|
6901
|
+
).action(async (options) => {
|
|
6902
|
+
await vision({
|
|
6903
|
+
list: options.list,
|
|
6904
|
+
screenshotsDir: options.screenshotsDir,
|
|
6905
|
+
image: options.image,
|
|
6906
|
+
sidecar: options.sidecar,
|
|
6907
|
+
manifestFile: options.manifestFile,
|
|
6908
|
+
manifestJson: options.manifestJson,
|
|
6909
|
+
route: options.route,
|
|
6910
|
+
styleguide: options.styleguide,
|
|
6911
|
+
output: options.output,
|
|
6912
|
+
model: options.model,
|
|
6913
|
+
baseUrl: options.baseUrl,
|
|
6914
|
+
stream: options.stream,
|
|
6915
|
+
debug: options.debug,
|
|
6916
|
+
debugFull: options.debugFull,
|
|
6917
|
+
debugDump: options.debugDump
|
|
6918
|
+
});
|
|
6919
|
+
});
|
|
4873
6920
|
var sessionCmd = program.command("session").description(
|
|
4874
6921
|
"Manage file tracking for agentic sessions (used by Cursor hooks)"
|
|
4875
6922
|
);
|