222 lines
6.0 KiB
Go
222 lines
6.0 KiB
Go
package doctor
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
_ "github.com/ncruces/go-sqlite3/driver"
|
|
_ "github.com/ncruces/go-sqlite3/embed"
|
|
"github.com/steveyegge/beads/cmd/bd/doctor/fix"
|
|
"github.com/steveyegge/beads/internal/beads"
|
|
"github.com/steveyegge/beads/internal/git"
|
|
)
|
|
|
|
// CheckInstallation verifies that .beads directory exists
|
|
func CheckInstallation(path string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
|
// Auto-detect prefix from directory name
|
|
prefix := filepath.Base(path)
|
|
prefix = strings.TrimRight(prefix, "-")
|
|
|
|
return DoctorCheck{
|
|
Name: "Installation",
|
|
Status: StatusError,
|
|
Message: "No .beads/ directory found",
|
|
Fix: fmt.Sprintf("Run 'bd init --prefix %s' to initialize beads", prefix),
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "Installation",
|
|
Status: StatusOK,
|
|
Message: ".beads/ directory found",
|
|
}
|
|
}
|
|
|
|
// CheckMultipleDatabases checks for multiple database files in .beads directory
|
|
func CheckMultipleDatabases(path string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
|
|
// Find all .db files (excluding backups and vc.db)
|
|
files, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Database Files",
|
|
Status: StatusError,
|
|
Message: "Unable to check for multiple databases",
|
|
}
|
|
}
|
|
|
|
// Filter out backups and vc.db
|
|
var dbFiles []string
|
|
for _, f := range files {
|
|
base := filepath.Base(f)
|
|
if !strings.HasSuffix(base, ".backup.db") && base != "vc.db" {
|
|
dbFiles = append(dbFiles, base)
|
|
}
|
|
}
|
|
|
|
if len(dbFiles) == 0 {
|
|
return DoctorCheck{
|
|
Name: "Database Files",
|
|
Status: StatusOK,
|
|
Message: "No database files (JSONL-only mode)",
|
|
}
|
|
}
|
|
|
|
if len(dbFiles) == 1 {
|
|
return DoctorCheck{
|
|
Name: "Database Files",
|
|
Status: StatusOK,
|
|
Message: "Single database file",
|
|
}
|
|
}
|
|
|
|
// Multiple databases found
|
|
return DoctorCheck{
|
|
Name: "Database Files",
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("Multiple database files found: %s", strings.Join(dbFiles, ", ")),
|
|
Fix: "Run 'bd migrate' to consolidate databases or manually remove old .db files",
|
|
}
|
|
}
|
|
|
|
// CheckPermissions verifies that .beads directory and database are readable/writable
|
|
func CheckPermissions(path string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
|
|
// Check if .beads/ is writable
|
|
testFile := filepath.Join(beadsDir, ".doctor-test-write")
|
|
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
|
|
return DoctorCheck{
|
|
Name: "Permissions",
|
|
Status: StatusError,
|
|
Message: ".beads/ directory is not writable",
|
|
Fix: "Run 'bd doctor --fix' to fix permissions",
|
|
}
|
|
}
|
|
_ = os.Remove(testFile) // Clean up test file (intentionally ignore error)
|
|
|
|
// Check database permissions
|
|
dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName)
|
|
if _, err := os.Stat(dbPath); err == nil {
|
|
// Try to open database
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Permissions",
|
|
Status: StatusError,
|
|
Message: "Database file exists but cannot be opened",
|
|
Fix: "Run 'bd doctor --fix' to fix permissions",
|
|
}
|
|
}
|
|
_ = db.Close() // Intentionally ignore close error
|
|
|
|
// Try a write test
|
|
db, err = sql.Open("sqlite", dbPath)
|
|
if err == nil {
|
|
_, err = db.Exec("SELECT 1")
|
|
_ = db.Close() // Intentionally ignore close error
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Permissions",
|
|
Status: StatusError,
|
|
Message: "Database file is not readable",
|
|
Fix: "Run 'bd doctor --fix' to fix permissions",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "Permissions",
|
|
Status: StatusOK,
|
|
Message: "All permissions OK",
|
|
}
|
|
}
|
|
|
|
// CheckUntrackedBeadsFiles checks for untracked .beads/*.jsonl files that should be committed
|
|
func CheckUntrackedBeadsFiles(path string) DoctorCheck {
|
|
beadsDir := filepath.Join(path, ".beads")
|
|
|
|
// Skip if .beads doesn't exist
|
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
|
return DoctorCheck{
|
|
Name: "Untracked Files",
|
|
Status: StatusOK,
|
|
Message: "N/A (no .beads directory)",
|
|
}
|
|
}
|
|
|
|
// Check if we're in a git repository using worktree-aware detection
|
|
_, err := git.GetGitDir()
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Untracked Files",
|
|
Status: StatusOK,
|
|
Message: "N/A (not a git repository)",
|
|
}
|
|
}
|
|
|
|
// Run git status --porcelain to find untracked files in .beads/
|
|
cmd := exec.Command("git", "status", "--porcelain", ".beads/")
|
|
cmd.Dir = path
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return DoctorCheck{
|
|
Name: "Untracked Files",
|
|
Status: StatusWarning,
|
|
Message: "Unable to check git status",
|
|
Detail: err.Error(),
|
|
}
|
|
}
|
|
|
|
// Parse output for untracked JSONL files (lines starting with "??")
|
|
var untrackedJSONL []string
|
|
for _, line := range strings.Split(string(output), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
// Untracked files start with "?? "
|
|
if strings.HasPrefix(line, "?? ") {
|
|
file := strings.TrimPrefix(line, "?? ")
|
|
// Only care about .jsonl files
|
|
if strings.HasSuffix(file, ".jsonl") {
|
|
untrackedJSONL = append(untrackedJSONL, filepath.Base(file))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(untrackedJSONL) == 0 {
|
|
return DoctorCheck{
|
|
Name: "Untracked Files",
|
|
Status: StatusOK,
|
|
Message: "All .beads/*.jsonl files are tracked",
|
|
}
|
|
}
|
|
|
|
return DoctorCheck{
|
|
Name: "Untracked Files",
|
|
Status: StatusWarning,
|
|
Message: fmt.Sprintf("Untracked JSONL files: %s", strings.Join(untrackedJSONL, ", ")),
|
|
Detail: "These files should be committed to propagate changes to other clones",
|
|
Fix: "Run 'bd doctor --fix' to stage and commit untracked files, or manually: git add .beads/*.jsonl && git commit",
|
|
}
|
|
}
|
|
|
|
// FixPermissions fixes file permission issues in the .beads directory
|
|
func FixPermissions(path string) error {
|
|
return fix.Permissions(path)
|
|
}
|
|
|
|
// FixUntrackedJSONL stages and commits untracked .beads/*.jsonl files
|
|
func FixUntrackedJSONL(path string) error {
|
|
return fix.UntrackedJSONL(path)
|
|
}
|