--- name: golang-flexible-config-skill description: This skill defines how an AI agent should implement configuration loading in any Go application. --- ## Skill: Go Configuration Pipeline (Launch Arg → Env → Config File → Defaults) **Description**: This skill defines how an AI agent should implement configuration loading in any Go application. The configuration must be loaded in the following order of priority (each source overrides the previous): 1. **Launch arguments** (command-line flags) – **must be handled via Cobra**. 2. **Environment variables** (uppercase, dots/dashes replaced by underscores). 3. **Configuration file** (YAML/JSON/TOML). 4. **Hardcoded defaults**. The skill enforces consistent logging of the config file location (found or not), a well-defined search path list, and a `generate-config` subcommand that produces a commented example config in either English or Russian. All subcommands and flag definitions **must use the Cobra library** (`github.com/spf13/cobra`). ### Env Variable Rules - All environment variables must use **UPPER_CASE**. - Dots (`.` ) and dashes (`-`) in config property names are replaced with underscores (`_`). - Example: property `db.path` maps to env `APP_DB_PATH`, `log-level` maps to `APP_LOG_LEVEL`. - Use `viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))`. ### Config File Search Order The config file is searched in the following order (first found wins). **The agent must log the exact path where the file was found.** If no file is found, log "Configuration file not found" and log the **list of all checked paths** so the user knows where to place one. 1. **Environment variable `APP_CONFIG_LOCATION`** (or custom prefix) – absolute path. 2. **Working directory** – `./config.yaml` (or other extensions). 3. **User config directory** – `~/.config/{app-name}/config.yaml`. 4. **Binary directory** – directory containing the executable. 5. **System Config default** - directory such as /etc/* on unix systems ### Subcommand: `generate-config` (Cobra based) The application must support a subcommand `generate-config` that writes an example config file with detailed comments. - **Flag `--lang`** (default `"en"`, possible values `"en"` and `"ru"`). - `en`: comments in English. - `ru`: comments in Russian. - **Flag `--output`** (optional; if not provided, prints to stdout; if given, writes to that file). The generated file must include every configuration parameter with a meaningful comment describing its purpose and type. ### Example Implementation (Template for AI Agent – Cobra version) Below is a complete, production‑ready example. The agent should use this pattern or a very similar one. ```go package main import ( "fmt" "log" "os" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" ) // Config structure with mapstructure tags type Config struct { Port int `mapstructure:"port"` DBPath string `mapstructure:"db_path"` LogLevel string `mapstructure:"log_level"` } func main() { var rootCmd = &cobra.Command{ Use: "myapp", Short: "MyApp configuration loader", Long: `Loads configuration from defaults, file, env, and CLI flags (in order of priority).`, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := loadConfig() if err != nil { return err } _ = cfg // use cfg in your application return nil }, } // Define main flags rootCmd.Flags().Int("port", 0, "listen port (overrides env/file)") rootCmd.Flags().String("db-path", "", "path to database") rootCmd.Flags().String("log-level", "", "log level (debug, info, warn, error)") // Bind Viper to flags viper.BindPFlag("port", rootCmd.Flags().Lookup("port")) viper.BindPFlag("db_path", rootCmd.Flags().Lookup("db-path")) viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level")) // Subcommand: generate-config var generateCmd = &cobra.Command{ Use: "generate-config", Short: "Generate an example config file", Run: func(cmd *cobra.Command, args []string) { lang, _ := cmd.Flags().GetString("lang") output, _ := cmd.Flags().GetString("output") generateConfig(lang, output) }, } generateCmd.Flags().String("lang", "en", "Language for comments: en or ru") generateCmd.Flags().String("output", "", "Output file path (if empty, prints to stdout)") rootCmd.AddCommand(generateCmd) if err := rootCmd.Execute(); err != nil { log.Fatal(err) } } // loadConfig implements the ordered configuration loading func loadConfig() (*Config, error) { // 1. Defaults (lowest priority) viper.SetDefault("port", 8080) viper.SetDefault("db_path", "./data.db") viper.SetDefault("log_level", "info") // 2. Config file search with logging viper.SetConfigName("config") viper.SetConfigType("yaml") // can also support json, toml, etc. // Collect all search paths for logging searchPaths := []string{} // a) Environment variable APP_CONFIG_LOCATION if envLoc := os.Getenv("APP_CONFIG_LOCATION"); envLoc != "" { viper.SetConfigFile(envLoc) searchPaths = append(searchPaths, fmt.Sprintf("ENV: APP_CONFIG_LOCATION = %s", envLoc)) if err := viper.ReadInConfig(); err == nil { log.Printf("Config file found: %s (from APP_CONFIG_LOCATION)", viper.ConfigFileUsed()) } else { return nil, fmt.Errorf("config file specified in APP_CONFIG_LOCATION not found: %s", envLoc) } } else { // b) Working directory wd, _ := os.Getwd() searchPaths = append(searchPaths, fmt.Sprintf("Working directory: %s", filepath.Join(wd, "config.yaml"))) viper.AddConfigPath(".") // c) ~/.config/{app-name}/ homeDir, _ := os.UserHomeDir() appName := "myapp" // replace with your application name userConfigPath := filepath.Join(homeDir, ".config", appName) searchPaths = append(searchPaths, fmt.Sprintf("User config: %s", filepath.Join(userConfigPath, "config.yaml"))) viper.AddConfigPath(userConfigPath) // d) Binary directory exePath, _ := os.Executable() exeDir := filepath.Dir(exePath) searchPaths = append(searchPaths, fmt.Sprintf("Binary directory: %s", filepath.Join(exeDir, "config.yaml"))) viper.AddConfigPath(exeDir) // e) /etc/{app-name}/ etcPath := filepath.Join("/etc", appName) searchPaths = append(searchPaths, fmt.Sprintf("System config: %s", filepath.Join(etcPath, "config.yaml"))) viper.AddConfigPath(etcPath) // Try to read config if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { log.Println("Configuration file not found. Using defaults and env/args.") log.Println("Search paths checked:") for _, p := range searchPaths { log.Println(" ", p) } } else { return nil, fmt.Errorf("error reading config: %w", err) } } else { log.Printf("Config file found: %s", viper.ConfigFileUsed()) } } // 3. Environment variables viper.SetEnvPrefix("APP") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) viper.AutomaticEnv() // 4. Command-line flags – already bound via Cobra, Viper will read them // No additional code needed, flags are already processed by Cobra // 5. Decode into Config struct var cfg Config if err := viper.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("config unmarshal failed: %w", err) } return &cfg, nil } // generateConfig writes an example config file with English or Russian comments func generateConfig(lang, output string) { var commentLines []string switch lang { case "ru": commentLines = []string{ "# Конфигурация приложения MyApp", "# Все значения могут быть переопределены через переменные окружения (префикс APP_)", "# или аргументы командной строки.", "", "port: 8080 # Порт, на котором будет слушать HTTP-сервер", "db_path: \"./data.db\" # Путь к файлу базы данных SQLite", "log_level: \"info\" # Уровень логирования: debug, info, warn, error", } default: // en commentLines = []string{ "# MyApp configuration file", "# All values can be overridden by environment variables (prefix APP_)", "# or command-line flags.", "", "port: 8080 # HTTP server listening port", "db_path: \"./data.db\" # Path to SQLite database file", "log_level: \"info\" # Log level: debug, info, warn, error", } } content := strings.Join(commentLines, "\n") + "\n" if output != "" { if err := os.WriteFile(output, []byte(content), 0644); err != nil { log.Fatalf("Failed to write example config: %v", err) } log.Printf("Example config written to %s", output) } else { fmt.Print(content) } } ``` ### Agent Instructions - Use the code above as a baseline. - **All subcommands and CLI flags must be implemented via Cobra** (`github.com/spf13/cobra`). - Always include logging of config file location (found or not) and all checked search paths. - The `generate-config` subcommand must accept `--lang` (default `"en"`, can be `"ru"`) and `--output`. - Comments inside the generated config must be meaningful and match the selected language. - The config search order and env variable rules are mandatory. - If the agent uses a different configuration library (e.g., `envconfig`, `cleanenv`), the same semantics must be maintained, but CLI handling must still be through Cobra. - **Never set `SilenceUsage: true` on the root command or any subcommand.** Incorrect flags or invalid subcommands **must** result in an explicit error message and the usage output. Cobra’s default behavior already provides this; leave `SilenceUsage` and `SilenceErrors` at their default (`false`). Do not suppress automatic usage printing on parse errors. --- ## Self‑Check: Adding a New Config Property When you introduce a new configuration property to the application (e.g. `max_connections`), verify each of the following points. **Tick all boxes before considering the change complete.** - [ ] 1. **Default value** – A sensible hardcoded default is set in `viper.SetDefault(...)` (or equivalent). - [ ] 2. **Struct field** – The property exists as a field in the `Config` struct with a correct `mapstructure` tag. - [ ] 3. **Env mapping** – The corresponding environment variable is automatically picked up. Ensure the env name follows the pattern: prefix + uppercase + underscores (dots/dashes replaced). *Example:* for `max_connections` you should be able to set `APP_MAX_CONNECTIONS`. - [ ] 4. **Command‑line flag (Cobra)** – A flag is defined on the appropriate Cobra command (root or sub) and bound via `viper.BindPFlag(...)`. *Example flag name:* `--max-connections` (dashes). The binding uses the struct key `max_connections`. - [ ] 5. **Config file key** – The property is documented as a key in the example config file (YAML key matches the struct field name). - [ ] 6. **Comments in `generate-config`** – The example config output includes a meaningful comment for the new property in **both** English and Russian (if the `--lang ru` case is implemented). The comment explains the unit, default, and allowed values if applicable. - [ ] 7. **Logging** – No additional logging is required for a single property change; the existing logging of the config file location is sufficient. - [ ] 8. **Consistency check** – Verify that the same property name is used consistently across: struct tag, env prefix replacement, flag binding, and config file key. **Run through this checklist every time you add or modify a configuration property.** This ensures all four sources (defaults, file, env, flags) work seamlessly together.