Skip to content

fix: bind CLI config file values to cobra flags#1566

Open
ExpertVagabond wants to merge 1 commit intokagent-dev:mainfrom
ExpertVagabond:fix/cli-config-values-973
Open

fix: bind CLI config file values to cobra flags#1566
ExpertVagabond wants to merge 1 commit intokagent-dev:mainfrom
ExpertVagabond:fix/cli-config-values-973

Conversation

@ExpertVagabond
Copy link
Copy Markdown

Summary

Fixes #973 — CLI config file values (~/.kagent/config.yaml) were silently ignored because cobra flag defaults always took precedence over viper-backed config.

Changes

  • config.BindFlags() — explicitly binds dash-separated cobra flag names (kagent-url, output-format) to underscore-separated viper/config keys (kagent_url, output_format)
  • config.Apply() — populates the shared Config struct from the merged viper state before any subcommand runs
  • PersistentPreRunE in main.go — calls Apply() after flag parsing so all commands see config file values
  • Tests — verifies config file values are used when flags are not set, and that explicit CLI flags still override config file values

Precedence (unchanged semantics, now actually works)

CLI flag > environment variable > config file > viper default

Testing

=== RUN   TestConfigFileValuesAreUsed
--- PASS: TestConfigFileValuesAreUsed (0.00s)
=== RUN   TestCLIFlagsOverrideConfigFile
--- PASS: TestCLIFlagsOverrideConfigFile (0.00s)

Config file values (~/.kagent/config.yaml) were ignored because cobra
flag defaults always took precedence. This wires viper-backed config
into the flag system so the precedence is: CLI flag > env > config file
> viper default.

Adds BindFlags() to explicitly map dash-separated flag names to
underscore-separated config keys, and Apply() to populate the shared
Config struct from the merged viper state before any subcommand runs.

Includes tests verifying config file values are used and that explicit
CLI flags still override them.
Copilot AI review requested due to automatic review settings March 30, 2026 14:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes CLI config precedence so values from ~/.kagent/config.yaml are not silently ignored when corresponding cobra flags exist, by explicitly binding cobra flag names to viper/config keys and applying merged viper state before command execution.

Changes:

  • Add explicit cobra-flag-to-viper-key bindings (kagent-urlkagent_url, etc.).
  • Introduce config.Apply() and run it via rootCmd.PersistentPreRunE so subcommands see merged config.
  • Add tests validating config-file defaults are honored and CLI flags still override.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
go/core/cli/internal/config/config.go Adds BindFlags() for explicit key mapping and Apply() to copy merged viper state into the shared Config.
go/core/cli/cmd/kagent/main.go Calls BindFlags() and wires Apply() into PersistentPreRunE so config is populated after flag parsing.
go/core/cli/internal/config/config_test.go New tests asserting config-file values apply when flags aren’t set and CLI flags override config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +74
func BindFlags(flags *pflag.FlagSet) {
viper.BindPFlag("kagent_url", flags.Lookup("kagent-url")) //nolint:errcheck
viper.BindPFlag("namespace", flags.Lookup("namespace")) //nolint:errcheck
viper.BindPFlag("output_format", flags.Lookup("output-format")) //nolint:errcheck
viper.BindPFlag("verbose", flags.Lookup("verbose")) //nolint:errcheck
viper.BindPFlag("timeout", flags.Lookup("timeout")) //nolint:errcheck
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BindFlags ignores the return value from viper.BindPFlag and also passes flags.Lookup(...) without checking for nil. If a flag is renamed/removed (or a different FlagSet is passed), the binding will silently fail and config precedence will regress. Please handle and surface these errors (e.g., return an error from BindFlags and check it in main), and explicitly error when Lookup returns nil for an expected flag.

Suggested change
func BindFlags(flags *pflag.FlagSet) {
viper.BindPFlag("kagent_url", flags.Lookup("kagent-url")) //nolint:errcheck
viper.BindPFlag("namespace", flags.Lookup("namespace")) //nolint:errcheck
viper.BindPFlag("output_format", flags.Lookup("output-format")) //nolint:errcheck
viper.BindPFlag("verbose", flags.Lookup("verbose")) //nolint:errcheck
viper.BindPFlag("timeout", flags.Lookup("timeout")) //nolint:errcheck
func BindFlags(flags *pflag.FlagSet) error {
if flags == nil {
return fmt.Errorf("BindFlags: flags must not be nil")
}
bind := func(key, flagName string) error {
f := flags.Lookup(flagName)
if f == nil {
return fmt.Errorf("BindFlags: expected flag %q not found in FlagSet", flagName)
}
if err := viper.BindPFlag(key, f); err != nil {
return fmt.Errorf("BindFlags: failed to bind viper key %q to flag %q: %w", key, flagName, err)
}
return nil
}
if err := bind("kagent_url", "kagent-url"); err != nil {
return err
}
if err := bind("namespace", "namespace"); err != nil {
return err
}
if err := bind("output_format", "output-format"); err != nil {
return err
}
if err := bind("verbose", "verbose"); err != nil {
return err
}
if err := bind("timeout", "timeout"); err != nil {
return err
}
return nil

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +80
// Apply populates the given Config from the merged viper state (config file +
// env + CLI flags). Call this after flag parsing to ensure cfg reflects all
// sources with the correct precedence: CLI flag > env > config file > default.
func Apply(cfg *Config) error {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Apply() docstring and precedence statement mention merging env vars, but this package currently only binds USER_ID and does not enable AutomaticEnv / BindEnv for the config keys (kagent_url, namespace, output_format, verbose, timeout). Either implement env var support for these keys (and document the expected env names), or update this comment so it matches actual behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +442 to +452
// Populate cfg from the merged viper state (config file + env + flags)
// before any subcommand runs, ensuring all commands see config file values.
originalPreRunE := rootCmd.PersistentPreRunE
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if err := config.Apply(cfg); err != nil {
return fmt.Errorf("error applying config: %w", err)
}
if originalPreRunE != nil {
return originalPreRunE(cmd, args)
}
return nil
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling config.Apply(cfg) from rootCmd.PersistentPreRunE overwrites the already-parsed flag-backed cfg fields with viper state. This breaks command-local overrides of shared config fields that are not bound to viper. For example, deployCmd defines its own --namespace flag (separate from the persistent one), so kagent deploy --namespace X will be parsed into cfg.Namespace and then potentially clobbered by Apply() since viper is only bound to the persistent flag. Consider binding using the executing command's flag set (e.g., in this PreRunE: BindFlags(cmd.Flags()) before Apply), or remove the command-local --namespace and rely on the persistent flag so CLI overrides are preserved.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI config values are not used

2 participants