* 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)
127 lines
3.5 KiB
Go
127 lines
3.5 KiB
Go
package fix
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// readLineUnbuffered reads a line from stdin without buffering.
|
|
// This ensures subprocess stdin isn't consumed by our buffered reader.
|
|
func readLineUnbuffered() (string, error) {
|
|
var result []byte
|
|
buf := make([]byte, 1)
|
|
for {
|
|
n, err := os.Stdin.Read(buf)
|
|
if err != nil {
|
|
return string(result), err
|
|
}
|
|
if n == 1 {
|
|
c := buf[0] // #nosec G602 -- n==1 guarantees buf has 1 byte
|
|
if c == '\n' {
|
|
return string(result), nil
|
|
}
|
|
result = append(result, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RepoFingerprint fixes repo fingerprint mismatches by prompting the user
|
|
// for which action to take. This is interactive because the consequences
|
|
// differ significantly between options:
|
|
// 1. Update repo ID (if URL changed or bd upgraded)
|
|
// 2. Reinitialize database (if wrong database was copied)
|
|
// 3. Skip (do nothing)
|
|
func RepoFingerprint(path string) error {
|
|
// Validate workspace
|
|
if err := validateBeadsWorkspace(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get bd binary path
|
|
bdBinary, err := getBdBinary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prompt user for action
|
|
fmt.Println("\n Repo fingerprint mismatch detected. Choose an action:")
|
|
fmt.Println()
|
|
fmt.Println(" [1] Update repo ID (if git remote URL changed or bd was upgraded)")
|
|
fmt.Println(" [2] Reinitialize database (if wrong .beads was copied here)")
|
|
fmt.Println(" [s] Skip (do nothing)")
|
|
fmt.Println()
|
|
fmt.Print(" Choice [1/2/s]: ")
|
|
|
|
// Read single character without buffering to avoid consuming input meant for subprocesses
|
|
response, err := readLineUnbuffered()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read input: %w", err)
|
|
}
|
|
|
|
response = strings.TrimSpace(strings.ToLower(response))
|
|
|
|
switch response {
|
|
case "1":
|
|
// Run bd migrate --update-repo-id
|
|
fmt.Println(" → Running 'bd migrate --update-repo-id'...")
|
|
cmd := exec.Command(bdBinary, "migrate", "--update-repo-id") // #nosec G204 -- bdBinary from validated executable path
|
|
cmd.Dir = path
|
|
cmd.Stdin = os.Stdin // Allow user to respond to migrate's confirmation prompt
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to update repo ID: %w", err)
|
|
}
|
|
return nil
|
|
|
|
case "2":
|
|
// Confirm before destructive action
|
|
fmt.Print(" ⚠️ This will DELETE .beads/beads.db. Continue? [y/N]: ")
|
|
confirm, err := readLineUnbuffered()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read confirmation: %w", err)
|
|
}
|
|
confirm = strings.TrimSpace(strings.ToLower(confirm))
|
|
if confirm != "y" && confirm != "yes" {
|
|
fmt.Println(" → Skipped (canceled)")
|
|
return nil
|
|
}
|
|
|
|
// Remove database and reinitialize
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
dbPath := filepath.Join(beadsDir, "beads.db")
|
|
|
|
fmt.Printf(" → Removing %s...\n", dbPath)
|
|
if err := os.Remove(dbPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to remove database: %w", err)
|
|
}
|
|
|
|
// Also remove WAL and SHM files if they exist
|
|
_ = os.Remove(dbPath + "-wal")
|
|
_ = os.Remove(dbPath + "-shm")
|
|
|
|
fmt.Println(" → Running 'bd init'...")
|
|
cmd := exec.Command(bdBinary, "init", "--quiet") // #nosec G204 -- bdBinary from validated executable path
|
|
cmd.Dir = path
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to initialize database: %w", err)
|
|
}
|
|
return nil
|
|
|
|
case "s", "":
|
|
fmt.Println(" → Skipped")
|
|
return nil
|
|
|
|
default:
|
|
fmt.Printf(" → Unrecognized input '%s', skipping\n", response)
|
|
return nil
|
|
}
|
|
}
|