Merge pull request #129 from dlukt/main
feat: Add gt config command for managing agent settings
This commit is contained in:
16
README.md
16
README.md
@@ -235,6 +235,22 @@ gt <role> attach # Jump into any agent session
|
||||
# e.g., gt mayor attach, gt witness attach
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
gt config agent list [--json] # List all agents (built-in + custom)
|
||||
gt config agent get <name> # Show agent configuration
|
||||
gt config agent set <name> <cmd> # Create or update custom agent
|
||||
gt config agent remove <name> # Remove custom agent (built-ins protected)
|
||||
gt config default-agent [name] # Get or set town default agent
|
||||
```
|
||||
|
||||
**Example**: Use a cheaper model for most work:
|
||||
```bash
|
||||
gt config agent set claude-glm "claude-glm --model glm-4"
|
||||
gt config default-agent claude-glm
|
||||
```
|
||||
|
||||
Most other work happens through agents - just ask them.
|
||||
|
||||
### For Agents
|
||||
|
||||
@@ -215,6 +215,28 @@ gt doctor # Health check
|
||||
gt doctor --fix # Auto-repair
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Agent management
|
||||
gt config agent list [--json] # List all agents (built-in + custom)
|
||||
gt config agent get <name> # Show agent configuration
|
||||
gt config agent set <name> <cmd> # Create or update custom agent
|
||||
gt config agent remove <name> # Remove custom agent (built-ins protected)
|
||||
|
||||
# Default agent
|
||||
gt config default-agent [name] # Get or set town default agent
|
||||
```
|
||||
|
||||
**Built-in agents**: `claude`, `gemini`, `codex`
|
||||
|
||||
**Custom agents**: Define per-town in `mayor/town.json`:
|
||||
```bash
|
||||
gt config agent set claude-glm "claude-glm --model glm-4"
|
||||
gt config agent set claude "claude-opus" # Override built-in
|
||||
gt config default-agent claude-glm # Set default
|
||||
```
|
||||
|
||||
### Rig Management
|
||||
|
||||
```bash
|
||||
|
||||
468
internal/cmd/config.go
Normal file
468
internal/cmd/config.go
Normal file
@@ -0,0 +1,468 @@
|
||||
// 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 <name> Show agent configuration
|
||||
gt config agent set <name> <cmd> Set custom agent command
|
||||
gt config agent remove <name> 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 <name>",
|
||||
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 <name> <command>",
|
||||
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 <name>",
|
||||
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 := []string{"claude", "gemini", "codex"}
|
||||
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 := []string{"claude", "gemini", "codex"}
|
||||
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 := []string{"claude", "gemini", "codex"}
|
||||
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 := []string{"claude", "gemini", "codex"}
|
||||
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)
|
||||
}
|
||||
583
internal/cmd/config_test.go
Normal file
583
internal/cmd/config_test.go
Normal file
@@ -0,0 +1,583 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
)
|
||||
|
||||
// setupTestTown creates a minimal Gas Town workspace for testing.
|
||||
func setupTestTownForConfig(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
townRoot := t.TempDir()
|
||||
|
||||
// Create mayor directory with required files
|
||||
mayorDir := filepath.Join(townRoot, "mayor")
|
||||
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir mayor: %v", err)
|
||||
}
|
||||
|
||||
// Create town.json
|
||||
townConfig := &config.TownConfig{
|
||||
Type: "town",
|
||||
Version: config.CurrentTownVersion,
|
||||
Name: "test-town",
|
||||
PublicName: "Test Town",
|
||||
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
townConfigPath := filepath.Join(mayorDir, "town.json")
|
||||
if err := config.SaveTownConfig(townConfigPath, townConfig); err != nil {
|
||||
t.Fatalf("save town.json: %v", err)
|
||||
}
|
||||
|
||||
// Create empty rigs.json
|
||||
rigsConfig := &config.RigsConfig{
|
||||
Version: 1,
|
||||
Rigs: make(map[string]config.RigEntry),
|
||||
}
|
||||
rigsPath := filepath.Join(mayorDir, "rigs.json")
|
||||
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
|
||||
t.Fatalf("save rigs.json: %v", err)
|
||||
}
|
||||
|
||||
return townRoot
|
||||
}
|
||||
|
||||
func TestConfigAgentList(t *testing.T) {
|
||||
t.Run("lists built-in agents", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Change to town root so workspace.FindFromCwd works
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{}
|
||||
err := runConfigAgentList(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentList failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify settings file was created (LoadOrCreate creates it)
|
||||
if _, err := os.Stat(settingsPath); err != nil {
|
||||
// This is OK - list command works without settings file
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("lists built-in and custom agents", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Create settings with custom agent
|
||||
settings := &config.TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: config.CurrentTownSettingsVersion,
|
||||
DefaultAgent: "claude",
|
||||
Agents: map[string]*config.RuntimeConfig{
|
||||
"my-custom": {
|
||||
Command: "my-agent",
|
||||
Args: []string{"--flag"},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
||||
t.Fatalf("save settings: %v", err)
|
||||
}
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Load agent registry
|
||||
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||
t.Fatalf("load agent registry: %v", err)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{}
|
||||
err := runConfigAgentList(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentList failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JSON output", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Set JSON flag
|
||||
configAgentListJSON = true
|
||||
defer func() { configAgentListJSON = false }()
|
||||
|
||||
// Load agent registry
|
||||
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||
t.Fatalf("load agent registry: %v", err)
|
||||
}
|
||||
|
||||
// Capture output
|
||||
// Note: This test verifies the command runs without error
|
||||
// Full JSON validation would require capturing stdout
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{}
|
||||
err := runConfigAgentList(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentList failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigAgentGet(t *testing.T) {
|
||||
t.Run("gets built-in agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Load agent registry
|
||||
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||
t.Fatalf("load agent registry: %v", err)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"claude"}
|
||||
err := runConfigAgentGet(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentGet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gets custom agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Create settings with custom agent
|
||||
settings := &config.TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: config.CurrentTownSettingsVersion,
|
||||
DefaultAgent: "claude",
|
||||
Agents: map[string]*config.RuntimeConfig{
|
||||
"my-custom": {
|
||||
Command: "my-agent",
|
||||
Args: []string{"--flag1", "--flag2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
||||
t.Fatalf("save settings: %v", err)
|
||||
}
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Load agent registry
|
||||
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||
t.Fatalf("load agent registry: %v", err)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"my-custom"}
|
||||
err := runConfigAgentGet(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentGet failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns error for unknown agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Load agent registry
|
||||
registryPath := config.DefaultAgentRegistryPath(townRoot)
|
||||
if err := config.LoadAgentRegistry(registryPath); err != nil {
|
||||
t.Fatalf("load agent registry: %v", err)
|
||||
}
|
||||
|
||||
// Run the command with unknown agent
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"unknown-agent"}
|
||||
err := runConfigAgentGet(cmd, args)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown agent")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
t.Errorf("error = %v, want 'not found'", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigAgentSet(t *testing.T) {
|
||||
t.Run("sets custom agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"my-agent", "my-agent --arg1 --arg2"}
|
||||
err := runConfigAgentSet(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentSet failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify settings were saved
|
||||
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load settings: %v", err)
|
||||
}
|
||||
|
||||
if loaded.Agents == nil {
|
||||
t.Fatal("Agents map is nil")
|
||||
}
|
||||
agent, ok := loaded.Agents["my-agent"]
|
||||
if !ok {
|
||||
t.Fatal("custom agent not found in settings")
|
||||
}
|
||||
if agent.Command != "my-agent" {
|
||||
t.Errorf("Command = %q, want 'my-agent'", agent.Command)
|
||||
}
|
||||
if len(agent.Args) != 2 {
|
||||
t.Errorf("Args count = %d, want 2", len(agent.Args))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets agent with single command (no args)", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"simple-agent", "simple-agent"}
|
||||
err := runConfigAgentSet(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentSet failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify settings were saved
|
||||
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load settings: %v", err)
|
||||
}
|
||||
|
||||
agent := loaded.Agents["simple-agent"]
|
||||
if agent.Command != "simple-agent" {
|
||||
t.Errorf("Command = %q, want 'simple-agent'", agent.Command)
|
||||
}
|
||||
if len(agent.Args) != 0 {
|
||||
t.Errorf("Args count = %d, want 0", len(agent.Args))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("overrides existing agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Create initial settings
|
||||
settings := &config.TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: config.CurrentTownSettingsVersion,
|
||||
DefaultAgent: "claude",
|
||||
Agents: map[string]*config.RuntimeConfig{
|
||||
"my-agent": {
|
||||
Command: "old-command",
|
||||
Args: []string{"--old"},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
||||
t.Fatalf("save initial settings: %v", err)
|
||||
}
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Run the command to override
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"my-agent", "new-command --new"}
|
||||
err := runConfigAgentSet(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentSet failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify settings were updated
|
||||
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load settings: %v", err)
|
||||
}
|
||||
|
||||
agent := loaded.Agents["my-agent"]
|
||||
if agent.Command != "new-command" {
|
||||
t.Errorf("Command = %q, want 'new-command'", agent.Command)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigAgentRemove(t *testing.T) {
|
||||
t.Run("removes custom agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Create settings with custom agent
|
||||
settings := &config.TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: config.CurrentTownSettingsVersion,
|
||||
DefaultAgent: "claude",
|
||||
Agents: map[string]*config.RuntimeConfig{
|
||||
"my-agent": {
|
||||
Command: "my-agent",
|
||||
Args: []string{"--flag"},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
||||
t.Fatalf("save settings: %v", err)
|
||||
}
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"my-agent"}
|
||||
err := runConfigAgentRemove(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigAgentRemove failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify agent was removed
|
||||
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load settings: %v", err)
|
||||
}
|
||||
|
||||
if loaded.Agents != nil {
|
||||
if _, ok := loaded.Agents["my-agent"]; ok {
|
||||
t.Error("agent still exists after removal")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects removing built-in agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Try to remove a built-in agent
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"claude"}
|
||||
err := runConfigAgentRemove(cmd, args)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when removing built-in agent")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot remove built-in") {
|
||||
t.Errorf("error = %v, want 'cannot remove built-in'", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns error for non-existent custom agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Try to remove a non-existent agent
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"non-existent"}
|
||||
err := runConfigAgentRemove(cmd, args)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-existent agent")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
t.Errorf("error = %v, want 'not found'", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigDefaultAgent(t *testing.T) {
|
||||
t.Run("gets default agent (shows current)", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Run the command with no args (should show current default)
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{}
|
||||
err := runConfigDefaultAgent(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigDefaultAgent failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets default agent to built-in", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Set default to gemini
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"gemini"}
|
||||
err := runConfigDefaultAgent(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigDefaultAgent failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify settings were saved
|
||||
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load settings: %v", err)
|
||||
}
|
||||
|
||||
if loaded.DefaultAgent != "gemini" {
|
||||
t.Errorf("DefaultAgent = %q, want 'gemini'", loaded.DefaultAgent)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets default agent to custom", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
settingsPath := config.TownSettingsPath(townRoot)
|
||||
|
||||
// Create settings with custom agent
|
||||
settings := &config.TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: config.CurrentTownSettingsVersion,
|
||||
DefaultAgent: "claude",
|
||||
Agents: map[string]*config.RuntimeConfig{
|
||||
"my-custom": {
|
||||
Command: "my-agent",
|
||||
Args: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
|
||||
t.Fatalf("save settings: %v", err)
|
||||
}
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Set default to custom agent
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"my-custom"}
|
||||
err := runConfigDefaultAgent(cmd, args)
|
||||
if err != nil {
|
||||
t.Fatalf("runConfigDefaultAgent failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify settings were saved
|
||||
loaded, err := config.LoadOrCreateTownSettings(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load settings: %v", err)
|
||||
}
|
||||
|
||||
if loaded.DefaultAgent != "my-custom" {
|
||||
t.Errorf("DefaultAgent = %q, want 'my-custom'", loaded.DefaultAgent)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns error for unknown agent", func(t *testing.T) {
|
||||
townRoot := setupTestTownForConfig(t)
|
||||
|
||||
// Change to town root
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
if err := os.Chdir(townRoot); err != nil {
|
||||
t.Fatalf("chdir: %v", err)
|
||||
}
|
||||
|
||||
// Try to set default to unknown agent
|
||||
cmd := &cobra.Command{}
|
||||
args := []string{"unknown-agent"}
|
||||
err := runConfigDefaultAgent(cmd, args)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown agent")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
t.Errorf("error = %v, want 'not found'", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -691,6 +691,31 @@ func LoadOrCreateTownSettings(path string) (*TownSettings, error) {
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// SaveTownSettings saves town settings to a file.
|
||||
func SaveTownSettings(path string, settings *TownSettings) error {
|
||||
if settings.Type != "town-settings" && settings.Type != "" {
|
||||
return fmt.Errorf("%w: expected type 'town-settings', got '%s'", ErrInvalidType, settings.Type)
|
||||
}
|
||||
if settings.Version > CurrentTownSettingsVersion {
|
||||
return fmt.Errorf("%w: got %d, max supported %d", ErrInvalidVersion, settings.Version, CurrentTownSettingsVersion)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding settings: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: settings files don't contain secrets
|
||||
return fmt.Errorf("writing settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveAgentConfig resolves the agent configuration for a rig.
|
||||
// It looks up the agent by name in town settings (custom agents) and built-in presets.
|
||||
//
|
||||
@@ -783,8 +808,13 @@ func fillRuntimeDefaults(rc *RuntimeConfig) *RuntimeConfig {
|
||||
// for starting an LLM session. It resolves the agent config and builds the command.
|
||||
func GetRuntimeCommand(rigPath string) string {
|
||||
if rigPath == "" {
|
||||
// Try to detect town root from cwd for town-level agents (mayor, deacon)
|
||||
townRoot, err := findTownRootFromCwd()
|
||||
if err != nil {
|
||||
return DefaultRuntimeConfig().BuildCommand()
|
||||
}
|
||||
return ResolveAgentConfig(townRoot, "").BuildCommand()
|
||||
}
|
||||
// Derive town root from rig path (rig is typically ~/gt/<rigname>)
|
||||
townRoot := filepath.Dir(rigPath)
|
||||
return ResolveAgentConfig(townRoot, rigPath).BuildCommand()
|
||||
@@ -793,15 +823,50 @@ func GetRuntimeCommand(rigPath string) string {
|
||||
// GetRuntimeCommandWithPrompt returns the full command with an initial prompt.
|
||||
func GetRuntimeCommandWithPrompt(rigPath, prompt string) string {
|
||||
if rigPath == "" {
|
||||
// Try to detect town root from cwd for town-level agents (mayor, deacon)
|
||||
townRoot, err := findTownRootFromCwd()
|
||||
if err != nil {
|
||||
return DefaultRuntimeConfig().BuildCommandWithPrompt(prompt)
|
||||
}
|
||||
return ResolveAgentConfig(townRoot, "").BuildCommandWithPrompt(prompt)
|
||||
}
|
||||
townRoot := filepath.Dir(rigPath)
|
||||
return ResolveAgentConfig(townRoot, rigPath).BuildCommandWithPrompt(prompt)
|
||||
}
|
||||
|
||||
// findTownRootFromCwd locates the town root by walking up from cwd.
|
||||
// It looks for the mayor/town.json marker file.
|
||||
// Returns empty string and no error if not found (caller should use defaults).
|
||||
func findTownRootFromCwd() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting cwd: %w", err)
|
||||
}
|
||||
|
||||
absDir, err := filepath.Abs(cwd)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolving path: %w", err)
|
||||
}
|
||||
|
||||
const marker = "mayor/town.json"
|
||||
|
||||
current := absDir
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(current, marker)); err == nil {
|
||||
return current, nil
|
||||
}
|
||||
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
return "", fmt.Errorf("town root not found (no %s marker)", marker)
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
// BuildStartupCommand builds a full startup command with environment exports.
|
||||
// envVars is a map of environment variable names to values.
|
||||
// rigPath is optional - if empty, uses defaults.
|
||||
// rigPath is optional - if empty, tries to detect town root from cwd.
|
||||
// prompt is optional - if provided, appended as the initial prompt.
|
||||
func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) string {
|
||||
var rc *RuntimeConfig
|
||||
@@ -810,7 +875,13 @@ func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) stri
|
||||
townRoot := filepath.Dir(rigPath)
|
||||
rc = ResolveAgentConfig(townRoot, rigPath)
|
||||
} else {
|
||||
// Try to detect town root from cwd for town-level agents (mayor, deacon)
|
||||
townRoot, err := findTownRootFromCwd()
|
||||
if err != nil {
|
||||
rc = DefaultRuntimeConfig()
|
||||
} else {
|
||||
rc = ResolveAgentConfig(townRoot, "")
|
||||
}
|
||||
}
|
||||
|
||||
// Build environment export prefix
|
||||
|
||||
@@ -970,3 +970,133 @@ func TestLoadRuntimeConfigFallsBackToDefaults(t *testing.T) {
|
||||
t.Errorf("Command = %q, want %q (default)", rc.Command, "claude")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveTownSettings(t *testing.T) {
|
||||
t.Run("saves valid town settings", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "settings", "config.json")
|
||||
|
||||
settings := &TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: CurrentTownSettingsVersion,
|
||||
DefaultAgent: "gemini",
|
||||
Agents: map[string]*RuntimeConfig{
|
||||
"my-agent": {
|
||||
Command: "my-agent",
|
||||
Args: []string{"--arg1", "--arg2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := SaveTownSettings(settingsPath, settings)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveTownSettings failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("reading settings file: %v", err)
|
||||
}
|
||||
|
||||
// Verify it contains expected content
|
||||
content := string(data)
|
||||
if !strings.Contains(content, `"type": "town-settings"`) {
|
||||
t.Errorf("missing type field")
|
||||
}
|
||||
if !strings.Contains(content, `"default_agent": "gemini"`) {
|
||||
t.Errorf("missing default_agent field")
|
||||
}
|
||||
if !strings.Contains(content, `"my-agent"`) {
|
||||
t.Errorf("missing custom agent")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("creates parent directories", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "deeply", "nested", "settings", "config.json")
|
||||
|
||||
settings := NewTownSettings()
|
||||
|
||||
err := SaveTownSettings(settingsPath, settings)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveTownSettings failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if _, err := os.Stat(settingsPath); err != nil {
|
||||
t.Errorf("settings file not created: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects invalid type", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "config.json")
|
||||
|
||||
settings := &TownSettings{
|
||||
Type: "invalid-type",
|
||||
Version: CurrentTownSettingsVersion,
|
||||
}
|
||||
|
||||
err := SaveTownSettings(settingsPath, settings)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects unsupported version", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "config.json")
|
||||
|
||||
settings := &TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: CurrentTownSettingsVersion + 100,
|
||||
}
|
||||
|
||||
err := SaveTownSettings(settingsPath, settings)
|
||||
if err == nil {
|
||||
t.Error("expected error for unsupported version")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("roundtrip save and load", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
settingsPath := filepath.Join(tmpDir, "config.json")
|
||||
|
||||
original := &TownSettings{
|
||||
Type: "town-settings",
|
||||
Version: CurrentTownSettingsVersion,
|
||||
DefaultAgent: "codex",
|
||||
Agents: map[string]*RuntimeConfig{
|
||||
"custom-1": {
|
||||
Command: "custom-agent",
|
||||
Args: []string{"--flag"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := SaveTownSettings(settingsPath, original)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveTownSettings failed: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadOrCreateTownSettings(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadOrCreateTownSettings failed: %v", err)
|
||||
}
|
||||
|
||||
if loaded.Type != original.Type {
|
||||
t.Errorf("Type = %q, want %q", loaded.Type, original.Type)
|
||||
}
|
||||
if loaded.Version != original.Version {
|
||||
t.Errorf("Version = %d, want %d", loaded.Version, original.Version)
|
||||
}
|
||||
if loaded.DefaultAgent != original.DefaultAgent {
|
||||
t.Errorf("DefaultAgent = %q, want %q", loaded.DefaultAgent, original.DefaultAgent)
|
||||
}
|
||||
|
||||
if len(loaded.Agents) != len(original.Agents) {
|
||||
t.Errorf("Agents count = %d, want %d", len(loaded.Agents), len(original.Agents))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user