Skip to content

Commit 672a55a

Browse files
committed
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.
1 parent a3ea271 commit 672a55a

File tree

4 files changed

+89
-1
lines changed

4 files changed

+89
-1
lines changed

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ require (
1111
github.com/gorilla/websocket v1.5.3
1212
github.com/itchyny/json2yaml v0.1.4
1313
github.com/kernel/hypeman-go v0.9.8
14+
github.com/knadh/koanf/parsers/yaml v1.1.0
15+
github.com/knadh/koanf/providers/file v1.2.1
16+
github.com/knadh/koanf/v2 v2.3.2
1417
github.com/muesli/reflow v0.3.0
1518
github.com/stretchr/testify v1.11.1
1619
github.com/tidwall/gjson v1.18.0
@@ -42,15 +45,20 @@ require (
4245
github.com/docker/go-units v0.5.0 // indirect
4346
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
4447
github.com/felixge/httpsnoop v1.0.4 // indirect
48+
github.com/fsnotify/fsnotify v1.9.0 // indirect
4549
github.com/go-logr/logr v1.4.3 // indirect
4650
github.com/go-logr/stdr v1.2.2 // indirect
51+
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
4752
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
4853
github.com/klauspost/compress v1.18.1 // indirect
54+
github.com/knadh/koanf/maps v0.1.2 // indirect
4955
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
5056
github.com/mattn/go-isatty v0.0.20 // indirect
5157
github.com/mattn/go-localereader v0.0.1 // indirect
5258
github.com/mattn/go-runewidth v0.0.16 // indirect
59+
github.com/mitchellh/copystructure v1.2.0 // indirect
5360
github.com/mitchellh/go-homedir v1.1.0 // indirect
61+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
5462
github.com/moby/docker-image-spec v1.3.1 // indirect
5563
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
5664
github.com/muesli/cancelreader v0.2.2 // indirect
@@ -70,6 +78,7 @@ require (
7078
go.opentelemetry.io/otel v1.38.0 // indirect
7179
go.opentelemetry.io/otel/metric v1.38.0 // indirect
7280
go.opentelemetry.io/otel/trace v1.38.0 // indirect
81+
go.yaml.in/yaml/v3 v3.0.4 // indirect
7382
golang.org/x/sync v0.18.0 // indirect
7483
golang.org/x/text v0.31.0 // indirect
7584
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,15 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
5555
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
5656
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
5757
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
58+
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
59+
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
5860
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
5961
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
6062
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
6163
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
6264
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
65+
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
66+
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
6367
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
6468
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
6569
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
@@ -76,6 +80,14 @@ github.com/kernel/hypeman-go v0.9.8 h1:DGx3em3Bzu/MR3mgVgu7sCe8NZxujlEUGVctnrzop
7680
github.com/kernel/hypeman-go v0.9.8/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
7781
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
7882
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
83+
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
84+
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
85+
github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4=
86+
github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg=
87+
github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM=
88+
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
89+
github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4=
90+
github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
7991
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
8092
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
8193
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
89101
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
90102
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
91103
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
104+
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
105+
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
92106
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
93107
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
108+
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
109+
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
94110
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
95111
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
96112
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
169185
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
170186
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
171187
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
188+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
189+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
172190
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
173191
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
174192
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=

pkg/cmd/cmdutil.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,24 @@ func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption {
2727
option.WithHeader("User-Agent", fmt.Sprintf("Hypeman/CLI %s", Version)),
2828
}
2929

30-
// Override base URL if the --base-url flag is provided
30+
// Load config file for fallback values
31+
cfg := loadCLIConfig()
32+
33+
// Precedence for base URL: CLI flag > env var > config file
3134
if baseURL := cmd.String("base-url"); baseURL != "" {
3235
opts = append(opts, option.WithBaseURL(baseURL))
36+
} else if baseURL := os.Getenv("HYPEMAN_BASE_URL"); baseURL != "" {
37+
opts = append(opts, option.WithBaseURL(baseURL))
38+
} else if cfg.BaseURL != "" {
39+
opts = append(opts, option.WithBaseURL(cfg.BaseURL))
40+
}
41+
42+
// Precedence for API key: env var > config file
43+
// (no CLI flag for API key for security reasons)
44+
if apiKey := os.Getenv("HYPEMAN_API_KEY"); apiKey != "" {
45+
opts = append(opts, option.WithAPIKey(apiKey))
46+
} else if cfg.APIKey != "" {
47+
opts = append(opts, option.WithAPIKey(cfg.APIKey))
3348
}
3449

3550
return opts

pkg/cmd/config.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/knadh/koanf/parsers/yaml"
8+
"github.com/knadh/koanf/providers/file"
9+
"github.com/knadh/koanf/v2"
10+
)
11+
12+
// CLIConfig holds CLI configuration loaded from cli.yaml
13+
type CLIConfig struct {
14+
BaseURL string `koanf:"base_url"`
15+
APIKey string `koanf:"api_key"`
16+
}
17+
18+
// getCLIConfigPath returns the path to the CLI config file.
19+
// The CLI uses ~/.config/hypeman/cli.yaml on all platforms.
20+
func getCLIConfigPath() string {
21+
home, err := os.UserHomeDir()
22+
if err != nil {
23+
return ""
24+
}
25+
return filepath.Join(home, ".config", "hypeman", "cli.yaml")
26+
}
27+
28+
// loadCLIConfig loads CLI configuration from the config file.
29+
// Returns an empty config if the file doesn't exist or can't be parsed.
30+
func loadCLIConfig() *CLIConfig {
31+
cfg := &CLIConfig{}
32+
33+
configPath := getCLIConfigPath()
34+
if configPath == "" {
35+
return cfg
36+
}
37+
38+
k := koanf.New(".")
39+
if err := k.Load(file.Provider(configPath), yaml.Parser()); err != nil {
40+
// File doesn't exist or can't be parsed - return empty config
41+
return cfg
42+
}
43+
44+
_ = k.Unmarshal("", cfg)
45+
return cfg
46+
}

0 commit comments

Comments
 (0)