Implement BEADS_DIR environment variable (bd-e16b)
Add BEADS_DIR as a replacement for BEADS_DB to point to the .beads directory instead of the database file directly. Rationale: - With --no-db mode, there's no .db file to point to - The .beads directory is the logical unit (contains config.yaml, db files, jsonl files) - More intuitive: point to the beads directory not the database file Implementation: - Add BEADS_DIR environment variable support to FindDatabasePath() - Priority order: BEADS_DIR > BEADS_DB > auto-discovery - Maintain backward compatibility with BEADS_DB (now deprecated) - Update --no-db mode to respect BEADS_DIR - Update MCP integration (config.py, bd_client.py) - Update documentation to show BEADS_DIR as preferred method Testing: - Backward compatibility: BEADS_DB still works - BEADS_DIR works with regular database mode - BEADS_DIR works with --no-db mode - Priority: BEADS_DIR takes precedence over BEADS_DB Follow-up issues for refactoring: - bd-efe8: Refactor path canonicalization into helper function - bd-c362: Extract database search logic into helper function Closes bd-e16b 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -146,7 +146,8 @@ This means bd found multiple `.beads` directories in your directory hierarchy. T
|
||||
1. **If you have nested projects** (intentional):
|
||||
- This is fine! bd is designed to support this
|
||||
- Just be aware which database you're using
|
||||
- Set `BEADS_DB` environment variable if you want to override the default selection
|
||||
- Set `BEADS_DIR` environment variable to point to your `.beads` directory if you want to override the default selection
|
||||
- Or use `BEADS_DB` (deprecated) to point directly to the database file
|
||||
|
||||
2. **If you have accidental duplicates** (unintentional):
|
||||
- Decide which database to keep
|
||||
@@ -156,10 +157,14 @@ This means bd found multiple `.beads` directories in your directory hierarchy. T
|
||||
|
||||
3. **Override database selection**:
|
||||
```bash
|
||||
# Temporarily use specific database
|
||||
BEADS_DB=/path/to/.beads/issues.db bd list
|
||||
# Temporarily use specific .beads directory (recommended)
|
||||
BEADS_DIR=/path/to/.beads bd list
|
||||
|
||||
# Or add to shell config for permanent override
|
||||
export BEADS_DIR=/path/to/.beads
|
||||
|
||||
# Legacy method (deprecated, points to database file directly)
|
||||
BEADS_DB=/path/to/.beads/issues.db bd list
|
||||
export BEADS_DB=/path/to/.beads/issues.db
|
||||
```
|
||||
|
||||
|
||||
57
beads.go
57
beads.go
@@ -118,12 +118,61 @@ func NewSQLiteStorage(dbPath string) (Storage, error) {
|
||||
}
|
||||
|
||||
// FindDatabasePath discovers the bd database path using bd's standard search order:
|
||||
// 1. $BEADS_DB environment variable
|
||||
// 2. .beads/*.db in current directory or ancestors
|
||||
// 1. $BEADS_DIR environment variable (points to .beads directory)
|
||||
// 2. $BEADS_DB environment variable (points directly to database file, deprecated)
|
||||
// 3. .beads/*.db in current directory or ancestors
|
||||
//
|
||||
// Returns empty string if no database is found.
|
||||
func FindDatabasePath() string {
|
||||
// 1. Check environment variable
|
||||
// 1. Check BEADS_DIR environment variable (preferred)
|
||||
if beadsDir := os.Getenv("BEADS_DIR"); beadsDir != "" {
|
||||
// Canonicalize the path to prevent nested .beads directories
|
||||
var absBeadsDir string
|
||||
if absPath, err := filepath.Abs(beadsDir); err == nil {
|
||||
if canonical, err := filepath.EvalSymlinks(absPath); err == nil {
|
||||
absBeadsDir = canonical
|
||||
} else {
|
||||
absBeadsDir = absPath
|
||||
}
|
||||
} else {
|
||||
absBeadsDir = beadsDir
|
||||
}
|
||||
|
||||
// Check for config.json first (single source of truth)
|
||||
if cfg, err := configfile.Load(absBeadsDir); err == nil && cfg != nil {
|
||||
dbPath := cfg.DatabasePath(absBeadsDir)
|
||||
if _, err := os.Stat(dbPath); err == nil {
|
||||
return dbPath
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to canonical beads.db for backward compatibility
|
||||
canonicalDB := filepath.Join(absBeadsDir, CanonicalDatabaseName)
|
||||
if _, err := os.Stat(canonicalDB); err == nil {
|
||||
return canonicalDB
|
||||
}
|
||||
|
||||
// Look for any .db file in the beads directory
|
||||
matches, err := filepath.Glob(filepath.Join(absBeadsDir, "*.db"))
|
||||
if err == nil && len(matches) > 0 {
|
||||
// Filter out backup files and vc.db
|
||||
var validDBs []string
|
||||
for _, match := range matches {
|
||||
baseName := filepath.Base(match)
|
||||
if !strings.Contains(baseName, ".backup") && baseName != "vc.db" {
|
||||
validDBs = append(validDBs, match)
|
||||
}
|
||||
}
|
||||
if len(validDBs) > 0 {
|
||||
return validDBs[0]
|
||||
}
|
||||
}
|
||||
|
||||
// BEADS_DIR is set but no database found - this is OK for --no-db mode
|
||||
// Return empty string and let the caller handle it
|
||||
}
|
||||
|
||||
// 2. Check BEADS_DB environment variable (deprecated but still supported)
|
||||
if envDB := os.Getenv("BEADS_DB"); envDB != "" {
|
||||
// Canonicalize the path to prevent nested .beads directories
|
||||
if absDB, err := filepath.Abs(envDB); err == nil {
|
||||
@@ -135,7 +184,7 @@ func FindDatabasePath() string {
|
||||
return envDB // Fallback to original if Abs fails
|
||||
}
|
||||
|
||||
// 2. Search for .beads/*.db in current directory and ancestors
|
||||
// 3. Search for .beads/*.db in current directory and ancestors
|
||||
if foundDB := findDatabaseInTree(); foundDB != "" {
|
||||
// Canonicalize found path
|
||||
if absDB, err := filepath.Abs(foundDB); err == nil {
|
||||
|
||||
@@ -177,7 +177,8 @@ var rootCmd = &cobra.Command{
|
||||
// No database found - error out instead of falling back to ~/.beads
|
||||
fmt.Fprintf(os.Stderr, "Error: no beads database found\n")
|
||||
fmt.Fprintf(os.Stderr, "Hint: run 'bd init' to create a database in the current directory\n")
|
||||
fmt.Fprintf(os.Stderr, " or set BEADS_DB environment variable to specify a database\n")
|
||||
fmt.Fprintf(os.Stderr, " or set BEADS_DIR to point to your .beads directory\n")
|
||||
fmt.Fprintf(os.Stderr, " or set BEADS_DB to point to your database file (deprecated)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -438,13 +439,29 @@ var rootCmd = &cobra.Command{
|
||||
// Handle --no-db mode: write memory storage back to JSONL
|
||||
if noDb {
|
||||
if store != nil {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
// Determine beads directory (respect BEADS_DIR)
|
||||
var beadsDir string
|
||||
if envDir := os.Getenv("BEADS_DIR"); envDir != "" {
|
||||
// Canonicalize the path
|
||||
if absDir, err := filepath.Abs(envDir); err == nil {
|
||||
if canonical, err := filepath.EvalSymlinks(absDir); err == nil {
|
||||
beadsDir = canonical
|
||||
} else {
|
||||
beadsDir = absDir
|
||||
}
|
||||
} else {
|
||||
beadsDir = envDir
|
||||
}
|
||||
} else {
|
||||
// Fall back to current directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
beadsDir = filepath.Join(cwd, ".beads")
|
||||
}
|
||||
|
||||
beadsDir := filepath.Join(cwd, ".beads")
|
||||
if memStore, ok := store.(*memory.MemoryStorage); ok {
|
||||
if err := writeIssuesToJSONL(memStore, beadsDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: failed to write JSONL: %v\n", err)
|
||||
|
||||
@@ -18,14 +18,31 @@ import (
|
||||
// This is called when --no-db flag is set
|
||||
func initializeNoDbMode() error {
|
||||
// Find .beads directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current directory: %w", err)
|
||||
var beadsDir string
|
||||
|
||||
// Check BEADS_DIR environment variable first
|
||||
if envDir := os.Getenv("BEADS_DIR"); envDir != "" {
|
||||
// Canonicalize the path
|
||||
if absDir, err := filepath.Abs(envDir); err == nil {
|
||||
if canonical, err := filepath.EvalSymlinks(absDir); err == nil {
|
||||
beadsDir = canonical
|
||||
} else {
|
||||
beadsDir = absDir
|
||||
}
|
||||
} else {
|
||||
beadsDir = envDir
|
||||
}
|
||||
} else {
|
||||
// Fall back to current directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current directory: %w", err)
|
||||
}
|
||||
beadsDir = filepath.Join(cwd, ".beads")
|
||||
}
|
||||
|
||||
beadsDir := filepath.Join(cwd, ".beads")
|
||||
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("no .beads directory found (hint: run 'bd init' first)")
|
||||
return fmt.Errorf("no .beads directory found (hint: run 'bd init' first or set BEADS_DIR)")
|
||||
}
|
||||
|
||||
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||
|
||||
@@ -371,13 +371,13 @@ Use when parsing programmatically or extracting specific fields.
|
||||
bd finds database in this order:
|
||||
|
||||
1. `--db` flag: `bd ready --db /path/to/db.db`
|
||||
2. `$BEADS_DB` environment variable
|
||||
3. `.beads/*.db` in current directory or ancestors
|
||||
4. `~/.beads/default.db` as fallback
|
||||
2. `$BEADS_DIR` environment variable (points to .beads directory)
|
||||
3. `$BEADS_DB` environment variable (deprecated, points to database file)
|
||||
4. `.beads/*.db` in current directory or ancestors
|
||||
|
||||
**Project-local** (`.beads/`): Project-specific work, git-tracked
|
||||
|
||||
**Global fallback** (`~/.beads/`): Cross-project tracking, personal tasks
|
||||
**Recommended**: Use `BEADS_DIR` to point to your `.beads` directory, especially when using `--no-db` mode
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ class BdCliClient(BdClientBase):
|
||||
"""Client for calling bd CLI commands and parsing JSON output."""
|
||||
|
||||
bd_path: str
|
||||
beads_dir: str | None
|
||||
beads_db: str | None
|
||||
actor: str | None
|
||||
no_auto_flush: bool
|
||||
@@ -153,6 +154,7 @@ class BdCliClient(BdClientBase):
|
||||
def __init__(
|
||||
self,
|
||||
bd_path: str | None = None,
|
||||
beads_dir: str | None = None,
|
||||
beads_db: str | None = None,
|
||||
actor: str | None = None,
|
||||
no_auto_flush: bool | None = None,
|
||||
@@ -163,7 +165,8 @@ class BdCliClient(BdClientBase):
|
||||
|
||||
Args:
|
||||
bd_path: Path to bd executable (optional, loads from config if not provided)
|
||||
beads_db: Path to beads database file (optional, loads from config if not provided)
|
||||
beads_dir: Path to .beads directory (optional, loads from config if not provided)
|
||||
beads_db: Path to beads database file (deprecated, optional, loads from config if not provided)
|
||||
actor: Actor name for audit trail (optional, loads from config if not provided)
|
||||
no_auto_flush: Disable automatic JSONL sync (optional, loads from config if not provided)
|
||||
no_auto_import: Disable automatic JSONL import (optional, loads from config if not provided)
|
||||
@@ -171,6 +174,7 @@ class BdCliClient(BdClientBase):
|
||||
"""
|
||||
config = load_config()
|
||||
self.bd_path = bd_path if bd_path is not None else config.beads_path
|
||||
self.beads_dir = beads_dir if beads_dir is not None else config.beads_dir
|
||||
self.beads_db = beads_db if beads_db is not None else config.beads_db
|
||||
self.actor = actor if actor is not None else config.beads_actor
|
||||
self.no_auto_flush = no_auto_flush if no_auto_flush is not None else config.beads_no_auto_flush
|
||||
@@ -225,7 +229,12 @@ class BdCliClient(BdClientBase):
|
||||
# Log database routing for debugging
|
||||
import sys
|
||||
working_dir = self._get_working_dir()
|
||||
db_info = self.beads_db if self.beads_db else "auto-discover"
|
||||
if self.beads_dir:
|
||||
db_info = f"BEADS_DIR={self.beads_dir}"
|
||||
elif self.beads_db:
|
||||
db_info = f"BEADS_DB={self.beads_db} (deprecated)"
|
||||
else:
|
||||
db_info = "auto-discover"
|
||||
print(f"[beads-mcp] Running bd command: {' '.join(args)}", file=sys.stderr)
|
||||
print(f"[beads-mcp] Database: {db_info}", file=sys.stderr)
|
||||
print(f"[beads-mcp] Working dir: {working_dir}", file=sys.stderr)
|
||||
@@ -656,6 +665,7 @@ BdClient = BdCliClient
|
||||
def create_bd_client(
|
||||
prefer_daemon: bool = False,
|
||||
bd_path: Optional[str] = None,
|
||||
beads_dir: Optional[str] = None,
|
||||
beads_db: Optional[str] = None,
|
||||
actor: Optional[str] = None,
|
||||
no_auto_flush: Optional[bool] = None,
|
||||
@@ -667,7 +677,8 @@ def create_bd_client(
|
||||
Args:
|
||||
prefer_daemon: If True, attempt to use daemon client first, fall back to CLI
|
||||
bd_path: Path to bd executable (for CLI client)
|
||||
beads_db: Path to beads database (for CLI client)
|
||||
beads_dir: Path to .beads directory (for CLI client)
|
||||
beads_db: Path to beads database (deprecated, for CLI client)
|
||||
actor: Actor name for audit trail
|
||||
no_auto_flush: Disable auto-flush (CLI only)
|
||||
no_auto_import: Disable auto-import (CLI only)
|
||||
@@ -732,6 +743,7 @@ def create_bd_client(
|
||||
# Use CLI client
|
||||
return BdCliClient(
|
||||
bd_path=bd_path,
|
||||
beads_dir=beads_dir,
|
||||
beads_db=beads_db,
|
||||
actor=actor,
|
||||
no_auto_flush=no_auto_flush,
|
||||
|
||||
@@ -32,6 +32,7 @@ class Config(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="")
|
||||
|
||||
beads_path: str = Field(default_factory=_default_beads_path)
|
||||
beads_dir: str | None = None
|
||||
beads_db: str | None = None
|
||||
beads_actor: str | None = None
|
||||
beads_no_auto_flush: bool = False
|
||||
@@ -75,6 +76,35 @@ class Config(BaseSettings):
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("beads_dir")
|
||||
@classmethod
|
||||
def validate_beads_dir(cls, v: str | None) -> str | None:
|
||||
"""Validate BEADS_DIR points to an existing .beads directory.
|
||||
|
||||
Args:
|
||||
v: Path to .beads directory or None
|
||||
|
||||
Returns:
|
||||
Validated path or None
|
||||
|
||||
Raises:
|
||||
ValueError: If path is set but directory doesn't exist
|
||||
"""
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
path = Path(v)
|
||||
if not path.exists():
|
||||
raise ValueError(
|
||||
f"BEADS_DIR points to non-existent directory: {v}\n"
|
||||
+ "Please verify the .beads directory path is correct."
|
||||
)
|
||||
|
||||
if not path.is_dir():
|
||||
raise ValueError(f"BEADS_DIR must point to a directory, not a file: {v}")
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("beads_db")
|
||||
@classmethod
|
||||
def validate_beads_db(cls, v: str | None) -> str | None:
|
||||
@@ -129,7 +159,8 @@ def load_config() -> Config:
|
||||
+ "After installation, restart Claude Code.\n\n"
|
||||
+ "Advanced configuration (optional):\n"
|
||||
+ f" BEADS_PATH - Path to bd executable (default: {default_path})\n"
|
||||
+ " BEADS_DB - Path to beads database file (default: auto-discover)\n"
|
||||
+ " BEADS_DIR - Path to .beads directory (default: auto-discover)\n"
|
||||
+ " BEADS_DB - Path to database file (deprecated, use BEADS_DIR)\n"
|
||||
+ " BEADS_WORKING_DIR - Working directory for bd commands (default: $PWD or cwd)\n"
|
||||
+ " BEADS_ACTOR - Actor name for audit trail (default: $USER)\n"
|
||||
+ " BEADS_NO_AUTO_FLUSH - Disable automatic JSONL sync (default: false)\n"
|
||||
|
||||
Reference in New Issue
Block a user