From 8200f68a896ae38d7b21236ce6ea903d2e006a53 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Tue, 31 Mar 2026 01:13:18 +0300 Subject: [PATCH] feat: change wand file via arg or env var --- README.md | 12 +++++++++ cmd/config.go | 14 +++++++--- cmd/config_test.go | 46 ++++++++++++++++++++++++--------- cmd/root.go | 21 ++++++++++++++- cmd/root_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e927552..1934533 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,18 @@ wand test --help The first config file found is used. +You can override config discovery with an explicit path: + +```bash +# via flag +wand --wand-file ./other-config.yml build + +# via environment variable +WAND_FILE=./other-config.yml wand build +``` + +The `--wand-file` flag takes precedence over `WAND_FILE`. + --- ## 📖 Config Reference diff --git a/cmd/config.go b/cmd/config.go index 1bb9b50..a8908e5 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -87,10 +87,16 @@ type rawConfig struct { Commands map[string]Command `yaml:",inline"` } -func loadConfig() (*Config, map[string]Command, error) { - configPath, err := findConfigFile() - if err != nil { - return nil, nil, err +func loadConfig(explicitPath string) (*Config, map[string]Command, error) { + var configPath string + var err error + if explicitPath != "" { + configPath = explicitPath + } else { + configPath, err = findConfigFile() + if err != nil { + return nil, nil, err + } } data, err := os.ReadFile(configPath) diff --git a/cmd/config_test.go b/cmd/config_test.go index 1228856..eeaeb19 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -115,7 +115,7 @@ build: cmd: go build `) - cfg, commands, err := loadConfig() + cfg, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -154,7 +154,7 @@ parent: cmd: echo grandchild `) - _, commands, err := loadConfig() + _, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -185,7 +185,7 @@ main: cmd: echo test `) - cfg, commands, err := loadConfig() + cfg, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ main: cmd: echo test `) - cfg, _, err := loadConfig() + cfg, _, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -239,7 +239,7 @@ main: cmd: echo test `) - cfg, _, err := loadConfig() + cfg, _, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -258,7 +258,7 @@ main: MY_VAR: hello `) - _, commands, err := loadConfig() + _, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -329,7 +329,7 @@ deploy: confirm: "Deploy to production?" `) - _, commands, err := loadConfig() + _, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -348,7 +348,7 @@ deploy: confirm: true `) - _, commands, err := loadConfig() + _, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -367,7 +367,7 @@ build: aliases: [b, compile] `) - _, commands, err := loadConfig() + _, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -386,7 +386,7 @@ main: working_dir: /tmp `) - _, commands, err := loadConfig() + _, commands, err := loadConfig("") if err != nil { t.Fatal(err) } @@ -396,6 +396,28 @@ main: } } +func TestLoadConfig_ExplicitPath(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "custom.yml") + err := os.WriteFile(configPath, []byte(` +main: + description: from explicit + cmd: echo explicit +`), 0644) + if err != nil { + t.Fatal(err) + } + + _, commands, err := loadConfig(configPath) + if err != nil { + t.Fatal(err) + } + + if commands["main"].Description != "from explicit" { + t.Errorf("description = %q, want 'from explicit'", commands["main"].Description) + } +} + func TestLoadConfig_NoConfigFile(t *testing.T) { dir := t.TempDir() origDir, _ := os.Getwd() @@ -408,7 +430,7 @@ func TestLoadConfig_NoConfigFile(t *testing.T) { // Remove HOME to prevent finding a real ~/.wand.yml t.Setenv("HOME", dir) - _, _, err := loadConfig() + _, _, err := loadConfig("") if err == nil { t.Error("expected error for missing config, got nil") } @@ -434,7 +456,7 @@ main: viper.Reset() }() - _, commands, err := loadConfig() + _, commands, err := loadConfig("") if err != nil { t.Fatal(err) } diff --git a/cmd/root.go b/cmd/root.go index a9470e8..4435c35 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,12 +1,16 @@ package cmd import ( + "os" + "strings" + "github.com/samber/lo" "github.com/spf13/cobra" ) func Execute() error { - cfg, commands, err := loadConfig() + configFile := resolveConfigFile() + cfg, commands, err := loadConfig(configFile) if err != nil { return err } @@ -17,6 +21,8 @@ func Execute() error { SilenceErrors: true, } + rootCmd.PersistentFlags().String("wand-file", "", "path to wand config file (overrides discovery)") + if main, ok := commands["main"]; ok { rootCmd.Short = main.Description rootCmd.Args = cobra.ArbitraryArgs @@ -62,6 +68,19 @@ func buildCobraCommand(cfg *Config, name string, cmd Command) *cobra.Command { return c } +// resolveConfigFile extracts --wand-file from os.Args or WAND_FILE env before cobra parses. +func resolveConfigFile() string { + for i, arg := range os.Args { + if arg == "--wand-file" && i+1 < len(os.Args) { + return os.Args[i+1] + } + if strings.HasPrefix(arg, "--wand-file=") { + return strings.TrimPrefix(arg, "--wand-file=") + } + } + return os.Getenv("WAND_FILE") +} + func registerFlags(c *cobra.Command, flags map[string]Flag) { lo.ForEach( lo.Entries(flags), diff --git a/cmd/root_test.go b/cmd/root_test.go index d96e8bb..8ea083a 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,6 +1,8 @@ package cmd import ( + "os" + "path/filepath" "testing" ) @@ -285,6 +287,68 @@ build: } } +func TestResolveConfigFile_Flag(t *testing.T) { + origArgs := setArgs("wand", "--wand-file", "/tmp/custom.yml", "build") + defer restoreArgs(origArgs) + + got := resolveConfigFile() + if got != "/tmp/custom.yml" { + t.Errorf("resolveConfigFile() = %q, want /tmp/custom.yml", got) + } +} + +func TestResolveConfigFile_FlagEquals(t *testing.T) { + origArgs := setArgs("wand", "--wand-file=/tmp/custom.yml", "build") + defer restoreArgs(origArgs) + + got := resolveConfigFile() + if got != "/tmp/custom.yml" { + t.Errorf("resolveConfigFile() = %q, want /tmp/custom.yml", got) + } +} + +func TestResolveConfigFile_EnvVar(t *testing.T) { + origArgs := setArgs("wand", "build") + defer restoreArgs(origArgs) + + t.Setenv("WAND_FILE", "/tmp/env.yml") + + got := resolveConfigFile() + if got != "/tmp/env.yml" { + t.Errorf("resolveConfigFile() = %q, want /tmp/env.yml", got) + } +} + +func TestResolveConfigFile_FlagOverridesEnv(t *testing.T) { + origArgs := setArgs("wand", "--wand-file", "/tmp/flag.yml") + defer restoreArgs(origArgs) + + t.Setenv("WAND_FILE", "/tmp/env.yml") + + got := resolveConfigFile() + if got != "/tmp/flag.yml" { + t.Errorf("resolveConfigFile() = %q, want /tmp/flag.yml (flag should override env)", got) + } +} + +func TestExecute_WithWandFileFlag(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "custom.yml") + _ = os.WriteFile(configPath, []byte(` +main: + description: custom + cmd: echo custom +`), 0644) + + origArgs := setArgs("wand", "--wand-file", configPath) + defer restoreArgs(origArgs) + + err := Execute() + if err != nil { + t.Fatalf("Execute() failed: %v", err) + } +} + func TestExecute_NoMain(t *testing.T) { setupTestConfig(t, ` build: