Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions beads_cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/storage/dolt"
"github.com/steveyegge/beads/internal/storage/doltlite"
"github.com/steveyegge/beads/internal/storage/embeddeddolt"
)

Expand All @@ -19,6 +20,8 @@ import (
// exclusive flock to prevent corruption from concurrent access. This matches
// the behavior of the bd CLI.
// - Server mode: Connects to an external dolt sql-server via OpenFromConfig.
// - Doltlite mode: Opens a local doltlite database when metadata.json has
// backend="doltlite".
//
// The returned Storage must be closed when no longer needed. In embedded mode
// the caller must also defer the Unlocker returned by the flock; pass nil-safe
Expand All @@ -40,6 +43,15 @@ func OpenBestAvailable(ctx context.Context, beadsDir string) (Storage, embeddedd
return store, embeddeddolt.NoopLock{}, nil
}

if err == nil && cfg != nil && cfg.IsDoltliteBackend() {
database := cfg.GetDoltDatabase()
store, err := doltlite.New(ctx, beadsDir, database, "main")
if err != nil {
return nil, nil, err
}
return store, embeddeddolt.NoopLock{}, nil
}

// Embedded mode: acquire exclusive flock first.
dataDir := filepath.Join(beadsDir, "embeddeddolt")
lock, err := embeddeddolt.TryLock(dataDir)
Expand Down
3 changes: 3 additions & 0 deletions beads_nocgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
// beadsDir is the path to the .beads directory.
func OpenBestAvailable(ctx context.Context, beadsDir string) (Storage, embeddeddolt.Unlocker, error) {
cfg, err := configfile.Load(beadsDir)
if err == nil && cfg != nil && cfg.IsDoltliteBackend() {
return nil, nil, fmt.Errorf("doltlite requires a CGO build")
}
if err == nil && cfg != nil && cfg.IsDoltServerMode() {
store, err := dolt.NewFromConfig(ctx, beadsDir)
if err != nil {
Expand Down
76 changes: 50 additions & 26 deletions cmd/bd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
sharedServer, _ := cmd.Flags().GetBool("shared-server")
externalServer, _ := cmd.Flags().GetBool("external")

// Handle --backend flag: "dolt" is the only supported backend.
// "sqlite" is accepted for backward compatibility but prints a
// deprecation notice and exits with an error.
// Handle --backend flag. Dolt remains the default; doltlite is an
// explicit local backend for environments that do not want a Dolt
// sql-server lifecycle.
backend := configfile.BackendDolt
if backendFlag == "sqlite" {
fmt.Fprintf(os.Stderr, "%s The SQLite backend has been removed.\n\n", ui.RenderWarn("⚠ DEPRECATED:"))
fmt.Fprintf(os.Stderr, "Dolt is now the default (and only) storage backend for beads.\n")
Expand All @@ -115,8 +116,17 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
fmt.Fprintf(os.Stderr, " bd init --from-jsonl\n\n")
fmt.Fprintf(os.Stderr, "See: https://github.com/steveyegge/beads/blob/main/docs/DOLT-BACKEND.md\n")
os.Exit(1)
} else if backendFlag != "" && backendFlag != "dolt" {
FatalError("unknown backend %q: only \"dolt\" is supported", backendFlag)
} else if backendFlag == configfile.BackendDoltlite {
backend = configfile.BackendDoltlite
} else if backendFlag != "" && backendFlag != configfile.BackendDolt {
FatalError("unknown backend %q: supported backends are \"dolt\" and \"doltlite\"", backendFlag)
}
useDoltlite := backend == configfile.BackendDoltlite
if useDoltlite && (initServerMode || sharedServer || externalServer) {
FatalError("--backend=doltlite is local-only and cannot be combined with --server, --shared-server, or --external")
}
if useDoltlite && initRemoteChanged && initRemote != "" {
FatalError("--backend=doltlite does not support Dolt remote bootstrap")
}

// Validate --database format early, before any side effects.
Expand Down Expand Up @@ -148,19 +158,16 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
FatalError("--team requires interactive prompts and cannot be used with --non-interactive")
}

// Dolt is the only supported backend
backend := configfile.BackendDolt

// Also treat BEADS_DOLT_SERVER_MODE=1 env var as --server.
if os.Getenv("BEADS_DOLT_SERVER_MODE") == "1" {
if !useDoltlite && os.Getenv("BEADS_DOLT_SERVER_MODE") == "1" {
initServerMode = true
}

// Shared server mode still uses a Dolt sql-server, so it must select
// the server-backed store path during init. Without this, init can
// persist shared-server intent in YAML while still creating an embedded
// store and recording dolt_mode=embedded in metadata.json (GH#2946).
if sharedServer || strings.EqualFold(os.Getenv("BEADS_DOLT_SHARED_SERVER"), "true") || os.Getenv("BEADS_DOLT_SHARED_SERVER") == "1" {
if !useDoltlite && (sharedServer || strings.EqualFold(os.Getenv("BEADS_DOLT_SHARED_SERVER"), "true") || os.Getenv("BEADS_DOLT_SHARED_SERVER") == "1") {
initServerMode = true
}

Expand All @@ -182,7 +189,7 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
// Reject hyphens in --database for embedded mode. Must run AFTER
// serverMode is set above — otherwise isEmbeddedMode() always returns
// true and incorrectly rejects server-mode names (GH#3231).
if database != "" && strings.ContainsRune(database, '-') && isEmbeddedMode() {
if database != "" && strings.ContainsRune(database, '-') && (isEmbeddedMode() || useDoltlite) {
FatalError("database name %q contains hyphens which are invalid in embedded mode; use underscores instead (e.g. %q)",
database, sanitizeDBName(database))
}
Expand Down Expand Up @@ -321,7 +328,7 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
earlyRemoteHasDoltData = gitOriginHasDoltDataRef()
}
}
if earlySyncURL != "" {
if !useDoltlite && earlySyncURL != "" {
earlyDecision := CheckRemoteSafety(RemoteSafetyInput{
Force: force,
ReinitLocal: reinitLocal,
Expand Down Expand Up @@ -575,15 +582,15 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
syncFromRemote := false
remoteHasDoltData := false

if syncURL != "" {
if !useDoltlite && syncURL != "" {
// sync.remote was explicitly configured. Treat as bootstrap-
// from-remote intent; CheckRemoteSafety still enforces that
// --force/--reinit-local can't silently fight that intent.
// Trust the URL format as-is: normalizeRemoteURL would convert
// http:// to git+http:// and break Dolt remotesapi endpoints
// configured explicitly by the user (GH#3339).
syncFromRemote = true
} else if syncRemoteSource == initSyncRemoteNone && isGitRepo() && !isBareGitRepo() {
} else if !useDoltlite && syncRemoteSource == initSyncRemoteNone && isGitRepo() && !isBareGitRepo() {
if originURL, err := gitOriginGetURL(); err == nil && originURL != "" {
syncURL = normalizeRemoteURL(originURL)
remoteHasDoltData = gitOriginHasDoltDataRef()
Expand Down Expand Up @@ -635,7 +642,7 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
}
}
}
if syncFromRemote {
if !useDoltlite && syncFromRemote {
var err error
cloneCfg := initTimeCloneConfig(initServerMode, serverHost, serverPort, serverSocket, serverUser, dbName)
err = cloneFromRemoteWithMode(ctx, beadsDir, syncURL, dbName, cloneCfg, initRemoteCloneMode(initServerMode, externalServer))
Expand Down Expand Up @@ -700,10 +707,13 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
doltCfg.ServerUser = serverUser
}

initLock, err := acquireEmbeddedLock(beadsDir, initServerMode)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
var initLock embeddeddolt.Unlocker = embeddeddolt.NoopLock{}
if !useDoltlite {
initLock, err = acquireEmbeddedLock(beadsDir, initServerMode)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
defer initLock.Unlock()

Expand Down Expand Up @@ -755,16 +765,21 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
}
}

store, err := newDoltStore(ctx, doltCfg, embeddeddolt.WithLock(initLock))
var store storage.DoltStorage
if useDoltlite {
store, err = newDoltliteStore(ctx, beadsDir, dbName)
} else {
store, err = newDoltStore(ctx, doltCfg, embeddeddolt.WithLock(initLock))
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to open Dolt store: %v\n", err)
fmt.Fprintf(os.Stderr, "Error: failed to open %s store: %v\n", backend, err)
os.Exit(1)
}

// Initialize global database schema and config in shared-server mode.
// Opens a separate store connection to beads_global with CreateIfMissing
// to trigger schema migration, then seeds the issue prefix and project ID.
if sharedServer || doltserver.IsSharedServerMode() {
if !useDoltlite && (sharedServer || doltserver.IsSharedServerMode()) {
initGlobalDatabaseConfig(ctx, doltCfg, quiet)
}

Expand All @@ -775,7 +790,7 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
// git origin URLs for plain source repos must NOT be registered —
// they cause every Dolt fetch to fail and leak tmp_pack_* files
// that can consume 100+ GB of disk space (GH#3354, GH#3356).
if shouldWireInitRemote(syncURL, syncFromRemote, syncURLFromConfig) {
if !useDoltlite && shouldWireInitRemote(syncURL, syncFromRemote, syncURLFromConfig) {
hasRemote, _ := store.HasRemote(ctx, "origin")
if !hasRemote {
if err := store.AddRemote(ctx, "origin", syncURL); err != nil {
Expand All @@ -795,7 +810,7 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
// Set the issue prefix in config (only if not already configured —
// avoid clobbering when multiple rigs share the same Dolt database)
existing, _ := store.GetConfig(ctx, "issue_prefix")
if existing == "" {
if existing == "" || useDoltlite {
// Sanitize dots to underscores so issue IDs (e.g. "GPUPolynomials_jl-1")
// remain valid identifiers. Must match DoltDatabase sanitization above.
issuePrefix := strings.ReplaceAll(prefix, ".", "_")
Expand Down Expand Up @@ -884,6 +899,15 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):

// Always store backend explicitly in metadata.json
cfg.Backend = backend
if backend == configfile.BackendDoltlite {
cfg.Database = "doltlite"
if database != "" {
cfg.DoltDatabase = database
} else if cfg.DoltDatabase == "" && prefix != "" {
cfg.DoltDatabase = strings.ReplaceAll(prefix, "-", "_")
}
cfg.DoltMode = configfile.DoltModeEmbedded
}
// Metadata.json.database should point to the Dolt directory (not beads.db).
// Backward-compat: older dolt setups left this as "beads.db", which is misleading.
if backend == configfile.BackendDolt {
Expand Down Expand Up @@ -1424,8 +1448,8 @@ func init() {
initCmd.Flags().Bool("non-interactive", false, "Skip all interactive prompts (auto-detected in CI or non-TTY environments)")
initCmd.Flags().String("role", "", "Set beads role without prompting: \"maintainer\" or \"contributor\"")

// Backend selection (dolt is the only supported backend; sqlite accepted for deprecation notice)
initCmd.Flags().String("backend", "", "Storage backend (default: dolt). --backend=sqlite prints deprecation notice.")
// Backend selection (dolt is the default; sqlite accepted for deprecation notice)
initCmd.Flags().String("backend", "", "Storage backend (dolt|doltlite; default: dolt). --backend=sqlite prints deprecation notice.")

// Dolt server connection flags
initCmd.Flags().Bool("server", false, "Use external dolt sql-server instead of embedded engine")
Expand Down
17 changes: 11 additions & 6 deletions cmd/bd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,11 @@ var rootCmd = &cobra.Command{
// Removing them WILL cause unrecoverable data corruption and data loss.
// Dolt manages these files itself; external interference is never safe.

store, err = newDoltStore(rootCtx, doltCfg)
if cfg != nil && cfg.IsDoltliteBackend() {
store, err = newDoltliteStore(rootCtx, beadsDir, doltCfg.Database)
} else {
store, err = newDoltStore(rootCtx, doltCfg)
}

// Track final read-only state for staleness checks (GH#1089)
storeIsReadOnly = doltCfg.ReadOnly
Expand Down Expand Up @@ -1097,11 +1101,12 @@ var rootCmd = &cobra.Command{
}
}

// Auto-backup: export JSONL to .beads/backup/ if enabled and due
maybeAutoBackup(rootCtx)

// Auto-export: write git-tracked JSONL for portability if enabled and due
maybeAutoExport(rootCtx)
// Auto-backup/export write JSONL and may run git operations. Keep read-only
// commands like list/show/status as pure reads.
if !isReadOnlyCommand(cmd.Name()) {
maybeAutoBackup(rootCtx)
maybeAutoExport(rootCtx)
}

// Auto-push: push to Dolt remote if enabled and due.
// Skip for read-only commands to avoid unnecessary network operations
Expand Down
14 changes: 14 additions & 0 deletions cmd/bd/store_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/steveyegge/beads/internal/doltserver"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/dolt"
"github.com/steveyegge/beads/internal/storage/doltlite"
"github.com/steveyegge/beads/internal/storage/embeddeddolt"
)

Expand Down Expand Up @@ -47,6 +48,13 @@ func newDoltStore(ctx context.Context, cfg *dolt.Config, opts ...embeddeddolt.Op
return embeddeddolt.New(ctx, cfg.BeadsDir, cfg.Database, "main", opts...)
}

func newDoltliteStore(ctx context.Context, beadsDir, database string) (storage.DoltStorage, error) {
if database == "" {
database = configfile.DefaultDoltDatabase
}
return doltlite.New(ctx, beadsDir, database, "main")
}

// acquireEmbeddedLock acquires an exclusive flock on the embeddeddolt data
// directory derived from beadsDir. The caller must defer lock.Unlock().
// Returns a no-op lock when serverMode is true (the server handles its own
Expand All @@ -67,6 +75,9 @@ func acquireEmbeddedLock(beadsDir string, serverMode bool) (embeddeddolt.Unlocke
// auto-sanitized to underscores and the fix is persisted to metadata.json.
func newDoltStoreFromConfig(ctx context.Context, beadsDir string) (storage.DoltStorage, error) {
cfg, err := configfile.Load(beadsDir)
if err == nil && cfg != nil && cfg.IsDoltliteBackend() {
return newDoltliteStore(ctx, beadsDir, cfg.GetDoltDatabase())
}
if err == nil && cfg != nil && cfg.IsDoltServerMode() {
return dolt.NewFromConfig(ctx, beadsDir)
}
Expand Down Expand Up @@ -131,6 +142,9 @@ func migrateHyphenatedDB(beadsDir string, cfg *configfile.Config, oldName, newNa
// hydration from mutating foreign projects (GH#3231).
func newReadOnlyStoreFromConfig(ctx context.Context, beadsDir string) (storage.DoltStorage, error) {
cfg, err := configfile.Load(beadsDir)
if err == nil && cfg != nil && cfg.IsDoltliteBackend() {
return newDoltliteStore(ctx, beadsDir, cfg.GetDoltDatabase())
}
if err == nil && cfg != nil && cfg.IsDoltServerMode() {
return dolt.NewFromConfigWithOptions(ctx, beadsDir, &dolt.Config{ReadOnly: true})
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/bd/store_factory_nocgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func newDoltStore(ctx context.Context, cfg *dolt.Config, _ ...embeddeddolt.Optio
return dolt.New(ctx, cfg)
}

func newDoltliteStore(_ context.Context, _, _ string) (storage.DoltStorage, error) {
return nil, fmt.Errorf("doltlite requires a CGO build")
}

// acquireEmbeddedLock returns a no-op lock in non-CGO builds.
func acquireEmbeddedLock(_ string, _ bool) (embeddeddolt.Unlocker, error) {
return embeddeddolt.NoopLock{}, nil
Expand All @@ -35,6 +39,9 @@ func acquireEmbeddedLock(_ string, _ bool) (embeddeddolt.Unlocker, error) {
// newDoltStoreFromConfig creates a server-mode storage backend from config.
func newDoltStoreFromConfig(ctx context.Context, beadsDir string) (storage.DoltStorage, error) {
cfg, err := configfile.Load(beadsDir)
if err == nil && cfg != nil && cfg.IsDoltliteBackend() {
return nil, fmt.Errorf("doltlite requires a CGO build")
}
if err == nil && cfg != nil && cfg.IsDoltServerMode() {
return dolt.NewFromConfig(ctx, beadsDir)
}
Expand All @@ -44,6 +51,9 @@ func newDoltStoreFromConfig(ctx context.Context, beadsDir string) (storage.DoltS
// newReadOnlyStoreFromConfig creates a read-only server-mode storage backend.
func newReadOnlyStoreFromConfig(ctx context.Context, beadsDir string) (storage.DoltStorage, error) {
cfg, err := configfile.Load(beadsDir)
if err == nil && cfg != nil && cfg.IsDoltliteBackend() {
return nil, fmt.Errorf("doltlite requires a CGO build")
}
if err == nil && cfg != nil && cfg.IsDoltServerMode() {
return dolt.NewFromConfigWithOptions(ctx, beadsDir, &dolt.Config{ReadOnly: true})
}
Expand Down
Loading
Loading