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:
@@ -54,6 +54,12 @@ With --stealth: configures per-repository git settings for invisible beads usage
|
|||||||
force, _ := cmd.Flags().GetBool("force")
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
fromJSONL, _ := cmd.Flags().GetBool("from-jsonl")
|
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
|
// Validate backend flag
|
||||||
if backend != "" && backend != configfile.BackendSQLite && backend != configfile.BackendDolt {
|
if backend != "" && backend != configfile.BackendSQLite && backend != configfile.BackendDolt {
|
||||||
fmt.Fprintf(os.Stderr, "Error: invalid backend '%s' (must be 'sqlite' or 'dolt')\n", backend)
|
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
|
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)
|
// Initialize config (PersistentPreRun doesn't run for init command)
|
||||||
if err := config.Initialize(); err != nil {
|
if err := config.Initialize(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to initialize config: %v\n", err)
|
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 {
|
if cfg.Database == "" || cfg.Database == beads.CanonicalDatabaseName {
|
||||||
cfg.Database = "dolt"
|
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 {
|
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("\n%s bd initialized successfully!\n\n", ui.RenderPass("✓"))
|
||||||
fmt.Printf(" Backend: %s\n", ui.RenderAccent(backend))
|
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(" Database: %s\n", ui.RenderAccent(storagePath))
|
||||||
fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
|
fmt.Printf(" Issue prefix: %s\n", ui.RenderAccent(prefix))
|
||||||
fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (e.g., "+prefix+"-a3f2dd)"))
|
fmt.Printf(" Issues will be named: %s\n\n", ui.RenderAccent(prefix+"-<hash> (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("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("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)")
|
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)
|
rootCmd.AddCommand(initCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ type Config struct {
|
|||||||
// Deletions configuration
|
// Deletions configuration
|
||||||
DeletionsRetentionDays int `json:"deletions_retention_days,omitempty"` // 0 means use default (3 days)
|
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.
|
// Deprecated: LastBdVersion is no longer used for version tracking.
|
||||||
// Version is now stored in .local_version (gitignored) to prevent
|
// Version is now stored in .local_version (gitignored) to prevent
|
||||||
// upgrade notifications firing after git operations reset metadata.json.
|
// upgrade notifications firing after git operations reset metadata.json.
|
||||||
@@ -185,3 +194,53 @@ func (c *Config) GetBackend() string {
|
|||||||
}
|
}
|
||||||
return c.Backend
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Options struct {
|
|||||||
ServerMode bool // Connect to dolt sql-server instead of embedded
|
ServerMode bool // Connect to dolt sql-server instead of embedded
|
||||||
ServerHost string // Server host (default: 127.0.0.1)
|
ServerHost string // Server host (default: 127.0.0.1)
|
||||||
ServerPort int // Server port (default: 3306)
|
ServerPort int // Server port (default: 3306)
|
||||||
|
ServerUser string // MySQL user (default: root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a storage backend based on the backend type.
|
// 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:
|
case configfile.BackendSQLite:
|
||||||
return NewWithOptions(ctx, backend, cfg.DatabasePath(beadsDir), opts)
|
return NewWithOptions(ctx, backend, cfg.DatabasePath(beadsDir), opts)
|
||||||
case configfile.BackendDolt:
|
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)
|
return NewWithOptions(ctx, backend, cfg.DatabasePath(beadsDir), opts)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown storage backend in config: %s", backend)
|
return nil, fmt.Errorf("unknown storage backend in config: %s", backend)
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func init() {
|
|||||||
ServerMode: opts.ServerMode,
|
ServerMode: opts.ServerMode,
|
||||||
ServerHost: opts.ServerHost,
|
ServerHost: opts.ServerHost,
|
||||||
ServerPort: opts.ServerPort,
|
ServerPort: opts.ServerPort,
|
||||||
|
ServerUser: opts.ServerUser,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user