Files
beads/cmd/bd/sync_mode.go
Peter Chanthamynavong b7d650bd8e fix(init): respect BEADS_DIR environment variable (#1273)
* fix(sync): read sync.mode from yaml first, then database

bd config set sync.mode writes to config.yaml (because sync.* is a
yaml-only prefix), but GetSyncMode() only read from the database.

This caused dolt-native mode to be ignored - JSONL export still
happened because the database had no sync.mode value.

Now GetSyncMode() checks config.yaml first (via config.GetSyncMode()),
falling back to database for backward compatibility.

Fixes: oss-5ca279

* fix(init): respect BEADS_DIR environment variable

Problem:
- `bd init` ignored BEADS_DIR when checking for existing data
- `bd init` created database at CWD/.beads instead of BEADS_DIR
- Contributor wizard used ~/.beads-planning as default, ignoring BEADS_DIR

Solution:
- Add BEADS_DIR check in checkExistingBeadsData() (matches FindBeadsDir pattern)
- Compute beadsDirForInit early, before initDBPath determination
- Use BEADS_DIR as default in contributor wizard when set
- Preserve precedence: --db > BEADS_DB > BEADS_DIR > default

Impact:
- Users with BEADS_DIR set now get consistent behavior across all bd commands
- ACF-style fork tracking (external .beads directory) now works correctly

Fixes: steveyegge/beads#???

* fix(doctor): respect BEADS_DIR environment variable

Also updates documentation to reflect BEADS_DIR support in init and doctor.

Changes:
- doctor.go: Check BEADS_DIR before falling back to CWD
- doctor_test.go: Add tests for BEADS_DIR path resolution
- WORKTREES.md: Document simplified BEADS_DIR+init workflow
- CONTRIBUTOR_NAMESPACE_ISOLATION.md: Note init/doctor BEADS_DIR support

* test(init): add BEADS_DB > BEADS_DIR precedence test

Verifies that BEADS_DB env var takes precedence over BEADS_DIR
when both are set, ensuring the documented precedence order:
--db > BEADS_DB > BEADS_DIR > default

* chore: fill in GH#1277 placeholder in sync_mode comment
2026-01-24 17:10:05 -08:00

137 lines
4.4 KiB
Go

package main
import (
"context"
"fmt"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/storage"
)
// Sync mode constants - re-exported from internal/config for backward compatibility.
// These are used with storage.Storage (database) while config.SyncMode* are used
// with viper (config.yaml).
const (
// SyncModeGitPortable exports to JSONL on push, imports on pull.
// This is the default mode - works with standard git workflows.
SyncModeGitPortable = string(config.SyncModeGitPortable)
// SyncModeRealtime exports to JSONL on every database mutation.
// Provides immediate persistence but more git noise.
SyncModeRealtime = string(config.SyncModeRealtime)
// SyncModeDoltNative uses Dolt remotes for sync, skipping JSONL.
// Requires Dolt backend and configured Dolt remote.
SyncModeDoltNative = string(config.SyncModeDoltNative)
// SyncModeBeltAndSuspenders uses both Dolt remotes AND JSONL.
// Maximum redundancy - Dolt for versioning, JSONL for git portability.
SyncModeBeltAndSuspenders = string(config.SyncModeBeltAndSuspenders)
// 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, checking config.yaml first (where bd config set writes),
// then falling back to database. This fixes GH#1277 where yaml and database were inconsistent.
func GetSyncMode(ctx context.Context, s storage.Storage) string {
// First check config.yaml (where bd config set writes for sync.* keys)
yamlMode := config.GetSyncMode()
if yamlMode != "" && yamlMode != config.SyncModeGitPortable {
// Non-default value in yaml takes precedence
return string(yamlMode)
}
// Fall back to database (legacy path)
mode, err := s.GetConfig(ctx, SyncModeConfigKey)
if err != nil || mode == "" {
return SyncModeGitPortable
}
// Validate mode using the shared validation
if !config.IsValidSyncMode(mode) {
return SyncModeGitPortable
}
return mode
}
// SetSyncMode sets the sync mode configuration in the database.
func SetSyncMode(ctx context.Context, s storage.Storage, mode string) error {
// Validate mode using the shared validation
if !config.IsValidSyncMode(mode) {
return fmt.Errorf("invalid sync mode: %s (valid: %s)",
mode, fmt.Sprintf("%v", config.ValidSyncModes()))
}
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"
}
}