feat(dolt): add server mode config to metadata.json schema (bd-dolt.2.2)

Add Dolt server mode configuration to metadata.json for multi-writer access:

- Add DoltMode, DoltServerHost, DoltServerPort, DoltServerUser fields to Config
- Add helper methods with sensible defaults (127.0.0.1:3306, root user)
- Update factory to read server mode config and pass to dolt.Config
- Add --server, --server-host, --server-port, --server-user flags to bd init
- Validate that --server requires --backend dolt
- Add comprehensive tests for server mode configuration

Example metadata.json for server mode:
{
  "backend": "dolt",
  "database": "dolt",
  "dolt_mode": "server",
  "dolt_server_host": "192.168.1.100",
  "dolt_server_port": 3306,
  "dolt_server_user": "beads"
}

Password should be set via BEADS_DOLT_PASSWORD env var for security.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
emma
2026-01-23 20:01:22 -08:00
committed by Steve Yegge
parent cec1356995
commit 484cd9d5fd
5 changed files with 324 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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)

View File

@@ -47,6 +47,7 @@ func init() {
ServerMode: opts.ServerMode,
ServerHost: opts.ServerHost,
ServerPort: opts.ServerPort,
ServerUser: opts.ServerUser,
})
})
}