package cmd import ( "fmt" "os" "os/exec" "path/filepath" "strings" "syscall" "github.com/steveyegge/gastown/internal/crew" "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) // inferRigFromCwd tries to determine the rig from the current directory. func inferRigFromCwd(townRoot string) (string, error) { cwd, err := filepath.Abs(".") if err != nil { return "", err } // Check if cwd is within a rig rel, err := filepath.Rel(townRoot, cwd) if err != nil { return "", fmt.Errorf("not in workspace") } // Normalize and split path - first component is the rig name rel = filepath.ToSlash(rel) parts := strings.Split(rel, "/") if len(parts) > 0 && parts[0] != "" && parts[0] != "." { return parts[0], nil } return "", fmt.Errorf("could not infer rig from current directory") } // getCrewManager returns a crew manager for the specified or inferred rig. func getCrewManager(rigName string) (*crew.Manager, *rig.Rig, error) { // Handle optional rig inference from cwd if rigName == "" { townRoot, err := workspace.FindFromCwdOrError() if err != nil { return nil, nil, fmt.Errorf("not in a Gas Town workspace: %w", err) } rigName, err = inferRigFromCwd(townRoot) if err != nil { return nil, nil, fmt.Errorf("could not determine rig (use --rig flag): %w", err) } } _, r, err := getRig(rigName) if err != nil { return nil, nil, err } crewGit := git.NewGit(r.Path) crewMgr := crew.NewManager(r, crewGit) return crewMgr, r, nil } // crewSessionName generates the tmux session name for a crew worker. func crewSessionName(rigName, crewName string) string { return fmt.Sprintf("gt-%s-crew-%s", rigName, crewName) } // crewDetection holds the result of detecting crew workspace from cwd. type crewDetection struct { rigName string crewName string } // detectCrewFromCwd attempts to detect the crew workspace from the current directory. // It looks for the pattern //crew// in the current path. func detectCrewFromCwd() (*crewDetection, error) { cwd, err := os.Getwd() if err != nil { return nil, fmt.Errorf("getting cwd: %w", err) } // Find town root townRoot, err := workspace.FindFromCwd() if err != nil { return nil, fmt.Errorf("not in Gas Town workspace: %w", err) } if townRoot == "" { return nil, fmt.Errorf("not in Gas Town workspace") } // Get relative path from town root relPath, err := filepath.Rel(townRoot, cwd) if err != nil { return nil, fmt.Errorf("getting relative path: %w", err) } // Normalize and split path relPath = filepath.ToSlash(relPath) parts := strings.Split(relPath, "/") // Look for pattern: /crew//... // Minimum: rig, crew, name = 3 parts if len(parts) < 3 { return nil, fmt.Errorf("not in a crew workspace (path too short)") } rigName := parts[0] if parts[1] != "crew" { return nil, fmt.Errorf("not in a crew workspace (not in crew/ directory)") } crewName := parts[2] return &crewDetection{ rigName: rigName, crewName: crewName, }, nil } // isShellCommand checks if the command is a shell (meaning Claude has exited). func isShellCommand(cmd string) bool { shells := []string{"bash", "zsh", "sh", "fish", "tcsh", "ksh"} for _, shell := range shells { if cmd == shell { return true } } return false } // execClaude execs claude, replacing the current process. // Used when we're already in the target session and just need to start Claude. // If prompt is provided, it's passed as the initial prompt to Claude. func execClaude(prompt string) error { claudePath, err := exec.LookPath("claude") if err != nil { return fmt.Errorf("claude not found: %w", err) } // exec replaces current process with claude args := []string{"claude", "--dangerously-skip-permissions"} if prompt != "" { args = append(args, prompt) } return syscall.Exec(claudePath, args, os.Environ()) } // isInTmuxSession checks if we're currently inside the target tmux session. func isInTmuxSession(targetSession string) bool { // TMUX env var format: /tmp/tmux-501/default,12345,0 // We need to get the current session name via tmux display-message tmuxEnv := os.Getenv("TMUX") if tmuxEnv == "" { return false // Not in tmux at all } // Get current session name cmd := exec.Command("tmux", "display-message", "-p", "#{session_name}") out, err := cmd.Output() if err != nil { return false } currentSession := strings.TrimSpace(string(out)) return currentSession == targetSession } // attachToTmuxSession attaches to a tmux session with proper TTY forwarding. func attachToTmuxSession(sessionID string) error { tmuxPath, err := exec.LookPath("tmux") if err != nil { return fmt.Errorf("tmux not found: %w", err) } cmd := exec.Command(tmuxPath, "attach-session", "-t", sessionID) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } // ensureMainBranch checks if a git directory is on main branch. // If not, warns the user and offers to switch. // Returns true if on main (or switched to main), false if user declined. func ensureMainBranch(dir, roleName string) bool { g := git.NewGit(dir) branch, err := g.CurrentBranch() if err != nil { // Not a git repo or other error, skip check return true } if branch == "main" || branch == "master" { return true } // Warn about wrong branch fmt.Printf("\n%s %s is on branch '%s', not main\n", style.Warning.Render("⚠"), roleName, branch) fmt.Println(" Persistent roles should work on main to avoid orphaned work.") fmt.Println() // Auto-switch to main fmt.Printf(" Switching to main...\n") if err := g.Checkout("main"); err != nil { fmt.Printf(" %s Could not switch to main: %v\n", style.Error.Render("✗"), err) fmt.Println(" Please manually run: git checkout main && git pull") return false } // Pull latest if err := g.Pull("origin", "main"); err != nil { fmt.Printf(" %s Pull failed (continuing anyway): %v\n", style.Warning.Render("⚠"), err) } else { fmt.Printf(" %s Switched to main and pulled latest\n", style.Success.Render("✓")) } return true }