Skip to content

Commit 396c944

Browse files
steveyeggeShybkoclaude
committed
fix: add shared Dolt server mode for multi-repo/agent setups (GH#2416)
Introduces BEADS_DOLT_SHARED_SERVER mode where multiple beads projects share a single Dolt server on a fixed port (3308), avoiding per-project server sprawl. Includes `bd init --shared-server`, `bd dolt shared-*` commands, doctor checks, and comprehensive tests. Co-Authored-By: Shybko <shybko@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 43bca27 commit 396c944

File tree

16 files changed

+503
-29
lines changed

16 files changed

+503
-29
lines changed

cmd/bd/doctor/dolt.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func RunDoltHealthChecksWithLock(path string, lockCheck DoctorCheck) []DoctorChe
136136
{Name: "Dolt Status", Status: StatusOK, Message: "N/A (SQLite backend)", Category: CategoryData},
137137
{Name: "Dolt Lock Health", Status: StatusOK, Message: "N/A (SQLite backend)", Category: CategoryRuntime},
138138
{Name: "Phantom Databases", Status: StatusOK, Message: "N/A (SQLite backend)", Category: CategoryData},
139+
{Name: "Shared Server", Status: StatusOK, Message: "N/A (SQLite backend)", Category: CategoryRuntime},
139140
}
140141
}
141142

@@ -149,6 +150,7 @@ func RunDoltHealthChecksWithLock(path string, lockCheck DoctorCheck) []DoctorChe
149150
{Name: "Dolt Status", Status: StatusError, Message: "Skipped (no connection)", Detail: connErr, Category: CategoryData},
150151
lockCheck,
151152
{Name: "Phantom Databases", Status: StatusError, Message: "Skipped (no connection)", Detail: connErr, Category: CategoryData},
153+
checkSharedServerHealth(beadsDir),
152154
}
153155
}
154156
defer conn.Close()
@@ -160,6 +162,7 @@ func RunDoltHealthChecksWithLock(path string, lockCheck DoctorCheck) []DoctorChe
160162
checkStatusWithDB(conn),
161163
lockCheck,
162164
checkPhantomDatabases(conn),
165+
checkSharedServerHealth(beadsDir),
163166
}
164167
}
165168

@@ -688,3 +691,63 @@ func probeForCorrectDatabase(conn *doltConn) string {
688691

689692
return ""
690693
}
694+
695+
// checkSharedServerHealth verifies shared server configuration and health.
696+
func checkSharedServerHealth(beadsDir string) DoctorCheck {
697+
if !doltserver.IsSharedServerMode() {
698+
return DoctorCheck{
699+
Name: "Shared Server",
700+
Status: StatusOK,
701+
Message: "N/A (per-project mode)",
702+
Category: CategoryRuntime,
703+
}
704+
}
705+
706+
sharedDir, err := doltserver.SharedServerDir()
707+
if err != nil {
708+
return DoctorCheck{
709+
Name: "Shared Server",
710+
Status: StatusError,
711+
Message: "Cannot access shared server directory",
712+
Detail: err.Error(),
713+
Fix: "Ensure ~/.beads/shared-server/ is writable",
714+
Category: CategoryRuntime,
715+
}
716+
}
717+
718+
state, err := doltserver.IsRunning(sharedDir)
719+
if err != nil {
720+
return DoctorCheck{
721+
Name: "Shared Server",
722+
Status: StatusWarning,
723+
Message: "Cannot check shared server status",
724+
Detail: err.Error(),
725+
Category: CategoryRuntime,
726+
}
727+
}
728+
729+
if state == nil || !state.Running {
730+
return DoctorCheck{
731+
Name: "Shared Server",
732+
Status: StatusWarning,
733+
Message: "Shared server not running (will auto-start on next bd command)",
734+
Detail: fmt.Sprintf("Server directory: %s", sharedDir),
735+
Fix: "Run 'bd dolt start' to start the shared server",
736+
Category: CategoryRuntime,
737+
}
738+
}
739+
740+
cfg, _ := configfile.Load(beadsDir)
741+
dbName := configfile.DefaultDoltDatabase
742+
if cfg != nil {
743+
dbName = cfg.GetDoltDatabase()
744+
}
745+
746+
return DoctorCheck{
747+
Name: "Shared Server",
748+
Status: StatusOK,
749+
Message: fmt.Sprintf("Running (PID %d, port %d), database: %s", state.PID, state.Port, dbName),
750+
Detail: fmt.Sprintf("Server directory: %s", sharedDir),
751+
Category: CategoryRuntime,
752+
}
753+
}

cmd/bd/doctor/dolt_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ func TestRunDoltHealthChecks_DoltBackendNoServer(t *testing.T) {
3333
t.Setenv("BEADS_DOLT_SERVER_PORT", "59998")
3434

3535
checks := RunDoltHealthChecks(tmpDir)
36-
if len(checks) != 6 {
37-
t.Fatalf("expected exactly 6 checks (consistent shape), got %d", len(checks))
36+
if len(checks) != 7 {
37+
t.Fatalf("expected exactly 7 checks (consistent shape), got %d", len(checks))
3838
}
3939

4040
if checks[0].Name != "Dolt Connection" {
@@ -45,7 +45,7 @@ func TestRunDoltHealthChecks_DoltBackendNoServer(t *testing.T) {
4545
}
4646

4747
// Verify placeholder checks for dimensions that require a connection
48-
expectedNames := []string{"Dolt Connection", "Dolt Schema", "Dolt Issue Count", "Dolt Status", "Dolt Lock Health", "Phantom Databases"}
48+
expectedNames := []string{"Dolt Connection", "Dolt Schema", "Dolt Issue Count", "Dolt Status", "Dolt Lock Health", "Phantom Databases", "Shared Server"}
4949
for i, name := range expectedNames {
5050
if checks[i].Name != name {
5151
t.Errorf("checks[%d].Name = %q, want %q", i, checks[i].Name, name)
@@ -103,8 +103,8 @@ func TestServerMode_NoLockAcquired(t *testing.T) {
103103
t.Setenv("BEADS_DOLT_SERVER_PORT", "59999")
104104

105105
checks := RunDoltHealthChecks(tmpDir)
106-
if len(checks) != 6 {
107-
t.Fatalf("expected exactly 6 checks (consistent shape), got %d", len(checks))
106+
if len(checks) != 7 {
107+
t.Fatalf("expected exactly 7 checks, got %d", len(checks))
108108
}
109109

110110
check := checks[0]

cmd/bd/dolt.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ required. Use this command for explicit control or diagnostics.`,
281281
fmt.Printf("Dolt server started (PID %d, port %d)\n", state.PID, state.Port)
282282
fmt.Printf(" Data: %s\n", state.DataDir)
283283
fmt.Printf(" Logs: %s\n", doltserver.LogPath(serverDir))
284+
if doltserver.IsSharedServerMode() {
285+
fmt.Println(" Mode: shared server")
286+
}
284287
},
285288
}
286289

@@ -345,6 +348,9 @@ Displays whether the server is running, its PID, port, and data directory.`,
345348
fmt.Printf(" Port: %d\n", state.Port)
346349
fmt.Printf(" Data: %s\n", state.DataDir)
347350
fmt.Printf(" Logs: %s\n", doltserver.LogPath(serverDir))
351+
if doltserver.IsSharedServerMode() {
352+
fmt.Println(" Mode: shared server")
353+
}
348354
},
349355
}
350356

@@ -887,6 +893,7 @@ func showDoltConfig(testConnection bool) {
887893
result["host"] = showHost
888894
result["port"] = showPort
889895
result["user"] = cfg.GetDoltServerUser()
896+
result["shared_server"] = doltserver.IsSharedServerMode()
890897
if testConnection {
891898
result["connection_ok"] = testServerConnection(showHost, showPort)
892899
}
@@ -906,6 +913,14 @@ func showDoltConfig(testConnection bool) {
906913
fmt.Printf(" Host: %s\n", showHost)
907914
fmt.Printf(" Port: %d\n", showPort)
908915
fmt.Printf(" User: %s\n", cfg.GetDoltServerUser())
916+
if doltserver.IsSharedServerMode() {
917+
fmt.Println(" Mode: shared server")
918+
if sharedDir, err := doltserver.SharedServerDir(); err == nil {
919+
fmt.Printf(" Server: %s\n", sharedDir)
920+
}
921+
} else {
922+
fmt.Println(" Mode: per-project")
923+
}
909924

910925
if testConnection {
911926
fmt.Println()
@@ -1048,9 +1063,37 @@ func setDoltConfig(key, value string, updateConfig bool) {
10481063
}
10491064
yamlKey = "dolt.data-dir"
10501065

1066+
case "shared-server":
1067+
lower := strings.ToLower(value)
1068+
if lower != "true" && lower != "false" {
1069+
fmt.Fprintf(os.Stderr, "Error: shared-server must be 'true' or 'false'\n")
1070+
os.Exit(1)
1071+
}
1072+
// shared-server is yaml-only (not stored in metadata.json)
1073+
if err := config.SetYamlConfig("dolt.shared-server", lower); err != nil {
1074+
fmt.Fprintf(os.Stderr, "Error setting shared-server: %v\n", err)
1075+
os.Exit(1)
1076+
}
1077+
if jsonOutput {
1078+
outputJSON(map[string]interface{}{
1079+
"key": "shared-server",
1080+
"value": lower,
1081+
"location": "config.yaml",
1082+
})
1083+
return
1084+
}
1085+
if lower == "true" {
1086+
fmt.Println("Shared server mode enabled.")
1087+
fmt.Println("All projects will use a single Dolt server at ~/.beads/shared-server/.")
1088+
fmt.Println("Each project's data remains isolated in its own database.")
1089+
} else {
1090+
fmt.Println("Shared server mode disabled. Each project will use its own Dolt server.")
1091+
}
1092+
return
1093+
10511094
default:
10521095
fmt.Fprintf(os.Stderr, "Error: unknown key '%s'\n", key)
1053-
fmt.Fprintf(os.Stderr, "Valid keys: database, host, port, user, data-dir\n")
1096+
fmt.Fprintf(os.Stderr, "Valid keys: database, host, port, user, data-dir, shared-server\n")
10541097
os.Exit(1)
10551098
}
10561099

cmd/bd/init.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ environment variable.`,
6464
serverUser, _ := cmd.Flags().GetString("server-user")
6565
database, _ := cmd.Flags().GetString("database")
6666
destroyToken, _ := cmd.Flags().GetString("destroy-token")
67+
sharedServer, _ := cmd.Flags().GetBool("shared-server")
6768

6869
// Handle --backend flag: "dolt" is the only supported backend.
6970
// "sqlite" is accepted for backward compatibility but prints a
@@ -91,6 +92,13 @@ environment variable.`,
9192
// Dolt is the only supported backend
9293
backend := configfile.BackendDolt
9394

95+
// Propagate --shared-server flag to env so that IsSharedServerMode(),
96+
// ResolveDoltDir(), and DefaultConfig() all see shared mode immediately
97+
// (before config.yaml exists). Safe: init runs once and exits.
98+
if sharedServer {
99+
_ = os.Setenv("BEADS_DOLT_SHARED_SERVER", "1")
100+
}
101+
94102
// Initialize config (PersistentPreRun doesn't run for init command)
95103
if err := config.Initialize(); err != nil {
96104
fmt.Fprintf(os.Stderr, "Warning: failed to initialize config: %v\n", err)
@@ -278,6 +286,13 @@ environment variable.`,
278286

279287
useLocalBeads := filepath.Clean(initDBDirAbs) == filepath.Clean(beadsDirAbs)
280288

289+
// Shared server mode: dolt data lives in ~/.beads/shared-server/dolt/
290+
// (a different path tree), but the project still needs its local .beads/
291+
// for metadata.json, config.yaml, .gitignore, etc.
292+
if doltserver.IsSharedServerMode() {
293+
useLocalBeads = true
294+
}
295+
281296
if useLocalBeads {
282297
// Create .beads directory
283298
if err := os.MkdirAll(beadsDir, 0750); err != nil {
@@ -558,6 +573,16 @@ environment variable.`,
558573
// Non-fatal - continue anyway
559574
}
560575

576+
// Enable shared server mode if requested via flag OR env var (GH#2377).
577+
// Persist to config.yaml so the project continues working without the env var.
578+
if sharedServer || doltserver.IsSharedServerMode() {
579+
if err := config.SetYamlConfig("dolt.shared-server", "true"); err != nil {
580+
fmt.Fprintf(os.Stderr, "Warning: failed to enable shared server mode: %v\n", err)
581+
} else if !quiet {
582+
fmt.Printf(" %s Shared server mode enabled\n", ui.RenderPass("✓"))
583+
}
584+
}
585+
561586
// In stealth mode, persist no-git-ops: true so bd prime
562587
// automatically uses stealth session-close protocol (GH#2159)
563588
if stealth {
@@ -889,6 +914,7 @@ func init() {
889914
initCmd.Flags().Int("server-port", 0, "Dolt server port (default: 3307)")
890915
initCmd.Flags().String("server-user", "", "Dolt server MySQL user (default: root)")
891916
initCmd.Flags().String("database", "", "Use existing server database name (overrides prefix-based naming)")
917+
initCmd.Flags().Bool("shared-server", false, "Enable shared Dolt server mode (all projects share one server at ~/.beads/shared-server/)")
892918

893919
rootCmd.AddCommand(initCmd)
894920
}

docs/ARCHITECTURE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,11 @@ Each workspace can run its own Dolt server for multi-writer access:
165165

166166
**Server mode:**
167167
- Connects to `dolt sql-server` (multi-writer, high-concurrency)
168-
- PID file at `.beads/dolt/sql-server.pid`
169-
- Logs at `.beads/dolt/sql-server.log`
168+
- PID file at `.beads/dolt-server.pid`
169+
- Logs at `.beads/dolt-server.log`
170+
- **Shared server mode** (opt-in): all projects share a single Dolt server at
171+
`~/.beads/shared-server/` instead of per-project servers. Enable via
172+
`dolt.shared-server: true` in config.yaml or `BEADS_DOLT_SHARED_SERVER=1`.
170173

171174
**Embedded mode:**
172175
- Direct database access (single-writer, no server process)

docs/CONFIG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Tool-level settings you can configure:
4747
| `backup.git-push` | - | `BD_BACKUP_GIT_PUSH` | `false` | Auto git-add + commit + push after export |
4848
| `dolt.auto-push` | - | `BD_DOLT_AUTO_PUSH` | (auto) | Auto-push to Dolt remote after writes (auto-enabled when origin exists) |
4949
| `dolt.auto-push-interval` | - | `BD_DOLT_AUTO_PUSH_INTERVAL` | `5m` | Minimum time between auto-pushes |
50+
| `dolt.shared-server` | `--shared-server` | `BEADS_DOLT_SHARED_SERVER` | `false` | Share a single Dolt server across all projects at `~/.beads/shared-server/` |
51+
| `dolt.idle-timeout` | - | - | `30m` | Idle auto-stop timeout (`"0"` disables) |
5052
| `db` | `--db` | `BD_DB` | (auto-discover) | Database path |
5153
| `actor` | `--actor` | `BD_ACTOR` | `git config user.name` | Actor name for audit trail (see below) |
5254

docs/DOLT-BACKEND.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ dolt:
7777
| `BEADS_DOLT_SERVER_PORT` | `3306` | Server port (MySQL protocol) |
7878
| `BEADS_DOLT_SERVER_USER` | `root` | MySQL username |
7979
| `BEADS_DOLT_SERVER_PASS` | (empty) | MySQL password |
80+
| `BEADS_DOLT_SHARED_SERVER` | (empty) | Shared server mode: `1` or `true` to enable |
8081

8182
### Server Lifecycle
8283

@@ -85,13 +86,32 @@ dolt:
8586
bd doctor
8687
8788
# Server auto-starts when needed
88-
# PID stored in: .beads/dolt/sql-server.pid
89-
# Logs written to: .beads/dolt/sql-server.log
89+
# PID stored in: .beads/dolt-server.pid
90+
# Logs written to: .beads/dolt-server.log
9091
91-
# Manual stop (rarely needed)
92-
kill $(cat .beads/dolt/sql-server.pid)
92+
# Start/stop/status
93+
bd dolt start
94+
bd dolt stop
95+
bd dolt status
9396
```
9497

98+
### Shared Server Mode
99+
100+
On multi-project machines, enable shared server mode to use a single Dolt server
101+
for all projects (instead of one server per project):
102+
103+
```bash
104+
# Enable via config
105+
bd dolt set shared-server true
106+
107+
# Or via environment variable (machine-wide)
108+
export BEADS_DOLT_SHARED_SERVER=1
109+
```
110+
111+
Shared server state lives in `~/.beads/shared-server/` and uses port 3308 by default
112+
(avoiding conflict with Gas Town on 3307). Each project's data remains isolated in its
113+
own database (named by project prefix). See [DOLT.md](DOLT.md) for details.
114+
95115
## Sync Modes
96116

97117
Dolt supports multiple sync strategies:

0 commit comments

Comments
 (0)