Files
gastown/internal/cmd/namepool.go
gastown/crew/dennis bda1dc97c5 fix(namepool): only persist runtime state, not config in state file
The pool state file was saving CustomNames even though Load() ignored
them (CustomNames come from settings/config.json). This caused the
state file to have stale/incorrect custom names data.

Changes:
- Create namePoolState struct for persisting only OverflowNext/MaxSize
- Save() now only writes runtime state, not configuration
- Load() uses the same struct for consistency
- Removed redundant runtime pool update from runNamepoolAdd since
  the settings file is the source of truth for custom names

Fixes: gt-ofqzwv

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 01:40:02 -08:00

337 lines
8.8 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/polecat"
"github.com/steveyegge/gastown/internal/workspace"
)
var (
namepoolListFlag bool
namepoolThemeFlag string
)
var namepoolCmd = &cobra.Command{
Use: "namepool",
GroupID: GroupWorkspace,
Short: "Manage polecat name pools",
Long: `Manage themed name pools for polecats in Gas Town.
By default, polecats get themed names from the Mad Max universe
(furiosa, nux, slit, etc.). You can change the theme or add custom names.
Examples:
gt namepool # Show current pool status
gt namepool --list # List available themes
gt namepool themes # Show theme names
gt namepool set minerals # Set theme to 'minerals'
gt namepool add ember # Add custom name to pool
gt namepool reset # Reset pool state`,
RunE: runNamepool,
}
var namepoolThemesCmd = &cobra.Command{
Use: "themes [theme]",
Short: "List available themes and their names",
RunE: runNamepoolThemes,
}
var namepoolSetCmd = &cobra.Command{
Use: "set <theme>",
Short: "Set the namepool theme for this rig",
Args: cobra.ExactArgs(1),
RunE: runNamepoolSet,
}
var namepoolAddCmd = &cobra.Command{
Use: "add <name>",
Short: "Add a custom name to the pool",
Args: cobra.ExactArgs(1),
RunE: runNamepoolAdd,
}
var namepoolResetCmd = &cobra.Command{
Use: "reset",
Short: "Reset the pool state (release all names)",
RunE: runNamepoolReset,
}
func init() {
rootCmd.AddCommand(namepoolCmd)
namepoolCmd.AddCommand(namepoolThemesCmd)
namepoolCmd.AddCommand(namepoolSetCmd)
namepoolCmd.AddCommand(namepoolAddCmd)
namepoolCmd.AddCommand(namepoolResetCmd)
namepoolCmd.Flags().BoolVarP(&namepoolListFlag, "list", "l", false, "List available themes")
}
func runNamepool(cmd *cobra.Command, args []string) error {
// List themes mode
if namepoolListFlag {
return runNamepoolThemes(cmd, nil)
}
// Show current pool status
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Load pool
pool := polecat.NewNamePool(rigPath, rigName)
if err := pool.Load(); err != nil {
// Pool doesn't exist yet, show defaults
fmt.Printf("Rig: %s\n", rigName)
fmt.Printf("Theme: %s (default)\n", polecat.DefaultTheme)
fmt.Printf("Active polecats: 0\n")
fmt.Printf("Max pool size: %d\n", polecat.DefaultPoolSize)
return nil
}
// Show pool status
fmt.Printf("Rig: %s\n", rigName)
fmt.Printf("Theme: %s\n", pool.GetTheme())
fmt.Printf("Active polecats: %d\n", pool.ActiveCount())
activeNames := pool.ActiveNames()
if len(activeNames) > 0 {
fmt.Printf("In use: %s\n", strings.Join(activeNames, ", "))
}
// Check if configured
settingsPath := filepath.Join(rigPath, "settings", "config.json")
if settings, err := config.LoadRigSettings(settingsPath); err == nil && settings.Namepool != nil {
fmt.Printf("(configured in settings/config.json)\n")
}
return nil
}
func runNamepoolThemes(cmd *cobra.Command, args []string) error {
themes := polecat.ListThemes()
if len(args) == 0 {
// List all themes
fmt.Println("Available themes:")
for _, theme := range themes {
names, _ := polecat.GetThemeNames(theme)
fmt.Printf("\n %s (%d names):\n", theme, len(names))
// Show first 10 names
preview := names
if len(preview) > 10 {
preview = preview[:10]
}
fmt.Printf(" %s...\n", strings.Join(preview, ", "))
}
return nil
}
// Show specific theme names
theme := args[0]
names, err := polecat.GetThemeNames(theme)
if err != nil {
return fmt.Errorf("unknown theme: %s (available: %s)", theme, strings.Join(themes, ", "))
}
fmt.Printf("Theme: %s (%d names)\n\n", theme, len(names))
for i, name := range names {
if i > 0 && i%5 == 0 {
fmt.Println()
}
fmt.Printf(" %-12s", name)
}
fmt.Println()
return nil
}
func runNamepoolSet(cmd *cobra.Command, args []string) error {
theme := args[0]
// Validate theme
themes := polecat.ListThemes()
valid := false
for _, t := range themes {
if t == theme {
valid = true
break
}
}
if !valid {
return fmt.Errorf("unknown theme: %s (available: %s)", theme, strings.Join(themes, ", "))
}
// Get rig
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Update pool
pool := polecat.NewNamePool(rigPath, rigName)
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("loading pool: %w", err)
}
if err := pool.SetTheme(theme); err != nil {
return err
}
if err := pool.Save(); err != nil {
return fmt.Errorf("saving pool: %w", err)
}
// Load existing settings to preserve custom names when changing theme
settingsPath := filepath.Join(rigPath, "settings", "config.json")
var existingNames []string
if existingSettings, err := config.LoadRigSettings(settingsPath); err == nil {
if existingSettings.Namepool != nil {
existingNames = existingSettings.Namepool.Names
}
}
// Also save to rig config, preserving existing custom names
if err := saveRigNamepoolConfig(rigPath, theme, existingNames); err != nil {
return fmt.Errorf("saving config: %w", err)
}
fmt.Printf("Theme '%s' set for rig '%s'\n", theme, rigName)
fmt.Printf("New polecats will use names from this theme.\n")
return nil
}
func runNamepoolAdd(cmd *cobra.Command, args []string) error {
name := args[0]
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Load existing rig settings to get current theme and custom names
settingsPath := filepath.Join(rigPath, "settings", "config.json")
settings, err := config.LoadRigSettings(settingsPath)
if err != nil {
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
settings = config.NewRigSettings()
} else {
return fmt.Errorf("loading settings: %w", err)
}
}
// Initialize namepool config if needed
if settings.Namepool == nil {
settings.Namepool = config.DefaultNamepoolConfig()
}
// Check if name already exists
for _, n := range settings.Namepool.Names {
if n == name {
fmt.Printf("Name '%s' already in pool\n", name)
return nil
}
}
// Append new name to existing custom names
settings.Namepool.Names = append(settings.Namepool.Names, name)
// Save to settings/config.json (the source of truth for config)
if err := config.SaveRigSettings(settingsPath, settings); err != nil {
return fmt.Errorf("saving settings: %w", err)
}
// Note: No need to update runtime pool state - the settings file is the source
// of truth for custom names. The pool state file only persists OverflowNext/MaxSize.
// New managers will load custom names from settings/config.json.
fmt.Printf("Added '%s' to the name pool\n", name)
return nil
}
func runNamepoolReset(cmd *cobra.Command, args []string) error {
rigName, rigPath := detectCurrentRigWithPath()
if rigName == "" {
return fmt.Errorf("not in a rig directory")
}
// Load pool
pool := polecat.NewNamePool(rigPath, rigName)
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("loading pool: %w", err)
}
pool.Reset()
if err := pool.Save(); err != nil {
return fmt.Errorf("saving pool: %w", err)
}
fmt.Printf("Pool reset for rig '%s'\n", rigName)
fmt.Printf("All names released and available for reuse.\n")
return nil
}
// detectCurrentRigWithPath determines the rig name and path from cwd.
func detectCurrentRigWithPath() (string, string) {
cwd, err := os.Getwd()
if err != nil {
return "", ""
}
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return "", ""
}
// Get path relative to town root
rel, err := filepath.Rel(townRoot, cwd)
if err != nil {
return "", ""
}
// Extract first path component (rig name)
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) > 0 && parts[0] != "." && parts[0] != "mayor" && parts[0] != "deacon" {
return parts[0], filepath.Join(townRoot, parts[0])
}
return "", ""
}
// saveRigNamepoolConfig saves the namepool config to rig settings.
func saveRigNamepoolConfig(rigPath, theme string, customNames []string) error {
settingsPath := filepath.Join(rigPath, "settings", "config.json")
// Load existing settings or create new
var settings *config.RigSettings
settings, err := config.LoadRigSettings(settingsPath)
if err != nil {
// Create new settings if not found
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
settings = config.NewRigSettings()
} else {
return fmt.Errorf("loading settings: %w", err)
}
}
// Set namepool
settings.Namepool = &config.NamepoolConfig{
Style: theme,
Names: customNames,
}
// Save (creates directory if needed)
if err := config.SaveRigSettings(settingsPath, settings); err != nil {
return fmt.Errorf("saving settings: %w", err)
}
return nil
}