Files
gastown/internal/cmd/rig.go
Steve Yegge 41609e8def feat: add gt rig add command with container-based structure
- Implement AddRig with container-based approach (rig root is NOT a clone)
- Create internal/cmd/rig.go with add/list/remove subcommands
- Clone repo into refinery/rig/, mayor/rig/, crew/main/
- Initialize rig-level .beads/ with derived prefix
- Update docs/architecture.md to match implementation
- File gt-jpt epic for town-level beads redesign

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 19:14:37 -08:00

246 lines
6.7 KiB
Go

// Package cmd provides CLI commands for the gt tool.
package cmd
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
var rigCmd = &cobra.Command{
Use: "rig",
Short: "Manage rigs in the workspace",
Long: `Manage rigs (project containers) in the Gas Town workspace.
A rig is a container for managing a project and its agents:
- refinery/rig/ Canonical main clone (Refinery's working copy)
- mayor/rig/ Mayor's working clone for this rig
- crew/<name>/ Human workspace(s)
- witness/ Witness agent (no clone)
- polecats/ Worker directories
- .beads/ Rig-level issue tracking`,
}
var rigAddCmd = &cobra.Command{
Use: "add <name> <git-url>",
Short: "Add a new rig to the workspace",
Long: `Add a new rig by cloning a repository.
This creates a rig container with:
- config.json Rig configuration
- .beads/ Rig-level issue tracking (initialized)
- refinery/rig/ Canonical main clone
- mayor/rig/ Mayor's working clone
- crew/main/ Default human workspace
- witness/ Witness agent directory
- polecats/ Worker directory (empty)
Example:
gt rig add gastown https://github.com/steveyegge/gastown
gt rig add my-project git@github.com:user/repo.git --prefix mp`,
Args: cobra.ExactArgs(2),
RunE: runRigAdd,
}
var rigListCmd = &cobra.Command{
Use: "list",
Short: "List all rigs in the workspace",
RunE: runRigList,
}
var rigRemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove a rig from the registry (does not delete files)",
Args: cobra.ExactArgs(1),
RunE: runRigRemove,
}
// Flags
var (
rigAddPrefix string
rigAddCrew string
)
func init() {
rootCmd.AddCommand(rigCmd)
rigCmd.AddCommand(rigAddCmd)
rigCmd.AddCommand(rigListCmd)
rigCmd.AddCommand(rigRemoveCmd)
rigAddCmd.Flags().StringVar(&rigAddPrefix, "prefix", "", "Beads issue prefix (default: derived from name)")
rigAddCmd.Flags().StringVar(&rigAddCrew, "crew", "main", "Default crew workspace name")
}
func runRigAdd(cmd *cobra.Command, args []string) error {
name := args[0]
gitURL := args[1]
// Find workspace
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Load rigs config
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
rigsConfig, err := config.LoadRigsConfig(rigsPath)
if err != nil {
// Create new if doesn't exist
rigsConfig = &config.RigsConfig{
Version: 1,
Rigs: make(map[string]config.RigEntry),
}
}
// Create rig manager
g := git.NewGit(townRoot)
mgr := rig.NewManager(townRoot, rigsConfig, g)
fmt.Printf("Creating rig %s...\n", style.Bold.Render(name))
fmt.Printf(" Repository: %s\n", gitURL)
startTime := time.Now()
// Add the rig
newRig, err := mgr.AddRig(rig.AddRigOptions{
Name: name,
GitURL: gitURL,
BeadsPrefix: rigAddPrefix,
CrewName: rigAddCrew,
})
if err != nil {
return fmt.Errorf("adding rig: %w", err)
}
// Save updated rigs config
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
return fmt.Errorf("saving rigs config: %w", err)
}
elapsed := time.Since(startTime)
fmt.Printf("\n%s Rig created in %.1fs\n", style.Success.Render("✓"), elapsed.Seconds())
fmt.Printf("\nStructure:\n")
fmt.Printf(" %s/\n", name)
fmt.Printf(" ├── config.json\n")
fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix)
fmt.Printf(" ├── refinery/rig/ (canonical main)\n")
fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n")
fmt.Printf(" ├── crew/%s/ (your workspace)\n", rigAddCrew)
fmt.Printf(" ├── witness/\n")
fmt.Printf(" └── polecats/\n")
fmt.Printf("\nNext steps:\n")
fmt.Printf(" cd %s/crew/%s # Work in your clone\n", filepath.Join(townRoot, name), rigAddCrew)
fmt.Printf(" bd ready # See available work\n")
return nil
}
func runRigList(cmd *cobra.Command, args []string) error {
// Find workspace
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Load rigs config
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
rigsConfig, err := config.LoadRigsConfig(rigsPath)
if err != nil {
fmt.Println("No rigs configured.")
return nil
}
if len(rigsConfig.Rigs) == 0 {
fmt.Println("No rigs configured.")
fmt.Printf("\nAdd one with: %s\n", style.Dim.Render("gt rig add <name> <git-url>"))
return nil
}
// Create rig manager to get details
g := git.NewGit(townRoot)
mgr := rig.NewManager(townRoot, rigsConfig, g)
fmt.Printf("Rigs in %s:\n\n", townRoot)
for name := range rigsConfig.Rigs {
r, err := mgr.GetRig(name)
if err != nil {
fmt.Printf(" %s %s\n", style.Warning.Render("!"), name)
continue
}
summary := r.Summary()
fmt.Printf(" %s\n", style.Bold.Render(name))
fmt.Printf(" Polecats: %d Crew: %d\n", summary.PolecatCount, summary.CrewCount)
agents := []string{}
if summary.HasRefinery {
agents = append(agents, "refinery")
}
if summary.HasWitness {
agents = append(agents, "witness")
}
if r.HasMayor {
agents = append(agents, "mayor")
}
if len(agents) > 0 {
fmt.Printf(" Agents: %v\n", agents)
}
fmt.Println()
}
return nil
}
func runRigRemove(cmd *cobra.Command, args []string) error {
name := args[0]
// Find workspace
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Load rigs config
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
rigsConfig, err := config.LoadRigsConfig(rigsPath)
if err != nil {
return fmt.Errorf("loading rigs config: %w", err)
}
// Create rig manager
g := git.NewGit(townRoot)
mgr := rig.NewManager(townRoot, rigsConfig, g)
if err := mgr.RemoveRig(name); err != nil {
return fmt.Errorf("removing rig: %w", err)
}
// Save updated config
if err := config.SaveRigsConfig(rigsPath, rigsConfig); err != nil {
return fmt.Errorf("saving rigs config: %w", err)
}
fmt.Printf("%s Rig %s removed from registry\n", style.Success.Render("✓"), name)
fmt.Printf("\nNote: Files at %s were NOT deleted.\n", filepath.Join(townRoot, name))
fmt.Printf("To delete: %s\n", style.Dim.Render(fmt.Sprintf("rm -rf %s", filepath.Join(townRoot, name))))
return nil
}
// Helper to check if path exists
func pathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}