diff options
| author | Claudomator Agent <agent@claudomator> | 2026-03-10 04:36:19 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator> | 2026-03-10 04:36:19 +0000 |
| commit | 8873187921c55d94be56364bf0b9d6b2d12127c2 (patch) | |
| tree | cc277db491104ebec5645e1a0422be9d5d4cafd2 | |
| parent | 7d6943c5f9f4124c652377148a35bea5f61be4bf (diff) | |
cli: implement --config flag to load TOML config file
The --config flag was registered but silently ignored. Now:
- config.LoadFile loads a TOML file on top of defaults
- PersistentPreRunE applies the file when --config is set
- Explicit CLI flags (--data-dir, --claude-bin) take precedence over the file
Tests: TestLoadFile_OverridesDefaults, TestLoadFile_MissingFile_ReturnsError,
TestRootCmd_ConfigFile_Loaded, TestRootCmd_ConfigFile_CLIFlagOverrides,
TestRootCmd_ConfigFile_Missing_ReturnsError
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/cli/root.go | 19 | ||||
| -rw-r--r-- | internal/cli/root_test.go | 69 | ||||
| -rw-r--r-- | internal/config/config.go | 15 | ||||
| -rw-r--r-- | internal/config/config_test.go | 31 |
4 files changed, 134 insertions, 0 deletions
diff --git a/internal/cli/root.go b/internal/cli/root.go index ab6ac1f..7c4f2ff 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -38,7 +38,26 @@ func NewRootCmd() *cobra.Command { cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") // Re-derive DBPath and LogDir after flags are parsed, so --data-dir takes effect. + // If --config is provided, load that file first; explicit CLI flags override it. cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if cfgFile != "" { + // Save values set by explicit CLI flags before overwriting cfg from file. + flagDataDir := cfg.DataDir + flagClaudeBin := cfg.ClaudeBinaryPath + + loaded, err := config.LoadFile(cfgFile) + if err != nil { + return err + } + *cfg = *loaded + + if cmd.Flags().Changed("data-dir") { + cfg.DataDir = flagDataDir + } + if cmd.Flags().Changed("claude-bin") { + cfg.ClaudeBinaryPath = flagClaudeBin + } + } cfg.DBPath = filepath.Join(cfg.DataDir, "claudomator.db") cfg.LogDir = filepath.Join(cfg.DataDir, "executions") return nil diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 0000000..4603d3b --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,69 @@ +package cli + +import ( + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" +) + +// noopCmd is a harmless subcommand used to trigger PersistentPreRunE. +func noopCmd() *cobra.Command { + return &cobra.Command{ + Use: "noop", + RunE: func(cmd *cobra.Command, args []string) error { return nil }, + } +} + +func TestRootCmd_ConfigFile_Loaded(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.toml") + if err := os.WriteFile(cfgPath, []byte("server_addr = \":9191\"\n"), 0600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", dir) + + cmd := NewRootCmd() + cmd.AddCommand(noopCmd()) + cmd.SetArgs([]string{"--config", cfgPath, "noop"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute: %v", err) + } + + if cfg.ServerAddr != ":9191" { + t.Errorf("ServerAddr = %q, want :9191", cfg.ServerAddr) + } +} + +func TestRootCmd_ConfigFile_CLIFlagOverrides(t *testing.T) { + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.toml") + // Config file sets data_dir, but --data-dir flag should win. + if err := os.WriteFile(cfgPath, []byte("data_dir = \"/from/file\"\n"), 0600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", dir) + + cmd := NewRootCmd() + cmd.AddCommand(noopCmd()) + cmd.SetArgs([]string{"--config", cfgPath, "--data-dir", "/from/flag", "noop"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute: %v", err) + } + + if cfg.DataDir != "/from/flag" { + t.Errorf("DataDir = %q, want /from/flag", cfg.DataDir) + } +} + +func TestRootCmd_ConfigFile_Missing_ReturnsError(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + cmd := NewRootCmd() + cmd.AddCommand(noopCmd()) + cmd.SetArgs([]string{"--config", "/nonexistent/config.toml", "noop"}) + if err := cmd.Execute(); err == nil { + t.Fatal("expected error for missing config file, got nil") + } +} diff --git a/internal/config/config.go b/internal/config/config.go index daf42fe..8c5aebf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "path/filepath" + + "github.com/BurntSushi/toml" ) type Config struct { @@ -42,6 +44,19 @@ func Default() (*Config, error) { }, nil } +// LoadFile loads a TOML config file on top of the defaults. +// Fields not present in the file retain their default values. +func LoadFile(path string) (*Config, error) { + cfg, err := Default() + if err != nil { + return nil, err + } + if _, err := toml.DecodeFile(path, cfg); err != nil { + return nil, fmt.Errorf("loading config file %q: %w", path, err) + } + return cfg, nil +} + // EnsureDirs creates the data directory structure. func (c *Config) EnsureDirs() error { for _, dir := range []string{c.DataDir, c.LogDir} { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 766b856..2bba2c4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,6 +1,8 @@ package config import ( + "os" + "path/filepath" "testing" ) @@ -22,3 +24,32 @@ func TestDefault_ValidHome_ReturnsConfig(t *testing.T) { t.Errorf("DataDir = %q, want /tmp/testhome/.claudomator", cfg.DataDir) } } + +func TestLoadFile_OverridesDefaults(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.toml") + if err := os.WriteFile(path, []byte("server_addr = \":9191\"\n"), 0600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", dir) + + cfg, err := LoadFile(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.ServerAddr != ":9191" { + t.Errorf("ServerAddr = %q, want :9191", cfg.ServerAddr) + } + // Unset fields retain defaults. + if cfg.MaxConcurrent != 3 { + t.Errorf("MaxConcurrent = %d, want 3", cfg.MaxConcurrent) + } +} + +func TestLoadFile_MissingFile_ReturnsError(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + _, err := LoadFile("/nonexistent/config.toml") + if err == nil { + t.Fatal("expected error for missing file, got nil") + } +} |
