// 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) } }