summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-10 04:36:19 +0000
committerClaudomator Agent <agent@claudomator>2026-03-10 04:36:19 +0000
commit8873187921c55d94be56364bf0b9d6b2d12127c2 (patch)
treecc277db491104ebec5645e1a0422be9d5d4cafd2
parent7d6943c5f9f4124c652377148a35bea5f61be4bf (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.go19
-rw-r--r--internal/cli/root_test.go69
-rw-r--r--internal/config/config.go15
-rw-r--r--internal/config/config_test.go31
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")
+ }
+}