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 file name and type v.SetConfigName("config") v.SetConfigType("yaml") // Add config search paths (in order of precedence) // 1. Walk up from CWD to find project .beads/ directory // This allows commands to work from subdirectories cwd, err := os.Getwd() if err == nil { // 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 - add this path v.AddConfigPath(beadsDir) break } // Also check if .beads directory exists (even without config.yaml) if info, err := os.Stat(beadsDir); err == nil && info.IsDir() { v.AddConfigPath(beadsDir) break } } // Also add CWD/.beads for backward compatibility v.AddConfigPath(filepath.Join(cwd, ".beads")) } // 2. User config directory (~/.config/bd/) if configDir, err := os.UserConfigDir(); err == nil { v.AddConfigPath(filepath.Join(configDir, "bd")) } // 3. Home directory (~/.beads/) if homeDir, err := os.UserHomeDir(); err == nil { v.AddConfigPath(filepath.Join(homeDir, ".beads")) } // 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 exists (don't error if not found) if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { // Config file found but another error occurred return fmt.Errorf("error reading config file: %w", err) } // Config file not found - this is ok, we'll use defaults } 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() }