@vibecodeqa/cli 0.37.4 → 0.38.1
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/cli.js +99 -13
- package/dist/monitor.js +103 -6
- package/dist/runners/react.js +31 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -227,7 +227,7 @@ async function writeOutputs(report, outputDir, flags) {
|
|
|
227
227
|
writeFileSync(join(outputDir, "report.sarif"), generateSARIF(report));
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
|
-
async function printResults(report, trend, flags, outputDir) {
|
|
230
|
+
async function printResults(report, trend, flags, outputDir, interactive) {
|
|
231
231
|
const { score, grade, checks } = report;
|
|
232
232
|
const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
|
|
233
233
|
if (flags.jsonOnly) {
|
|
@@ -248,15 +248,10 @@ async function printResults(report, trend, flags, outputDir) {
|
|
|
248
248
|
console.log(formatTrend(trend, scores));
|
|
249
249
|
}
|
|
250
250
|
console.log("");
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (flags.sarifMode)
|
|
256
|
-
console.log(` \x1b[2mSARIF: ${join(outputDir, "report.sarif")}\x1b[0m`);
|
|
257
|
-
console.log("");
|
|
258
|
-
// Top actionable issues
|
|
259
|
-
if (flags.topN > 0) {
|
|
251
|
+
// Top actionable issues. Explicit --top wins; otherwise show a few by default
|
|
252
|
+
// in an interactive terminal so the scan never dead-ends at a file path.
|
|
253
|
+
const effectiveTopN = flags.topN > 0 ? flags.topN : interactive ? 3 : 0;
|
|
254
|
+
if (effectiveTopN > 0) {
|
|
260
255
|
const allIssues = checks.flatMap((c) => c.issues.map((iss) => ({ check: c.name, weight: getCheckMeta(c.name).weight, ...iss })));
|
|
261
256
|
// Sort by: errors first, then by check weight (highest-impact first)
|
|
262
257
|
allIssues.sort((a, b) => {
|
|
@@ -266,9 +261,11 @@ async function printResults(report, trend, flags, outputDir) {
|
|
|
266
261
|
return sevDiff;
|
|
267
262
|
return b.weight - a.weight;
|
|
268
263
|
});
|
|
269
|
-
const top = allIssues.slice(0,
|
|
264
|
+
const top = allIssues.slice(0, effectiveTopN);
|
|
270
265
|
if (top.length > 0) {
|
|
271
|
-
|
|
266
|
+
const more = allIssues.length - top.length;
|
|
267
|
+
const moreStr = more > 0 ? ` \x1b[2m(+${more} more)\x1b[0m` : "";
|
|
268
|
+
console.log(` \x1b[1mTop ${top.length} issues to fix:\x1b[0m${moreStr}`);
|
|
272
269
|
for (const iss of top) {
|
|
273
270
|
const sevColor = iss.severity === "error" ? "\x1b[31m" : iss.severity === "warning" ? "\x1b[33m" : "\x1b[2m";
|
|
274
271
|
const sevChar = iss.severity[0].toUpperCase();
|
|
@@ -278,6 +275,37 @@ async function printResults(report, trend, flags, outputDir) {
|
|
|
278
275
|
console.log("");
|
|
279
276
|
}
|
|
280
277
|
}
|
|
278
|
+
// Next steps: surface the weakest scored dimensions and how to dig into each.
|
|
279
|
+
// Skipped in CI (clean machine-readable-ish output) — interactive runs get the on-ramp.
|
|
280
|
+
if (!flags.ciMode) {
|
|
281
|
+
const weakest = checks
|
|
282
|
+
.filter((c) => {
|
|
283
|
+
const det = c.details;
|
|
284
|
+
return !det.skipped && !det.comingSoon && c.score < 70;
|
|
285
|
+
})
|
|
286
|
+
.sort((a, b) => a.score - b.score)
|
|
287
|
+
.slice(0, 3);
|
|
288
|
+
if (weakest.length > 0) {
|
|
289
|
+
console.log(" \x1b[1mWeakest areas:\x1b[0m");
|
|
290
|
+
for (const c of weakest) {
|
|
291
|
+
const gc = color(c.grade);
|
|
292
|
+
const label = getCheckMeta(c.name).label || c.name;
|
|
293
|
+
console.log(` ${gc}${c.grade}\x1b[0m \x1b[2m${String(c.score).padStart(3)}\x1b[0m ${label.padEnd(18)}\x1b[2m→ vcqa explain ${c.name}\x1b[0m`);
|
|
294
|
+
}
|
|
295
|
+
console.log("");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Report paths + the interactive on-ramp.
|
|
299
|
+
console.log(` \x1b[2mReport: ${join(outputDir, "report/index.html")}\x1b[0m`);
|
|
300
|
+
console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
|
|
301
|
+
if (flags.badgeMode)
|
|
302
|
+
console.log(` \x1b[2mBadge: ${join(outputDir, "badge.svg")}\x1b[0m`);
|
|
303
|
+
if (flags.sarifMode)
|
|
304
|
+
console.log(` \x1b[2mSARIF: ${join(outputDir, "report.sarif")}\x1b[0m`);
|
|
305
|
+
if (!interactive && !flags.ciMode) {
|
|
306
|
+
console.log(` \x1b[2mExplore: \x1b[0m\x1b[1mvcqa monitor\x1b[0m\x1b[2m — live TUI to drill into issues & copy fix-prompts\x1b[0m`);
|
|
307
|
+
}
|
|
308
|
+
console.log("");
|
|
281
309
|
}
|
|
282
310
|
}
|
|
283
311
|
async function handleUpload(report, cwd, jsonOnly) {
|
|
@@ -866,11 +894,14 @@ async function main() {
|
|
|
866
894
|
};
|
|
867
895
|
const trend = computeTrend(report, outputDir);
|
|
868
896
|
await writeOutputs(report, outputDir, flags);
|
|
897
|
+
// Interactive = a real terminal session (not piped, CI, JSON, or watch). Gates the
|
|
898
|
+
// post-scan prompt and the default top-issues view.
|
|
899
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !quietMode && !ciMode && !watchMode;
|
|
869
900
|
if (flags.markdownMode) {
|
|
870
901
|
console.log(generateMarkdown(report, trend));
|
|
871
902
|
}
|
|
872
903
|
else {
|
|
873
|
-
await printResults(report, trend, flags, outputDir);
|
|
904
|
+
await printResults(report, trend, flags, outputDir, interactive);
|
|
874
905
|
}
|
|
875
906
|
if (flags.annotations) {
|
|
876
907
|
emitAnnotations(report);
|
|
@@ -900,7 +931,62 @@ async function main() {
|
|
|
900
931
|
}
|
|
901
932
|
if (watchMode) {
|
|
902
933
|
await startWatch(cwd);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
// Interactive on-ramp: offer to open the live monitor or the HTML report.
|
|
937
|
+
if (interactive && !flags.uploadMode && !flags.prComment) {
|
|
938
|
+
await promptNextAction(cwd, outputDir);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
/** Read a single keypress from a TTY, restoring stdin state afterward. */
|
|
942
|
+
function readKey() {
|
|
943
|
+
return new Promise((resolve) => {
|
|
944
|
+
const stdin = process.stdin;
|
|
945
|
+
const wasRaw = stdin.isRaw;
|
|
946
|
+
stdin.setRawMode?.(true);
|
|
947
|
+
stdin.resume();
|
|
948
|
+
stdin.once("data", (buf) => {
|
|
949
|
+
stdin.setRawMode?.(wasRaw ?? false);
|
|
950
|
+
stdin.pause();
|
|
951
|
+
resolve(buf.toString("utf-8"));
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
/** Open a file/URL with the OS default handler (detached). */
|
|
956
|
+
function openPath(target) {
|
|
957
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
958
|
+
import("node:child_process").then(({ spawn }) => {
|
|
959
|
+
try {
|
|
960
|
+
spawn(cmd, [target], { detached: true, stdio: "ignore", shell: process.platform === "win32" }).unref();
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
/* opening is best-effort */
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/** Post-scan prompt: [m] monitor · [o] open report · anything else quits. */
|
|
968
|
+
async function promptNextAction(cwd, outputDir) {
|
|
969
|
+
process.stdout.write(" \x1b[1m[m]\x1b[0m\x1b[2m monitor\x1b[0m \x1b[1m[o]\x1b[0m\x1b[2m open report\x1b[0m \x1b[1m[enter]\x1b[0m\x1b[2m quit\x1b[0m ");
|
|
970
|
+
let key;
|
|
971
|
+
try {
|
|
972
|
+
key = await readKey();
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
process.stdout.write("\n");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
process.stdout.write("\n");
|
|
979
|
+
const k = key.toLowerCase();
|
|
980
|
+
if (k === "m") {
|
|
981
|
+
const { startMonitor } = await import("./monitor.js");
|
|
982
|
+
await startMonitor(cwd);
|
|
983
|
+
}
|
|
984
|
+
else if (k === "o") {
|
|
985
|
+
const reportPath = join(outputDir, "report/index.html");
|
|
986
|
+
openPath(reportPath);
|
|
987
|
+
console.log(` \x1b[2mOpening ${reportPath}\x1b[0m`);
|
|
903
988
|
}
|
|
989
|
+
// any other key (enter, q, ctrl-c, …) → quit
|
|
904
990
|
}
|
|
905
991
|
async function checkForUpdate(currentVersion) {
|
|
906
992
|
try {
|
package/dist/monitor.js
CHANGED
|
@@ -425,6 +425,9 @@ function MonitorApp({ cwd }) {
|
|
|
425
425
|
{ time: ts(), text: cached ? `Loaded cached scan: ${cached.grade} ${cached.score}/100` : `Monitoring ${basename(cwd)}...`, type: cached ? "scan" : "info" },
|
|
426
426
|
]);
|
|
427
427
|
const [copied, setCopied] = useState(false);
|
|
428
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
429
|
+
const [search, setSearch] = useState("");
|
|
430
|
+
const [searchActive, setSearchActive] = useState(false);
|
|
428
431
|
const scanningRef = useRef(false);
|
|
429
432
|
const prevScoreRef = useRef(cached ? cached.score : null);
|
|
430
433
|
const addLog = useCallback((text, type = "info") => {
|
|
@@ -438,7 +441,16 @@ function MonitorApp({ cwd }) {
|
|
|
438
441
|
const o = { error: 0, warning: 1, info: 2 };
|
|
439
442
|
return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
|
|
440
443
|
}), [state.checks]);
|
|
441
|
-
|
|
444
|
+
// Issues filtered by the dashboard search query (matches message / check / file).
|
|
445
|
+
const displayIssues = useMemo(() => {
|
|
446
|
+
if (!search)
|
|
447
|
+
return allIssues;
|
|
448
|
+
const q = search.toLowerCase();
|
|
449
|
+
return allIssues.filter((i) => i.message.toLowerCase().includes(q) ||
|
|
450
|
+
i.check.toLowerCase().includes(q) ||
|
|
451
|
+
(typeof i.file === "string" && i.file.toLowerCase().includes(q)));
|
|
452
|
+
}, [allIssues, search]);
|
|
453
|
+
const currentList = panel === "checks" ? activeChecks : displayIssues;
|
|
442
454
|
// Derived data for file views (memoized, used by both render + keyboard)
|
|
443
455
|
const filesWithIssues = useMemo(() => {
|
|
444
456
|
const map = new Map();
|
|
@@ -529,13 +541,57 @@ function MonitorApp({ cwd }) {
|
|
|
529
541
|
}, [pendingCfg, monCfg]);
|
|
530
542
|
// ── Keyboard — single handler for all views ──
|
|
531
543
|
useInput((input, key) => {
|
|
544
|
+
// Ctrl-C always quits, even mid-search
|
|
545
|
+
if (key.ctrl && input === "c") {
|
|
546
|
+
exit();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Help overlay swallows all input until dismissed
|
|
550
|
+
if (showHelp) {
|
|
551
|
+
if (input === "?" || input === "q" || key.escape || key.return)
|
|
552
|
+
setShowHelp(false);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// Search input mode (filters the dashboard Issues panel)
|
|
556
|
+
if (searchActive) {
|
|
557
|
+
if (key.escape) {
|
|
558
|
+
setSearch("");
|
|
559
|
+
setSearchActive(false);
|
|
560
|
+
setCursor(0);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (key.return) {
|
|
564
|
+
setSearchActive(false);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (key.backspace || key.delete) {
|
|
568
|
+
setSearch((s) => s.slice(0, -1));
|
|
569
|
+
setCursor(0);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (input && input.length === 1 && input >= " " && !key.ctrl && !key.meta) {
|
|
573
|
+
setSearch((s) => s + input);
|
|
574
|
+
setCursor(0);
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
532
578
|
// Quit from anywhere
|
|
533
|
-
if (input === "q"
|
|
579
|
+
if (input === "q") {
|
|
534
580
|
exit();
|
|
535
581
|
return;
|
|
536
582
|
}
|
|
583
|
+
// Help overlay: ? opens from any view
|
|
584
|
+
if (input === "?") {
|
|
585
|
+
setShowHelp(true);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
537
588
|
// Esc: drill up one level or quit
|
|
538
589
|
if (key.escape) {
|
|
590
|
+
if (mode.view === "dashboard" && search) {
|
|
591
|
+
setSearch("");
|
|
592
|
+
setCursor(0);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
539
595
|
if (mode.view === "config") {
|
|
540
596
|
setPendingCfg(null);
|
|
541
597
|
setMode({ view: "dashboard" });
|
|
@@ -634,6 +690,12 @@ function MonitorApp({ cwd }) {
|
|
|
634
690
|
}
|
|
635
691
|
// ── Dashboard navigation ──
|
|
636
692
|
if (mode.view === "dashboard") {
|
|
693
|
+
if (input === "/") {
|
|
694
|
+
setSearchActive(true);
|
|
695
|
+
setPanel("issues");
|
|
696
|
+
setCursor(0);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
637
699
|
if (key.tab) {
|
|
638
700
|
setPanel((p) => p === "checks" ? "issues" : "checks");
|
|
639
701
|
setCursor(0);
|
|
@@ -648,8 +710,8 @@ function MonitorApp({ cwd }) {
|
|
|
648
710
|
setCursor(0);
|
|
649
711
|
return;
|
|
650
712
|
}
|
|
651
|
-
if (key.return && panel === "issues" &&
|
|
652
|
-
setMode({ view: "check-detail", checkName:
|
|
713
|
+
if (key.return && panel === "issues" && displayIssues[cursor]) {
|
|
714
|
+
setMode({ view: "check-detail", checkName: displayIssues[cursor].check });
|
|
653
715
|
setCursor(0);
|
|
654
716
|
return;
|
|
655
717
|
}
|
|
@@ -740,6 +802,9 @@ function MonitorApp({ cwd }) {
|
|
|
740
802
|
const proj = basename(cwd);
|
|
741
803
|
const p = monCfg.panels;
|
|
742
804
|
// ── Render views ──
|
|
805
|
+
if (showHelp) {
|
|
806
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), _jsx(HelpOverlay, { height: rows - 3 }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "? or Esc to close" }) })] }));
|
|
807
|
+
}
|
|
743
808
|
if (mode.view === "all-files") {
|
|
744
809
|
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), _jsx(AllFilesView, { checks: state.checks, height: rows - 3, cursor: cursor }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Esc back \u00B7 \u2191\u2193 select \u00B7 Enter view file issues \u00B7 q quit" }) })] }));
|
|
745
810
|
}
|
|
@@ -781,10 +846,42 @@ function MonitorApp({ cwd }) {
|
|
|
781
846
|
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), _jsx(Box, { paddingX: 1, gap: 2, height: 1, children: state.scanCount > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "E " }), _jsx(Text, { color: "red", bold: true, children: errorCount })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "W " }), _jsx(Text, { color: "yellow", bold: true, children: warnCount })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "checks " }), _jsx(Text, { bold: true, children: activeChecks.length })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "scan " }), _jsxs(Text, { children: [state.duration, "ms"] })] }), state.scores.length >= 2 && _jsx(Text, { color: "cyan", children: spark(state.scores) })] })) }), _jsxs(Box, { height: bodyRows, children: [sidebarVisible && (_jsxs(Box, { flexDirection: "column", width: 26, children: [p.score && _jsx(ScorePanel, { state: state, height: scoreH }), p.checks && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: panel === "checks" ? "magenta" : "gray", paddingX: 1, width: 24, height: checksH, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 Checks ", panel === "checks" && _jsx(Text, { dimColor: true, children: "\u25C4" })] }), activeChecks.slice(0, checksH - 3).map((c, i) => {
|
|
782
847
|
const sel = panel === "checks" && i === cursor;
|
|
783
848
|
return (_jsxs(Text, { children: [_jsxs(Text, { color: sel ? "white" : gc(c.grade), children: [sel ? "▸" : " ", c.grade === "A" ? "●" : c.grade === "B" ? "◐" : "○", " "] }), _jsx(Text, { bold: sel, children: c.name.slice(0, 13).padEnd(13) }), _jsx(Text, { color: gc(c.grade), children: String(c.score).padStart(3) })] }, c.name));
|
|
784
|
-
})] }))] })), mainVisible && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [p.activity && _jsx(ActivityPanel, { log: log, height: activityH }), p.issues && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: panel === "issues" ? "magenta" : "gray", paddingX: 1, height: issuesH, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" ", "\u25C8 Issues (", allIssues.length, ") ", panel === "issues" && _jsx(Text, { dimColor: true, children: "\u25C4" })] }),
|
|
849
|
+
})] }))] })), mainVisible && (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [p.activity && _jsx(ActivityPanel, { log: log, height: activityH }), p.issues && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: panel === "issues" ? "magenta" : "gray", paddingX: 1, height: issuesH, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" ", "\u25C8 Issues (", displayIssues.length, search ? `/${allIssues.length}` : "", ") ", panel === "issues" && _jsx(Text, { dimColor: true, children: "\u25C4" })] }), (searchActive || search) && (_jsxs(Text, { children: [" ", _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { color: searchActive ? "white" : "cyan", children: search }), searchActive && _jsx(Text, { color: "white", children: "\u258C" })] })), displayIssues.length === 0 && (_jsxs(Text, { color: search ? "yellow" : "green", children: [" ", search ? "No issues match." : state.scanCount > 0 ? "✓ No issues — clean scan!" : "Scanning…"] })), displayIssues.slice(0, issuesH - 3).map((iss, i) => {
|
|
785
850
|
const sel = panel === "issues" && i === cursor;
|
|
786
851
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: sel ? "white" : "gray", children: sel ? "▸" : " " }), _jsxs(Text, { color: sc(iss.severity), bold: true, children: [iss.severity[0].toUpperCase(), " "] }), _jsxs(Text, { dimColor: true, children: [(iss.check || "").slice(0, 11).padEnd(11), " "] }), iss.file && _jsxs(Text, { color: "cyan", children: [basename(String(iss.file)).slice(0, 18).padEnd(18), " "] }), _jsx(Text, { children: iss.message.slice(0, 40) })] }, `${iss.check}-${iss.file || ""}-${iss.line || i}`));
|
|
787
|
-
}),
|
|
852
|
+
}), displayIssues.length > issuesH - 3 && _jsxs(Text, { dimColor: true, children: [" +", displayIssues.length - (issuesH - 3), " more"] })] }))] })), !sidebarVisible && !mainVisible && (_jsx(Box, { height: bodyRows, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "All panels hidden. Press c to configure." }) }))] }), _jsx(Box, { paddingX: 1, justifyContent: "space-between", children: _jsx(Text, { dimColor: true, children: "Tab \u00B7 \u2191\u2193 Enter Esc \u00B7 / search \u00B7 r scan \u00B7 f files \u00B7 g git \u00B7 t trends \u00B7 c config \u00B7 ? help \u00B7 q" }) })] }));
|
|
853
|
+
}
|
|
854
|
+
function HelpOverlay({ height }) {
|
|
855
|
+
const groups = [
|
|
856
|
+
{
|
|
857
|
+
title: "Navigate",
|
|
858
|
+
keys: [
|
|
859
|
+
["↑ ↓", "move selection"],
|
|
860
|
+
["Enter", "drill in (check → issue → source)"],
|
|
861
|
+
["Tab", "switch Checks / Issues panel"],
|
|
862
|
+
["Esc", "back up one level (or clear search)"],
|
|
863
|
+
["q · Ctrl-C", "quit"],
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
title: "Views",
|
|
868
|
+
keys: [
|
|
869
|
+
["r", "re-scan now"],
|
|
870
|
+
["f", "all files by issue count"],
|
|
871
|
+
["g", "git-changed files"],
|
|
872
|
+
["t", "score trends"],
|
|
873
|
+
["c", "config (thresholds, panels)"],
|
|
874
|
+
],
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
title: "Issues",
|
|
878
|
+
keys: [
|
|
879
|
+
["/", "search / filter issues"],
|
|
880
|
+
["y", "copy an AI fix-prompt to clipboard"],
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
];
|
|
884
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 2, paddingY: 1, overflowY: "hidden", children: [_jsx(Text, { bold: true, color: "magenta", children: "\u25C8 Keyboard shortcuts" }), _jsx(Text, { children: " " }), groups.map((g) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: g.title }), g.keys.map(([k, desc]) => (_jsxs(Text, { children: [_jsx(Text, { color: "white", bold: true, children: ` ${k}`.padEnd(16) }), _jsx(Text, { dimColor: true, children: desc })] }, k)))] }, g.title)))] }));
|
|
788
885
|
}
|
|
789
886
|
function Header({ proj, stack, workspace, state }) {
|
|
790
887
|
return (_jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { children: [_jsx(Text, { color: "magenta", bold: true, children: "vcqa monitor" }), _jsxs(Text, { dimColor: true, children: [" v", VERSION] })] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: proj }), _jsxs(Text, { dimColor: true, children: [" ", stack.language, "/", stack.framework, workspace.isMonorepo ? ` · ${workspace.tool}` : ""] })] }), _jsxs(Text, { children: [state.score > 0 && _jsxs(Text, { color: gc(state.grade), bold: true, children: [state.grade, " ", state.score, " "] }), _jsx(Text, { dimColor: true, children: state.scanning ? "⟳ scanning" : "● watching" })] })] }));
|
package/dist/runners/react.js
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
* This runner catches patterns beyond what the plugin covers. */
|
|
4
4
|
import { getProductionFiles, readDeps } from "../fs-utils.js";
|
|
5
5
|
import { gradeFromScore } from "../types.js";
|
|
6
|
+
/** True when the `.map()` callback starting at line `i` actually returns JSX.
|
|
7
|
+
* `after` is the line text from `.map(` onward, right-trimmed. Distinguishes JSX
|
|
8
|
+
* returns from data maps (`=> ({...})`, `=> fn(...)`), TS generics, and comparisons. */
|
|
9
|
+
function mapCallbackReturnsJsx(after, lines, i) {
|
|
10
|
+
if (/=>\s*<[A-Za-z]/.test(after))
|
|
11
|
+
return true; // inline: => <Tag
|
|
12
|
+
if (/=>\s*\($/.test(after)) {
|
|
13
|
+
// multiline arrow body: => ( followed by JSX on the next non-empty line
|
|
14
|
+
return /^<[A-Za-z]/.test((lines[i + 1] || "").trim());
|
|
15
|
+
}
|
|
16
|
+
if (/=>\s*\{$/.test(after)) {
|
|
17
|
+
// block body: JSX iff it returns `<Tag` or `(` then `<Tag`
|
|
18
|
+
for (let j = i; j < Math.min(i + 12, lines.length); j++) {
|
|
19
|
+
const lt = (lines[j] || "").trim();
|
|
20
|
+
if (/return\s*<[A-Za-z]/.test(lt))
|
|
21
|
+
return true;
|
|
22
|
+
if (/return\s*\($/.test(lt))
|
|
23
|
+
return /^<[A-Za-z]/.test((lines[j + 1] || "").trim());
|
|
24
|
+
if (j > i && /^return\b/.test(lt))
|
|
25
|
+
return false; // returns a non-JSX value
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
6
30
|
export function runReact(cwd, stack) {
|
|
7
31
|
const start = Date.now();
|
|
8
32
|
if (stack.framework !== "react") {
|
|
@@ -68,11 +92,13 @@ export function runReact(cwd, stack) {
|
|
|
68
92
|
rule: "conditional-hook",
|
|
69
93
|
});
|
|
70
94
|
}
|
|
71
|
-
// 2. Missing key in .map() returning JSX
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
// 2. Missing key in .map() returning JSX. Only flag genuine JSX returns —
|
|
96
|
+
// not data maps, TS generics, or comparisons (see mapCallbackReturnsJsx).
|
|
97
|
+
const mapIdx = trimmed.indexOf(".map(");
|
|
98
|
+
if (mapIdx !== -1 && mapCallbackReturnsJsx(trimmed.slice(mapIdx).trimEnd(), lines, i)) {
|
|
99
|
+
// Inspect just the JSX head for a key — enough to cover the opening element.
|
|
100
|
+
const head = lines.slice(i, Math.min(i + 8, lines.length)).join("\n");
|
|
101
|
+
if (!head.includes("key=") && !head.includes("key:")) {
|
|
76
102
|
missingKeys++;
|
|
77
103
|
issues.push({ severity: "error", message: "JSX in .map() without key prop", file: f.path, line: i + 1, rule: "missing-key" });
|
|
78
104
|
}
|