// Package cmd provides CLI commands for the gt tool. package cmd import ( "encoding/json" "fmt" "os" "sort" "strings" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) var configCmd = &cobra.Command{ Use: "config", GroupID: GroupConfig, Short: "Manage Gas Town configuration", RunE: requireSubcommand, Long: `Manage Gas Town configuration settings. This command allows you to view and modify configuration settings for your Gas Town workspace, including agent aliases and defaults. Commands: gt config agent list List all agents (built-in and custom) gt config agent get Show agent configuration gt config agent set Set custom agent command gt config agent remove Remove custom agent gt config default-agent [name] Get or set default agent`, } // Agent subcommands var configAgentListCmd = &cobra.Command{ Use: "list", Short: "List all agents", Long: `List all available agents (built-in and custom). Shows all built-in agent presets (claude, gemini, codex) and any custom agents defined in your town settings. Examples: gt config agent list # Text output gt config agent list --json # JSON output`, RunE: runConfigAgentList, } var configAgentGetCmd = &cobra.Command{ Use: "get ", Short: "Show agent configuration", Long: `Show the configuration for a specific agent. Displays the full configuration for an agent, including command, arguments, and other settings. Works for both built-in and custom agents. Examples: gt config agent get claude gt config agent get my-custom-agent`, Args: cobra.ExactArgs(1), RunE: runConfigAgentGet, } var configAgentSetCmd = &cobra.Command{ Use: "set ", Short: "Set custom agent command", Long: `Set a custom agent command in town settings. This creates or updates a custom agent definition that overrides or extends the built-in presets. The custom agent will be available to all rigs in the town. The command can include arguments. Use quotes if the command or arguments contain spaces. Examples: gt config agent set claude-glm \"claude-glm --model glm-4\" gt config agent set gemini-custom gemini --approval-mode yolo gt config agent set claude \"claude-glm\" # Override built-in claude`, Args: cobra.ExactArgs(2), RunE: runConfigAgentSet, } var configAgentRemoveCmd = &cobra.Command{ Use: "remove ", Short: "Remove custom agent", Long: `Remove a custom agent definition from town settings. This removes a custom agent from your town settings. Built-in agents (claude, gemini, codex) cannot be removed. Examples: gt config agent remove claude-glm`, Args: cobra.ExactArgs(1), RunE: runConfigAgentRemove, } // Default-agent subcommand var configDefaultAgentCmd = &cobra.Command{ Use: "default-agent [name]", Short: "Get or set default agent", Long: `Get or set the default agent for the town. With no arguments, shows the current default agent. With an argument, sets the default agent to the specified name. The default agent is used when a rig doesn't specify its own agent setting. Can be a built-in preset (claude, gemini, codex) or a custom agent name. Examples: gt config default-agent # Show current default gt config default-agent claude # Set to claude gt config default-agent gemini # Set to gemini gt config default-agent my-custom # Set to custom agent`, RunE: runConfigDefaultAgent, } // Flags var ( configAgentListJSON bool ) // AgentListItem represents an agent in list output. type AgentListItem struct { Name string `json:"name"` Command string `json:"command"` Args string `json:"args,omitempty"` Type string `json:"type"` // "built-in" or "custom" IsCustom bool `json:"is_custom"` } func runConfigAgentList(cmd *cobra.Command, args []string) error { townRoot, err := workspace.FindFromCwd() if err != nil { return fmt.Errorf("finding town root: %w", err) } // Load town settings settingsPath := config.TownSettingsPath(townRoot) townSettings, err := config.LoadOrCreateTownSettings(settingsPath) if err != nil { return fmt.Errorf("loading town settings: %w", err) } // Load agent registry registryPath := config.DefaultAgentRegistryPath(townRoot) if err := config.LoadAgentRegistry(registryPath); err != nil { return fmt.Errorf("loading agent registry: %w", err) } // Collect all agents builtInAgents := config.ListAgentPresets() customAgents := make(map[string]*config.RuntimeConfig) if townSettings.Agents != nil { for name, runtime := range townSettings.Agents { customAgents[name] = runtime } } // Build list items var items []AgentListItem for _, name := range builtInAgents { preset := config.GetAgentPresetByName(name) if preset != nil { items = append(items, AgentListItem{ Name: name, Command: preset.Command, Args: strings.Join(preset.Args, " "), Type: "built-in", IsCustom: false, }) } } for name, runtime := range customAgents { argsStr := "" if runtime.Args != nil { argsStr = strings.Join(runtime.Args, " ") } items = append(items, AgentListItem{ Name: name, Command: runtime.Command, Args: argsStr, Type: "custom", IsCustom: true, }) } // Sort by name sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name }) if configAgentListJSON { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(items) } // Text output fmt.Printf("%s\n\n", style.Bold.Render("Available Agents")) for _, item := range items { typeLabel := style.Dim.Render("[" + item.Type + "]") fmt.Printf(" %s %s %s", style.Bold.Render(item.Name), typeLabel, item.Command) if item.Args != "" { fmt.Printf(" %s", item.Args) } fmt.Println() } // Show default defaultAgent := townSettings.DefaultAgent if defaultAgent == "" { defaultAgent = "claude" } fmt.Printf("\nDefault: %s\n", style.Bold.Render(defaultAgent)) return nil } func runConfigAgentGet(cmd *cobra.Command, args []string) error { name := args[0] townRoot, err := workspace.FindFromCwd() if err != nil { return fmt.Errorf("finding town root: %w", err) } // Load town settings for custom agents settingsPath := config.TownSettingsPath(townRoot) townSettings, err := config.LoadOrCreateTownSettings(settingsPath) if err != nil { return fmt.Errorf("loading town settings: %w", err) } // Load agent registry registryPath := config.DefaultAgentRegistryPath(townRoot) if err := config.LoadAgentRegistry(registryPath); err != nil { return fmt.Errorf("loading agent registry: %w", err) } // Check custom agents first if townSettings.Agents != nil { if runtime, ok := townSettings.Agents[name]; ok { displayAgentConfig(name, runtime, nil, true) return nil } } // Check built-in agents preset := config.GetAgentPresetByName(name) if preset != nil { runtime := &config.RuntimeConfig{ Command: preset.Command, Args: preset.Args, } displayAgentConfig(name, runtime, preset, false) return nil } return fmt.Errorf("agent '%s' not found", name) } func displayAgentConfig(name string, runtime *config.RuntimeConfig, preset *config.AgentPresetInfo, isCustom bool) { fmt.Printf("%s\n\n", style.Bold.Render("Agent: "+name)) typeLabel := "custom" if !isCustom { typeLabel = "built-in" } fmt.Printf("Type: %s\n", typeLabel) fmt.Printf("Command: %s\n", runtime.Command) if runtime.Args != nil && len(runtime.Args) > 0 { fmt.Printf("Args: %s\n", strings.Join(runtime.Args, " ")) } if preset != nil { if preset.SessionIDEnv != "" { fmt.Printf("Session ID Env: %s\n", preset.SessionIDEnv) } if preset.ResumeFlag != "" { fmt.Printf("Resume Style: %s (%s)\n", preset.ResumeStyle, preset.ResumeFlag) } fmt.Printf("Supports Hooks: %v\n", preset.SupportsHooks) } } func runConfigAgentSet(cmd *cobra.Command, args []string) error { name := args[0] commandLine := args[1] townRoot, err := workspace.FindFromCwd() if err != nil { return fmt.Errorf("finding town root: %w", err) } // Load town settings settingsPath := config.TownSettingsPath(townRoot) townSettings, err := config.LoadOrCreateTownSettings(settingsPath) if err != nil { return fmt.Errorf("loading town settings: %w", err) } // Parse command line into command and args parts := strings.Fields(commandLine) if len(parts) == 0 { return fmt.Errorf("command cannot be empty") } // Initialize agents map if needed if townSettings.Agents == nil { townSettings.Agents = make(map[string]*config.RuntimeConfig) } // Create or update the agent townSettings.Agents[name] = &config.RuntimeConfig{ Command: parts[0], Args: parts[1:], } // Save settings if err := config.SaveTownSettings(settingsPath, townSettings); err != nil { return fmt.Errorf("saving town settings: %w", err) } fmt.Printf("Agent '%s' set to: %s\n", style.Bold.Render(name), commandLine) // Check if this overrides a built-in builtInAgents := config.ListAgentPresets() for _, builtin := range builtInAgents { if name == builtin { fmt.Printf("\n%s\n", style.Dim.Render("(overriding built-in '"+builtin+"' preset)")) break } } return nil } func runConfigAgentRemove(cmd *cobra.Command, args []string) error { name := args[0] townRoot, err := workspace.FindFromCwd() if err != nil { return fmt.Errorf("finding town root: %w", err) } // Check if trying to remove built-in builtInAgents := config.ListAgentPresets() for _, builtin := range builtInAgents { if name == builtin { return fmt.Errorf("cannot remove built-in agent '%s' (use 'gt config agent set' to override it)", name) } } // Load town settings settingsPath := config.TownSettingsPath(townRoot) townSettings, err := config.LoadOrCreateTownSettings(settingsPath) if err != nil { return fmt.Errorf("loading town settings: %w", err) } // Check if agent exists if townSettings.Agents == nil || townSettings.Agents[name] == nil { return fmt.Errorf("custom agent '%s' not found", name) } // Remove the agent delete(townSettings.Agents, name) // Save settings if err := config.SaveTownSettings(settingsPath, townSettings); err != nil { return fmt.Errorf("saving town settings: %w", err) } fmt.Printf("Removed custom agent '%s'\n", style.Bold.Render(name)) return nil } func runConfigDefaultAgent(cmd *cobra.Command, args []string) error { townRoot, err := workspace.FindFromCwd() if err != nil { return fmt.Errorf("finding town root: %w", err) } // Load town settings settingsPath := config.TownSettingsPath(townRoot) townSettings, err := config.LoadOrCreateTownSettings(settingsPath) if err != nil { return fmt.Errorf("loading town settings: %w", err) } // Load agent registry registryPath := config.DefaultAgentRegistryPath(townRoot) if err := config.LoadAgentRegistry(registryPath); err != nil { return fmt.Errorf("loading agent registry: %w", err) } if len(args) == 0 { // Show current default defaultAgent := townSettings.DefaultAgent if defaultAgent == "" { defaultAgent = "claude" } fmt.Printf("Default agent: %s\n", style.Bold.Render(defaultAgent)) return nil } // Set new default name := args[0] // Verify agent exists isValid := false builtInAgents := config.ListAgentPresets() for _, builtin := range builtInAgents { if name == builtin { isValid = true break } } if !isValid && townSettings.Agents != nil { if _, ok := townSettings.Agents[name]; ok { isValid = true } } if !isValid { return fmt.Errorf("agent '%s' not found (use 'gt config agent list' to see available agents)", name) } // Set default townSettings.DefaultAgent = name // Save settings if err := config.SaveTownSettings(settingsPath, townSettings); err != nil { return fmt.Errorf("saving town settings: %w", err) } fmt.Printf("Default agent set to '%s'\n", style.Bold.Render(name)) return nil } func init() { // Add flags configAgentListCmd.Flags().BoolVar(&configAgentListJSON, "json", false, "Output as JSON") // Add agent subcommands configAgentCmd := &cobra.Command{ Use: "agent", Short: "Manage agent configuration", RunE: requireSubcommand, } configAgentCmd.AddCommand(configAgentListCmd) configAgentCmd.AddCommand(configAgentGetCmd) configAgentCmd.AddCommand(configAgentSetCmd) configAgentCmd.AddCommand(configAgentRemoveCmd) // Add subcommands to config configCmd.AddCommand(configAgentCmd) configCmd.AddCommand(configDefaultAgentCmd) // Register with root rootCmd.AddCommand(configCmd) }