Files
gastown/internal/cmd/uninstall.go
Sohail Mohammad 81bfe48ed3 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>
2026-01-08 20:25:01 -08:00

172 lines
4.9 KiB
Go

// 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 ""
}