Implement gt rig dock <rig> and gt rig undock <rig> commands for global/persistent rig control: - dock: stops witness/refinery, sets status:docked label on rig bead - undock: removes docked label, allows daemon to restart agents This is Level 2 (global/persistent) control: - Uses rig identity bead labels (synced via git) - Affects all clones of the rig - Persists until explicitly undocked Also includes cherry-picked rig identity bead infrastructure: - RigFields struct for rig metadata - CreateRigBead and RigBeadID helpers - Auto-create rig bead for legacy rigs on first dock 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
254 lines
7.0 KiB
Go
254 lines
7.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/refinery"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/tmux"
|
|
"github.com/steveyegge/gastown/internal/witness"
|
|
)
|
|
|
|
// RigDockedLabel is the label set on rig identity beads when docked.
|
|
const RigDockedLabel = "status:docked"
|
|
|
|
var rigDockCmd = &cobra.Command{
|
|
Use: "dock <rig>",
|
|
Short: "Dock a rig (global, persistent shutdown)",
|
|
Long: `Dock a rig to persistently disable it across all clones.
|
|
|
|
Docking a rig:
|
|
- Stops the witness if running
|
|
- Stops the refinery if running
|
|
- Sets status:docked label on the rig identity bead
|
|
- Syncs via git so all clones see the docked status
|
|
|
|
This is a Level 2 (global/persistent) operation:
|
|
- Affects all clones of this rig (via git sync)
|
|
- Persists until explicitly undocked
|
|
- The daemon respects this status and won't auto-restart agents
|
|
|
|
Use 'gt rig undock' to resume normal operation.
|
|
|
|
Examples:
|
|
gt rig dock gastown
|
|
gt rig dock beads`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runRigDock,
|
|
}
|
|
|
|
var rigUndockCmd = &cobra.Command{
|
|
Use: "undock <rig>",
|
|
Short: "Undock a rig (remove global docked status)",
|
|
Long: `Undock a rig to remove the persistent docked status.
|
|
|
|
Undocking a rig:
|
|
- Removes the status:docked label from the rig identity bead
|
|
- Syncs via git so all clones see the undocked status
|
|
- Allows the daemon to auto-restart agents
|
|
- Does NOT automatically start agents (use 'gt rig start' for that)
|
|
|
|
Examples:
|
|
gt rig undock gastown
|
|
gt rig undock beads`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runRigUndock,
|
|
}
|
|
|
|
func init() {
|
|
rigCmd.AddCommand(rigDockCmd)
|
|
rigCmd.AddCommand(rigUndockCmd)
|
|
}
|
|
|
|
func runRigDock(cmd *cobra.Command, args []string) error {
|
|
rigName := args[0]
|
|
|
|
// Get rig
|
|
_, r, err := getRig(rigName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get rig prefix for bead ID
|
|
prefix := "gt" // default
|
|
if r.Config != nil && r.Config.Prefix != "" {
|
|
prefix = r.Config.Prefix
|
|
}
|
|
|
|
// Find the rig identity bead
|
|
rigBeadID := beads.RigBeadIDWithPrefix(prefix, rigName)
|
|
bd := beads.New(r.BeadsPath())
|
|
|
|
// Check if rig bead exists, create if not
|
|
rigBead, err := bd.Show(rigBeadID)
|
|
if err != nil {
|
|
// Rig identity bead doesn't exist (legacy rig) - create it
|
|
fmt.Printf(" Creating rig identity bead %s...\n", rigBeadID)
|
|
rigBead, err = bd.CreateRigBead(rigBeadID, rigName, &beads.RigFields{
|
|
Repo: r.GitURL,
|
|
Prefix: prefix,
|
|
State: "active",
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("creating rig identity bead: %w", err)
|
|
}
|
|
}
|
|
|
|
// Check if already docked
|
|
for _, label := range rigBead.Labels {
|
|
if label == RigDockedLabel {
|
|
fmt.Printf("%s Rig %s is already docked\n", style.Dim.Render("•"), rigName)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Docking rig %s...\n", style.Bold.Render(rigName))
|
|
|
|
var stoppedAgents []string
|
|
|
|
t := tmux.NewTmux()
|
|
|
|
// Stop witness if running
|
|
witnessSession := fmt.Sprintf("gt-%s-witness", rigName)
|
|
witnessRunning, _ := t.HasSession(witnessSession)
|
|
if witnessRunning {
|
|
fmt.Printf(" Stopping witness...\n")
|
|
witMgr := witness.NewManager(r)
|
|
if err := witMgr.Stop(); err != nil {
|
|
fmt.Printf(" %s Failed to stop witness: %v\n", style.Warning.Render("!"), err)
|
|
} else {
|
|
stoppedAgents = append(stoppedAgents, "Witness stopped")
|
|
}
|
|
}
|
|
|
|
// Stop refinery if running
|
|
refinerySession := fmt.Sprintf("gt-%s-refinery", rigName)
|
|
refineryRunning, _ := t.HasSession(refinerySession)
|
|
if refineryRunning {
|
|
fmt.Printf(" Stopping refinery...\n")
|
|
refMgr := refinery.NewManager(r)
|
|
if err := refMgr.Stop(); err != nil {
|
|
fmt.Printf(" %s Failed to stop refinery: %v\n", style.Warning.Render("!"), err)
|
|
} else {
|
|
stoppedAgents = append(stoppedAgents, "Refinery stopped")
|
|
}
|
|
}
|
|
|
|
// Set docked label on rig identity bead
|
|
if err := bd.Update(rigBeadID, beads.UpdateOptions{
|
|
AddLabels: []string{RigDockedLabel},
|
|
}); err != nil {
|
|
return fmt.Errorf("setting docked label: %w", err)
|
|
}
|
|
|
|
// Sync beads to propagate to other clones
|
|
fmt.Printf(" Syncing beads...\n")
|
|
syncCmd := exec.Command("bd", "sync")
|
|
syncCmd.Dir = r.BeadsPath()
|
|
if output, err := syncCmd.CombinedOutput(); err != nil {
|
|
fmt.Printf(" %s bd sync warning: %v\n%s", style.Warning.Render("!"), err, string(output))
|
|
}
|
|
|
|
// Output
|
|
fmt.Printf("%s Rig %s docked (global)\n", style.Success.Render("✓"), rigName)
|
|
fmt.Printf(" Label added: %s\n", RigDockedLabel)
|
|
for _, msg := range stoppedAgents {
|
|
fmt.Printf(" %s\n", msg)
|
|
}
|
|
fmt.Printf(" Run '%s' to propagate to other clones\n", style.Dim.Render("bd sync"))
|
|
|
|
return nil
|
|
}
|
|
|
|
func runRigUndock(cmd *cobra.Command, args []string) error {
|
|
rigName := args[0]
|
|
|
|
// Get rig and town root
|
|
_, r, err := getRig(rigName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get rig prefix for bead ID
|
|
prefix := "gt" // default
|
|
if r.Config != nil && r.Config.Prefix != "" {
|
|
prefix = r.Config.Prefix
|
|
}
|
|
|
|
// Find the rig identity bead
|
|
rigBeadID := beads.RigBeadIDWithPrefix(prefix, rigName)
|
|
bd := beads.New(r.BeadsPath())
|
|
|
|
// Check if rig bead exists, create if not
|
|
rigBead, err := bd.Show(rigBeadID)
|
|
if err != nil {
|
|
// Rig identity bead doesn't exist (legacy rig) - can't be docked
|
|
fmt.Printf("%s Rig %s has no identity bead and is not docked\n", style.Dim.Render("•"), rigName)
|
|
return nil
|
|
}
|
|
|
|
// Check if actually docked
|
|
isDocked := false
|
|
for _, label := range rigBead.Labels {
|
|
if label == RigDockedLabel {
|
|
isDocked = true
|
|
break
|
|
}
|
|
}
|
|
if !isDocked {
|
|
fmt.Printf("%s Rig %s is not docked\n", style.Dim.Render("•"), rigName)
|
|
return nil
|
|
}
|
|
|
|
// Remove docked label from rig identity bead
|
|
if err := bd.Update(rigBeadID, beads.UpdateOptions{
|
|
RemoveLabels: []string{RigDockedLabel},
|
|
}); err != nil {
|
|
return fmt.Errorf("removing docked label: %w", err)
|
|
}
|
|
|
|
// Sync beads to propagate to other clones
|
|
fmt.Printf(" Syncing beads...\n")
|
|
syncCmd := exec.Command("bd", "sync")
|
|
syncCmd.Dir = r.BeadsPath()
|
|
if output, err := syncCmd.CombinedOutput(); err != nil {
|
|
fmt.Printf(" %s bd sync warning: %v\n%s", style.Warning.Render("!"), err, string(output))
|
|
}
|
|
|
|
fmt.Printf("%s Rig %s undocked\n", style.Success.Render("✓"), rigName)
|
|
fmt.Printf(" Label removed: %s\n", RigDockedLabel)
|
|
fmt.Printf(" Daemon can now auto-restart agents\n")
|
|
fmt.Printf(" Use '%s' to start agents immediately\n", style.Dim.Render("gt rig start "+rigName))
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsRigDocked checks if a rig is docked by checking for the status:docked label
|
|
// on the rig identity bead. This function is exported for use by the daemon.
|
|
func IsRigDocked(townRoot, rigName, prefix string) bool {
|
|
// Construct the rig beads path
|
|
rigPath := townRoot + "/" + rigName
|
|
beadsPath := rigPath + "/mayor/rig"
|
|
if _, err := exec.Command("test", "-d", beadsPath).CombinedOutput(); err != nil {
|
|
beadsPath = rigPath
|
|
}
|
|
|
|
bd := beads.New(beadsPath)
|
|
rigBeadID := beads.RigBeadIDWithPrefix(prefix, rigName)
|
|
|
|
rigBead, err := bd.Show(rigBeadID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
for _, label := range rigBead.Labels {
|
|
if label == RigDockedLabel {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|