@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.
Files changed (2) hide show
  1. package/dist/monitor.js +128 -88
  2. 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({ monCfg, onSave, onClose, }) {
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, onClose }) {
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 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;
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(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"] })] }))] }));
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
- const cached = loadCachedScan(cwd);
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
- const workspace = detectWorkspace(cwd);
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
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
- // ── Keyboard unified navigation ──
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
- // Global shortcuts
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
- // View switching
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" && mode.view !== "config") {
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, onClose: () => setMode({ view: "dashboard" }) }) }));
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, { monCfg: monCfg, onSave: (cfg) => { setMonCfg(cfg); saveMonitorConfig(cwd, cfg); addLog("Settings saved", "info"); }, onClose: () => setMode({ view: "dashboard" }) }) }));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.35.7",
3
+ "version": "0.35.9",
4
4
  "description": "Code health scanner for the AI coding era. 25 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {