sickbay 1.16.2 → 1.16.3
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/{TuiApp-D4Wn81Qv.js → TuiApp-LHb149v8.js} +3 -3
- package/dist/{TuiApp-D4Wn81Qv.js.map → TuiApp-LHb149v8.js.map} +1 -1
- package/dist/badge-CTayKnYV.js +56 -0
- package/dist/badge-CTayKnYV.js.map +1 -0
- package/dist/index.js +7 -7
- package/dist/{web-D5etye5Q.js → web-CFBLfBdc.js} +27 -22
- package/dist/web-CFBLfBdc.js.map +1 -0
- package/package.json +1 -1
- package/dist/badge-DB3y0fWt.js +0 -37
- package/dist/badge-DB3y0fWt.js.map +0 -1
- package/dist/web-D5etye5Q.js.map +0 -1
|
@@ -765,7 +765,7 @@ function TuiApp({ projectPath, checks, watchEnabled, refreshInterval, quotes, an
|
|
|
765
765
|
return () => timers.forEach(clearTimeout);
|
|
766
766
|
}, []);
|
|
767
767
|
useEffect(() => {
|
|
768
|
-
checkForUpdate("1.16.
|
|
768
|
+
checkForUpdate("1.16.2").then((info) => {
|
|
769
769
|
if (info) setUpdateInfo(info);
|
|
770
770
|
});
|
|
771
771
|
}, []);
|
|
@@ -883,7 +883,7 @@ function TuiApp({ projectPath, checks, watchEnabled, refreshInterval, quotes, an
|
|
|
883
883
|
const webReport = monorepoReportRef.current ?? reportRef.current;
|
|
884
884
|
if (!webReport) return;
|
|
885
885
|
try {
|
|
886
|
-
const { serveWeb } = await import("./web-
|
|
886
|
+
const { serveWeb } = await import("./web-CFBLfBdc.js");
|
|
887
887
|
const { default: openBrowser } = await import("open");
|
|
888
888
|
let aiService;
|
|
889
889
|
if (withAI && process.env.ANTHROPIC_API_KEY && !monorepoReportRef.current) {
|
|
@@ -1039,4 +1039,4 @@ function TuiApp({ projectPath, checks, watchEnabled, refreshInterval, quotes, an
|
|
|
1039
1039
|
//#endregion
|
|
1040
1040
|
export { TuiApp };
|
|
1041
1041
|
|
|
1042
|
-
//# sourceMappingURL=TuiApp-
|
|
1042
|
+
//# sourceMappingURL=TuiApp-LHb149v8.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TuiApp-D4Wn81Qv.js","names":["scoreColor","scoreBar","scoreBar","scoreColor"],"sources":["../src/components/tui/ActivityPanel.tsx","../src/components/tui/hooks/useGitStatus.ts","../src/components/tui/GitPanel.tsx","../src/components/tui/HealthPanel.tsx","../src/components/tui/HelpPanel.tsx","../src/components/tui/hooks/useFileWatcher.ts","../src/components/tui/hooks/useSickbayRunner.ts","../src/components/tui/hooks/useTerminalSize.ts","../src/components/tui/HotkeyBar.tsx","../src/components/tui/MonorepoPanel.tsx","../src/components/tui/PanelBorder.tsx","../src/components/tui/QuickWinsPanel.tsx","../src/components/tui/ScorePanel.tsx","../src/components/tui/TrendPanel.tsx","../src/components/tui/TuiApp.tsx"],"sourcesContent":["import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nexport interface ActivityEntry {\n timestamp: Date;\n type: 'scan-start' | 'scan-complete' | 'file-change' | 'git-change' | 'regression' | 'info';\n message: string;\n}\n\ninterface ActivityPanelProps {\n entries: ActivityEntry[];\n availableHeight: number;\n}\n\nconst TYPE_COLOR: Record<ActivityEntry['type'], string> = {\n 'scan-start': 'cyan',\n 'scan-complete': 'green',\n 'file-change': 'yellow',\n 'git-change': 'magenta',\n regression: 'red',\n info: 'gray',\n};\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString('en-US', { hour12: false });\n}\n\nexport function ActivityPanel({ entries, availableHeight }: ActivityPanelProps) {\n if (entries.length === 0) {\n return (\n <Box>\n <Text dimColor>No activity yet.</Text>\n </Box>\n );\n }\n\n const visible = entries.slice(-Math.max(1, availableHeight));\n\n return (\n <Box flexDirection=\"column\">\n {visible.map((entry) => (\n <Box key={`${entry.timestamp.getTime()}-${entry.type}`}>\n <Text dimColor>\n {formatTime(entry.timestamp)}\n {' '}\n </Text>\n <Text color={TYPE_COLOR[entry.type] || 'white'}>{entry.message}</Text>\n </Box>\n ))}\n </Box>\n );\n}\n","import { execFile } from 'child_process';\nimport { promisify } from 'util';\n\nimport { useState, useEffect, useRef } from 'react';\n\nconst exec = promisify(execFile);\n\nexport interface GitStatus {\n branch: string;\n modified: number;\n staged: number;\n untracked: number;\n ahead: number;\n behind: number;\n stashes: number;\n lastCommit: string;\n lastCommitTime: string;\n}\n\nasync function git(args: string[], cwd: string): Promise<string> {\n try {\n const { stdout } = await exec('git', args, { cwd, timeout: 5000 });\n return stdout.trim();\n } catch {\n return '';\n }\n}\n\nasync function fetchGitStatus(projectPath: string): Promise<GitStatus> {\n const [branch, porcelain, revList, stashList, logLine] = await Promise.all([\n git(['branch', '--show-current'], projectPath),\n git(['status', '--porcelain'], projectPath),\n git(['rev-list', '--left-right', '--count', 'HEAD...@{upstream}'], projectPath),\n git(['stash', 'list'], projectPath),\n git(['log', '-1', '--format=%s|%cr'], projectPath),\n ]);\n\n const lines = porcelain ? porcelain.split('\\n') : [];\n const staged = lines.filter((l) => /^[MADRC]/.test(l)).length;\n const modified = lines.filter((l) => /^.[MD]/.test(l)).length;\n const untracked = lines.filter((l) => l.startsWith('??')).length;\n\n let ahead = 0;\n let behind = 0;\n if (revList) {\n const parts = revList.split(/\\s+/);\n ahead = parseInt(parts[0], 10) || 0;\n behind = parseInt(parts[1], 10) || 0;\n }\n\n const stashes = stashList ? stashList.split('\\n').filter(Boolean).length : 0;\n const [lastCommit, lastCommitTime] = logLine.split('|');\n\n return {\n branch: branch || 'unknown',\n modified,\n staged,\n untracked,\n ahead,\n behind,\n stashes,\n lastCommit: lastCommit || '',\n lastCommitTime: lastCommitTime || '',\n };\n}\n\nexport function useGitStatus(projectPath: string, pollInterval = 10000) {\n const [status, setStatus] = useState<GitStatus | null>(null);\n const intervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);\n\n useEffect(() => {\n let mounted = true;\n\n fetchGitStatus(projectPath).then((s) => {\n if (mounted) setStatus(s);\n });\n\n intervalRef.current = setInterval(() => {\n fetchGitStatus(projectPath).then((s) => {\n if (mounted) setStatus(s);\n });\n }, pollInterval);\n\n return () => {\n mounted = false;\n if (intervalRef.current) clearInterval(intervalRef.current);\n };\n }, [projectPath, pollInterval]);\n\n return status;\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { useGitStatus } from './hooks/useGitStatus.js';\n\ninterface GitPanelProps {\n projectPath: string;\n availableWidth?: number;\n}\n\nexport function GitPanel({ projectPath, availableWidth = 30 }: GitPanelProps) {\n const status = useGitStatus(projectPath);\n\n if (!status) {\n return (\n <Box>\n <Text dimColor>Loading git info...</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Text dimColor>Branch: </Text>\n <Text bold color=\"green\">\n {status.branch}\n </Text>\n </Box>\n {(status.ahead > 0 || status.behind > 0) && (\n <Box>\n {status.ahead > 0 && (\n <Text color=\"green\">\n {'\\u2191'}\n {status.ahead}{' '}\n </Text>\n )}\n {status.behind > 0 && (\n <Text color=\"red\">\n {'\\u2193'}\n {status.behind}\n </Text>\n )}\n </Box>\n )}\n <Box>\n <Text dimColor>Modified: </Text>\n <Text color={status.modified > 0 ? 'yellow' : 'green'}>{status.modified}</Text>\n </Box>\n <Box>\n <Text dimColor>Staged: </Text>\n <Text color={status.staged > 0 ? 'cyan' : 'green'}>{status.staged}</Text>\n </Box>\n <Box>\n <Text dimColor>Untracked: </Text>\n <Text>{status.untracked}</Text>\n </Box>\n {status.stashes > 0 && (\n <Box>\n <Text dimColor>Stashes: </Text>\n <Text>{status.stashes}</Text>\n </Box>\n )}\n {status.lastCommit && (\n <Box marginTop={1} flexDirection=\"column\">\n <Text dimColor>Last commit:</Text>\n <Text>\n {status.lastCommit.length > availableWidth\n ? status.lastCommit.slice(0, availableWidth - 3) + '...'\n : status.lastCommit}\n </Text>\n <Text dimColor>{status.lastCommitTime}</Text>\n </Box>\n )}\n </Box>\n );\n}\n","import React, { useState, useEffect } from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { LOADING_MESSAGES } from '../../lib/messages.js';\n\nimport type { CheckResult } from 'sickbay-core';\n\ninterface HealthPanelProps {\n checks: CheckResult[];\n isScanning: boolean;\n progress: Array<{ name: string; status: string }>;\n scrollOffset: number;\n availableHeight: number;\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nfunction scoreBar(score: number, width = 10): string {\n const filled = Math.round((score / 100) * width);\n return '\\u2588'.repeat(filled) + '\\u2591'.repeat(width - filled);\n}\n\nfunction truncate(str: string, maxLen: number): string {\n return str.length > maxLen ? str.slice(0, maxLen - 1) + '\\u2026' : str;\n}\n\nfunction statusIcon(status: string): string {\n switch (status) {\n case 'pass':\n return '\\u2713';\n case 'fail':\n return '\\u2717';\n case 'warning':\n return '\\u26A0';\n case 'skipped':\n return '\\u25CB';\n default:\n return '?';\n }\n}\n\nexport function HealthPanel({\n checks,\n isScanning,\n progress,\n scrollOffset,\n availableHeight,\n}: HealthPanelProps) {\n const [msgIdx, setMsgIdx] = useState(0);\n\n useEffect(() => {\n if (!isScanning || progress.length > 0) return;\n const id = setInterval(() => {\n setMsgIdx((i) => (i + 1) % LOADING_MESSAGES.length);\n }, 4000);\n return () => clearInterval(id);\n }, [isScanning, progress.length]);\n\n if (isScanning && checks.length === 0) {\n if (progress.length === 0) {\n return (\n <Box>\n <Text dimColor>{LOADING_MESSAGES[msgIdx]}</Text>\n </Box>\n );\n }\n return (\n <Box flexDirection=\"column\">\n {progress.map((p) => (\n <Box key={p.name}>\n <Text\n color={p.status === 'done' ? 'green' : p.status === 'running' ? 'yellow' : 'gray'}\n >\n {p.status === 'done' ? '\\u2713' : p.status === 'running' ? '\\u25CC' : '\\u25CB'}\n </Text>\n <Text> {p.name}</Text>\n </Box>\n ))}\n </Box>\n );\n }\n\n if (checks.length === 0) {\n return (\n <Box>\n <Text dimColor>No results yet. Press [r] to scan.</Text>\n </Box>\n );\n }\n\n const maxVisible = Math.max(1, availableHeight);\n const visible = checks.slice(scrollOffset, scrollOffset + maxVisible);\n const hasMore = scrollOffset + maxVisible < checks.length;\n const hasLess = scrollOffset > 0;\n\n return (\n <Box flexDirection=\"column\">\n {hasLess && <Text dimColor> \\u25B2 {scrollOffset} more above</Text>}\n {visible.map((check) => (\n <Box key={check.id}>\n <Text\n color={check.status === 'pass' ? 'green' : check.status === 'fail' ? 'red' : 'yellow'}\n >\n {statusIcon(check.status)}\n </Text>\n <Text> </Text>\n <Box width={26}>\n <Text wrap=\"truncate\">{truncate(check.name, 26)}</Text>\n </Box>\n <Text> </Text>\n <Text color={scoreColor(check.score)}>{scoreBar(check.score)}</Text>\n <Text color={scoreColor(check.score)}> {String(check.score).padStart(3)}</Text>\n </Box>\n ))}\n {hasMore && (\n <Text dimColor>\n {' \\u25BC '}\n {checks.length - scrollOffset - maxVisible} more below\n </Text>\n )}\n {isScanning && <Text color=\"yellow\">{' \\u25CC Re-scanning...'}</Text>}\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\ninterface HelpRow {\n keys: string[];\n description: string;\n}\n\ninterface HelpSection {\n title: string;\n rows: HelpRow[];\n}\n\nconst SECTIONS: HelpSection[] = [\n {\n title: 'PANELS',\n rows: [\n { keys: ['h'], description: 'Focus / unfocus Health Checks' },\n { keys: ['g'], description: 'Focus / unfocus Git Status' },\n { keys: ['t'], description: 'Focus / unfocus Trend' },\n { keys: ['q'], description: 'Focus / unfocus Quick Wins' },\n { keys: ['a'], description: 'Focus / unfocus Activity' },\n { keys: ['f'], description: 'Expand focused panel to full screen' },\n { keys: ['Esc'], description: 'Unfocus panel / exit full screen' },\n ],\n },\n {\n title: 'SCROLLING',\n rows: [{ keys: ['↑', '↓'], description: 'Scroll Health Checks (when focused)' }],\n },\n {\n title: 'ACTIONS',\n rows: [\n { keys: ['r'], description: 'Re-run scan' },\n { keys: ['w'], description: 'Open web dashboard in browser' },\n { keys: ['W'], description: 'Open web dashboard with AI insights' },\n { keys: ['?'], description: 'Toggle this help screen' },\n ],\n },\n];\n\nexport function HelpPanel() {\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1}>\n <Box marginBottom={1}>\n <Text bold color=\"cyan\">\n SICKBAY TUI — KEYBOARD SHORTCUTS\n </Text>\n </Box>\n\n {SECTIONS.map((section) => (\n <Box key={section.title} flexDirection=\"column\" marginBottom={1}>\n <Box marginBottom={0}>\n <Text bold color=\"white\">\n {section.title}\n </Text>\n </Box>\n {section.rows.map((row) => (\n <Box key={row.description} marginLeft={2}>\n <Box width={16}>\n {row.keys.map((k, i) => (\n <Text key={k}>\n <Text color=\"cyan\" bold>\n [{k}]\n </Text>\n {i < row.keys.length - 1 ? <Text dimColor> / </Text> : null}\n </Text>\n ))}\n </Box>\n <Text dimColor>{row.description}</Text>\n </Box>\n ))}\n </Box>\n ))}\n\n <Box marginTop={1} borderStyle=\"single\" borderColor=\"gray\" paddingX={1}>\n <Text dimColor>Pro tip: focus a panel with its hotkey, then press </Text>\n <Text color=\"cyan\" bold>\n [f]\n </Text>\n <Text dimColor> to expand it full screen for more detail.</Text>\n </Box>\n\n <Box marginTop={1}>\n <Text dimColor>Press </Text>\n <Text color=\"cyan\" bold>\n [?]\n </Text>\n <Text dimColor> or </Text>\n <Text color=\"cyan\" bold>\n [Esc]\n </Text>\n <Text dimColor> to close</Text>\n </Box>\n </Box>\n );\n}\n","import { useState, useEffect, useRef } from 'react';\n\nimport type { FSWatcher } from 'chokidar';\n\ninterface UseFileWatcherOptions {\n projectPath: string;\n enabled: boolean;\n debounceMs?: number;\n onFilesChanged?: (files: string[]) => void;\n}\n\nexport function useFileWatcher({\n projectPath,\n enabled,\n debounceMs = 2000,\n onFilesChanged,\n}: UseFileWatcherOptions) {\n const [changedFiles, setChangedFiles] = useState<string[]>([]);\n const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const pendingRef = useRef<string[]>([]);\n const watcherRef = useRef<FSWatcher | null>(null);\n // Keep a ref so the debounce callback always calls the latest version,\n // even if the parent re-renders and passes a new function reference.\n const onFilesChangedRef = useRef(onFilesChanged);\n onFilesChangedRef.current = onFilesChanged;\n\n useEffect(() => {\n if (!enabled) return;\n\n let mounted = true;\n\n (async () => {\n try {\n const { watch } = await import('chokidar');\n if (!mounted) return;\n\n const watcher = watch(['**/*.{ts,tsx,js,jsx,json}'], {\n cwd: projectPath,\n ignored: [\n '**/node_modules/**',\n '**/dist/**',\n '**/.git/**',\n '**/build/**',\n '**/.next/**',\n '**/.turbo/**',\n '**/coverage/**',\n ],\n ignoreInitial: true,\n persistent: true,\n });\n\n watcherRef.current = watcher;\n\n watcher.on('change', (path: string) => {\n pendingRef.current.push(path);\n setChangedFiles((prev) => [...prev, path]);\n\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => {\n const files = [...pendingRef.current];\n pendingRef.current = [];\n onFilesChangedRef.current?.(files);\n }, debounceMs);\n });\n } catch {\n // chokidar not available — silently skip\n }\n })();\n\n return () => {\n mounted = false;\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (watcherRef.current) watcherRef.current.close();\n };\n }, [projectPath, enabled, debounceMs]);\n\n return changedFiles;\n}\n","import { useState, useCallback, useRef } from 'react';\n\nimport { runSickbay, runSickbayMonorepo, detectMonorepo, buildSummary } from 'sickbay-core';\n\nimport type { SickbayReport, MonorepoReport, CheckResult } from 'sickbay-core';\n\ninterface ProgressItem {\n name: string;\n status: 'pending' | 'running' | 'done';\n}\n\ninterface UseSickbayRunnerOptions {\n projectPath: string;\n checks?: string[];\n quotes?: boolean;\n}\n\n/**\n * Synthesize a rolled-up SickbayReport from a MonorepoReport for use in\n * TUI panels that only understand single-project reports. Strategy: merge\n * all checks from all packages, keeping the worst score per check name.\n */\nfunction rollUpMonorepoReport(monorepo: MonorepoReport): SickbayReport {\n const byId = new Map<string, CheckResult>();\n for (const pkg of monorepo.packages) {\n for (const check of pkg.checks) {\n const existing = byId.get(check.id);\n if (!existing || check.score < existing.score) {\n byId.set(check.id, check);\n }\n }\n }\n const checks = Array.from(byId.values());\n return {\n timestamp: monorepo.timestamp,\n projectPath: monorepo.rootPath,\n projectInfo: {\n name: `monorepo (${monorepo.packages.length} packages)`,\n version: '0.0.0',\n hasTypeScript: false,\n hasESLint: false,\n hasPrettier: false,\n framework: 'node',\n packageManager: monorepo.packageManager,\n totalDependencies: 0,\n dependencies: {},\n devDependencies: {},\n },\n checks,\n overallScore: monorepo.overallScore,\n summary: buildSummary(checks),\n };\n}\n\nexport function useSickbayRunner({ projectPath, checks, quotes }: UseSickbayRunnerOptions) {\n const [report, setReport] = useState<SickbayReport | null>(null);\n const [monorepoReport, setMonorepoReport] = useState<MonorepoReport | null>(null);\n const [isScanning, setIsScanning] = useState(false);\n const [progress, setProgress] = useState<ProgressItem[]>([]);\n const [error, setError] = useState<string | null>(null);\n const scanningRef = useRef(false);\n\n const scan = useCallback(async () => {\n if (scanningRef.current) return null;\n scanningRef.current = true;\n setIsScanning(true);\n setError(null);\n\n try {\n const monorepoInfo = await detectMonorepo(projectPath);\n\n if (monorepoInfo.isMonorepo) {\n const result = await runSickbayMonorepo({\n projectPath,\n checks,\n quotes,\n onPackageStart: (name) => {\n setProgress((prev) => [...prev, { name, status: 'running' }]);\n },\n onPackageComplete: (pkg) => {\n setProgress((prev) =>\n prev.map((p) => (p.name === pkg.name ? { ...p, status: 'done' } : p)),\n );\n },\n });\n\n setMonorepoReport(result);\n const rolledUp = rollUpMonorepoReport(result);\n setReport(rolledUp);\n\n setIsScanning(false);\n scanningRef.current = false;\n return rolledUp;\n }\n\n const result = await runSickbay({\n projectPath,\n checks,\n quotes,\n onRunnersReady: (names) => {\n setProgress(names.map((name) => ({ name, status: 'pending' as const })));\n },\n onCheckStart: (name) => {\n setProgress((prev) =>\n prev.map((p) => (p.name === name ? { ...p, status: 'running' } : p)),\n );\n },\n onCheckComplete: (check) => {\n setProgress((prev) =>\n prev.map((p) => (p.name === check.id ? { ...p, status: 'done' } : p)),\n );\n },\n });\n\n setMonorepoReport(null);\n setReport(result);\n\n // Save to trend history\n try {\n const { saveEntry } = await import('../../../lib/history.js');\n saveEntry(result);\n } catch {\n // Non-critical\n }\n\n setIsScanning(false);\n scanningRef.current = false;\n return result;\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n setIsScanning(false);\n scanningRef.current = false;\n return null;\n }\n }, [projectPath, checks, quotes]);\n\n return { report, monorepoReport, isScanning, progress, error, scan };\n}\n","import { useState, useEffect } from 'react';\n\nexport function useTerminalSize() {\n const [size, setSize] = useState({\n columns: process.stdout.columns || 80,\n rows: process.stdout.rows || 24,\n });\n\n useEffect(() => {\n const handler = () => {\n setSize({\n columns: process.stdout.columns || 80,\n rows: process.stdout.rows || 24,\n });\n };\n process.stdout.on('resize', handler);\n return () => {\n process.stdout.off('resize', handler);\n };\n }, []);\n\n return size;\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nexport type PanelId = 'health' | 'git' | 'trend' | 'quickwins' | 'activity';\n\ninterface HotkeyBarProps {\n activePanel: PanelId | null;\n}\n\nconst HOTKEYS: Array<{ key: string; label: string; panel?: PanelId }> = [\n { key: 'h', label: 'health', panel: 'health' },\n { key: 'g', label: 'git', panel: 'git' },\n { key: 't', label: 'trend', panel: 'trend' },\n { key: 'q', label: 'quick wins', panel: 'quickwins' },\n { key: 'a', label: 'activity', panel: 'activity' },\n { key: 'r', label: 're-run' },\n { key: 'w', label: 'web' },\n { key: 'W', label: 'web+AI' },\n { key: '?', label: 'help' },\n];\n\nexport function HotkeyBar({ activePanel }: HotkeyBarProps) {\n return (\n <Box>\n {HOTKEYS.map((hk) => {\n const isActive = hk.panel !== undefined && hk.panel === activePanel;\n return (\n <Box key={hk.key} marginRight={2}>\n <Text bold={isActive} color={isActive ? 'cyan' : undefined}>\n [{hk.key}]\n </Text>\n <Text bold={isActive} color={isActive ? 'cyan' : 'gray'}>\n {' '}\n {hk.label}\n </Text>\n </Box>\n );\n })}\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport type { MonorepoReport, PackageReport } from 'sickbay-core';\n\ninterface MonorepoPanelProps {\n report: MonorepoReport | null;\n availableWidth?: number;\n}\n\nfunction scoreBar(score: number, width = 8): string {\n const filled = Math.round((score / 100) * width);\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nexport function MonorepoPanel({ report, availableWidth }: MonorepoPanelProps) {\n if (!report) {\n return (\n <Box>\n <Text dimColor>Waiting for scan...</Text>\n </Box>\n );\n }\n\n const nameWidth = Math.max(12, Math.min(20, (availableWidth ?? 36) - 22));\n\n return (\n <Box flexDirection=\"column\">\n <Box marginBottom={1}>\n <Text bold>{report.packages.length} packages</Text>\n <Text dimColor> · </Text>\n <Text dimColor>{report.monorepoType} workspaces</Text>\n <Text dimColor> · </Text>\n <Text color={scoreColor(report.overallScore)} bold>\n {report.overallScore}\n </Text>\n <Text dimColor> avg</Text>\n </Box>\n\n {report.packages.map((pkg: PackageReport) => {\n const name = pkg.name.includes('/') ? (pkg.name.split('/').pop() ?? pkg.name) : pkg.name;\n const truncatedName =\n name.length > nameWidth\n ? name.slice(0, nameWidth - 1) + '\\u2026'\n : name.padEnd(nameWidth);\n\n return (\n <Box key={pkg.path} gap={1}>\n <Text color={scoreColor(pkg.score)}>{scoreBar(pkg.score)}</Text>\n <Text color={scoreColor(pkg.score)} bold>\n {String(pkg.score).padStart(3)}\n </Text>\n <Text>{truncatedName}</Text>\n {pkg.summary.critical > 0 && <Text color=\"red\"> ⚠</Text>}\n </Box>\n );\n })}\n\n <Box marginTop={1}>\n <Text dimColor>Press </Text>\n <Text color=\"cyan\">[w]</Text>\n <Text dimColor> for per-package web view</Text>\n </Box>\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\ninterface PanelBorderProps {\n title: string;\n color: string;\n focused?: boolean;\n visible?: boolean;\n flash?: 'green' | 'red';\n children: React.ReactNode;\n}\n\nexport function PanelBorder({\n title,\n color,\n focused,\n visible = true,\n flash,\n children,\n}: PanelBorderProps) {\n const borderColor = flash ?? (focused ? color : 'gray');\n const borderStyle = flash ? 'double' : focused ? 'double' : 'single';\n\n return (\n <Box\n flexDirection=\"column\"\n borderStyle={borderStyle}\n borderColor={borderColor}\n paddingX={1}\n flexGrow={1}\n >\n <Text bold color={flash ?? color}>\n {title}\n </Text>\n {visible ? children : <Text dimColor>···</Text>}\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport type { SickbayReport } from 'sickbay-core';\n\ninterface QuickWinsPanelProps {\n report: SickbayReport | null;\n availableWidth?: number;\n}\n\nfunction replacePackageManager(cmd: string, pm: string): string {\n if (pm === 'npm') return cmd;\n const install = pm === 'pnpm' ? 'pnpm add' : pm === 'yarn' ? 'yarn add' : 'bun add';\n const uninstall = pm === 'pnpm' ? 'pnpm remove' : pm === 'yarn' ? 'yarn remove' : 'bun remove';\n const update = pm === 'pnpm' ? 'pnpm update' : pm === 'yarn' ? 'yarn upgrade' : 'bun update';\n const auditFix =\n pm === 'pnpm' ? 'pnpm audit --fix' : pm === 'yarn' ? 'yarn npm audit --fix' : 'bun audit';\n return cmd\n .replace(/^npm install(?=\\s)/, install)\n .replace(/^npm uninstall(?=\\s)/, uninstall)\n .replace(/^npm update(?=\\s)/, update)\n .replace(/^npm audit fix/, auditFix);\n}\n\nfunction shortenPath(filepath: string): string {\n const parts = filepath.split('/');\n if (parts.length <= 2) return filepath;\n return '\\u2026/' + parts.slice(-2).join('/');\n}\n\nfunction smartTruncate(str: string, maxLen: number): string {\n if (str.length <= maxLen) return str;\n // If it contains a file path, shorten the path but keep the filename\n const pathMatch = str.match(/(\\S*\\/\\S+)/);\n if (pathMatch) {\n const shortened = str.replace(pathMatch[1], shortenPath(pathMatch[1]));\n if (shortened.length <= maxLen) return shortened;\n return shortened.slice(0, maxLen - 1) + '\\u2026';\n }\n return str.slice(0, maxLen - 1) + '\\u2026';\n}\n\nfunction shortenCommand(cmd: string, maxLen: number): string {\n if (cmd.length <= maxLen) return cmd;\n // Shorten any file paths in the command first\n const shortened = cmd.replace(/(\\S*\\/\\S+)/g, (match) => shortenPath(match));\n if (shortened.length <= maxLen) return shortened;\n return shortened.slice(0, maxLen - 1) + '\\u2026';\n}\n\nexport function QuickWinsPanel({ report, availableWidth }: QuickWinsPanelProps) {\n if (!report) {\n return (\n <Box>\n <Text dimColor>Waiting for scan...</Text>\n </Box>\n );\n }\n\n const fixes: Array<{\n description: string;\n command: string;\n severity: string;\n }> = [];\n\n for (const check of report.checks) {\n for (const issue of check.issues) {\n if (issue.fix?.command) {\n fixes.push({\n description: issue.fix.description,\n command: issue.fix.command,\n severity: issue.severity,\n });\n }\n }\n }\n\n const order: Record<string, number> = { critical: 0, warning: 1, info: 2 };\n fixes.sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3));\n const top = fixes.slice(0, 5);\n\n if (top.length === 0) {\n return (\n <Box>\n <Text color=\"green\">Looking good!</Text>\n </Box>\n );\n }\n\n const pm = report.projectInfo?.packageManager ?? 'npm';\n const maxTextLen = Math.max(16, (availableWidth ?? 26) - 4);\n\n return (\n <Box flexDirection=\"column\">\n {top.map((fix, i) => (\n <Box key={i} flexDirection=\"column\" marginBottom={i < top.length - 1 ? 1 : 0}>\n <Text>\n <Text\n color={\n fix.severity === 'critical' ? 'red' : fix.severity === 'warning' ? 'yellow' : 'gray'\n }\n >\n {'\\u2192'}{' '}\n </Text>\n {smartTruncate(fix.description, maxTextLen)}\n </Text>\n <Text dimColor>\n {' '}\n {shortenCommand(replacePackageManager(fix.command, pm), maxTextLen)}\n </Text>\n </Box>\n ))}\n </Box>\n );\n}\n","import React, { useState, useEffect, useRef } from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { countUniqueIssues } from '../../lib/issue-grouping.js';\n\nimport type { SickbayReport } from 'sickbay-core';\n\ninterface ScorePanelProps {\n report: SickbayReport | null;\n previousScore: number | null;\n animate?: boolean;\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nfunction scoreBar(score: number, width = 15): string {\n const filled = Math.round((score / 100) * width);\n return '\\u2588'.repeat(filled) + '\\u2591'.repeat(width - filled);\n}\n\nexport function ScorePanel({ report, previousScore, animate = true }: ScorePanelProps) {\n const [animatedScore, setAnimatedScore] = useState(0);\n const prevTargetRef = useRef(0);\n\n useEffect(() => {\n if (!animate) return;\n\n if (!report) {\n setAnimatedScore(0);\n prevTargetRef.current = 0;\n return;\n }\n\n const target = report.overallScore;\n const start = prevTargetRef.current;\n prevTargetRef.current = target;\n\n if (start === target) return;\n\n let current = start;\n const step = target > start ? 1 : -1;\n\n const id = setInterval(() => {\n current += step;\n setAnimatedScore(current);\n if (current === target) clearInterval(id);\n }, 20);\n\n return () => clearInterval(id);\n }, [report, animate]);\n\n // When animate=false, render score directly from prop (no state update needed)\n const displayScore = animate ? animatedScore : (report?.overallScore ?? 0);\n\n if (!report) {\n return (\n <Box>\n <Text dimColor>Waiting for scan...</Text>\n </Box>\n );\n }\n\n const delta = previousScore !== null ? report.overallScore - previousScore : null;\n const u = countUniqueIssues(report.checks);\n\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Text color={scoreColor(displayScore)} bold>\n {displayScore}/100\n </Text>\n <Text> </Text>\n <Text color={scoreColor(displayScore)}>{scoreBar(displayScore)}</Text>\n </Box>\n {delta !== null && (\n <Text dimColor>\n {delta > 0 ? `+${delta}` : delta === 0 ? '\\u00B10' : `${delta}`} since last scan\n </Text>\n )}\n <Box marginTop={1} flexDirection=\"column\">\n <Box>\n <Text color=\"red\">\n {'\\u2717'} {u.critical} critical\n {u.totalCritical > u.critical ? ` (${u.totalCritical})` : ''}\n </Text>\n <Text>{' '}</Text>\n <Text color=\"yellow\">\n {'\\u26A0'} {u.warnings} warn\n {u.totalWarnings > u.warnings ? ` (${u.totalWarnings})` : ''}\n </Text>\n <Text>{' '}</Text>\n <Text dimColor>i {u.info} info</Text>\n </Box>\n </Box>\n {report.quote && (\n <Box marginTop={1}>\n <Text italic dimColor>\n \"{report.quote.text}\" — {report.quote.source}\n </Text>\n </Box>\n )}\n {report.config?.hasCustomConfig && (\n <Text dimColor italic>\n Custom config\n </Text>\n )}\n </Box>\n );\n}\n","import React, { useState, useEffect } from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { sparkline, trendArrow } from '../../commands/trend.js';\nimport { loadHistory } from '../../lib/history.js';\n\ninterface TrendPanelProps {\n projectPath: string;\n lastScanTime: Date | null;\n availableHeight?: number;\n}\n\nconst CATEGORY_COLORS: Record<string, string> = {\n overall: 'white',\n dependencies: 'blue',\n security: 'green',\n 'code-quality': 'yellow',\n performance: 'magenta',\n git: 'cyan',\n};\n\nconst SHORT_LABELS: Record<string, string> = {\n dependencies: 'Deps',\n security: 'Security',\n 'code-quality': 'Quality',\n performance: 'Perf',\n git: 'Git',\n};\n\nexport function TrendPanel({ projectPath, lastScanTime, availableHeight }: TrendPanelProps) {\n const [trends, setTrends] = useState<\n Array<{\n label: string;\n spark: string;\n latest: number;\n arrow: string;\n color: string;\n }>\n >([]);\n\n useEffect(() => {\n const history = loadHistory(projectPath);\n if (!history || history.entries.length === 0) {\n setTrends([]);\n return;\n }\n\n const entries = history.entries.slice(-10);\n const overallValues = entries.map((e) => e.overallScore);\n const overallArrow = trendArrow(overallValues);\n\n const result: typeof trends = [\n {\n label: 'Overall',\n spark: sparkline(overallValues),\n latest: overallValues[overallValues.length - 1],\n arrow: overallArrow.label,\n color: 'white',\n },\n ];\n\n const categories = Object.keys(entries[entries.length - 1].categoryScores);\n for (const cat of categories) {\n const values = entries\n .map((e) => e.categoryScores[cat])\n .filter((v): v is number => v !== undefined);\n\n if (values.length === 0) continue;\n\n const arrow = trendArrow(values);\n result.push({\n label: SHORT_LABELS[cat] || cat.charAt(0).toUpperCase() + cat.slice(1),\n spark: sparkline(values),\n latest: values[values.length - 1],\n arrow: arrow.label,\n color: CATEGORY_COLORS[cat] || 'white',\n });\n }\n\n setTrends(result);\n }, [projectPath, lastScanTime]);\n\n if (trends.length === 0) {\n return (\n <Box>\n <Text dimColor>No trend data yet.</Text>\n </Box>\n );\n }\n\n const maxRows = availableHeight ?? trends.length;\n const visible = trends.slice(0, maxRows);\n\n return (\n <Box flexDirection=\"column\">\n {visible.map((t) => (\n <Box key={t.label}>\n <Box width={10}>\n <Text color={t.color}>{t.label}</Text>\n </Box>\n <Text>{t.spark}</Text>\n <Text bold> {String(t.latest).padStart(3)}</Text>\n <Text> {t.arrow}</Text>\n </Box>\n ))}\n </Box>\n );\n}\n","import React, { useState, useEffect, useCallback, useRef } from 'react';\n\nimport { Box, Text, useInput } from 'ink';\n\nimport { checkForUpdate } from '../../lib/update-check.js';\nimport { UpdateNotice } from '../UpdateNotice.js';\nimport { ActivityPanel, type ActivityEntry } from './ActivityPanel.js';\nimport { GitPanel } from './GitPanel.js';\nimport { HealthPanel } from './HealthPanel.js';\nimport { HelpPanel } from './HelpPanel.js';\nimport { useFileWatcher } from './hooks/useFileWatcher.js';\nimport { useSickbayRunner } from './hooks/useSickbayRunner.js';\nimport { useTerminalSize } from './hooks/useTerminalSize.js';\nimport { HotkeyBar, type PanelId } from './HotkeyBar.js';\nimport { MonorepoPanel } from './MonorepoPanel.js';\nimport { PanelBorder } from './PanelBorder.js';\nimport { QuickWinsPanel } from './QuickWinsPanel.js';\nimport { ScorePanel } from './ScorePanel.js';\nimport { TrendPanel } from './TrendPanel.js';\n\nimport type { UpdateInfo } from '../../lib/update-check.js';\nimport type { SickbayReport, MonorepoReport } from 'sickbay-core';\n\ndeclare const __VERSION__: string;\n\ninterface TuiAppProps {\n projectPath: string;\n checks?: string[];\n watchEnabled: boolean;\n refreshInterval: number;\n quotes?: boolean;\n animateOnMount?: boolean;\n}\n\nexport function TuiApp({\n projectPath,\n checks,\n watchEnabled,\n refreshInterval,\n quotes,\n animateOnMount = true,\n}: TuiAppProps) {\n const { rows, columns } = useTerminalSize();\n const { report, monorepoReport, isScanning, progress, scan } = useSickbayRunner({\n projectPath,\n checks,\n quotes,\n });\n\n const [focusedPanel, setFocusedPanel] = useState<PanelId | null>(null);\n const [expandedPanel, setExpandedPanel] = useState<PanelId | null>(null);\n const [showHelp, setShowHelp] = useState(false);\n const [lastScanTime, setLastScanTime] = useState<Date | null>(null);\n const [previousScore, setPreviousScore] = useState<number | null>(null);\n const [activityLog, setActivityLog] = useState<ActivityEntry[]>([]);\n const [healthScrollOffset, setHealthScrollOffset] = useState(0);\n const [scoreFlash, setScoreFlash] = useState<'green' | 'red' | undefined>();\n const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);\n const ALL_PANELS = new Set(['health', 'score', 'trend', 'git', 'quickwins', 'activity']);\n const [visiblePanels, setVisiblePanels] = useState<Set<string>>(\n animateOnMount ? new Set() : ALL_PANELS,\n );\n const refreshRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);\n\n // Panel entrance animation — stagger panels appearing on mount\n useEffect(() => {\n const schedule: Array<[string, number]> = [\n ['health', 100],\n ['score', 300],\n ['trend', 500],\n ['git', 700],\n ['quickwins', 900],\n ['activity', 1100],\n ];\n const timers = schedule.map(([panel, delay]) =>\n setTimeout(() => {\n setVisiblePanels((prev) => new Set([...prev, panel]));\n }, delay),\n );\n return () => timers.forEach(clearTimeout);\n }, []);\n\n useEffect(() => {\n checkForUpdate(__VERSION__).then((info) => {\n if (info) setUpdateInfo(info);\n });\n }, []);\n\n const reportRef = useRef<SickbayReport | null>(null);\n const monorepoReportRef = useRef<MonorepoReport | null>(null);\n\n // Keep refs in sync\n useEffect(() => {\n reportRef.current = report;\n }, [report]);\n\n useEffect(() => {\n monorepoReportRef.current = monorepoReport;\n }, [monorepoReport]);\n\n const addActivity = useCallback((type: ActivityEntry['type'], message: string) => {\n setActivityLog((prev) => [...prev, { timestamp: new Date(), type, message }]);\n }, []);\n\n const handleScanComplete = useCallback(\n async (result: SickbayReport) => {\n const prevScore = reportRef.current?.overallScore ?? null;\n if (prevScore !== null) setPreviousScore(prevScore);\n setLastScanTime(new Date());\n\n // Auto-save last report snapshot\n try {\n const { saveLastReport } = await import('../../lib/history.js');\n saveLastReport(result);\n } catch {\n // Non-critical\n }\n\n // Cache dependency tree for web dashboard\n try {\n const { getDependencyTree } = await import('sickbay-core');\n const { saveDepTree } = await import('../../lib/history.js');\n const tree = await getDependencyTree(projectPath, result.projectInfo.packageManager);\n saveDepTree(projectPath, tree);\n } catch {\n /* dep tree is optional */\n }\n\n const delta = prevScore !== null ? result.overallScore - prevScore : null;\n addActivity(\n 'scan-complete',\n `Scan complete: ${result.overallScore}/100${delta !== null ? ` (${delta >= 0 ? '+' : ''}${delta})` : ''}`,\n );\n\n // Flash score panel border on score change\n if (delta !== null && delta !== 0) {\n const flash = delta > 0 ? 'green' : 'red';\n setScoreFlash(flash);\n setTimeout(() => setScoreFlash(undefined), 600);\n }\n\n // Check for regressions\n try {\n const { loadHistory, detectRegressions } = await import('../../lib/history.js');\n const history = loadHistory(projectPath);\n if (history) {\n const regressions = detectRegressions(history.entries);\n for (const reg of regressions) {\n addActivity(\n 'regression',\n `\\u26A0 ${reg.category} regressed: ${reg.from} \\u2192 ${reg.to}`,\n );\n }\n }\n } catch {\n // Non-critical\n }\n },\n [addActivity, projectPath],\n );\n\n // Initial scan\n useEffect(() => {\n addActivity('info', 'TUI started');\n addActivity('scan-start', 'Starting initial health scan...');\n scan().then((result) => {\n if (result) handleScanComplete(result);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Auto-refresh timer\n useEffect(() => {\n if (refreshInterval <= 0) return;\n refreshRef.current = setInterval(async () => {\n addActivity('scan-start', 'Auto-scan triggered');\n const result = await scan();\n if (result) handleScanComplete(result);\n }, refreshInterval * 1000);\n\n return () => {\n if (refreshRef.current) clearInterval(refreshRef.current);\n };\n }, [refreshInterval, scan, handleScanComplete, addActivity]);\n\n // File watcher\n useFileWatcher({\n projectPath,\n enabled: watchEnabled,\n onFilesChanged: async (files) => {\n for (const f of files.slice(0, 3)) {\n addActivity('file-change', `File changed: ${f}`);\n }\n if (files.length > 3) {\n addActivity('file-change', `...and ${files.length - 3} more files`);\n }\n addActivity('scan-start', 'Re-scan triggered (file change)');\n const result = await scan();\n if (result) handleScanComplete(result);\n },\n });\n\n // Manual re-scan\n const triggerRescan = useCallback(async () => {\n addActivity('scan-start', 'Manual re-scan triggered');\n const result = await scan();\n if (result) handleScanComplete(result);\n }, [addActivity, scan, handleScanComplete]);\n\n // Keyboard input — only active when stdin supports raw mode (interactive TTY)\n const isTTY = !!(process.stdin.isTTY && process.stdin.setRawMode);\n useInput(\n (input, key) => {\n // Help overlay: ? toggles, Escape closes\n if (showHelp) {\n if (input === '?' || key.escape) {\n setShowHelp(false);\n }\n return;\n }\n\n // Expanded panel: escape or f to exit\n if (expandedPanel) {\n if (key.escape || input === 'f') {\n setExpandedPanel(null);\n return;\n }\n }\n\n if (input === '?') {\n setShowHelp(true);\n return;\n }\n\n // Panel focus toggles\n if (input === 'h') setFocusedPanel((prev) => (prev === 'health' ? null : 'health'));\n else if (input === 'g') setFocusedPanel((prev) => (prev === 'git' ? null : 'git'));\n else if (input === 't') setFocusedPanel((prev) => (prev === 'trend' ? null : 'trend'));\n else if (input === 'q')\n setFocusedPanel((prev) => (prev === 'quickwins' ? null : 'quickwins'));\n else if (input === 'a') setFocusedPanel((prev) => (prev === 'activity' ? null : 'activity'));\n else if (input === 'r') triggerRescan();\n else if (input === 'f' && focusedPanel) setExpandedPanel(focusedPanel);\n else if (input === 'w' || input === 'W') {\n const withAI = input === 'W';\n (async () => {\n const webReport = monorepoReportRef.current ?? reportRef.current;\n if (!webReport) return;\n try {\n const { serveWeb } = await import('../../commands/web.js');\n const { default: openBrowser } = await import('open');\n\n let aiService;\n if (withAI && process.env.ANTHROPIC_API_KEY && !monorepoReportRef.current) {\n const { createAIService } = await import('../../services/ai.js');\n aiService = createAIService(process.env.ANTHROPIC_API_KEY);\n addActivity('info', 'Launching web dashboard with AI...');\n } else {\n addActivity('info', 'Launching web dashboard...');\n }\n\n const url = await serveWeb(webReport, 3030, aiService);\n await openBrowser(url);\n addActivity(\n 'info',\n `Web dashboard at ${url}${withAI && aiService ? ' (AI enabled)' : ''}`,\n );\n } catch (err) {\n addActivity('info', `Failed to open web: ${err instanceof Error ? err.message : err}`);\n }\n })();\n } else if (key.escape) {\n setFocusedPanel(null);\n }\n\n // Scrolling in focused health panel\n if (focusedPanel === 'health') {\n if (key.upArrow) setHealthScrollOffset((prev) => Math.max(0, prev - 1));\n if (key.downArrow) {\n const maxOffset = Math.max(0, visibleChecks.length - 5);\n setHealthScrollOffset((prev) => Math.min(maxOffset, prev + 1));\n }\n }\n },\n { isActive: isTTY },\n );\n\n const visibleChecks = report?.checks.filter((c) => c.status !== 'skipped') ?? [];\n\n // Layout calculations — reserve 1 row for project header + 1 for hotkey bar\n const topHeight = Math.floor((rows - 2) * 0.6);\n const bottomHeight = rows - 2 - topHeight;\n\n // Help overlay\n if (showHelp) {\n return (\n <Box flexDirection=\"column\" width={columns} height={rows}>\n <Box flexGrow={1}>\n <PanelBorder title=\"HELP\" color=\"cyan\" focused>\n <HelpPanel />\n </PanelBorder>\n </Box>\n <HotkeyBar activePanel={focusedPanel} />\n </Box>\n );\n }\n\n // Full-screen expand mode\n if (expandedPanel) {\n return (\n <Box flexDirection=\"column\" width={columns} height={rows}>\n <Box flexGrow={1}>\n {expandedPanel === 'health' && (\n <PanelBorder title=\"HEALTH CHECKS\" color=\"green\" focused>\n <HealthPanel\n checks={visibleChecks}\n isScanning={isScanning}\n progress={progress}\n scrollOffset={healthScrollOffset}\n availableHeight={rows - 4}\n />\n </PanelBorder>\n )}\n {expandedPanel === 'git' && (\n <PanelBorder title=\"GIT STATUS\" color=\"yellow\" focused>\n <GitPanel projectPath={projectPath} availableWidth={columns - 4} />\n </PanelBorder>\n )}\n {expandedPanel === 'trend' && (\n <PanelBorder title=\"TREND\" color=\"magenta\" focused>\n <TrendPanel projectPath={projectPath} lastScanTime={lastScanTime} />\n </PanelBorder>\n )}\n {expandedPanel === 'quickwins' && (\n <PanelBorder title={monorepoReport ? 'MONOREPO' : 'QUICK WINS'} color=\"red\" focused>\n {monorepoReport ? (\n <MonorepoPanel report={monorepoReport} />\n ) : (\n <QuickWinsPanel report={report} />\n )}\n </PanelBorder>\n )}\n {expandedPanel === 'activity' && (\n <PanelBorder title=\"ACTIVITY\" color=\"cyan\" focused>\n <ActivityPanel entries={activityLog} availableHeight={rows - 4} />\n </PanelBorder>\n )}\n </Box>\n <HotkeyBar activePanel={expandedPanel} />\n </Box>\n );\n }\n\n // Normal grid layout\n const projectName = monorepoReport\n ? `${monorepoReport.rootPath.split('/').pop()} (monorepo)`\n : (report?.projectInfo?.name ?? '—');\n const projectVersion = report?.projectInfo?.version;\n const scanLabel = lastScanTime\n ? `Last scan ${lastScanTime.toLocaleTimeString()}`\n : isScanning\n ? 'Scanning…'\n : 'Not yet scanned';\n\n return (\n <Box flexDirection=\"column\" width={columns} height={rows}>\n {updateInfo && (\n <UpdateNotice\n currentVersion={updateInfo.currentVersion}\n latestVersion={updateInfo.latestVersion}\n />\n )}\n {/* Project header */}\n <Box paddingX={1} justifyContent=\"space-between\">\n <Box gap={1}>\n <Text bold color=\"cyan\">\n SICKBAY\n </Text>\n <Text bold>{projectName}</Text>\n {projectVersion && <Text dimColor>v{projectVersion}</Text>}\n </Box>\n <Text dimColor>{scanLabel}</Text>\n </Box>\n\n {/* Top Row */}\n <Box height={topHeight}>\n <Box width=\"55%\">\n <PanelBorder\n title=\"HEALTH CHECKS\"\n color=\"green\"\n focused={focusedPanel === 'health'}\n visible={visiblePanels.has('health')}\n >\n <HealthPanel\n checks={visibleChecks}\n isScanning={isScanning}\n progress={progress}\n scrollOffset={healthScrollOffset}\n availableHeight={topHeight - 4}\n />\n </PanelBorder>\n </Box>\n <Box width=\"45%\" flexDirection=\"column\">\n <Box height=\"50%\">\n <PanelBorder\n title=\"SCORE\"\n color=\"blue\"\n visible={visiblePanels.has('score')}\n flash={scoreFlash}\n >\n <ScorePanel report={report} previousScore={previousScore} animate={animateOnMount} />\n </PanelBorder>\n </Box>\n <Box height=\"50%\">\n <PanelBorder\n title=\"TREND\"\n color=\"magenta\"\n focused={focusedPanel === 'trend'}\n visible={visiblePanels.has('trend')}\n >\n <TrendPanel\n projectPath={projectPath}\n lastScanTime={lastScanTime}\n availableHeight={Math.floor(topHeight / 2) - 3}\n />\n </PanelBorder>\n </Box>\n </Box>\n </Box>\n\n {/* Bottom Row */}\n <Box height={bottomHeight}>\n <Box width=\"25%\">\n <PanelBorder\n title=\"GIT STATUS\"\n color=\"yellow\"\n focused={focusedPanel === 'git'}\n visible={visiblePanels.has('git')}\n >\n <GitPanel projectPath={projectPath} availableWidth={Math.floor(columns * 0.25) - 4} />\n </PanelBorder>\n </Box>\n <Box width=\"30%\">\n <PanelBorder\n title={monorepoReport ? 'MONOREPO' : 'QUICK WINS'}\n color=\"red\"\n focused={focusedPanel === 'quickwins'}\n visible={visiblePanels.has('quickwins')}\n >\n {monorepoReport ? (\n <MonorepoPanel\n report={monorepoReport}\n availableWidth={Math.floor(columns * 0.3) - 4}\n />\n ) : (\n <QuickWinsPanel report={report} availableWidth={Math.floor(columns * 0.3) - 4} />\n )}\n </PanelBorder>\n </Box>\n <Box width=\"45%\">\n <PanelBorder\n title=\"ACTIVITY\"\n color=\"cyan\"\n focused={focusedPanel === 'activity'}\n visible={visiblePanels.has('activity')}\n >\n <ActivityPanel entries={activityLog} availableHeight={bottomHeight - 4} />\n </PanelBorder>\n </Box>\n </Box>\n\n {/* Hotkey Bar */}\n <HotkeyBar activePanel={focusedPanel} />\n </Box>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TuiApp-LHb149v8.js","names":["scoreColor","scoreBar","scoreBar","scoreColor"],"sources":["../src/components/tui/ActivityPanel.tsx","../src/components/tui/hooks/useGitStatus.ts","../src/components/tui/GitPanel.tsx","../src/components/tui/HealthPanel.tsx","../src/components/tui/HelpPanel.tsx","../src/components/tui/hooks/useFileWatcher.ts","../src/components/tui/hooks/useSickbayRunner.ts","../src/components/tui/hooks/useTerminalSize.ts","../src/components/tui/HotkeyBar.tsx","../src/components/tui/MonorepoPanel.tsx","../src/components/tui/PanelBorder.tsx","../src/components/tui/QuickWinsPanel.tsx","../src/components/tui/ScorePanel.tsx","../src/components/tui/TrendPanel.tsx","../src/components/tui/TuiApp.tsx"],"sourcesContent":["import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nexport interface ActivityEntry {\n timestamp: Date;\n type: 'scan-start' | 'scan-complete' | 'file-change' | 'git-change' | 'regression' | 'info';\n message: string;\n}\n\ninterface ActivityPanelProps {\n entries: ActivityEntry[];\n availableHeight: number;\n}\n\nconst TYPE_COLOR: Record<ActivityEntry['type'], string> = {\n 'scan-start': 'cyan',\n 'scan-complete': 'green',\n 'file-change': 'yellow',\n 'git-change': 'magenta',\n regression: 'red',\n info: 'gray',\n};\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString('en-US', { hour12: false });\n}\n\nexport function ActivityPanel({ entries, availableHeight }: ActivityPanelProps) {\n if (entries.length === 0) {\n return (\n <Box>\n <Text dimColor>No activity yet.</Text>\n </Box>\n );\n }\n\n const visible = entries.slice(-Math.max(1, availableHeight));\n\n return (\n <Box flexDirection=\"column\">\n {visible.map((entry) => (\n <Box key={`${entry.timestamp.getTime()}-${entry.type}`}>\n <Text dimColor>\n {formatTime(entry.timestamp)}\n {' '}\n </Text>\n <Text color={TYPE_COLOR[entry.type] || 'white'}>{entry.message}</Text>\n </Box>\n ))}\n </Box>\n );\n}\n","import { execFile } from 'child_process';\nimport { promisify } from 'util';\n\nimport { useState, useEffect, useRef } from 'react';\n\nconst exec = promisify(execFile);\n\nexport interface GitStatus {\n branch: string;\n modified: number;\n staged: number;\n untracked: number;\n ahead: number;\n behind: number;\n stashes: number;\n lastCommit: string;\n lastCommitTime: string;\n}\n\nasync function git(args: string[], cwd: string): Promise<string> {\n try {\n const { stdout } = await exec('git', args, { cwd, timeout: 5000 });\n return stdout.trim();\n } catch {\n return '';\n }\n}\n\nasync function fetchGitStatus(projectPath: string): Promise<GitStatus> {\n const [branch, porcelain, revList, stashList, logLine] = await Promise.all([\n git(['branch', '--show-current'], projectPath),\n git(['status', '--porcelain'], projectPath),\n git(['rev-list', '--left-right', '--count', 'HEAD...@{upstream}'], projectPath),\n git(['stash', 'list'], projectPath),\n git(['log', '-1', '--format=%s|%cr'], projectPath),\n ]);\n\n const lines = porcelain ? porcelain.split('\\n') : [];\n const staged = lines.filter((l) => /^[MADRC]/.test(l)).length;\n const modified = lines.filter((l) => /^.[MD]/.test(l)).length;\n const untracked = lines.filter((l) => l.startsWith('??')).length;\n\n let ahead = 0;\n let behind = 0;\n if (revList) {\n const parts = revList.split(/\\s+/);\n ahead = parseInt(parts[0], 10) || 0;\n behind = parseInt(parts[1], 10) || 0;\n }\n\n const stashes = stashList ? stashList.split('\\n').filter(Boolean).length : 0;\n const [lastCommit, lastCommitTime] = logLine.split('|');\n\n return {\n branch: branch || 'unknown',\n modified,\n staged,\n untracked,\n ahead,\n behind,\n stashes,\n lastCommit: lastCommit || '',\n lastCommitTime: lastCommitTime || '',\n };\n}\n\nexport function useGitStatus(projectPath: string, pollInterval = 10000) {\n const [status, setStatus] = useState<GitStatus | null>(null);\n const intervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);\n\n useEffect(() => {\n let mounted = true;\n\n fetchGitStatus(projectPath).then((s) => {\n if (mounted) setStatus(s);\n });\n\n intervalRef.current = setInterval(() => {\n fetchGitStatus(projectPath).then((s) => {\n if (mounted) setStatus(s);\n });\n }, pollInterval);\n\n return () => {\n mounted = false;\n if (intervalRef.current) clearInterval(intervalRef.current);\n };\n }, [projectPath, pollInterval]);\n\n return status;\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { useGitStatus } from './hooks/useGitStatus.js';\n\ninterface GitPanelProps {\n projectPath: string;\n availableWidth?: number;\n}\n\nexport function GitPanel({ projectPath, availableWidth = 30 }: GitPanelProps) {\n const status = useGitStatus(projectPath);\n\n if (!status) {\n return (\n <Box>\n <Text dimColor>Loading git info...</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Text dimColor>Branch: </Text>\n <Text bold color=\"green\">\n {status.branch}\n </Text>\n </Box>\n {(status.ahead > 0 || status.behind > 0) && (\n <Box>\n {status.ahead > 0 && (\n <Text color=\"green\">\n {'\\u2191'}\n {status.ahead}{' '}\n </Text>\n )}\n {status.behind > 0 && (\n <Text color=\"red\">\n {'\\u2193'}\n {status.behind}\n </Text>\n )}\n </Box>\n )}\n <Box>\n <Text dimColor>Modified: </Text>\n <Text color={status.modified > 0 ? 'yellow' : 'green'}>{status.modified}</Text>\n </Box>\n <Box>\n <Text dimColor>Staged: </Text>\n <Text color={status.staged > 0 ? 'cyan' : 'green'}>{status.staged}</Text>\n </Box>\n <Box>\n <Text dimColor>Untracked: </Text>\n <Text>{status.untracked}</Text>\n </Box>\n {status.stashes > 0 && (\n <Box>\n <Text dimColor>Stashes: </Text>\n <Text>{status.stashes}</Text>\n </Box>\n )}\n {status.lastCommit && (\n <Box marginTop={1} flexDirection=\"column\">\n <Text dimColor>Last commit:</Text>\n <Text>\n {status.lastCommit.length > availableWidth\n ? status.lastCommit.slice(0, availableWidth - 3) + '...'\n : status.lastCommit}\n </Text>\n <Text dimColor>{status.lastCommitTime}</Text>\n </Box>\n )}\n </Box>\n );\n}\n","import React, { useState, useEffect } from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { LOADING_MESSAGES } from '../../lib/messages.js';\n\nimport type { CheckResult } from 'sickbay-core';\n\ninterface HealthPanelProps {\n checks: CheckResult[];\n isScanning: boolean;\n progress: Array<{ name: string; status: string }>;\n scrollOffset: number;\n availableHeight: number;\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nfunction scoreBar(score: number, width = 10): string {\n const filled = Math.round((score / 100) * width);\n return '\\u2588'.repeat(filled) + '\\u2591'.repeat(width - filled);\n}\n\nfunction truncate(str: string, maxLen: number): string {\n return str.length > maxLen ? str.slice(0, maxLen - 1) + '\\u2026' : str;\n}\n\nfunction statusIcon(status: string): string {\n switch (status) {\n case 'pass':\n return '\\u2713';\n case 'fail':\n return '\\u2717';\n case 'warning':\n return '\\u26A0';\n case 'skipped':\n return '\\u25CB';\n default:\n return '?';\n }\n}\n\nexport function HealthPanel({\n checks,\n isScanning,\n progress,\n scrollOffset,\n availableHeight,\n}: HealthPanelProps) {\n const [msgIdx, setMsgIdx] = useState(0);\n\n useEffect(() => {\n if (!isScanning || progress.length > 0) return;\n const id = setInterval(() => {\n setMsgIdx((i) => (i + 1) % LOADING_MESSAGES.length);\n }, 4000);\n return () => clearInterval(id);\n }, [isScanning, progress.length]);\n\n if (isScanning && checks.length === 0) {\n if (progress.length === 0) {\n return (\n <Box>\n <Text dimColor>{LOADING_MESSAGES[msgIdx]}</Text>\n </Box>\n );\n }\n return (\n <Box flexDirection=\"column\">\n {progress.map((p) => (\n <Box key={p.name}>\n <Text\n color={p.status === 'done' ? 'green' : p.status === 'running' ? 'yellow' : 'gray'}\n >\n {p.status === 'done' ? '\\u2713' : p.status === 'running' ? '\\u25CC' : '\\u25CB'}\n </Text>\n <Text> {p.name}</Text>\n </Box>\n ))}\n </Box>\n );\n }\n\n if (checks.length === 0) {\n return (\n <Box>\n <Text dimColor>No results yet. Press [r] to scan.</Text>\n </Box>\n );\n }\n\n const maxVisible = Math.max(1, availableHeight);\n const visible = checks.slice(scrollOffset, scrollOffset + maxVisible);\n const hasMore = scrollOffset + maxVisible < checks.length;\n const hasLess = scrollOffset > 0;\n\n return (\n <Box flexDirection=\"column\">\n {hasLess && <Text dimColor> \\u25B2 {scrollOffset} more above</Text>}\n {visible.map((check) => (\n <Box key={check.id}>\n <Text\n color={check.status === 'pass' ? 'green' : check.status === 'fail' ? 'red' : 'yellow'}\n >\n {statusIcon(check.status)}\n </Text>\n <Text> </Text>\n <Box width={26}>\n <Text wrap=\"truncate\">{truncate(check.name, 26)}</Text>\n </Box>\n <Text> </Text>\n <Text color={scoreColor(check.score)}>{scoreBar(check.score)}</Text>\n <Text color={scoreColor(check.score)}> {String(check.score).padStart(3)}</Text>\n </Box>\n ))}\n {hasMore && (\n <Text dimColor>\n {' \\u25BC '}\n {checks.length - scrollOffset - maxVisible} more below\n </Text>\n )}\n {isScanning && <Text color=\"yellow\">{' \\u25CC Re-scanning...'}</Text>}\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\ninterface HelpRow {\n keys: string[];\n description: string;\n}\n\ninterface HelpSection {\n title: string;\n rows: HelpRow[];\n}\n\nconst SECTIONS: HelpSection[] = [\n {\n title: 'PANELS',\n rows: [\n { keys: ['h'], description: 'Focus / unfocus Health Checks' },\n { keys: ['g'], description: 'Focus / unfocus Git Status' },\n { keys: ['t'], description: 'Focus / unfocus Trend' },\n { keys: ['q'], description: 'Focus / unfocus Quick Wins' },\n { keys: ['a'], description: 'Focus / unfocus Activity' },\n { keys: ['f'], description: 'Expand focused panel to full screen' },\n { keys: ['Esc'], description: 'Unfocus panel / exit full screen' },\n ],\n },\n {\n title: 'SCROLLING',\n rows: [{ keys: ['↑', '↓'], description: 'Scroll Health Checks (when focused)' }],\n },\n {\n title: 'ACTIONS',\n rows: [\n { keys: ['r'], description: 'Re-run scan' },\n { keys: ['w'], description: 'Open web dashboard in browser' },\n { keys: ['W'], description: 'Open web dashboard with AI insights' },\n { keys: ['?'], description: 'Toggle this help screen' },\n ],\n },\n];\n\nexport function HelpPanel() {\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1}>\n <Box marginBottom={1}>\n <Text bold color=\"cyan\">\n SICKBAY TUI — KEYBOARD SHORTCUTS\n </Text>\n </Box>\n\n {SECTIONS.map((section) => (\n <Box key={section.title} flexDirection=\"column\" marginBottom={1}>\n <Box marginBottom={0}>\n <Text bold color=\"white\">\n {section.title}\n </Text>\n </Box>\n {section.rows.map((row) => (\n <Box key={row.description} marginLeft={2}>\n <Box width={16}>\n {row.keys.map((k, i) => (\n <Text key={k}>\n <Text color=\"cyan\" bold>\n [{k}]\n </Text>\n {i < row.keys.length - 1 ? <Text dimColor> / </Text> : null}\n </Text>\n ))}\n </Box>\n <Text dimColor>{row.description}</Text>\n </Box>\n ))}\n </Box>\n ))}\n\n <Box marginTop={1} borderStyle=\"single\" borderColor=\"gray\" paddingX={1}>\n <Text dimColor>Pro tip: focus a panel with its hotkey, then press </Text>\n <Text color=\"cyan\" bold>\n [f]\n </Text>\n <Text dimColor> to expand it full screen for more detail.</Text>\n </Box>\n\n <Box marginTop={1}>\n <Text dimColor>Press </Text>\n <Text color=\"cyan\" bold>\n [?]\n </Text>\n <Text dimColor> or </Text>\n <Text color=\"cyan\" bold>\n [Esc]\n </Text>\n <Text dimColor> to close</Text>\n </Box>\n </Box>\n );\n}\n","import { useState, useEffect, useRef } from 'react';\n\nimport type { FSWatcher } from 'chokidar';\n\ninterface UseFileWatcherOptions {\n projectPath: string;\n enabled: boolean;\n debounceMs?: number;\n onFilesChanged?: (files: string[]) => void;\n}\n\nexport function useFileWatcher({\n projectPath,\n enabled,\n debounceMs = 2000,\n onFilesChanged,\n}: UseFileWatcherOptions) {\n const [changedFiles, setChangedFiles] = useState<string[]>([]);\n const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const pendingRef = useRef<string[]>([]);\n const watcherRef = useRef<FSWatcher | null>(null);\n // Keep a ref so the debounce callback always calls the latest version,\n // even if the parent re-renders and passes a new function reference.\n const onFilesChangedRef = useRef(onFilesChanged);\n onFilesChangedRef.current = onFilesChanged;\n\n useEffect(() => {\n if (!enabled) return;\n\n let mounted = true;\n\n (async () => {\n try {\n const { watch } = await import('chokidar');\n if (!mounted) return;\n\n const watcher = watch(['**/*.{ts,tsx,js,jsx,json}'], {\n cwd: projectPath,\n ignored: [\n '**/node_modules/**',\n '**/dist/**',\n '**/.git/**',\n '**/build/**',\n '**/.next/**',\n '**/.turbo/**',\n '**/coverage/**',\n ],\n ignoreInitial: true,\n persistent: true,\n });\n\n watcherRef.current = watcher;\n\n watcher.on('change', (path: string) => {\n pendingRef.current.push(path);\n setChangedFiles((prev) => [...prev, path]);\n\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => {\n const files = [...pendingRef.current];\n pendingRef.current = [];\n onFilesChangedRef.current?.(files);\n }, debounceMs);\n });\n } catch {\n // chokidar not available — silently skip\n }\n })();\n\n return () => {\n mounted = false;\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (watcherRef.current) watcherRef.current.close();\n };\n }, [projectPath, enabled, debounceMs]);\n\n return changedFiles;\n}\n","import { useState, useCallback, useRef } from 'react';\n\nimport { runSickbay, runSickbayMonorepo, detectMonorepo, buildSummary } from 'sickbay-core';\n\nimport type { SickbayReport, MonorepoReport, CheckResult } from 'sickbay-core';\n\ninterface ProgressItem {\n name: string;\n status: 'pending' | 'running' | 'done';\n}\n\ninterface UseSickbayRunnerOptions {\n projectPath: string;\n checks?: string[];\n quotes?: boolean;\n}\n\n/**\n * Synthesize a rolled-up SickbayReport from a MonorepoReport for use in\n * TUI panels that only understand single-project reports. Strategy: merge\n * all checks from all packages, keeping the worst score per check name.\n */\nfunction rollUpMonorepoReport(monorepo: MonorepoReport): SickbayReport {\n const byId = new Map<string, CheckResult>();\n for (const pkg of monorepo.packages) {\n for (const check of pkg.checks) {\n const existing = byId.get(check.id);\n if (!existing || check.score < existing.score) {\n byId.set(check.id, check);\n }\n }\n }\n const checks = Array.from(byId.values());\n return {\n timestamp: monorepo.timestamp,\n projectPath: monorepo.rootPath,\n projectInfo: {\n name: `monorepo (${monorepo.packages.length} packages)`,\n version: '0.0.0',\n hasTypeScript: false,\n hasESLint: false,\n hasPrettier: false,\n framework: 'node',\n packageManager: monorepo.packageManager,\n totalDependencies: 0,\n dependencies: {},\n devDependencies: {},\n },\n checks,\n overallScore: monorepo.overallScore,\n summary: buildSummary(checks),\n };\n}\n\nexport function useSickbayRunner({ projectPath, checks, quotes }: UseSickbayRunnerOptions) {\n const [report, setReport] = useState<SickbayReport | null>(null);\n const [monorepoReport, setMonorepoReport] = useState<MonorepoReport | null>(null);\n const [isScanning, setIsScanning] = useState(false);\n const [progress, setProgress] = useState<ProgressItem[]>([]);\n const [error, setError] = useState<string | null>(null);\n const scanningRef = useRef(false);\n\n const scan = useCallback(async () => {\n if (scanningRef.current) return null;\n scanningRef.current = true;\n setIsScanning(true);\n setError(null);\n\n try {\n const monorepoInfo = await detectMonorepo(projectPath);\n\n if (monorepoInfo.isMonorepo) {\n const result = await runSickbayMonorepo({\n projectPath,\n checks,\n quotes,\n onPackageStart: (name) => {\n setProgress((prev) => [...prev, { name, status: 'running' }]);\n },\n onPackageComplete: (pkg) => {\n setProgress((prev) =>\n prev.map((p) => (p.name === pkg.name ? { ...p, status: 'done' } : p)),\n );\n },\n });\n\n setMonorepoReport(result);\n const rolledUp = rollUpMonorepoReport(result);\n setReport(rolledUp);\n\n setIsScanning(false);\n scanningRef.current = false;\n return rolledUp;\n }\n\n const result = await runSickbay({\n projectPath,\n checks,\n quotes,\n onRunnersReady: (names) => {\n setProgress(names.map((name) => ({ name, status: 'pending' as const })));\n },\n onCheckStart: (name) => {\n setProgress((prev) =>\n prev.map((p) => (p.name === name ? { ...p, status: 'running' } : p)),\n );\n },\n onCheckComplete: (check) => {\n setProgress((prev) =>\n prev.map((p) => (p.name === check.id ? { ...p, status: 'done' } : p)),\n );\n },\n });\n\n setMonorepoReport(null);\n setReport(result);\n\n // Save to trend history\n try {\n const { saveEntry } = await import('../../../lib/history.js');\n saveEntry(result);\n } catch {\n // Non-critical\n }\n\n setIsScanning(false);\n scanningRef.current = false;\n return result;\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n setIsScanning(false);\n scanningRef.current = false;\n return null;\n }\n }, [projectPath, checks, quotes]);\n\n return { report, monorepoReport, isScanning, progress, error, scan };\n}\n","import { useState, useEffect } from 'react';\n\nexport function useTerminalSize() {\n const [size, setSize] = useState({\n columns: process.stdout.columns || 80,\n rows: process.stdout.rows || 24,\n });\n\n useEffect(() => {\n const handler = () => {\n setSize({\n columns: process.stdout.columns || 80,\n rows: process.stdout.rows || 24,\n });\n };\n process.stdout.on('resize', handler);\n return () => {\n process.stdout.off('resize', handler);\n };\n }, []);\n\n return size;\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nexport type PanelId = 'health' | 'git' | 'trend' | 'quickwins' | 'activity';\n\ninterface HotkeyBarProps {\n activePanel: PanelId | null;\n}\n\nconst HOTKEYS: Array<{ key: string; label: string; panel?: PanelId }> = [\n { key: 'h', label: 'health', panel: 'health' },\n { key: 'g', label: 'git', panel: 'git' },\n { key: 't', label: 'trend', panel: 'trend' },\n { key: 'q', label: 'quick wins', panel: 'quickwins' },\n { key: 'a', label: 'activity', panel: 'activity' },\n { key: 'r', label: 're-run' },\n { key: 'w', label: 'web' },\n { key: 'W', label: 'web+AI' },\n { key: '?', label: 'help' },\n];\n\nexport function HotkeyBar({ activePanel }: HotkeyBarProps) {\n return (\n <Box>\n {HOTKEYS.map((hk) => {\n const isActive = hk.panel !== undefined && hk.panel === activePanel;\n return (\n <Box key={hk.key} marginRight={2}>\n <Text bold={isActive} color={isActive ? 'cyan' : undefined}>\n [{hk.key}]\n </Text>\n <Text bold={isActive} color={isActive ? 'cyan' : 'gray'}>\n {' '}\n {hk.label}\n </Text>\n </Box>\n );\n })}\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport type { MonorepoReport, PackageReport } from 'sickbay-core';\n\ninterface MonorepoPanelProps {\n report: MonorepoReport | null;\n availableWidth?: number;\n}\n\nfunction scoreBar(score: number, width = 8): string {\n const filled = Math.round((score / 100) * width);\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nexport function MonorepoPanel({ report, availableWidth }: MonorepoPanelProps) {\n if (!report) {\n return (\n <Box>\n <Text dimColor>Waiting for scan...</Text>\n </Box>\n );\n }\n\n const nameWidth = Math.max(12, Math.min(20, (availableWidth ?? 36) - 22));\n\n return (\n <Box flexDirection=\"column\">\n <Box marginBottom={1}>\n <Text bold>{report.packages.length} packages</Text>\n <Text dimColor> · </Text>\n <Text dimColor>{report.monorepoType} workspaces</Text>\n <Text dimColor> · </Text>\n <Text color={scoreColor(report.overallScore)} bold>\n {report.overallScore}\n </Text>\n <Text dimColor> avg</Text>\n </Box>\n\n {report.packages.map((pkg: PackageReport) => {\n const name = pkg.name.includes('/') ? (pkg.name.split('/').pop() ?? pkg.name) : pkg.name;\n const truncatedName =\n name.length > nameWidth\n ? name.slice(0, nameWidth - 1) + '\\u2026'\n : name.padEnd(nameWidth);\n\n return (\n <Box key={pkg.path} gap={1}>\n <Text color={scoreColor(pkg.score)}>{scoreBar(pkg.score)}</Text>\n <Text color={scoreColor(pkg.score)} bold>\n {String(pkg.score).padStart(3)}\n </Text>\n <Text>{truncatedName}</Text>\n {pkg.summary.critical > 0 && <Text color=\"red\"> ⚠</Text>}\n </Box>\n );\n })}\n\n <Box marginTop={1}>\n <Text dimColor>Press </Text>\n <Text color=\"cyan\">[w]</Text>\n <Text dimColor> for per-package web view</Text>\n </Box>\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\ninterface PanelBorderProps {\n title: string;\n color: string;\n focused?: boolean;\n visible?: boolean;\n flash?: 'green' | 'red';\n children: React.ReactNode;\n}\n\nexport function PanelBorder({\n title,\n color,\n focused,\n visible = true,\n flash,\n children,\n}: PanelBorderProps) {\n const borderColor = flash ?? (focused ? color : 'gray');\n const borderStyle = flash ? 'double' : focused ? 'double' : 'single';\n\n return (\n <Box\n flexDirection=\"column\"\n borderStyle={borderStyle}\n borderColor={borderColor}\n paddingX={1}\n flexGrow={1}\n >\n <Text bold color={flash ?? color}>\n {title}\n </Text>\n {visible ? children : <Text dimColor>···</Text>}\n </Box>\n );\n}\n","import React from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport type { SickbayReport } from 'sickbay-core';\n\ninterface QuickWinsPanelProps {\n report: SickbayReport | null;\n availableWidth?: number;\n}\n\nfunction replacePackageManager(cmd: string, pm: string): string {\n if (pm === 'npm') return cmd;\n const install = pm === 'pnpm' ? 'pnpm add' : pm === 'yarn' ? 'yarn add' : 'bun add';\n const uninstall = pm === 'pnpm' ? 'pnpm remove' : pm === 'yarn' ? 'yarn remove' : 'bun remove';\n const update = pm === 'pnpm' ? 'pnpm update' : pm === 'yarn' ? 'yarn upgrade' : 'bun update';\n const auditFix =\n pm === 'pnpm' ? 'pnpm audit --fix' : pm === 'yarn' ? 'yarn npm audit --fix' : 'bun audit';\n return cmd\n .replace(/^npm install(?=\\s)/, install)\n .replace(/^npm uninstall(?=\\s)/, uninstall)\n .replace(/^npm update(?=\\s)/, update)\n .replace(/^npm audit fix/, auditFix);\n}\n\nfunction shortenPath(filepath: string): string {\n const parts = filepath.split('/');\n if (parts.length <= 2) return filepath;\n return '\\u2026/' + parts.slice(-2).join('/');\n}\n\nfunction smartTruncate(str: string, maxLen: number): string {\n if (str.length <= maxLen) return str;\n // If it contains a file path, shorten the path but keep the filename\n const pathMatch = str.match(/(\\S*\\/\\S+)/);\n if (pathMatch) {\n const shortened = str.replace(pathMatch[1], shortenPath(pathMatch[1]));\n if (shortened.length <= maxLen) return shortened;\n return shortened.slice(0, maxLen - 1) + '\\u2026';\n }\n return str.slice(0, maxLen - 1) + '\\u2026';\n}\n\nfunction shortenCommand(cmd: string, maxLen: number): string {\n if (cmd.length <= maxLen) return cmd;\n // Shorten any file paths in the command first\n const shortened = cmd.replace(/(\\S*\\/\\S+)/g, (match) => shortenPath(match));\n if (shortened.length <= maxLen) return shortened;\n return shortened.slice(0, maxLen - 1) + '\\u2026';\n}\n\nexport function QuickWinsPanel({ report, availableWidth }: QuickWinsPanelProps) {\n if (!report) {\n return (\n <Box>\n <Text dimColor>Waiting for scan...</Text>\n </Box>\n );\n }\n\n const fixes: Array<{\n description: string;\n command: string;\n severity: string;\n }> = [];\n\n for (const check of report.checks) {\n for (const issue of check.issues) {\n if (issue.fix?.command) {\n fixes.push({\n description: issue.fix.description,\n command: issue.fix.command,\n severity: issue.severity,\n });\n }\n }\n }\n\n const order: Record<string, number> = { critical: 0, warning: 1, info: 2 };\n fixes.sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3));\n const top = fixes.slice(0, 5);\n\n if (top.length === 0) {\n return (\n <Box>\n <Text color=\"green\">Looking good!</Text>\n </Box>\n );\n }\n\n const pm = report.projectInfo?.packageManager ?? 'npm';\n const maxTextLen = Math.max(16, (availableWidth ?? 26) - 4);\n\n return (\n <Box flexDirection=\"column\">\n {top.map((fix, i) => (\n <Box key={i} flexDirection=\"column\" marginBottom={i < top.length - 1 ? 1 : 0}>\n <Text>\n <Text\n color={\n fix.severity === 'critical' ? 'red' : fix.severity === 'warning' ? 'yellow' : 'gray'\n }\n >\n {'\\u2192'}{' '}\n </Text>\n {smartTruncate(fix.description, maxTextLen)}\n </Text>\n <Text dimColor>\n {' '}\n {shortenCommand(replacePackageManager(fix.command, pm), maxTextLen)}\n </Text>\n </Box>\n ))}\n </Box>\n );\n}\n","import React, { useState, useEffect, useRef } from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { countUniqueIssues } from '../../lib/issue-grouping.js';\n\nimport type { SickbayReport } from 'sickbay-core';\n\ninterface ScorePanelProps {\n report: SickbayReport | null;\n previousScore: number | null;\n animate?: boolean;\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nfunction scoreBar(score: number, width = 15): string {\n const filled = Math.round((score / 100) * width);\n return '\\u2588'.repeat(filled) + '\\u2591'.repeat(width - filled);\n}\n\nexport function ScorePanel({ report, previousScore, animate = true }: ScorePanelProps) {\n const [animatedScore, setAnimatedScore] = useState(0);\n const prevTargetRef = useRef(0);\n\n useEffect(() => {\n if (!animate) return;\n\n if (!report) {\n setAnimatedScore(0);\n prevTargetRef.current = 0;\n return;\n }\n\n const target = report.overallScore;\n const start = prevTargetRef.current;\n prevTargetRef.current = target;\n\n if (start === target) return;\n\n let current = start;\n const step = target > start ? 1 : -1;\n\n const id = setInterval(() => {\n current += step;\n setAnimatedScore(current);\n if (current === target) clearInterval(id);\n }, 20);\n\n return () => clearInterval(id);\n }, [report, animate]);\n\n // When animate=false, render score directly from prop (no state update needed)\n const displayScore = animate ? animatedScore : (report?.overallScore ?? 0);\n\n if (!report) {\n return (\n <Box>\n <Text dimColor>Waiting for scan...</Text>\n </Box>\n );\n }\n\n const delta = previousScore !== null ? report.overallScore - previousScore : null;\n const u = countUniqueIssues(report.checks);\n\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Text color={scoreColor(displayScore)} bold>\n {displayScore}/100\n </Text>\n <Text> </Text>\n <Text color={scoreColor(displayScore)}>{scoreBar(displayScore)}</Text>\n </Box>\n {delta !== null && (\n <Text dimColor>\n {delta > 0 ? `+${delta}` : delta === 0 ? '\\u00B10' : `${delta}`} since last scan\n </Text>\n )}\n <Box marginTop={1} flexDirection=\"column\">\n <Box>\n <Text color=\"red\">\n {'\\u2717'} {u.critical} critical\n {u.totalCritical > u.critical ? ` (${u.totalCritical})` : ''}\n </Text>\n <Text>{' '}</Text>\n <Text color=\"yellow\">\n {'\\u26A0'} {u.warnings} warn\n {u.totalWarnings > u.warnings ? ` (${u.totalWarnings})` : ''}\n </Text>\n <Text>{' '}</Text>\n <Text dimColor>i {u.info} info</Text>\n </Box>\n </Box>\n {report.quote && (\n <Box marginTop={1}>\n <Text italic dimColor>\n \"{report.quote.text}\" — {report.quote.source}\n </Text>\n </Box>\n )}\n {report.config?.hasCustomConfig && (\n <Text dimColor italic>\n Custom config\n </Text>\n )}\n </Box>\n );\n}\n","import React, { useState, useEffect } from 'react';\n\nimport { Box, Text } from 'ink';\n\nimport { sparkline, trendArrow } from '../../commands/trend.js';\nimport { loadHistory } from '../../lib/history.js';\n\ninterface TrendPanelProps {\n projectPath: string;\n lastScanTime: Date | null;\n availableHeight?: number;\n}\n\nconst CATEGORY_COLORS: Record<string, string> = {\n overall: 'white',\n dependencies: 'blue',\n security: 'green',\n 'code-quality': 'yellow',\n performance: 'magenta',\n git: 'cyan',\n};\n\nconst SHORT_LABELS: Record<string, string> = {\n dependencies: 'Deps',\n security: 'Security',\n 'code-quality': 'Quality',\n performance: 'Perf',\n git: 'Git',\n};\n\nexport function TrendPanel({ projectPath, lastScanTime, availableHeight }: TrendPanelProps) {\n const [trends, setTrends] = useState<\n Array<{\n label: string;\n spark: string;\n latest: number;\n arrow: string;\n color: string;\n }>\n >([]);\n\n useEffect(() => {\n const history = loadHistory(projectPath);\n if (!history || history.entries.length === 0) {\n setTrends([]);\n return;\n }\n\n const entries = history.entries.slice(-10);\n const overallValues = entries.map((e) => e.overallScore);\n const overallArrow = trendArrow(overallValues);\n\n const result: typeof trends = [\n {\n label: 'Overall',\n spark: sparkline(overallValues),\n latest: overallValues[overallValues.length - 1],\n arrow: overallArrow.label,\n color: 'white',\n },\n ];\n\n const categories = Object.keys(entries[entries.length - 1].categoryScores);\n for (const cat of categories) {\n const values = entries\n .map((e) => e.categoryScores[cat])\n .filter((v): v is number => v !== undefined);\n\n if (values.length === 0) continue;\n\n const arrow = trendArrow(values);\n result.push({\n label: SHORT_LABELS[cat] || cat.charAt(0).toUpperCase() + cat.slice(1),\n spark: sparkline(values),\n latest: values[values.length - 1],\n arrow: arrow.label,\n color: CATEGORY_COLORS[cat] || 'white',\n });\n }\n\n setTrends(result);\n }, [projectPath, lastScanTime]);\n\n if (trends.length === 0) {\n return (\n <Box>\n <Text dimColor>No trend data yet.</Text>\n </Box>\n );\n }\n\n const maxRows = availableHeight ?? trends.length;\n const visible = trends.slice(0, maxRows);\n\n return (\n <Box flexDirection=\"column\">\n {visible.map((t) => (\n <Box key={t.label}>\n <Box width={10}>\n <Text color={t.color}>{t.label}</Text>\n </Box>\n <Text>{t.spark}</Text>\n <Text bold> {String(t.latest).padStart(3)}</Text>\n <Text> {t.arrow}</Text>\n </Box>\n ))}\n </Box>\n );\n}\n","import React, { useState, useEffect, useCallback, useRef } from 'react';\n\nimport { Box, Text, useInput } from 'ink';\n\nimport { checkForUpdate } from '../../lib/update-check.js';\nimport { UpdateNotice } from '../UpdateNotice.js';\nimport { ActivityPanel, type ActivityEntry } from './ActivityPanel.js';\nimport { GitPanel } from './GitPanel.js';\nimport { HealthPanel } from './HealthPanel.js';\nimport { HelpPanel } from './HelpPanel.js';\nimport { useFileWatcher } from './hooks/useFileWatcher.js';\nimport { useSickbayRunner } from './hooks/useSickbayRunner.js';\nimport { useTerminalSize } from './hooks/useTerminalSize.js';\nimport { HotkeyBar, type PanelId } from './HotkeyBar.js';\nimport { MonorepoPanel } from './MonorepoPanel.js';\nimport { PanelBorder } from './PanelBorder.js';\nimport { QuickWinsPanel } from './QuickWinsPanel.js';\nimport { ScorePanel } from './ScorePanel.js';\nimport { TrendPanel } from './TrendPanel.js';\n\nimport type { UpdateInfo } from '../../lib/update-check.js';\nimport type { SickbayReport, MonorepoReport } from 'sickbay-core';\n\ndeclare const __VERSION__: string;\n\ninterface TuiAppProps {\n projectPath: string;\n checks?: string[];\n watchEnabled: boolean;\n refreshInterval: number;\n quotes?: boolean;\n animateOnMount?: boolean;\n}\n\nexport function TuiApp({\n projectPath,\n checks,\n watchEnabled,\n refreshInterval,\n quotes,\n animateOnMount = true,\n}: TuiAppProps) {\n const { rows, columns } = useTerminalSize();\n const { report, monorepoReport, isScanning, progress, scan } = useSickbayRunner({\n projectPath,\n checks,\n quotes,\n });\n\n const [focusedPanel, setFocusedPanel] = useState<PanelId | null>(null);\n const [expandedPanel, setExpandedPanel] = useState<PanelId | null>(null);\n const [showHelp, setShowHelp] = useState(false);\n const [lastScanTime, setLastScanTime] = useState<Date | null>(null);\n const [previousScore, setPreviousScore] = useState<number | null>(null);\n const [activityLog, setActivityLog] = useState<ActivityEntry[]>([]);\n const [healthScrollOffset, setHealthScrollOffset] = useState(0);\n const [scoreFlash, setScoreFlash] = useState<'green' | 'red' | undefined>();\n const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);\n const ALL_PANELS = new Set(['health', 'score', 'trend', 'git', 'quickwins', 'activity']);\n const [visiblePanels, setVisiblePanels] = useState<Set<string>>(\n animateOnMount ? new Set() : ALL_PANELS,\n );\n const refreshRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);\n\n // Panel entrance animation — stagger panels appearing on mount\n useEffect(() => {\n const schedule: Array<[string, number]> = [\n ['health', 100],\n ['score', 300],\n ['trend', 500],\n ['git', 700],\n ['quickwins', 900],\n ['activity', 1100],\n ];\n const timers = schedule.map(([panel, delay]) =>\n setTimeout(() => {\n setVisiblePanels((prev) => new Set([...prev, panel]));\n }, delay),\n );\n return () => timers.forEach(clearTimeout);\n }, []);\n\n useEffect(() => {\n checkForUpdate(__VERSION__).then((info) => {\n if (info) setUpdateInfo(info);\n });\n }, []);\n\n const reportRef = useRef<SickbayReport | null>(null);\n const monorepoReportRef = useRef<MonorepoReport | null>(null);\n\n // Keep refs in sync\n useEffect(() => {\n reportRef.current = report;\n }, [report]);\n\n useEffect(() => {\n monorepoReportRef.current = monorepoReport;\n }, [monorepoReport]);\n\n const addActivity = useCallback((type: ActivityEntry['type'], message: string) => {\n setActivityLog((prev) => [...prev, { timestamp: new Date(), type, message }]);\n }, []);\n\n const handleScanComplete = useCallback(\n async (result: SickbayReport) => {\n const prevScore = reportRef.current?.overallScore ?? null;\n if (prevScore !== null) setPreviousScore(prevScore);\n setLastScanTime(new Date());\n\n // Auto-save last report snapshot\n try {\n const { saveLastReport } = await import('../../lib/history.js');\n saveLastReport(result);\n } catch {\n // Non-critical\n }\n\n // Cache dependency tree for web dashboard\n try {\n const { getDependencyTree } = await import('sickbay-core');\n const { saveDepTree } = await import('../../lib/history.js');\n const tree = await getDependencyTree(projectPath, result.projectInfo.packageManager);\n saveDepTree(projectPath, tree);\n } catch {\n /* dep tree is optional */\n }\n\n const delta = prevScore !== null ? result.overallScore - prevScore : null;\n addActivity(\n 'scan-complete',\n `Scan complete: ${result.overallScore}/100${delta !== null ? ` (${delta >= 0 ? '+' : ''}${delta})` : ''}`,\n );\n\n // Flash score panel border on score change\n if (delta !== null && delta !== 0) {\n const flash = delta > 0 ? 'green' : 'red';\n setScoreFlash(flash);\n setTimeout(() => setScoreFlash(undefined), 600);\n }\n\n // Check for regressions\n try {\n const { loadHistory, detectRegressions } = await import('../../lib/history.js');\n const history = loadHistory(projectPath);\n if (history) {\n const regressions = detectRegressions(history.entries);\n for (const reg of regressions) {\n addActivity(\n 'regression',\n `\\u26A0 ${reg.category} regressed: ${reg.from} \\u2192 ${reg.to}`,\n );\n }\n }\n } catch {\n // Non-critical\n }\n },\n [addActivity, projectPath],\n );\n\n // Initial scan\n useEffect(() => {\n addActivity('info', 'TUI started');\n addActivity('scan-start', 'Starting initial health scan...');\n scan().then((result) => {\n if (result) handleScanComplete(result);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Auto-refresh timer\n useEffect(() => {\n if (refreshInterval <= 0) return;\n refreshRef.current = setInterval(async () => {\n addActivity('scan-start', 'Auto-scan triggered');\n const result = await scan();\n if (result) handleScanComplete(result);\n }, refreshInterval * 1000);\n\n return () => {\n if (refreshRef.current) clearInterval(refreshRef.current);\n };\n }, [refreshInterval, scan, handleScanComplete, addActivity]);\n\n // File watcher\n useFileWatcher({\n projectPath,\n enabled: watchEnabled,\n onFilesChanged: async (files) => {\n for (const f of files.slice(0, 3)) {\n addActivity('file-change', `File changed: ${f}`);\n }\n if (files.length > 3) {\n addActivity('file-change', `...and ${files.length - 3} more files`);\n }\n addActivity('scan-start', 'Re-scan triggered (file change)');\n const result = await scan();\n if (result) handleScanComplete(result);\n },\n });\n\n // Manual re-scan\n const triggerRescan = useCallback(async () => {\n addActivity('scan-start', 'Manual re-scan triggered');\n const result = await scan();\n if (result) handleScanComplete(result);\n }, [addActivity, scan, handleScanComplete]);\n\n // Keyboard input — only active when stdin supports raw mode (interactive TTY)\n const isTTY = !!(process.stdin.isTTY && process.stdin.setRawMode);\n useInput(\n (input, key) => {\n // Help overlay: ? toggles, Escape closes\n if (showHelp) {\n if (input === '?' || key.escape) {\n setShowHelp(false);\n }\n return;\n }\n\n // Expanded panel: escape or f to exit\n if (expandedPanel) {\n if (key.escape || input === 'f') {\n setExpandedPanel(null);\n return;\n }\n }\n\n if (input === '?') {\n setShowHelp(true);\n return;\n }\n\n // Panel focus toggles\n if (input === 'h') setFocusedPanel((prev) => (prev === 'health' ? null : 'health'));\n else if (input === 'g') setFocusedPanel((prev) => (prev === 'git' ? null : 'git'));\n else if (input === 't') setFocusedPanel((prev) => (prev === 'trend' ? null : 'trend'));\n else if (input === 'q')\n setFocusedPanel((prev) => (prev === 'quickwins' ? null : 'quickwins'));\n else if (input === 'a') setFocusedPanel((prev) => (prev === 'activity' ? null : 'activity'));\n else if (input === 'r') triggerRescan();\n else if (input === 'f' && focusedPanel) setExpandedPanel(focusedPanel);\n else if (input === 'w' || input === 'W') {\n const withAI = input === 'W';\n (async () => {\n const webReport = monorepoReportRef.current ?? reportRef.current;\n if (!webReport) return;\n try {\n const { serveWeb } = await import('../../commands/web.js');\n const { default: openBrowser } = await import('open');\n\n let aiService;\n if (withAI && process.env.ANTHROPIC_API_KEY && !monorepoReportRef.current) {\n const { createAIService } = await import('../../services/ai.js');\n aiService = createAIService(process.env.ANTHROPIC_API_KEY);\n addActivity('info', 'Launching web dashboard with AI...');\n } else {\n addActivity('info', 'Launching web dashboard...');\n }\n\n const url = await serveWeb(webReport, 3030, aiService);\n await openBrowser(url);\n addActivity(\n 'info',\n `Web dashboard at ${url}${withAI && aiService ? ' (AI enabled)' : ''}`,\n );\n } catch (err) {\n addActivity('info', `Failed to open web: ${err instanceof Error ? err.message : err}`);\n }\n })();\n } else if (key.escape) {\n setFocusedPanel(null);\n }\n\n // Scrolling in focused health panel\n if (focusedPanel === 'health') {\n if (key.upArrow) setHealthScrollOffset((prev) => Math.max(0, prev - 1));\n if (key.downArrow) {\n const maxOffset = Math.max(0, visibleChecks.length - 5);\n setHealthScrollOffset((prev) => Math.min(maxOffset, prev + 1));\n }\n }\n },\n { isActive: isTTY },\n );\n\n const visibleChecks = report?.checks.filter((c) => c.status !== 'skipped') ?? [];\n\n // Layout calculations — reserve 1 row for project header + 1 for hotkey bar\n const topHeight = Math.floor((rows - 2) * 0.6);\n const bottomHeight = rows - 2 - topHeight;\n\n // Help overlay\n if (showHelp) {\n return (\n <Box flexDirection=\"column\" width={columns} height={rows}>\n <Box flexGrow={1}>\n <PanelBorder title=\"HELP\" color=\"cyan\" focused>\n <HelpPanel />\n </PanelBorder>\n </Box>\n <HotkeyBar activePanel={focusedPanel} />\n </Box>\n );\n }\n\n // Full-screen expand mode\n if (expandedPanel) {\n return (\n <Box flexDirection=\"column\" width={columns} height={rows}>\n <Box flexGrow={1}>\n {expandedPanel === 'health' && (\n <PanelBorder title=\"HEALTH CHECKS\" color=\"green\" focused>\n <HealthPanel\n checks={visibleChecks}\n isScanning={isScanning}\n progress={progress}\n scrollOffset={healthScrollOffset}\n availableHeight={rows - 4}\n />\n </PanelBorder>\n )}\n {expandedPanel === 'git' && (\n <PanelBorder title=\"GIT STATUS\" color=\"yellow\" focused>\n <GitPanel projectPath={projectPath} availableWidth={columns - 4} />\n </PanelBorder>\n )}\n {expandedPanel === 'trend' && (\n <PanelBorder title=\"TREND\" color=\"magenta\" focused>\n <TrendPanel projectPath={projectPath} lastScanTime={lastScanTime} />\n </PanelBorder>\n )}\n {expandedPanel === 'quickwins' && (\n <PanelBorder title={monorepoReport ? 'MONOREPO' : 'QUICK WINS'} color=\"red\" focused>\n {monorepoReport ? (\n <MonorepoPanel report={monorepoReport} />\n ) : (\n <QuickWinsPanel report={report} />\n )}\n </PanelBorder>\n )}\n {expandedPanel === 'activity' && (\n <PanelBorder title=\"ACTIVITY\" color=\"cyan\" focused>\n <ActivityPanel entries={activityLog} availableHeight={rows - 4} />\n </PanelBorder>\n )}\n </Box>\n <HotkeyBar activePanel={expandedPanel} />\n </Box>\n );\n }\n\n // Normal grid layout\n const projectName = monorepoReport\n ? `${monorepoReport.rootPath.split('/').pop()} (monorepo)`\n : (report?.projectInfo?.name ?? '—');\n const projectVersion = report?.projectInfo?.version;\n const scanLabel = lastScanTime\n ? `Last scan ${lastScanTime.toLocaleTimeString()}`\n : isScanning\n ? 'Scanning…'\n : 'Not yet scanned';\n\n return (\n <Box flexDirection=\"column\" width={columns} height={rows}>\n {updateInfo && (\n <UpdateNotice\n currentVersion={updateInfo.currentVersion}\n latestVersion={updateInfo.latestVersion}\n />\n )}\n {/* Project header */}\n <Box paddingX={1} justifyContent=\"space-between\">\n <Box gap={1}>\n <Text bold color=\"cyan\">\n SICKBAY\n </Text>\n <Text bold>{projectName}</Text>\n {projectVersion && <Text dimColor>v{projectVersion}</Text>}\n </Box>\n <Text dimColor>{scanLabel}</Text>\n </Box>\n\n {/* Top Row */}\n <Box height={topHeight}>\n <Box width=\"55%\">\n <PanelBorder\n title=\"HEALTH CHECKS\"\n color=\"green\"\n focused={focusedPanel === 'health'}\n visible={visiblePanels.has('health')}\n >\n <HealthPanel\n checks={visibleChecks}\n isScanning={isScanning}\n progress={progress}\n scrollOffset={healthScrollOffset}\n availableHeight={topHeight - 4}\n />\n </PanelBorder>\n </Box>\n <Box width=\"45%\" flexDirection=\"column\">\n <Box height=\"50%\">\n <PanelBorder\n title=\"SCORE\"\n color=\"blue\"\n visible={visiblePanels.has('score')}\n flash={scoreFlash}\n >\n <ScorePanel report={report} previousScore={previousScore} animate={animateOnMount} />\n </PanelBorder>\n </Box>\n <Box height=\"50%\">\n <PanelBorder\n title=\"TREND\"\n color=\"magenta\"\n focused={focusedPanel === 'trend'}\n visible={visiblePanels.has('trend')}\n >\n <TrendPanel\n projectPath={projectPath}\n lastScanTime={lastScanTime}\n availableHeight={Math.floor(topHeight / 2) - 3}\n />\n </PanelBorder>\n </Box>\n </Box>\n </Box>\n\n {/* Bottom Row */}\n <Box height={bottomHeight}>\n <Box width=\"25%\">\n <PanelBorder\n title=\"GIT STATUS\"\n color=\"yellow\"\n focused={focusedPanel === 'git'}\n visible={visiblePanels.has('git')}\n >\n <GitPanel projectPath={projectPath} availableWidth={Math.floor(columns * 0.25) - 4} />\n </PanelBorder>\n </Box>\n <Box width=\"30%\">\n <PanelBorder\n title={monorepoReport ? 'MONOREPO' : 'QUICK WINS'}\n color=\"red\"\n focused={focusedPanel === 'quickwins'}\n visible={visiblePanels.has('quickwins')}\n >\n {monorepoReport ? (\n <MonorepoPanel\n report={monorepoReport}\n availableWidth={Math.floor(columns * 0.3) - 4}\n />\n ) : (\n <QuickWinsPanel report={report} availableWidth={Math.floor(columns * 0.3) - 4} />\n )}\n </PanelBorder>\n </Box>\n <Box width=\"45%\">\n <PanelBorder\n title=\"ACTIVITY\"\n color=\"cyan\"\n focused={focusedPanel === 'activity'}\n visible={visiblePanels.has('activity')}\n >\n <ActivityPanel entries={activityLog} availableHeight={bottomHeight - 4} />\n </PanelBorder>\n </Box>\n </Box>\n\n {/* Hotkey Bar */}\n <HotkeyBar activePanel={focusedPanel} />\n </Box>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
//#region src/commands/badge.ts
|
|
4
|
+
function getScoreColor(score) {
|
|
5
|
+
if (score >= 90) return "brightgreen";
|
|
6
|
+
if (score >= 80) return "green";
|
|
7
|
+
if (score >= 60) return "yellow";
|
|
8
|
+
return "red";
|
|
9
|
+
}
|
|
10
|
+
function encodeLabel(label) {
|
|
11
|
+
return label.replace(/-/g, "--").replace(/_/g, "__").replace(/ /g, "%20");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Escape characters that break out of HTML attributes when interpolated.
|
|
15
|
+
* `--label` is user-controlled CLI input, so embedding it in `<img alt="...">`
|
|
16
|
+
* without escaping turns `" onload="alert(1)` into an executable handler if
|
|
17
|
+
* the generated snippet is pasted into a README rendered as HTML.
|
|
18
|
+
*/
|
|
19
|
+
function escapeHtml(s) {
|
|
20
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Escape characters that break the `` syntax when interpolated
|
|
24
|
+
* into the alt-text position. A label like `](javascript:alert(1)` would
|
|
25
|
+
* otherwise close the alt early and inject a link target — some renderers
|
|
26
|
+
* strip `javascript:` URIs, but not all, so treat it as unsafe by default.
|
|
27
|
+
* Backslash is escaped first so later escapes don't double up on it.
|
|
28
|
+
*/
|
|
29
|
+
function escapeMarkdownAlt(s) {
|
|
30
|
+
return s.replace(/\\/g, "\\\\").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
31
|
+
}
|
|
32
|
+
function badgeUrl(score, label = "sickbay") {
|
|
33
|
+
const color = getScoreColor(score);
|
|
34
|
+
return `https://img.shields.io/badge/${encodeLabel(label)}-${score}%2F100-${color}`;
|
|
35
|
+
}
|
|
36
|
+
function badgeMarkdown(score, label = "sickbay") {
|
|
37
|
+
return `})`;
|
|
38
|
+
}
|
|
39
|
+
function badgeHtml(score, label = "sickbay") {
|
|
40
|
+
return `<img src="${escapeHtml(badgeUrl(score, label))}" alt="${escapeHtml(label)}" />`;
|
|
41
|
+
}
|
|
42
|
+
function loadScoreFromLastReport(projectPath) {
|
|
43
|
+
const filePath = join(projectPath, ".sickbay", "last-report.json");
|
|
44
|
+
if (!existsSync(filePath)) return null;
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
47
|
+
if (typeof data.overallScore !== "number") return null;
|
|
48
|
+
return data.overallScore;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { badgeHtml, badgeMarkdown, badgeUrl, loadScoreFromLastReport };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=badge-CTayKnYV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"badge-CTayKnYV.js","names":[],"sources":["../src/commands/badge.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport function getScoreColor(score: number): string {\n if (score >= 90) return 'brightgreen';\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nfunction encodeLabel(label: string): string {\n return label.replace(/-/g, '--').replace(/_/g, '__').replace(/ /g, '%20');\n}\n\n/**\n * Escape characters that break out of HTML attributes when interpolated.\n * `--label` is user-controlled CLI input, so embedding it in `<img alt=\"...\">`\n * without escaping turns `\" onload=\"alert(1)` into an executable handler if\n * the generated snippet is pasted into a README rendered as HTML.\n */\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n\n/**\n * Escape characters that break the `` syntax when interpolated\n * into the alt-text position. A label like `](javascript:alert(1)` would\n * otherwise close the alt early and inject a link target — some renderers\n * strip `javascript:` URIs, but not all, so treat it as unsafe by default.\n * Backslash is escaped first so later escapes don't double up on it.\n */\nfunction escapeMarkdownAlt(s: string): string {\n return s\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\\[/g, '\\\\[')\n .replace(/\\]/g, '\\\\]')\n .replace(/\\(/g, '\\\\(')\n .replace(/\\)/g, '\\\\)');\n}\n\nexport function badgeUrl(score: number, label = 'sickbay'): string {\n const color = getScoreColor(score);\n const encoded = encodeLabel(label);\n return `https://img.shields.io/badge/${encoded}-${score}%2F100-${color}`;\n}\n\nexport function badgeMarkdown(score: number, label = 'sickbay'): string {\n return `})`;\n}\n\nexport function badgeHtml(score: number, label = 'sickbay'): string {\n return `<img src=\"${escapeHtml(badgeUrl(score, label))}\" alt=\"${escapeHtml(label)}\" />`;\n}\n\nexport function loadScoreFromLastReport(projectPath: string): number | null {\n const filePath = join(projectPath, '.sickbay', 'last-report.json');\n if (!existsSync(filePath)) return null;\n try {\n const data = JSON.parse(readFileSync(filePath, 'utf-8'));\n if (typeof data.overallScore !== 'number') return null;\n return data.overallScore;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;AAGA,SAAgB,cAAc,OAAuB;AACnD,KAAI,SAAS,GAAI,QAAO;AACxB,KAAI,SAAS,GAAI,QAAO;AACxB,KAAI,SAAS,GAAI,QAAO;AACxB,QAAO;;AAGT,SAAS,YAAY,OAAuB;AAC1C,QAAO,MAAM,QAAQ,MAAM,KAAK,CAAC,QAAQ,MAAM,KAAK,CAAC,QAAQ,MAAM,MAAM;;;;;;;;AAS3E,SAAS,WAAW,GAAmB;AACrC,QAAO,EACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO;;;;;;;;;AAU1B,SAAS,kBAAkB,GAAmB;AAC5C,QAAO,EACJ,QAAQ,OAAO,OAAO,CACtB,QAAQ,OAAO,MAAM,CACrB,QAAQ,OAAO,MAAM,CACrB,QAAQ,OAAO,MAAM,CACrB,QAAQ,OAAO,MAAM;;AAG1B,SAAgB,SAAS,OAAe,QAAQ,WAAmB;CACjE,MAAM,QAAQ,cAAc,MAAM;AAElC,QAAO,gCADS,YAAY,MAAM,CACa,GAAG,MAAM,SAAS;;AAGnE,SAAgB,cAAc,OAAe,QAAQ,WAAmB;AACtE,QAAO,KAAK,kBAAkB,MAAM,CAAC,IAAI,SAAS,OAAO,MAAM,CAAC;;AAGlE,SAAgB,UAAU,OAAe,QAAQ,WAAmB;AAClE,QAAO,aAAa,WAAW,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,WAAW,MAAM,CAAC;;AAGpF,SAAgB,wBAAwB,aAAoC;CAC1E,MAAM,WAAW,KAAK,aAAa,YAAY,mBAAmB;AAClE,KAAI,CAAC,WAAW,SAAS,CAAE,QAAO;AAClC,KAAI;EACF,MAAM,OAAO,KAAK,MAAM,aAAa,UAAU,QAAQ,CAAC;AACxD,MAAI,OAAO,KAAK,iBAAiB,SAAU,QAAO;AAClD,SAAO,KAAK;SACN;AACN,SAAO"}
|
package/dist/index.js
CHANGED
|
@@ -130,7 +130,7 @@ function Header({ projectName }) {
|
|
|
130
130
|
return /* @__PURE__ */ React.createElement(Box, {
|
|
131
131
|
flexDirection: "column",
|
|
132
132
|
marginBottom: 1
|
|
133
|
-
}, /* @__PURE__ */ React.createElement(Text, { color: "green" }, ASCII_ART), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " v", "1.16.
|
|
133
|
+
}, /* @__PURE__ */ React.createElement(Text, { color: "green" }, ASCII_ART), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " v", "1.16.2")), projectName && /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " Analyzing "), /* @__PURE__ */ React.createElement(Text, {
|
|
134
134
|
bold: true,
|
|
135
135
|
color: "white"
|
|
136
136
|
}, projectName)));
|
|
@@ -302,7 +302,7 @@ function App({ projectPath, checks, openWeb, enableAI, verbose, quotes, isMonore
|
|
|
302
302
|
if (openWeb) {
|
|
303
303
|
setPhase("opening-web");
|
|
304
304
|
try {
|
|
305
|
-
const { serveWeb } = await import("./web-
|
|
305
|
+
const { serveWeb } = await import("./web-CFBLfBdc.js");
|
|
306
306
|
const { default: openBrowser } = await import("open");
|
|
307
307
|
let aiService;
|
|
308
308
|
if (enableAI && process.env.ANTHROPIC_API_KEY) {
|
|
@@ -369,7 +369,7 @@ function App({ projectPath, checks, openWeb, enableAI, verbose, quotes, isMonore
|
|
|
369
369
|
if (openWeb) {
|
|
370
370
|
setPhase("opening-web");
|
|
371
371
|
try {
|
|
372
|
-
const { serveWeb } = await import("./web-
|
|
372
|
+
const { serveWeb } = await import("./web-CFBLfBdc.js");
|
|
373
373
|
const { default: openBrowser } = await import("open");
|
|
374
374
|
let aiService;
|
|
375
375
|
if (enableAI && process.env.ANTHROPIC_API_KEY) {
|
|
@@ -469,7 +469,7 @@ config({
|
|
|
469
469
|
quiet: true
|
|
470
470
|
});
|
|
471
471
|
const program = new Command();
|
|
472
|
-
program.name("sickbay").description("React project health check CLI").version("1.16.
|
|
472
|
+
program.name("sickbay").description("React project health check CLI").version("1.16.2", "-v, --version").enablePositionalOptions().passThroughOptions().option("-p, --path <path>", "project path to analyze", process.cwd()).option("-c, --checks <checks>", "comma-separated list of checks to run").option("--package <name>", "scope to a single named package (monorepo only)").option("--json", "output raw JSON report").option("--web", "open web dashboard after scan").option("--no-ai", "disable AI features").option("--no-quotes", "suppress personality quotes in output").option("--verbose", "show verbose output").action(async (options) => {
|
|
473
473
|
if (!existsSync(options.path)) {
|
|
474
474
|
process.stderr.write(`Error: path does not exist: ${options.path}\n`);
|
|
475
475
|
process.exit(1);
|
|
@@ -484,7 +484,7 @@ program.name("sickbay").description("React project health check CLI").version("1
|
|
|
484
484
|
const updatePromise = (async () => {
|
|
485
485
|
try {
|
|
486
486
|
const { checkForUpdate } = await import("./update-check-C7bNNfvf.js");
|
|
487
|
-
return await checkForUpdate("1.16.
|
|
487
|
+
return await checkForUpdate("1.16.2");
|
|
488
488
|
} catch {
|
|
489
489
|
return null;
|
|
490
490
|
}
|
|
@@ -654,7 +654,7 @@ program.command("stats").description("Show a quick codebase overview and project
|
|
|
654
654
|
}));
|
|
655
655
|
});
|
|
656
656
|
program.command("tui").description("Launch the persistent developer dashboard").option("-p, --path <path>", "project path to monitor", process.cwd()).option("--no-watch", "disable file-watching auto-refresh").option("--no-quotes", "suppress personality quotes in output").option("--refresh <seconds>", "auto-refresh interval in seconds", "300").option("-c, --checks <checks>", "comma-separated list of checks to run").action(async (options) => {
|
|
657
|
-
const { TuiApp } = await import("./TuiApp-
|
|
657
|
+
const { TuiApp } = await import("./TuiApp-LHb149v8.js");
|
|
658
658
|
const checks = options.checks ? options.checks.split(",").map((s) => s.trim()) : void 0;
|
|
659
659
|
render(React.createElement(TuiApp, {
|
|
660
660
|
projectPath: options.path,
|
|
@@ -689,7 +689,7 @@ program.command("badge").description("Generate a health score badge for your REA
|
|
|
689
689
|
const { resolveProject } = await import("./resolve-package-DS27JBhN.js");
|
|
690
690
|
const resolution = await resolveProject(options.path, options.package);
|
|
691
691
|
const projectPath = resolution.isMonorepo ? resolution.targetPath ?? options.path : resolution.targetPath;
|
|
692
|
-
const { loadScoreFromLastReport, badgeUrl, badgeMarkdown, badgeHtml } = await import("./badge-
|
|
692
|
+
const { loadScoreFromLastReport, badgeUrl, badgeMarkdown, badgeHtml } = await import("./badge-CTayKnYV.js");
|
|
693
693
|
let score = options.scan ? null : loadScoreFromLastReport(projectPath);
|
|
694
694
|
if (score === null) {
|
|
695
695
|
const { runSickbay } = await import("./dist-Bjhxbcak.js").then((n) => n.s);
|
|
@@ -94,6 +94,12 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
94
94
|
if (!distDir) throw new Error("Web dashboard assets not found. Try reinstalling: npx sickbay@latest --web");
|
|
95
95
|
const reportJson = JSON.stringify(report);
|
|
96
96
|
const port = await getFreePort(preferredPort);
|
|
97
|
+
const serverUrl = `http://localhost:${port}`;
|
|
98
|
+
const jsonHeaders = {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"Access-Control-Allow-Origin": serverUrl,
|
|
101
|
+
Vary: "Origin"
|
|
102
|
+
};
|
|
97
103
|
let aiSummary = null;
|
|
98
104
|
if (aiService && !("isMonorepo" in report)) try {
|
|
99
105
|
aiSummary = await aiService.generateSummary(report);
|
|
@@ -103,17 +109,17 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
103
109
|
const server = http.createServer(async (req, res) => {
|
|
104
110
|
const url = req.url ?? "/";
|
|
105
111
|
if (url === "/sickbay-report.json") {
|
|
106
|
-
res.writeHead(200,
|
|
112
|
+
res.writeHead(200, jsonHeaders);
|
|
107
113
|
res.end(reportJson);
|
|
108
114
|
return;
|
|
109
115
|
}
|
|
110
116
|
if (url === "/sickbay-history.json") {
|
|
111
117
|
const historyPath = join("isMonorepo" in report ? report.rootPath : report.projectPath, ".sickbay", "history.json");
|
|
112
118
|
if (existsSync(historyPath)) {
|
|
113
|
-
res.writeHead(200,
|
|
119
|
+
res.writeHead(200, jsonHeaders);
|
|
114
120
|
res.end(readFileSync(historyPath, "utf-8"));
|
|
115
121
|
} else {
|
|
116
|
-
res.writeHead(404,
|
|
122
|
+
res.writeHead(404, jsonHeaders);
|
|
117
123
|
res.end("{}");
|
|
118
124
|
}
|
|
119
125
|
return;
|
|
@@ -121,10 +127,10 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
121
127
|
if (url === "/sickbay-dep-tree.json") {
|
|
122
128
|
const treePath = join("isMonorepo" in report ? report.rootPath : report.projectPath, ".sickbay", "dep-tree.json");
|
|
123
129
|
if (existsSync(treePath)) {
|
|
124
|
-
res.writeHead(200,
|
|
130
|
+
res.writeHead(200, jsonHeaders);
|
|
125
131
|
res.end(readFileSync(treePath, "utf-8"));
|
|
126
132
|
} else {
|
|
127
|
-
res.writeHead(404,
|
|
133
|
+
res.writeHead(404, jsonHeaders);
|
|
128
134
|
res.end("{}");
|
|
129
135
|
}
|
|
130
136
|
return;
|
|
@@ -135,14 +141,14 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
135
141
|
const { loadConfig } = await import("./dist-Bjhxbcak.js").then((n) => n.s);
|
|
136
142
|
const config = await loadConfig(basePath);
|
|
137
143
|
if (config) {
|
|
138
|
-
res.writeHead(200,
|
|
144
|
+
res.writeHead(200, jsonHeaders);
|
|
139
145
|
res.end(JSON.stringify(config));
|
|
140
146
|
} else {
|
|
141
|
-
res.writeHead(404,
|
|
147
|
+
res.writeHead(404, jsonHeaders);
|
|
142
148
|
res.end("{}");
|
|
143
149
|
}
|
|
144
150
|
} catch {
|
|
145
|
-
res.writeHead(500,
|
|
151
|
+
res.writeHead(500, jsonHeaders);
|
|
146
152
|
res.end("{}");
|
|
147
153
|
}
|
|
148
154
|
return;
|
|
@@ -151,36 +157,35 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
151
157
|
const packageName = parsedUrl.searchParams.get("package");
|
|
152
158
|
if (parsedUrl.pathname === "/ai/summary") {
|
|
153
159
|
if (req.method === "HEAD") {
|
|
154
|
-
|
|
155
|
-
else res.writeHead(404);
|
|
160
|
+
res.writeHead(aiService ? 200 : 404, jsonHeaders);
|
|
156
161
|
res.end();
|
|
157
162
|
return;
|
|
158
163
|
}
|
|
159
164
|
if (packageName && aiService && "isMonorepo" in report) {
|
|
160
165
|
const pkg = report.packages.find((p) => p.name === packageName);
|
|
161
166
|
if (!pkg) {
|
|
162
|
-
res.writeHead(404,
|
|
167
|
+
res.writeHead(404, jsonHeaders);
|
|
163
168
|
res.end(JSON.stringify({ error: "Package not found" }));
|
|
164
169
|
return;
|
|
165
170
|
}
|
|
166
171
|
try {
|
|
167
172
|
const pkgReport = packageReportToSickbayReport(pkg, report);
|
|
168
173
|
const summary = await aiService.generateSummary(pkgReport);
|
|
169
|
-
res.writeHead(200,
|
|
174
|
+
res.writeHead(200, jsonHeaders);
|
|
170
175
|
res.end(JSON.stringify({ summary }));
|
|
171
176
|
} catch (err) {
|
|
172
177
|
const message = err instanceof Error ? err.message : "Internal error";
|
|
173
|
-
res.writeHead(500,
|
|
178
|
+
res.writeHead(500, jsonHeaders);
|
|
174
179
|
res.end(JSON.stringify({ error: message }));
|
|
175
180
|
}
|
|
176
181
|
return;
|
|
177
182
|
}
|
|
178
183
|
if (aiSummary) {
|
|
179
|
-
res.writeHead(200,
|
|
184
|
+
res.writeHead(200, jsonHeaders);
|
|
180
185
|
res.end(JSON.stringify({ summary: aiSummary }));
|
|
181
186
|
return;
|
|
182
187
|
}
|
|
183
|
-
res.writeHead(404,
|
|
188
|
+
res.writeHead(404, jsonHeaders);
|
|
184
189
|
res.end("{}");
|
|
185
190
|
return;
|
|
186
191
|
}
|
|
@@ -193,7 +198,7 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
193
198
|
body += chunk;
|
|
194
199
|
if (Buffer.byteLength(body) > MAX_BODY) {
|
|
195
200
|
aborted = true;
|
|
196
|
-
res.writeHead(413,
|
|
201
|
+
res.writeHead(413, jsonHeaders);
|
|
197
202
|
res.end(JSON.stringify({ error: "Payload too large" }));
|
|
198
203
|
req.destroy();
|
|
199
204
|
}
|
|
@@ -206,23 +211,23 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
206
211
|
if (packageName && "isMonorepo" in report) {
|
|
207
212
|
const pkg = report.packages.find((p) => p.name === packageName);
|
|
208
213
|
if (!pkg) {
|
|
209
|
-
res.writeHead(404,
|
|
214
|
+
res.writeHead(404, jsonHeaders);
|
|
210
215
|
res.end(JSON.stringify({ error: "Package not found" }));
|
|
211
216
|
return;
|
|
212
217
|
}
|
|
213
218
|
chatReport = packageReportToSickbayReport(pkg, report);
|
|
214
219
|
} else if (!("isMonorepo" in report)) chatReport = report;
|
|
215
220
|
else {
|
|
216
|
-
res.writeHead(400,
|
|
221
|
+
res.writeHead(400, jsonHeaders);
|
|
217
222
|
res.end(JSON.stringify({ error: "Package name required for monorepo" }));
|
|
218
223
|
return;
|
|
219
224
|
}
|
|
220
225
|
const response = await aiService.chat(message, chatReport, history ?? []);
|
|
221
|
-
res.writeHead(200,
|
|
226
|
+
res.writeHead(200, jsonHeaders);
|
|
222
227
|
res.end(JSON.stringify({ response }));
|
|
223
228
|
} catch (err) {
|
|
224
229
|
const errMessage = err instanceof Error ? err.message : "Internal error";
|
|
225
|
-
res.writeHead(500,
|
|
230
|
+
res.writeHead(500, jsonHeaders);
|
|
226
231
|
res.end(JSON.stringify({ error: errMessage }));
|
|
227
232
|
}
|
|
228
233
|
});
|
|
@@ -249,11 +254,11 @@ async function serveWeb(report, preferredPort = 3030, aiService) {
|
|
|
249
254
|
else reject(err);
|
|
250
255
|
});
|
|
251
256
|
server.listen(port, "127.0.0.1", () => {
|
|
252
|
-
resolve(
|
|
257
|
+
resolve(serverUrl);
|
|
253
258
|
});
|
|
254
259
|
});
|
|
255
260
|
}
|
|
256
261
|
//#endregion
|
|
257
262
|
export { serveWeb };
|
|
258
263
|
|
|
259
|
-
//# sourceMappingURL=web-
|
|
264
|
+
//# sourceMappingURL=web-CFBLfBdc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-CFBLfBdc.js","names":[],"sources":["../src/commands/web.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'fs';\nimport http from 'http';\nimport { join, extname, resolve, relative, isAbsolute } from 'path';\nimport { fileURLToPath } from 'url';\n\nimport type { AIService } from '../services/ai.js';\nimport type { SickbayReport, MonorepoReport } from 'sickbay-core';\n\n/**\n * This module implements a simple HTTP server to serve a web dashboard for visualizing Sickbay reports.\n * It dynamically serves the report data as JSON and provides endpoints for AI-generated summaries and chat interactions if an AI service is available.\n * The server also serves static files from the built dashboard located in the sickbay-web package. This allows users to interact with their health reports in a user-friendly web interface.\n */\n\nconst MIME_TYPES: Record<string, string> = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\n/**\n * Safely resolve a URL pathname to a file path within baseDir, rejecting\n * any attempt to escape the directory via `..`, absolute paths, URL-encoded\n * traversal (`%2e%2e`), or null bytes. Returns null if the path is unsafe.\n *\n * Note that `new URL()` (used by the request handler) already normalizes\n * `..` segments from the pathname before this function sees it, so in\n * practice most traversal attempts are neutralized upstream. This function\n * is defense-in-depth — it guards against null bytes, manually constructed\n * malformed paths, and any future code path that bypasses URL normalization.\n *\n * Even though the dashboard server binds to 127.0.0.1, the dashboard exposes\n * scan results including secret-scan findings — treat it like any public\n * static server.\n *\n * Exported for direct unit testing; not part of the stable public API.\n */\nexport function resolveSafeStaticPath(baseDir: string, pathname: string): string | null {\n let decoded: string;\n try {\n decoded = decodeURIComponent(pathname);\n } catch {\n return null;\n }\n if (decoded.includes('\\0')) return null;\n // Strip leading slashes so resolve() treats the path as relative to baseDir.\n const cleaned = decoded.replace(/^\\/+/, '');\n const resolved = resolve(baseDir, cleaned);\n const rel = relative(baseDir, resolved);\n // relative() returns '' when identical, '..'-prefixed when outside, or absolute on other drives (Windows).\n if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) {\n return resolved;\n }\n return null;\n}\n\nfunction findWebDist(): string | null {\n // Resolve relative to this file's location\n const thisFile = fileURLToPath(import.meta.url);\n const candidates = [\n join(thisFile, '..', 'web'), // published: dist/web/ (embedded at build time)\n join(thisFile, '..', '..', '..', 'web', 'dist'), // monorepo dev: apps/web/dist/\n ];\n for (const p of candidates) {\n if (existsSync(join(p, 'index.html'))) return p;\n }\n return null;\n}\n\nasync function getFreePort(preferred: number, attempts = 0): Promise<number> {\n if (attempts >= 10) {\n throw new Error(\n `No free port found after 10 attempts starting from port ${preferred - attempts}`,\n );\n }\n return new Promise((resolve, reject) => {\n const server = http.createServer();\n // Probe must bind to the same host as the real server (127.0.0.1), otherwise\n // Node defaults to `::` and we can miss an IPv4-loopback-only conflict —\n // the probe says \"free\", then the real listen crashes with EADDRINUSE.\n server.listen(preferred, '127.0.0.1', () => {\n const addr = server.address() as { port: number };\n server.close(() => resolve(addr.port));\n });\n server.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n resolve(getFreePort(preferred + 1, attempts + 1));\n } else {\n reject(err);\n }\n });\n });\n}\n\nfunction packageReportToSickbayReport(\n pkg: import('sickbay-core').PackageReport,\n parent: MonorepoReport,\n): SickbayReport {\n return {\n timestamp: parent.timestamp,\n projectPath: pkg.path,\n projectInfo: {\n name: pkg.name,\n version: 'unknown',\n hasTypeScript: false,\n hasESLint: false,\n hasPrettier: false,\n framework: pkg.framework,\n packageManager: parent.packageManager,\n totalDependencies:\n Object.keys(pkg.dependencies).length + Object.keys(pkg.devDependencies).length,\n dependencies: pkg.dependencies,\n devDependencies: pkg.devDependencies,\n },\n checks: pkg.checks,\n overallScore: pkg.score,\n summary: pkg.summary,\n };\n}\n\nexport async function serveWeb(\n report: SickbayReport | MonorepoReport,\n preferredPort = 3030,\n aiService?: AIService,\n): Promise<string> {\n const distDir = findWebDist();\n if (!distDir) {\n throw new Error('Web dashboard assets not found. Try reinstalling: npx sickbay@latest --web');\n }\n\n const reportJson = JSON.stringify(report);\n const port = await getFreePort(preferredPort);\n\n // Single source of truth for the dashboard URL. The server binds to\n // 127.0.0.1 but this string (used for both the returned URL and the\n // Access-Control-Allow-Origin header) must stay consistent — browser\n // origins are derived from the URL string the tab was opened at, not\n // from the resolved IP, so mixing `localhost` in one place and\n // `127.0.0.1` in another would produce a mismatch that only bites\n // when the user navigates via a different loopback representation.\n const serverUrl = `http://localhost:${port}`;\n\n // Lock API responses to the dashboard's own origin. Without this header,\n // browser default behavior is already to block cross-origin reads of the\n // response body (so an evil.com tab couldn't exfiltrate a report), but\n // declaring it explicitly is defense-in-depth: it makes the intent clear,\n // prevents future browser-policy relaxations from widening the surface,\n // and makes the server's contract auditable from response headers alone.\n const jsonHeaders = {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': serverUrl,\n Vary: 'Origin',\n };\n\n // Generate AI summary on startup if service is available (single-project only)\n let aiSummary: string | null = null;\n if (aiService && !('isMonorepo' in report)) {\n try {\n aiSummary = await aiService.generateSummary(report);\n } catch (err) {\n console.warn('AI summary generation failed:', err);\n }\n }\n\n const server = http.createServer(async (req, res) => {\n const url = req.url ?? '/';\n\n // Serve the report JSON directly from memory\n if (url === '/sickbay-report.json') {\n res.writeHead(200, jsonHeaders);\n res.end(reportJson);\n return;\n }\n\n // Serve project-local history\n if (url === '/sickbay-history.json') {\n const basePath = 'isMonorepo' in report ? report.rootPath : report.projectPath;\n const historyPath = join(basePath, '.sickbay', 'history.json');\n if (existsSync(historyPath)) {\n res.writeHead(200, jsonHeaders);\n res.end(readFileSync(historyPath, 'utf-8'));\n } else {\n res.writeHead(404, jsonHeaders);\n res.end('{}');\n }\n return;\n }\n\n // Serve dependency tree\n if (url === '/sickbay-dep-tree.json') {\n const basePath = 'isMonorepo' in report ? report.rootPath : report.projectPath;\n const treePath = join(basePath, '.sickbay', 'dep-tree.json');\n if (existsSync(treePath)) {\n res.writeHead(200, jsonHeaders);\n res.end(readFileSync(treePath, 'utf-8'));\n } else {\n res.writeHead(404, jsonHeaders);\n res.end('{}');\n }\n return;\n }\n\n // Serve project config\n if (url === '/sickbay-config.json') {\n const basePath = 'isMonorepo' in report ? report.rootPath : report.projectPath;\n try {\n const { loadConfig } = await import('sickbay-core');\n const config = await loadConfig(basePath);\n if (config) {\n res.writeHead(200, jsonHeaders);\n res.end(JSON.stringify(config));\n } else {\n res.writeHead(404, jsonHeaders);\n res.end('{}');\n }\n } catch {\n res.writeHead(500, jsonHeaders);\n res.end('{}');\n }\n return;\n }\n\n // AI summary endpoint\n const parsedUrl = new URL(url, 'http://localhost');\n const packageName = parsedUrl.searchParams.get('package');\n\n if (parsedUrl.pathname === '/ai/summary') {\n if (req.method === 'HEAD') {\n // Availability check. Include CORS headers for consistency with the\n // rest of the JSON endpoints even though opaque cross-origin responses\n // already hide the status from attacker scripts.\n res.writeHead(aiService ? 200 : 404, jsonHeaders);\n res.end();\n return;\n }\n if (packageName && aiService && 'isMonorepo' in report) {\n // Per-package summary: generate on demand\n const pkg = report.packages.find((p) => p.name === packageName);\n if (!pkg) {\n res.writeHead(404, jsonHeaders);\n res.end(JSON.stringify({ error: 'Package not found' }));\n return;\n }\n try {\n const pkgReport = packageReportToSickbayReport(pkg, report);\n const summary = await aiService.generateSummary(pkgReport);\n res.writeHead(200, jsonHeaders);\n res.end(JSON.stringify({ summary }));\n } catch (err) {\n // Don't leak internal error details (stack, constructor name, dep internals)\n // over the wire, even on loopback — other processes on the machine can reach it.\n const message = err instanceof Error ? err.message : 'Internal error';\n res.writeHead(500, jsonHeaders);\n res.end(JSON.stringify({ error: message }));\n }\n return;\n }\n if (aiSummary) {\n res.writeHead(200, jsonHeaders);\n res.end(JSON.stringify({ summary: aiSummary }));\n return;\n }\n res.writeHead(404, jsonHeaders);\n res.end('{}');\n return;\n }\n\n // AI chat endpoint\n if (parsedUrl.pathname === '/ai/chat' && req.method === 'POST' && aiService) {\n // Cap request body to prevent unbounded memory growth from a malicious\n // or misbehaving client on the loopback interface.\n const MAX_BODY = 1024 * 1024; // 1 MB\n let body = '';\n let aborted = false;\n req.on('data', (chunk) => {\n if (aborted) return;\n body += chunk;\n if (Buffer.byteLength(body) > MAX_BODY) {\n aborted = true;\n res.writeHead(413, jsonHeaders);\n res.end(JSON.stringify({ error: 'Payload too large' }));\n req.destroy();\n }\n });\n req.on('end', async () => {\n if (aborted) return;\n try {\n const { message, history } = JSON.parse(body);\n let chatReport: SickbayReport;\n if (packageName && 'isMonorepo' in report) {\n const pkg = report.packages.find((p) => p.name === packageName);\n if (!pkg) {\n res.writeHead(404, jsonHeaders);\n res.end(JSON.stringify({ error: 'Package not found' }));\n return;\n }\n chatReport = packageReportToSickbayReport(pkg, report);\n } else if (!('isMonorepo' in report)) {\n chatReport = report;\n } else {\n res.writeHead(400, jsonHeaders);\n res.end(JSON.stringify({ error: 'Package name required for monorepo' }));\n return;\n }\n const response = await aiService.chat(message, chatReport, history ?? []);\n res.writeHead(200, jsonHeaders);\n res.end(JSON.stringify({ response }));\n } catch (err) {\n // See above — no internal error leakage.\n const errMessage = err instanceof Error ? err.message : 'Internal error';\n res.writeHead(500, jsonHeaders);\n res.end(JSON.stringify({ error: errMessage }));\n }\n });\n return;\n }\n\n // Serve static files from dist. Use the parsed pathname (not the raw url)\n // so query strings are stripped, and route through resolveSafeStaticPath\n // to prevent path traversal (e.g. `/../../etc/passwd`, `%2e%2e/`).\n const requestedPath = parsedUrl.pathname === '/' ? 'index.html' : parsedUrl.pathname;\n const filePath = resolveSafeStaticPath(distDir, requestedPath);\n if (filePath && existsSync(filePath)) {\n const ext = extname(filePath);\n const mime = MIME_TYPES[ext] ?? 'application/octet-stream';\n res.writeHead(200, { 'Content-Type': mime });\n res.end(readFileSync(filePath));\n } else {\n // SPA fallback — serve index.html for all unknown routes\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end(readFileSync(join(distDir, 'index.html')));\n }\n });\n\n return new Promise((resolve, reject) => {\n // Close server on SIGINT/SIGTERM so the process can exit cleanly when\n // the user presses Ctrl+C. Without this the HTTP handle keeps the event\n // loop alive after Ink's render loop exits.\n const shutdown = () => server.close();\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n\n // Reject the promise on any listen failure (EADDRINUSE, EACCES, etc.)\n // instead of letting an unhandled 'error' event crash the process with a\n // raw Node stack trace. The CLI renders rejection messages nicely.\n server.once('error', (err: NodeJS.ErrnoException) => {\n process.off('SIGINT', shutdown);\n process.off('SIGTERM', shutdown);\n if (err.code === 'EADDRINUSE') {\n reject(new Error(`Port ${port} is already in use. Stop the other process or try again.`));\n } else {\n reject(err);\n }\n });\n\n // SECURITY: bind explicitly to loopback so the dashboard (which contains\n // file paths, dep lists, and secret-scan results) is not exposed to other\n // hosts on the LAN, container networks, or VPN peers.\n server.listen(port, '127.0.0.1', () => {\n resolve(serverUrl);\n });\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/dist/badge-DB3y0fWt.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
//#region src/commands/badge.ts
|
|
4
|
-
function getScoreColor(score) {
|
|
5
|
-
if (score >= 90) return "brightgreen";
|
|
6
|
-
if (score >= 80) return "green";
|
|
7
|
-
if (score >= 60) return "yellow";
|
|
8
|
-
return "red";
|
|
9
|
-
}
|
|
10
|
-
function encodeLabel(label) {
|
|
11
|
-
return label.replace(/-/g, "--").replace(/_/g, "__").replace(/ /g, "%20");
|
|
12
|
-
}
|
|
13
|
-
function badgeUrl(score, label = "sickbay") {
|
|
14
|
-
const color = getScoreColor(score);
|
|
15
|
-
return `https://img.shields.io/badge/${encodeLabel(label)}-${score}%2F100-${color}`;
|
|
16
|
-
}
|
|
17
|
-
function badgeMarkdown(score, label = "sickbay") {
|
|
18
|
-
return `})`;
|
|
19
|
-
}
|
|
20
|
-
function badgeHtml(score, label = "sickbay") {
|
|
21
|
-
return `<img src="${badgeUrl(score, label)}" alt="${label}" />`;
|
|
22
|
-
}
|
|
23
|
-
function loadScoreFromLastReport(projectPath) {
|
|
24
|
-
const filePath = join(projectPath, ".sickbay", "last-report.json");
|
|
25
|
-
if (!existsSync(filePath)) return null;
|
|
26
|
-
try {
|
|
27
|
-
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
28
|
-
if (typeof data.overallScore !== "number") return null;
|
|
29
|
-
return data.overallScore;
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
35
|
-
export { badgeHtml, badgeMarkdown, badgeUrl, loadScoreFromLastReport };
|
|
36
|
-
|
|
37
|
-
//# sourceMappingURL=badge-DB3y0fWt.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"badge-DB3y0fWt.js","names":[],"sources":["../src/commands/badge.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport function getScoreColor(score: number): string {\n if (score >= 90) return 'brightgreen';\n if (score >= 80) return 'green';\n if (score >= 60) return 'yellow';\n return 'red';\n}\n\nfunction encodeLabel(label: string): string {\n return label.replace(/-/g, '--').replace(/_/g, '__').replace(/ /g, '%20');\n}\n\nexport function badgeUrl(score: number, label = 'sickbay'): string {\n const color = getScoreColor(score);\n const encoded = encodeLabel(label);\n return `https://img.shields.io/badge/${encoded}-${score}%2F100-${color}`;\n}\n\nexport function badgeMarkdown(score: number, label = 'sickbay'): string {\n return `})`;\n}\n\nexport function badgeHtml(score: number, label = 'sickbay'): string {\n return `<img src=\"${badgeUrl(score, label)}\" alt=\"${label}\" />`;\n}\n\nexport function loadScoreFromLastReport(projectPath: string): number | null {\n const filePath = join(projectPath, '.sickbay', 'last-report.json');\n if (!existsSync(filePath)) return null;\n try {\n const data = JSON.parse(readFileSync(filePath, 'utf-8'));\n if (typeof data.overallScore !== 'number') return null;\n return data.overallScore;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;AAGA,SAAgB,cAAc,OAAuB;AACnD,KAAI,SAAS,GAAI,QAAO;AACxB,KAAI,SAAS,GAAI,QAAO;AACxB,KAAI,SAAS,GAAI,QAAO;AACxB,QAAO;;AAGT,SAAS,YAAY,OAAuB;AAC1C,QAAO,MAAM,QAAQ,MAAM,KAAK,CAAC,QAAQ,MAAM,KAAK,CAAC,QAAQ,MAAM,MAAM;;AAG3E,SAAgB,SAAS,OAAe,QAAQ,WAAmB;CACjE,MAAM,QAAQ,cAAc,MAAM;AAElC,QAAO,gCADS,YAAY,MAAM,CACa,GAAG,MAAM,SAAS;;AAGnE,SAAgB,cAAc,OAAe,QAAQ,WAAmB;AACtE,QAAO,KAAK,MAAM,IAAI,SAAS,OAAO,MAAM,CAAC;;AAG/C,SAAgB,UAAU,OAAe,QAAQ,WAAmB;AAClE,QAAO,aAAa,SAAS,OAAO,MAAM,CAAC,SAAS,MAAM;;AAG5D,SAAgB,wBAAwB,aAAoC;CAC1E,MAAM,WAAW,KAAK,aAAa,YAAY,mBAAmB;AAClE,KAAI,CAAC,WAAW,SAAS,CAAE,QAAO;AAClC,KAAI;EACF,MAAM,OAAO,KAAK,MAAM,aAAa,UAAU,QAAQ,CAAC;AACxD,MAAI,OAAO,KAAK,iBAAiB,SAAU,QAAO;AAClD,SAAO,KAAK;SACN;AACN,SAAO"}
|
package/dist/web-D5etye5Q.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"web-D5etye5Q.js","names":[],"sources":["../src/commands/web.ts"],"sourcesContent":["import { readFileSync, existsSync } from 'fs';\nimport http from 'http';\nimport { join, extname, resolve, relative, isAbsolute } from 'path';\nimport { fileURLToPath } from 'url';\n\nimport type { AIService } from '../services/ai.js';\nimport type { SickbayReport, MonorepoReport } from 'sickbay-core';\n\n/**\n * This module implements a simple HTTP server to serve a web dashboard for visualizing Sickbay reports.\n * It dynamically serves the report data as JSON and provides endpoints for AI-generated summaries and chat interactions if an AI service is available.\n * The server also serves static files from the built dashboard located in the sickbay-web package. This allows users to interact with their health reports in a user-friendly web interface.\n */\n\nconst MIME_TYPES: Record<string, string> = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\n/**\n * Safely resolve a URL pathname to a file path within baseDir, rejecting\n * any attempt to escape the directory via `..`, absolute paths, URL-encoded\n * traversal (`%2e%2e`), or null bytes. Returns null if the path is unsafe.\n *\n * Note that `new URL()` (used by the request handler) already normalizes\n * `..` segments from the pathname before this function sees it, so in\n * practice most traversal attempts are neutralized upstream. This function\n * is defense-in-depth — it guards against null bytes, manually constructed\n * malformed paths, and any future code path that bypasses URL normalization.\n *\n * Even though the dashboard server binds to 127.0.0.1, the dashboard exposes\n * scan results including secret-scan findings — treat it like any public\n * static server.\n *\n * Exported for direct unit testing; not part of the stable public API.\n */\nexport function resolveSafeStaticPath(baseDir: string, pathname: string): string | null {\n let decoded: string;\n try {\n decoded = decodeURIComponent(pathname);\n } catch {\n return null;\n }\n if (decoded.includes('\\0')) return null;\n // Strip leading slashes so resolve() treats the path as relative to baseDir.\n const cleaned = decoded.replace(/^\\/+/, '');\n const resolved = resolve(baseDir, cleaned);\n const rel = relative(baseDir, resolved);\n // relative() returns '' when identical, '..'-prefixed when outside, or absolute on other drives (Windows).\n if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) {\n return resolved;\n }\n return null;\n}\n\nfunction findWebDist(): string | null {\n // Resolve relative to this file's location\n const thisFile = fileURLToPath(import.meta.url);\n const candidates = [\n join(thisFile, '..', 'web'), // published: dist/web/ (embedded at build time)\n join(thisFile, '..', '..', '..', 'web', 'dist'), // monorepo dev: apps/web/dist/\n ];\n for (const p of candidates) {\n if (existsSync(join(p, 'index.html'))) return p;\n }\n return null;\n}\n\nasync function getFreePort(preferred: number, attempts = 0): Promise<number> {\n if (attempts >= 10) {\n throw new Error(\n `No free port found after 10 attempts starting from port ${preferred - attempts}`,\n );\n }\n return new Promise((resolve, reject) => {\n const server = http.createServer();\n // Probe must bind to the same host as the real server (127.0.0.1), otherwise\n // Node defaults to `::` and we can miss an IPv4-loopback-only conflict —\n // the probe says \"free\", then the real listen crashes with EADDRINUSE.\n server.listen(preferred, '127.0.0.1', () => {\n const addr = server.address() as { port: number };\n server.close(() => resolve(addr.port));\n });\n server.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n resolve(getFreePort(preferred + 1, attempts + 1));\n } else {\n reject(err);\n }\n });\n });\n}\n\nfunction packageReportToSickbayReport(\n pkg: import('sickbay-core').PackageReport,\n parent: MonorepoReport,\n): SickbayReport {\n return {\n timestamp: parent.timestamp,\n projectPath: pkg.path,\n projectInfo: {\n name: pkg.name,\n version: 'unknown',\n hasTypeScript: false,\n hasESLint: false,\n hasPrettier: false,\n framework: pkg.framework,\n packageManager: parent.packageManager,\n totalDependencies:\n Object.keys(pkg.dependencies).length + Object.keys(pkg.devDependencies).length,\n dependencies: pkg.dependencies,\n devDependencies: pkg.devDependencies,\n },\n checks: pkg.checks,\n overallScore: pkg.score,\n summary: pkg.summary,\n };\n}\n\nexport async function serveWeb(\n report: SickbayReport | MonorepoReport,\n preferredPort = 3030,\n aiService?: AIService,\n): Promise<string> {\n const distDir = findWebDist();\n if (!distDir) {\n throw new Error('Web dashboard assets not found. Try reinstalling: npx sickbay@latest --web');\n }\n\n const reportJson = JSON.stringify(report);\n const port = await getFreePort(preferredPort);\n\n // Generate AI summary on startup if service is available (single-project only)\n let aiSummary: string | null = null;\n if (aiService && !('isMonorepo' in report)) {\n try {\n aiSummary = await aiService.generateSummary(report);\n } catch (err) {\n console.warn('AI summary generation failed:', err);\n }\n }\n\n const server = http.createServer(async (req, res) => {\n const url = req.url ?? '/';\n\n // Serve the report JSON directly from memory\n if (url === '/sickbay-report.json') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(reportJson);\n return;\n }\n\n // Serve project-local history\n if (url === '/sickbay-history.json') {\n const basePath = 'isMonorepo' in report ? report.rootPath : report.projectPath;\n const historyPath = join(basePath, '.sickbay', 'history.json');\n if (existsSync(historyPath)) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(readFileSync(historyPath, 'utf-8'));\n } else {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end('{}');\n }\n return;\n }\n\n // Serve dependency tree\n if (url === '/sickbay-dep-tree.json') {\n const basePath = 'isMonorepo' in report ? report.rootPath : report.projectPath;\n const treePath = join(basePath, '.sickbay', 'dep-tree.json');\n if (existsSync(treePath)) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(readFileSync(treePath, 'utf-8'));\n } else {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end('{}');\n }\n return;\n }\n\n // Serve project config\n if (url === '/sickbay-config.json') {\n const basePath = 'isMonorepo' in report ? report.rootPath : report.projectPath;\n try {\n const { loadConfig } = await import('sickbay-core');\n const config = await loadConfig(basePath);\n if (config) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(config));\n } else {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end('{}');\n }\n } catch {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end('{}');\n }\n return;\n }\n\n // AI summary endpoint\n const parsedUrl = new URL(url, 'http://localhost');\n const packageName = parsedUrl.searchParams.get('package');\n\n if (parsedUrl.pathname === '/ai/summary') {\n if (req.method === 'HEAD') {\n // Availability check\n if (aiService) {\n res.writeHead(200);\n } else {\n res.writeHead(404);\n }\n res.end();\n return;\n }\n if (packageName && aiService && 'isMonorepo' in report) {\n // Per-package summary: generate on demand\n const pkg = report.packages.find((p) => p.name === packageName);\n if (!pkg) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Package not found' }));\n return;\n }\n try {\n const pkgReport = packageReportToSickbayReport(pkg, report);\n const summary = await aiService.generateSummary(pkgReport);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ summary }));\n } catch (err) {\n // Don't leak internal error details (stack, constructor name, dep internals)\n // over the wire, even on loopback — other processes on the machine can reach it.\n const message = err instanceof Error ? err.message : 'Internal error';\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: message }));\n }\n return;\n }\n if (aiSummary) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ summary: aiSummary }));\n return;\n }\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end('{}');\n return;\n }\n\n // AI chat endpoint\n if (parsedUrl.pathname === '/ai/chat' && req.method === 'POST' && aiService) {\n // Cap request body to prevent unbounded memory growth from a malicious\n // or misbehaving client on the loopback interface.\n const MAX_BODY = 1024 * 1024; // 1 MB\n let body = '';\n let aborted = false;\n req.on('data', (chunk) => {\n if (aborted) return;\n body += chunk;\n if (Buffer.byteLength(body) > MAX_BODY) {\n aborted = true;\n res.writeHead(413, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payload too large' }));\n req.destroy();\n }\n });\n req.on('end', async () => {\n if (aborted) return;\n try {\n const { message, history } = JSON.parse(body);\n let chatReport: SickbayReport;\n if (packageName && 'isMonorepo' in report) {\n const pkg = report.packages.find((p) => p.name === packageName);\n if (!pkg) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Package not found' }));\n return;\n }\n chatReport = packageReportToSickbayReport(pkg, report);\n } else if (!('isMonorepo' in report)) {\n chatReport = report;\n } else {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Package name required for monorepo' }));\n return;\n }\n const response = await aiService.chat(message, chatReport, history ?? []);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ response }));\n } catch (err) {\n // See above — no internal error leakage.\n const errMessage = err instanceof Error ? err.message : 'Internal error';\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errMessage }));\n }\n });\n return;\n }\n\n // Serve static files from dist. Use the parsed pathname (not the raw url)\n // so query strings are stripped, and route through resolveSafeStaticPath\n // to prevent path traversal (e.g. `/../../etc/passwd`, `%2e%2e/`).\n const requestedPath = parsedUrl.pathname === '/' ? 'index.html' : parsedUrl.pathname;\n const filePath = resolveSafeStaticPath(distDir, requestedPath);\n if (filePath && existsSync(filePath)) {\n const ext = extname(filePath);\n const mime = MIME_TYPES[ext] ?? 'application/octet-stream';\n res.writeHead(200, { 'Content-Type': mime });\n res.end(readFileSync(filePath));\n } else {\n // SPA fallback — serve index.html for all unknown routes\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end(readFileSync(join(distDir, 'index.html')));\n }\n });\n\n return new Promise((resolve, reject) => {\n // Close server on SIGINT/SIGTERM so the process can exit cleanly when\n // the user presses Ctrl+C. Without this the HTTP handle keeps the event\n // loop alive after Ink's render loop exits.\n const shutdown = () => server.close();\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n\n // Reject the promise on any listen failure (EADDRINUSE, EACCES, etc.)\n // instead of letting an unhandled 'error' event crash the process with a\n // raw Node stack trace. The CLI renders rejection messages nicely.\n server.once('error', (err: NodeJS.ErrnoException) => {\n process.off('SIGINT', shutdown);\n process.off('SIGTERM', shutdown);\n if (err.code === 'EADDRINUSE') {\n reject(new Error(`Port ${port} is already in use. Stop the other process or try again.`));\n } else {\n reject(err);\n }\n });\n\n // SECURITY: bind explicitly to loopback so the dashboard (which contains\n // file paths, dep lists, and secret-scan results) is not exposed to other\n // hosts on the LAN, container networks, or VPN peers.\n server.listen(port, '127.0.0.1', () => {\n resolve(`http://localhost:${port}`);\n });\n });\n}\n"],"mappings":"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"}
|