Add bd doctor --fix flag to automatically repair issues (bd-ykd9)

Implements automatic fix capability for bd doctor with user confirmation
and security hardening.

Features:
- Organizes fix implementations under doctor/fix/ directory structure
- Shows all fixable issues and prompts for confirmation (Y/n) before applying
- Provides clear output about what was fixed and any errors encountered
- Re-runs diagnostics after fixes to show updated state
- Each fix is idempotent and safe to run multiple times

Automatic fixes implemented:
- Git hooks (runs bd hooks install)
- Daemon health issues (runs bd daemons killall)
- DB-JSONL sync problems (runs bd sync --import-only)
- File permissions (fixes .beads/ and database permissions)
- Database version mismatches (runs bd migrate)
- Schema compatibility issues (runs bd migrate)
- Gitignore updates (writes canonical template)

Security improvements:
- Prevents command injection by using os.Executable() instead of PATH lookup
- Prevents path traversal attacks with workspace validation
- Fixes race conditions by using cmd.Dir instead of os.Chdir()
- Corrects file permission logic (proper bit masking)
- Validates all operations run in beads workspaces only

Files changed:
- cmd/bd/doctor.go: Enhanced applyFixes() with confirmation and better UX
- cmd/bd/doctor/gitignore.go: Fixed permissions (0600 → 0644)
- cmd/bd/doctor/fix/common.go: Security helpers (getBdBinary, validateBeadsWorkspace)
- cmd/bd/doctor/fix/hooks.go: Git hooks fix
- cmd/bd/doctor/fix/daemon.go: Daemon health fix
- cmd/bd/doctor/fix/sync.go: DB-JSONL sync fix
- cmd/bd/doctor/fix/permissions.go: File permissions fix
- cmd/bd/doctor/fix/migrate.go: Database migration fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-20 19:33:12 -05:00
parent 4f481c88b1
commit 7806937b0a
8 changed files with 363 additions and 15 deletions

View File

@@ -17,6 +17,7 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/cmd/bd/doctor/fix"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/daemon"
@@ -133,19 +134,85 @@ func init() {
}
func applyFixes(result doctorResult) {
// Collect all fixable issues
var fixableIssues []doctorCheck
for _, check := range result.Checks {
if check.Status == statusWarning || check.Status == statusError {
switch check.Name {
case "Gitignore":
fmt.Println("Fixing .beads/.gitignore...")
if err := doctor.FixGitignore(); err != nil {
fmt.Fprintf(os.Stderr, " Error: %v\n", err)
} else {
fmt.Println(" ✓ Updated .beads/.gitignore")
}
}
if (check.Status == statusWarning || check.Status == statusError) && check.Fix != "" {
fixableIssues = append(fixableIssues, check)
}
}
if len(fixableIssues) == 0 {
fmt.Println("\nNo fixable issues found.")
return
}
// Show what will be fixed
fmt.Println("\nFixable issues:")
for i, issue := range fixableIssues {
fmt.Printf(" %d. %s: %s\n", i+1, issue.Name, issue.Message)
}
// Ask for confirmation
fmt.Printf("\nThis will attempt to fix %d issue(s). Continue? (Y/n): ", len(fixableIssues))
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
return
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "" && response != "y" && response != "yes" {
fmt.Println("Fix cancelled.")
return
}
// Apply fixes
fmt.Println("\nApplying fixes...")
fixedCount := 0
errorCount := 0
for _, check := range fixableIssues {
fmt.Printf("\nFixing %s...\n", check.Name)
var err error
switch check.Name {
case "Gitignore":
err = doctor.FixGitignore()
case "Git Hooks":
err = fix.GitHooks(result.Path)
case "Daemon Health":
err = fix.Daemon(result.Path)
case "DB-JSONL Sync":
err = fix.DBJSONLSync(result.Path)
case "Permissions":
err = fix.Permissions(result.Path)
case "Database":
err = fix.DatabaseVersion(result.Path)
case "Schema Compatibility":
err = fix.SchemaCompatibility(result.Path)
default:
fmt.Printf(" ⚠ No automatic fix available for %s\n", check.Name)
fmt.Printf(" Manual fix: %s\n", check.Fix)
continue
}
if err != nil {
errorCount++
color.Red(" ✗ Error: %v\n", err)
fmt.Printf(" Manual fix: %s\n", check.Fix)
} else {
fixedCount++
color.Green(" ✓ Fixed\n")
}
}
// Summary
fmt.Printf("\nFix summary: %d fixed, %d errors\n", fixedCount, errorCount)
if errorCount > 0 {
fmt.Println("\nSome fixes failed. Please review the errors above and apply manual fixes as needed.")
}
}
func runDiagnostics(path string) doctorResult {