diff --git a/README.md b/README.md index e7bb9f6..12238d1 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,9 @@ This creates two binaries in `bin/`: | `map down [-f]` | Stop the daemon (force immediate shutdown with -f) | | `map clean` | Clean up orphaned processes, tmux sessions, and socket files | | `map watch` | Stream real-time events from the daemon | +| `map config list` | List all configuration values | +| `map config get ` | Get a configuration value | +| `map config set ` | Set a configuration value | ### Agent Management @@ -442,11 +445,64 @@ mapcli/ ## Configuration +MAP supports persistent configuration via a YAML file at `~/.mapd/config.yaml`. Configuration values can be set via: + +1. **CLI flags** (highest priority) +2. **Environment variables** (with `MAP_` prefix) +3. **Config file** (`~/.mapd/config.yaml`) +4. **Defaults** (lowest priority) + +### Config Commands + +| Command | Description | +|---------|-------------| +| `map config list` | List all configuration values (alias: `ls`) | +| `map config get ` | Get a specific configuration value | +| `map config set ` | Set and persist a configuration value | + +### Configuration File + +```yaml +# ~/.mapd/config.yaml +socket: /tmp/mapd.sock +data-dir: ~/.mapd + +agent: + default-type: claude # claude or codex + default-count: 1 # number of agents to spawn + default-branch: "" # git branch for worktrees + use-worktree: true # worktree isolation + skip-permissions: true # skip permission prompts +``` + +### Configuration Options + +| Key | Default | Description | +|-----|---------|-------------| +| `socket` | `/tmp/mapd.sock` | Unix socket path for daemon communication | +| `data-dir` | `~/.mapd` | Data directory for SQLite and worktrees | +| `agent.default-type` | `claude` | Default agent type (`claude` or `codex`) | +| `agent.default-count` | `1` | Default number of agents to spawn | +| `agent.default-branch` | `""` | Default git branch for worktrees (empty = current branch) | +| `agent.use-worktree` | `true` | Use worktree isolation by default | +| `agent.skip-permissions` | `true` | Skip permission prompts by default | + +### Environment Variables + +All configuration options can be set via environment variables with the `MAP_` prefix. Nested keys use underscores: + +```bash +export MAP_SOCKET=/custom/path.sock +export MAP_AGENT_DEFAULT_TYPE=codex +export MAP_AGENT_DEFAULT_COUNT=3 +``` + ### Global CLI Flags | Flag | Default | Description | |------|---------|-------------| | `-s, --socket` | `/tmp/mapd.sock` | Unix socket path for daemon communication | +| `--config` | `~/.mapd/config.yaml` | Path to config file | ### Daemon (`map up`) @@ -515,6 +571,7 @@ Core runtime dependencies: | Package | Purpose | |---------|---------| | `github.com/spf13/cobra` | CLI framework | +| `github.com/spf13/viper` | Configuration management | | `google.golang.org/grpc` | gRPC communication | | `google.golang.org/protobuf` | Protocol buffer support | | `modernc.org/sqlite` | SQLite database (pure Go) | diff --git a/go.mod b/go.mod index 85f81e1..205c704 100644 --- a/go.mod +++ b/go.mod @@ -12,16 +12,33 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.17.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.55.3 // indirect modernc.org/mathutil v1.6.0 // indirect diff --git a/go.sum b/go.sum index cd0fd98..ed53548 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= @@ -9,21 +13,56 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= @@ -44,6 +83,10 @@ google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFN google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= diff --git a/internal/cli/agent_merge.go b/internal/cli/agent_merge.go index cdb1f8b..0486117 100644 --- a/internal/cli/agent_merge.go +++ b/internal/cli/agent_merge.go @@ -46,7 +46,7 @@ func runAgentMerge(cmd *cobra.Command, args []string) error { agentID := args[0] // Connect to daemon to get agent info - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/agent_watch.go b/internal/cli/agent_watch.go index 014ae1b..9d86ae2 100644 --- a/internal/cli/agent_watch.go +++ b/internal/cli/agent_watch.go @@ -45,7 +45,7 @@ func runAgentWatch(cmd *cobra.Command, args []string) error { return fmt.Errorf("tmux not found in PATH - required for agent watch") } - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/agents.go b/internal/cli/agents.go index 74b353e..2a1ee4c 100644 --- a/internal/cli/agents.go +++ b/internal/cli/agents.go @@ -23,7 +23,7 @@ func init() { } func runAgents(cmd *cobra.Command, args []string) error { - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/clean.go b/internal/cli/clean.go index e762904..78c3496 100644 --- a/internal/cli/clean.go +++ b/internal/cli/clean.go @@ -49,11 +49,11 @@ func runClean(cmd *cobra.Command, args []string) error { } // 3. Remove socket file if it exists - if _, err := os.Stat(socketPath); err == nil { - if err := os.Remove(socketPath); err != nil { - fmt.Printf("warning: failed to remove socket %s: %v\n", socketPath, err) + if _, err := os.Stat(getSocketPath()); err == nil { + if err := os.Remove(getSocketPath()); err != nil { + fmt.Printf("warning: failed to remove socket %s: %v\n", getSocketPath(), err) } else { - fmt.Printf("removed socket %s\n", socketPath) + fmt.Printf("removed socket %s\n", getSocketPath()) cleaned = true } } diff --git a/internal/cli/config.go b/internal/cli/config.go new file mode 100644 index 0000000..6da8d9d --- /dev/null +++ b/internal/cli/config.go @@ -0,0 +1,189 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +// configCmd is the parent command for config management +var configCmd = &cobra.Command{ + Use: "config", + Short: "Manage configuration settings", + Long: `View and modify MAP configuration settings stored in ~/.mapd/config.yaml.`, +} + +var configListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List all configuration values", + Long: `Display all configuration values including defaults and overrides.`, + RunE: runConfigList, +} + +var configGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get a configuration value", + Long: `Get a specific configuration value by key. + +Examples: + map config get socket + map config get agent.default-type + map config get agent.default-count`, + Args: cobra.ExactArgs(1), + RunE: runConfigGet, +} + +var configSetCmd = &cobra.Command{ + Use: "set ", + Short: "Set a configuration value", + Long: `Set a configuration value and persist it to ~/.mapd/config.yaml. + +Examples: + map config set socket /custom/path.sock + map config set agent.default-type codex + map config set agent.default-count 3 + map config set agent.use-worktree false`, + Args: cobra.ExactArgs(2), + RunE: runConfigSet, +} + +func init() { + rootCmd.AddCommand(configCmd) + configCmd.AddCommand(configListCmd) + configCmd.AddCommand(configGetCmd) + configCmd.AddCommand(configSetCmd) +} + +// initConfig reads in config file and ENV variables if set +func initConfig() error { + // Set defaults + viper.SetDefault("socket", "/tmp/mapd.sock") + viper.SetDefault("data-dir", filepath.Join(os.Getenv("HOME"), ".mapd")) + viper.SetDefault("agent.default-type", "claude") + viper.SetDefault("agent.default-count", 1) + viper.SetDefault("agent.default-branch", "") + viper.SetDefault("agent.use-worktree", true) + viper.SetDefault("agent.skip-permissions", true) + + if cfgFile != "" { + // Use config file from the flag + viper.SetConfigFile(cfgFile) + } else { + // Search for config in ~/.mapd directory + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("get home directory: %w", err) + } + + configDir := filepath.Join(home, ".mapd") + viper.AddConfigPath(configDir) + viper.SetConfigType("yaml") + viper.SetConfigName("config") + } + + // Environment variables with MAP_ prefix + viper.SetEnvPrefix("MAP") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + viper.AutomaticEnv() + + // Read config file (ignore if not found) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return fmt.Errorf("read config: %w", err) + } + } + + // Bind the socket flag to viper + if err := viper.BindPFlag("socket", rootCmd.PersistentFlags().Lookup("socket")); err != nil { + return fmt.Errorf("bind socket flag: %w", err) + } + + return nil +} + +// writeConfig writes the current configuration to the config file +func writeConfig() error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("get home directory: %w", err) + } + + configDir := filepath.Join(home, ".mapd") + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("create config directory: %w", err) + } + + configPath := filepath.Join(configDir, "config.yaml") + if err := viper.WriteConfigAs(configPath); err != nil { + return fmt.Errorf("write config: %w", err) + } + + return nil +} + +func runConfigList(cmd *cobra.Command, args []string) error { + keys := viper.AllKeys() + sort.Strings(keys) + + fmt.Printf("%-25s %s\n", "KEY", "VALUE") + fmt.Println(strings.Repeat("-", 50)) + + for _, key := range keys { + value := viper.Get(key) + fmt.Printf("%-25s %v\n", key, value) + } + + // Show config file location if it exists + if viper.ConfigFileUsed() != "" { + fmt.Printf("\nConfig file: %s\n", viper.ConfigFileUsed()) + } + + return nil +} + +func runConfigGet(cmd *cobra.Command, args []string) error { + key := args[0] + + if !viper.IsSet(key) { + return fmt.Errorf("key %q not found", key) + } + + fmt.Println(viper.Get(key)) + return nil +} + +func runConfigSet(cmd *cobra.Command, args []string) error { + key := args[0] + value := args[1] + + // Handle boolean values + switch strings.ToLower(value) { + case "true": + viper.Set(key, true) + case "false": + viper.Set(key, false) + default: + // Try to parse as integer + var intVal int + if _, err := fmt.Sscanf(value, "%d", &intVal); err == nil { + viper.Set(key, intVal) + } else { + viper.Set(key, value) + } + } + + if err := writeConfig(); err != nil { + return err + } + + fmt.Printf("set %s = %v\n", key, viper.Get(key)) + return nil +} diff --git a/internal/cli/down.go b/internal/cli/down.go index 2b85701..a09da11 100644 --- a/internal/cli/down.go +++ b/internal/cli/down.go @@ -24,12 +24,12 @@ func init() { } func runDown(cmd *cobra.Command, args []string) error { - if !client.IsDaemonRunning(socketPath) { + if !client.IsDaemonRunning(getSocketPath()) { fmt.Println("daemon is not running") return nil } - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/root.go b/internal/cli/root.go index 149d3b1..71fc2de 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -5,13 +5,12 @@ import ( "os" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // Version is set via -ldflags at build time var Version = "dev" -var socketPath string - // rootCmd is the base command var rootCmd = &cobra.Command{ Use: "map", @@ -28,6 +27,16 @@ func Execute() { } } +// getSocketPath returns the socket path from Viper (flag > env > config > default) +func getSocketPath() string { + return viper.GetString("socket") +} + func init() { - rootCmd.PersistentFlags().StringVarP(&socketPath, "socket", "s", "/tmp/mapd.sock", "daemon socket path") + rootCmd.PersistentFlags().StringP("socket", "s", "/tmp/mapd.sock", "daemon socket path") + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: ~/.mapd/config.yaml)") + + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + return initConfig() + } } diff --git a/internal/cli/spawn.go b/internal/cli/spawn.go index ae88061..ce4e006 100644 --- a/internal/cli/spawn.go +++ b/internal/cli/spawn.go @@ -9,6 +9,7 @@ import ( "github.com/pmarsceill/mapcli/internal/client" mapv1 "github.com/pmarsceill/mapcli/proto/map/v1" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var agentCmd = &cobra.Command{ @@ -84,20 +85,42 @@ func init() { } func runAgentCreate(cmd *cobra.Command, args []string) error { - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } defer func() { _ = c.Close() }() + // Get flag values, using Viper defaults when flags aren't explicitly set count, _ := cmd.Flags().GetInt("count") + if !cmd.Flags().Changed("count") { + count = viper.GetInt("agent.default-count") + } + branch, _ := cmd.Flags().GetString("branch") + if !cmd.Flags().Changed("branch") { + branch = viper.GetString("agent.default-branch") + } + noWorktree, _ := cmd.Flags().GetBool("no-worktree") worktree, _ := cmd.Flags().GetBool("worktree") + if !cmd.Flags().Changed("worktree") && !cmd.Flags().Changed("no-worktree") { + worktree = viper.GetBool("agent.use-worktree") + } + name, _ := cmd.Flags().GetString("name") prompt, _ := cmd.Flags().GetString("prompt") + agentType, _ := cmd.Flags().GetString("agent-type") + if !cmd.Flags().Changed("agent-type") { + agentType = viper.GetString("agent.default-type") + } + requirePermissions, _ := cmd.Flags().GetBool("require-permissions") + skipPermissions := !requirePermissions + if !cmd.Flags().Changed("require-permissions") { + skipPermissions = viper.GetBool("agent.skip-permissions") + } // Validate agent type if agentType != "claude" && agentType != "codex" { @@ -107,9 +130,6 @@ func runAgentCreate(cmd *cobra.Command, args []string) error { // no-worktree overrides worktree useWorktree := worktree && !noWorktree - // Skip permissions by default for autonomous operation (inverted from require-permissions flag) - skipPermissions := !requirePermissions - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() @@ -157,7 +177,7 @@ func runAgentCreate(cmd *cobra.Command, args []string) error { } func runAgentList(cmd *cobra.Command, args []string) error { - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } @@ -194,7 +214,7 @@ func runAgentKill(cmd *cobra.Command, args []string) error { force, _ := cmd.Flags().GetBool("force") killAll, _ := cmd.Flags().GetBool("all") - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } @@ -269,7 +289,7 @@ func runAgentKill(cmd *cobra.Command, args []string) error { func runAgentRespawn(cmd *cobra.Command, args []string) error { agentID := args[0] - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/task.go b/internal/cli/task.go index 7cce19d..da16a9d 100644 --- a/internal/cli/task.go +++ b/internal/cli/task.go @@ -70,7 +70,7 @@ func init() { func runTaskSubmit(cmd *cobra.Command, args []string) error { description := strings.Join(args, " ") - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } @@ -89,7 +89,7 @@ func runTaskSubmit(cmd *cobra.Command, args []string) error { } func runTaskList(cmd *cobra.Command, args []string) error { - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } @@ -130,7 +130,7 @@ func runTaskList(cmd *cobra.Command, args []string) error { func runTaskShow(cmd *cobra.Command, args []string) error { taskID := args[0] - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } @@ -167,7 +167,7 @@ func runTaskShow(cmd *cobra.Command, args []string) error { func runTaskCancel(cmd *cobra.Command, args []string) error { taskID := args[0] - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/task_sync.go b/internal/cli/task_sync.go index 68338d2..9c86d6b 100644 --- a/internal/cli/task_sync.go +++ b/internal/cli/task_sync.go @@ -173,7 +173,7 @@ func runTaskSyncGHProject(cmd *cobra.Command, args []string) error { } // Connect to daemon - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/up.go b/internal/cli/up.go index 1e191d8..15d2af2 100644 --- a/internal/cli/up.go +++ b/internal/cli/up.go @@ -32,7 +32,7 @@ func init() { func runUp(cmd *cobra.Command, args []string) error { // Check if already running - if client.IsDaemonRunning(socketPath) { + if client.IsDaemonRunning(getSocketPath()) { fmt.Println("daemon is already running") return nil } @@ -46,7 +46,7 @@ func runUp(cmd *cobra.Command, args []string) error { func runForeground() error { cfg := &daemon.Config{ - SocketPath: socketPath, + SocketPath: getSocketPath(), DataDir: dataDir, } @@ -76,7 +76,7 @@ func runBackground() error { return fmt.Errorf("get executable: %w", err) } - args := []string{"up", "-f", "-s", socketPath} + args := []string{"up", "-f", "-s", getSocketPath()} if dataDir != "" { args = append(args, "-d", dataDir) } diff --git a/internal/cli/watch.go b/internal/cli/watch.go index 89529fb..92dd32d 100644 --- a/internal/cli/watch.go +++ b/internal/cli/watch.go @@ -26,7 +26,7 @@ func init() { } func runWatch(cmd *cobra.Command, args []string) error { - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } diff --git a/internal/cli/worktree.go b/internal/cli/worktree.go index 51f1861..619fadb 100644 --- a/internal/cli/worktree.go +++ b/internal/cli/worktree.go @@ -44,7 +44,7 @@ func init() { } func runWorktreeLs(cmd *cobra.Command, args []string) error { - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) } @@ -85,7 +85,7 @@ func runWorktreeCleanup(cmd *cobra.Command, args []string) error { agentID, _ := cmd.Flags().GetString("agent") all, _ := cmd.Flags().GetBool("all") - c, err := client.New(socketPath) + c, err := client.New(getSocketPath()) if err != nil { return fmt.Errorf("connect to daemon: %w", err) }