diff --git a/internal/cmd/rig_config.go b/internal/cmd/rig_config.go new file mode 100644 index 00000000..e2246bea --- /dev/null +++ b/internal/cmd/rig_config.go @@ -0,0 +1,322 @@ +// Package cmd provides CLI commands for the gt tool. +// This file implements the gt rig config commands for viewing and manipulating +// rig configuration across property layers. +package cmd + +import ( + "fmt" + "sort" + "strconv" + + "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/rig" + "github.com/steveyegge/gastown/internal/style" + "github.com/steveyegge/gastown/internal/wisp" +) + +var rigConfigCmd = &cobra.Command{ + Use: "config", + Short: "View and manage rig configuration", + Long: `View and manage rig configuration across property layers. + +Configuration is looked up through multiple layers: +1. Wisp layer (transient, local) - .beads-wisp/config/ +2. Bead layer (persistent, synced) - rig identity bead labels +3. Town defaults - ~/gt/settings/config.json +4. System defaults - compiled-in fallbacks + +Most properties use override semantics (first non-nil wins). +Integer properties like priority_adjustment use stacking semantics (values add).`, + RunE: requireSubcommand, +} + +var rigConfigShowCmd = &cobra.Command{ + Use: "show ", + Short: "Show effective configuration for a rig", + Long: `Show the effective configuration for a rig. + +By default, shows only the resolved values. Use --layers to see +which layer each value comes from. + +Example output: + gt rig config show gastown --layers + Key Value Source + status parked wisp + priority_adjustment 10 bead + auto_restart true system + max_polecats 4 town`, + Args: cobra.ExactArgs(1), + RunE: runRigConfigShow, +} + +var rigConfigSetCmd = &cobra.Command{ + Use: "set [value]", + Short: "Set a configuration value", + Long: `Set a configuration value in the wisp layer (local, ephemeral). + +Use --global to set in the bead layer (persistent, synced globally). +Use --block to explicitly block a key (prevents inheritance). + +Examples: + gt rig config set gastown status parked # Wisp layer + gt rig config set gastown status docked --global # Bead layer + gt rig config set gastown auto_restart --block # Block inheritance`, + Args: cobra.RangeArgs(2, 3), + RunE: runRigConfigSet, +} + +var rigConfigUnsetCmd = &cobra.Command{ + Use: "unset ", + Short: "Remove a configuration value from the wisp layer", + Long: `Remove a configuration value from the wisp layer. + +This clears both regular values and blocked markers for the key. +Values set in the bead layer remain unchanged. + +Example: + gt rig config unset gastown status`, + Args: cobra.ExactArgs(2), + RunE: runRigConfigUnset, +} + +// Flags +var ( + rigConfigShowLayers bool + rigConfigSetGlobal bool + rigConfigSetBlock bool +) + +func init() { + rigCmd.AddCommand(rigConfigCmd) + rigConfigCmd.AddCommand(rigConfigShowCmd) + rigConfigCmd.AddCommand(rigConfigSetCmd) + rigConfigCmd.AddCommand(rigConfigUnsetCmd) + + rigConfigShowCmd.Flags().BoolVar(&rigConfigShowLayers, "layers", false, "Show which layer each value comes from") + + rigConfigSetCmd.Flags().BoolVar(&rigConfigSetGlobal, "global", false, "Set in bead layer (persistent, synced)") + rigConfigSetCmd.Flags().BoolVar(&rigConfigSetBlock, "block", false, "Block inheritance for this key") +} + +func runRigConfigShow(cmd *cobra.Command, args []string) error { + rigName := args[0] + + townRoot, r, err := getRig(rigName) + if err != nil { + return err + } + + // Collect all known keys + allKeys := getConfigKeys(townRoot, r) + + if rigConfigShowLayers { + // Show with sources + fmt.Printf("%-25s %-15s %s\n", "Key", "Value", "Source") + fmt.Printf("%-25s %-15s %s\n", "---", "-----", "------") + for _, key := range allKeys { + result := r.GetConfigWithSource(key) + valueStr := formatValue(result.Value) + sourceStr := string(result.Source) + if result.Source == rig.SourceBlocked { + valueStr = "(blocked)" + } + fmt.Printf("%-25s %-15s %s\n", key, valueStr, sourceStr) + } + } else { + // Show only effective values + fmt.Printf("%-25s %s\n", "Key", "Value") + fmt.Printf("%-25s %s\n", "---", "-----") + for _, key := range allKeys { + result := r.GetConfigWithSource(key) + if result.Source == rig.SourceNone { + continue // Skip unset keys + } + valueStr := formatValue(result.Value) + if result.Source == rig.SourceBlocked { + valueStr = "(blocked)" + } + fmt.Printf("%-25s %s\n", key, valueStr) + } + } + + return nil +} + +func runRigConfigSet(cmd *cobra.Command, args []string) error { + rigName := args[0] + key := args[1] + + // Validate: --block requires no value, otherwise value is required + if rigConfigSetBlock { + if len(args) > 2 { + return fmt.Errorf("--block does not take a value") + } + } else { + if len(args) < 3 { + return fmt.Errorf("value is required (use --block to block inheritance instead)") + } + } + + townRoot, r, err := getRig(rigName) + if err != nil { + return err + } + + if rigConfigSetBlock { + // Block inheritance via wisp layer + wispCfg := wisp.NewConfig(townRoot, r.Name) + if err := wispCfg.Block(key); err != nil { + return fmt.Errorf("blocking %s: %w", key, err) + } + fmt.Printf("%s Blocked %s for rig %s\n", style.Success.Render("✓"), key, rigName) + return nil + } + + value := args[2] + + if rigConfigSetGlobal { + // Set in bead layer (rig identity bead labels) + if err := setBeadLabel(townRoot, r, key, value); err != nil { + return fmt.Errorf("setting bead label: %w", err) + } + fmt.Printf("%s Set %s=%s in bead layer for rig %s\n", style.Success.Render("✓"), key, value, rigName) + } else { + // Set in wisp layer + wispCfg := wisp.NewConfig(townRoot, r.Name) + // Try to parse as appropriate type + var typedValue interface{} = value + if b, err := strconv.ParseBool(value); err == nil { + typedValue = b + } else if i, err := strconv.Atoi(value); err == nil { + typedValue = i + } + if err := wispCfg.Set(key, typedValue); err != nil { + return fmt.Errorf("setting %s: %w", key, err) + } + fmt.Printf("%s Set %s=%s in wisp layer for rig %s\n", style.Success.Render("✓"), key, value, rigName) + } + + return nil +} + +func runRigConfigUnset(cmd *cobra.Command, args []string) error { + rigName := args[0] + key := args[1] + + townRoot, r, err := getRig(rigName) + if err != nil { + return err + } + + wispCfg := wisp.NewConfig(townRoot, r.Name) + if err := wispCfg.Unset(key); err != nil { + return fmt.Errorf("unsetting %s: %w", key, err) + } + + fmt.Printf("%s Unset %s from wisp layer for rig %s\n", style.Success.Render("✓"), key, rigName) + return nil +} + +// getConfigKeys returns all known configuration keys, sorted. +func getConfigKeys(townRoot string, r *rig.Rig) []string { + keySet := make(map[string]bool) + + // System defaults + for k := range rig.SystemDefaults { + keySet[k] = true + } + + // Wisp keys + wispCfg := wisp.NewConfig(townRoot, r.Name) + for _, k := range wispCfg.Keys() { + keySet[k] = true + } + + // Bead labels (from rig identity bead) + prefix := "gt" + if r.Config != nil && r.Config.Prefix != "" { + prefix = r.Config.Prefix + } + rigBeadID := beads.RigBeadIDWithPrefix(prefix, r.Name) + beadsDir := beads.ResolveBeadsDir(r.Path) + bd := beads.NewWithBeadsDir(townRoot, beadsDir) + if issue, err := bd.Show(rigBeadID); err == nil { + for _, label := range issue.Labels { + // Labels are in format "key:value" + for i, c := range label { + if c == ':' { + keySet[label[:i]] = true + break + } + } + } + } + + // Sort keys + keys := make([]string, 0, len(keySet)) + for k := range keySet { + keys = append(keys, k) + } + sort.Strings(keys) + + return keys +} + +// setBeadLabel sets a label on the rig identity bead. +func setBeadLabel(townRoot string, r *rig.Rig, key, value string) error { + prefix := "gt" + if r.Config != nil && r.Config.Prefix != "" { + prefix = r.Config.Prefix + } + + rigBeadID := beads.RigBeadIDWithPrefix(prefix, r.Name) + beadsDir := beads.ResolveBeadsDir(r.Path) + bd := beads.NewWithBeadsDir(townRoot, beadsDir) + + // Check if bead exists + issue, err := bd.Show(rigBeadID) + if err != nil { + return fmt.Errorf("rig identity bead %s not found (run 'gt rig add' to create it)", rigBeadID) + } + + // Build new labels list: remove existing key:* and add new key:value + newLabels := make([]string, 0, len(issue.Labels)+1) + keyPrefix := key + ":" + for _, label := range issue.Labels { + if len(label) > len(keyPrefix) && label[:len(keyPrefix)] == keyPrefix { + continue // Remove old value for this key + } + newLabels = append(newLabels, label) + } + newLabels = append(newLabels, key+":"+value) + + // Update the bead + return bd.Update(rigBeadID, beads.UpdateOptions{ + SetLabels: newLabels, + }) +} + +// formatValue formats a config value for display. +func formatValue(v interface{}) string { + if v == nil { + return "(nil)" + } + switch val := v.(type) { + case bool: + if val { + return "true" + } + return "false" + case int: + return strconv.Itoa(val) + case int64: + return strconv.FormatInt(val, 10) + case float64: + return strconv.FormatFloat(val, 'f', -1, 64) + case string: + return val + default: + return fmt.Sprintf("%v", v) + } +}