diff --git a/cmd/bd/init.go b/cmd/bd/init.go index 544981a7..46d621b2 100644 --- a/cmd/bd/init.go +++ b/cmd/bd/init.go @@ -54,6 +54,12 @@ With --stealth: configures per-repository git settings for invisible beads usage force, _ := cmd.Flags().GetBool("force") fromJSONL, _ := cmd.Flags().GetBool("from-jsonl") + // Dolt server mode flags (bd-dolt.2.2) + serverMode, _ := cmd.Flags().GetBool("server") + serverHost, _ := cmd.Flags().GetString("server-host") + serverPort, _ := cmd.Flags().GetInt("server-port") + serverUser, _ := cmd.Flags().GetString("server-user") + // Validate backend flag if backend != "" && backend != configfile.BackendSQLite && backend != configfile.BackendDolt { fmt.Fprintf(os.Stderr, "Error: invalid backend '%s' (must be 'sqlite' or 'dolt')\n", backend) @@ -63,6 +69,12 @@ With --stealth: configures per-repository git settings for invisible beads usage backend = configfile.BackendSQLite // Default to SQLite } + // Validate server mode requires dolt backend + if serverMode && backend != configfile.BackendDolt { + fmt.Fprintf(os.Stderr, "Error: --server flag requires --backend dolt\n") + os.Exit(1) + } + // Initialize config (PersistentPreRun doesn't run for init command) if err := config.Initialize(); err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to initialize config: %v\n", err) @@ -413,6 +425,20 @@ With --stealth: configures per-repository git settings for invisible beads usage if cfg.Database == "" || cfg.Database == beads.CanonicalDatabaseName { cfg.Database = "dolt" } + + // Save server mode configuration (bd-dolt.2.2) + if serverMode { + cfg.DoltMode = configfile.DoltModeServer + if serverHost != "" { + cfg.DoltServerHost = serverHost + } + if serverPort != 0 { + cfg.DoltServerPort = serverPort + } + if serverUser != "" { + cfg.DoltServerUser = serverUser + } + } } if err := cfg.Save(beadsDir); err != nil { @@ -625,6 +651,22 @@ With --stealth: configures per-repository git settings for invisible beads usage fmt.Printf("\n%s bd initialized successfully!\n\n", ui.RenderPass("✓")) fmt.Printf(" Backend: %s\n", ui.RenderAccent(backend)) + if serverMode { + host := serverHost + if host == "" { + host = configfile.DefaultDoltServerHost + } + port := serverPort + if port == 0 { + port = configfile.DefaultDoltServerPort + } + user := serverUser + if user == "" { + user = configfile.DefaultDoltServerUser + } + fmt.Printf(" Mode: %s\n", ui.RenderAccent("server")) + fmt.Printf(" Server: %s\n", ui.RenderAccent(fmt.Sprintf("%s@%s:%d", user, host, port))) + } fmt.Printf(" Database: %s\n", ui.RenderAccent(storagePath)) fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix)) fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"- (e.g., "+prefix+"-a3f2dd)")) @@ -666,6 +708,13 @@ func init() { initCmd.Flags().Bool("skip-merge-driver", false, "Skip git merge driver setup") initCmd.Flags().Bool("force", false, "Force re-initialization even if JSONL already has issues (may cause data loss)") initCmd.Flags().Bool("from-jsonl", false, "Import from current .beads/issues.jsonl file instead of git history (preserves manual cleanups)") + + // Dolt server mode flags (bd-dolt.2.2) + initCmd.Flags().Bool("server", false, "Configure Dolt in server mode (connect to external dolt sql-server)") + initCmd.Flags().String("server-host", "", "Dolt server host (default: 127.0.0.1)") + initCmd.Flags().Int("server-port", 0, "Dolt server port (default: 3306)") + initCmd.Flags().String("server-user", "", "Dolt server MySQL user (default: root)") + rootCmd.AddCommand(initCmd) } diff --git a/internal/configfile/configfile.go b/internal/configfile/configfile.go index 44edfeec..1d8a8d53 100644 --- a/internal/configfile/configfile.go +++ b/internal/configfile/configfile.go @@ -18,6 +18,15 @@ type Config struct { // Deletions configuration DeletionsRetentionDays int `json:"deletions_retention_days,omitempty"` // 0 means use default (3 days) + // Dolt server mode configuration (bd-dolt.2.2) + // When Mode is "server", connects to external dolt sql-server instead of embedded. + // This enables multi-writer access for multi-agent environments. + DoltMode string `json:"dolt_mode,omitempty"` // "embedded" (default) or "server" + DoltServerHost string `json:"dolt_server_host,omitempty"` // Server host (default: 127.0.0.1) + DoltServerPort int `json:"dolt_server_port,omitempty"` // Server port (default: 3306) + DoltServerUser string `json:"dolt_server_user,omitempty"` // MySQL user (default: root) + // Note: Password should be set via BEADS_DOLT_PASSWORD env var for security + // Deprecated: LastBdVersion is no longer used for version tracking. // Version is now stored in .local_version (gitignored) to prevent // upgrade notifications firing after git operations reset metadata.json. @@ -185,3 +194,53 @@ func (c *Config) GetBackend() string { } return c.Backend } + +// Dolt mode constants +const ( + DoltModeEmbedded = "embedded" + DoltModeServer = "server" +) + +// Default Dolt server settings +const ( + DefaultDoltServerHost = "127.0.0.1" + DefaultDoltServerPort = 3306 + DefaultDoltServerUser = "root" +) + +// IsDoltServerMode returns true if Dolt is configured for server mode. +func (c *Config) IsDoltServerMode() bool { + return c.GetBackend() == BackendDolt && c.DoltMode == DoltModeServer +} + +// GetDoltMode returns the Dolt connection mode, defaulting to embedded. +func (c *Config) GetDoltMode() string { + if c.DoltMode == "" { + return DoltModeEmbedded + } + return c.DoltMode +} + +// GetDoltServerHost returns the Dolt server host, defaulting to 127.0.0.1. +func (c *Config) GetDoltServerHost() string { + if c.DoltServerHost == "" { + return DefaultDoltServerHost + } + return c.DoltServerHost +} + +// GetDoltServerPort returns the Dolt server port, defaulting to 3306. +func (c *Config) GetDoltServerPort() int { + if c.DoltServerPort == 0 { + return DefaultDoltServerPort + } + return c.DoltServerPort +} + +// GetDoltServerUser returns the Dolt server user, defaulting to root. +func (c *Config) GetDoltServerUser() string { + if c.DoltServerUser == "" { + return DefaultDoltServerUser + } + return c.DoltServerUser +} diff --git a/internal/configfile/configfile_test.go b/internal/configfile/configfile_test.go index 97b42bd8..0cfe1886 100644 --- a/internal/configfile/configfile_test.go +++ b/internal/configfile/configfile_test.go @@ -179,3 +179,204 @@ func TestGetDeletionsRetentionDays(t *testing.T) { }) } } + +// TestDoltServerMode tests the Dolt server mode configuration (bd-dolt.2.2) +func TestDoltServerMode(t *testing.T) { + t.Run("IsDoltServerMode", func(t *testing.T) { + tests := []struct { + name string + cfg *Config + want bool + }{ + { + name: "sqlite backend", + cfg: &Config{Backend: BackendSQLite}, + want: false, + }, + { + name: "dolt embedded mode", + cfg: &Config{Backend: BackendDolt, DoltMode: DoltModeEmbedded}, + want: false, + }, + { + name: "dolt server mode", + cfg: &Config{Backend: BackendDolt, DoltMode: DoltModeServer}, + want: true, + }, + { + name: "dolt default mode", + cfg: &Config{Backend: BackendDolt}, + want: false, // Default is embedded + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.cfg.IsDoltServerMode() + if got != tt.want { + t.Errorf("IsDoltServerMode() = %v, want %v", got, tt.want) + } + }) + } + }) + + t.Run("GetDoltMode", func(t *testing.T) { + tests := []struct { + name string + cfg *Config + want string + }{ + { + name: "empty defaults to embedded", + cfg: &Config{}, + want: DoltModeEmbedded, + }, + { + name: "explicit embedded", + cfg: &Config{DoltMode: DoltModeEmbedded}, + want: DoltModeEmbedded, + }, + { + name: "explicit server", + cfg: &Config{DoltMode: DoltModeServer}, + want: DoltModeServer, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.cfg.GetDoltMode() + if got != tt.want { + t.Errorf("GetDoltMode() = %q, want %q", got, tt.want) + } + }) + } + }) + + t.Run("GetDoltServerHost", func(t *testing.T) { + tests := []struct { + name string + cfg *Config + want string + }{ + { + name: "empty defaults to 127.0.0.1", + cfg: &Config{}, + want: DefaultDoltServerHost, + }, + { + name: "custom host", + cfg: &Config{DoltServerHost: "192.168.1.100"}, + want: "192.168.1.100", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.cfg.GetDoltServerHost() + if got != tt.want { + t.Errorf("GetDoltServerHost() = %q, want %q", got, tt.want) + } + }) + } + }) + + t.Run("GetDoltServerPort", func(t *testing.T) { + tests := []struct { + name string + cfg *Config + want int + }{ + { + name: "zero defaults to 3306", + cfg: &Config{}, + want: DefaultDoltServerPort, + }, + { + name: "custom port", + cfg: &Config{DoltServerPort: 13306}, + want: 13306, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.cfg.GetDoltServerPort() + if got != tt.want { + t.Errorf("GetDoltServerPort() = %d, want %d", got, tt.want) + } + }) + } + }) + + t.Run("GetDoltServerUser", func(t *testing.T) { + tests := []struct { + name string + cfg *Config + want string + }{ + { + name: "empty defaults to root", + cfg: &Config{}, + want: DefaultDoltServerUser, + }, + { + name: "custom user", + cfg: &Config{DoltServerUser: "beads"}, + want: "beads", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.cfg.GetDoltServerUser() + if got != tt.want { + t.Errorf("GetDoltServerUser() = %q, want %q", got, tt.want) + } + }) + } + }) +} + +// TestDoltServerModeRoundtrip tests that server mode config survives save/load +func TestDoltServerModeRoundtrip(t *testing.T) { + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0750); err != nil { + t.Fatalf("failed to create .beads directory: %v", err) + } + + cfg := &Config{ + Database: "dolt", + Backend: BackendDolt, + DoltMode: DoltModeServer, + DoltServerHost: "192.168.1.50", + DoltServerPort: 13306, + DoltServerUser: "beads_admin", + } + + if err := cfg.Save(beadsDir); err != nil { + t.Fatalf("Save() failed: %v", err) + } + + loaded, err := Load(beadsDir) + if err != nil { + t.Fatalf("Load() failed: %v", err) + } + + if !loaded.IsDoltServerMode() { + t.Error("IsDoltServerMode() = false after load, want true") + } + if loaded.GetDoltMode() != DoltModeServer { + t.Errorf("GetDoltMode() = %q, want %q", loaded.GetDoltMode(), DoltModeServer) + } + if loaded.GetDoltServerHost() != "192.168.1.50" { + t.Errorf("GetDoltServerHost() = %q, want %q", loaded.GetDoltServerHost(), "192.168.1.50") + } + if loaded.GetDoltServerPort() != 13306 { + t.Errorf("GetDoltServerPort() = %d, want %d", loaded.GetDoltServerPort(), 13306) + } + if loaded.GetDoltServerUser() != "beads_admin" { + t.Errorf("GetDoltServerUser() = %q, want %q", loaded.GetDoltServerUser(), "beads_admin") + } +} diff --git a/internal/storage/factory/factory.go b/internal/storage/factory/factory.go index c33c9a2e..6fbde06d 100644 --- a/internal/storage/factory/factory.go +++ b/internal/storage/factory/factory.go @@ -31,6 +31,7 @@ type Options struct { ServerMode bool // Connect to dolt sql-server instead of embedded ServerHost string // Server host (default: 127.0.0.1) ServerPort int // Server port (default: 3306) + ServerUser string // MySQL user (default: root) } // New creates a storage backend based on the backend type. @@ -88,6 +89,19 @@ func NewFromConfigWithOptions(ctx context.Context, beadsDir string, opts Options case configfile.BackendSQLite: return NewWithOptions(ctx, backend, cfg.DatabasePath(beadsDir), opts) case configfile.BackendDolt: + // Merge Dolt server mode config into options (config provides defaults, opts can override) + if cfg.IsDoltServerMode() { + opts.ServerMode = true + if opts.ServerHost == "" { + opts.ServerHost = cfg.GetDoltServerHost() + } + if opts.ServerPort == 0 { + opts.ServerPort = cfg.GetDoltServerPort() + } + if opts.ServerUser == "" { + opts.ServerUser = cfg.GetDoltServerUser() + } + } return NewWithOptions(ctx, backend, cfg.DatabasePath(beadsDir), opts) default: return nil, fmt.Errorf("unknown storage backend in config: %s", backend) diff --git a/internal/storage/factory/factory_dolt.go b/internal/storage/factory/factory_dolt.go index 4d38be13..e73cd193 100644 --- a/internal/storage/factory/factory_dolt.go +++ b/internal/storage/factory/factory_dolt.go @@ -47,6 +47,7 @@ func init() { ServerMode: opts.ServerMode, ServerHost: opts.ServerHost, ServerPort: opts.ServerPort, + ServerUser: opts.ServerUser, }) }) }