@vibecodeqa/cli 0.35.8 → 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 +90 -80
  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
  }
@@ -312,11 +256,16 @@ function MonitorApp({ cwd }) {
312
256
  const { exit } = useApp();
313
257
  const { stdout } = useStdout();
314
258
  const rows = stdout?.rows ?? 30;
315
- 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]);
316
263
  const [monCfg, setMonCfg] = useState(() => loadMonitorConfig(cwd));
317
264
  const [mode, setMode] = useState({ view: "dashboard" });
318
265
  const [panel, setPanel] = useState("checks");
319
266
  const [cursor, setCursor] = useState(0);
267
+ const [configCursor, setConfigCursor] = useState(0);
268
+ const [pendingCfg, setPendingCfg] = useState(null);
320
269
  const [state, setState] = useState(cached ?? {
321
270
  checks: [], score: 0, grade: "?", duration: 0,
322
271
  totalIssues: 0, scanning: true, scanCount: 0, scores: [],
@@ -330,17 +279,19 @@ function MonitorApp({ cwd }) {
330
279
  const addLog = useCallback((text, type = "info") => {
331
280
  setLog((prev) => [...prev.slice(-50), { time: ts(), text, type }]);
332
281
  }, []);
333
- const workspace = detectWorkspace(cwd);
334
- const stack = detectStack(cwd, workspace);
335
- // Derived lists for navigation
336
- const activeChecks = state.checks.filter((c) => !c.details.skipped && !c.details.comingSoon);
337
- 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
338
285
  .flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })))
339
286
  .sort((a, b) => {
340
287
  const o = { error: 0, warning: 1, info: 2 };
341
288
  return (o[a.severity] ?? 2) - (o[b.severity] ?? 2);
342
- });
289
+ }), [state.checks]);
343
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]);
344
295
  const doScan = useCallback(async () => {
345
296
  if (scanningRef.current)
346
297
  return;
@@ -400,7 +351,20 @@ function MonitorApp({ cwd }) {
400
351
  return () => { for (const w of watchers)
401
352
  w.close(); };
402
353
  }, [cwd, workspace, doScan, addLog, monCfg.debounceMs]);
403
- // ── 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 ──
404
368
  useInput((input, key) => {
405
369
  // Quit from anywhere
406
370
  if (input === "q" || (key.ctrl && input === "c")) {
@@ -409,6 +373,9 @@ function MonitorApp({ cwd }) {
409
373
  }
410
374
  // Esc: drill up or quit
411
375
  if (key.escape) {
376
+ if (mode.view === "config") {
377
+ setPendingCfg(null);
378
+ }
412
379
  if (mode.view !== "dashboard") {
413
380
  setMode({ view: "dashboard" });
414
381
  setCursor(0);
@@ -417,48 +384,91 @@ function MonitorApp({ cwd }) {
417
384
  exit();
418
385
  return;
419
386
  }
420
- // Global shortcuts
421
- if (input === "r" && mode.view === "dashboard") {
387
+ // Rescan from dashboard or check-detail
388
+ if (input === "r" && (mode.view === "dashboard" || mode.view === "check-detail")) {
422
389
  doScan();
423
390
  return;
424
391
  }
425
- // 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)
426
438
  if (input === "t") {
427
439
  setMode({ view: "trends" });
428
440
  setCursor(0);
429
441
  return;
430
442
  }
431
- if (input === "c" && mode.view !== "config") {
443
+ if (input === "c") {
432
444
  setMode({ view: "config" });
445
+ setConfigCursor(0);
446
+ setPendingCfg({ ...monCfg, panels: { ...monCfg.panels } });
433
447
  return;
434
448
  }
435
- // Dashboard navigation
449
+ // ── Dashboard navigation ──
436
450
  if (mode.view === "dashboard") {
437
- // Tab switches focused panel
438
451
  if (key.tab) {
439
452
  setPanel((p) => p === "checks" ? "issues" : "checks");
440
453
  setCursor(0);
441
454
  return;
442
455
  }
443
- // ↑↓ navigate within panel
444
456
  if (key.upArrow)
445
457
  setCursor((c) => Math.max(0, c - 1));
446
458
  if (key.downArrow)
447
459
  setCursor((c) => Math.min(currentList.length - 1, c + 1));
448
- // Enter: drill into selected check
449
460
  if (key.return && panel === "checks" && activeChecks[cursor]) {
450
461
  setMode({ view: "check-detail", checkName: activeChecks[cursor].name });
451
462
  setCursor(0);
452
463
  return;
453
464
  }
454
- // Enter on issue: drill into that check
455
465
  if (key.return && panel === "issues" && allIssues[cursor]) {
456
466
  setMode({ view: "check-detail", checkName: allIssues[cursor].check });
457
467
  setCursor(0);
458
468
  return;
459
469
  }
460
470
  }
461
- // Check detail: ↑↓ scroll, Enter copies fix prompt
471
+ // ── Check detail: ↑↓ scroll, Enter copies fix prompt ──
462
472
  if (mode.view === "check-detail") {
463
473
  const check = state.checks.find((c) => c.name === mode.checkName);
464
474
  if (check) {
@@ -476,7 +486,7 @@ function MonitorApp({ cwd }) {
476
486
  }
477
487
  }
478
488
  }
479
- // Trends: ↑↓ scroll
489
+ // ── Trends: ↑↓ scroll ──
480
490
  if (mode.view === "trends") {
481
491
  if (key.upArrow)
482
492
  setCursor((c) => Math.max(0, c - 1));
@@ -492,10 +502,10 @@ function MonitorApp({ cwd }) {
492
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" }) })] }));
493
503
  }
494
504
  if (mode.view === "trends") {
495
- 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 }) }));
496
506
  }
497
507
  if (mode.view === "config") {
498
- 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 }) }));
499
509
  }
500
510
  // ── Dashboard ──
501
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.8",
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": {