From 1c7863a1652a92cd3fa8dca48038351945be7179 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 15 Feb 2026 13:06:58 -0500 Subject: [PATCH 1/7] feat: add YAML config file support via koanf Add ~/.config/hypeman/cli.yaml support so the CLI can read base_url and api_key from a config file instead of requiring environment variables. Config precedence: CLI flags > env vars > config file. This pairs with the server-side config migration in kernel/hypeman to enable a zero-config local experience after running install.sh. --- go.mod | 9 +++++++++ go.sum | 18 ++++++++++++++++++ pkg/cmd/cmdutil.go | 17 ++++++++++++++++- pkg/cmd/config.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 pkg/cmd/config.go diff --git a/go.mod b/go.mod index 72b6685..69cd6e4 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,9 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/itchyny/json2yaml v0.1.4 github.com/kernel/hypeman-go v0.11.0 + github.com/knadh/koanf/parsers/yaml v1.1.0 + github.com/knadh/koanf/providers/file v1.2.1 + github.com/knadh/koanf/v2 v2.3.2 github.com/muesli/reflow v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 @@ -42,15 +45,20 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/klauspost/compress v1.18.1 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -70,6 +78,7 @@ require ( go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/go.sum b/go.sum index 9c99ded..510dcdc 100644 --- a/go.sum +++ b/go.sum @@ -55,11 +55,15 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= @@ -76,6 +80,14 @@ github.com/kernel/hypeman-go v0.11.0 h1:hCXNUHtrhGKswJapzyWyozBOXhKK/oreKvm0AXHu github.com/kernel/hypeman-go v0.11.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= +github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= +github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= +github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= +github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -89,8 +101,12 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -169,6 +185,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 8093413..a01f91e 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -27,9 +27,24 @@ func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { option.WithHeader("User-Agent", fmt.Sprintf("Hypeman/CLI %s", Version)), } - // Override base URL if the --base-url flag is provided + // Load config file for fallback values + cfg := loadCLIConfig() + + // Precedence for base URL: CLI flag > env var > config file if baseURL := cmd.String("base-url"); baseURL != "" { opts = append(opts, option.WithBaseURL(baseURL)) + } else if baseURL := os.Getenv("HYPEMAN_BASE_URL"); baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } else if cfg.BaseURL != "" { + opts = append(opts, option.WithBaseURL(cfg.BaseURL)) + } + + // Precedence for API key: env var > config file + // (no CLI flag for API key for security reasons) + if apiKey := os.Getenv("HYPEMAN_API_KEY"); apiKey != "" { + opts = append(opts, option.WithAPIKey(apiKey)) + } else if cfg.APIKey != "" { + opts = append(opts, option.WithAPIKey(cfg.APIKey)) } return opts diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go new file mode 100644 index 0000000..40e3230 --- /dev/null +++ b/pkg/cmd/config.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "os" + "path/filepath" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" +) + +// CLIConfig holds CLI configuration loaded from cli.yaml +type CLIConfig struct { + BaseURL string `koanf:"base_url"` + APIKey string `koanf:"api_key"` +} + +// getCLIConfigPath returns the path to the CLI config file. +// The CLI uses ~/.config/hypeman/cli.yaml on all platforms. +func getCLIConfigPath() string { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".config", "hypeman", "cli.yaml") +} + +// loadCLIConfig loads CLI configuration from the config file. +// Returns an empty config if the file doesn't exist or can't be parsed. +func loadCLIConfig() *CLIConfig { + cfg := &CLIConfig{} + + configPath := getCLIConfigPath() + if configPath == "" { + return cfg + } + + k := koanf.New(".") + if err := k.Load(file.Provider(configPath), yaml.Parser()); err != nil { + // File doesn't exist or can't be parsed - return empty config + return cfg + } + + _ = k.Unmarshal("", cfg) + return cfg +} From ece2071367da05026466449e4e7c6276f2f1c717 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 15 Feb 2026 13:18:16 -0500 Subject: [PATCH 2/7] fix: unify credential resolution across all commands - Add resolveBaseURL() and resolveAPIKey() helpers to config.go - Update exec, cp, and push commands to use shared helpers - All commands now consistently use: flag > env > config file - WebSocket commands no longer bypass cli.yaml configuration --- pkg/cmd/config.go | 30 ++++++++++++++++++++++++++++++ pkg/cmd/cp.go | 14 ++++---------- pkg/cmd/exec.go | 14 ++++---------- pkg/cmd/push.go | 13 ++----------- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go index 40e3230..3121a13 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -7,6 +7,7 @@ import ( "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/v2" + "github.com/urfave/cli/v3" ) // CLIConfig holds CLI configuration loaded from cli.yaml @@ -44,3 +45,32 @@ func loadCLIConfig() *CLIConfig { _ = k.Unmarshal("", cfg) return cfg } + +// resolveBaseURL returns the effective base URL with precedence: +// CLI flag > env var > config file > default. +func resolveBaseURL(cmd *cli.Command) string { + if u := cmd.Root().String("base-url"); u != "" { + return u + } + if u := os.Getenv("HYPEMAN_BASE_URL"); u != "" { + return u + } + cfg := loadCLIConfig() + if cfg.BaseURL != "" { + return cfg.BaseURL + } + return "http://localhost:8080" +} + +// resolveAPIKey returns the effective API key with precedence: +// env var > config file. +func resolveAPIKey() string { + if k := os.Getenv("HYPEMAN_BEARER_TOKEN"); k != "" { + return k + } + if k := os.Getenv("HYPEMAN_API_KEY"); k != "" { + return k + } + cfg := loadCLIConfig() + return cfg.APIKey +} diff --git a/pkg/cmd/cp.go b/pkg/cmd/cp.go index 97d0410..43a653b 100644 --- a/pkg/cmd/cp.go +++ b/pkg/cmd/cp.go @@ -145,18 +145,12 @@ func handleCp(ctx context.Context, cmd *cli.Command) error { return err } - // Get base URL and API key - baseURL := cmd.Root().String("base-url") - if baseURL == "" { - baseURL = os.Getenv("HYPEMAN_BASE_URL") - } - if baseURL == "" { - baseURL = "http://localhost:8080" - } + // Get base URL and API key (flag > env > config file) + baseURL := resolveBaseURL(cmd) - apiKey := os.Getenv("HYPEMAN_API_KEY") + apiKey := resolveAPIKey() if apiKey == "" { - return fmt.Errorf("HYPEMAN_API_KEY environment variable required") + return fmt.Errorf("API key required: set HYPEMAN_API_KEY or configure api_key in ~/.config/hypeman/cli.yaml") } archive := cmd.Bool("archive") diff --git a/pkg/cmd/exec.go b/pkg/cmd/exec.go index a58ea75..d53efe5 100644 --- a/pkg/cmd/exec.go +++ b/pkg/cmd/exec.go @@ -145,18 +145,12 @@ func handleExec(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("failed to marshal request: %w", err) } - // Get base URL and API key - baseURL := cmd.Root().String("base-url") - if baseURL == "" { - baseURL = os.Getenv("HYPEMAN_BASE_URL") - } - if baseURL == "" { - baseURL = "http://localhost:8080" - } + // Get base URL and API key (flag > env > config file) + baseURL := resolveBaseURL(cmd) - apiKey := os.Getenv("HYPEMAN_API_KEY") + apiKey := resolveAPIKey() if apiKey == "" { - return fmt.Errorf("HYPEMAN_API_KEY environment variable required") + return fmt.Errorf("API key required: set HYPEMAN_API_KEY or configure api_key in ~/.config/hypeman/cli.yaml") } // Build WebSocket URL diff --git a/pkg/cmd/push.go b/pkg/cmd/push.go index e66f61e..da8f30e 100644 --- a/pkg/cmd/push.go +++ b/pkg/cmd/push.go @@ -35,13 +35,7 @@ func handlePush(ctx context.Context, cmd *cli.Command) error { targetName = args[1] } - baseURL := cmd.String("base-url") - if baseURL == "" { - baseURL = os.Getenv("HYPEMAN_BASE_URL") - } - if baseURL == "" { - baseURL = "http://localhost:8080" - } + baseURL := resolveBaseURL(cmd) parsedURL, err := url.Parse(baseURL) if err != nil { @@ -71,10 +65,7 @@ func handlePush(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("invalid target: %w", err) } - token := os.Getenv("HYPEMAN_BEARER_TOKEN") - if token == "" { - token = os.Getenv("HYPEMAN_API_KEY") - } + token := resolveAPIKey() // Use custom transport that always sends Basic auth header transport := &authTransport{ From 206fc2d7756f9239dfbdd2244905dfd8b4231c2e Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 15 Feb 2026 13:39:52 -0500 Subject: [PATCH 3/7] fix: unify auth precedence in SDK and WebSocket paths Update getDefaultRequestOptions to use resolveBaseURL() and resolveAPIKey() so SDK calls and WebSocket calls share the same HYPEMAN_BEARER_TOKEN > HYPEMAN_API_KEY > config file precedence. --- pkg/cmd/cmdutil.go | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index a01f91e..b4f2992 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -27,24 +27,13 @@ func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { option.WithHeader("User-Agent", fmt.Sprintf("Hypeman/CLI %s", Version)), } - // Load config file for fallback values - cfg := loadCLIConfig() - - // Precedence for base URL: CLI flag > env var > config file - if baseURL := cmd.String("base-url"); baseURL != "" { - opts = append(opts, option.WithBaseURL(baseURL)) - } else if baseURL := os.Getenv("HYPEMAN_BASE_URL"); baseURL != "" { + // Use the same resolvers as WebSocket commands for consistent precedence + if baseURL := resolveBaseURL(cmd); baseURL != "" { opts = append(opts, option.WithBaseURL(baseURL)) - } else if cfg.BaseURL != "" { - opts = append(opts, option.WithBaseURL(cfg.BaseURL)) } - // Precedence for API key: env var > config file - // (no CLI flag for API key for security reasons) - if apiKey := os.Getenv("HYPEMAN_API_KEY"); apiKey != "" { + if apiKey := resolveAPIKey(); apiKey != "" { opts = append(opts, option.WithAPIKey(apiKey)) - } else if cfg.APIKey != "" { - opts = append(opts, option.WithAPIKey(cfg.APIKey)) } return opts From 5ee2a1731469afce1793c40531ae2e6be46a2765 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 15 Feb 2026 13:45:58 -0500 Subject: [PATCH 4/7] fix: give HYPEMAN_API_KEY precedence over HYPEMAN_BEARER_TOKEN HYPEMAN_API_KEY is the documented primary env var. HYPEMAN_BEARER_TOKEN is a legacy fallback and should not override it. --- pkg/cmd/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go index 3121a13..ea7c4b7 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -63,12 +63,12 @@ func resolveBaseURL(cmd *cli.Command) string { } // resolveAPIKey returns the effective API key with precedence: -// env var > config file. +// HYPEMAN_API_KEY env var > HYPEMAN_BEARER_TOKEN env var (legacy) > config file. func resolveAPIKey() string { - if k := os.Getenv("HYPEMAN_BEARER_TOKEN"); k != "" { + if k := os.Getenv("HYPEMAN_API_KEY"); k != "" { return k } - if k := os.Getenv("HYPEMAN_API_KEY"); k != "" { + if k := os.Getenv("HYPEMAN_BEARER_TOKEN"); k != "" { return k } cfg := loadCLIConfig() From 92d0e3835d5811faee6ed18f19f187dd6047e547 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 15 Feb 2026 15:52:11 -0500 Subject: [PATCH 5/7] chore: remove internal process comment from cmdutil.go --- pkg/cmd/cmdutil.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index b4f2992..e50b2e0 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -27,7 +27,6 @@ func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { option.WithHeader("User-Agent", fmt.Sprintf("Hypeman/CLI %s", Version)), } - // Use the same resolvers as WebSocket commands for consistent precedence if baseURL := resolveBaseURL(cmd); baseURL != "" { opts = append(opts, option.WithBaseURL(baseURL)) } From 440e9381b88930b17d389bcc6502905319ba3cc4 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 15 Feb 2026 16:40:36 -0500 Subject: [PATCH 6/7] refactor: use koanf env provider for HYPEMAN_ env vars Load HYPEMAN_BASE_URL and HYPEMAN_API_KEY via koanf's env provider in loadCLIConfig() instead of manual os.Getenv checks. This gives consistent precedence (env > config file) in one place. HYPEMAN_BEARER_TOKEN remains as a legacy fallback checked separately. --- go.mod | 1 + go.sum | 2 ++ pkg/cmd/config.go | 38 +++++++++++++++++++++----------------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 69cd6e4..f591003 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/klauspost/compress v1.18.1 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/knadh/koanf/providers/env v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect diff --git a/go.sum b/go.sum index 510dcdc..9c04859 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpb github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= +github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc= +github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go index ea7c4b7..5be3561 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -3,8 +3,10 @@ package cmd import ( "os" "path/filepath" + "strings" "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/v2" "github.com/urfave/cli/v3" @@ -26,35 +28,37 @@ func getCLIConfigPath() string { return filepath.Join(home, ".config", "hypeman", "cli.yaml") } -// loadCLIConfig loads CLI configuration from the config file. +// loadCLIConfig loads CLI configuration from the config file, then +// overlays HYPEMAN_-prefixed environment variables (highest precedence). +// HYPEMAN_BASE_URL -> base_url, HYPEMAN_API_KEY -> api_key. // Returns an empty config if the file doesn't exist or can't be parsed. func loadCLIConfig() *CLIConfig { cfg := &CLIConfig{} + k := koanf.New(".") configPath := getCLIConfigPath() - if configPath == "" { - return cfg + if configPath != "" { + _ = k.Load(file.Provider(configPath), yaml.Parser()) } - k := koanf.New(".") - if err := k.Load(file.Provider(configPath), yaml.Parser()); err != nil { - // File doesn't exist or can't be parsed - return empty config - return cfg - } + // Overlay HYPEMAN_-prefixed env vars: HYPEMAN_BASE_URL -> base_url + _ = k.Load(env.ProviderWithValue("HYPEMAN_", ".", func(key string, value string) (string, interface{}) { + if value == "" { + return "", nil + } + return strings.ToLower(strings.TrimPrefix(key, "HYPEMAN_")), value + }), nil) _ = k.Unmarshal("", cfg) return cfg } // resolveBaseURL returns the effective base URL with precedence: -// CLI flag > env var > config file > default. +// CLI flag > HYPEMAN_BASE_URL env > config file > default. func resolveBaseURL(cmd *cli.Command) string { if u := cmd.Root().String("base-url"); u != "" { return u } - if u := os.Getenv("HYPEMAN_BASE_URL"); u != "" { - return u - } cfg := loadCLIConfig() if cfg.BaseURL != "" { return cfg.BaseURL @@ -63,14 +67,14 @@ func resolveBaseURL(cmd *cli.Command) string { } // resolveAPIKey returns the effective API key with precedence: -// HYPEMAN_API_KEY env var > HYPEMAN_BEARER_TOKEN env var (legacy) > config file. +// HYPEMAN_API_KEY env > config file > HYPEMAN_BEARER_TOKEN env (legacy). func resolveAPIKey() string { - if k := os.Getenv("HYPEMAN_API_KEY"); k != "" { - return k + cfg := loadCLIConfig() + if cfg.APIKey != "" { + return cfg.APIKey } if k := os.Getenv("HYPEMAN_BEARER_TOKEN"); k != "" { return k } - cfg := loadCLIConfig() - return cfg.APIKey + return "" } From b959cf4034086824bfb9d8bcb628f248bd1cf1be Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 15 Feb 2026 16:52:15 -0500 Subject: [PATCH 7/7] refactor: remove legacy HYPEMAN_BEARER_TOKEN support --- pkg/cmd/config.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go index 5be3561..2cd4146 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -67,14 +67,8 @@ func resolveBaseURL(cmd *cli.Command) string { } // resolveAPIKey returns the effective API key with precedence: -// HYPEMAN_API_KEY env > config file > HYPEMAN_BEARER_TOKEN env (legacy). +// HYPEMAN_API_KEY env > config file. func resolveAPIKey() string { cfg := loadCLIConfig() - if cfg.APIKey != "" { - return cfg.APIKey - } - if k := os.Getenv("HYPEMAN_BEARER_TOKEN"); k != "" { - return k - } - return "" + return cfg.APIKey }