For bd-307: Multi-repo hydration layer Changes: - Add MultiRepoConfig to internal/config - Add GetMultiRepoConfig() to retrieve repos.primary and repos.additional - Add source_repo field to Issue type to track ownership - Prepare for hydration logic that reads from N repos
203 lines
5.3 KiB
Go
203 lines
5.3 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()
|
|
}
|
|
|
|
// GetStringSlice retrieves a string slice configuration value
|
|
func GetStringSlice(key string) []string {
|
|
if v == nil {
|
|
return []string{}
|
|
}
|
|
return v.GetStringSlice(key)
|
|
}
|
|
|
|
// MultiRepoConfig contains configuration for multi-repo support
|
|
type MultiRepoConfig struct {
|
|
Primary string // Primary repo path (where canonical issues live)
|
|
Additional []string // Additional repos to hydrate from
|
|
}
|
|
|
|
// GetMultiRepoConfig retrieves multi-repo configuration
|
|
// Returns nil if multi-repo is not configured (single-repo mode)
|
|
func GetMultiRepoConfig() *MultiRepoConfig {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
// Check if repos.primary is set (indicates multi-repo mode)
|
|
primary := v.GetString("repos.primary")
|
|
if primary == "" {
|
|
return nil // Single-repo mode
|
|
}
|
|
|
|
return &MultiRepoConfig{
|
|
Primary: primary,
|
|
Additional: v.GetStringSlice("repos.additional"),
|
|
}
|
|
}
|