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