Files
pmg/config/viper.go
T
Sahil BansalandGitHub d1dd2560a4 feat: consolidate proxy config into structured section and add support for custom commands to skip proxy (#240)
* feat: add ProxyConfig struct with per-PM skip_commands and legacy fallback

* feat: consolidate proxy config into structured section with backward compat

Replaces flat proxy_mode/proxy_install_only keys with a structured proxy
section supporting per-package-manager skip_commands. Legacy keys are
respected via fallback when user's config lacks the new proxy section.
Removes deprecated experimental_proxy_mode config and flag.

* fix: env var resolution for nested config keys and deduplicate skip command matching

- Add "." to "_" in Viper env key replacer so nested keys like
  sandbox.enabled resolve from PMG_SANDBOX_ENABLED (was silently broken)
- Export IsFirstNonFlagArgInList and remove duplicate from proxy_flow.go
- Add table-driven tests for skip command matching with real-world cases
- Remove redundant env var test

* docs: update proxy configuration and env var documentation

Update config.md env var table to reflect new proxy.enabled and
proxy.install_only keys. Add proxy configuration section to proxy.md
covering config structure, per-PM skip commands, CLI flags, and env vars.

* fix: legacy fallback precedence
2026-05-06 18:27:23 +05:30

90 lines
2.7 KiB
Go

package config
import (
"fmt"
"os"
"strings"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
// loadViperConfig loads the configuration using Viper.
// Precedence (highest to lowest): cobra flags > env vars > config file > defaults.
// Cobra flags write directly to the config struct after this function runs.
func loadViperConfig() error {
configPath, err := configFilePath()
if err != nil {
return fmt.Errorf("failed to get config file path: %w", err)
}
v := viper.New()
v.SetConfigType("yaml")
v.SetEnvPrefix("PMG")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
// Load the embedded template as the base so Viper knows all keys and their
// defaults. This is required for AutomaticEnv to resolve PMG_* env vars for
// keys that are absent from or newer than the user's config file.
if err := v.ReadConfig(strings.NewReader(templateConfig)); err != nil {
return fmt.Errorf("failed to load default config: %w", err)
}
// Merge user config on top if it exists.
if _, statErr := os.Stat(configPath); statErr == nil {
v.SetConfigFile(configPath)
if err := v.MergeInConfig(); err != nil {
return fmt.Errorf("failed to read config file %s: %w", configPath, err)
}
}
merged := globalConfig.Config
if err := v.Unmarshal(&merged); err != nil {
return fmt.Errorf("failed to unmarshal config: %w", err)
}
globalConfig.Config = merged
// Resolve proxy config: new proxy section > legacy flat keys.
// Viper can't distinguish "value from template" vs "value from user config"
// (v.IsSet is always true for template keys), so we check the raw user file.
if !hasProxySectionInFile(configPath) {
applyProxyLegacyFallback(v)
}
return nil
}
// hasProxySectionInFile checks whether the user's config file contains a
// top-level "proxy" key. Returns false if the file doesn't exist or can't
// be parsed.
func hasProxySectionInFile(path string) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
var raw map[string]any
if err := yaml.Unmarshal(data, &raw); err != nil {
return false
}
_, ok := raw["proxy"]
return ok
}
// applyProxyLegacyFallback populates the new Proxy struct from deprecated
// flat keys when the user's config file does not have a proxy: section.
// New env vars (PMG_PROXY_ENABLED, PMG_PROXY_INSTALL_ONLY) take precedence
// over legacy config file keys to respect the documented precedence order.
func applyProxyLegacyFallback(v *viper.Viper) {
if os.Getenv("PMG_PROXY_ENABLED") == "" && v.IsSet("proxy_mode") {
globalConfig.Config.Proxy.Enabled = v.GetBool("proxy_mode")
}
if os.Getenv("PMG_PROXY_INSTALL_ONLY") == "" && v.IsSet("proxy_install_only") {
globalConfig.Config.Proxy.InstallOnly = v.GetBool("proxy_install_only")
}
}