Files
beads/cmd/bd/sync_mode.go
beads/crew/emma 356ab92b78 feat(sync): wire up sync.mode config to change sync behavior
Implements hq-ew1mbr.27: The sync.mode config now actually changes how
bd sync operates:

- git-portable (default): JSONL exported on push, imported on pull
- realtime: JSONL exported on every change (placeholder for daemon hook)
- dolt-native: Uses Dolt Push/Pull, skips JSONL workflow entirely
- belt-and-suspenders: Both Dolt remotes AND JSONL for redundancy

Changes:
- Add sync_mode.go with mode constants, Get/Set functions, and helpers
- Update bd sync --status to show actual mode from config
- Add --set-mode flag to bd sync for configuring the mode
- Modify doExportSync to respect mode (Dolt push for dolt-native)
- Modify doPullFirstSync to use Dolt pull for dolt-native mode
- Add RemoteStorage interface for Push/Pull operations
- Add comprehensive tests for sync mode functionality

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 10:36:20 -08:00

130 lines
3.9 KiB
Go

package main
import (
"context"
"fmt"
"github.com/steveyegge/beads/internal/storage"
)
// Sync mode constants define how beads synchronizes data with git.
const (
// SyncModeGitPortable exports to JSONL on push, imports on pull.
// This is the default mode - works with standard git workflows.
SyncModeGitPortable = "git-portable"
// SyncModeRealtime exports to JSONL on every database mutation.
// Provides immediate persistence but more git noise.
SyncModeRealtime = "realtime"
// SyncModeDoltNative uses Dolt remotes for sync, skipping JSONL.
// Requires Dolt backend and configured Dolt remote.
SyncModeDoltNative = "dolt-native"
// SyncModeBeltAndSuspenders uses both Dolt remotes AND JSONL.
// Maximum redundancy - Dolt for versioning, JSONL for git portability.
SyncModeBeltAndSuspenders = "belt-and-suspenders"
// SyncModeConfigKey is the database config key for sync mode.
SyncModeConfigKey = "sync.mode"
// SyncExportOnConfigKey controls when JSONL export happens.
SyncExportOnConfigKey = "sync.export_on"
// SyncImportOnConfigKey controls when JSONL import happens.
SyncImportOnConfigKey = "sync.import_on"
)
// Trigger constants for export_on and import_on settings.
const (
// TriggerPush triggers on git push (export) or git pull (import).
TriggerPush = "push"
TriggerPull = "pull"
// TriggerChange triggers on every database mutation (realtime mode).
TriggerChange = "change"
)
// GetSyncMode returns the configured sync mode, defaulting to git-portable.
func GetSyncMode(ctx context.Context, s storage.Storage) string {
mode, err := s.GetConfig(ctx, SyncModeConfigKey)
if err != nil || mode == "" {
return SyncModeGitPortable
}
// Validate mode
switch mode {
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
return mode
default:
// Invalid mode, return default
return SyncModeGitPortable
}
}
// SetSyncMode sets the sync mode configuration.
func SetSyncMode(ctx context.Context, s storage.Storage, mode string) error {
// Validate mode
switch mode {
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
// Valid
default:
return fmt.Errorf("invalid sync mode: %s (valid: %s, %s, %s, %s)",
mode, SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders)
}
return s.SetConfig(ctx, SyncModeConfigKey, mode)
}
// GetExportTrigger returns when JSONL export should happen.
func GetExportTrigger(ctx context.Context, s storage.Storage) string {
trigger, err := s.GetConfig(ctx, SyncExportOnConfigKey)
if err != nil || trigger == "" {
// Default based on sync mode
mode := GetSyncMode(ctx, s)
if mode == SyncModeRealtime {
return TriggerChange
}
return TriggerPush
}
return trigger
}
// GetImportTrigger returns when JSONL import should happen.
func GetImportTrigger(ctx context.Context, s storage.Storage) string {
trigger, err := s.GetConfig(ctx, SyncImportOnConfigKey)
if err != nil || trigger == "" {
return TriggerPull
}
return trigger
}
// ShouldExportJSONL returns true if the current sync mode uses JSONL export.
func ShouldExportJSONL(ctx context.Context, s storage.Storage) bool {
mode := GetSyncMode(ctx, s)
// All modes except dolt-native use JSONL
return mode != SyncModeDoltNative
}
// ShouldUseDoltRemote returns true if the current sync mode uses Dolt remotes.
func ShouldUseDoltRemote(ctx context.Context, s storage.Storage) bool {
mode := GetSyncMode(ctx, s)
return mode == SyncModeDoltNative || mode == SyncModeBeltAndSuspenders
}
// SyncModeDescription returns a human-readable description of the sync mode.
func SyncModeDescription(mode string) string {
switch mode {
case SyncModeGitPortable:
return "JSONL exported on push, imported on pull"
case SyncModeRealtime:
return "JSONL exported on every change"
case SyncModeDoltNative:
return "Dolt remotes only, no JSONL"
case SyncModeBeltAndSuspenders:
return "Both Dolt remotes and JSONL"
default:
return "unknown mode"
}
}