Files
beads/internal/config/config.go
Steve Yegge 0215e73780 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>
2025-11-02 14:31:22 -08:00

170 lines
4.5 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/viper"
)
var v *viper.Viper
// Initialize sets up the viper configuration singleton
// Should be called once at application startup
func Initialize() error {
v = viper.New()
// Set config type to yaml (we only load config.yaml, not config.json)
v.SetConfigType("yaml")
// Explicitly locate config.yaml and use SetConfigFile to avoid picking up config.json
// 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
cwd, err := os.Getwd()
if err == nil && !configFileSet {
// Walk up parent directories to find .beads/config.yaml
for dir := cwd; dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
beadsDir := filepath.Join(dir, ".beads")
configPath := filepath.Join(beadsDir, "config.yaml")
if _, err := os.Stat(configPath); err == nil {
// Found .beads/config.yaml - set it explicitly
v.SetConfigFile(configPath)
configFileSet = true
break
}
}
}
// 2. User config directory (~/.config/bd/config.yaml)
if !configFileSet {
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/config.yaml)
if !configFileSet {
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
// Environment variables take precedence over config file
// E.g., BD_JSON, BD_NO_DAEMON, BD_ACTOR, BD_DB
v.SetEnvPrefix("BD")
// Replace hyphens and dots with underscores for env var mapping
// This allows BD_NO_DAEMON to map to "no-daemon" config key
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
v.AutomaticEnv()
// Set defaults for all flags
v.SetDefault("json", false)
v.SetDefault("no-daemon", false)
v.SetDefault("no-auto-flush", false)
v.SetDefault("no-auto-import", false)
v.SetDefault("no-db", false)
v.SetDefault("db", "")
v.SetDefault("actor", "")
v.SetDefault("issue-prefix", "")
// Additional environment variables (not prefixed with BD_)
// These are bound explicitly for backward compatibility
_ = v.BindEnv("flush-debounce", "BEADS_FLUSH_DEBOUNCE")
_ = v.BindEnv("auto-start-daemon", "BEADS_AUTO_START_DAEMON")
// Set defaults for additional settings
v.SetDefault("flush-debounce", "30s")
v.SetDefault("auto-start-daemon", true)
// Read config file if it was found
if configFileSet {
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("error reading config file: %w", err)
}
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
}
// GetString retrieves a string configuration value
func GetString(key string) string {
if v == nil {
return ""
}
return v.GetString(key)
}
// GetBool retrieves a boolean configuration value
func GetBool(key string) bool {
if v == nil {
return false
}
return v.GetBool(key)
}
// GetInt retrieves an integer configuration value
func GetInt(key string) int {
if v == nil {
return 0
}
return v.GetInt(key)
}
// GetDuration retrieves a duration configuration value
func GetDuration(key string) time.Duration {
if v == nil {
return 0
}
return v.GetDuration(key)
}
// Set sets a configuration value
func Set(key string, value interface{}) {
if v != nil {
v.Set(key, value)
}
}
// BindPFlag is reserved for future use if we want to bind Cobra flags directly to Viper
// For now, we handle flag precedence manually in PersistentPreRun
// Uncomment and implement if needed:
//
// func BindPFlag(key string, flag *pflag.Flag) error {
// if v == nil {
// return fmt.Errorf("viper not initialized")
// }
// return v.BindPFlag(key, flag)
// }
// AllSettings returns all configuration settings as a map
func AllSettings() map[string]interface{} {
if v == nil {
return map[string]interface{}{}
}
return v.AllSettings()
}