Fix config system: rename config.json → metadata.json, fix config.yaml loading
- Renamed config.json to metadata.json to clarify purpose (database metadata) - Fixed config.yaml/config.json conflict by making Viper explicitly load only config.yaml - Added automatic migration from config.json to metadata.json on first read - Fixed jsonOutput variable shadowing across 22 command files - Updated bd init to create both metadata.json and config.yaml template - Fixed 5 failing JSON output tests - All tests passing Resolves config file confusion and makes config.yaml work correctly. Closes #178 (global flags), addresses config issues from #193 Amp-Thread-ID: https://ampcode.com/threads/T-e6ac8192-e18f-4ed7-83bc-4a5986718bb7 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
+19
-18
File diff suppressed because one or more lines are too long
@@ -1,87 +0,0 @@
|
|||||||
# Next Session: Agent-Supervised Migration Safety
|
|
||||||
|
|
||||||
## Context
|
|
||||||
We identified that database migrations can lose user data through edge cases (e.g., GH #201 where `bd migrate` failed to set `issue_prefix`, breaking commands). Since beads is designed for AI agents, we should leverage **agent supervision** to make migrations safer.
|
|
||||||
|
|
||||||
## Key Architectural Decision
|
|
||||||
**Beads provides observability primitives; agents supervise using their own reasoning.**
|
|
||||||
|
|
||||||
Beads does NOT:
|
|
||||||
- ❌ Make AI API calls
|
|
||||||
- ❌ Invoke external models
|
|
||||||
- ❌ Call agents
|
|
||||||
|
|
||||||
Beads DOES:
|
|
||||||
- ✅ Provide deterministic invariant checks
|
|
||||||
- ✅ Expose migration state via `--dry-run --json`
|
|
||||||
- ✅ Roll back on validation failures
|
|
||||||
- ✅ Give agents structured data to analyze
|
|
||||||
|
|
||||||
## The Work (bd-627d)
|
|
||||||
|
|
||||||
### Phase 1: Migration Invariants (Start here!)
|
|
||||||
Create `internal/storage/sqlite/migration_invariants.go` with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type MigrationInvariant struct {
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
Check func(*sql.DB, *Snapshot) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Snapshot struct {
|
|
||||||
IssueCount int
|
|
||||||
ConfigKeys []string
|
|
||||||
DependencyCount int
|
|
||||||
LabelCount int
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Implement these invariants:
|
|
||||||
1. **required_config_present** - Would have caught GH #201!
|
|
||||||
2. **foreign_keys_valid** - Detect orphaned dependencies
|
|
||||||
3. **issue_count_stable** - Catch unexpected data loss
|
|
||||||
|
|
||||||
### Phase 2: Inspection Tools
|
|
||||||
Add CLI commands for agents to inspect migrations:
|
|
||||||
|
|
||||||
1. `bd migrate --dry-run --json` - Shows what will change
|
|
||||||
2. `bd info --schema --json` - Current schema + detected prefix
|
|
||||||
3. Update `RunMigrations()` to check invariants and rollback on failure
|
|
||||||
|
|
||||||
### Phase 3 & 4: MCP Tools + Agent Workflows
|
|
||||||
Add MCP tools so agents can:
|
|
||||||
- Inspect migration plans before running
|
|
||||||
- Detect missing config (like `issue_prefix`)
|
|
||||||
- Auto-fix issues before migration
|
|
||||||
- Validate post-migration state
|
|
||||||
|
|
||||||
## Starting Prompt for Next Session
|
|
||||||
|
|
||||||
```
|
|
||||||
Let's implement Phase 1 of bd-627d (agent-supervised migration safety).
|
|
||||||
|
|
||||||
We need to create migration invariants that check for common data loss scenarios:
|
|
||||||
1. Missing required config keys (would have caught GH #201)
|
|
||||||
2. Foreign key integrity (no orphaned dependencies)
|
|
||||||
3. Issue count stability (detect unexpected deletions)
|
|
||||||
|
|
||||||
Start by creating internal/storage/sqlite/migration_invariants.go with the Snapshot type and invariant infrastructure. Then integrate it into RunMigrations() in migrations.go.
|
|
||||||
|
|
||||||
The goal: migrations should automatically roll back if invariants fail, preventing data loss.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related Issues
|
|
||||||
- bd-627d: Main epic for agent-supervised migrations
|
|
||||||
- GH #201: Real-world example of migration data loss (missing issue_prefix)
|
|
||||||
- bd-d355a07d: False positive data loss warnings
|
|
||||||
- bd-b245: Migration registry (just completed - makes migrations introspectable!)
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
After Phase 1, migrations should:
|
|
||||||
- ✅ Check invariants before committing
|
|
||||||
- ✅ Roll back on any invariant failure
|
|
||||||
- ✅ Provide clear error messages
|
|
||||||
- ✅ Have unit tests for each invariant
|
|
||||||
|
|
||||||
This prevents silent data loss like GH #201 where users discovered breakage only after migration completed.
|
|
||||||
+1
-1
@@ -64,7 +64,7 @@ var createCmd = &cobra.Command{
|
|||||||
externalRef, _ := cmd.Flags().GetString("external-ref")
|
externalRef, _ := cmd.Flags().GetString("external-ref")
|
||||||
deps, _ := cmd.Flags().GetStringSlice("deps")
|
deps, _ := cmd.Flags().GetStringSlice("deps")
|
||||||
forceCreate, _ := cmd.Flags().GetBool("force")
|
forceCreate, _ := cmd.Flags().GetBool("force")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Check for conflicting flags
|
// Check for conflicting flags
|
||||||
if explicitID != "" && parentID != "" {
|
if explicitID != "" && parentID != "" {
|
||||||
|
|||||||
+5
-5
@@ -36,7 +36,7 @@ var daemonsListCmd = &cobra.Command{
|
|||||||
uptime, last activity, and exclusive lock status.`,
|
uptime, last activity, and exclusive lock status.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
searchRoots, _ := cmd.Flags().GetStringSlice("search")
|
searchRoots, _ := cmd.Flags().GetStringSlice("search")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Discover daemons
|
// Discover daemons
|
||||||
daemons, err := daemon.DiscoverDaemons(searchRoots)
|
daemons, err := daemon.DiscoverDaemons(searchRoots)
|
||||||
@@ -139,7 +139,7 @@ Sends shutdown command via RPC, with SIGTERM fallback if RPC fails.`,
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
target := args[0]
|
target := args[0]
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Discover all daemons
|
// Discover all daemons
|
||||||
daemons, err := daemon.DiscoverDaemons(nil)
|
daemons, err := daemon.DiscoverDaemons(nil)
|
||||||
@@ -209,7 +209,7 @@ Supports tail mode (last N lines) and follow mode (like tail -f).`,
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
target := args[0]
|
target := args[0]
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
follow, _ := cmd.Flags().GetBool("follow")
|
follow, _ := cmd.Flags().GetBool("follow")
|
||||||
lines, _ := cmd.Flags().GetInt("lines")
|
lines, _ := cmd.Flags().GetInt("lines")
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ var daemonsKillallCmd = &cobra.Command{
|
|||||||
Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
|
Uses escalating shutdown strategy: RPC (2s) → SIGTERM (3s) → SIGKILL (1s).`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
searchRoots, _ := cmd.Flags().GetStringSlice("search")
|
searchRoots, _ := cmd.Flags().GetStringSlice("search")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
force, _ := cmd.Flags().GetBool("force")
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
// Discover all daemons
|
// Discover all daemons
|
||||||
@@ -411,7 +411,7 @@ var daemonsHealthCmd = &cobra.Command{
|
|||||||
stale sockets, version mismatches, and unresponsive daemons.`,
|
stale sockets, version mismatches, and unresponsive daemons.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
searchRoots, _ := cmd.Flags().GetStringSlice("search")
|
searchRoots, _ := cmd.Flags().GetStringSlice("search")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Discover daemons
|
// Discover daemons
|
||||||
daemons, err := daemon.DiscoverDaemons(searchRoots)
|
daemons, err := daemon.DiscoverDaemons(searchRoots)
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ Force: Delete and orphan dependents
|
|||||||
force, _ := cmd.Flags().GetBool("force")
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
cascade, _ := cmd.Flags().GetBool("cascade")
|
cascade, _ := cmd.Flags().GetBool("cascade")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Collect issue IDs from args and/or file
|
// Collect issue IDs from args and/or file
|
||||||
issueIDs := make([]string, 0, len(args))
|
issueIDs := make([]string, 0, len(args))
|
||||||
|
|||||||
+3
-4
@@ -63,8 +63,7 @@ Examples:
|
|||||||
bd doctor /path/to/repo # Check specific repository
|
bd doctor /path/to/repo # Check specific repository
|
||||||
bd doctor --json # Machine-readable output`,
|
bd doctor --json # Machine-readable output`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Get json flag from command
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
||||||
|
|
||||||
// Determine path to check
|
// Determine path to check
|
||||||
checkPath := "."
|
checkPath := "."
|
||||||
@@ -201,7 +200,7 @@ func checkInstallation(path string) doctorCheck {
|
|||||||
func checkDatabaseVersion(path string) doctorCheck {
|
func checkDatabaseVersion(path string) doctorCheck {
|
||||||
beadsDir := filepath.Join(path, ".beads")
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
// Check config.json first for custom database name
|
// Check metadata.json first for custom database name
|
||||||
var dbPath string
|
var dbPath string
|
||||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
dbPath = cfg.DatabasePath(beadsDir)
|
dbPath = cfg.DatabasePath(beadsDir)
|
||||||
@@ -275,7 +274,7 @@ func checkDatabaseVersion(path string) doctorCheck {
|
|||||||
func checkIDFormat(path string) doctorCheck {
|
func checkIDFormat(path string) doctorCheck {
|
||||||
beadsDir := filepath.Join(path, ".beads")
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
// Check config.json first for custom database name
|
// Check metadata.json first for custom database name
|
||||||
var dbPath string
|
var dbPath string
|
||||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
dbPath = cfg.DatabasePath(beadsDir)
|
dbPath = cfg.DatabasePath(beadsDir)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Example:
|
|||||||
|
|
||||||
autoMerge, _ := cmd.Flags().GetBool("auto-merge")
|
autoMerge, _ := cmd.Flags().GetBool("auto-merge")
|
||||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ var epicStatusCmd = &cobra.Command{
|
|||||||
Short: "Show epic completion status",
|
Short: "Show epic completion status",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
eligibleOnly, _ := cmd.Flags().GetBool("eligible-only")
|
eligibleOnly, _ := cmd.Flags().GetBool("eligible-only")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
var epics []*types.EpicStatus
|
var epics []*types.EpicStatus
|
||||||
var err error
|
var err error
|
||||||
@@ -115,7 +115,7 @@ var closeEligibleEpicsCmd = &cobra.Command{
|
|||||||
Short: "Close epics where all children are complete",
|
Short: "Close epics where all children are complete",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
var eligibleEpics []*types.EpicStatus
|
var eligibleEpics []*types.EpicStatus
|
||||||
|
|
||||||
|
|||||||
+76
-18
@@ -149,6 +149,7 @@ bd.db
|
|||||||
|
|
||||||
# Keep JSONL exports and config (source of truth for git)
|
# Keep JSONL exports and config (source of truth for git)
|
||||||
!*.jsonl
|
!*.jsonl
|
||||||
|
!metadata.json
|
||||||
!config.json
|
!config.json
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(gitignorePath, []byte(gitignoreContent), 0600); err != nil {
|
if err := os.WriteFile(gitignorePath, []byte(gitignoreContent), 0600); err != nil {
|
||||||
@@ -211,36 +212,93 @@ bd.db
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create config.json for explicit configuration
|
// Create metadata.json for database metadata
|
||||||
if useLocalBeads {
|
if useLocalBeads {
|
||||||
cfg := configfile.DefaultConfig(Version)
|
cfg := configfile.DefaultConfig(Version)
|
||||||
if err := cfg.Save(localBeadsDir); err != nil {
|
if err := cfg.Save(localBeadsDir); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to create config.json: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: failed to create metadata.json: %v\n", err)
|
||||||
|
// Non-fatal - continue anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create config.yaml template for user preferences
|
||||||
|
configYamlPath := filepath.Join(localBeadsDir, "config.yaml")
|
||||||
|
if _, err := os.Stat(configYamlPath); os.IsNotExist(err) {
|
||||||
|
configYamlTemplate := `# Beads Configuration File
|
||||||
|
# This file configures default behavior for all bd commands in this repository
|
||||||
|
# All settings can also be set via environment variables (BD_* prefix)
|
||||||
|
# or overridden with command-line flags
|
||||||
|
|
||||||
|
# Issue prefix for this repository (used by bd init)
|
||||||
|
# If not set, bd init will auto-detect from directory name
|
||||||
|
# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc.
|
||||||
|
# issue-prefix: ""
|
||||||
|
|
||||||
|
# Use no-db mode: load from JSONL, no SQLite, write back after each command
|
||||||
|
# When true, bd will use .beads/issues.jsonl as the source of truth
|
||||||
|
# instead of SQLite database
|
||||||
|
# no-db: false
|
||||||
|
|
||||||
|
# Disable daemon for RPC communication (forces direct database access)
|
||||||
|
# no-daemon: false
|
||||||
|
|
||||||
|
# Disable auto-flush of database to JSONL after mutations
|
||||||
|
# no-auto-flush: false
|
||||||
|
|
||||||
|
# Disable auto-import from JSONL when it's newer than database
|
||||||
|
# no-auto-import: false
|
||||||
|
|
||||||
|
# Enable JSON output by default
|
||||||
|
# json: false
|
||||||
|
|
||||||
|
# Default actor for audit trails (overridden by BD_ACTOR or --actor)
|
||||||
|
# actor: ""
|
||||||
|
|
||||||
|
# Path to database (overridden by BEADS_DB or --db)
|
||||||
|
# db: ""
|
||||||
|
|
||||||
|
# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)
|
||||||
|
# auto-start-daemon: true
|
||||||
|
|
||||||
|
# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)
|
||||||
|
# flush-debounce: "5s"
|
||||||
|
|
||||||
|
# Integration settings (access with 'bd config get/set')
|
||||||
|
# These are stored in the database, not in this file:
|
||||||
|
# - jira.url
|
||||||
|
# - jira.project
|
||||||
|
# - linear.url
|
||||||
|
# - linear.api-key
|
||||||
|
# - github.org
|
||||||
|
# - github.repo
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configYamlPath, []byte(configYamlTemplate), 0600); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to create config.yaml: %v\n", err)
|
||||||
// Non-fatal - continue anyway
|
// Non-fatal - continue anyway
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if git has existing issues to import (fresh clone scenario)
|
// Check if git has existing issues to import (fresh clone scenario)
|
||||||
issueCount, jsonlPath := checkGitForIssues()
|
issueCount, jsonlPath := checkGitForIssues()
|
||||||
if issueCount > 0 {
|
if issueCount > 0 {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
fmt.Fprintf(os.Stderr, "\n✓ Database initialized. Found %d issues in git, importing...\n", issueCount)
|
fmt.Fprintf(os.Stderr, "\n✓ Database initialized. Found %d issues in git, importing...\n", issueCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := importFromGit(ctx, initDBPath, store, jsonlPath); err != nil {
|
if err := importFromGit(ctx, initDBPath, store, jsonlPath); err != nil {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: auto-import failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: auto-import failed: %v\n", err)
|
||||||
fmt.Fprintf(os.Stderr, "Try manually: git show HEAD:%s | bd import -i /dev/stdin\n", jsonlPath)
|
fmt.Fprintf(os.Stderr, "Try manually: git show HEAD:%s | bd import -i /dev/stdin\n", jsonlPath)
|
||||||
}
|
}
|
||||||
// Non-fatal - continue with empty database
|
// Non-fatal - continue with empty database
|
||||||
} else if !quiet {
|
} else if !quiet {
|
||||||
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
|
fmt.Fprintf(os.Stderr, "✓ Successfully imported %d issues from git.\n\n", issueCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.Close(); err != nil {
|
if err := store.Close(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: failed to close database: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're in a git repo and hooks aren't installed
|
// Check if we're in a git repo and hooks aren't installed
|
||||||
// Do this BEFORE quiet mode return so hooks get installed for agents
|
// Do this BEFORE quiet mode return so hooks get installed for agents
|
||||||
|
|||||||
+4
-4
@@ -79,7 +79,7 @@ var labelAddCmd = &cobra.Command{
|
|||||||
Short: "Add a label to one or more issues",
|
Short: "Add a label to one or more issues",
|
||||||
Args: cobra.MinimumNArgs(2),
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
issueIDs, label := parseLabelArgs(args)
|
issueIDs, label := parseLabelArgs(args)
|
||||||
|
|
||||||
// Resolve partial IDs
|
// Resolve partial IDs
|
||||||
@@ -125,7 +125,7 @@ var labelRemoveCmd = &cobra.Command{
|
|||||||
Short: "Remove a label from one or more issues",
|
Short: "Remove a label from one or more issues",
|
||||||
Args: cobra.MinimumNArgs(2),
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
issueIDs, label := parseLabelArgs(args)
|
issueIDs, label := parseLabelArgs(args)
|
||||||
|
|
||||||
// Resolve partial IDs
|
// Resolve partial IDs
|
||||||
@@ -170,7 +170,7 @@ var labelListCmd = &cobra.Command{
|
|||||||
Short: "List labels for an issue",
|
Short: "List labels for an issue",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial ID first
|
// Resolve partial ID first
|
||||||
@@ -245,7 +245,7 @@ var labelListAllCmd = &cobra.Command{
|
|||||||
Use: "list-all",
|
Use: "list-all",
|
||||||
Short: "List all unique labels in the database",
|
Short: "List all unique labels in the database",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
var issues []*types.Issue
|
var issues []*types.Issue
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ var listCmd = &cobra.Command{
|
|||||||
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
|
labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
|
||||||
titleSearch, _ := cmd.Flags().GetString("title")
|
titleSearch, _ := cmd.Flags().GetString("title")
|
||||||
idFilter, _ := cmd.Flags().GetString("id")
|
idFilter, _ := cmd.Flags().GetString("id")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Normalize labels: trim, dedupe, remove empty
|
// Normalize labels: trim, dedupe, remove empty
|
||||||
labels = normalizeLabels(labels)
|
labels = normalizeLabels(labels)
|
||||||
|
|||||||
@@ -78,6 +78,23 @@ var (
|
|||||||
noDb bool // Use --no-db mode: load from JSONL, write back after each command
|
noDb bool // Use --no-db mode: load from JSONL, write back after each command
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Initialize viper configuration
|
||||||
|
if err := config.Initialize(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to initialize config: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register persistent flags
|
||||||
|
rootCmd.PersistentFlags().StringVar(&dbPath, "db", "", "Database path (default: auto-discover .beads/*.db)")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&actor, "actor", "", "Actor name for audit trail (default: $BD_ACTOR or $USER)")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "Output in JSON format")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&noDaemon, "no-daemon", false, "Force direct storage mode, bypass daemon if running")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&noAutoFlush, "no-auto-flush", false, "Disable automatic JSONL sync after CRUD operations")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&noAutoImport, "no-auto-import", false, "Disable automatic JSONL import when newer than DB")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&sandboxMode, "sandbox", false, "Sandbox mode: disables daemon and auto-sync")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&noDb, "no-db", false, "Use no-db mode: load from JSONL, no SQLite")
|
||||||
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "bd",
|
Use: "bd",
|
||||||
Short: "bd - Dependency-aware issue tracker",
|
Short: "bd - Dependency-aware issue tracker",
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ Example:
|
|||||||
|
|
||||||
sourceIDs := args
|
sourceIDs := args
|
||||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Validate merge operation
|
// Validate merge operation
|
||||||
if err := validateMerge(targetID, sourceIDs); err != nil {
|
if err := validateMerge(targetID, sourceIDs); err != nil {
|
||||||
|
|||||||
+3
-3
@@ -100,7 +100,7 @@ This command:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if target database exists and is current (use config.json name)
|
// Check if target database exists and is current (use metadata.json name)
|
||||||
targetPath := cfg.DatabasePath(beadsDir)
|
targetPath := cfg.DatabasePath(beadsDir)
|
||||||
var currentDB *dbInfo
|
var currentDB *dbInfo
|
||||||
var oldDBs []*dbInfo
|
var oldDBs []*dbInfo
|
||||||
@@ -444,7 +444,7 @@ This command:
|
|||||||
if !dryRun {
|
if !dryRun {
|
||||||
if err := cfg.Save(beadsDir); err != nil {
|
if err := cfg.Save(beadsDir); err != nil {
|
||||||
if !jsonOutput {
|
if !jsonOutput {
|
||||||
color.Yellow("Warning: failed to update config.json version: %v\n", err)
|
color.Yellow("Warning: failed to update metadata.json version: %v\n", err)
|
||||||
}
|
}
|
||||||
// Don't fail migration if config save fails
|
// Don't fail migration if config save fails
|
||||||
}
|
}
|
||||||
@@ -667,7 +667,7 @@ func handleUpdateRepoID(dryRun bool, autoYes bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadOrCreateConfig loads config.json or creates default if not found
|
// loadOrCreateConfig loads metadata.json or creates default if not found
|
||||||
func loadOrCreateConfig(beadsDir string) (*configfile.Config, error) {
|
func loadOrCreateConfig(beadsDir string) (*configfile.Config, error) {
|
||||||
cfg, err := configfile.Load(beadsDir)
|
cfg, err := configfile.Load(beadsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -127,18 +127,18 @@ func TestFormatDBList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMigrateRespectsConfigJSON(t *testing.T) {
|
func TestMigrateRespectsConfigJSON(t *testing.T) {
|
||||||
// Test that migrate respects custom database name from config.json
|
// Test that migrate respects custom database name from metadata.json
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||||
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
||||||
t.Fatalf("Failed to create .beads directory: %v", err)
|
t.Fatalf("Failed to create .beads directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create config.json with custom database name
|
// Create metadata.json with custom database name
|
||||||
configPath := filepath.Join(beadsDir, "config.json")
|
configPath := filepath.Join(beadsDir, "metadata.json")
|
||||||
configData := `{"database": "beady.db", "version": "0.21.1", "jsonl_export": "beady.jsonl"}`
|
configData := `{"database": "beady.db", "version": "0.21.1", "jsonl_export": "beady.jsonl"}`
|
||||||
if err := os.WriteFile(configPath, []byte(configData), 0600); err != nil {
|
if err := os.WriteFile(configPath, []byte(configData), 0600); err != nil {
|
||||||
t.Fatalf("Failed to create config.json: %v", err)
|
t.Fatalf("Failed to create metadata.json: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create old database with custom name
|
// Create old database with custom name
|
||||||
|
|||||||
+3
-3
@@ -20,7 +20,7 @@ var readyCmd = &cobra.Command{
|
|||||||
limit, _ := cmd.Flags().GetInt("limit")
|
limit, _ := cmd.Flags().GetInt("limit")
|
||||||
assignee, _ := cmd.Flags().GetString("assignee")
|
assignee, _ := cmd.Flags().GetString("assignee")
|
||||||
sortPolicy, _ := cmd.Flags().GetString("sort")
|
sortPolicy, _ := cmd.Flags().GetString("sort")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
||||||
|
|
||||||
filter := types.WorkFilter{
|
filter := types.WorkFilter{
|
||||||
// Leave Status empty to get both 'open' and 'in_progress' (bd-165)
|
// Leave Status empty to get both 'open' and 'in_progress' (bd-165)
|
||||||
@@ -153,7 +153,7 @@ var blockedCmd = &cobra.Command{
|
|||||||
Use: "blocked",
|
Use: "blocked",
|
||||||
Short: "Show blocked issues",
|
Short: "Show blocked issues",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
||||||
|
|
||||||
// If daemon is running but doesn't support this command, use direct storage
|
// If daemon is running but doesn't support this command, use direct storage
|
||||||
if daemonClient != nil && store == nil {
|
if daemonClient != nil && store == nil {
|
||||||
@@ -208,7 +208,7 @@ var statsCmd = &cobra.Command{
|
|||||||
Use: "stats",
|
Use: "stats",
|
||||||
Short: "Show statistics",
|
Short: "Show statistics",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun (respects config.yaml + env vars)
|
||||||
|
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
|
|||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
reason, _ := cmd.Flags().GetString("reason")
|
reason, _ := cmd.Flags().GetString("reason")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -21,7 +21,7 @@ var showCmd = &cobra.Command{
|
|||||||
Short: "Show issue details",
|
Short: "Show issue details",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial IDs first
|
// Resolve partial IDs first
|
||||||
@@ -346,7 +346,7 @@ var updateCmd = &cobra.Command{
|
|||||||
Short: "Update one or more issues",
|
Short: "Update one or more issues",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]interface{})
|
||||||
|
|
||||||
if cmd.Flags().Changed("status") {
|
if cmd.Flags().Changed("status") {
|
||||||
@@ -710,7 +710,7 @@ var closeCmd = &cobra.Command{
|
|||||||
if reason == "" {
|
if reason == "" {
|
||||||
reason = "Closed"
|
reason = "Closed"
|
||||||
}
|
}
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ func TestShowCommand(t *testing.T) {
|
|||||||
os.Stdout = w
|
os.Stdout = w
|
||||||
|
|
||||||
// Reset command state
|
// Reset command state
|
||||||
|
jsonOutput = true
|
||||||
|
defer func() { jsonOutput = false }()
|
||||||
rootCmd.SetArgs([]string{"show", issue1.ID, "--json"})
|
rootCmd.SetArgs([]string{"show", issue1.ID, "--json"})
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
err := rootCmd.Execute()
|
||||||
@@ -171,6 +173,8 @@ func TestShowCommand(t *testing.T) {
|
|||||||
os.Stdout = w
|
os.Stdout = w
|
||||||
|
|
||||||
// Reset command state
|
// Reset command state
|
||||||
|
jsonOutput = true
|
||||||
|
defer func() { jsonOutput = false }()
|
||||||
rootCmd.SetArgs([]string{"show", issue1.ID, issue2.ID, "--json"})
|
rootCmd.SetArgs([]string{"show", issue1.ID, issue2.ID, "--json"})
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
err := rootCmd.Execute()
|
||||||
@@ -204,6 +208,8 @@ func TestShowCommand(t *testing.T) {
|
|||||||
os.Stdout = w
|
os.Stdout = w
|
||||||
|
|
||||||
// Reset command state
|
// Reset command state
|
||||||
|
jsonOutput = true
|
||||||
|
defer func() { jsonOutput = false }()
|
||||||
rootCmd.SetArgs([]string{"show", issue2.ID, "--json"})
|
rootCmd.SetArgs([]string{"show", issue2.ID, "--json"})
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
err := rootCmd.Execute()
|
||||||
@@ -490,6 +496,8 @@ func TestUpdateCommand(t *testing.T) {
|
|||||||
os.Stdout = w
|
os.Stdout = w
|
||||||
|
|
||||||
// Reset command state
|
// Reset command state
|
||||||
|
jsonOutput = true
|
||||||
|
defer func() { jsonOutput = false }()
|
||||||
rootCmd.SetArgs([]string{"update", issue.ID, "--priority", "3", "--json"})
|
rootCmd.SetArgs([]string{"update", issue.ID, "--priority", "3", "--json"})
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
err := rootCmd.Execute()
|
||||||
@@ -779,6 +787,8 @@ func TestCloseCommand(t *testing.T) {
|
|||||||
os.Stdout = w
|
os.Stdout = w
|
||||||
|
|
||||||
// Reset command state
|
// Reset command state
|
||||||
|
jsonOutput = true
|
||||||
|
defer func() { jsonOutput = false }()
|
||||||
rootCmd.SetArgs([]string{"close", issue.ID, "--reason", "Fixed", "--json"})
|
rootCmd.SetArgs([]string{"close", issue.ID, "--reason", "Fixed", "--json"})
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
err := rootCmd.Execute()
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ This helps identify:
|
|||||||
days, _ := cmd.Flags().GetInt("days")
|
days, _ := cmd.Flags().GetInt("days")
|
||||||
status, _ := cmd.Flags().GetString("status")
|
status, _ := cmd.Flags().GetString("status")
|
||||||
limit, _ := cmd.Flags().GetInt("limit")
|
limit, _ := cmd.Flags().GetInt("limit")
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
// Use global jsonOutput set by PersistentPreRun
|
||||||
|
|
||||||
// Validate status if provided
|
// Validate status if provided
|
||||||
if status != "" && status != "open" && status != "in_progress" && status != "blocked" {
|
if status != "" && status != "open" && status != "in_progress" && status != "blocked" {
|
||||||
|
|||||||
Vendored
+2
-1
@@ -2,7 +2,7 @@
|
|||||||
bd init --prefix test
|
bd init --prefix test
|
||||||
stdout 'initialized successfully'
|
stdout 'initialized successfully'
|
||||||
exists .beads/beads.db
|
exists .beads/beads.db
|
||||||
exists .beads/config.json
|
exists .beads/metadata.json
|
||||||
exists .beads/.gitignore
|
exists .beads/.gitignore
|
||||||
grep '^\*\.db$' .beads/.gitignore
|
grep '^\*\.db$' .beads/.gitignore
|
||||||
grep '^\*\.db-journal$' .beads/.gitignore
|
grep '^\*\.db-journal$' .beads/.gitignore
|
||||||
@@ -12,4 +12,5 @@ grep '^daemon\.log$' .beads/.gitignore
|
|||||||
grep '^daemon\.pid$' .beads/.gitignore
|
grep '^daemon\.pid$' .beads/.gitignore
|
||||||
grep '^bd\.sock$' .beads/.gitignore
|
grep '^bd\.sock$' .beads/.gitignore
|
||||||
grep '^!\*\.jsonl$' .beads/.gitignore
|
grep '^!\*\.jsonl$' .beads/.gitignore
|
||||||
|
grep '^!metadata\.json$' .beads/.gitignore
|
||||||
grep '^!config\.json$' .beads/.gitignore
|
grep '^!config\.json$' .beads/.gitignore
|
||||||
|
|||||||
+39
-26
@@ -17,43 +17,50 @@ var v *viper.Viper
|
|||||||
func Initialize() error {
|
func Initialize() error {
|
||||||
v = viper.New()
|
v = viper.New()
|
||||||
|
|
||||||
// Set config file name and type
|
// Set config type to yaml (we only load config.yaml, not config.json)
|
||||||
v.SetConfigName("config")
|
|
||||||
v.SetConfigType("yaml")
|
v.SetConfigType("yaml")
|
||||||
|
|
||||||
// Add config search paths (in order of precedence)
|
// Explicitly locate config.yaml and use SetConfigFile to avoid picking up config.json
|
||||||
// 1. Walk up from CWD to find project .beads/ directory
|
// Precedence: project .beads/config.yaml > ~/.config/bd/config.yaml > ~/.beads/config.yaml
|
||||||
|
configFileSet := false
|
||||||
|
|
||||||
|
// 1. Walk up from CWD to find project .beads/config.yaml
|
||||||
// This allows commands to work from subdirectories
|
// This allows commands to work from subdirectories
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err == nil {
|
if err == nil && !configFileSet {
|
||||||
// Walk up parent directories to find .beads/config.yaml
|
// Walk up parent directories to find .beads/config.yaml
|
||||||
for dir := cwd; dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
|
for dir := cwd; dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
|
||||||
beadsDir := filepath.Join(dir, ".beads")
|
beadsDir := filepath.Join(dir, ".beads")
|
||||||
configPath := filepath.Join(beadsDir, "config.yaml")
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
// Found .beads/config.yaml - add this path
|
// Found .beads/config.yaml - set it explicitly
|
||||||
v.AddConfigPath(beadsDir)
|
v.SetConfigFile(configPath)
|
||||||
break
|
configFileSet = true
|
||||||
}
|
|
||||||
// Also check if .beads directory exists (even without config.yaml)
|
|
||||||
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
|
||||||
v.AddConfigPath(beadsDir)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also add CWD/.beads for backward compatibility
|
|
||||||
v.AddConfigPath(filepath.Join(cwd, ".beads"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. User config directory (~/.config/bd/)
|
// 2. User config directory (~/.config/bd/config.yaml)
|
||||||
if configDir, err := os.UserConfigDir(); err == nil {
|
if !configFileSet {
|
||||||
v.AddConfigPath(filepath.Join(configDir, "bd"))
|
if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
configPath := filepath.Join(configDir, "bd", "config.yaml")
|
||||||
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
v.SetConfigFile(configPath)
|
||||||
|
configFileSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Home directory (~/.beads/)
|
// 3. Home directory (~/.beads/config.yaml)
|
||||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
if !configFileSet {
|
||||||
v.AddConfigPath(filepath.Join(homeDir, ".beads"))
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||||
|
configPath := filepath.Join(homeDir, ".beads", "config.yaml")
|
||||||
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
v.SetConfigFile(configPath)
|
||||||
|
configFileSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatic environment variable binding
|
// Automatic environment variable binding
|
||||||
@@ -85,13 +92,19 @@ func Initialize() error {
|
|||||||
v.SetDefault("flush-debounce", "30s")
|
v.SetDefault("flush-debounce", "30s")
|
||||||
v.SetDefault("auto-start-daemon", true)
|
v.SetDefault("auto-start-daemon", true)
|
||||||
|
|
||||||
// Read config file if it exists (don't error if not found)
|
// Read config file if it was found
|
||||||
if err := v.ReadInConfig(); err != nil {
|
if configFileSet {
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
if err := v.ReadInConfig(); err != nil {
|
||||||
// Config file found but another error occurred
|
|
||||||
return fmt.Errorf("error reading config file: %w", err)
|
return fmt.Errorf("error reading config file: %w", err)
|
||||||
}
|
}
|
||||||
// Config file not found - this is ok, we'll use defaults
|
if os.Getenv("BD_DEBUG") != "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: loaded config from %s\n", v.ConfigFileUsed())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No config.yaml found - use defaults and environment variables
|
||||||
|
if os.Getenv("BD_DEBUG") != "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Debug: no config.yaml found; using defaults and environment variables\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ConfigFileName = "config.json"
|
const ConfigFileName = "metadata.json"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Database string `json:"database"`
|
Database string `json:"database"`
|
||||||
@@ -32,7 +32,31 @@ func Load(beadsDir string) (*Config, error) {
|
|||||||
|
|
||||||
data, err := os.ReadFile(configPath) // #nosec G304 - controlled path from config
|
data, err := os.ReadFile(configPath) // #nosec G304 - controlled path from config
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, nil
|
// Try legacy config.json location (migration path)
|
||||||
|
legacyPath := filepath.Join(beadsDir, "config.json")
|
||||||
|
data, err = os.ReadFile(legacyPath) // #nosec G304 - controlled path from config
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading legacy config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate: parse legacy config, save as metadata.json, remove old file
|
||||||
|
var cfg Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing legacy config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to new location
|
||||||
|
if err := cfg.Save(beadsDir); err != nil {
|
||||||
|
return nil, fmt.Errorf("migrating config to metadata.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove legacy file (best effort)
|
||||||
|
_ = os.Remove(legacyPath)
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("reading config: %w", err)
|
return nil, fmt.Errorf("reading config: %w", err)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func TestJSONLPath(t *testing.T) {
|
|||||||
func TestConfigPath(t *testing.T) {
|
func TestConfigPath(t *testing.T) {
|
||||||
beadsDir := "/home/user/project/.beads"
|
beadsDir := "/home/user/project/.beads"
|
||||||
got := ConfigPath(beadsDir)
|
got := ConfigPath(beadsDir)
|
||||||
want := filepath.Join(beadsDir, "config.json")
|
want := filepath.Join(beadsDir, "metadata.json")
|
||||||
|
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("ConfigPath() = %q, want %q", got, want)
|
t.Errorf("ConfigPath() = %q, want %q", got, want)
|
||||||
|
|||||||
Reference in New Issue
Block a user