@vibecodeqa/cli 0.35.5 → 0.35.7
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/monitor.js +183 -39
- package/package.json +1 -1
package/dist/monitor.js
CHANGED
|
@@ -10,9 +10,10 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|
|
10
10
|
import { render, Box, Text, useApp, useInput, useStdout } from "ink";
|
|
11
11
|
import { resolve, join, basename } from "node:path";
|
|
12
12
|
import { watch, existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
13
|
-
import { execFile } from "node:child_process";
|
|
13
|
+
import { execFile, execSync } from "node:child_process";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { detectStack, detectWorkspace } from "./detect.js";
|
|
16
|
+
import { loadHistory } from "./history.js";
|
|
16
17
|
const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
17
18
|
const CLI_PATH = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
18
19
|
const DEFAULTS = {
|
|
@@ -67,9 +68,19 @@ function spark(values) {
|
|
|
67
68
|
function ts() {
|
|
68
69
|
return new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
69
70
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
function copyToClipboard(text) {
|
|
72
|
+
try {
|
|
73
|
+
const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
|
|
74
|
+
execSync(cmd, { input: text, stdio: ["pipe", "pipe", "pipe"] });
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function buildFixPrompt(checkName, issue) {
|
|
82
|
+
const loc = issue.file ? `${issue.file}${issue.line ? `:${issue.line}` : ""}` : "";
|
|
83
|
+
return `Fix this ${issue.severity} in ${loc || "the project"}:\n${issue.message}${issue.rule ? ` (${issue.rule})` : ""}\nCheck: ${checkName}\n\nAnalyze the code, explain the issue, and provide the fix.`;
|
|
73
84
|
}
|
|
74
85
|
// ── Scan via child process — UI never freezes ──
|
|
75
86
|
function runScanProcess(cwd, skipTests) {
|
|
@@ -110,12 +121,6 @@ function ScorePanel({ state, height }) {
|
|
|
110
121
|
const active = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
|
|
111
122
|
return (_jsxs(Box, { flexDirection: "column", width: 24, height: height, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Score" }), _jsx(Box, { justifyContent: "center", marginY: 1, children: _jsx(Text, { color: gc(state.grade), bold: true, children: state.scanning ? " scanning..." : ` ${state.grade} ${state.score}/100` }) }), _jsxs(Text, { dimColor: true, children: [" ", active.length, " checks \u00B7 ", state.totalIssues, " issues"] }), _jsxs(Text, { dimColor: true, children: [" ", state.duration, "ms \u00B7 scan #", state.scanCount] }), s && _jsxs(Text, { color: "cyan", children: [" ", s] })] }));
|
|
112
123
|
}
|
|
113
|
-
function ChecksPanel({ checks, height }) {
|
|
114
|
-
const active = checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
|
|
115
|
-
const pro = checks.filter((c) => c.details.comingSoon);
|
|
116
|
-
const visibleLines = Math.max(1, height - 3);
|
|
117
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: 24, height: height, overflowY: "hidden", children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Checks" }), active.slice(0, visibleLines).map((c) => (_jsxs(Text, { children: [_jsxs(Text, { color: gc(c.grade), children: [" ", c.grade === "A" ? "●" : c.grade === "B" ? "◐" : "○", " "] }), _jsx(Text, { children: c.name.slice(0, 13).padEnd(13) }), _jsxs(Text, { dimColor: true, children: [" ", bar(c.score, 4), " "] }), _jsx(Text, { color: gc(c.grade), children: String(c.score).padStart(3) })] }, c.name))), active.length > visibleLines && _jsxs(Text, { dimColor: true, children: [" +", active.length - visibleLines, " more"] }), pro.length > 0 && _jsxs(Text, { color: "magenta", children: [" \u25C6 ", pro.length, " Pro"] })] }));
|
|
118
|
-
}
|
|
119
124
|
function ActivityPanel({ log, height }) {
|
|
120
125
|
const colors = {
|
|
121
126
|
info: "gray", scan: "cyan", change: "yellow",
|
|
@@ -124,18 +129,6 @@ function ActivityPanel({ log, height }) {
|
|
|
124
129
|
const visibleLines = Math.max(1, height - 3);
|
|
125
130
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, height: height, overflowY: "hidden", children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Activity" }), log.slice(-visibleLines).map((entry, i) => (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { dimColor: true, children: [entry.time, " "] }), _jsx(Text, { color: colors[entry.type], children: entry.text })] }, i)))] }));
|
|
126
131
|
}
|
|
127
|
-
function IssuesPanel({ checks, height }) {
|
|
128
|
-
const issues = checks
|
|
129
|
-
.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
|
|
130
|
-
.sort((a, b) => {
|
|
131
|
-
const o = { error: 0, warning: 1, info: 2 };
|
|
132
|
-
return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
|
|
133
|
-
});
|
|
134
|
-
const errors = issues.filter((i) => i.severity === "error").length;
|
|
135
|
-
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
136
|
-
const visibleLines = Math.max(1, height - 3);
|
|
137
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, height: height, overflowY: "hidden", children: [_jsxs(Text, { bold: true, color: "magenta", children: [" ", "\u25C8 Issues (", issues.length, ")", errors > 0 && _jsxs(Text, { color: "red", children: [" ", errors, "E"] }), warnings > 0 && _jsxs(Text, { color: "yellow", children: [" ", warnings, "W"] })] }), issues.slice(0, visibleLines).map((iss, i) => (_jsxs(Text, { wrap: "truncate", children: [_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, 45) })] }, i))), issues.length > visibleLines && _jsxs(Text, { dimColor: true, children: [" +", issues.length - visibleLines, " more"] })] }));
|
|
138
|
-
}
|
|
139
132
|
function ConfigScreen({ monCfg, onSave, onClose, }) {
|
|
140
133
|
const [cursor, setCursor] = useState(0);
|
|
141
134
|
const [cfg, setCfg] = useState(JSON.parse(JSON.stringify(monCfg)));
|
|
@@ -199,6 +192,55 @@ function ConfigScreen({ monCfg, onSave, onClose, }) {
|
|
|
199
192
|
return (_jsxs(Text, { children: [_jsxs(Text, { color: selected ? "white" : "gray", children: [prefix, opt.label.padEnd(28)] }), _jsxs(Text, { color: "cyan", bold: true, children: ["\u25C0 ", String(opt.value).padStart(5), " \u25B6"] })] }, opt.key));
|
|
200
193
|
}), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Space toggle \u00B7 \u2190\u2192 adjust \u00B7 Shift+\u2190\u2192 \u00D710" }), _jsx(Text, { dimColor: true, children: " s save & close \u00B7 Esc cancel" })] }));
|
|
201
194
|
}
|
|
195
|
+
// ── Trends Screen ──
|
|
196
|
+
function sparkFull(values, width) {
|
|
197
|
+
if (values.length < 2)
|
|
198
|
+
return "";
|
|
199
|
+
const bars = " ▁▂▃▄▅▆▇█";
|
|
200
|
+
const min = Math.min(...values);
|
|
201
|
+
const max = Math.max(...values);
|
|
202
|
+
const range = max - min || 1;
|
|
203
|
+
// Resample to fit width
|
|
204
|
+
const sampled = [];
|
|
205
|
+
for (let i = 0; i < width; i++) {
|
|
206
|
+
const idx = Math.round((i / (width - 1)) * (values.length - 1));
|
|
207
|
+
sampled.push(values[idx]);
|
|
208
|
+
}
|
|
209
|
+
return sampled.map((v) => bars[Math.round(((v - min) / range) * 8)]).join("");
|
|
210
|
+
}
|
|
211
|
+
function TrendsScreen({ cwd, height, onClose }) {
|
|
212
|
+
const historyDir = join(cwd, ".vibe-check", "history");
|
|
213
|
+
const history = loadHistory(historyDir);
|
|
214
|
+
useInput((input, key) => {
|
|
215
|
+
if (key.escape || input === "t")
|
|
216
|
+
onClose();
|
|
217
|
+
});
|
|
218
|
+
if (history.length < 2) {
|
|
219
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: " \u25C8 Trends" }), _jsx(Text, { dimColor: true, children: " Need at least 2 scans. Run the scanner a few more times." }), _jsx(Text, { dimColor: true, children: " Esc to go back" })] }));
|
|
220
|
+
}
|
|
221
|
+
const latest = history[history.length - 1];
|
|
222
|
+
const first = history[0];
|
|
223
|
+
const overallDelta = latest.score - first.score;
|
|
224
|
+
const scores = history.map((h) => h.score);
|
|
225
|
+
const chartWidth = 40;
|
|
226
|
+
// Get all check names from latest
|
|
227
|
+
const checkNames = [...latest.checkScores.keys()];
|
|
228
|
+
// Build per-check trends
|
|
229
|
+
const checkTrends = checkNames
|
|
230
|
+
.map((name) => {
|
|
231
|
+
const values = history.map((h) => h.checkScores.get(name) ?? 0).filter((v) => v > 0);
|
|
232
|
+
if (values.length < 2)
|
|
233
|
+
return null;
|
|
234
|
+
const current = values[values.length - 1];
|
|
235
|
+
const prev = values[0];
|
|
236
|
+
const delta = current - prev;
|
|
237
|
+
return { name, current, delta, spark: sparkFull(values, 20) };
|
|
238
|
+
})
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.sort((a, b) => a.delta - b.delta);
|
|
241
|
+
const visibleChecks = Math.max(1, height - 10);
|
|
242
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 Trends \u2014 ", history.length, " scans"] }), _jsxs(Text, { dimColor: true, children: [" ", first.timestamp.split("T")[0], " \u2192 ", latest.timestamp.split("T")[0]] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Overall Score" }), _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [" ", sparkFull(scores, chartWidth), " "] }), _jsxs(Text, { color: gc(latest.score >= 90 ? "A" : latest.score >= 75 ? "B" : "C"), bold: true, children: [" ", latest.score] }), _jsxs(Text, { color: overallDelta >= 0 ? "green" : "red", children: [" ", overallDelta >= 0 ? "+" : "", overallDelta] })] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: " Per-Check (first \u2192 latest)" }), checkTrends.slice(0, visibleChecks).map((t) => (_jsxs(Text, { children: [_jsxs(Text, { children: [" ", t.name.slice(0, 14).padEnd(14), " "] }), _jsxs(Text, { color: "cyan", children: [t.spark, " "] }), _jsxs(Text, { color: gc(t.current >= 90 ? "A" : t.current >= 75 ? "B" : "C"), children: [String(t.current).padStart(3), " "] }), _jsxs(Text, { color: t.delta >= 0 ? "green" : "red", children: [t.delta >= 0 ? "+" : "", t.delta] })] }, t.name))), checkTrends.length > visibleChecks && _jsxs(Text, { dimColor: true, children: [" +", checkTrends.length - visibleChecks, " more"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " Esc back to dashboard" }) })] }));
|
|
243
|
+
}
|
|
202
244
|
// ── Main App ──
|
|
203
245
|
/** Load last scan from .vibe-check/report.json for instant display on startup. */
|
|
204
246
|
function loadCachedScan(cwd) {
|
|
@@ -224,13 +266,27 @@ function loadCachedScan(cwd) {
|
|
|
224
266
|
return null;
|
|
225
267
|
}
|
|
226
268
|
}
|
|
269
|
+
// ── Check Detail View ──
|
|
270
|
+
function CheckDetail({ check, height, cursor, copied }) {
|
|
271
|
+
const visibleIssues = Math.max(1, height - 6);
|
|
272
|
+
// Scroll window: keep cursor visible
|
|
273
|
+
const scrollStart = Math.max(0, Math.min(cursor - Math.floor(visibleIssues / 2), check.issues.length - visibleIssues));
|
|
274
|
+
const visibleSlice = check.issues.slice(scrollStart, scrollStart + visibleIssues);
|
|
275
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: [" \u25C8 ", check.name] }), _jsxs(Text, { children: [_jsxs(Text, { color: gc(check.grade), bold: true, children: [" ", check.grade, " ", check.score, "/100"] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", check.issues.length, " issues \u00B7 ", check.duration, "ms"] }), copied && _jsx(Text, { color: "green", bold: true, children: " \u2713 Copied!" })] }), _jsx(Text, { children: " " }), check.issues.length === 0 ? (_jsx(Text, { color: "green", children: " No issues found." })) : (_jsxs(_Fragment, { children: [visibleSlice.map((iss, i) => {
|
|
276
|
+
const idx = scrollStart + i;
|
|
277
|
+
const sel = idx === cursor;
|
|
278
|
+
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(), " "] }), iss.file && _jsxs(Text, { color: "cyan", children: [String(iss.file).slice(0, 28).padEnd(28), " "] }), iss.line && _jsx(Text, { dimColor: true, children: String(iss.line).padEnd(5) }), _jsx(Text, { children: iss.message.slice(0, 55) }), iss.rule && _jsxs(Text, { dimColor: true, children: [" (", iss.rule, ")"] })] }, idx));
|
|
279
|
+
}), check.issues.length > scrollStart + visibleIssues && _jsxs(Text, { dimColor: true, children: [" +", check.issues.length - scrollStart - visibleIssues, " more"] })] }))] }));
|
|
280
|
+
}
|
|
227
281
|
function MonitorApp({ cwd }) {
|
|
228
282
|
const { exit } = useApp();
|
|
229
283
|
const { stdout } = useStdout();
|
|
230
284
|
const rows = stdout?.rows ?? 30;
|
|
231
285
|
const cached = loadCachedScan(cwd);
|
|
232
286
|
const [monCfg, setMonCfg] = useState(() => loadMonitorConfig(cwd));
|
|
233
|
-
const [mode, setMode] = useState("
|
|
287
|
+
const [mode, setMode] = useState({ view: "dashboard" });
|
|
288
|
+
const [panel, setPanel] = useState("checks");
|
|
289
|
+
const [cursor, setCursor] = useState(0);
|
|
234
290
|
const [state, setState] = useState(cached ?? {
|
|
235
291
|
checks: [], score: 0, grade: "?", duration: 0,
|
|
236
292
|
totalIssues: 0, scanning: true, scanCount: 0, scores: [],
|
|
@@ -238,6 +294,7 @@ function MonitorApp({ cwd }) {
|
|
|
238
294
|
const [log, setLog] = useState([
|
|
239
295
|
{ time: ts(), text: cached ? `Loaded cached scan: ${cached.grade} ${cached.score}/100` : `Monitoring ${basename(cwd)}...`, type: cached ? "scan" : "info" },
|
|
240
296
|
]);
|
|
297
|
+
const [copied, setCopied] = useState(false);
|
|
241
298
|
const scanningRef = useRef(false);
|
|
242
299
|
const prevScoreRef = useRef(cached ? cached.score : null);
|
|
243
300
|
const addLog = useCallback((text, type = "info") => {
|
|
@@ -245,6 +302,15 @@ function MonitorApp({ cwd }) {
|
|
|
245
302
|
}, []);
|
|
246
303
|
const workspace = detectWorkspace(cwd);
|
|
247
304
|
const stack = detectStack(cwd, workspace);
|
|
305
|
+
// Derived lists for navigation
|
|
306
|
+
const activeChecks = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
|
|
307
|
+
const allIssues = state.checks
|
|
308
|
+
.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
|
|
309
|
+
.sort((a, b) => {
|
|
310
|
+
const o = { error: 0, warning: 1, info: 2 };
|
|
311
|
+
return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
|
|
312
|
+
});
|
|
313
|
+
const currentList = panel === "checks" ? activeChecks : allIssues;
|
|
248
314
|
const doScan = useCallback(async () => {
|
|
249
315
|
if (scanningRef.current)
|
|
250
316
|
return;
|
|
@@ -282,7 +348,6 @@ function MonitorApp({ cwd }) {
|
|
|
282
348
|
prevScoreRef.current = result.score;
|
|
283
349
|
scanningRef.current = false;
|
|
284
350
|
}, [cwd, monCfg, addLog]);
|
|
285
|
-
// Initial scan
|
|
286
351
|
useEffect(() => { doScan(); }, [doScan]);
|
|
287
352
|
// File watcher
|
|
288
353
|
useEffect(() => {
|
|
@@ -305,33 +370,104 @@ function MonitorApp({ cwd }) {
|
|
|
305
370
|
return () => { for (const w of watchers)
|
|
306
371
|
w.close(); };
|
|
307
372
|
}, [cwd, workspace, doScan, addLog, monCfg.debounceMs]);
|
|
308
|
-
// Keyboard —
|
|
373
|
+
// ── Keyboard — unified navigation ──
|
|
309
374
|
useInput((input, key) => {
|
|
375
|
+
// Quit from anywhere
|
|
376
|
+
if (input === "q" || (key.ctrl && input === "c")) {
|
|
377
|
+
exit();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Esc: drill up or quit
|
|
310
381
|
if (key.escape) {
|
|
311
|
-
if (mode
|
|
312
|
-
setMode("
|
|
382
|
+
if (mode.view !== "dashboard") {
|
|
383
|
+
setMode({ view: "dashboard" });
|
|
384
|
+
setCursor(0);
|
|
313
385
|
return;
|
|
314
386
|
}
|
|
315
387
|
exit();
|
|
316
388
|
return;
|
|
317
389
|
}
|
|
318
|
-
|
|
319
|
-
|
|
390
|
+
// Global shortcuts
|
|
391
|
+
if (input === "r" && mode.view === "dashboard") {
|
|
392
|
+
doScan();
|
|
320
393
|
return;
|
|
321
394
|
}
|
|
322
|
-
|
|
395
|
+
// View switching
|
|
396
|
+
if (input === "t") {
|
|
397
|
+
setMode({ view: "trends" });
|
|
398
|
+
setCursor(0);
|
|
323
399
|
return;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
400
|
+
}
|
|
401
|
+
if (input === "c" && mode.view !== "config") {
|
|
402
|
+
setMode({ view: "config" });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// Dashboard navigation
|
|
406
|
+
if (mode.view === "dashboard") {
|
|
407
|
+
// Tab switches focused panel
|
|
408
|
+
if (key.tab) {
|
|
409
|
+
setPanel((p) => p === "checks" ? "issues" : "checks");
|
|
410
|
+
setCursor(0);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// ↑↓ navigate within panel
|
|
414
|
+
if (key.upArrow)
|
|
415
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
416
|
+
if (key.downArrow)
|
|
417
|
+
setCursor((c) => Math.min(currentList.length - 1, c + 1));
|
|
418
|
+
// Enter: drill into selected check
|
|
419
|
+
if (key.return && panel === "checks" && activeChecks[cursor]) {
|
|
420
|
+
setMode({ view: "check-detail", checkName: activeChecks[cursor].name });
|
|
421
|
+
setCursor(0);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Enter on issue: drill into that check
|
|
425
|
+
if (key.return && panel === "issues" && allIssues[cursor]) {
|
|
426
|
+
setMode({ view: "check-detail", checkName: allIssues[cursor].check });
|
|
427
|
+
setCursor(0);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Check detail: ↑↓ scroll, Enter copies fix prompt
|
|
432
|
+
if (mode.view === "check-detail") {
|
|
433
|
+
const check = state.checks.find((c) => c.name === mode.checkName);
|
|
434
|
+
if (check) {
|
|
435
|
+
if (key.upArrow)
|
|
436
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
437
|
+
if (key.downArrow)
|
|
438
|
+
setCursor((c) => Math.min(check.issues.length - 1, c + 1));
|
|
439
|
+
if (key.return && check.issues[cursor]) {
|
|
440
|
+
const prompt = buildFixPrompt(check.name, check.issues[cursor]);
|
|
441
|
+
if (copyToClipboard(prompt)) {
|
|
442
|
+
setCopied(true);
|
|
443
|
+
addLog(`Copied fix prompt for ${check.name}:${check.issues[cursor].file || ""}`, "info");
|
|
444
|
+
setTimeout(() => setCopied(false), 2000);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Trends: ↑↓ scroll
|
|
450
|
+
if (mode.view === "trends") {
|
|
451
|
+
if (key.upArrow)
|
|
452
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
453
|
+
if (key.downArrow)
|
|
454
|
+
setCursor((c) => c + 1);
|
|
455
|
+
}
|
|
328
456
|
});
|
|
329
457
|
const proj = basename(cwd);
|
|
330
458
|
const p = monCfg.panels;
|
|
331
|
-
|
|
332
|
-
|
|
459
|
+
// ── Render views ──
|
|
460
|
+
if (mode.view === "check-detail") {
|
|
461
|
+
const check = state.checks.find((c) => c.name === mode.checkName);
|
|
462
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { proj: proj, stack: stack, workspace: workspace, state: state }), check ? _jsx(CheckDetail, { check: check, height: rows - 3, cursor: cursor, copied: copied }) : _jsx(Text, { dimColor: true, children: " Check not found" }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Esc back \u00B7 \u2191\u2193 select \u00B7 Enter copy fix prompt \u00B7 q quit" }) })] }));
|
|
333
463
|
}
|
|
334
|
-
|
|
464
|
+
if (mode.view === "trends") {
|
|
465
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(TrendsScreen, { cwd: cwd, height: rows, onClose: () => setMode({ view: "dashboard" }) }) }));
|
|
466
|
+
}
|
|
467
|
+
if (mode.view === "config") {
|
|
468
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, justifyContent: "center", alignItems: "center", children: _jsx(ConfigScreen, { monCfg: monCfg, onSave: (cfg) => { setMonCfg(cfg); saveMonitorConfig(cwd, cfg); addLog("Settings saved", "info"); }, onClose: () => setMode({ view: "dashboard" }) }) }));
|
|
469
|
+
}
|
|
470
|
+
// ── Dashboard ──
|
|
335
471
|
const sidebarVisible = p.score || p.checks;
|
|
336
472
|
const mainVisible = p.activity || p.issues;
|
|
337
473
|
const bodyRows = rows - 4;
|
|
@@ -341,8 +477,16 @@ function MonitorApp({ cwd }) {
|
|
|
341
477
|
const issuesH = p.issues ? bodyRows - activityH : 0;
|
|
342
478
|
const errorCount = state.checks.reduce((s, c) => s + c.issues.filter((i) => i.severity === "error").length, 0);
|
|
343
479
|
const warnCount = state.checks.reduce((s, c) => s + c.issues.filter((i) => i.severity === "warning").length, 0);
|
|
344
|
-
|
|
345
|
-
|
|
480
|
+
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) => {
|
|
481
|
+
const sel = panel === "checks" && i === cursor;
|
|
482
|
+
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));
|
|
483
|
+
})] }))] })), 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" })] }), allIssues.slice(0, issuesH - 3).map((iss, i) => {
|
|
484
|
+
const sel = panel === "issues" && i === cursor;
|
|
485
|
+
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) })] }, i));
|
|
486
|
+
}), allIssues.length > issuesH - 3 && _jsxs(Text, { dimColor: true, children: [" +", allIssues.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 panel \u00B7 \u2191\u2193 select \u00B7 Enter drill \u00B7 Esc back \u00B7 r scan \u00B7 t trends \u00B7 c config \u00B7 q quit" }) })] }));
|
|
487
|
+
}
|
|
488
|
+
function Header({ proj, stack, workspace, state }) {
|
|
489
|
+
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" })] })] }));
|
|
346
490
|
}
|
|
347
491
|
// ── Entry ──
|
|
348
492
|
export async function startMonitor(cwd) {
|