package cmd import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "runtime" "github.com/spf13/cobra" "github.com/warrenronsiek/ctask/internal/config" "github.com/warrenronsiek/ctask/internal/workspace" ) var doctorCmd = &cobra.Command{ Use: "doctor", Short: "Verify ctask setup and diagnose configuration problems", Args: cobra.NoArgs, SilenceUsage: true, RunE: runDoctor, } func init() { rootCmd.AddCommand(doctorCmd) } func runDoctor(cmd *cobra.Command, args []string) error { passed := 0 failed := 0 // Check 1: Workspace root exists and is writable root := config.ResolveRoot() if info, err := os.Stat(root); err == nil && info.IsDir() { // Test writability by creating a temp file testFile := filepath.Join(root, ".ctask-doctor-test") if err := os.WriteFile(testFile, []byte("test"), 0644); err == nil { os.Remove(testFile) fmt.Printf(" [PASS] Workspace root exists: %s\n", root) passed++ } else { fmt.Printf(" [FAIL] Workspace root not writable: %s\n", root) fmt.Printf(" Fix: check permissions on %s\n", root) failed++ } } else { fmt.Printf(" [FAIL] Workspace root not found: %s\n", root) fmt.Printf(" Fix: create it with: mkdir %s\n", root) failed++ } // Check 2: Default agent on PATH agent := config.ResolveAgent() if _, err := exec.LookPath(agent); err == nil { fmt.Printf(" [PASS] Default agent found: %s\n", agent) passed++ } else { fmt.Printf(" [FAIL] Agent command not found: %s\n", agent) fmt.Printf(" Fix: install it or set CTASK_AGENT to a different command\n") failed++ } // Check 3: Status line helper exists // The canonical location is next to the ctask binary in GOPATH/bin, // or on Unix in ~/.local/bin. We check both the bash and powershell variants. statusLineFound := false statusLinePath := "" // Determine expected locations based on platform var searchPaths []string home, _ := os.UserHomeDir() gopath := os.Getenv("GOPATH") if gopath == "" { gopath = filepath.Join(home, "go") } gopathBin := filepath.Join(gopath, "bin") if runtime.GOOS == "windows" { searchPaths = []string{ filepath.Join(gopathBin, "ctask-statusline.sh"), filepath.Join(gopathBin, "ctask-statusline.ps1"), } } else { searchPaths = []string{ filepath.Join(home, ".local", "bin", "ctask-statusline.sh"), filepath.Join(gopathBin, "ctask-statusline.sh"), "/usr/local/bin/ctask-statusline.sh", } } for _, p := range searchPaths { if _, err := os.Stat(p); err == nil { statusLineFound = true statusLinePath = p break } } if statusLineFound { fmt.Printf(" [PASS] Status line helper found: %s\n", statusLinePath) passed++ } else { fmt.Printf(" [FAIL] Status line helper not found\n") fmt.Printf(" Fix: copy ctask-statusline.sh to %s\n", searchPaths[0]) failed++ } // Check 4: Claude Code status line configured claudeSettingsPath := filepath.Join(home, ".claude", "settings.json") statusLineConfigured := false if data, err := os.ReadFile(claudeSettingsPath); err == nil { var settings map[string]interface{} if err := json.Unmarshal(data, &settings); err == nil { if sl, ok := settings["statusLine"].(map[string]interface{}); ok { slType, _ := sl["type"].(string) slCmd, _ := sl["command"].(string) if slType == "command" && slCmd != "" { // Check if the referenced file in the command exists // Extract the file path from the command (last argument typically) statusLineConfigured = true fmt.Printf(" [PASS] Claude Code status line configured\n") passed++ } } } } if !statusLineConfigured { fmt.Printf(" [FAIL] Claude Code status line not configured or misconfigured\n") fmt.Printf(" Fix: add to %s:\n", claudeSettingsPath) if runtime.GOOS == "windows" { fmt.Printf(" \"statusLine\": {\"type\": \"command\", \"command\": \"bash /c/Users//go/bin/ctask-statusline.sh\"}\n") } else { fmt.Printf(" \"statusLine\": {\"type\": \"command\", \"command\": \"bash ~/.local/bin/ctask-statusline.sh\"}\n") } failed++ } // Check 5: Workspace root has workspaces results, err := workspace.ListWorkspaces(root, workspace.ListOpts{ IncludeArchived: true, Limit: 0, // no limit }) if err == nil && len(results) > 0 { activeCount := 0 archivedCount := 0 for _, r := range results { if r.Meta.Status == "archived" { archivedCount++ } else { activeCount++ } } fmt.Printf(" [PASS] Workspaces found: %d tasks", len(results)) if archivedCount > 0 { fmt.Printf(" (%d archived)", archivedCount) } fmt.Println() passed++ } else { fmt.Printf(" [FAIL] No workspaces found\n") fmt.Printf(" Fix: create one with: ctask new \"my first task\"\n") failed++ } // Summary fmt.Println() fmt.Printf("%d checks passed, %d failed\n", passed, failed) if failed > 0 { return fmt.Errorf("%d checks failed", failed) } return nil }