Skip to content

Commit dc8bb7a

Browse files
steveyeggeclaude
andcommitted
feat(cli): add bd backend and bd sync mode subcommands
Add discoverable subcommands for backend and sync mode management: - bd backend list: Show available backends (sqlite, dolt, jsonl) - bd backend show: Show current backend configuration - bd sync mode list: Show available sync modes - bd sync mode current: Show current sync mode - bd sync mode set <mode>: Set sync mode with validation All commands support --json output for programmatic use. Closes: bd-ats9.3.3 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3abec58 commit dc8bb7a

2 files changed

Lines changed: 363 additions & 0 deletions

File tree

cmd/bd/backend.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/steveyegge/beads/internal/beads"
10+
"github.com/steveyegge/beads/internal/configfile"
11+
)
12+
13+
// Available backends with descriptions
14+
var availableBackends = []struct {
15+
Name string
16+
Description string
17+
}{
18+
{"sqlite", "SQLite database (default) - single file, portable, no dependencies"},
19+
{"dolt", "Dolt database - Git-like versioning for data, SQL interface"},
20+
{"jsonl", "JSONL only (--no-db mode) - plain text, no database required"},
21+
}
22+
23+
var backendCmd = &cobra.Command{
24+
Use: "backend",
25+
GroupID: "sync",
26+
Short: "Manage storage backend configuration",
27+
Long: `Manage storage backend configuration.
28+
29+
The backend determines how beads stores issue data:
30+
- sqlite: Default SQLite database (single file, portable)
31+
- dolt: Dolt database (Git-like versioning, SQL interface)
32+
- jsonl: JSONL only mode (plain text, use with --no-db flag)
33+
34+
The backend is set at initialization time with 'bd init --backend <type>'.
35+
To change backends, use 'bd migrate dolt' or reinitialize.
36+
37+
Commands:
38+
bd backend list List available backends
39+
bd backend show Show current backend configuration`,
40+
}
41+
42+
var backendListCmd = &cobra.Command{
43+
Use: "list",
44+
Short: "List available storage backends",
45+
Long: `List all available storage backends and their descriptions.
46+
47+
Available backends:
48+
sqlite SQLite database (default) - single file, portable, no dependencies
49+
dolt Dolt database - Git-like versioning for data, SQL interface
50+
jsonl JSONL only (--no-db mode) - plain text, no database required
51+
52+
The backend is chosen at initialization time:
53+
bd init # Uses sqlite (default)
54+
bd init --backend dolt # Uses dolt
55+
bd init --no-db # Uses jsonl only`,
56+
Run: func(cmd *cobra.Command, args []string) {
57+
if jsonOutput {
58+
backends := make([]map[string]string, len(availableBackends))
59+
for i, b := range availableBackends {
60+
backends[i] = map[string]string{
61+
"name": b.Name,
62+
"description": b.Description,
63+
}
64+
}
65+
outputJSON(map[string]interface{}{
66+
"backends": backends,
67+
})
68+
return
69+
}
70+
71+
fmt.Println("Available backends:")
72+
for _, b := range availableBackends {
73+
fmt.Printf(" %-8s %s\n", b.Name, b.Description)
74+
}
75+
fmt.Println("\nSet backend at init time: bd init --backend <name>")
76+
fmt.Println("Migrate to Dolt: bd migrate dolt")
77+
},
78+
}
79+
80+
var backendShowCmd = &cobra.Command{
81+
Use: "show",
82+
Short: "Show current backend configuration",
83+
Long: `Show the current storage backend configuration.
84+
85+
Displays:
86+
- Current backend type (sqlite, dolt, or jsonl)
87+
- Backend-specific settings (e.g., Dolt server mode)
88+
- Database location`,
89+
Run: func(cmd *cobra.Command, args []string) {
90+
// Find the beads directory
91+
beadsDir := beads.FindBeadsDir()
92+
if beadsDir == "" {
93+
fmt.Fprintf(os.Stderr, "Error: not in a beads repository (no .beads directory found)\n")
94+
os.Exit(1)
95+
}
96+
97+
// Check for no-db mode first
98+
if noDb || isNoDbModeConfigured(beadsDir) {
99+
if jsonOutput {
100+
outputJSON(map[string]interface{}{
101+
"backend": "jsonl",
102+
"description": "JSONL only (no database)",
103+
"beads_dir": beadsDir,
104+
"jsonl_file": filepath.Join(beadsDir, "issues.jsonl"),
105+
})
106+
return
107+
}
108+
fmt.Println("Current backend: jsonl")
109+
fmt.Println(" Mode: JSONL only (no database)")
110+
fmt.Printf(" Beads dir: %s\n", beadsDir)
111+
fmt.Printf(" JSONL file: %s\n", filepath.Join(beadsDir, "issues.jsonl"))
112+
return
113+
}
114+
115+
// Load metadata.json
116+
cfg, err := configfile.Load(beadsDir)
117+
if err != nil {
118+
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
119+
os.Exit(1)
120+
}
121+
if cfg == nil {
122+
cfg = configfile.DefaultConfig()
123+
}
124+
125+
backend := cfg.GetBackend()
126+
127+
if jsonOutput {
128+
result := map[string]interface{}{
129+
"backend": backend,
130+
"beads_dir": beadsDir,
131+
"database": cfg.DatabasePath(beadsDir),
132+
}
133+
134+
// Add Dolt-specific info
135+
if backend == configfile.BackendDolt {
136+
result["dolt_mode"] = cfg.GetDoltMode()
137+
if cfg.IsDoltServerMode() {
138+
result["dolt_server_host"] = cfg.GetDoltServerHost()
139+
result["dolt_server_port"] = cfg.GetDoltServerPort()
140+
result["dolt_server_user"] = cfg.GetDoltServerUser()
141+
result["dolt_database"] = cfg.GetDoltDatabase()
142+
}
143+
}
144+
145+
outputJSON(result)
146+
return
147+
}
148+
149+
// Text output
150+
fmt.Printf("Current backend: %s\n", backend)
151+
152+
// Find description
153+
for _, b := range availableBackends {
154+
if b.Name == backend {
155+
fmt.Printf(" Description: %s\n", b.Description)
156+
break
157+
}
158+
}
159+
160+
fmt.Printf(" Beads dir: %s\n", beadsDir)
161+
fmt.Printf(" Database: %s\n", cfg.DatabasePath(beadsDir))
162+
163+
// Dolt-specific info
164+
if backend == configfile.BackendDolt {
165+
fmt.Printf(" Dolt mode: %s\n", cfg.GetDoltMode())
166+
if cfg.IsDoltServerMode() {
167+
fmt.Printf(" Server: %s:%d\n", cfg.GetDoltServerHost(), cfg.GetDoltServerPort())
168+
fmt.Printf(" User: %s\n", cfg.GetDoltServerUser())
169+
fmt.Printf(" Database: %s\n", cfg.GetDoltDatabase())
170+
}
171+
}
172+
},
173+
}
174+
175+
func init() {
176+
backendCmd.AddCommand(backendListCmd)
177+
backendCmd.AddCommand(backendShowCmd)
178+
rootCmd.AddCommand(backendCmd)
179+
}

cmd/bd/sync_mode_cmd.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/steveyegge/beads/internal/config"
10+
)
11+
12+
// Sync mode info for display
13+
var syncModeInfo = []struct {
14+
Mode string
15+
Description string
16+
}{
17+
{SyncModeGitPortable, "JSONL exported on push, imported on pull (default)"},
18+
{SyncModeRealtime, "JSONL exported on every change (more git noise)"},
19+
{SyncModeDoltNative, "Dolt remotes only, no JSONL (requires Dolt backend)"},
20+
{SyncModeBeltAndSuspenders, "Both Dolt remotes and JSONL (maximum redundancy)"},
21+
}
22+
23+
var syncModeCmd = &cobra.Command{
24+
Use: "mode",
25+
Short: "Manage sync mode configuration",
26+
Long: `Manage sync mode configuration.
27+
28+
Sync mode controls how beads synchronizes data with git:
29+
30+
git-portable (default)
31+
JSONL exported on push, imported on pull.
32+
Works with standard git workflows.
33+
34+
realtime
35+
JSONL exported on every database change.
36+
Provides immediate persistence but more git noise.
37+
38+
dolt-native
39+
Uses Dolt remotes for sync, skips JSONL.
40+
Requires Dolt backend and configured Dolt remote.
41+
42+
belt-and-suspenders
43+
Uses both Dolt remotes AND JSONL.
44+
Maximum redundancy - Dolt for versioning, JSONL for git portability.
45+
46+
Commands:
47+
bd sync mode list List available sync modes
48+
bd sync mode current Show current sync mode
49+
bd sync mode set Set sync mode`,
50+
}
51+
52+
var syncModeListCmd = &cobra.Command{
53+
Use: "list",
54+
Short: "List available sync modes",
55+
Long: `List all available sync modes and their descriptions.
56+
57+
Sync modes control how beads synchronizes with git and Dolt remotes.`,
58+
Run: func(cmd *cobra.Command, args []string) {
59+
if jsonOutput {
60+
modes := make([]map[string]string, len(syncModeInfo))
61+
for i, m := range syncModeInfo {
62+
modes[i] = map[string]string{
63+
"mode": m.Mode,
64+
"description": m.Description,
65+
}
66+
}
67+
outputJSON(map[string]interface{}{
68+
"modes": modes,
69+
})
70+
return
71+
}
72+
73+
fmt.Println("Available sync modes:")
74+
for _, m := range syncModeInfo {
75+
fmt.Printf(" %-22s %s\n", m.Mode, m.Description)
76+
}
77+
fmt.Println("\nSet sync mode: bd sync mode set <mode>")
78+
},
79+
}
80+
81+
var syncModeCurrentCmd = &cobra.Command{
82+
Use: "current",
83+
Short: "Show current sync mode",
84+
Long: `Show the currently configured sync mode.`,
85+
Run: func(cmd *cobra.Command, args []string) {
86+
// Try to get from database if available
87+
var currentMode string
88+
if store != nil {
89+
currentMode = GetSyncMode(rootCtx, store)
90+
} else {
91+
// Fall back to config.yaml
92+
currentMode = string(config.GetSyncMode())
93+
}
94+
95+
if jsonOutput {
96+
result := map[string]interface{}{
97+
"mode": currentMode,
98+
}
99+
// Add description
100+
for _, m := range syncModeInfo {
101+
if m.Mode == currentMode {
102+
result["description"] = m.Description
103+
break
104+
}
105+
}
106+
outputJSON(result)
107+
return
108+
}
109+
110+
fmt.Printf("Current sync mode: %s\n", currentMode)
111+
// Show description
112+
for _, m := range syncModeInfo {
113+
if m.Mode == currentMode {
114+
fmt.Printf(" %s\n", m.Description)
115+
break
116+
}
117+
}
118+
},
119+
}
120+
121+
var syncModeSetCmd = &cobra.Command{
122+
Use: "set <mode>",
123+
Short: "Set sync mode",
124+
Long: `Set the sync mode.
125+
126+
Valid modes:
127+
git-portable JSONL exported on push, imported on pull (default)
128+
realtime JSONL exported on every change
129+
dolt-native Dolt remotes only, no JSONL (requires Dolt backend)
130+
belt-and-suspenders Both Dolt remotes and JSONL
131+
132+
Example:
133+
bd sync mode set realtime
134+
bd sync mode set dolt-native`,
135+
Args: cobra.ExactArgs(1),
136+
Run: func(cmd *cobra.Command, args []string) {
137+
CheckReadonly("sync mode set")
138+
139+
mode := strings.TrimSpace(args[0])
140+
141+
// Validate mode
142+
if !config.IsValidSyncMode(mode) {
143+
fmt.Fprintf(os.Stderr, "Error: invalid sync mode: %s\n", mode)
144+
fmt.Fprintf(os.Stderr, "Valid modes: %s\n", strings.Join(config.ValidSyncModes(), ", "))
145+
os.Exit(1)
146+
}
147+
148+
// Require direct mode for database writes
149+
if err := ensureDirectMode("sync mode set requires direct database access"); err != nil {
150+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
151+
os.Exit(1)
152+
}
153+
154+
// Set the mode
155+
if err := SetSyncMode(rootCtx, store, mode); err != nil {
156+
fmt.Fprintf(os.Stderr, "Error setting sync mode: %v\n", err)
157+
os.Exit(1)
158+
}
159+
160+
if jsonOutput {
161+
outputJSON(map[string]interface{}{
162+
"mode": mode,
163+
"message": fmt.Sprintf("Sync mode set to %s", mode),
164+
})
165+
return
166+
}
167+
168+
fmt.Printf("Sync mode set to: %s\n", mode)
169+
// Show description
170+
for _, m := range syncModeInfo {
171+
if m.Mode == mode {
172+
fmt.Printf(" %s\n", m.Description)
173+
break
174+
}
175+
}
176+
},
177+
}
178+
179+
func init() {
180+
syncModeCmd.AddCommand(syncModeListCmd)
181+
syncModeCmd.AddCommand(syncModeCurrentCmd)
182+
syncModeCmd.AddCommand(syncModeSetCmd)
183+
syncCmd.AddCommand(syncModeCmd)
184+
}

0 commit comments

Comments
 (0)