@vibecodeqa/cli 0.35.7 → 0.35.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/monitor.js +128 -88
- package/package.json +1 -1
package/dist/monitor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
* Press 'c' to open settings: thresholds, panel toggles, scan options.
|
|
7
7
|
* Config persists to .vibe-check/monitor.json.
|
|
8
8
|
*/
|
|
9
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
9
|
+
import { useState, useEffect, useCallback, useRef, useMemo } 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";
|
|
@@ -129,59 +129,7 @@ function ActivityPanel({ log, height }) {
|
|
|
129
129
|
const visibleLines = Math.max(1, height - 3);
|
|
130
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)))] }));
|
|
131
131
|
}
|
|
132
|
-
function ConfigScreen({
|
|
133
|
-
const [cursor, setCursor] = useState(0);
|
|
134
|
-
const [cfg, setCfg] = useState(JSON.parse(JSON.stringify(monCfg)));
|
|
135
|
-
const options = [
|
|
136
|
-
{ key: "alertBelow", label: "Alert when score below", type: "number", value: cfg.alertBelow, path: ["alertBelow"] },
|
|
137
|
-
{ key: "alertDrop", label: "Alert on score drop ≥", type: "number", value: cfg.alertDrop, path: ["alertDrop"] },
|
|
138
|
-
{ key: "debounceMs", label: "Scan debounce (ms)", type: "number", value: cfg.debounceMs, path: ["debounceMs"] },
|
|
139
|
-
{ key: "skipTests", label: "Skip test execution", type: "toggle", value: cfg.skipTests, path: ["skipTests"] },
|
|
140
|
-
{ key: "p-score", label: "Panel: Score", type: "toggle", value: cfg.panels.score, path: ["panels", "score"] },
|
|
141
|
-
{ key: "p-checks", label: "Panel: Checks", type: "toggle", value: cfg.panels.checks, path: ["panels", "checks"] },
|
|
142
|
-
{ key: "p-activity", label: "Panel: Activity", type: "toggle", value: cfg.panels.activity, path: ["panels", "activity"] },
|
|
143
|
-
{ key: "p-issues", label: "Panel: Issues", type: "toggle", value: cfg.panels.issues, path: ["panels", "issues"] },
|
|
144
|
-
];
|
|
145
|
-
useInput((input, key) => {
|
|
146
|
-
if (key.escape || input === "c") {
|
|
147
|
-
onClose();
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
if (key.upArrow)
|
|
151
|
-
setCursor((c) => Math.max(0, c - 1));
|
|
152
|
-
if (key.downArrow)
|
|
153
|
-
setCursor((c) => Math.min(options.length - 1, c + 1));
|
|
154
|
-
const opt = options[cursor];
|
|
155
|
-
if (!opt)
|
|
156
|
-
return;
|
|
157
|
-
if (opt.type === "toggle" && (input === " " || key.return)) {
|
|
158
|
-
const next = { ...cfg };
|
|
159
|
-
if (opt.path.length === 1) {
|
|
160
|
-
next[opt.path[0]] = !opt.value;
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
next.panels[opt.path[1]] = !opt.value;
|
|
164
|
-
}
|
|
165
|
-
setCfg(next);
|
|
166
|
-
}
|
|
167
|
-
if (opt.type === "number") {
|
|
168
|
-
const step = key.shift ? 10 : 1;
|
|
169
|
-
let v = opt.value;
|
|
170
|
-
if (key.rightArrow)
|
|
171
|
-
v += step;
|
|
172
|
-
if (key.leftArrow)
|
|
173
|
-
v = Math.max(0, v - step);
|
|
174
|
-
if (v !== opt.value) {
|
|
175
|
-
const next = { ...cfg };
|
|
176
|
-
next[opt.path[0]] = v;
|
|
177
|
-
setCfg(next);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (input === "s") {
|
|
181
|
-
onSave(cfg);
|
|
182
|
-
onClose();
|
|
183
|
-
}
|
|
184
|
-
});
|
|
132
|
+
function ConfigScreen({ cursor, options }) {
|
|
185
133
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "magenta", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: " \u2699 Settings" }), _jsx(Text, { dimColor: true, children: " " }), options.map((opt, i) => {
|
|
186
134
|
const selected = i === cursor;
|
|
187
135
|
const prefix = selected ? "▸ " : " ";
|
|
@@ -208,13 +156,9 @@ function sparkFull(values, width) {
|
|
|
208
156
|
}
|
|
209
157
|
return sampled.map((v) => bars[Math.round(((v - min) / range) * 8)]).join("");
|
|
210
158
|
}
|
|
211
|
-
function TrendsScreen({ cwd, height
|
|
159
|
+
function TrendsScreen({ cwd, height }) {
|
|
212
160
|
const historyDir = join(cwd, ".vibe-check", "history");
|
|
213
161
|
const history = loadHistory(historyDir);
|
|
214
|
-
useInput((input, key) => {
|
|
215
|
-
if (key.escape || input === "t")
|
|
216
|
-
onClose();
|
|
217
|
-
});
|
|
218
162
|
if (history.length < 2) {
|
|
219
163
|
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
164
|
}
|
|
@@ -268,25 +212,60 @@ function loadCachedScan(cwd) {
|
|
|
268
212
|
}
|
|
269
213
|
// ── Check Detail View ──
|
|
270
214
|
function CheckDetail({ check, height, cursor, copied }) {
|
|
271
|
-
const
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
215
|
+
const bodyHeight = height - 5; // header + score + blank + footer margin
|
|
216
|
+
// Each issue takes 2-3 lines: header line + message (wraps if long)
|
|
217
|
+
// Estimate lines per issue for scroll calculation
|
|
218
|
+
const issueHeights = check.issues.map((iss) => {
|
|
219
|
+
const msgLen = iss.message.length;
|
|
220
|
+
return msgLen > 80 ? 3 : 2; // 2 lines base, 3 if message wraps
|
|
221
|
+
});
|
|
222
|
+
// Find scroll window that keeps cursor visible
|
|
223
|
+
let scrollStart = 0;
|
|
224
|
+
let linesUsed = 0;
|
|
225
|
+
// First, find how many items fit
|
|
226
|
+
const fits = [];
|
|
227
|
+
for (let i = 0; i < check.issues.length; i++) {
|
|
228
|
+
if (linesUsed + issueHeights[i] > bodyHeight)
|
|
229
|
+
break;
|
|
230
|
+
fits.push(i);
|
|
231
|
+
linesUsed += issueHeights[i];
|
|
232
|
+
}
|
|
233
|
+
const maxVisible = fits.length || 1;
|
|
234
|
+
// Adjust scroll so cursor is visible
|
|
235
|
+
if (cursor >= scrollStart + maxVisible)
|
|
236
|
+
scrollStart = cursor - maxVisible + 1;
|
|
237
|
+
if (cursor < scrollStart)
|
|
238
|
+
scrollStart = cursor;
|
|
239
|
+
scrollStart = Math.max(0, Math.min(scrollStart, check.issues.length - maxVisible));
|
|
240
|
+
// Collect visible items within height budget
|
|
241
|
+
const visible = [];
|
|
242
|
+
let usedLines = 0;
|
|
243
|
+
for (let i = scrollStart; i < check.issues.length; i++) {
|
|
244
|
+
if (usedLines + issueHeights[i] > bodyHeight)
|
|
245
|
+
break;
|
|
246
|
+
visible.push({ issue: check.issues[i], idx: i });
|
|
247
|
+
usedLines += issueHeights[i];
|
|
248
|
+
}
|
|
249
|
+
const remaining = check.issues.length - (scrollStart + visible.length);
|
|
250
|
+
return (_jsxs(Box, { flexDirection: "column", height: height, paddingX: 1, overflowY: "hidden", 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: [visible.map(({ issue: iss, idx }) => {
|
|
277
251
|
const sel = idx === cursor;
|
|
278
|
-
return (_jsxs(
|
|
279
|
-
}),
|
|
252
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Text, { 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), iss.line ? `:${iss.line}` : "", " "] }), iss.rule && _jsxs(Text, { dimColor: true, children: ["(", iss.rule, ")"] })] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: sel ? "white" : "gray", children: " " }), _jsx(Text, { color: sel ? "white" : undefined, children: iss.message })] })] }, idx));
|
|
253
|
+
}), remaining > 0 && _jsxs(Text, { dimColor: true, children: [" +", remaining, " more (\u2193 to scroll)"] })] }))] }));
|
|
280
254
|
}
|
|
281
255
|
function MonitorApp({ cwd }) {
|
|
282
256
|
const { exit } = useApp();
|
|
283
257
|
const { stdout } = useStdout();
|
|
284
258
|
const rows = stdout?.rows ?? 30;
|
|
285
|
-
|
|
259
|
+
// Memoize filesystem I/O — only run once
|
|
260
|
+
const cached = useMemo(() => loadCachedScan(cwd), [cwd]);
|
|
261
|
+
const workspace = useMemo(() => detectWorkspace(cwd), [cwd]);
|
|
262
|
+
const stack = useMemo(() => detectStack(cwd, workspace), [cwd, workspace]);
|
|
286
263
|
const [monCfg, setMonCfg] = useState(() => loadMonitorConfig(cwd));
|
|
287
264
|
const [mode, setMode] = useState({ view: "dashboard" });
|
|
288
265
|
const [panel, setPanel] = useState("checks");
|
|
289
266
|
const [cursor, setCursor] = useState(0);
|
|
267
|
+
const [configCursor, setConfigCursor] = useState(0);
|
|
268
|
+
const [pendingCfg, setPendingCfg] = useState(null);
|
|
290
269
|
const [state, setState] = useState(cached ?? {
|
|
291
270
|
checks: [], score: 0, grade: "?", duration: 0,
|
|
292
271
|
totalIssues: 0, scanning: true, scanCount: 0, scores: [],
|
|
@@ -300,17 +279,19 @@ function MonitorApp({ cwd }) {
|
|
|
300
279
|
const addLog = useCallback((text, type = "info") => {
|
|
301
280
|
setLog((prev) => [...prev.slice(-50), { time: ts(), text, type }]);
|
|
302
281
|
}, []);
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
const activeChecks = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
|
|
307
|
-
const allIssues = state.checks
|
|
282
|
+
// Memoize derived lists
|
|
283
|
+
const activeChecks = useMemo(() => state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon), [state.checks]);
|
|
284
|
+
const allIssues = useMemo(() => state.checks
|
|
308
285
|
.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
|
|
309
286
|
.sort((a, b) => {
|
|
310
287
|
const o = { error: 0, warning: 1, info: 2 };
|
|
311
288
|
return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
|
|
312
|
-
});
|
|
289
|
+
}), [state.checks]);
|
|
313
290
|
const currentList = panel === "checks" ? activeChecks : allIssues;
|
|
291
|
+
// Clamp cursor when data changes
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
setCursor((c) => Math.min(c, Math.max(0, currentList.length - 1)));
|
|
294
|
+
}, [currentList.length]);
|
|
314
295
|
const doScan = useCallback(async () => {
|
|
315
296
|
if (scanningRef.current)
|
|
316
297
|
return;
|
|
@@ -370,7 +351,20 @@ function MonitorApp({ cwd }) {
|
|
|
370
351
|
return () => { for (const w of watchers)
|
|
371
352
|
w.close(); };
|
|
372
353
|
}, [cwd, workspace, doScan, addLog, monCfg.debounceMs]);
|
|
373
|
-
|
|
354
|
+
const configOptions = useMemo(() => {
|
|
355
|
+
const cfg = pendingCfg ?? monCfg;
|
|
356
|
+
return [
|
|
357
|
+
{ key: "alertBelow", label: "Alert when score below", type: "number", value: cfg.alertBelow, path: ["alertBelow"] },
|
|
358
|
+
{ key: "alertDrop", label: "Alert on score drop ≥", type: "number", value: cfg.alertDrop, path: ["alertDrop"] },
|
|
359
|
+
{ key: "debounceMs", label: "Scan debounce (ms)", type: "number", value: cfg.debounceMs, path: ["debounceMs"] },
|
|
360
|
+
{ key: "skipTests", label: "Skip test execution", type: "toggle", value: cfg.skipTests, path: ["skipTests"] },
|
|
361
|
+
{ key: "p-score", label: "Panel: Score", type: "toggle", value: cfg.panels.score, path: ["panels", "score"] },
|
|
362
|
+
{ key: "p-checks", label: "Panel: Checks", type: "toggle", value: cfg.panels.checks, path: ["panels", "checks"] },
|
|
363
|
+
{ key: "p-activity", label: "Panel: Activity", type: "toggle", value: cfg.panels.activity, path: ["panels", "activity"] },
|
|
364
|
+
{ key: "p-issues", label: "Panel: Issues", type: "toggle", value: cfg.panels.issues, path: ["panels", "issues"] },
|
|
365
|
+
];
|
|
366
|
+
}, [pendingCfg, monCfg]);
|
|
367
|
+
// ── Keyboard — single handler for all views ──
|
|
374
368
|
useInput((input, key) => {
|
|
375
369
|
// Quit from anywhere
|
|
376
370
|
if (input === "q" || (key.ctrl && input === "c")) {
|
|
@@ -379,6 +373,9 @@ function MonitorApp({ cwd }) {
|
|
|
379
373
|
}
|
|
380
374
|
// Esc: drill up or quit
|
|
381
375
|
if (key.escape) {
|
|
376
|
+
if (mode.view === "config") {
|
|
377
|
+
setPendingCfg(null);
|
|
378
|
+
}
|
|
382
379
|
if (mode.view !== "dashboard") {
|
|
383
380
|
setMode({ view: "dashboard" });
|
|
384
381
|
setCursor(0);
|
|
@@ -387,48 +384,91 @@ function MonitorApp({ cwd }) {
|
|
|
387
384
|
exit();
|
|
388
385
|
return;
|
|
389
386
|
}
|
|
390
|
-
//
|
|
391
|
-
if (input === "r" && mode.view === "dashboard") {
|
|
387
|
+
// Rescan from dashboard or check-detail
|
|
388
|
+
if (input === "r" && (mode.view === "dashboard" || mode.view === "check-detail")) {
|
|
392
389
|
doScan();
|
|
393
390
|
return;
|
|
394
391
|
}
|
|
395
|
-
//
|
|
392
|
+
// ── Config view ──
|
|
393
|
+
if (mode.view === "config") {
|
|
394
|
+
const cfg = pendingCfg ?? { ...monCfg };
|
|
395
|
+
if (key.upArrow) {
|
|
396
|
+
setConfigCursor((c) => Math.max(0, c - 1));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (key.downArrow) {
|
|
400
|
+
setConfigCursor((c) => Math.min(configOptions.length - 1, c + 1));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const opt = configOptions[configCursor];
|
|
404
|
+
if (!opt)
|
|
405
|
+
return;
|
|
406
|
+
if (opt.type === "toggle" && (input === " " || key.return)) {
|
|
407
|
+
const next = { ...cfg, panels: { ...cfg.panels } };
|
|
408
|
+
if (opt.path.length === 1)
|
|
409
|
+
next[opt.path[0]] = !opt.value;
|
|
410
|
+
else
|
|
411
|
+
next.panels[opt.path[1]] = !opt.value;
|
|
412
|
+
setPendingCfg(next);
|
|
413
|
+
}
|
|
414
|
+
if (opt.type === "number") {
|
|
415
|
+
const step = key.shift ? 10 : 1;
|
|
416
|
+
let v = opt.value;
|
|
417
|
+
if (key.rightArrow)
|
|
418
|
+
v += step;
|
|
419
|
+
if (key.leftArrow)
|
|
420
|
+
v = Math.max(0, v - step);
|
|
421
|
+
if (v !== opt.value) {
|
|
422
|
+
const next = { ...cfg, panels: { ...cfg.panels } };
|
|
423
|
+
next[opt.path[0]] = v;
|
|
424
|
+
setPendingCfg(next);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (input === "s" && pendingCfg) {
|
|
428
|
+
setMonCfg(pendingCfg);
|
|
429
|
+
saveMonitorConfig(cwd, pendingCfg);
|
|
430
|
+
addLog("Settings saved", "info");
|
|
431
|
+
setPendingCfg(null);
|
|
432
|
+
setMode({ view: "dashboard" });
|
|
433
|
+
setCursor(0);
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// View switching (not from config — handled above)
|
|
396
438
|
if (input === "t") {
|
|
397
439
|
setMode({ view: "trends" });
|
|
398
440
|
setCursor(0);
|
|
399
441
|
return;
|
|
400
442
|
}
|
|
401
|
-
if (input === "c"
|
|
443
|
+
if (input === "c") {
|
|
402
444
|
setMode({ view: "config" });
|
|
445
|
+
setConfigCursor(0);
|
|
446
|
+
setPendingCfg({ ...monCfg, panels: { ...monCfg.panels } });
|
|
403
447
|
return;
|
|
404
448
|
}
|
|
405
|
-
// Dashboard navigation
|
|
449
|
+
// ── Dashboard navigation ──
|
|
406
450
|
if (mode.view === "dashboard") {
|
|
407
|
-
// Tab switches focused panel
|
|
408
451
|
if (key.tab) {
|
|
409
452
|
setPanel((p) => p === "checks" ? "issues" : "checks");
|
|
410
453
|
setCursor(0);
|
|
411
454
|
return;
|
|
412
455
|
}
|
|
413
|
-
// ↑↓ navigate within panel
|
|
414
456
|
if (key.upArrow)
|
|
415
457
|
setCursor((c) => Math.max(0, c - 1));
|
|
416
458
|
if (key.downArrow)
|
|
417
459
|
setCursor((c) => Math.min(currentList.length - 1, c + 1));
|
|
418
|
-
// Enter: drill into selected check
|
|
419
460
|
if (key.return && panel === "checks" && activeChecks[cursor]) {
|
|
420
461
|
setMode({ view: "check-detail", checkName: activeChecks[cursor].name });
|
|
421
462
|
setCursor(0);
|
|
422
463
|
return;
|
|
423
464
|
}
|
|
424
|
-
// Enter on issue: drill into that check
|
|
425
465
|
if (key.return && panel === "issues" && allIssues[cursor]) {
|
|
426
466
|
setMode({ view: "check-detail", checkName: allIssues[cursor].check });
|
|
427
467
|
setCursor(0);
|
|
428
468
|
return;
|
|
429
469
|
}
|
|
430
470
|
}
|
|
431
|
-
// Check detail: ↑↓ scroll, Enter copies fix prompt
|
|
471
|
+
// ── Check detail: ↑↓ scroll, Enter copies fix prompt ──
|
|
432
472
|
if (mode.view === "check-detail") {
|
|
433
473
|
const check = state.checks.find((c) => c.name === mode.checkName);
|
|
434
474
|
if (check) {
|
|
@@ -446,7 +486,7 @@ function MonitorApp({ cwd }) {
|
|
|
446
486
|
}
|
|
447
487
|
}
|
|
448
488
|
}
|
|
449
|
-
// Trends: ↑↓ scroll
|
|
489
|
+
// ── Trends: ↑↓ scroll ──
|
|
450
490
|
if (mode.view === "trends") {
|
|
451
491
|
if (key.upArrow)
|
|
452
492
|
setCursor((c) => Math.max(0, c - 1));
|
|
@@ -462,10 +502,10 @@ function MonitorApp({ cwd }) {
|
|
|
462
502
|
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" }) })] }));
|
|
463
503
|
}
|
|
464
504
|
if (mode.view === "trends") {
|
|
465
|
-
return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(TrendsScreen, { cwd: cwd, height: rows
|
|
505
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, children: _jsx(TrendsScreen, { cwd: cwd, height: rows }) }));
|
|
466
506
|
}
|
|
467
507
|
if (mode.view === "config") {
|
|
468
|
-
return (_jsx(Box, { flexDirection: "column", height: rows, justifyContent: "center", alignItems: "center", children: _jsx(ConfigScreen, {
|
|
508
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, justifyContent: "center", alignItems: "center", children: _jsx(ConfigScreen, { cursor: configCursor, options: configOptions }) }));
|
|
469
509
|
}
|
|
470
510
|
// ── Dashboard ──
|
|
471
511
|
const sidebarVisible = p.score || p.checks;
|