feat: Set and Forget - Seamless Gas Town Integration (#255)
Adds shell integration for automatic Gas Town context detection. Features: - `gt enable` / `gt disable` - Global on/off switch - `gt shell install|remove|status` - Shell integration management - `gt rig quick-add [path]` - One-command project setup - `gt uninstall` - Clean removal with options - Shell hook auto-sets GT_TOWN_ROOT/GT_RIG on cd Implementation: - XDG-compliant state storage (~/.local/state/gastown/) - Safe RC file manipulation with block markers - Environment overrides (GASTOWN_DISABLED/ENABLED) - Doctor check for global state validation Co-authored-by: Sohail Mohammad <sohailm25@gmail.com>
This commit is contained in:
@@ -84,14 +84,12 @@ func (v beadsVersion) compare(other beadsVersion) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// getBeadsVersion executes `bd --version` and parses the output.
|
||||
// Returns the version string (e.g., "0.44.0") or error.
|
||||
func getBeadsVersion() (string, error) {
|
||||
cmd := exec.Command("bd", "--version")
|
||||
cmd := exec.Command("bd", "version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("bd --version failed: %s", string(exitErr.Stderr))
|
||||
return "", fmt.Errorf("bd version failed: %s", string(exitErr.Stderr))
|
||||
}
|
||||
return "", fmt.Errorf("failed to run bd: %w (is beads installed?)", err)
|
||||
}
|
||||
|
||||
@@ -56,9 +56,7 @@ func runCrewAdd(cmd *cobra.Command, args []string) error {
|
||||
crewGit := git.NewGit(r.Path)
|
||||
crewMgr := crew.NewManager(r, crewGit)
|
||||
|
||||
// Beads for agent bead creation (use mayor/rig where beads.db lives)
|
||||
// The rig root .beads/ only has config.yaml, no database.
|
||||
bd := beads.New(filepath.Join(r.Path, "mayor", "rig"))
|
||||
bd := beads.New(beads.ResolveBeadsDir(r.Path))
|
||||
|
||||
// Track results
|
||||
var created []string
|
||||
|
||||
72
internal/cmd/disable.go
Normal file
72
internal/cmd/disable.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// ABOUTME: Command to disable Gas Town system-wide.
|
||||
// ABOUTME: Sets the global state to disabled so tools work vanilla.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/shell"
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
var disableClean bool
|
||||
|
||||
var disableCmd = &cobra.Command{
|
||||
Use: "disable",
|
||||
GroupID: GroupConfig,
|
||||
Short: "Disable Gas Town system-wide",
|
||||
Long: `Disable Gas Town for all agentic coding tools.
|
||||
|
||||
When disabled:
|
||||
- Shell hooks become no-ops
|
||||
- Claude Code SessionStart hooks skip 'gt prime'
|
||||
- Tools work 100% vanilla (no Gas Town behavior)
|
||||
|
||||
The workspace (~/gt) is preserved. Use 'gt enable' to re-enable.
|
||||
|
||||
Flags:
|
||||
--clean Also remove shell integration from ~/.zshrc/~/.bashrc
|
||||
|
||||
Environment overrides still work:
|
||||
GASTOWN_ENABLED=1 - Enable for current session only`,
|
||||
RunE: runDisable,
|
||||
}
|
||||
|
||||
func init() {
|
||||
disableCmd.Flags().BoolVar(&disableClean, "clean", false,
|
||||
"Remove shell integration from RC files")
|
||||
rootCmd.AddCommand(disableCmd)
|
||||
}
|
||||
|
||||
func runDisable(cmd *cobra.Command, args []string) error {
|
||||
if err := state.Disable(); err != nil {
|
||||
return fmt.Errorf("disabling Gas Town: %w", err)
|
||||
}
|
||||
|
||||
if disableClean {
|
||||
if err := removeShellIntegration(); err != nil {
|
||||
fmt.Printf("%s Could not clean shell integration: %v\n",
|
||||
style.Warning.Render("!"), err)
|
||||
} else {
|
||||
fmt.Println(" Removed shell integration from RC files")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%s Gas Town disabled\n", style.Success.Render("✓"))
|
||||
fmt.Println()
|
||||
fmt.Println("All agentic coding tools now work vanilla.")
|
||||
if !disableClean {
|
||||
fmt.Printf("Use %s to also remove shell hooks\n",
|
||||
style.Dim.Render("gt disable --clean"))
|
||||
}
|
||||
fmt.Printf("Use %s to re-enable\n", style.Dim.Render("gt enable"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeShellIntegration() error {
|
||||
return shell.Remove()
|
||||
}
|
||||
@@ -108,6 +108,8 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
// Register workspace-level checks first (fundamental)
|
||||
d.RegisterAll(doctor.WorkspaceChecks()...)
|
||||
|
||||
d.Register(doctor.NewGlobalStateCheck())
|
||||
|
||||
// Register built-in checks
|
||||
d.Register(doctor.NewTownGitCheck())
|
||||
d.Register(doctor.NewDaemonCheck())
|
||||
|
||||
54
internal/cmd/enable.go
Normal file
54
internal/cmd/enable.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// ABOUTME: Command to enable Gas Town system-wide.
|
||||
// ABOUTME: Sets the global state to enabled for all agentic coding tools.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
var enableCmd = &cobra.Command{
|
||||
Use: "enable",
|
||||
GroupID: GroupConfig,
|
||||
Short: "Enable Gas Town system-wide",
|
||||
Long: `Enable Gas Town for all agentic coding tools.
|
||||
|
||||
When enabled:
|
||||
- Shell hooks set GT_TOWN_ROOT and GT_RIG environment variables
|
||||
- Claude Code SessionStart hooks run 'gt prime' for context
|
||||
- Git repos are auto-registered as rigs (configurable)
|
||||
|
||||
Use 'gt disable' to turn off. Use 'gt status --global' to check state.
|
||||
|
||||
Environment overrides:
|
||||
GASTOWN_DISABLED=1 - Disable for current session only
|
||||
GASTOWN_ENABLED=1 - Enable for current session only`,
|
||||
RunE: runEnable,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(enableCmd)
|
||||
}
|
||||
|
||||
func runEnable(cmd *cobra.Command, args []string) error {
|
||||
if err := state.Enable(Version); err != nil {
|
||||
return fmt.Errorf("enabling Gas Town: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Gas Town enabled\n", style.Success.Render("✓"))
|
||||
fmt.Println()
|
||||
fmt.Println("Gas Town will now:")
|
||||
fmt.Println(" • Inject context into Claude Code sessions")
|
||||
fmt.Println(" • Set GT_TOWN_ROOT and GT_RIG environment variables")
|
||||
fmt.Println(" • Auto-register git repos as rigs (if configured)")
|
||||
fmt.Println()
|
||||
fmt.Printf("Use %s to disable, %s to check status\n",
|
||||
style.Dim.Render("gt disable"),
|
||||
style.Dim.Render("gt status --global"))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -16,9 +16,12 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/deps"
|
||||
"github.com/steveyegge/gastown/internal/formula"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/shell"
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/templates"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
"github.com/steveyegge/gastown/internal/wrappers"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,6 +33,8 @@ var (
|
||||
installGit bool
|
||||
installGitHub string
|
||||
installPublic bool
|
||||
installShell bool
|
||||
installWrappers bool
|
||||
)
|
||||
|
||||
var installCmd = &cobra.Command{
|
||||
@@ -55,7 +60,8 @@ Examples:
|
||||
gt install ~/gt --no-beads # Skip .beads/ initialization
|
||||
gt install ~/gt --git # Also init git with .gitignore
|
||||
gt install ~/gt --github=user/repo # Create private GitHub repo (default)
|
||||
gt install ~/gt --github=user/repo --public # Create public GitHub repo`,
|
||||
gt install ~/gt --github=user/repo --public # Create public GitHub repo
|
||||
gt install ~/gt --shell # Install shell integration (sets GT_TOWN_ROOT/GT_RIG)`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runInstall,
|
||||
}
|
||||
@@ -69,6 +75,8 @@ func init() {
|
||||
installCmd.Flags().BoolVar(&installGit, "git", false, "Initialize git with .gitignore")
|
||||
installCmd.Flags().StringVar(&installGitHub, "github", "", "Create GitHub repo (format: owner/repo, private by default)")
|
||||
installCmd.Flags().BoolVar(&installPublic, "public", false, "Make GitHub repo public (use with --github)")
|
||||
installCmd.Flags().BoolVar(&installShell, "shell", false, "Install shell integration (sets GT_TOWN_ROOT/GT_RIG env vars)")
|
||||
installCmd.Flags().BoolVar(&installWrappers, "wrappers", false, "Install gt-codex/gt-opencode wrapper scripts to ~/bin/")
|
||||
rootCmd.AddCommand(installCmd)
|
||||
}
|
||||
|
||||
@@ -260,6 +268,29 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf(" ✓ Created .claude/commands/ (slash commands for all agents)\n")
|
||||
}
|
||||
|
||||
if installShell {
|
||||
fmt.Println()
|
||||
if err := shell.Install(); err != nil {
|
||||
fmt.Printf(" %s Could not install shell integration: %v\n", style.Dim.Render("⚠"), err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Installed shell integration (%s)\n", shell.RCFilePath(shell.DetectShell()))
|
||||
}
|
||||
if err := state.Enable(Version); err != nil {
|
||||
fmt.Printf(" %s Could not enable Gas Town: %v\n", style.Dim.Render("⚠"), err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Enabled Gas Town globally\n")
|
||||
}
|
||||
}
|
||||
|
||||
if installWrappers {
|
||||
fmt.Println()
|
||||
if err := wrappers.Install(); err != nil {
|
||||
fmt.Printf(" %s Could not install wrapper scripts: %v\n", style.Dim.Render("⚠"), err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ Installed gt-codex and gt-opencode to %s\n", wrappers.BinDir())
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s HQ created successfully!\n", style.Bold.Render("✓"))
|
||||
fmt.Println()
|
||||
fmt.Println("Next steps:")
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/lock"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/templates"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -81,12 +82,15 @@ func init() {
|
||||
type RoleContext = RoleInfo
|
||||
|
||||
func runPrime(cmd *cobra.Command, args []string) error {
|
||||
if !state.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
// Find town root
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
|
||||
147
internal/cmd/rig_detect.go
Normal file
147
internal/cmd/rig_detect.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// ABOUTME: Hidden command for shell hook to detect rigs and update cache.
|
||||
// ABOUTME: Called by shell integration to set GT_TOWN_ROOT and GT_RIG env vars.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
var rigDetectCache string
|
||||
|
||||
var rigDetectCmd = &cobra.Command{
|
||||
Use: "detect [path]",
|
||||
Short: "Detect rig from repository path (internal use)",
|
||||
Hidden: true,
|
||||
Long: `Detect rig from a repository path and optionally cache the result.
|
||||
|
||||
This is an internal command used by shell integration. It checks if the given
|
||||
path is inside a Gas Town rig and outputs shell variable assignments.
|
||||
|
||||
When --cache is specified, the result is written to ~/.cache/gastown/rigs.cache
|
||||
for fast lookups by the shell hook.
|
||||
|
||||
Output format (to stdout):
|
||||
export GT_TOWN_ROOT=/path/to/town
|
||||
export GT_RIG=rigname
|
||||
|
||||
Or if not in a rig:
|
||||
unset GT_TOWN_ROOT GT_RIG`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runRigDetect,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rigCmd.AddCommand(rigDetectCmd)
|
||||
rigDetectCmd.Flags().StringVar(&rigDetectCache, "cache", "", "Repository path to cache detection result for")
|
||||
}
|
||||
|
||||
func runRigDetect(cmd *cobra.Command, args []string) error {
|
||||
checkPath := "."
|
||||
if len(args) > 0 {
|
||||
checkPath = args[0]
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(checkPath)
|
||||
if err != nil {
|
||||
return outputNotInRig()
|
||||
}
|
||||
|
||||
townRoot, err := workspace.Find(absPath)
|
||||
if err != nil || townRoot == "" {
|
||||
return outputNotInRig()
|
||||
}
|
||||
|
||||
rigName := detectRigFromPath(townRoot, absPath)
|
||||
|
||||
if rigName != "" {
|
||||
fmt.Printf("export GT_TOWN_ROOT=%q\n", townRoot)
|
||||
fmt.Printf("export GT_RIG=%q\n", rigName)
|
||||
} else {
|
||||
fmt.Printf("export GT_TOWN_ROOT=%q\n", townRoot)
|
||||
fmt.Println("unset GT_RIG")
|
||||
}
|
||||
|
||||
if rigDetectCache != "" {
|
||||
if err := updateRigCache(rigDetectCache, townRoot, rigName); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: could not update cache: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectRigFromPath(townRoot, absPath string) string {
|
||||
rel, err := filepath.Rel(townRoot, absPath)
|
||||
if err != nil || strings.HasPrefix(rel, "..") {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(rel, string(filepath.Separator))
|
||||
if len(parts) == 0 || parts[0] == "." {
|
||||
return ""
|
||||
}
|
||||
|
||||
candidateRig := parts[0]
|
||||
|
||||
switch candidateRig {
|
||||
case "mayor", "deacon", ".beads", ".claude", ".git", "plugins":
|
||||
return ""
|
||||
}
|
||||
|
||||
rigConfigPath := filepath.Join(townRoot, candidateRig, "config.json")
|
||||
if _, err := os.Stat(rigConfigPath); err == nil {
|
||||
return candidateRig
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func outputNotInRig() error {
|
||||
fmt.Println("unset GT_TOWN_ROOT GT_RIG")
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateRigCache(repoRoot, townRoot, rigName string) error {
|
||||
cacheDir := state.CacheDir()
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cachePath := filepath.Join(cacheDir, "rigs.cache")
|
||||
|
||||
existing := make(map[string]string)
|
||||
if data, err := os.ReadFile(cachePath); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if idx := strings.Index(line, ":"); idx > 0 {
|
||||
existing[line[:idx]] = line[idx+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var value string
|
||||
if rigName != "" {
|
||||
value = fmt.Sprintf("export GT_TOWN_ROOT=%q; export GT_RIG=%q", townRoot, rigName)
|
||||
} else if townRoot != "" {
|
||||
value = fmt.Sprintf("export GT_TOWN_ROOT=%q; unset GT_RIG", townRoot)
|
||||
} else {
|
||||
value = "unset GT_TOWN_ROOT GT_RIG"
|
||||
}
|
||||
|
||||
existing[repoRoot] = value
|
||||
|
||||
var lines []string
|
||||
for k, v := range existing {
|
||||
lines = append(lines, k+":"+v)
|
||||
}
|
||||
|
||||
return os.WriteFile(cachePath, []byte(strings.Join(lines, "\n")+"\n"), 0644)
|
||||
}
|
||||
186
internal/cmd/rig_quick_add.go
Normal file
186
internal/cmd/rig_quick_add.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// ABOUTME: Quick-add command for adding a repo to Gas Town with minimal friction.
|
||||
// ABOUTME: Used by shell hook for automatic "add to Gas Town?" prompts.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
var (
|
||||
quickAddUser string
|
||||
quickAddYes bool
|
||||
quickAddQuiet bool
|
||||
)
|
||||
|
||||
var rigQuickAddCmd = &cobra.Command{
|
||||
Use: "quick-add [path]",
|
||||
Short: "Quickly add current repo to Gas Town",
|
||||
Hidden: true,
|
||||
Long: `Quickly add a git repository to Gas Town with minimal interaction.
|
||||
|
||||
This command is designed for the shell hook's "Add to Gas Town?" prompt.
|
||||
It infers the rig name from the directory and git URL from the remote.
|
||||
|
||||
Examples:
|
||||
gt rig quick-add # Add current directory
|
||||
gt rig quick-add ~/Repos/myproject # Add specific path
|
||||
gt rig quick-add --yes # Non-interactive`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runRigQuickAdd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rigCmd.AddCommand(rigQuickAddCmd)
|
||||
rigQuickAddCmd.Flags().StringVar(&quickAddUser, "user", "", "Crew workspace name (default: $USER)")
|
||||
rigQuickAddCmd.Flags().BoolVar(&quickAddYes, "yes", false, "Non-interactive, assume yes")
|
||||
rigQuickAddCmd.Flags().BoolVar(&quickAddQuiet, "quiet", false, "Minimal output")
|
||||
}
|
||||
|
||||
func runRigQuickAdd(cmd *cobra.Command, args []string) error {
|
||||
targetPath := "."
|
||||
if len(args) > 0 {
|
||||
targetPath = args[0]
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving path: %w", err)
|
||||
}
|
||||
|
||||
if townRoot, err := workspace.Find(absPath); err == nil && townRoot != "" {
|
||||
return fmt.Errorf("already part of a Gas Town workspace: %s", townRoot)
|
||||
}
|
||||
|
||||
gitRoot, err := findGitRoot(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("not a git repository: %w", err)
|
||||
}
|
||||
|
||||
gitURL, err := findGitRemoteURL(gitRoot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("no git remote found: %w", err)
|
||||
}
|
||||
|
||||
rigName := sanitizeRigName(filepath.Base(gitRoot))
|
||||
|
||||
townRoot, err := findOrCreateTown()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding Gas Town: %w", err)
|
||||
}
|
||||
|
||||
rigPath := filepath.Join(townRoot, rigName)
|
||||
if _, err := os.Stat(rigPath); err == nil {
|
||||
return fmt.Errorf("rig %q already exists in %s", rigName, townRoot)
|
||||
}
|
||||
|
||||
originalName := filepath.Base(gitRoot)
|
||||
if rigName != originalName && !quickAddQuiet {
|
||||
fmt.Printf("Note: Using %q as rig name (sanitized from %q)\n", rigName, originalName)
|
||||
}
|
||||
|
||||
if !quickAddQuiet {
|
||||
fmt.Printf("Adding %s to Gas Town...\n", style.Bold.Render(rigName))
|
||||
fmt.Printf(" Repository: %s\n", gitURL)
|
||||
fmt.Printf(" Town: %s\n", townRoot)
|
||||
}
|
||||
|
||||
addArgs := []string{"rig", "add", rigName, gitURL}
|
||||
addCmd := exec.Command("gt", addArgs...)
|
||||
addCmd.Dir = townRoot
|
||||
addCmd.Stdout = os.Stdout
|
||||
addCmd.Stderr = os.Stderr
|
||||
if err := addCmd.Run(); err != nil {
|
||||
fmt.Printf("\n%s Failed to add rig. You can try manually:\n", style.Warning.Render("⚠"))
|
||||
fmt.Printf(" cd %s && gt rig add %s %s\n", townRoot, rigName, gitURL)
|
||||
return fmt.Errorf("gt rig add failed: %w", err)
|
||||
}
|
||||
|
||||
user := quickAddUser
|
||||
if user == "" {
|
||||
user = os.Getenv("USER")
|
||||
}
|
||||
if user == "" {
|
||||
user = "default"
|
||||
}
|
||||
|
||||
if !quickAddQuiet {
|
||||
fmt.Printf("\nCreating crew workspace for %s...\n", user)
|
||||
}
|
||||
|
||||
crewArgs := []string{"crew", "add", user, "--rig", rigName}
|
||||
crewCmd := exec.Command("gt", crewArgs...)
|
||||
crewCmd.Dir = filepath.Join(townRoot, rigName)
|
||||
crewCmd.Stdout = os.Stdout
|
||||
crewCmd.Stderr = os.Stderr
|
||||
if err := crewCmd.Run(); err != nil {
|
||||
fmt.Printf(" %s Could not create crew workspace: %v\n", style.Dim.Render("⚠"), err)
|
||||
fmt.Printf(" Run manually: cd %s && gt crew add %s --rig %s\n", filepath.Join(townRoot, rigName), user, rigName)
|
||||
}
|
||||
|
||||
crewPath := filepath.Join(townRoot, rigName, "crew", user)
|
||||
if !quickAddQuiet {
|
||||
fmt.Printf("\n%s Added to Gas Town!\n", style.Success.Render("✓"))
|
||||
fmt.Printf("\nYour workspace: %s\n", style.Bold.Render(crewPath))
|
||||
}
|
||||
|
||||
fmt.Printf("GT_CREW_PATH=%s\n", crewPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findGitRoot(path string) (string, error) {
|
||||
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
cmd.Dir = path
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func findGitRemoteURL(gitRoot string) (string, error) {
|
||||
cmd := exec.Command("git", "remote", "get-url", "origin")
|
||||
cmd.Dir = gitRoot
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func sanitizeRigName(name string) string {
|
||||
name = strings.ReplaceAll(name, "-", "_")
|
||||
name = strings.ReplaceAll(name, ".", "_")
|
||||
name = strings.ReplaceAll(name, " ", "_")
|
||||
return name
|
||||
}
|
||||
|
||||
func findOrCreateTown() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
filepath.Join(home, "gt"),
|
||||
filepath.Join(home, "gastown"),
|
||||
}
|
||||
|
||||
for _, path := range candidates {
|
||||
mayorDir := filepath.Join(path, "mayor")
|
||||
if _, err := os.Stat(mayorDir); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no Gas Town found - run 'gt install ~/gt' first")
|
||||
}
|
||||
99
internal/cmd/shell.go
Normal file
99
internal/cmd/shell.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// ABOUTME: Shell integration management commands.
|
||||
// ABOUTME: Install/remove shell hooks without full HQ setup.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/shell"
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
var shellCmd = &cobra.Command{
|
||||
Use: "shell",
|
||||
GroupID: GroupConfig,
|
||||
Short: "Manage shell integration",
|
||||
RunE: requireSubcommand,
|
||||
}
|
||||
|
||||
var shellInstallCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install or update shell integration",
|
||||
Long: `Install or update the Gas Town shell integration.
|
||||
|
||||
This adds a hook to your shell RC file that:
|
||||
- Sets GT_TOWN_ROOT and GT_RIG when you cd into a Gas Town rig
|
||||
- Offers to add new git repos to Gas Town on first visit
|
||||
|
||||
Run this after upgrading gt to get the latest shell hook features.`,
|
||||
RunE: runShellInstall,
|
||||
}
|
||||
|
||||
var shellRemoveCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Remove shell integration",
|
||||
RunE: runShellRemove,
|
||||
}
|
||||
|
||||
var shellStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show shell integration status",
|
||||
RunE: runShellStatus,
|
||||
}
|
||||
|
||||
func init() {
|
||||
shellCmd.AddCommand(shellInstallCmd)
|
||||
shellCmd.AddCommand(shellRemoveCmd)
|
||||
shellCmd.AddCommand(shellStatusCmd)
|
||||
rootCmd.AddCommand(shellCmd)
|
||||
}
|
||||
|
||||
func runShellInstall(cmd *cobra.Command, args []string) error {
|
||||
if err := shell.Install(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := state.Enable(Version); err != nil {
|
||||
fmt.Printf("%s Could not enable Gas Town: %v\n", style.Dim.Render("⚠"), err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Shell integration installed (%s)\n", style.Success.Render("✓"), shell.RCFilePath(shell.DetectShell()))
|
||||
fmt.Println()
|
||||
fmt.Println("Run 'source ~/.zshrc' or open a new terminal to activate.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runShellRemove(cmd *cobra.Command, args []string) error {
|
||||
if err := shell.Remove(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s Shell integration removed\n", style.Success.Render("✓"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runShellStatus(cmd *cobra.Command, args []string) error {
|
||||
s, err := state.Load()
|
||||
if err != nil {
|
||||
fmt.Println("Gas Town: not configured")
|
||||
fmt.Println("Shell integration: not installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.Enabled {
|
||||
fmt.Println("Gas Town: enabled")
|
||||
} else {
|
||||
fmt.Println("Gas Town: disabled")
|
||||
}
|
||||
|
||||
if s.ShellIntegration != "" {
|
||||
fmt.Printf("Shell integration: %s (%s)\n", s.ShellIntegration, shell.RCFilePath(s.ShellIntegration))
|
||||
} else {
|
||||
fmt.Println("Shell integration: not installed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
171
internal/cmd/uninstall.go
Normal file
171
internal/cmd/uninstall.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// ABOUTME: Command to completely uninstall Gas Town from the system.
|
||||
// ABOUTME: Removes shell integration, wrappers, state, and optionally workspace.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/shell"
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/wrappers"
|
||||
)
|
||||
|
||||
var (
|
||||
uninstallWorkspace bool
|
||||
uninstallForce bool
|
||||
)
|
||||
|
||||
var uninstallCmd = &cobra.Command{
|
||||
Use: "uninstall",
|
||||
GroupID: GroupConfig,
|
||||
Short: "Remove Gas Town from the system",
|
||||
Long: `Completely remove Gas Town from the system.
|
||||
|
||||
By default, removes:
|
||||
- Shell integration (~/.zshrc or ~/.bashrc)
|
||||
- Wrapper scripts (~/bin/gt-codex, ~/bin/gt-opencode)
|
||||
- State directory (~/.local/state/gastown/)
|
||||
- Config directory (~/.config/gastown/)
|
||||
- Cache directory (~/.cache/gastown/)
|
||||
|
||||
The workspace (e.g., ~/gt) is NOT removed unless --workspace is specified.
|
||||
|
||||
Use --force to skip confirmation prompts.
|
||||
|
||||
Examples:
|
||||
gt uninstall # Remove Gas Town, keep workspace
|
||||
gt uninstall --workspace # Also remove workspace directory
|
||||
gt uninstall --force # Skip confirmation`,
|
||||
RunE: runUninstall,
|
||||
}
|
||||
|
||||
func init() {
|
||||
uninstallCmd.Flags().BoolVar(&uninstallWorkspace, "workspace", false,
|
||||
"Also remove the workspace directory (DESTRUCTIVE)")
|
||||
uninstallCmd.Flags().BoolVarP(&uninstallForce, "force", "f", false,
|
||||
"Skip confirmation prompts")
|
||||
rootCmd.AddCommand(uninstallCmd)
|
||||
}
|
||||
|
||||
func runUninstall(cmd *cobra.Command, args []string) error {
|
||||
if !uninstallForce {
|
||||
fmt.Println("This will remove Gas Town from your system.")
|
||||
fmt.Println()
|
||||
fmt.Println("The following will be removed:")
|
||||
fmt.Printf(" • Shell integration (%s)\n", shell.RCFilePath(shell.DetectShell()))
|
||||
fmt.Printf(" • Wrapper scripts (%s)\n", wrappers.BinDir())
|
||||
fmt.Printf(" • State directory (%s)\n", state.StateDir())
|
||||
fmt.Printf(" • Config directory (%s)\n", state.ConfigDir())
|
||||
fmt.Printf(" • Cache directory (%s)\n", state.CacheDir())
|
||||
|
||||
if uninstallWorkspace {
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s WORKSPACE WILL BE DELETED\n", style.Warning.Render("⚠"))
|
||||
fmt.Println(" This cannot be undone!")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Print("Continue? [y/N] ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.TrimSpace(strings.ToLower(response))
|
||||
|
||||
if response != "y" && response != "yes" {
|
||||
fmt.Println("Aborted.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var errors []string
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Removing Gas Town...")
|
||||
|
||||
if err := shell.Remove(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("shell integration: %v", err))
|
||||
} else {
|
||||
fmt.Printf(" %s Removed shell integration\n", style.Success.Render("✓"))
|
||||
}
|
||||
|
||||
if err := wrappers.Remove(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("wrapper scripts: %v", err))
|
||||
} else {
|
||||
fmt.Printf(" %s Removed wrapper scripts\n", style.Success.Render("✓"))
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(state.StateDir()); err != nil && !os.IsNotExist(err) {
|
||||
errors = append(errors, fmt.Sprintf("state directory: %v", err))
|
||||
} else {
|
||||
fmt.Printf(" %s Removed state directory\n", style.Success.Render("✓"))
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(state.ConfigDir()); err != nil && !os.IsNotExist(err) {
|
||||
errors = append(errors, fmt.Sprintf("config directory: %v", err))
|
||||
} else {
|
||||
fmt.Printf(" %s Removed config directory\n", style.Success.Render("✓"))
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(state.CacheDir()); err != nil && !os.IsNotExist(err) {
|
||||
errors = append(errors, fmt.Sprintf("cache directory: %v", err))
|
||||
} else {
|
||||
fmt.Printf(" %s Removed cache directory\n", style.Success.Render("✓"))
|
||||
}
|
||||
|
||||
if uninstallWorkspace {
|
||||
workspaceDir := findWorkspaceForUninstall()
|
||||
if workspaceDir != "" {
|
||||
if err := os.RemoveAll(workspaceDir); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("workspace: %v", err))
|
||||
} else {
|
||||
fmt.Printf(" %s Removed workspace: %s\n", style.Success.Render("✓"), workspaceDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s Some components could not be removed:\n", style.Warning.Render("⚠"))
|
||||
for _, e := range errors {
|
||||
fmt.Printf(" • %s\n", e)
|
||||
}
|
||||
return fmt.Errorf("uninstall incomplete")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s Gas Town has been uninstalled\n", style.Success.Render("✓"))
|
||||
fmt.Println()
|
||||
fmt.Println("To reinstall, run:")
|
||||
fmt.Printf(" %s\n", style.Dim.Render("go install github.com/steveyegge/gastown/cmd/gt@latest"))
|
||||
fmt.Printf(" %s\n", style.Dim.Render("gt install ~/gt --shell"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findWorkspaceForUninstall() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
filepath.Join(home, "gt"),
|
||||
filepath.Join(home, "gastown"),
|
||||
}
|
||||
|
||||
for _, path := range candidates {
|
||||
mayorDir := filepath.Join(path, "mayor")
|
||||
if _, err := os.Stat(mayorDir); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user