diff --git a/beads.go b/beads.go index 888b57a7..1b919849 100644 --- a/beads.go +++ b/beads.go @@ -9,6 +9,7 @@ package beads import ( "context" + "fmt" "os" "path/filepath" @@ -162,6 +163,7 @@ type DatabaseInfo struct { } // findDatabaseInTree walks up the directory tree looking for .beads/*.db +// Prefers beads.db and returns an error (via stderr warning) if multiple .db files exist func findDatabaseInTree() string { dir, err := os.Getwd() if err != nil { @@ -172,11 +174,38 @@ func findDatabaseInTree() string { for { beadsDir := filepath.Join(dir, ".beads") if info, err := os.Stat(beadsDir); err == nil && info.IsDir() { + // Check for canonical beads.db first + canonicalDB := filepath.Join(beadsDir, "beads.db") + if _, err := os.Stat(canonicalDB); err == nil { + return canonicalDB + } + // Found .beads/ directory, look for *.db files matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db")) if err == nil && len(matches) > 0 { - // Return first .db file found - return matches[0] + // Filter out backup files + var validDBs []string + for _, match := range matches { + baseName := filepath.Base(match) + // Skip backup files (e.g., beads.db.backup, bd.db.backup) + if filepath.Ext(baseName) != ".backup" { + validDBs = append(validDBs, match) + } + } + + if len(validDBs) > 1 { + // Multiple databases found - this is ambiguous + // Print error to stderr but return the first one for backward compatibility + fmt.Fprintf(os.Stderr, "Warning: Multiple database files found in %s:\n", beadsDir) + for _, db := range validDBs { + fmt.Fprintf(os.Stderr, " - %s\n", filepath.Base(db)) + } + fmt.Fprintf(os.Stderr, "Run 'bd init' to migrate to beads.db or manually remove old databases.\n\n") + } + + if len(validDBs) > 0 { + return validDBs[0] + } } } diff --git a/cmd/bd/daemon.go b/cmd/bd/daemon.go index be68c863..4b40bdb7 100644 --- a/cmd/bd/daemon.go +++ b/cmd/bd/daemon.go @@ -1069,6 +1069,27 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p } } + // Check for multiple .db files (ambiguity error) + beadsDir := filepath.Dir(daemonDBPath) + matches, err := filepath.Glob(filepath.Join(beadsDir, "*.db")) + if err == nil && len(matches) > 1 { + // Filter out backup files + var validDBs []string + for _, match := range matches { + if filepath.Ext(filepath.Base(match)) != ".backup" { + validDBs = append(validDBs, match) + } + } + if len(validDBs) > 1 { + log.log("Error: Multiple database files found in %s:", beadsDir) + for _, db := range validDBs { + log.log(" - %s", filepath.Base(db)) + } + log.log("Run 'bd init' to migrate to beads.db or manually remove old databases") + os.Exit(1) + } + } + log.log("Using database: %s", daemonDBPath) store, err := sqlite.New(daemonDBPath) @@ -1079,8 +1100,7 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush bool, logPath, p defer func() { _ = store.Close() }() log.log("Database opened: %s", daemonDBPath) - // Get workspace path (.beads directory) - beadsDir := filepath.Dir(daemonDBPath) + // Get workspace path (.beads directory) - beadsDir already defined above // Get actual workspace root (parent of .beads) workspacePath := filepath.Dir(beadsDir) socketPath := filepath.Join(beadsDir, "bd.sock") diff --git a/cmd/bd/init.go b/cmd/bd/init.go index e373be22..5ac3e82e 100644 --- a/cmd/bd/init.go +++ b/cmd/bd/init.go @@ -44,13 +44,19 @@ and database file. Optionally specify a custom issue prefix.`, prefix = strings.TrimRight(prefix, "-") // Create database - // Use global dbPath if set via --db flag or BEADS_DB env var, otherwise default to .beads/{prefix}.db + // Use global dbPath if set via --db flag or BEADS_DB env var, otherwise default to .beads/beads.db initDBPath := dbPath if initDBPath == "" { - initDBPath = filepath.Join(".beads", prefix+".db") + initDBPath = filepath.Join(".beads", "beads.db") } - - // Determine if we should create .beads/ directory in CWD + + // Migrate old database files if they exist + if err := migrateOldDatabases(initDBPath, quiet); err != nil { + fmt.Fprintf(os.Stderr, "Error during database migration: %v\n", err) + os.Exit(1) + } + + // Determine if we should create .beads/ directory in CWD // Only create it if the database will be stored there cwd, err := os.Getwd() if err != nil { @@ -355,3 +361,63 @@ exit 0 return nil } + +// migrateOldDatabases detects and migrates old database files to beads.db +func migrateOldDatabases(targetPath string, quiet bool) error { + targetDir := filepath.Dir(targetPath) + targetName := filepath.Base(targetPath) + + // If target already exists, no migration needed + if _, err := os.Stat(targetPath); err == nil { + return nil + } + + // Create .beads directory if it doesn't exist + if err := os.MkdirAll(targetDir, 0750); err != nil { + return fmt.Errorf("failed to create .beads directory: %w", err) + } + + // Look for existing .db files in the .beads directory + pattern := filepath.Join(targetDir, "*.db") + matches, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("failed to search for existing databases: %w", err) + } + + // Filter out the target file name and any backup files + var oldDBs []string + for _, match := range matches { + baseName := filepath.Base(match) + if baseName != targetName && !strings.HasSuffix(baseName, ".backup.db") { + oldDBs = append(oldDBs, match) + } + } + + if len(oldDBs) == 0 { + // No old databases to migrate + return nil + } + + if len(oldDBs) > 1 { + // Multiple databases found - ambiguous, require manual intervention + return fmt.Errorf("multiple database files found in %s: %v\nPlease manually rename the correct database to %s and remove others", + targetDir, oldDBs, targetName) + } + + // Migrate the single old database + oldDB := oldDBs[0] + if !quiet { + fmt.Fprintf(os.Stderr, "→ Migrating database: %s → %s\n", filepath.Base(oldDB), targetName) + } + + // Rename the old database to the new canonical name + if err := os.Rename(oldDB, targetPath); err != nil { + return fmt.Errorf("failed to migrate database %s to %s: %w", oldDB, targetPath, err) + } + + if !quiet { + fmt.Fprintf(os.Stderr, "✓ Database migration complete\n\n") + } + + return nil +} diff --git a/cmd/bd/init_test.go b/cmd/bd/init_test.go index f5d03d9b..5a872ab4 100644 --- a/cmd/bd/init_test.go +++ b/cmd/bd/init_test.go @@ -143,19 +143,10 @@ func TestInitCommand(t *testing.T) { } } - // Verify database was created - var dbPath string - if tt.prefix != "" { - expectedPrefix := strings.TrimRight(tt.prefix, "-") - dbPath = filepath.Join(beadsDir, expectedPrefix+".db") - } else { - // Should use directory name as prefix - dirName := filepath.Base(tmpDir) - dbPath = filepath.Join(beadsDir, dirName+".db") - } - + // Verify database was created (always beads.db now) + dbPath := filepath.Join(beadsDir, "beads.db") if _, err := os.Stat(dbPath); os.IsNotExist(err) { - t.Errorf("Database file was not created at %s", dbPath) + t.Errorf("Database file was not created at %s", dbPath) } // Verify database has correct prefix @@ -228,8 +219,8 @@ func TestInitAlreadyInitialized(t *testing.T) { t.Fatalf("Second init failed: %v", err) } - // Verify database still works - dbPath := filepath.Join(tmpDir, ".beads", "test.db") + // Verify database still works (always beads.db now) + dbPath := filepath.Join(tmpDir, ".beads", "beads.db") store, err := sqlite.New(dbPath) if err != nil { t.Fatalf("Failed to open database after re-init: %v", err)