package doctor import ( "database/sql" "fmt" "os" "path/filepath" "github.com/steveyegge/beads/internal/daemon" "github.com/steveyegge/beads/internal/git" "github.com/steveyegge/beads/internal/syncbranch" ) // CheckDaemonStatus checks the health of the daemon for a workspace. // It checks for stale sockets, multiple daemons, and version mismatches. func CheckDaemonStatus(path string, cliVersion string) DoctorCheck { // Normalize path for reliable comparison (handles symlinks) wsNorm, err := filepath.EvalSymlinks(path) if err != nil { // Fallback to absolute path if EvalSymlinks fails wsNorm, _ = filepath.Abs(path) } // Use global daemon discovery (registry-based) daemons, err := daemon.DiscoverDaemons(nil) if err != nil { return DoctorCheck{ Name: "Daemon Health", Status: StatusWarning, Message: "Unable to check daemon health", Detail: err.Error(), } } // Filter to this workspace using normalized paths var workspaceDaemons []daemon.DaemonInfo for _, d := range daemons { dPath, err := filepath.EvalSymlinks(d.WorkspacePath) if err != nil { dPath, _ = filepath.Abs(d.WorkspacePath) } if dPath == wsNorm { workspaceDaemons = append(workspaceDaemons, d) } } // Check for stale socket directly (catches cases where RPC failed so WorkspacePath is empty) // Follow redirect to resolve actual beads directory (bd-tvus fix) beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) socketPath := filepath.Join(beadsDir, "bd.sock") if _, err := os.Stat(socketPath); err == nil { // Socket exists - try to connect if len(workspaceDaemons) == 0 { // Socket exists but no daemon found in registry - likely stale return DoctorCheck{ Name: "Daemon Health", Status: StatusWarning, Message: "Stale daemon socket detected", Detail: fmt.Sprintf("Socket exists at %s but daemon is not responding", socketPath), Fix: "Run 'bd daemons killall' to clean up stale sockets", } } } if len(workspaceDaemons) == 0 { return DoctorCheck{ Name: "Daemon Health", Status: StatusOK, Message: "No daemon running (will auto-start on next command)", } } // Warn if multiple daemons for same workspace if len(workspaceDaemons) > 1 { return DoctorCheck{ Name: "Daemon Health", Status: StatusWarning, Message: fmt.Sprintf("Multiple daemons detected for this workspace (%d)", len(workspaceDaemons)), Fix: "Run 'bd daemons killall' to clean up duplicate daemons", } } // Check for stale or version mismatched daemons for _, d := range workspaceDaemons { if !d.Alive { return DoctorCheck{ Name: "Daemon Health", Status: StatusWarning, Message: "Stale daemon detected", Detail: fmt.Sprintf("PID %d is not alive", d.PID), Fix: "Run 'bd daemons killall' to clean up stale daemons", } } if d.Version != cliVersion { return DoctorCheck{ Name: "Daemon Health", Status: StatusWarning, Message: fmt.Sprintf("Version mismatch (daemon: %s, CLI: %s)", d.Version, cliVersion), Fix: "Run 'bd daemons killall' to restart daemons with current version", } } } return DoctorCheck{ Name: "Daemon Health", Status: StatusOK, Message: fmt.Sprintf("Daemon running (PID %d, version %s)", workspaceDaemons[0].PID, workspaceDaemons[0].Version), } } // CheckVersionMismatch checks if the database version matches the CLI version. // Returns a warning message if there's a mismatch, or empty string if versions match or can't be read. func CheckVersionMismatch(db *sql.DB, cliVersion string) string { var dbVersion string err := db.QueryRow("SELECT value FROM metadata WHERE key = 'bd_version'").Scan(&dbVersion) if err != nil { return "" // Can't read version, skip } if dbVersion != "" && dbVersion != cliVersion { return fmt.Sprintf("Version mismatch (CLI: %s, database: %s)", cliVersion, dbVersion) } return "" } // CheckGitSyncSetup checks if git repository and sync-branch are configured for daemon sync. // This is informational - beads works fine without git sync, but users may want to enable it. func CheckGitSyncSetup(path string) DoctorCheck { // Check if we're in a git repository _, err := git.GetGitDir() if err != nil { return DoctorCheck{ Name: "Git Sync Setup", Status: StatusWarning, Message: "No git repository (background sync unavailable)", Detail: "The daemon requires a git repository for background sync. Without it, beads runs in direct mode.", Fix: "Run 'git init' to enable background sync", Category: CategoryRuntime, } } // Git repo exists - check if sync-branch is configured if !syncbranch.IsConfigured() { return DoctorCheck{ Name: "Git Sync Setup", Status: StatusOK, Message: "Git repository detected (sync-branch not configured)", Detail: "Beads commits directly to current branch. For team collaboration or to keep beads changes isolated, consider using a sync-branch.", Fix: "Run 'bd config set sync.branch beads-sync' to use a dedicated branch for beads metadata", Category: CategoryRuntime, } } return DoctorCheck{ Name: "Git Sync Setup", Status: StatusOK, Message: "Git repository and sync-branch configured", Category: CategoryRuntime, } }