fix(doctor): UX improvements for diagnostics and daemon (#687)

* fix(doctor): UX improvements for diagnostics and daemon

- Add Repo Fingerprint check to detect when database belongs to a
  different repository (copied .beads dir or git remote URL change)
- Add interactive fix for repo fingerprint with options: update repo ID,
  reinitialize database, or skip
- Add visible warning when daemon takes >5s to start, recommending
  'bd doctor' for diagnosis
- Detect install method (Homebrew vs script) and show only relevant
  upgrade command
- Improve WARNINGS section:
  - Add icons (⚠ or ✖) next to each item
  - Color numbers by severity (yellow for warnings, red for errors)
  - Render entire error lines in red
  - Sort by severity (errors first)
  - Fix alignment with checkmarks above
- Use heavier fail icon (✖) for better visibility
- Add integration and validation tests for doctor fixes

* fix(lint): address errcheck and gosec warnings

- mol_bond.go: explicitly ignore ephStore.Close() error
- beads.go: add nosec for .gitignore file permissions (0644 is standard)
This commit is contained in:
Ryan
2025-12-22 01:25:23 -08:00
committed by GitHub
parent 9d30e80fdf
commit a11b20960a
12 changed files with 563 additions and 10 deletions

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
@@ -371,6 +372,8 @@ func applyFixList(path string, fixes []doctorCheck) {
err = fix.DatabaseVersion(path)
case "Schema Compatibility":
err = fix.SchemaCompatibility(path)
case "Repo Fingerprint":
err = fix.RepoFingerprint(path)
case "Git Merge Driver":
err = fix.MergeDriver(path)
case "Sync Branch Config":
@@ -586,7 +589,14 @@ func runDiagnostics(path string) doctorResult {
result.OverallOK = false
}
// Check 2b: Database integrity (bd-2au)
// Check 2b: Repo fingerprint (detects wrong database or URL change)
fingerprintCheck := convertWithCategory(doctor.CheckRepoFingerprint(path), doctor.CategoryCore)
result.Checks = append(result.Checks, fingerprintCheck)
if fingerprintCheck.Status == statusError {
result.OverallOK = false
}
// Check 2c: Database integrity (bd-2au)
integrityCheck := convertWithCategory(doctor.CheckDatabaseIntegrity(path), doctor.CategoryCore)
result.Checks = append(result.Checks, integrityCheck)
if integrityCheck.Status == statusError {
@@ -883,10 +893,30 @@ func printDiagnostics(result doctorResult) {
if len(warnings) > 0 {
fmt.Println()
fmt.Println(ui.RenderWarn(ui.IconWarn + " WARNINGS"))
for _, check := range warnings {
fmt.Printf(" %s: %s\n", check.Name, check.Message)
// Sort by severity: errors first, then warnings
sort.Slice(warnings, func(i, j int) bool {
// Errors (statusError) come before warnings (statusWarning)
if warnings[i].Status == statusError && warnings[j].Status != statusError {
return true
}
if warnings[i].Status != statusError && warnings[j].Status == statusError {
return false
}
return false // maintain original order within same severity
})
for i, check := range warnings {
// Show numbered items with icon and color based on status
// Errors get entire line in red, warnings just the number in yellow
line := fmt.Sprintf("%s: %s", check.Name, check.Message)
if check.Status == statusError {
fmt.Printf(" %s %s %s\n", ui.RenderFailIcon(), ui.RenderFail(fmt.Sprintf("%d.", i+1)), ui.RenderFail(line))
} else {
fmt.Printf(" %s %s %s\n", ui.RenderWarnIcon(), ui.RenderWarn(fmt.Sprintf("%d.", i+1)), line)
}
if check.Fix != "" {
fmt.Printf(" %s%s\n", ui.MutedStyle.Render(ui.TreeLast), check.Fix)
fmt.Printf(" %s%s\n", ui.MutedStyle.Render(ui.TreeLast), check.Fix)
}
}
} else {