feat(polecat): Add bulk removal with --all and multi-polecat support (gt-hcc0)

- Add --all flag to remove all polecats from a rig: `gt polecat remove gastown --all`
- Support multiple rig/polecat args: `gt polecat remove gastown/A gastown/B`
- Report summary of removed polecats and any failures
- Validate that --all is not used with rig/polecat format

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-20 13:16:26 -08:00
parent df20acf782
commit 2a9f31a02d

View File

@@ -22,9 +22,10 @@ import (
// Polecat command flags // Polecat command flags
var ( var (
polecatListJSON bool polecatListJSON bool
polecatListAll bool polecatListAll bool
polecatForce bool polecatForce bool
polecatRemoveAll bool
) )
var polecatCmd = &cobra.Command{ var polecatCmd = &cobra.Command{
@@ -70,18 +71,20 @@ Example:
} }
var polecatRemoveCmd = &cobra.Command{ var polecatRemoveCmd = &cobra.Command{
Use: "remove <rig>/<polecat>", Use: "remove <rig>/<polecat>... | <rig> --all",
Short: "Remove a polecat from a rig", Short: "Remove polecats from a rig",
Long: `Remove a polecat from a rig. Long: `Remove one or more polecats from a rig.
Fails if session is running (stop first). Fails if session is running (stop first).
Warns if uncommitted changes exist. Warns if uncommitted changes exist.
Use --force to bypass checks. Use --force to bypass checks.
Example: Examples:
gt polecat remove gastown/Toast gt polecat remove gastown/Toast
gt polecat remove gastown/Toast --force`, gt polecat remove gastown/Toast gastown/Furiosa
Args: cobra.ExactArgs(1), gt polecat remove gastown --all
gt polecat remove gastown --all --force`,
Args: cobra.MinimumNArgs(1),
RunE: runPolecatRemove, RunE: runPolecatRemove,
} }
@@ -202,6 +205,7 @@ func init() {
// Remove flags // Remove flags
polecatRemoveCmd.Flags().BoolVarP(&polecatForce, "force", "f", false, "Force removal, bypassing checks") polecatRemoveCmd.Flags().BoolVarP(&polecatForce, "force", "f", false, "Force removal, bypassing checks")
polecatRemoveCmd.Flags().BoolVar(&polecatRemoveAll, "all", false, "Remove all polecats in the rig")
// Sync flags // Sync flags
polecatSyncCmd.Flags().BoolVar(&polecatSyncAll, "all", false, "Sync all polecats in the rig") polecatSyncCmd.Flags().BoolVar(&polecatSyncAll, "all", false, "Sync all polecats in the rig")
@@ -384,36 +388,115 @@ func runPolecatAdd(cmd *cobra.Command, args []string) error {
} }
func runPolecatRemove(cmd *cobra.Command, args []string) error { func runPolecatRemove(cmd *cobra.Command, args []string) error {
rigName, polecatName, err := parseAddress(args[0]) // Build list of polecats to remove
if err != nil { type polecatToRemove struct {
return err rigName string
polecatName string
mgr *polecat.Manager
r *rig.Rig
} }
var toRemove []polecatToRemove
mgr, r, err := getPolecatManager(rigName) if polecatRemoveAll {
if err != nil { // --all flag: first arg is just the rig name
return err rigName := args[0]
} // Check if it looks like rig/polecat format
if _, _, err := parseAddress(rigName); err == nil {
return fmt.Errorf("with --all, provide just the rig name (e.g., 'gt polecat remove gastown --all')")
}
// Check if session is running mgr, r, err := getPolecatManager(rigName)
if !polecatForce { if err != nil {
t := tmux.NewTmux() return err
sessMgr := session.NewManager(t, r) }
running, _ := sessMgr.IsRunning(polecatName)
if running { polecats, err := mgr.List()
return fmt.Errorf("session is running. Stop it first with: gt session stop %s/%s", rigName, polecatName) if err != nil {
return fmt.Errorf("listing polecats: %w", err)
}
if len(polecats) == 0 {
fmt.Println("No polecats to remove.")
return nil
}
for _, p := range polecats {
toRemove = append(toRemove, polecatToRemove{
rigName: rigName,
polecatName: p.Name,
mgr: mgr,
r: r,
})
}
} else {
// Multiple rig/polecat arguments
for _, arg := range args {
rigName, polecatName, err := parseAddress(arg)
if err != nil {
return fmt.Errorf("invalid address '%s': %w", arg, err)
}
mgr, r, err := getPolecatManager(rigName)
if err != nil {
return err
}
toRemove = append(toRemove, polecatToRemove{
rigName: rigName,
polecatName: polecatName,
mgr: mgr,
r: r,
})
} }
} }
fmt.Printf("Removing polecat %s/%s...\n", rigName, polecatName) // Remove each polecat
t := tmux.NewTmux()
var removeErrors []string
removed := 0
if err := mgr.Remove(polecatName, polecatForce); err != nil { for _, p := range toRemove {
if errors.Is(err, polecat.ErrHasChanges) { // Check if session is running
return fmt.Errorf("polecat has uncommitted changes. Use --force to remove anyway") if !polecatForce {
sessMgr := session.NewManager(t, p.r)
running, _ := sessMgr.IsRunning(p.polecatName)
if running {
removeErrors = append(removeErrors, fmt.Sprintf("%s/%s: session is running (stop first or use --force)", p.rigName, p.polecatName))
continue
}
} }
return fmt.Errorf("removing polecat: %w", err)
fmt.Printf("Removing polecat %s/%s...\n", p.rigName, p.polecatName)
if err := p.mgr.Remove(p.polecatName, polecatForce); err != nil {
if errors.Is(err, polecat.ErrHasChanges) {
removeErrors = append(removeErrors, fmt.Sprintf("%s/%s: has uncommitted changes (use --force)", p.rigName, p.polecatName))
} else {
removeErrors = append(removeErrors, fmt.Sprintf("%s/%s: %v", p.rigName, p.polecatName, err))
}
continue
}
fmt.Printf(" %s removed\n", style.Success.Render("✓"))
removed++
}
// Report results
if len(removeErrors) > 0 {
fmt.Printf("\n%s Some removals failed:\n", style.Warning.Render("Warning:"))
for _, e := range removeErrors {
fmt.Printf(" - %s\n", e)
}
}
if removed > 0 {
fmt.Printf("\n%s Removed %d polecat(s).\n", style.SuccessPrefix, removed)
}
if len(removeErrors) > 0 {
return fmt.Errorf("%d removal(s) failed", len(removeErrors))
} }
fmt.Printf("%s Polecat %s removed.\n", style.SuccessPrefix, polecatName)
return nil return nil
} }