sickbay 1.16.0 → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DiffApp-DeNEtlY7.js +181 -0
- package/dist/DiffApp-DeNEtlY7.js.map +1 -0
- package/dist/DoctorApp-CaiqnhHH.js +415 -0
- package/dist/DoctorApp-CaiqnhHH.js.map +1 -0
- package/dist/FixApp-MWXohP60.js +337 -0
- package/dist/FixApp-MWXohP60.js.map +1 -0
- package/dist/StatsApp-BC4P4hII.js +372 -0
- package/dist/StatsApp-BC4P4hII.js.map +1 -0
- package/dist/TrendApp-DOBt7ITA.js +127 -0
- package/dist/TrendApp-DOBt7ITA.js.map +1 -0
- package/dist/TuiApp-D4Wn81Qv.js +1042 -0
- package/dist/TuiApp-D4Wn81Qv.js.map +1 -0
- package/dist/ai-DxA5r3hA.js +69 -0
- package/dist/ai-DxA5r3hA.js.map +1 -0
- package/dist/badge-DB3y0fWt.js +37 -0
- package/dist/badge-DB3y0fWt.js.map +1 -0
- package/dist/{claude-SN6IVGBI.js → claude-C5dUtWBJ.js} +13 -12
- package/dist/claude-C5dUtWBJ.js.map +1 -0
- package/dist/config.d.ts +27 -1
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -6
- package/dist/dist-Bjhxbcak.js +5431 -0
- package/dist/dist-Bjhxbcak.js.map +1 -0
- package/dist/history-DVegAwFp.js +83 -0
- package/dist/history-DVegAwFp.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +692 -564
- package/dist/index.js.map +1 -0
- package/dist/init-Dn7eDMEp.js +199 -0
- package/dist/init-Dn7eDMEp.js.map +1 -0
- package/dist/resolve-package-DS27JBhN.js +60 -0
- package/dist/resolve-package-DS27JBhN.js.map +1 -0
- package/dist/rolldown-runtime-CiIaOW0V.js +13 -0
- package/dist/trend-D-Hufthb.js +45 -0
- package/dist/trend-D-Hufthb.js.map +1 -0
- package/dist/update-check-C7bNNfvf.js +58 -0
- package/dist/update-check-C7bNNfvf.js.map +1 -0
- package/dist/web-D5etye5Q.js +259 -0
- package/dist/web-D5etye5Q.js.map +1 -0
- package/package.json +6 -5
- package/dist/DiffApp-ASQWWDLS.js +0 -172
- package/dist/DoctorApp-SMIZDD64.js +0 -440
- package/dist/FixApp-63PU5VF7.js +0 -351
- package/dist/StatsApp-CFZBEITZ.js +0 -356
- package/dist/TrendApp-BKGRSP6Q.js +0 -106
- package/dist/TuiApp-K7KP35RC.js +0 -980
- package/dist/ai-7DGOLNJX.js +0 -64
- package/dist/badge-KQ73KEIN.js +0 -41
- package/dist/chunk-352RAVRZ.js +0 -19
- package/dist/chunk-3OR2GFVE.js +0 -90
- package/dist/chunk-DJA6K3MN.js +0 -5564
- package/dist/chunk-LZOEMCE7.js +0 -62
- package/dist/chunk-MBVA75EM.js +0 -19
- package/dist/chunk-SHO3ZXTH.js +0 -23
- package/dist/chunk-TWCTPAXQ.js +0 -73
- package/dist/chunk-VBM6NZAY.js +0 -60
- package/dist/dist-LQY3FYCP.js +0 -64
- package/dist/history-XLNVZEDI.js +0 -14
- package/dist/init-DD2HOQE2.js +0 -217
- package/dist/resolve-package-774YTW5Q.js +0 -9
- package/dist/update-check-M7IFOMLJ.js +0 -12
- package/dist/web-2KWCYTW4.js +0 -269
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
//#region src/services/ai.ts
|
|
3
|
+
function createAIService(apiKey) {
|
|
4
|
+
const client = new Anthropic({ apiKey });
|
|
5
|
+
async function generateSummary(report) {
|
|
6
|
+
const prompt = `You are a code health analyst. Analyze this project health report and provide a structured summary with the following sections:
|
|
7
|
+
|
|
8
|
+
**Health Assessment**
|
|
9
|
+
A one-sentence overall health statement using the score (${report.overallScore}/100).
|
|
10
|
+
|
|
11
|
+
**Critical Issues**
|
|
12
|
+
List 2-3 most important issues to address. If none, say "No critical issues detected."
|
|
13
|
+
|
|
14
|
+
**What's Going Well**
|
|
15
|
+
Highlight 1-2 positive aspects or passing checks.
|
|
16
|
+
|
|
17
|
+
**Next Steps**
|
|
18
|
+
One actionable recommendation to improve the codebase.
|
|
19
|
+
|
|
20
|
+
Use markdown formatting (**bold** for headings). Be direct and specific.
|
|
21
|
+
|
|
22
|
+
Report:
|
|
23
|
+
${JSON.stringify(report, null, 2)}`;
|
|
24
|
+
const textBlock = (await client.messages.create({
|
|
25
|
+
model: "claude-sonnet-4-20250514",
|
|
26
|
+
max_tokens: 600,
|
|
27
|
+
messages: [{
|
|
28
|
+
role: "user",
|
|
29
|
+
content: prompt
|
|
30
|
+
}]
|
|
31
|
+
})).content.find((block) => block.type === "text");
|
|
32
|
+
return textBlock && "text" in textBlock ? textBlock.text : "Unable to generate summary.";
|
|
33
|
+
}
|
|
34
|
+
async function chat(message, report, history) {
|
|
35
|
+
const systemPrompt = `You are an expert code health assistant analyzing a project's Sickbay report.
|
|
36
|
+
|
|
37
|
+
The report contains:
|
|
38
|
+
- Overall score: ${report.overallScore}/100
|
|
39
|
+
- ${report.summary.critical} critical issues, ${report.summary.warnings} warnings, ${report.summary.info} info items
|
|
40
|
+
- Checks across dependencies, security, code quality, performance, and git health
|
|
41
|
+
|
|
42
|
+
Full report data:
|
|
43
|
+
${JSON.stringify(report, null, 2)}
|
|
44
|
+
|
|
45
|
+
Answer questions clearly and concisely. Reference specific checks, scores, and issues when relevant. Be helpful and actionable.`;
|
|
46
|
+
const messages = [...history.map((h) => ({
|
|
47
|
+
role: h.role,
|
|
48
|
+
content: h.content
|
|
49
|
+
})), {
|
|
50
|
+
role: "user",
|
|
51
|
+
content: message
|
|
52
|
+
}];
|
|
53
|
+
const textBlock = (await client.messages.create({
|
|
54
|
+
model: "claude-sonnet-4-20250514",
|
|
55
|
+
max_tokens: 1e3,
|
|
56
|
+
system: systemPrompt,
|
|
57
|
+
messages
|
|
58
|
+
})).content.find((block) => block.type === "text");
|
|
59
|
+
return textBlock && "text" in textBlock ? textBlock.text : "Unable to generate response.";
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
generateSummary,
|
|
63
|
+
chat
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { createAIService };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=ai-DxA5r3hA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-DxA5r3hA.js","names":[],"sources":["../src/services/ai.ts"],"sourcesContent":["import Anthropic from '@anthropic-ai/sdk';\n\nimport type { SickbayReport } from 'sickbay-core';\n\n/**\n * AIService provides methods to generate a natural language summary of the Sickbay report and to engage in a chat conversation about the report.\n * It uses the Anthropic API to process the report data and user messages, returning insightful and actionable responses.\n */\n\nexport interface AIService {\n generateSummary(report: SickbayReport): Promise<string>;\n chat(\n message: string,\n report: SickbayReport,\n history: Array<{ role: 'user' | 'assistant'; content: string }>,\n ): Promise<string>;\n}\n\nexport function createAIService(apiKey: string): AIService {\n const client = new Anthropic({ apiKey });\n\n async function generateSummary(report: SickbayReport): Promise<string> {\n const prompt = `You are a code health analyst. Analyze this project health report and provide a structured summary with the following sections:\n\n**Health Assessment**\nA one-sentence overall health statement using the score (${report.overallScore}/100).\n\n**Critical Issues**\nList 2-3 most important issues to address. If none, say \"No critical issues detected.\"\n\n**What's Going Well**\nHighlight 1-2 positive aspects or passing checks.\n\n**Next Steps**\nOne actionable recommendation to improve the codebase.\n\nUse markdown formatting (**bold** for headings). Be direct and specific.\n\nReport:\n${JSON.stringify(report, null, 2)}`;\n\n const response = await client.messages.create({\n model: 'claude-sonnet-4-20250514',\n max_tokens: 600,\n messages: [{ role: 'user', content: prompt }],\n });\n\n const textBlock = response.content.find((block) => block.type === 'text');\n return textBlock && 'text' in textBlock ? textBlock.text : 'Unable to generate summary.';\n }\n\n async function chat(\n message: string,\n report: SickbayReport,\n history: Array<{ role: 'user' | 'assistant'; content: string }>,\n ): Promise<string> {\n const systemPrompt = `You are an expert code health assistant analyzing a project's Sickbay report.\n\nThe report contains:\n- Overall score: ${report.overallScore}/100\n- ${report.summary.critical} critical issues, ${report.summary.warnings} warnings, ${report.summary.info} info items\n- Checks across dependencies, security, code quality, performance, and git health\n\nFull report data:\n${JSON.stringify(report, null, 2)}\n\nAnswer questions clearly and concisely. Reference specific checks, scores, and issues when relevant. Be helpful and actionable.`;\n\n const messages = [\n ...history.map((h) => ({\n role: h.role as 'user' | 'assistant',\n content: h.content,\n })),\n { role: 'user' as const, content: message },\n ];\n\n const response = await client.messages.create({\n model: 'claude-sonnet-4-20250514',\n max_tokens: 1000,\n system: systemPrompt,\n messages,\n });\n\n const textBlock = response.content.find((block) => block.type === 'text');\n return textBlock && 'text' in textBlock ? textBlock.text : 'Unable to generate response.';\n }\n\n return { generateSummary, chat };\n}\n"],"mappings":";;AAkBA,SAAgB,gBAAgB,QAA2B;CACzD,MAAM,SAAS,IAAI,UAAU,EAAE,QAAQ,CAAC;CAExC,eAAe,gBAAgB,QAAwC;EACrE,MAAM,SAAS;;;2DAGwC,OAAO,aAAa;;;;;;;;;;;;;;EAc7E,KAAK,UAAU,QAAQ,MAAM,EAAE;EAQ7B,MAAM,aANW,MAAM,OAAO,SAAS,OAAO;GAC5C,OAAO;GACP,YAAY;GACZ,UAAU,CAAC;IAAE,MAAM;IAAQ,SAAS;IAAQ,CAAC;GAC9C,CAAC,EAEyB,QAAQ,MAAM,UAAU,MAAM,SAAS,OAAO;AACzE,SAAO,aAAa,UAAU,YAAY,UAAU,OAAO;;CAG7D,eAAe,KACb,SACA,QACA,SACiB;EACjB,MAAM,eAAe;;;mBAGN,OAAO,aAAa;IACnC,OAAO,QAAQ,SAAS,oBAAoB,OAAO,QAAQ,SAAS,aAAa,OAAO,QAAQ,KAAK;;;;EAIvG,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;;;EAI9B,MAAM,WAAW,CACf,GAAG,QAAQ,KAAK,OAAO;GACrB,MAAM,EAAE;GACR,SAAS,EAAE;GACZ,EAAE,EACH;GAAE,MAAM;GAAiB,SAAS;GAAS,CAC5C;EASD,MAAM,aAPW,MAAM,OAAO,SAAS,OAAO;GAC5C,OAAO;GACP,YAAY;GACZ,QAAQ;GACR;GACD,CAAC,EAEyB,QAAQ,MAAM,UAAU,MAAM,SAAS,OAAO;AACzE,SAAO,aAAa,UAAU,YAAY,UAAU,OAAO;;AAG7D,QAAO;EAAE;EAAiB;EAAM"}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// src/commands/claude.ts
|
|
2
1
|
import { mkdirSync, writeFileSync } from "fs";
|
|
3
2
|
import { join } from "path";
|
|
4
|
-
|
|
3
|
+
//#region src/commands/claude.ts
|
|
4
|
+
const SKILL_CONTENT = `---
|
|
5
5
|
name: sickbay
|
|
6
6
|
description: Understand Sickbay health reports, scores, issues, config, and CLI commands
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# Sickbay
|
|
9
|
+
# Sickbay — Project Health Reports
|
|
10
10
|
|
|
11
11
|
Sickbay is a zero-config health check CLI for JS/TS projects. It produces structured
|
|
12
12
|
JSON reports scoring a project across five categories. Use this skill when you encounter
|
|
@@ -104,13 +104,14 @@ export default {
|
|
|
104
104
|
| \`sickbay stats\` | Quick codebase overview |
|
|
105
105
|
`;
|
|
106
106
|
function generateClaudeSkill(projectPath) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
const skillsDir = join(projectPath, ".claude", "skills");
|
|
108
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
109
|
+
const skillPath = join(skillsDir, "sickbay.md");
|
|
110
|
+
writeFileSync(skillPath, SKILL_CONTENT);
|
|
111
|
+
console.log(`Created ${skillPath}`);
|
|
112
|
+
console.log("\nClaude Code will now understand your Sickbay reports, config, and CLI commands.");
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
//#endregion
|
|
115
|
+
export { generateClaudeSkill };
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=claude-C5dUtWBJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-C5dUtWBJ.js","names":[],"sources":["../src/commands/claude.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from 'fs';\nimport { join } from 'path';\n\nconst SKILL_CONTENT = `---\nname: sickbay\ndescription: Understand Sickbay health reports, scores, issues, config, and CLI commands\n---\n\n# Sickbay — Project Health Reports\n\nSickbay is a zero-config health check CLI for JS/TS projects. It produces structured\nJSON reports scoring a project across five categories. Use this skill when you encounter\n\\`.sickbay/\\` files, \\`sickbay.config.ts\\`, or when asked to interpret health results.\n\n## Report Locations\n\n| File | Purpose |\n|------|---------|\n| \\`.sickbay/last-report.json\\` | Most recent scan result |\n| \\`.sickbay/baseline.json\\` | Committed team baseline (created by \\`sickbay init\\`) |\n| \\`.sickbay/history.json\\` | Local trend history (gitignored) |\n\n## Report Schema\n\n\\`\\`\\`ts\ninterface SickbayReport {\n overallScore: number; // 0-100 weighted average\n checks: CheckResult[]; // individual check results\n summary: { critical: number; warnings: number; info: number };\n projectInfo: { name, framework, packageManager, totalDependencies, ... };\n}\n\ninterface CheckResult {\n id: string; // e.g. \"knip\", \"npm-audit\", \"complexity\"\n category: \"dependencies\" | \"security\" | \"code-quality\" | \"performance\" | \"git\";\n score: number; // 0-100\n status: \"pass\" | \"warning\" | \"fail\" | \"skipped\";\n issues: Issue[];\n}\n\ninterface Issue {\n severity: \"critical\" | \"warning\" | \"info\";\n message: string;\n file?: string; // affected file path\n fix?: {\n command?: string; // shell command to run\n description: string; // human-readable fix\n codeChange?: { before: string; after: string };\n };\n}\n\\`\\`\\`\n\n## Score Interpretation\n\n| Range | Rating | Color |\n|-------|--------|-------|\n| 80-100 | Good | Green |\n| 60-79 | Fair | Yellow |\n| 0-59 | Needs work | Red |\n\nCategory weights: security 30%, dependencies 25%, code-quality 25%, performance 15%, git 5%.\n\n## Acting on Issues\n\n1. **Read the report**: parse \\`.sickbay/last-report.json\\`\n2. **Prioritize**: fix \\`critical\\` severity issues first\n3. **Use fix suggestions**: if \\`issue.fix.command\\` exists, run it; if \\`issue.fix.codeChange\\` exists, apply the diff\n4. **Verify**: re-run \\`sickbay\\` to confirm the score improved\n\n## Configuration (\\`sickbay.config.ts\\`)\n\n\\`\\`\\`ts\nexport default {\n checks: {\n knip: true, // enable (default)\n 'source-map-explorer': false, // disable entirely\n 'npm-audit': {\n suppress: [ // hide accepted findings (improves score)\n { match: 'lodash', reason: 'pinned, CVE does not affect us' },\n ],\n },\n complexity: {\n thresholds: { general: 15 }, // override default thresholds\n },\n },\n exclude: ['dist/**', 'coverage/**'], // global file exclusions\n weights: { security: 0.4 }, // override category weights\n}\n\\`\\`\\`\n\n## CLI Commands\n\n| Command | Purpose |\n|---------|---------|\n| \\`sickbay\\` | Run a full scan (terminal UI) |\n| \\`sickbay --json\\` | Output raw JSON report |\n| \\`sickbay --web\\` | Open web dashboard after scan |\n| \\`sickbay fix\\` | Interactively apply fix suggestions |\n| \\`sickbay diff <branch>\\` | Compare score against another branch |\n| \\`sickbay trend\\` | Show score history over time |\n| \\`sickbay init\\` | Scaffold config + baseline |\n| \\`sickbay doctor\\` | Diagnose project setup issues |\n| \\`sickbay badge\\` | Generate a README health badge |\n| \\`sickbay stats\\` | Quick codebase overview |\n`;\n\nexport function generateClaudeSkill(projectPath: string): void {\n const skillsDir = join(projectPath, '.claude', 'skills');\n mkdirSync(skillsDir, { recursive: true });\n\n const skillPath = join(skillsDir, 'sickbay.md');\n writeFileSync(skillPath, SKILL_CONTENT);\n\n console.log(`Created ${skillPath}`);\n console.log('\\nClaude Code will now understand your Sickbay reports, config, and CLI commands.');\n}\n"],"mappings":";;;AAGA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuGtB,SAAgB,oBAAoB,aAA2B;CAC7D,MAAM,YAAY,KAAK,aAAa,WAAW,SAAS;AACxD,WAAU,WAAW,EAAE,WAAW,MAAM,CAAC;CAEzC,MAAM,YAAY,KAAK,WAAW,aAAa;AAC/C,eAAc,WAAW,cAAc;AAEvC,SAAQ,IAAI,WAAW,YAAY;AACnC,SAAQ,IAAI,oFAAoF"}
|
package/dist/config.d.ts
CHANGED
|
@@ -1 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
//#region ../../packages/core/dist/index.d.ts
|
|
2
|
+
//#region src/config.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Scoring categories. Excludes 'unknown-category' from CheckResult['category']
|
|
5
|
+
* because unknown categories have no configurable weight (they fall back to 0.1 in scoring.ts).
|
|
6
|
+
*/
|
|
7
|
+
type Category = 'dependencies' | 'performance' | 'code-quality' | 'security' | 'git';
|
|
8
|
+
interface SickbayConfig {
|
|
9
|
+
checks?: Record<string, boolean | CheckConfig>;
|
|
10
|
+
exclude?: string[];
|
|
11
|
+
weights?: Partial<Record<Category, number>>;
|
|
12
|
+
}
|
|
13
|
+
interface CheckConfig {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
thresholds?: Record<string, unknown>;
|
|
16
|
+
exclude?: string[];
|
|
17
|
+
suppress?: SuppressionRule[];
|
|
18
|
+
}
|
|
19
|
+
interface SuppressionRule {
|
|
20
|
+
path?: string;
|
|
21
|
+
match?: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
}
|
|
24
|
+
declare function defineConfig(config: SickbayConfig): SickbayConfig;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { type Category, type CheckConfig, type SickbayConfig, type SuppressionRule, defineConfig };
|
|
27
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","names":["Category","SickbayConfig","CheckConfig","Record","Partial","checks","exclude","weights","SuppressionRule","enabled","thresholds","suppress","path","match","reason","ResolvedConfigMeta","hasCustomConfig","overriddenChecks","disabledChecks","defineConfig","config","getCheckConfig","checkId","isCheckDisabled","resolveConfigMeta","validateConfig","knownCheckIds","getUnlistedChecks","runnerNames","mergeConfigs","root","pkg","loadConfig","Promise","projectPath","Quote","text","source","severity","SickbayReport","ProjectInfo","CheckResult","Recommendation","timestamp","projectInfo","overallScore","summary","critical","warnings","info","recommendations","quote","name","version","hasTypeScript","hasESLint","hasPrettier","framework","packageManager","totalDependencies","dependencies","devDependencies","overrides","Issue","id","category","score","status","issues","toolsUsed","duration","metadata","FixSuggestion","message","file","fix","reportedBy","suppressMatch","command","description","modifiesSource","nextSteps","codeChange","before","after","Framework","title","learnMoreUrl","Runtime","BuildTool","TestFramework","ProjectContext","runtime","frameworks","buildTool","testFramework","ToolRunner","RunOptions","run","options","isApplicable","context","isApplicableToContext","Array","verbose","timeout","checkConfig","ToolResult","tool","data","error","MonorepoInfo","isMonorepo","type","packagePaths","PackageReport","relativePath","MonorepoReport","rootPath","monorepoType","packages","RunnerOptions","quotes","onRunnersReady","names","onCheckStart","onCheckComplete","result","onPackageStart","onPackageComplete","report","_config","getAvailableChecks","runSickbay","runSickbayMonorepo","DependencyTreeNode","DependencyTree","MonorepoDependencyTree","getDependencyTree","CATEGORY_WEIGHTS","normalizeWeights","userWeights","defaults","calculateOverallScore","buildSummary","getScoreColor","getScoreEmoji","createExcludeFilter","patterns","filePath","SuppressionResult","suppressedCount","applySuppression","rules","detectProject","detectPackageManager","detectContext","NotMonorepo","detectMonorepo","BaseAdvisor","WARN_LINES","CRITICAL_LINES","SCORE_EXCELLENT","SCORE_GOOD","SCORE_FAIR"],"sources":["../../../packages/core/dist/index.d.ts"],"mappings":";;;;;;KAKKA,QAAAA;AAAAA,UACKC,aAAAA;EACRI,MAAAA,GAASF,MAAAA,mBAAyBD,WAAAA;EAClCI,OAAAA;EACAC,OAAAA,GAAUH,OAAAA,CAAQD,MAAAA,CAAOH,QAAAA;AAAAA;AAAAA,UAEjBE,WAAAA;EACRO,OAAAA;EACAC,UAAAA,GAAaP,MAAAA;EACbG,OAAAA;EACAK,QAAAA,GAAWH,eAAAA;AAAAA;AAAAA,UAEHA,eAAAA;EACRI,IAAAA;EACAC,KAAAA;EACAC,MAAAA;AAAAA;AAAAA,iBAOeK,YAAAA,CAAaC,MAAAA,EAAQnB,aAAAA,GAAgBA,aAAAA"}
|
package/dist/config.js
CHANGED