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):
|
1. **If you have nested projects** (intentional):
|
||||||
- This is fine! bd is designed to support this
|
- This is fine! bd is designed to support this
|
||||||
- Just be aware which database you're using
|
- 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):
|
2. **If you have accidental duplicates** (unintentional):
|
||||||
- Decide which database to keep
|
- 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**:
|
3. **Override database selection**:
|
||||||
```bash
|
```bash
|
||||||
# Temporarily use specific database
|
# Temporarily use specific .beads directory (recommended)
|
||||||
BEADS_DB=/path/to/.beads/issues.db bd list
|
BEADS_DIR=/path/to/.beads bd list
|
||||||
|
|
||||||
# Or add to shell config for permanent override
|
# 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
|
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:
|
// FindDatabasePath discovers the bd database path using bd's standard search order:
|
||||||
// 1. $BEADS_DB environment variable
|
// 1. $BEADS_DIR environment variable (points to .beads directory)
|
||||||
// 2. .beads/*.db in current directory or ancestors
|
// 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.
|
// Returns empty string if no database is found.
|
||||||
func FindDatabasePath() string {
|
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 != "" {
|
if envDB := os.Getenv("BEADS_DB"); envDB != "" {
|
||||||
// Canonicalize the path to prevent nested .beads directories
|
// Canonicalize the path to prevent nested .beads directories
|
||||||
if absDB, err := filepath.Abs(envDB); err == nil {
|
if absDB, err := filepath.Abs(envDB); err == nil {
|
||||||
@@ -135,7 +184,7 @@ func FindDatabasePath() string {
|
|||||||
return envDB // Fallback to original if Abs fails
|
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 != "" {
|
if foundDB := findDatabaseInTree(); foundDB != "" {
|
||||||
// Canonicalize found path
|
// Canonicalize found path
|
||||||
if absDB, err := filepath.Abs(foundDB); err == nil {
|
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
|
// 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, "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, "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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,13 +439,29 @@ var rootCmd = &cobra.Command{
|
|||||||
// Handle --no-db mode: write memory storage back to JSONL
|
// Handle --no-db mode: write memory storage back to JSONL
|
||||||
if noDb {
|
if noDb {
|
||||||
if store != nil {
|
if store != nil {
|
||||||
cwd, err := os.Getwd()
|
// Determine beads directory (respect BEADS_DIR)
|
||||||
if err != nil {
|
var beadsDir string
|
||||||
fmt.Fprintf(os.Stderr, "Error: failed to get current directory: %v\n", err)
|
if envDir := os.Getenv("BEADS_DIR"); envDir != "" {
|
||||||
os.Exit(1)
|
// 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 memStore, ok := store.(*memory.MemoryStorage); ok {
|
||||||
if err := writeIssuesToJSONL(memStore, beadsDir); err != nil {
|
if err := writeIssuesToJSONL(memStore, beadsDir); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: failed to write JSONL: %v\n", err)
|
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
|
// This is called when --no-db flag is set
|
||||||
func initializeNoDbMode() error {
|
func initializeNoDbMode() error {
|
||||||
// Find .beads directory
|
// Find .beads directory
|
||||||
cwd, err := os.Getwd()
|
var beadsDir string
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get current directory: %w", err)
|
// 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) {
|
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")
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||||
|
|||||||
@@ -371,13 +371,13 @@ Use when parsing programmatically or extracting specific fields.
|
|||||||
bd finds database in this order:
|
bd finds database in this order:
|
||||||
|
|
||||||
1. `--db` flag: `bd ready --db /path/to/db.db`
|
1. `--db` flag: `bd ready --db /path/to/db.db`
|
||||||
2. `$BEADS_DB` environment variable
|
2. `$BEADS_DIR` environment variable (points to .beads directory)
|
||||||
3. `.beads/*.db` in current directory or ancestors
|
3. `$BEADS_DB` environment variable (deprecated, points to database file)
|
||||||
4. `~/.beads/default.db` as fallback
|
4. `.beads/*.db` in current directory or ancestors
|
||||||
|
|
||||||
**Project-local** (`.beads/`): Project-specific work, git-tracked
|
**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."""
|
"""Client for calling bd CLI commands and parsing JSON output."""
|
||||||
|
|
||||||
bd_path: str
|
bd_path: str
|
||||||
|
beads_dir: str | None
|
||||||
beads_db: str | None
|
beads_db: str | None
|
||||||
actor: str | None
|
actor: str | None
|
||||||
no_auto_flush: bool
|
no_auto_flush: bool
|
||||||
@@ -153,6 +154,7 @@ class BdCliClient(BdClientBase):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
bd_path: str | None = None,
|
bd_path: str | None = None,
|
||||||
|
beads_dir: str | None = None,
|
||||||
beads_db: str | None = None,
|
beads_db: str | None = None,
|
||||||
actor: str | None = None,
|
actor: str | None = None,
|
||||||
no_auto_flush: bool | None = None,
|
no_auto_flush: bool | None = None,
|
||||||
@@ -163,7 +165,8 @@ class BdCliClient(BdClientBase):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
bd_path: Path to bd executable (optional, loads from config if not provided)
|
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)
|
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_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)
|
no_auto_import: Disable automatic JSONL import (optional, loads from config if not provided)
|
||||||
@@ -171,6 +174,7 @@ class BdCliClient(BdClientBase):
|
|||||||
"""
|
"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
self.bd_path = bd_path if bd_path is not None else config.beads_path
|
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.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.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
|
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
|
# Log database routing for debugging
|
||||||
import sys
|
import sys
|
||||||
working_dir = self._get_working_dir()
|
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] Running bd command: {' '.join(args)}", file=sys.stderr)
|
||||||
print(f"[beads-mcp] Database: {db_info}", file=sys.stderr)
|
print(f"[beads-mcp] Database: {db_info}", file=sys.stderr)
|
||||||
print(f"[beads-mcp] Working dir: {working_dir}", file=sys.stderr)
|
print(f"[beads-mcp] Working dir: {working_dir}", file=sys.stderr)
|
||||||
@@ -656,6 +665,7 @@ BdClient = BdCliClient
|
|||||||
def create_bd_client(
|
def create_bd_client(
|
||||||
prefer_daemon: bool = False,
|
prefer_daemon: bool = False,
|
||||||
bd_path: Optional[str] = None,
|
bd_path: Optional[str] = None,
|
||||||
|
beads_dir: Optional[str] = None,
|
||||||
beads_db: Optional[str] = None,
|
beads_db: Optional[str] = None,
|
||||||
actor: Optional[str] = None,
|
actor: Optional[str] = None,
|
||||||
no_auto_flush: Optional[bool] = None,
|
no_auto_flush: Optional[bool] = None,
|
||||||
@@ -667,7 +677,8 @@ def create_bd_client(
|
|||||||
Args:
|
Args:
|
||||||
prefer_daemon: If True, attempt to use daemon client first, fall back to CLI
|
prefer_daemon: If True, attempt to use daemon client first, fall back to CLI
|
||||||
bd_path: Path to bd executable (for CLI client)
|
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
|
actor: Actor name for audit trail
|
||||||
no_auto_flush: Disable auto-flush (CLI only)
|
no_auto_flush: Disable auto-flush (CLI only)
|
||||||
no_auto_import: Disable auto-import (CLI only)
|
no_auto_import: Disable auto-import (CLI only)
|
||||||
@@ -732,6 +743,7 @@ def create_bd_client(
|
|||||||
# Use CLI client
|
# Use CLI client
|
||||||
return BdCliClient(
|
return BdCliClient(
|
||||||
bd_path=bd_path,
|
bd_path=bd_path,
|
||||||
|
beads_dir=beads_dir,
|
||||||
beads_db=beads_db,
|
beads_db=beads_db,
|
||||||
actor=actor,
|
actor=actor,
|
||||||
no_auto_flush=no_auto_flush,
|
no_auto_flush=no_auto_flush,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Config(BaseSettings):
|
|||||||
model_config = SettingsConfigDict(env_prefix="")
|
model_config = SettingsConfigDict(env_prefix="")
|
||||||
|
|
||||||
beads_path: str = Field(default_factory=_default_beads_path)
|
beads_path: str = Field(default_factory=_default_beads_path)
|
||||||
|
beads_dir: str | None = None
|
||||||
beads_db: str | None = None
|
beads_db: str | None = None
|
||||||
beads_actor: str | None = None
|
beads_actor: str | None = None
|
||||||
beads_no_auto_flush: bool = False
|
beads_no_auto_flush: bool = False
|
||||||
@@ -75,6 +76,35 @@ class Config(BaseSettings):
|
|||||||
|
|
||||||
return v
|
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")
|
@field_validator("beads_db")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_beads_db(cls, v: str | None) -> str | None:
|
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"
|
+ "After installation, restart Claude Code.\n\n"
|
||||||
+ "Advanced configuration (optional):\n"
|
+ "Advanced configuration (optional):\n"
|
||||||
+ f" BEADS_PATH - Path to bd executable (default: {default_path})\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_WORKING_DIR - Working directory for bd commands (default: $PWD or cwd)\n"
|
||||||
+ " BEADS_ACTOR - Actor name for audit trail (default: $USER)\n"
|
+ " BEADS_ACTOR - Actor name for audit trail (default: $USER)\n"
|
||||||
+ " BEADS_NO_AUTO_FLUSH - Disable automatic JSONL sync (default: false)\n"
|
+ " BEADS_NO_AUTO_FLUSH - Disable automatic JSONL sync (default: false)\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user