sickbay 1.16.0 → 1.16.2

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 (63) hide show
  1. package/README.md +1 -1
  2. package/dist/DiffApp-DeNEtlY7.js +181 -0
  3. package/dist/DiffApp-DeNEtlY7.js.map +1 -0
  4. package/dist/DoctorApp-CaiqnhHH.js +415 -0
  5. package/dist/DoctorApp-CaiqnhHH.js.map +1 -0
  6. package/dist/FixApp-MWXohP60.js +337 -0
  7. package/dist/FixApp-MWXohP60.js.map +1 -0
  8. package/dist/StatsApp-BC4P4hII.js +372 -0
  9. package/dist/StatsApp-BC4P4hII.js.map +1 -0
  10. package/dist/TrendApp-DOBt7ITA.js +127 -0
  11. package/dist/TrendApp-DOBt7ITA.js.map +1 -0
  12. package/dist/TuiApp-D4Wn81Qv.js +1042 -0
  13. package/dist/TuiApp-D4Wn81Qv.js.map +1 -0
  14. package/dist/ai-DxA5r3hA.js +69 -0
  15. package/dist/ai-DxA5r3hA.js.map +1 -0
  16. package/dist/badge-DB3y0fWt.js +37 -0
  17. package/dist/badge-DB3y0fWt.js.map +1 -0
  18. package/dist/{claude-SN6IVGBI.js → claude-C5dUtWBJ.js} +13 -12
  19. package/dist/claude-C5dUtWBJ.js.map +1 -0
  20. package/dist/config.d.ts +27 -1
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +2 -6
  23. package/dist/dist-Bjhxbcak.js +5431 -0
  24. package/dist/dist-Bjhxbcak.js.map +1 -0
  25. package/dist/history-DVegAwFp.js +83 -0
  26. package/dist/history-DVegAwFp.js.map +1 -0
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +692 -564
  29. package/dist/index.js.map +1 -0
  30. package/dist/init-Dn7eDMEp.js +199 -0
  31. package/dist/init-Dn7eDMEp.js.map +1 -0
  32. package/dist/resolve-package-DS27JBhN.js +60 -0
  33. package/dist/resolve-package-DS27JBhN.js.map +1 -0
  34. package/dist/rolldown-runtime-CiIaOW0V.js +13 -0
  35. package/dist/trend-D-Hufthb.js +45 -0
  36. package/dist/trend-D-Hufthb.js.map +1 -0
  37. package/dist/update-check-C7bNNfvf.js +58 -0
  38. package/dist/update-check-C7bNNfvf.js.map +1 -0
  39. package/dist/web-D5etye5Q.js +259 -0
  40. package/dist/web-D5etye5Q.js.map +1 -0
  41. package/package.json +6 -5
  42. package/dist/DiffApp-ASQWWDLS.js +0 -172
  43. package/dist/DoctorApp-SMIZDD64.js +0 -440
  44. package/dist/FixApp-63PU5VF7.js +0 -351
  45. package/dist/StatsApp-CFZBEITZ.js +0 -356
  46. package/dist/TrendApp-BKGRSP6Q.js +0 -106
  47. package/dist/TuiApp-K7KP35RC.js +0 -980
  48. package/dist/ai-7DGOLNJX.js +0 -64
  49. package/dist/badge-KQ73KEIN.js +0 -41
  50. package/dist/chunk-352RAVRZ.js +0 -19
  51. package/dist/chunk-3OR2GFVE.js +0 -90
  52. package/dist/chunk-DJA6K3MN.js +0 -5564
  53. package/dist/chunk-LZOEMCE7.js +0 -62
  54. package/dist/chunk-MBVA75EM.js +0 -19
  55. package/dist/chunk-SHO3ZXTH.js +0 -23
  56. package/dist/chunk-TWCTPAXQ.js +0 -73
  57. package/dist/chunk-VBM6NZAY.js +0 -60
  58. package/dist/dist-LQY3FYCP.js +0 -64
  59. package/dist/history-XLNVZEDI.js +0 -14
  60. package/dist/init-DD2HOQE2.js +0 -217
  61. package/dist/resolve-package-774YTW5Q.js +0 -9
  62. package/dist/update-check-M7IFOMLJ.js +0 -12
  63. package/dist/web-2KWCYTW4.js +0 -269
@@ -0,0 +1,1042 @@
1
+ import { d as runSickbayMonorepo, i as detectMonorepo, t as buildSummary, u as runSickbay } from "./dist-Bjhxbcak.js";
2
+ import { a as countUniqueIssues, i as LOADING_MESSAGES, t as UpdateNotice } from "./index.js";
3
+ import { r as loadHistory } from "./history-DVegAwFp.js";
4
+ import { n as trendArrow, t as sparkline } from "./trend-D-Hufthb.js";
5
+ import { checkForUpdate } from "./update-check-C7bNNfvf.js";
6
+ import React, { useCallback, useEffect, useRef, useState } from "react";
7
+ import { Box, Text, useInput } from "ink";
8
+ import { execFile } from "child_process";
9
+ import { promisify } from "util";
10
+ //#region src/components/tui/ActivityPanel.tsx
11
+ const TYPE_COLOR = {
12
+ "scan-start": "cyan",
13
+ "scan-complete": "green",
14
+ "file-change": "yellow",
15
+ "git-change": "magenta",
16
+ regression: "red",
17
+ info: "gray"
18
+ };
19
+ function formatTime(date) {
20
+ return date.toLocaleTimeString("en-US", { hour12: false });
21
+ }
22
+ function ActivityPanel({ entries, availableHeight }) {
23
+ if (entries.length === 0) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "No activity yet."));
24
+ const visible = entries.slice(-Math.max(1, availableHeight));
25
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, visible.map((entry) => /* @__PURE__ */ React.createElement(Box, { key: `${entry.timestamp.getTime()}-${entry.type}` }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, formatTime(entry.timestamp), " "), /* @__PURE__ */ React.createElement(Text, { color: TYPE_COLOR[entry.type] || "white" }, entry.message))));
26
+ }
27
+ //#endregion
28
+ //#region src/components/tui/hooks/useGitStatus.ts
29
+ const exec = promisify(execFile);
30
+ async function git(args, cwd) {
31
+ try {
32
+ const { stdout } = await exec("git", args, {
33
+ cwd,
34
+ timeout: 5e3
35
+ });
36
+ return stdout.trim();
37
+ } catch {
38
+ return "";
39
+ }
40
+ }
41
+ async function fetchGitStatus(projectPath) {
42
+ const [branch, porcelain, revList, stashList, logLine] = await Promise.all([
43
+ git(["branch", "--show-current"], projectPath),
44
+ git(["status", "--porcelain"], projectPath),
45
+ git([
46
+ "rev-list",
47
+ "--left-right",
48
+ "--count",
49
+ "HEAD...@{upstream}"
50
+ ], projectPath),
51
+ git(["stash", "list"], projectPath),
52
+ git([
53
+ "log",
54
+ "-1",
55
+ "--format=%s|%cr"
56
+ ], projectPath)
57
+ ]);
58
+ const lines = porcelain ? porcelain.split("\n") : [];
59
+ const staged = lines.filter((l) => /^[MADRC]/.test(l)).length;
60
+ const modified = lines.filter((l) => /^.[MD]/.test(l)).length;
61
+ const untracked = lines.filter((l) => l.startsWith("??")).length;
62
+ let ahead = 0;
63
+ let behind = 0;
64
+ if (revList) {
65
+ const parts = revList.split(/\s+/);
66
+ ahead = parseInt(parts[0], 10) || 0;
67
+ behind = parseInt(parts[1], 10) || 0;
68
+ }
69
+ const stashes = stashList ? stashList.split("\n").filter(Boolean).length : 0;
70
+ const [lastCommit, lastCommitTime] = logLine.split("|");
71
+ return {
72
+ branch: branch || "unknown",
73
+ modified,
74
+ staged,
75
+ untracked,
76
+ ahead,
77
+ behind,
78
+ stashes,
79
+ lastCommit: lastCommit || "",
80
+ lastCommitTime: lastCommitTime || ""
81
+ };
82
+ }
83
+ function useGitStatus(projectPath, pollInterval = 1e4) {
84
+ const [status, setStatus] = useState(null);
85
+ const intervalRef = useRef(void 0);
86
+ useEffect(() => {
87
+ let mounted = true;
88
+ fetchGitStatus(projectPath).then((s) => {
89
+ if (mounted) setStatus(s);
90
+ });
91
+ intervalRef.current = setInterval(() => {
92
+ fetchGitStatus(projectPath).then((s) => {
93
+ if (mounted) setStatus(s);
94
+ });
95
+ }, pollInterval);
96
+ return () => {
97
+ mounted = false;
98
+ if (intervalRef.current) clearInterval(intervalRef.current);
99
+ };
100
+ }, [projectPath, pollInterval]);
101
+ return status;
102
+ }
103
+ //#endregion
104
+ //#region src/components/tui/GitPanel.tsx
105
+ function GitPanel({ projectPath, availableWidth = 30 }) {
106
+ const status = useGitStatus(projectPath);
107
+ if (!status) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Loading git info..."));
108
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Branch: "), /* @__PURE__ */ React.createElement(Text, {
109
+ bold: true,
110
+ color: "green"
111
+ }, status.branch)), (status.ahead > 0 || status.behind > 0) && /* @__PURE__ */ React.createElement(Box, null, status.ahead > 0 && /* @__PURE__ */ React.createElement(Text, { color: "green" }, "↑", status.ahead, " "), status.behind > 0 && /* @__PURE__ */ React.createElement(Text, { color: "red" }, "↓", status.behind)), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Modified: "), /* @__PURE__ */ React.createElement(Text, { color: status.modified > 0 ? "yellow" : "green" }, status.modified)), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Staged: "), /* @__PURE__ */ React.createElement(Text, { color: status.staged > 0 ? "cyan" : "green" }, status.staged)), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Untracked: "), /* @__PURE__ */ React.createElement(Text, null, status.untracked)), status.stashes > 0 && /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Stashes: "), /* @__PURE__ */ React.createElement(Text, null, status.stashes)), status.lastCommit && /* @__PURE__ */ React.createElement(Box, {
112
+ marginTop: 1,
113
+ flexDirection: "column"
114
+ }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Last commit:"), /* @__PURE__ */ React.createElement(Text, null, status.lastCommit.length > availableWidth ? status.lastCommit.slice(0, availableWidth - 3) + "..." : status.lastCommit), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, status.lastCommitTime)));
115
+ }
116
+ //#endregion
117
+ //#region src/components/tui/HealthPanel.tsx
118
+ function scoreColor$2(score) {
119
+ if (score >= 80) return "green";
120
+ if (score >= 60) return "yellow";
121
+ return "red";
122
+ }
123
+ function scoreBar$2(score, width = 10) {
124
+ const filled = Math.round(score / 100 * width);
125
+ return "█".repeat(filled) + "░".repeat(width - filled);
126
+ }
127
+ function truncate(str, maxLen) {
128
+ return str.length > maxLen ? str.slice(0, maxLen - 1) + "…" : str;
129
+ }
130
+ function statusIcon(status) {
131
+ switch (status) {
132
+ case "pass": return "✓";
133
+ case "fail": return "✗";
134
+ case "warning": return "⚠";
135
+ case "skipped": return "○";
136
+ default: return "?";
137
+ }
138
+ }
139
+ function HealthPanel({ checks, isScanning, progress, scrollOffset, availableHeight }) {
140
+ const [msgIdx, setMsgIdx] = useState(0);
141
+ useEffect(() => {
142
+ if (!isScanning || progress.length > 0) return;
143
+ const id = setInterval(() => {
144
+ setMsgIdx((i) => (i + 1) % LOADING_MESSAGES.length);
145
+ }, 4e3);
146
+ return () => clearInterval(id);
147
+ }, [isScanning, progress.length]);
148
+ if (isScanning && checks.length === 0) {
149
+ if (progress.length === 0) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, LOADING_MESSAGES[msgIdx]));
150
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, progress.map((p) => /* @__PURE__ */ React.createElement(Box, { key: p.name }, /* @__PURE__ */ React.createElement(Text, { color: p.status === "done" ? "green" : p.status === "running" ? "yellow" : "gray" }, p.status === "done" ? "✓" : p.status === "running" ? "◌" : "○"), /* @__PURE__ */ React.createElement(Text, null, " ", p.name))));
151
+ }
152
+ if (checks.length === 0) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "No results yet. Press [r] to scan."));
153
+ const maxVisible = Math.max(1, availableHeight);
154
+ const visible = checks.slice(scrollOffset, scrollOffset + maxVisible);
155
+ const hasMore = scrollOffset + maxVisible < checks.length;
156
+ const hasLess = scrollOffset > 0;
157
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, hasLess && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \\u25B2 ", scrollOffset, " more above"), visible.map((check) => /* @__PURE__ */ React.createElement(Box, { key: check.id }, /* @__PURE__ */ React.createElement(Text, { color: check.status === "pass" ? "green" : check.status === "fail" ? "red" : "yellow" }, statusIcon(check.status)), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(Box, { width: 26 }, /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, truncate(check.name, 26))), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(Text, { color: scoreColor$2(check.score) }, scoreBar$2(check.score)), /* @__PURE__ */ React.createElement(Text, { color: scoreColor$2(check.score) }, " ", String(check.score).padStart(3)))), hasMore && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " ▼ ", checks.length - scrollOffset - maxVisible, " more below"), isScanning && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, " ◌ Re-scanning..."));
158
+ }
159
+ //#endregion
160
+ //#region src/components/tui/HelpPanel.tsx
161
+ const SECTIONS = [
162
+ {
163
+ title: "PANELS",
164
+ rows: [
165
+ {
166
+ keys: ["h"],
167
+ description: "Focus / unfocus Health Checks"
168
+ },
169
+ {
170
+ keys: ["g"],
171
+ description: "Focus / unfocus Git Status"
172
+ },
173
+ {
174
+ keys: ["t"],
175
+ description: "Focus / unfocus Trend"
176
+ },
177
+ {
178
+ keys: ["q"],
179
+ description: "Focus / unfocus Quick Wins"
180
+ },
181
+ {
182
+ keys: ["a"],
183
+ description: "Focus / unfocus Activity"
184
+ },
185
+ {
186
+ keys: ["f"],
187
+ description: "Expand focused panel to full screen"
188
+ },
189
+ {
190
+ keys: ["Esc"],
191
+ description: "Unfocus panel / exit full screen"
192
+ }
193
+ ]
194
+ },
195
+ {
196
+ title: "SCROLLING",
197
+ rows: [{
198
+ keys: ["↑", "↓"],
199
+ description: "Scroll Health Checks (when focused)"
200
+ }]
201
+ },
202
+ {
203
+ title: "ACTIONS",
204
+ rows: [
205
+ {
206
+ keys: ["r"],
207
+ description: "Re-run scan"
208
+ },
209
+ {
210
+ keys: ["w"],
211
+ description: "Open web dashboard in browser"
212
+ },
213
+ {
214
+ keys: ["W"],
215
+ description: "Open web dashboard with AI insights"
216
+ },
217
+ {
218
+ keys: ["?"],
219
+ description: "Toggle this help screen"
220
+ }
221
+ ]
222
+ }
223
+ ];
224
+ function HelpPanel() {
225
+ return /* @__PURE__ */ React.createElement(Box, {
226
+ flexDirection: "column",
227
+ paddingX: 2,
228
+ paddingY: 1
229
+ }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, {
230
+ bold: true,
231
+ color: "cyan"
232
+ }, "SICKBAY TUI — KEYBOARD SHORTCUTS")), SECTIONS.map((section) => /* @__PURE__ */ React.createElement(Box, {
233
+ key: section.title,
234
+ flexDirection: "column",
235
+ marginBottom: 1
236
+ }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 0 }, /* @__PURE__ */ React.createElement(Text, {
237
+ bold: true,
238
+ color: "white"
239
+ }, section.title)), section.rows.map((row) => /* @__PURE__ */ React.createElement(Box, {
240
+ key: row.description,
241
+ marginLeft: 2
242
+ }, /* @__PURE__ */ React.createElement(Box, { width: 16 }, row.keys.map((k, i) => /* @__PURE__ */ React.createElement(Text, { key: k }, /* @__PURE__ */ React.createElement(Text, {
243
+ color: "cyan",
244
+ bold: true
245
+ }, "[", k, "]"), i < row.keys.length - 1 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " / ") : null))), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, row.description))))), /* @__PURE__ */ React.createElement(Box, {
246
+ marginTop: 1,
247
+ borderStyle: "single",
248
+ borderColor: "gray",
249
+ paddingX: 1
250
+ }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Pro tip: focus a panel with its hotkey, then press "), /* @__PURE__ */ React.createElement(Text, {
251
+ color: "cyan",
252
+ bold: true
253
+ }, "[f]"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " to expand it full screen for more detail.")), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press "), /* @__PURE__ */ React.createElement(Text, {
254
+ color: "cyan",
255
+ bold: true
256
+ }, "[?]"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " or "), /* @__PURE__ */ React.createElement(Text, {
257
+ color: "cyan",
258
+ bold: true
259
+ }, "[Esc]"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " to close")));
260
+ }
261
+ //#endregion
262
+ //#region src/components/tui/hooks/useFileWatcher.ts
263
+ function useFileWatcher({ projectPath, enabled, debounceMs = 2e3, onFilesChanged }) {
264
+ const [changedFiles, setChangedFiles] = useState([]);
265
+ const debounceRef = useRef(void 0);
266
+ const pendingRef = useRef([]);
267
+ const watcherRef = useRef(null);
268
+ const onFilesChangedRef = useRef(onFilesChanged);
269
+ onFilesChangedRef.current = onFilesChanged;
270
+ useEffect(() => {
271
+ if (!enabled) return;
272
+ let mounted = true;
273
+ (async () => {
274
+ try {
275
+ const { watch } = await import("chokidar");
276
+ if (!mounted) return;
277
+ const watcher = watch(["**/*.{ts,tsx,js,jsx,json}"], {
278
+ cwd: projectPath,
279
+ ignored: [
280
+ "**/node_modules/**",
281
+ "**/dist/**",
282
+ "**/.git/**",
283
+ "**/build/**",
284
+ "**/.next/**",
285
+ "**/.turbo/**",
286
+ "**/coverage/**"
287
+ ],
288
+ ignoreInitial: true,
289
+ persistent: true
290
+ });
291
+ watcherRef.current = watcher;
292
+ watcher.on("change", (path) => {
293
+ pendingRef.current.push(path);
294
+ setChangedFiles((prev) => [...prev, path]);
295
+ if (debounceRef.current) clearTimeout(debounceRef.current);
296
+ debounceRef.current = setTimeout(() => {
297
+ const files = [...pendingRef.current];
298
+ pendingRef.current = [];
299
+ onFilesChangedRef.current?.(files);
300
+ }, debounceMs);
301
+ });
302
+ } catch {}
303
+ })();
304
+ return () => {
305
+ mounted = false;
306
+ if (debounceRef.current) clearTimeout(debounceRef.current);
307
+ if (watcherRef.current) watcherRef.current.close();
308
+ };
309
+ }, [
310
+ projectPath,
311
+ enabled,
312
+ debounceMs
313
+ ]);
314
+ return changedFiles;
315
+ }
316
+ //#endregion
317
+ //#region src/components/tui/hooks/useSickbayRunner.ts
318
+ /**
319
+ * Synthesize a rolled-up SickbayReport from a MonorepoReport for use in
320
+ * TUI panels that only understand single-project reports. Strategy: merge
321
+ * all checks from all packages, keeping the worst score per check name.
322
+ */
323
+ function rollUpMonorepoReport(monorepo) {
324
+ const byId = /* @__PURE__ */ new Map();
325
+ for (const pkg of monorepo.packages) for (const check of pkg.checks) {
326
+ const existing = byId.get(check.id);
327
+ if (!existing || check.score < existing.score) byId.set(check.id, check);
328
+ }
329
+ const checks = Array.from(byId.values());
330
+ return {
331
+ timestamp: monorepo.timestamp,
332
+ projectPath: monorepo.rootPath,
333
+ projectInfo: {
334
+ name: `monorepo (${monorepo.packages.length} packages)`,
335
+ version: "0.0.0",
336
+ hasTypeScript: false,
337
+ hasESLint: false,
338
+ hasPrettier: false,
339
+ framework: "node",
340
+ packageManager: monorepo.packageManager,
341
+ totalDependencies: 0,
342
+ dependencies: {},
343
+ devDependencies: {}
344
+ },
345
+ checks,
346
+ overallScore: monorepo.overallScore,
347
+ summary: buildSummary(checks)
348
+ };
349
+ }
350
+ function useSickbayRunner({ projectPath, checks, quotes }) {
351
+ const [report, setReport] = useState(null);
352
+ const [monorepoReport, setMonorepoReport] = useState(null);
353
+ const [isScanning, setIsScanning] = useState(false);
354
+ const [progress, setProgress] = useState([]);
355
+ const [error, setError] = useState(null);
356
+ const scanningRef = useRef(false);
357
+ return {
358
+ report,
359
+ monorepoReport,
360
+ isScanning,
361
+ progress,
362
+ error,
363
+ scan: useCallback(async () => {
364
+ if (scanningRef.current) return null;
365
+ scanningRef.current = true;
366
+ setIsScanning(true);
367
+ setError(null);
368
+ try {
369
+ if ((await detectMonorepo(projectPath)).isMonorepo) {
370
+ const result = await runSickbayMonorepo({
371
+ projectPath,
372
+ checks,
373
+ quotes,
374
+ onPackageStart: (name) => {
375
+ setProgress((prev) => [...prev, {
376
+ name,
377
+ status: "running"
378
+ }]);
379
+ },
380
+ onPackageComplete: (pkg) => {
381
+ setProgress((prev) => prev.map((p) => p.name === pkg.name ? {
382
+ ...p,
383
+ status: "done"
384
+ } : p));
385
+ }
386
+ });
387
+ setMonorepoReport(result);
388
+ const rolledUp = rollUpMonorepoReport(result);
389
+ setReport(rolledUp);
390
+ setIsScanning(false);
391
+ scanningRef.current = false;
392
+ return rolledUp;
393
+ }
394
+ const result = await runSickbay({
395
+ projectPath,
396
+ checks,
397
+ quotes,
398
+ onRunnersReady: (names) => {
399
+ setProgress(names.map((name) => ({
400
+ name,
401
+ status: "pending"
402
+ })));
403
+ },
404
+ onCheckStart: (name) => {
405
+ setProgress((prev) => prev.map((p) => p.name === name ? {
406
+ ...p,
407
+ status: "running"
408
+ } : p));
409
+ },
410
+ onCheckComplete: (check) => {
411
+ setProgress((prev) => prev.map((p) => p.name === check.id ? {
412
+ ...p,
413
+ status: "done"
414
+ } : p));
415
+ }
416
+ });
417
+ setMonorepoReport(null);
418
+ setReport(result);
419
+ try {
420
+ const { saveEntry } = await import("./history-DVegAwFp.js").then((n) => n.n);
421
+ saveEntry(result);
422
+ } catch {}
423
+ setIsScanning(false);
424
+ scanningRef.current = false;
425
+ return result;
426
+ } catch (err) {
427
+ setError(err instanceof Error ? err.message : String(err));
428
+ setIsScanning(false);
429
+ scanningRef.current = false;
430
+ return null;
431
+ }
432
+ }, [
433
+ projectPath,
434
+ checks,
435
+ quotes
436
+ ])
437
+ };
438
+ }
439
+ //#endregion
440
+ //#region src/components/tui/hooks/useTerminalSize.ts
441
+ function useTerminalSize() {
442
+ const [size, setSize] = useState({
443
+ columns: process.stdout.columns || 80,
444
+ rows: process.stdout.rows || 24
445
+ });
446
+ useEffect(() => {
447
+ const handler = () => {
448
+ setSize({
449
+ columns: process.stdout.columns || 80,
450
+ rows: process.stdout.rows || 24
451
+ });
452
+ };
453
+ process.stdout.on("resize", handler);
454
+ return () => {
455
+ process.stdout.off("resize", handler);
456
+ };
457
+ }, []);
458
+ return size;
459
+ }
460
+ //#endregion
461
+ //#region src/components/tui/HotkeyBar.tsx
462
+ const HOTKEYS = [
463
+ {
464
+ key: "h",
465
+ label: "health",
466
+ panel: "health"
467
+ },
468
+ {
469
+ key: "g",
470
+ label: "git",
471
+ panel: "git"
472
+ },
473
+ {
474
+ key: "t",
475
+ label: "trend",
476
+ panel: "trend"
477
+ },
478
+ {
479
+ key: "q",
480
+ label: "quick wins",
481
+ panel: "quickwins"
482
+ },
483
+ {
484
+ key: "a",
485
+ label: "activity",
486
+ panel: "activity"
487
+ },
488
+ {
489
+ key: "r",
490
+ label: "re-run"
491
+ },
492
+ {
493
+ key: "w",
494
+ label: "web"
495
+ },
496
+ {
497
+ key: "W",
498
+ label: "web+AI"
499
+ },
500
+ {
501
+ key: "?",
502
+ label: "help"
503
+ }
504
+ ];
505
+ function HotkeyBar({ activePanel }) {
506
+ return /* @__PURE__ */ React.createElement(Box, null, HOTKEYS.map((hk) => {
507
+ const isActive = hk.panel !== void 0 && hk.panel === activePanel;
508
+ return /* @__PURE__ */ React.createElement(Box, {
509
+ key: hk.key,
510
+ marginRight: 2
511
+ }, /* @__PURE__ */ React.createElement(Text, {
512
+ bold: isActive,
513
+ color: isActive ? "cyan" : void 0
514
+ }, "[", hk.key, "]"), /* @__PURE__ */ React.createElement(Text, {
515
+ bold: isActive,
516
+ color: isActive ? "cyan" : "gray"
517
+ }, " ", hk.label));
518
+ }));
519
+ }
520
+ //#endregion
521
+ //#region src/components/tui/MonorepoPanel.tsx
522
+ function scoreBar$1(score, width = 8) {
523
+ const filled = Math.round(score / 100 * width);
524
+ return "█".repeat(filled) + "░".repeat(width - filled);
525
+ }
526
+ function scoreColor$1(score) {
527
+ if (score >= 80) return "green";
528
+ if (score >= 60) return "yellow";
529
+ return "red";
530
+ }
531
+ function MonorepoPanel({ report, availableWidth }) {
532
+ if (!report) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Waiting for scan..."));
533
+ const nameWidth = Math.max(12, Math.min(20, (availableWidth ?? 36) - 22));
534
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, { marginBottom: 1 }, /* @__PURE__ */ React.createElement(Text, { bold: true }, report.packages.length, " packages"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " · "), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, report.monorepoType, " workspaces"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " · "), /* @__PURE__ */ React.createElement(Text, {
535
+ color: scoreColor$1(report.overallScore),
536
+ bold: true
537
+ }, report.overallScore), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " avg")), report.packages.map((pkg) => {
538
+ const name = pkg.name.includes("/") ? pkg.name.split("/").pop() ?? pkg.name : pkg.name;
539
+ const truncatedName = name.length > nameWidth ? name.slice(0, nameWidth - 1) + "…" : name.padEnd(nameWidth);
540
+ return /* @__PURE__ */ React.createElement(Box, {
541
+ key: pkg.path,
542
+ gap: 1
543
+ }, /* @__PURE__ */ React.createElement(Text, { color: scoreColor$1(pkg.score) }, scoreBar$1(pkg.score)), /* @__PURE__ */ React.createElement(Text, {
544
+ color: scoreColor$1(pkg.score),
545
+ bold: true
546
+ }, String(pkg.score).padStart(3)), /* @__PURE__ */ React.createElement(Text, null, truncatedName), pkg.summary.critical > 0 && /* @__PURE__ */ React.createElement(Text, { color: "red" }, " ⚠"));
547
+ }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Press "), /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, "[w]"), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " for per-package web view")));
548
+ }
549
+ //#endregion
550
+ //#region src/components/tui/PanelBorder.tsx
551
+ function PanelBorder({ title, color, focused, visible = true, flash, children }) {
552
+ const borderColor = flash ?? (focused ? color : "gray");
553
+ const borderStyle = flash ? "double" : focused ? "double" : "single";
554
+ return /* @__PURE__ */ React.createElement(Box, {
555
+ flexDirection: "column",
556
+ borderStyle,
557
+ borderColor,
558
+ paddingX: 1,
559
+ flexGrow: 1
560
+ }, /* @__PURE__ */ React.createElement(Text, {
561
+ bold: true,
562
+ color: flash ?? color
563
+ }, title), visible ? children : /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "···"));
564
+ }
565
+ //#endregion
566
+ //#region src/components/tui/QuickWinsPanel.tsx
567
+ function replacePackageManager(cmd, pm) {
568
+ if (pm === "npm") return cmd;
569
+ const install = pm === "pnpm" ? "pnpm add" : pm === "yarn" ? "yarn add" : "bun add";
570
+ const uninstall = pm === "pnpm" ? "pnpm remove" : pm === "yarn" ? "yarn remove" : "bun remove";
571
+ const update = pm === "pnpm" ? "pnpm update" : pm === "yarn" ? "yarn upgrade" : "bun update";
572
+ const auditFix = pm === "pnpm" ? "pnpm audit --fix" : pm === "yarn" ? "yarn npm audit --fix" : "bun audit";
573
+ return cmd.replace(/^npm install(?=\s)/, install).replace(/^npm uninstall(?=\s)/, uninstall).replace(/^npm update(?=\s)/, update).replace(/^npm audit fix/, auditFix);
574
+ }
575
+ function shortenPath(filepath) {
576
+ const parts = filepath.split("/");
577
+ if (parts.length <= 2) return filepath;
578
+ return "…/" + parts.slice(-2).join("/");
579
+ }
580
+ function smartTruncate(str, maxLen) {
581
+ if (str.length <= maxLen) return str;
582
+ const pathMatch = str.match(/(\S*\/\S+)/);
583
+ if (pathMatch) {
584
+ const shortened = str.replace(pathMatch[1], shortenPath(pathMatch[1]));
585
+ if (shortened.length <= maxLen) return shortened;
586
+ return shortened.slice(0, maxLen - 1) + "…";
587
+ }
588
+ return str.slice(0, maxLen - 1) + "…";
589
+ }
590
+ function shortenCommand(cmd, maxLen) {
591
+ if (cmd.length <= maxLen) return cmd;
592
+ const shortened = cmd.replace(/(\S*\/\S+)/g, (match) => shortenPath(match));
593
+ if (shortened.length <= maxLen) return shortened;
594
+ return shortened.slice(0, maxLen - 1) + "…";
595
+ }
596
+ function QuickWinsPanel({ report, availableWidth }) {
597
+ if (!report) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Waiting for scan..."));
598
+ const fixes = [];
599
+ for (const check of report.checks) for (const issue of check.issues) if (issue.fix?.command) fixes.push({
600
+ description: issue.fix.description,
601
+ command: issue.fix.command,
602
+ severity: issue.severity
603
+ });
604
+ const order = {
605
+ critical: 0,
606
+ warning: 1,
607
+ info: 2
608
+ };
609
+ fixes.sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3));
610
+ const top = fixes.slice(0, 5);
611
+ if (top.length === 0) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "green" }, "Looking good!"));
612
+ const pm = report.projectInfo?.packageManager ?? "npm";
613
+ const maxTextLen = Math.max(16, (availableWidth ?? 26) - 4);
614
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, top.map((fix, i) => /* @__PURE__ */ React.createElement(Box, {
615
+ key: i,
616
+ flexDirection: "column",
617
+ marginBottom: i < top.length - 1 ? 1 : 0
618
+ }, /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { color: fix.severity === "critical" ? "red" : fix.severity === "warning" ? "yellow" : "gray" }, "→", " "), smartTruncate(fix.description, maxTextLen)), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " ", shortenCommand(replacePackageManager(fix.command, pm), maxTextLen)))));
619
+ }
620
+ //#endregion
621
+ //#region src/components/tui/ScorePanel.tsx
622
+ function scoreColor(score) {
623
+ if (score >= 80) return "green";
624
+ if (score >= 60) return "yellow";
625
+ return "red";
626
+ }
627
+ function scoreBar(score, width = 15) {
628
+ const filled = Math.round(score / 100 * width);
629
+ return "█".repeat(filled) + "░".repeat(width - filled);
630
+ }
631
+ function ScorePanel({ report, previousScore, animate = true }) {
632
+ const [animatedScore, setAnimatedScore] = useState(0);
633
+ const prevTargetRef = useRef(0);
634
+ useEffect(() => {
635
+ if (!animate) return;
636
+ if (!report) {
637
+ setAnimatedScore(0);
638
+ prevTargetRef.current = 0;
639
+ return;
640
+ }
641
+ const target = report.overallScore;
642
+ const start = prevTargetRef.current;
643
+ prevTargetRef.current = target;
644
+ if (start === target) return;
645
+ let current = start;
646
+ const step = target > start ? 1 : -1;
647
+ const id = setInterval(() => {
648
+ current += step;
649
+ setAnimatedScore(current);
650
+ if (current === target) clearInterval(id);
651
+ }, 20);
652
+ return () => clearInterval(id);
653
+ }, [report, animate]);
654
+ const displayScore = animate ? animatedScore : report?.overallScore ?? 0;
655
+ if (!report) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "Waiting for scan..."));
656
+ const delta = previousScore !== null ? report.overallScore - previousScore : null;
657
+ const u = countUniqueIssues(report.checks);
658
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, {
659
+ color: scoreColor(displayScore),
660
+ bold: true
661
+ }, displayScore, "/100"), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(Text, { color: scoreColor(displayScore) }, scoreBar(displayScore))), delta !== null && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, delta > 0 ? `+${delta}` : delta === 0 ? "±0" : `${delta}`, " since last scan"), /* @__PURE__ */ React.createElement(Box, {
662
+ marginTop: 1,
663
+ flexDirection: "column"
664
+ }, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "red" }, "✗", " ", u.critical, " critical", u.totalCritical > u.critical ? ` (${u.totalCritical})` : ""), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "⚠", " ", u.warnings, " warn", u.totalWarnings > u.warnings ? ` (${u.totalWarnings})` : ""), /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "i ", u.info, " info"))), report.quote && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, {
665
+ italic: true,
666
+ dimColor: true
667
+ }, "\"", report.quote.text, "\" — ", report.quote.source)), report.config?.hasCustomConfig && /* @__PURE__ */ React.createElement(Text, {
668
+ dimColor: true,
669
+ italic: true
670
+ }, "Custom config"));
671
+ }
672
+ //#endregion
673
+ //#region src/components/tui/TrendPanel.tsx
674
+ const CATEGORY_COLORS = {
675
+ overall: "white",
676
+ dependencies: "blue",
677
+ security: "green",
678
+ "code-quality": "yellow",
679
+ performance: "magenta",
680
+ git: "cyan"
681
+ };
682
+ const SHORT_LABELS = {
683
+ dependencies: "Deps",
684
+ security: "Security",
685
+ "code-quality": "Quality",
686
+ performance: "Perf",
687
+ git: "Git"
688
+ };
689
+ function TrendPanel({ projectPath, lastScanTime, availableHeight }) {
690
+ const [trends, setTrends] = useState([]);
691
+ useEffect(() => {
692
+ const history = loadHistory(projectPath);
693
+ if (!history || history.entries.length === 0) {
694
+ setTrends([]);
695
+ return;
696
+ }
697
+ const entries = history.entries.slice(-10);
698
+ const overallValues = entries.map((e) => e.overallScore);
699
+ const overallArrow = trendArrow(overallValues);
700
+ const result = [{
701
+ label: "Overall",
702
+ spark: sparkline(overallValues),
703
+ latest: overallValues[overallValues.length - 1],
704
+ arrow: overallArrow.label,
705
+ color: "white"
706
+ }];
707
+ const categories = Object.keys(entries[entries.length - 1].categoryScores);
708
+ for (const cat of categories) {
709
+ const values = entries.map((e) => e.categoryScores[cat]).filter((v) => v !== void 0);
710
+ if (values.length === 0) continue;
711
+ const arrow = trendArrow(values);
712
+ result.push({
713
+ label: SHORT_LABELS[cat] || cat.charAt(0).toUpperCase() + cat.slice(1),
714
+ spark: sparkline(values),
715
+ latest: values[values.length - 1],
716
+ arrow: arrow.label,
717
+ color: CATEGORY_COLORS[cat] || "white"
718
+ });
719
+ }
720
+ setTrends(result);
721
+ }, [projectPath, lastScanTime]);
722
+ if (trends.length === 0) return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "No trend data yet."));
723
+ const maxRows = availableHeight ?? trends.length;
724
+ const visible = trends.slice(0, maxRows);
725
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, visible.map((t) => /* @__PURE__ */ React.createElement(Box, { key: t.label }, /* @__PURE__ */ React.createElement(Box, { width: 10 }, /* @__PURE__ */ React.createElement(Text, { color: t.color }, t.label)), /* @__PURE__ */ React.createElement(Text, null, t.spark), /* @__PURE__ */ React.createElement(Text, { bold: true }, " ", String(t.latest).padStart(3)), /* @__PURE__ */ React.createElement(Text, null, " ", t.arrow))));
726
+ }
727
+ //#endregion
728
+ //#region src/components/tui/TuiApp.tsx
729
+ function TuiApp({ projectPath, checks, watchEnabled, refreshInterval, quotes, animateOnMount = true }) {
730
+ const { rows, columns } = useTerminalSize();
731
+ const { report, monorepoReport, isScanning, progress, scan } = useSickbayRunner({
732
+ projectPath,
733
+ checks,
734
+ quotes
735
+ });
736
+ const [focusedPanel, setFocusedPanel] = useState(null);
737
+ const [expandedPanel, setExpandedPanel] = useState(null);
738
+ const [showHelp, setShowHelp] = useState(false);
739
+ const [lastScanTime, setLastScanTime] = useState(null);
740
+ const [previousScore, setPreviousScore] = useState(null);
741
+ const [activityLog, setActivityLog] = useState([]);
742
+ const [healthScrollOffset, setHealthScrollOffset] = useState(0);
743
+ const [scoreFlash, setScoreFlash] = useState();
744
+ const [updateInfo, setUpdateInfo] = useState(null);
745
+ const [visiblePanels, setVisiblePanels] = useState(animateOnMount ? /* @__PURE__ */ new Set() : new Set([
746
+ "health",
747
+ "score",
748
+ "trend",
749
+ "git",
750
+ "quickwins",
751
+ "activity"
752
+ ]));
753
+ const refreshRef = useRef(void 0);
754
+ useEffect(() => {
755
+ const timers = [
756
+ ["health", 100],
757
+ ["score", 300],
758
+ ["trend", 500],
759
+ ["git", 700],
760
+ ["quickwins", 900],
761
+ ["activity", 1100]
762
+ ].map(([panel, delay]) => setTimeout(() => {
763
+ setVisiblePanels((prev) => new Set([...prev, panel]));
764
+ }, delay));
765
+ return () => timers.forEach(clearTimeout);
766
+ }, []);
767
+ useEffect(() => {
768
+ checkForUpdate("1.16.1").then((info) => {
769
+ if (info) setUpdateInfo(info);
770
+ });
771
+ }, []);
772
+ const reportRef = useRef(null);
773
+ const monorepoReportRef = useRef(null);
774
+ useEffect(() => {
775
+ reportRef.current = report;
776
+ }, [report]);
777
+ useEffect(() => {
778
+ monorepoReportRef.current = monorepoReport;
779
+ }, [monorepoReport]);
780
+ const addActivity = useCallback((type, message) => {
781
+ setActivityLog((prev) => [...prev, {
782
+ timestamp: /* @__PURE__ */ new Date(),
783
+ type,
784
+ message
785
+ }]);
786
+ }, []);
787
+ const handleScanComplete = useCallback(async (result) => {
788
+ const prevScore = reportRef.current?.overallScore ?? null;
789
+ if (prevScore !== null) setPreviousScore(prevScore);
790
+ setLastScanTime(/* @__PURE__ */ new Date());
791
+ try {
792
+ const { saveLastReport } = await import("./history-DVegAwFp.js").then((n) => n.n);
793
+ saveLastReport(result);
794
+ } catch {}
795
+ try {
796
+ const { getDependencyTree } = await import("./dist-Bjhxbcak.js").then((n) => n.s);
797
+ const { saveDepTree } = await import("./history-DVegAwFp.js").then((n) => n.n);
798
+ saveDepTree(projectPath, await getDependencyTree(projectPath, result.projectInfo.packageManager));
799
+ } catch {}
800
+ const delta = prevScore !== null ? result.overallScore - prevScore : null;
801
+ addActivity("scan-complete", `Scan complete: ${result.overallScore}/100${delta !== null ? ` (${delta >= 0 ? "+" : ""}${delta})` : ""}`);
802
+ if (delta !== null && delta !== 0) {
803
+ setScoreFlash(delta > 0 ? "green" : "red");
804
+ setTimeout(() => setScoreFlash(void 0), 600);
805
+ }
806
+ try {
807
+ const { loadHistory, detectRegressions } = await import("./history-DVegAwFp.js").then((n) => n.n);
808
+ const history = loadHistory(projectPath);
809
+ if (history) {
810
+ const regressions = detectRegressions(history.entries);
811
+ for (const reg of regressions) addActivity("regression", `\u26A0 ${reg.category} regressed: ${reg.from} \u2192 ${reg.to}`);
812
+ }
813
+ } catch {}
814
+ }, [addActivity, projectPath]);
815
+ useEffect(() => {
816
+ addActivity("info", "TUI started");
817
+ addActivity("scan-start", "Starting initial health scan...");
818
+ scan().then((result) => {
819
+ if (result) handleScanComplete(result);
820
+ });
821
+ }, []);
822
+ useEffect(() => {
823
+ if (refreshInterval <= 0) return;
824
+ refreshRef.current = setInterval(async () => {
825
+ addActivity("scan-start", "Auto-scan triggered");
826
+ const result = await scan();
827
+ if (result) handleScanComplete(result);
828
+ }, refreshInterval * 1e3);
829
+ return () => {
830
+ if (refreshRef.current) clearInterval(refreshRef.current);
831
+ };
832
+ }, [
833
+ refreshInterval,
834
+ scan,
835
+ handleScanComplete,
836
+ addActivity
837
+ ]);
838
+ useFileWatcher({
839
+ projectPath,
840
+ enabled: watchEnabled,
841
+ onFilesChanged: async (files) => {
842
+ for (const f of files.slice(0, 3)) addActivity("file-change", `File changed: ${f}`);
843
+ if (files.length > 3) addActivity("file-change", `...and ${files.length - 3} more files`);
844
+ addActivity("scan-start", "Re-scan triggered (file change)");
845
+ const result = await scan();
846
+ if (result) handleScanComplete(result);
847
+ }
848
+ });
849
+ const triggerRescan = useCallback(async () => {
850
+ addActivity("scan-start", "Manual re-scan triggered");
851
+ const result = await scan();
852
+ if (result) handleScanComplete(result);
853
+ }, [
854
+ addActivity,
855
+ scan,
856
+ handleScanComplete
857
+ ]);
858
+ useInput((input, key) => {
859
+ if (showHelp) {
860
+ if (input === "?" || key.escape) setShowHelp(false);
861
+ return;
862
+ }
863
+ if (expandedPanel) {
864
+ if (key.escape || input === "f") {
865
+ setExpandedPanel(null);
866
+ return;
867
+ }
868
+ }
869
+ if (input === "?") {
870
+ setShowHelp(true);
871
+ return;
872
+ }
873
+ if (input === "h") setFocusedPanel((prev) => prev === "health" ? null : "health");
874
+ else if (input === "g") setFocusedPanel((prev) => prev === "git" ? null : "git");
875
+ else if (input === "t") setFocusedPanel((prev) => prev === "trend" ? null : "trend");
876
+ else if (input === "q") setFocusedPanel((prev) => prev === "quickwins" ? null : "quickwins");
877
+ else if (input === "a") setFocusedPanel((prev) => prev === "activity" ? null : "activity");
878
+ else if (input === "r") triggerRescan();
879
+ else if (input === "f" && focusedPanel) setExpandedPanel(focusedPanel);
880
+ else if (input === "w" || input === "W") {
881
+ const withAI = input === "W";
882
+ (async () => {
883
+ const webReport = monorepoReportRef.current ?? reportRef.current;
884
+ if (!webReport) return;
885
+ try {
886
+ const { serveWeb } = await import("./web-D5etye5Q.js");
887
+ const { default: openBrowser } = await import("open");
888
+ let aiService;
889
+ if (withAI && process.env.ANTHROPIC_API_KEY && !monorepoReportRef.current) {
890
+ const { createAIService } = await import("./ai-DxA5r3hA.js");
891
+ aiService = createAIService(process.env.ANTHROPIC_API_KEY);
892
+ addActivity("info", "Launching web dashboard with AI...");
893
+ } else addActivity("info", "Launching web dashboard...");
894
+ const url = await serveWeb(webReport, 3030, aiService);
895
+ await openBrowser(url);
896
+ addActivity("info", `Web dashboard at ${url}${withAI && aiService ? " (AI enabled)" : ""}`);
897
+ } catch (err) {
898
+ addActivity("info", `Failed to open web: ${err instanceof Error ? err.message : err}`);
899
+ }
900
+ })();
901
+ } else if (key.escape) setFocusedPanel(null);
902
+ if (focusedPanel === "health") {
903
+ if (key.upArrow) setHealthScrollOffset((prev) => Math.max(0, prev - 1));
904
+ if (key.downArrow) {
905
+ const maxOffset = Math.max(0, visibleChecks.length - 5);
906
+ setHealthScrollOffset((prev) => Math.min(maxOffset, prev + 1));
907
+ }
908
+ }
909
+ }, { isActive: !!(process.stdin.isTTY && process.stdin.setRawMode) });
910
+ const visibleChecks = report?.checks.filter((c) => c.status !== "skipped") ?? [];
911
+ const topHeight = Math.floor((rows - 2) * .6);
912
+ const bottomHeight = rows - 2 - topHeight;
913
+ if (showHelp) return /* @__PURE__ */ React.createElement(Box, {
914
+ flexDirection: "column",
915
+ width: columns,
916
+ height: rows
917
+ }, /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, /* @__PURE__ */ React.createElement(PanelBorder, {
918
+ title: "HELP",
919
+ color: "cyan",
920
+ focused: true
921
+ }, /* @__PURE__ */ React.createElement(HelpPanel, null))), /* @__PURE__ */ React.createElement(HotkeyBar, { activePanel: focusedPanel }));
922
+ if (expandedPanel) return /* @__PURE__ */ React.createElement(Box, {
923
+ flexDirection: "column",
924
+ width: columns,
925
+ height: rows
926
+ }, /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, expandedPanel === "health" && /* @__PURE__ */ React.createElement(PanelBorder, {
927
+ title: "HEALTH CHECKS",
928
+ color: "green",
929
+ focused: true
930
+ }, /* @__PURE__ */ React.createElement(HealthPanel, {
931
+ checks: visibleChecks,
932
+ isScanning,
933
+ progress,
934
+ scrollOffset: healthScrollOffset,
935
+ availableHeight: rows - 4
936
+ })), expandedPanel === "git" && /* @__PURE__ */ React.createElement(PanelBorder, {
937
+ title: "GIT STATUS",
938
+ color: "yellow",
939
+ focused: true
940
+ }, /* @__PURE__ */ React.createElement(GitPanel, {
941
+ projectPath,
942
+ availableWidth: columns - 4
943
+ })), expandedPanel === "trend" && /* @__PURE__ */ React.createElement(PanelBorder, {
944
+ title: "TREND",
945
+ color: "magenta",
946
+ focused: true
947
+ }, /* @__PURE__ */ React.createElement(TrendPanel, {
948
+ projectPath,
949
+ lastScanTime
950
+ })), expandedPanel === "quickwins" && /* @__PURE__ */ React.createElement(PanelBorder, {
951
+ title: monorepoReport ? "MONOREPO" : "QUICK WINS",
952
+ color: "red",
953
+ focused: true
954
+ }, monorepoReport ? /* @__PURE__ */ React.createElement(MonorepoPanel, { report: monorepoReport }) : /* @__PURE__ */ React.createElement(QuickWinsPanel, { report })), expandedPanel === "activity" && /* @__PURE__ */ React.createElement(PanelBorder, {
955
+ title: "ACTIVITY",
956
+ color: "cyan",
957
+ focused: true
958
+ }, /* @__PURE__ */ React.createElement(ActivityPanel, {
959
+ entries: activityLog,
960
+ availableHeight: rows - 4
961
+ }))), /* @__PURE__ */ React.createElement(HotkeyBar, { activePanel: expandedPanel }));
962
+ const projectName = monorepoReport ? `${monorepoReport.rootPath.split("/").pop()} (monorepo)` : report?.projectInfo?.name ?? "—";
963
+ const projectVersion = report?.projectInfo?.version;
964
+ const scanLabel = lastScanTime ? `Last scan ${lastScanTime.toLocaleTimeString()}` : isScanning ? "Scanning…" : "Not yet scanned";
965
+ return /* @__PURE__ */ React.createElement(Box, {
966
+ flexDirection: "column",
967
+ width: columns,
968
+ height: rows
969
+ }, updateInfo && /* @__PURE__ */ React.createElement(UpdateNotice, {
970
+ currentVersion: updateInfo.currentVersion,
971
+ latestVersion: updateInfo.latestVersion
972
+ }), /* @__PURE__ */ React.createElement(Box, {
973
+ paddingX: 1,
974
+ justifyContent: "space-between"
975
+ }, /* @__PURE__ */ React.createElement(Box, { gap: 1 }, /* @__PURE__ */ React.createElement(Text, {
976
+ bold: true,
977
+ color: "cyan"
978
+ }, "SICKBAY"), /* @__PURE__ */ React.createElement(Text, { bold: true }, projectName), projectVersion && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "v", projectVersion)), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, scanLabel)), /* @__PURE__ */ React.createElement(Box, { height: topHeight }, /* @__PURE__ */ React.createElement(Box, { width: "55%" }, /* @__PURE__ */ React.createElement(PanelBorder, {
979
+ title: "HEALTH CHECKS",
980
+ color: "green",
981
+ focused: focusedPanel === "health",
982
+ visible: visiblePanels.has("health")
983
+ }, /* @__PURE__ */ React.createElement(HealthPanel, {
984
+ checks: visibleChecks,
985
+ isScanning,
986
+ progress,
987
+ scrollOffset: healthScrollOffset,
988
+ availableHeight: topHeight - 4
989
+ }))), /* @__PURE__ */ React.createElement(Box, {
990
+ width: "45%",
991
+ flexDirection: "column"
992
+ }, /* @__PURE__ */ React.createElement(Box, { height: "50%" }, /* @__PURE__ */ React.createElement(PanelBorder, {
993
+ title: "SCORE",
994
+ color: "blue",
995
+ visible: visiblePanels.has("score"),
996
+ flash: scoreFlash
997
+ }, /* @__PURE__ */ React.createElement(ScorePanel, {
998
+ report,
999
+ previousScore,
1000
+ animate: animateOnMount
1001
+ }))), /* @__PURE__ */ React.createElement(Box, { height: "50%" }, /* @__PURE__ */ React.createElement(PanelBorder, {
1002
+ title: "TREND",
1003
+ color: "magenta",
1004
+ focused: focusedPanel === "trend",
1005
+ visible: visiblePanels.has("trend")
1006
+ }, /* @__PURE__ */ React.createElement(TrendPanel, {
1007
+ projectPath,
1008
+ lastScanTime,
1009
+ availableHeight: Math.floor(topHeight / 2) - 3
1010
+ }))))), /* @__PURE__ */ React.createElement(Box, { height: bottomHeight }, /* @__PURE__ */ React.createElement(Box, { width: "25%" }, /* @__PURE__ */ React.createElement(PanelBorder, {
1011
+ title: "GIT STATUS",
1012
+ color: "yellow",
1013
+ focused: focusedPanel === "git",
1014
+ visible: visiblePanels.has("git")
1015
+ }, /* @__PURE__ */ React.createElement(GitPanel, {
1016
+ projectPath,
1017
+ availableWidth: Math.floor(columns * .25) - 4
1018
+ }))), /* @__PURE__ */ React.createElement(Box, { width: "30%" }, /* @__PURE__ */ React.createElement(PanelBorder, {
1019
+ title: monorepoReport ? "MONOREPO" : "QUICK WINS",
1020
+ color: "red",
1021
+ focused: focusedPanel === "quickwins",
1022
+ visible: visiblePanels.has("quickwins")
1023
+ }, monorepoReport ? /* @__PURE__ */ React.createElement(MonorepoPanel, {
1024
+ report: monorepoReport,
1025
+ availableWidth: Math.floor(columns * .3) - 4
1026
+ }) : /* @__PURE__ */ React.createElement(QuickWinsPanel, {
1027
+ report,
1028
+ availableWidth: Math.floor(columns * .3) - 4
1029
+ }))), /* @__PURE__ */ React.createElement(Box, { width: "45%" }, /* @__PURE__ */ React.createElement(PanelBorder, {
1030
+ title: "ACTIVITY",
1031
+ color: "cyan",
1032
+ focused: focusedPanel === "activity",
1033
+ visible: visiblePanels.has("activity")
1034
+ }, /* @__PURE__ */ React.createElement(ActivityPanel, {
1035
+ entries: activityLog,
1036
+ availableHeight: bottomHeight - 4
1037
+ })))), /* @__PURE__ */ React.createElement(HotkeyBar, { activePanel: focusedPanel }));
1038
+ }
1039
+ //#endregion
1040
+ export { TuiApp };
1041
+
1042
+ //# sourceMappingURL=TuiApp-D4Wn81Qv.js.map