Add global daemon auto-start support (bd-149)
- Implement shouldUseGlobalDaemon() with multi-repo detection - Auto-detect 4+ beads repos and prefer global daemon - Support BEADS_PREFER_GLOBAL_DAEMON env var for explicit control - Add 'bd daemon --migrate-to-global' migration helper - Update auto-start logic to use global daemon when appropriate - Update documentation in AGENTS.md and README.md Amp-Thread-ID: https://ampcode.com/threads/T-9af9372d-f3f3-4698-920d-e5ad1486d849 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -44,6 +44,7 @@ Use --health to check daemon health and metrics.`,
|
||||
stop, _ := cmd.Flags().GetBool("stop")
|
||||
status, _ := cmd.Flags().GetBool("status")
|
||||
health, _ := cmd.Flags().GetBool("health")
|
||||
migrateToGlobal, _ := cmd.Flags().GetBool("migrate-to-global")
|
||||
interval, _ := cmd.Flags().GetDuration("interval")
|
||||
autoCommit, _ := cmd.Flags().GetBool("auto-commit")
|
||||
autoPush, _ := cmd.Flags().GetBool("auto-push")
|
||||
@@ -71,6 +72,11 @@ Use --health to check daemon health and metrics.`,
|
||||
return
|
||||
}
|
||||
|
||||
if migrateToGlobal {
|
||||
migrateToGlobalDaemon()
|
||||
return
|
||||
}
|
||||
|
||||
if stop {
|
||||
stopDaemon(pidFile)
|
||||
return
|
||||
@@ -127,6 +133,7 @@ func init() {
|
||||
daemonCmd.Flags().Bool("stop", false, "Stop running daemon")
|
||||
daemonCmd.Flags().Bool("status", false, "Show daemon status")
|
||||
daemonCmd.Flags().Bool("health", false, "Check daemon health and metrics")
|
||||
daemonCmd.Flags().Bool("migrate-to-global", false, "Migrate from local to global daemon")
|
||||
daemonCmd.Flags().String("log", "", "Log file path (default: .beads/daemon.log)")
|
||||
daemonCmd.Flags().Bool("global", false, "Run as global daemon (socket at ~/.beads/bd.sock)")
|
||||
rootCmd.AddCommand(daemonCmd)
|
||||
@@ -330,6 +337,73 @@ func showDaemonHealth(global bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func migrateToGlobalDaemon() {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: cannot get home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
localPIDFile := filepath.Join(".beads", "daemon.pid")
|
||||
globalPIDFile := filepath.Join(home, ".beads", "daemon.pid")
|
||||
|
||||
// Check if local daemon is running
|
||||
localRunning, localPID := isDaemonRunning(localPIDFile)
|
||||
if !localRunning {
|
||||
fmt.Println("No local daemon is running")
|
||||
} else {
|
||||
fmt.Printf("Stopping local daemon (PID %d)...\n", localPID)
|
||||
stopDaemon(localPIDFile)
|
||||
}
|
||||
|
||||
// Check if global daemon is already running
|
||||
globalRunning, globalPID := isDaemonRunning(globalPIDFile)
|
||||
if globalRunning {
|
||||
fmt.Printf("✓ Global daemon already running (PID %d)\n", globalPID)
|
||||
return
|
||||
}
|
||||
|
||||
// Start global daemon
|
||||
fmt.Println("Starting global daemon...")
|
||||
binPath, err := os.Executable()
|
||||
if err != nil {
|
||||
binPath = os.Args[0]
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, "daemon", "--global")
|
||||
devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
|
||||
if err == nil {
|
||||
cmd.Stdout = devNull
|
||||
cmd.Stderr = devNull
|
||||
cmd.Stdin = devNull
|
||||
defer devNull.Close()
|
||||
}
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to start global daemon: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go cmd.Wait()
|
||||
|
||||
// Wait for daemon to be ready
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
if isRunning, pid := isDaemonRunning(globalPIDFile); isRunning {
|
||||
fmt.Printf("✓ Global daemon started successfully (PID %d)\n", pid)
|
||||
fmt.Println()
|
||||
fmt.Println("Migration complete! The global daemon will now serve all your beads repositories.")
|
||||
fmt.Println("Set BEADS_PREFER_GLOBAL_DAEMON=1 in your shell to make this permanent.")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error: global daemon failed to start\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func stopDaemon(pidFile string) {
|
||||
if isRunning, pid := isDaemonRunning(pidFile); !isRunning {
|
||||
fmt.Println("Daemon is not running")
|
||||
|
||||
103
cmd/bd/main.go
103
cmd/bd/main.go
@@ -219,6 +219,100 @@ func shouldAutoStartDaemon() bool {
|
||||
return true // Default to enabled
|
||||
}
|
||||
|
||||
// shouldUseGlobalDaemon determines if global daemon should be preferred
|
||||
// based on environment variables, config, or heuristics (multi-repo detection)
|
||||
func shouldUseGlobalDaemon() bool {
|
||||
// Check explicit environment variable first
|
||||
if pref := os.Getenv("BEADS_PREFER_GLOBAL_DAEMON"); pref != "" {
|
||||
return pref == "1" || strings.ToLower(pref) == "true"
|
||||
}
|
||||
|
||||
// Heuristic: detect multiple beads repositories
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Count .beads directories under home
|
||||
repoCount := 0
|
||||
maxDepth := 5 // Don't scan too deep
|
||||
|
||||
var countRepos func(string, int) error
|
||||
countRepos = func(dir string, depth int) error {
|
||||
if depth > maxDepth || repoCount > 3 {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil // Skip directories we can't read
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
|
||||
// Skip hidden dirs except .beads
|
||||
if strings.HasPrefix(name, ".") && name != ".beads" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip common large directories
|
||||
if name == "node_modules" || name == "vendor" || name == "target" || name == ".git" {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, name)
|
||||
|
||||
// Check if this is a .beads directory with a database
|
||||
if name == ".beads" {
|
||||
dbPath := filepath.Join(path, "db.sqlite")
|
||||
if _, err := os.Stat(dbPath); err == nil {
|
||||
repoCount++
|
||||
if repoCount > 3 {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Recurse into subdirectories
|
||||
if depth < maxDepth {
|
||||
countRepos(path, depth+1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan common project directories
|
||||
projectDirs := []string{
|
||||
filepath.Join(home, "src"),
|
||||
filepath.Join(home, "projects"),
|
||||
filepath.Join(home, "code"),
|
||||
filepath.Join(home, "workspace"),
|
||||
filepath.Join(home, "dev"),
|
||||
}
|
||||
|
||||
for _, dir := range projectDirs {
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
countRepos(dir, 0)
|
||||
if repoCount > 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("BD_DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "Debug: found %d beads repositories, prefer global: %v\n", repoCount, repoCount > 3)
|
||||
}
|
||||
|
||||
// Use global daemon if we found more than 3 repositories
|
||||
return repoCount > 3
|
||||
}
|
||||
|
||||
// tryAutoStartDaemon attempts to start the daemon in the background
|
||||
// Returns true if daemon was started successfully and socket is ready
|
||||
func tryAutoStartDaemon(socketPath string) bool {
|
||||
@@ -305,11 +399,20 @@ func tryAutoStartDaemon(socketPath string) bool {
|
||||
}
|
||||
|
||||
// Determine if we should start global or local daemon
|
||||
// If requesting local socket, check if we should suggest global instead
|
||||
isGlobal := false
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
globalSocket := filepath.Join(home, ".beads", "bd.sock")
|
||||
if socketPath == globalSocket {
|
||||
isGlobal = true
|
||||
} else if shouldUseGlobalDaemon() {
|
||||
// User has multiple repos, but requested local daemon
|
||||
// Auto-start global daemon instead and log suggestion
|
||||
isGlobal = true
|
||||
socketPath = globalSocket
|
||||
if os.Getenv("BD_DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "Debug: detected multiple repos, auto-starting global daemon\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user