Add three layers of protection to prevent accidental branch switches in the town root (~/gt), which should always stay on main: 1. Doctor check `town-root-branch`: Verifies town root is on main/master. Fixable via `gt doctor --fix` to switch back to main. 2. Doctor check `pre-checkout-hook`: Verifies git pre-checkout hook is installed. The hook blocks checkout from main to any other branch. Fixable via `gt doctor --fix` or `gt git-init`. 3. Runtime warning in all gt commands: Non-blocking warning if town root is on wrong branch, with fix instructions. The root cause of this issue was git commands running in the wrong directory, switching the town root to a polecat branch. This broke gt commands because rigs.json and other configs were on main, not the polecat branch. Closes: hq-1kwuj Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
183 lines
5.5 KiB
Go
183 lines
5.5 KiB
Go
// Package cmd provides CLI commands for the gt tool.
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "gt",
|
|
Short: "Gas Town - Multi-agent workspace manager",
|
|
Version: Version,
|
|
Long: `Gas Town (gt) manages multi-agent workspaces called rigs.
|
|
|
|
It coordinates agent spawning, work distribution, and communication
|
|
across distributed teams of AI agents working on shared codebases.`,
|
|
PersistentPreRunE: persistentPreRun,
|
|
}
|
|
|
|
// Commands that don't require beads to be installed/checked.
|
|
// These are basic utility commands that should work without beads.
|
|
var beadsExemptCommands = map[string]bool{
|
|
"version": true,
|
|
"help": true,
|
|
"completion": true,
|
|
}
|
|
|
|
// Commands exempt from the town root branch warning.
|
|
// These are commands that help fix the problem or are diagnostic.
|
|
var branchCheckExemptCommands = map[string]bool{
|
|
"version": true,
|
|
"help": true,
|
|
"completion": true,
|
|
"doctor": true, // Used to fix the problem
|
|
"install": true, // Initial setup
|
|
"git-init": true, // Git setup
|
|
}
|
|
|
|
// persistentPreRun runs before every command.
|
|
func persistentPreRun(cmd *cobra.Command, args []string) error {
|
|
// Get the root command name being run
|
|
cmdName := cmd.Name()
|
|
|
|
// Check town root branch (warning only, non-blocking)
|
|
if !branchCheckExemptCommands[cmdName] {
|
|
warnIfTownRootOffMain()
|
|
}
|
|
|
|
// Skip beads check for exempt commands
|
|
if beadsExemptCommands[cmdName] {
|
|
return nil
|
|
}
|
|
|
|
// Check beads version
|
|
return CheckBeadsVersion()
|
|
}
|
|
|
|
// warnIfTownRootOffMain prints a warning if the town root is not on main branch.
|
|
// This is a non-blocking warning to help catch accidental branch switches.
|
|
func warnIfTownRootOffMain() {
|
|
// Find town root (silently - don't error if not in workspace)
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil || townRoot == "" {
|
|
return
|
|
}
|
|
|
|
// Check if it's a git repo
|
|
gitDir := townRoot + "/.git"
|
|
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
|
|
return
|
|
}
|
|
|
|
// Get current branch
|
|
gitCmd := exec.Command("git", "branch", "--show-current")
|
|
gitCmd.Dir = townRoot
|
|
out, err := gitCmd.Output()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
branch := strings.TrimSpace(string(out))
|
|
if branch == "" || branch == "main" || branch == "master" {
|
|
return
|
|
}
|
|
|
|
// Town root is on wrong branch - warn the user
|
|
fmt.Fprintf(os.Stderr, "\n%s Town root is on branch '%s' (should be 'main')\n",
|
|
style.Bold.Render("⚠️ WARNING:"), branch)
|
|
fmt.Fprintf(os.Stderr, " This can cause gt commands to fail. Run: %s\n\n",
|
|
style.Dim.Render("gt doctor --fix"))
|
|
}
|
|
|
|
// checkBeadsDependency verifies beads meets minimum version requirements.
|
|
// Skips check for exempt commands (version, help, completion).
|
|
// Deprecated: Use persistentPreRun instead, which calls CheckBeadsVersion.
|
|
func checkBeadsDependency(cmd *cobra.Command, args []string) error {
|
|
// Get the root command name being run
|
|
cmdName := cmd.Name()
|
|
|
|
// Skip check for exempt commands
|
|
if beadsExemptCommands[cmdName] {
|
|
return nil
|
|
}
|
|
|
|
// Check beads version
|
|
return CheckBeadsVersion()
|
|
}
|
|
|
|
// Execute runs the root command and returns an exit code.
|
|
// The caller (main) should call os.Exit with this code.
|
|
func Execute() int {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
// Check for silent exit (scripting commands that signal status via exit code)
|
|
if code, ok := IsSilentExit(err); ok {
|
|
return code
|
|
}
|
|
// Other errors already printed by cobra
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Command group IDs - used by subcommands to organize help output
|
|
const (
|
|
GroupWork = "work"
|
|
GroupAgents = "agents"
|
|
GroupComm = "comm"
|
|
GroupServices = "services"
|
|
GroupWorkspace = "workspace"
|
|
GroupConfig = "config"
|
|
GroupDiag = "diag"
|
|
)
|
|
|
|
func init() {
|
|
// Enable prefix matching for subcommands (e.g., "gt ref at" -> "gt refinery attach")
|
|
cobra.EnablePrefixMatching = true
|
|
|
|
// Define command groups (order determines help output order)
|
|
rootCmd.AddGroup(
|
|
&cobra.Group{ID: GroupWork, Title: "Work Management:"},
|
|
&cobra.Group{ID: GroupAgents, Title: "Agent Management:"},
|
|
&cobra.Group{ID: GroupComm, Title: "Communication:"},
|
|
&cobra.Group{ID: GroupServices, Title: "Services:"},
|
|
&cobra.Group{ID: GroupWorkspace, Title: "Workspace:"},
|
|
&cobra.Group{ID: GroupConfig, Title: "Configuration:"},
|
|
&cobra.Group{ID: GroupDiag, Title: "Diagnostics:"},
|
|
)
|
|
|
|
// Put help and completion in a sensible group
|
|
rootCmd.SetHelpCommandGroupID(GroupDiag)
|
|
rootCmd.SetCompletionCommandGroupID(GroupConfig)
|
|
|
|
// Global flags can be added here
|
|
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
|
}
|
|
|
|
// buildCommandPath walks the command hierarchy to build the full command path.
|
|
// For example: "gt mail send", "gt status", etc.
|
|
func buildCommandPath(cmd *cobra.Command) string {
|
|
var parts []string
|
|
for c := cmd; c != nil; c = c.Parent() {
|
|
parts = append([]string{c.Name()}, parts...)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// requireSubcommand returns a RunE function for parent commands that require
|
|
// a subcommand. Without this, Cobra silently shows help and exits 0 for
|
|
// unknown subcommands like "gt mol foobar", masking errors.
|
|
func requireSubcommand(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("requires a subcommand\n\nRun '%s --help' for usage", buildCommandPath(cmd))
|
|
}
|
|
return fmt.Errorf("unknown command %q for %q\n\nRun '%s --help' for available commands",
|
|
args[0], buildCommandPath(cmd), buildCommandPath(cmd))
|
|
}
|