Implements config viewing and manipulation commands for rig configuration across property layers. Commands: - gt rig config show <rig> # Show effective config - gt rig config show <rig> --layers # Show source of each value - gt rig config set <rig> <key> <value> # Set in wisp layer - gt rig config set <rig> <key> <value> --global # Set in bead layer - gt rig config set <rig> <key> --block # Block inheritance - gt rig config unset <rig> <key> # Remove from wisp Includes cherry-picked dependencies: - Property layer lookup (cb927a73, gt-emh1c) - Rig identity bead schema for bead layer 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
323 lines
8.7 KiB
Go
323 lines
8.7 KiB
Go
// 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 <rig>",
|
|
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 <rig> <key> [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 <rig> <key>",
|
|
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)
|
|
}
|
|
}
|