doctor.go
1 package tui 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "github.com/charmbracelet/lipgloss" 12 13 "github.com/Kocoro-lab/ShanClaw/internal/client" 14 "github.com/Kocoro-lab/ShanClaw/internal/mcp" 15 "github.com/Kocoro-lab/ShanClaw/internal/permissions" 16 ) 17 18 type doctorCheck struct { 19 name string 20 ok bool 21 detail string 22 } 23 24 type doctorDoneMsg struct { 25 checks []doctorCheck 26 } 27 28 // runDoctorChecks runs all sync diagnostic checks. 29 func runDoctorChecks(shannonDir, apiKey string, perms *permissions.PermissionsConfig, mcpServers map[string]mcp.MCPServerConfig, toolCount int) []doctorCheck { 30 var checks []doctorCheck 31 32 // 1. Config directory 33 if shannonDir != "" { 34 cfgPath := filepath.Join(shannonDir, "config.yaml") 35 if _, err := os.Stat(cfgPath); err == nil { 36 checks = append(checks, doctorCheck{"Config", true, cfgPath}) 37 } else { 38 checks = append(checks, doctorCheck{"Config", false, "config.yaml not found in " + shannonDir}) 39 } 40 } else { 41 checks = append(checks, doctorCheck{"Config", false, "shannon directory not set"}) 42 } 43 44 // 2. API key 45 if apiKey != "" { 46 masked := "****" 47 if len(apiKey) > 4 { 48 masked = "****" + apiKey[len(apiKey)-4:] 49 } 50 checks = append(checks, doctorCheck{"API key", true, masked}) 51 } else { 52 checks = append(checks, doctorCheck{"API key", false, "not configured"}) 53 } 54 55 // 3. Tools 56 checks = append(checks, doctorCheck{"Tools", toolCount > 0, fmt.Sprintf("%d registered", toolCount)}) 57 58 // 4. Sessions dir writable 59 sessDir := filepath.Join(shannonDir, "sessions") 60 if tmpFile, err := os.CreateTemp(sessDir, ".doctor-check-*"); err == nil { 61 tmpFile.Close() 62 os.Remove(tmpFile.Name()) 63 checks = append(checks, doctorCheck{"Sessions dir", true, sessDir}) 64 } else { 65 checks = append(checks, doctorCheck{"Sessions dir", false, fmt.Sprintf("not writable: %v", err)}) 66 } 67 68 // 5. Permissions summary 69 if perms != nil { 70 checks = append(checks, doctorCheck{"Permissions", true, fmt.Sprintf("%d allowed, %d denied", len(perms.AllowedCommands), len(perms.DeniedCommands))}) 71 } else { 72 checks = append(checks, doctorCheck{"Permissions", true, "default (no rules)"}) 73 } 74 75 // 6. MCP servers 76 mcpCount := len(mcpServers) 77 if mcpCount > 0 { 78 checks = append(checks, doctorCheck{"MCP servers", true, fmt.Sprintf("%d configured", mcpCount)}) 79 } else { 80 checks = append(checks, doctorCheck{"MCP servers", false, "none configured"}) 81 } 82 83 return checks 84 } 85 86 // runDoctorWithHealth runs all checks including the async API health check. 87 func runDoctorWithHealth(shannonDir, apiKey, endpoint string, gw *client.GatewayClient, perms *permissions.PermissionsConfig, mcpServers map[string]mcp.MCPServerConfig, toolCount int) []doctorCheck { 88 checks := runDoctorChecks(shannonDir, apiKey, perms, mcpServers, toolCount) 89 90 // API health check (with timeout) 91 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 92 defer cancel() 93 if gw != nil { 94 if err := gw.Health(ctx); err == nil { 95 checks = append(checks, doctorCheck{"API reachable", true, endpoint}) 96 } else { 97 checks = append(checks, doctorCheck{"API reachable", false, fmt.Sprintf("%s: %v", endpoint, err)}) 98 } 99 } else { 100 checks = append(checks, doctorCheck{"API reachable", false, "gateway client not initialized"}) 101 } 102 103 return checks 104 } 105 106 func formatDoctorResults(checks []doctorCheck) string { 107 dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("243")) 108 okStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("42")) 109 failStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196")) 110 111 var sb strings.Builder 112 sb.WriteString(dimStyle.Render(" Diagnostics:") + "\n") 113 for _, c := range checks { 114 icon := okStyle.Render("[ok]") 115 if !c.ok { 116 icon = failStyle.Render("[!!]") 117 } 118 sb.WriteString(fmt.Sprintf(" %s %s: %s\n", icon, dimStyle.Render(c.name), dimStyle.Render(c.detail))) 119 } 120 return strings.TrimRight(sb.String(), "\n") 121 }