Import beads' UX design system into gastown: - Add internal/ui/ package with Ayu theme colors and semantic styling - styles.go: AdaptiveColor definitions for light/dark mode - terminal.go: TTY detection, NO_COLOR/CLICOLOR support - markdown.go: Glamour rendering with agent mode bypass - pager.go: Smart paging with GT_PAGER support - Add colorized help output (internal/cmd/help.go) - Group headers in accent color - Command names styled for scannability - Flag types and defaults muted - Add gt thanks command (internal/cmd/thanks.go) - Contributor display with same logic as bd thanks - Styled with Ayu theme colors - Update gt doctor to match bd doctor UX - Category grouping (Core, Infrastructure, Rig, Patrol, etc.) - Semantic icons (✓ ⚠ ✖) with Ayu colors - Tree connectors for detail lines - Summary line with pass/warn/fail counts - Warnings section at end with numbered issues - Migrate existing styles to use ui package - internal/style/style.go uses ui.ColorPass etc. - internal/tui/feed/styles.go uses ui package colors Co-Authored-By: SageOx <ox@sageox.ai>
187 lines
4.1 KiB
Go
187 lines
4.1 KiB
Go
package doctor
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// CrashReportCheck looks for recent macOS crash reports related to tmux or Claude.
|
|
// This helps diagnose mass session death events.
|
|
type CrashReportCheck struct {
|
|
BaseCheck
|
|
crashReports []crashReport // Cached during Run for display
|
|
}
|
|
|
|
// crashReport represents a found crash report file.
|
|
type crashReport struct {
|
|
path string
|
|
name string
|
|
modTime time.Time
|
|
process string // "tmux", "claude", "node", etc.
|
|
}
|
|
|
|
// NewCrashReportCheck creates a new crash report check.
|
|
func NewCrashReportCheck() *CrashReportCheck {
|
|
return &CrashReportCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: "crash-reports",
|
|
CheckDescription: "Check for recent macOS crash reports (tmux, Claude)",
|
|
CheckCategory: CategoryCleanup,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Run checks for recent crash reports in macOS diagnostic directories.
|
|
func (c *CrashReportCheck) Run(ctx *CheckContext) *CheckResult {
|
|
// Only run on macOS
|
|
if runtime.GOOS != "darwin" {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: "Crash report check not applicable (non-macOS)",
|
|
}
|
|
}
|
|
|
|
// Look for crash reports in the last 24 hours
|
|
lookbackWindow := 24 * time.Hour
|
|
cutoff := time.Now().Add(-lookbackWindow)
|
|
|
|
// macOS crash report locations
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: "Could not determine home directory",
|
|
Details: []string{err.Error()},
|
|
}
|
|
}
|
|
|
|
crashDirs := []string{
|
|
filepath.Join(homeDir, "Library", "Logs", "DiagnosticReports"),
|
|
"/Library/Logs/DiagnosticReports",
|
|
}
|
|
|
|
// Processes we care about
|
|
relevantProcesses := []string{
|
|
"tmux",
|
|
"claude",
|
|
"claude-code",
|
|
"node",
|
|
}
|
|
|
|
var reports []crashReport
|
|
|
|
for _, dir := range crashDirs {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue // Directory may not exist
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
name := entry.Name()
|
|
|
|
// Check if this is a crash report for a relevant process
|
|
var matchedProcess string
|
|
nameLower := strings.ToLower(name)
|
|
for _, proc := range relevantProcesses {
|
|
if strings.Contains(nameLower, proc) {
|
|
matchedProcess = proc
|
|
break
|
|
}
|
|
}
|
|
|
|
if matchedProcess == "" {
|
|
continue
|
|
}
|
|
|
|
// Check modification time
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if info.ModTime().Before(cutoff) {
|
|
continue // Too old
|
|
}
|
|
|
|
reports = append(reports, crashReport{
|
|
path: filepath.Join(dir, name),
|
|
name: name,
|
|
modTime: info.ModTime(),
|
|
process: matchedProcess,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort by time (most recent first)
|
|
sort.Slice(reports, func(i, j int) bool {
|
|
return reports[i].modTime.After(reports[j].modTime)
|
|
})
|
|
|
|
// Cache for display
|
|
c.crashReports = reports
|
|
|
|
if len(reports) == 0 {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: "No recent crash reports found",
|
|
}
|
|
}
|
|
|
|
// Group by process
|
|
processCounts := make(map[string]int)
|
|
for _, r := range reports {
|
|
processCounts[r.process]++
|
|
}
|
|
|
|
// Build details
|
|
var details []string
|
|
for _, r := range reports {
|
|
age := time.Since(r.modTime).Round(time.Minute)
|
|
details = append(details, fmt.Sprintf("%s (%s ago): %s", r.process, age, r.name))
|
|
}
|
|
|
|
// Build summary
|
|
var summary []string
|
|
for proc, count := range processCounts {
|
|
summary = append(summary, fmt.Sprintf("%d %s", count, proc))
|
|
}
|
|
|
|
message := fmt.Sprintf("Found %d crash report(s): %s", len(reports), strings.Join(summary, ", "))
|
|
|
|
// tmux crashes are particularly concerning
|
|
status := StatusWarning
|
|
if processCounts["tmux"] > 0 {
|
|
message += " - TMUX CRASHED (may explain session deaths)"
|
|
}
|
|
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: status,
|
|
Message: message,
|
|
Details: details,
|
|
FixHint: "Review crash reports in Console.app → User Reports or check ~/Library/Logs/DiagnosticReports/",
|
|
}
|
|
}
|
|
|
|
// Fix does nothing - crash reports are informational.
|
|
func (c *CrashReportCheck) Fix(ctx *CheckContext) error {
|
|
return nil
|
|
}
|
|
|
|
// CanFix returns false - crash reports cannot be auto-fixed.
|
|
func (c *CrashReportCheck) CanFix() bool {
|
|
return false
|
|
}
|