package main import ( "fmt" "os" "sort" "strings" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/syncbranch" ) var configCmd = &cobra.Command{ Use: "config", GroupID: "setup", Short: "Manage configuration settings", Long: `Manage configuration settings for external integrations and preferences. Configuration is stored per-project in .beads/*.db and is version-control-friendly. Common namespaces: - jira.* Jira integration settings - linear.* Linear integration settings - github.* GitHub integration settings - custom.* Custom integration settings - status.* Issue status configuration Custom Status States: You can define custom status states for multi-step pipelines using the status.custom config key. Statuses should be comma-separated. Example: bd config set status.custom "awaiting_review,awaiting_testing,awaiting_docs" This enables issues to use statuses like 'awaiting_review' in addition to the built-in statuses (open, in_progress, blocked, deferred, closed). Examples: bd config set jira.url "https://company.atlassian.net" bd config set jira.project "PROJ" bd config set status.custom "awaiting_review,awaiting_testing" bd config get jira.url bd config list bd config unset jira.url`, } var configSetCmd = &cobra.Command{ Use: "set ", Short: "Set a configuration value", Args: cobra.ExactArgs(2), Run: func(_ *cobra.Command, args []string) { key := args[0] value := args[1] // Check if this is a yaml-only key (startup settings like no-db, no-daemon, etc.) // These must be written to config.yaml, not SQLite, because they're read // before the database is opened. (GH#536) if config.IsYamlOnlyKey(key) { if err := config.SetYamlConfig(key, value); err != nil { fmt.Fprintf(os.Stderr, "Error setting config: %v\n", err) os.Exit(1) } if jsonOutput { outputJSON(map[string]interface{}{ "key": key, "value": value, "location": "config.yaml", }) } else { fmt.Printf("Set %s = %s (in config.yaml)\n", key, value) } return } // Database-stored config requires direct mode if err := ensureDirectMode("config set requires direct database access"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } ctx := rootCtx // Special handling for sync.branch to apply validation if strings.TrimSpace(key) == syncbranch.ConfigKey { if err := syncbranch.Set(ctx, store, value); err != nil { fmt.Fprintf(os.Stderr, "Error setting config: %v\n", err) os.Exit(1) } } else { if err := store.SetConfig(ctx, key, value); err != nil { fmt.Fprintf(os.Stderr, "Error setting config: %v\n", err) os.Exit(1) } } if jsonOutput { outputJSON(map[string]string{ "key": key, "value": value, }) } else { fmt.Printf("Set %s = %s\n", key, value) } }, } var configGetCmd = &cobra.Command{ Use: "get ", Short: "Get a configuration value", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { key := args[0] // Check if this is a yaml-only key (startup settings) // These are read from config.yaml via viper, not SQLite. (GH#536) if config.IsYamlOnlyKey(key) { value := config.GetYamlConfig(key) if jsonOutput { outputJSON(map[string]interface{}{ "key": key, "value": value, "location": "config.yaml", }) } else { if value == "" { fmt.Printf("%s (not set in config.yaml)\n", key) } else { fmt.Printf("%s\n", value) } } return } // Database-stored config requires direct mode if err := ensureDirectMode("config get requires direct database access"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } ctx := rootCtx var value string var err error // Special handling for sync.branch to support env var override if strings.TrimSpace(key) == syncbranch.ConfigKey { value, err = syncbranch.Get(ctx, store) } else { value, err = store.GetConfig(ctx, key) } if err != nil { fmt.Fprintf(os.Stderr, "Error getting config: %v\n", err) os.Exit(1) } if jsonOutput { outputJSON(map[string]string{ "key": key, "value": value, }) } else { if value == "" { fmt.Printf("%s (not set)\n", key) } else { fmt.Printf("%s\n", value) } } }, } var configListCmd = &cobra.Command{ Use: "list", Short: "List all configuration", Run: func(cmd *cobra.Command, args []string) { // Config operations work in direct mode only if err := ensureDirectMode("config list requires direct database access"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } ctx := rootCtx config, err := store.GetAllConfig(ctx) if err != nil { fmt.Fprintf(os.Stderr, "Error listing config: %v\n", err) os.Exit(1) } if jsonOutput { outputJSON(config) return } if len(config) == 0 { fmt.Println("No configuration set") return } // Sort keys for consistent output keys := make([]string, 0, len(config)) for k := range config { keys = append(keys, k) } sort.Strings(keys) fmt.Println("\nConfiguration:") for _, k := range keys { fmt.Printf(" %s = %s\n", k, config[k]) } // Check for config.yaml overrides that take precedence (bd-20j) // This helps diagnose when effective config differs from database config showConfigYAMLOverrides(config) }, } // showConfigYAMLOverrides warns when config.yaml or env vars override database settings. // This addresses the confusion when `bd config list` shows one value but the effective // value used by commands is different due to higher-priority config sources. func showConfigYAMLOverrides(dbConfig map[string]string) { var overrides []string // Check sync.branch - can be overridden by BEADS_SYNC_BRANCH env var or config.yaml sync-branch if dbSyncBranch, ok := dbConfig[syncbranch.ConfigKey]; ok && dbSyncBranch != "" { effectiveBranch := syncbranch.GetFromYAML() if effectiveBranch != "" && effectiveBranch != dbSyncBranch { overrides = append(overrides, fmt.Sprintf(" sync.branch: database has '%s' but effective value is '%s' (from config.yaml or env)", dbSyncBranch, effectiveBranch)) } } if len(overrides) > 0 { fmt.Println("\n⚠️ Config overrides (higher priority sources):") for _, o := range overrides { fmt.Println(o) } fmt.Println("\nNote: config.yaml and environment variables take precedence over database config.") } } var configUnsetCmd = &cobra.Command{ Use: "unset ", Short: "Delete a configuration value", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Config operations work in direct mode only if err := ensureDirectMode("config unset requires direct database access"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } key := args[0] ctx := rootCtx if err := store.DeleteConfig(ctx, key); err != nil { fmt.Fprintf(os.Stderr, "Error deleting config: %v\n", err) os.Exit(1) } if jsonOutput { outputJSON(map[string]string{ "key": key, }) } else { fmt.Printf("Unset %s\n", key) } }, } func init() { configCmd.AddCommand(configSetCmd) configCmd.AddCommand(configGetCmd) configCmd.AddCommand(configListCmd) configCmd.AddCommand(configUnsetCmd) rootCmd.AddCommand(configCmd) }