Files
beads/cmd/bd/config.go
Steve Yegge 2c57d41275 fix: Show warning when config.yaml overrides database config (bd-20j)
bd config list now shows a warning when config.yaml or environment
variables override database settings. This addresses the confusion when
sync.branch from config.yaml takes precedence over the database value.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 14:03:39 -08:00

278 lines
7.3 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <key> <value>",
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 <key>",
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 <key>",
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)
}