Parent commands (mol, mail, crew, polecat, etc.) previously showed help and exited 0 for unknown subcommands like "gt mol foobar". This masked errors in scripts and confused users. Added requireSubcommand() helper to root.go and applied it to all parent commands. Now unknown subcommands properly error with exit code 1. Example before: gt mol unhook → shows help, exits 0 Example after: gt mol unhook → "Error: unknown command "unhook"", exits 1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
336 lines
8.7 KiB
Go
336 lines
8.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/config"
|
|
"github.com/steveyegge/gastown/internal/constants"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
// Account command flags
|
|
var (
|
|
accountJSON bool
|
|
accountEmail string
|
|
accountDescription string
|
|
)
|
|
|
|
var accountCmd = &cobra.Command{
|
|
Use: "account",
|
|
GroupID: GroupConfig,
|
|
Short: "Manage Claude Code accounts",
|
|
RunE: requireSubcommand,
|
|
Long: `Manage multiple Claude Code accounts for Gas Town.
|
|
|
|
This enables switching between accounts (e.g., personal vs work) with
|
|
easy account selection per spawn or globally.
|
|
|
|
Commands:
|
|
gt account list List registered accounts
|
|
gt account add <handle> Add a new account
|
|
gt account default <handle> Set the default account
|
|
gt account status Show current account info`,
|
|
}
|
|
|
|
var accountListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List registered accounts",
|
|
Long: `List all registered Claude Code accounts.
|
|
|
|
Shows account handles, emails, and which is the default.
|
|
|
|
Examples:
|
|
gt account list # Text output
|
|
gt account list --json # JSON output`,
|
|
RunE: runAccountList,
|
|
}
|
|
|
|
var accountAddCmd = &cobra.Command{
|
|
Use: "add <handle>",
|
|
Short: "Add a new account",
|
|
Long: `Add a new Claude Code account.
|
|
|
|
Creates a config directory at ~/.claude-accounts/<handle> and registers
|
|
the account. You'll need to run 'claude' with CLAUDE_CONFIG_DIR set to
|
|
that directory to complete the login.
|
|
|
|
Examples:
|
|
gt account add work
|
|
gt account add work --email steve@company.com
|
|
gt account add work --email steve@company.com --desc "Work account"`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runAccountAdd,
|
|
}
|
|
|
|
var accountDefaultCmd = &cobra.Command{
|
|
Use: "default <handle>",
|
|
Short: "Set the default account",
|
|
Long: `Set the default Claude Code account.
|
|
|
|
The default account is used when no --account flag or GT_ACCOUNT env var
|
|
is specified during spawn or attach.
|
|
|
|
Examples:
|
|
gt account default work
|
|
gt account default personal`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runAccountDefault,
|
|
}
|
|
|
|
// AccountListItem represents an account in list output.
|
|
type AccountListItem struct {
|
|
Handle string `json:"handle"`
|
|
Email string `json:"email"`
|
|
Description string `json:"description,omitempty"`
|
|
ConfigDir string `json:"config_dir"`
|
|
IsDefault bool `json:"is_default"`
|
|
}
|
|
|
|
func runAccountList(cmd *cobra.Command, args []string) error {
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil {
|
|
return fmt.Errorf("finding town root: %w", err)
|
|
}
|
|
|
|
accountsPath := constants.MayorAccountsPath(townRoot)
|
|
cfg, err := config.LoadAccountsConfig(accountsPath)
|
|
if err != nil {
|
|
// If file doesn't exist, show empty message
|
|
fmt.Println("No accounts configured.")
|
|
fmt.Println("\nTo add an account:")
|
|
fmt.Println(" gt account add <handle>")
|
|
return nil
|
|
}
|
|
|
|
if len(cfg.Accounts) == 0 {
|
|
fmt.Println("No accounts configured.")
|
|
fmt.Println("\nTo add an account:")
|
|
fmt.Println(" gt account add <handle>")
|
|
return nil
|
|
}
|
|
|
|
// Build list items
|
|
var items []AccountListItem
|
|
for handle, acct := range cfg.Accounts {
|
|
items = append(items, AccountListItem{
|
|
Handle: handle,
|
|
Email: acct.Email,
|
|
Description: acct.Description,
|
|
ConfigDir: acct.ConfigDir,
|
|
IsDefault: handle == cfg.Default,
|
|
})
|
|
}
|
|
|
|
// Sort by handle for consistent output
|
|
sort.Slice(items, func(i, j int) bool {
|
|
return items[i].Handle < items[j].Handle
|
|
})
|
|
|
|
if accountJSON {
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(items)
|
|
}
|
|
|
|
// Text output
|
|
fmt.Printf("%s\n\n", style.Bold.Render("Claude Code Accounts"))
|
|
for _, item := range items {
|
|
marker := " "
|
|
if item.IsDefault {
|
|
marker = "* "
|
|
}
|
|
|
|
fmt.Printf("%s%s", marker, style.Bold.Render(item.Handle))
|
|
if item.Email != "" {
|
|
fmt.Printf(" %s", item.Email)
|
|
}
|
|
if item.IsDefault {
|
|
fmt.Printf(" %s", style.Dim.Render("(default)"))
|
|
}
|
|
fmt.Println()
|
|
|
|
if item.Description != "" {
|
|
fmt.Printf(" %s\n", style.Dim.Render(item.Description))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runAccountAdd(cmd *cobra.Command, args []string) error {
|
|
handle := args[0]
|
|
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil {
|
|
return fmt.Errorf("finding town root: %w", err)
|
|
}
|
|
|
|
accountsPath := constants.MayorAccountsPath(townRoot)
|
|
|
|
// Load existing config or create new
|
|
cfg, err := config.LoadAccountsConfig(accountsPath)
|
|
if err != nil {
|
|
cfg = config.NewAccountsConfig()
|
|
}
|
|
|
|
// Check if account already exists
|
|
if _, exists := cfg.Accounts[handle]; exists {
|
|
return fmt.Errorf("account '%s' already exists", handle)
|
|
}
|
|
|
|
// Build config directory path
|
|
configDir := config.DefaultAccountsConfigDir() + "/" + handle
|
|
|
|
// Create the config directory
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return fmt.Errorf("creating config directory: %w", err)
|
|
}
|
|
|
|
// Add account
|
|
cfg.Accounts[handle] = config.Account{
|
|
Email: accountEmail,
|
|
Description: accountDescription,
|
|
ConfigDir: configDir,
|
|
}
|
|
|
|
// If this is the first account, make it default
|
|
if cfg.Default == "" {
|
|
cfg.Default = handle
|
|
}
|
|
|
|
// Save config
|
|
if err := config.SaveAccountsConfig(accountsPath, cfg); err != nil {
|
|
return fmt.Errorf("saving accounts config: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Added account '%s'\n", handle)
|
|
fmt.Printf("Config directory: %s\n", configDir)
|
|
fmt.Println()
|
|
fmt.Println("To complete login, run:")
|
|
fmt.Printf(" CLAUDE_CONFIG_DIR=%s claude\n", configDir)
|
|
fmt.Println("Then use /login to authenticate.")
|
|
|
|
return nil
|
|
}
|
|
|
|
func runAccountDefault(cmd *cobra.Command, args []string) error {
|
|
handle := args[0]
|
|
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil {
|
|
return fmt.Errorf("finding town root: %w", err)
|
|
}
|
|
|
|
accountsPath := constants.MayorAccountsPath(townRoot)
|
|
cfg, err := config.LoadAccountsConfig(accountsPath)
|
|
if err != nil {
|
|
return fmt.Errorf("loading accounts config: %w", err)
|
|
}
|
|
|
|
// Check if account exists
|
|
if _, exists := cfg.Accounts[handle]; !exists {
|
|
return fmt.Errorf("account '%s' not found", handle)
|
|
}
|
|
|
|
// Update default
|
|
cfg.Default = handle
|
|
|
|
// Save config
|
|
if err := config.SaveAccountsConfig(accountsPath, cfg); err != nil {
|
|
return fmt.Errorf("saving accounts config: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Default account set to '%s'\n", handle)
|
|
return nil
|
|
}
|
|
|
|
var accountStatusCmd = &cobra.Command{
|
|
Use: "status",
|
|
Short: "Show current account info",
|
|
Long: `Show which Claude Code account would be used for new sessions.
|
|
|
|
Displays the currently resolved account based on:
|
|
1. GT_ACCOUNT environment variable (highest priority)
|
|
2. Default account from config
|
|
|
|
Examples:
|
|
gt account status # Show current account
|
|
GT_ACCOUNT=work gt account status # Show with env override`,
|
|
RunE: runAccountStatus,
|
|
}
|
|
|
|
func runAccountStatus(cmd *cobra.Command, args []string) error {
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil {
|
|
return fmt.Errorf("finding town root: %w", err)
|
|
}
|
|
|
|
accountsPath := constants.MayorAccountsPath(townRoot)
|
|
|
|
// Resolve account (empty flag since we want to show default resolution)
|
|
configDir, handle, err := config.ResolveAccountConfigDir(accountsPath, "")
|
|
if err != nil {
|
|
return fmt.Errorf("resolving account: %w", err)
|
|
}
|
|
|
|
if handle == "" {
|
|
fmt.Println("No account configured.")
|
|
fmt.Println("\nTo add an account:")
|
|
fmt.Println(" gt account add <handle>")
|
|
return nil
|
|
}
|
|
|
|
// Check if GT_ACCOUNT is overriding
|
|
envAccount := os.Getenv("GT_ACCOUNT")
|
|
|
|
// Load config to get full account info
|
|
cfg, err := config.LoadAccountsConfig(accountsPath)
|
|
if err != nil {
|
|
return fmt.Errorf("loading accounts config: %w", err)
|
|
}
|
|
|
|
acct := cfg.GetAccount(handle)
|
|
if acct == nil {
|
|
return fmt.Errorf("account '%s' not found", handle)
|
|
}
|
|
|
|
fmt.Printf("%s\n\n", style.Bold.Render("Current Account"))
|
|
fmt.Printf("Handle: %s\n", style.Bold.Render(handle))
|
|
if acct.Email != "" {
|
|
fmt.Printf("Email: %s\n", acct.Email)
|
|
}
|
|
if acct.Description != "" {
|
|
fmt.Printf("Description: %s\n", acct.Description)
|
|
}
|
|
fmt.Printf("Config Dir: %s\n", configDir)
|
|
|
|
if envAccount != "" {
|
|
fmt.Printf("\n%s\n", style.Dim.Render("(set via GT_ACCOUNT environment variable)"))
|
|
} else if handle == cfg.Default {
|
|
fmt.Printf("\n%s\n", style.Dim.Render("(default account)"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func init() {
|
|
// Add flags
|
|
accountListCmd.Flags().BoolVar(&accountJSON, "json", false, "Output as JSON")
|
|
|
|
accountAddCmd.Flags().StringVar(&accountEmail, "email", "", "Account email address")
|
|
accountAddCmd.Flags().StringVar(&accountDescription, "desc", "", "Account description")
|
|
|
|
// Add subcommands
|
|
accountCmd.AddCommand(accountListCmd)
|
|
accountCmd.AddCommand(accountAddCmd)
|
|
accountCmd.AddCommand(accountDefaultCmd)
|
|
accountCmd.AddCommand(accountStatusCmd)
|
|
|
|
rootCmd.AddCommand(accountCmd)
|
|
}
|