|
| 1 | +#!/usr/bin/env bash |
| 2 | +# gstack-uninstall — remove gstack skills, state, and browse daemons |
| 3 | +# |
| 4 | +# Usage: |
| 5 | +# gstack-uninstall — interactive uninstall (prompts before removing) |
| 6 | +# gstack-uninstall --force — remove everything without prompting |
| 7 | +# gstack-uninstall --keep-state — remove skills but keep ~/.gstack/ data |
| 8 | +# |
| 9 | +# What gets removed: |
| 10 | +# ~/.claude/skills/gstack — global Claude skill install (git clone or vendored) |
| 11 | +# ~/.claude/skills/{skill} — per-skill symlinks created by setup |
| 12 | +# ~/.codex/skills/gstack* — Codex skill install + per-skill symlinks |
| 13 | +# ~/.kiro/skills/gstack* — Kiro skill install + per-skill symlinks |
| 14 | +# ~/.gstack/ — global state (config, analytics, sessions, projects, repos) |
| 15 | +# .gstack/ — per-project browse state (in current git repo) |
| 16 | +# .agents/skills/gstack* — Codex/Gemini/Cursor sidecar (in current git repo) |
| 17 | +# Running browse daemons — stopped via SIGTERM before cleanup |
| 18 | +# |
| 19 | +# What is NOT removed: |
| 20 | +# ~/Library/Caches/ms-playwright/ — Playwright Chromium (shared, may be used by other tools) |
| 21 | +# ~/.gstack-dev/ — developer eval artifacts (only present in gstack contributors) |
| 22 | +# |
| 23 | +# Env overrides (for testing): |
| 24 | +# GSTACK_DIR — override auto-detected gstack root |
| 25 | +# GSTACK_STATE_DIR — override ~/.gstack state directory |
| 26 | +# |
| 27 | +# NOTE: Uses set -uo pipefail (no -e) — uninstall must never abort partway. |
| 28 | +set -uo pipefail |
| 29 | + |
| 30 | +GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" |
| 31 | +STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}" |
| 32 | +_GIT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" |
| 33 | + |
| 34 | +# ─── Parse flags ───────────────────────────────────────────── |
| 35 | +FORCE=0 |
| 36 | +KEEP_STATE=0 |
| 37 | +while [ $# -gt 0 ]; do |
| 38 | + case "$1" in |
| 39 | + --force) FORCE=1; shift ;; |
| 40 | + --keep-state) KEEP_STATE=1; shift ;; |
| 41 | + -h|--help) |
| 42 | + sed -n '2,/^[^#]/{ /^#/s/^# \{0,1\}//p; }' "$0" |
| 43 | + exit 0 |
| 44 | + ;; |
| 45 | + *) |
| 46 | + echo "Unknown option: $1" >&2 |
| 47 | + echo "Usage: gstack-uninstall [--force] [--keep-state]" >&2 |
| 48 | + exit 1 |
| 49 | + ;; |
| 50 | + esac |
| 51 | +done |
| 52 | + |
| 53 | +# ─── Confirmation ──────────────────────────────────────────── |
| 54 | +if [ "$FORCE" -eq 0 ]; then |
| 55 | + echo "This will remove gstack from your system:" |
| 56 | + { [ -d "$HOME/.claude/skills/gstack" ] || [ -L "$HOME/.claude/skills/gstack" ]; } && echo " ~/.claude/skills/gstack" |
| 57 | + [ -d "$HOME/.codex/skills" ] && echo " ~/.codex/skills/gstack*" |
| 58 | + [ -d "$HOME/.kiro/skills" ] && echo " ~/.kiro/skills/gstack*" |
| 59 | + [ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR" |
| 60 | + |
| 61 | + if [ -n "$_GIT_ROOT" ]; then |
| 62 | + [ -d "$_GIT_ROOT/.gstack" ] && echo " $_GIT_ROOT/.gstack/" |
| 63 | + [ -d "$_GIT_ROOT/.agents/skills" ] && echo " $_GIT_ROOT/.agents/skills/gstack*" |
| 64 | + fi |
| 65 | + |
| 66 | + printf "\nContinue? [y/N] " |
| 67 | + read -r REPLY |
| 68 | + case "$REPLY" in |
| 69 | + y|Y|yes|YES) ;; |
| 70 | + *) echo "Aborted."; exit 0 ;; |
| 71 | + esac |
| 72 | +fi |
| 73 | + |
| 74 | +removed=() |
| 75 | + |
| 76 | +# ─── Stop running browse daemons ───────────────────────────── |
| 77 | +# Browse servers write PID to {project}/.gstack/browse.json. |
| 78 | +# Stop any we can find before removing state directories. |
| 79 | +stop_browse_daemon() { |
| 80 | + local state_file="$1" |
| 81 | + if [ ! -f "$state_file" ]; then |
| 82 | + return |
| 83 | + fi |
| 84 | + local pid |
| 85 | + pid="$(awk -F'[:,]' '/"pid"/ { for(i=1;i<=NF;i++) if($i ~ /"pid"/) { gsub(/[^0-9]/, "", $(i+1)); print $(i+1); exit } }' "$state_file" 2>/dev/null || true)" |
| 86 | + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then |
| 87 | + kill "$pid" 2>/dev/null || true |
| 88 | + # Wait up to 2s for graceful shutdown |
| 89 | + local waited=0 |
| 90 | + while [ "$waited" -lt 4 ] && kill -0 "$pid" 2>/dev/null; do |
| 91 | + sleep 0.5 |
| 92 | + waited=$(( waited + 1 )) |
| 93 | + done |
| 94 | + if kill -0 "$pid" 2>/dev/null; then |
| 95 | + kill -9 "$pid" 2>/dev/null || true |
| 96 | + fi |
| 97 | + removed+=("browse daemon (PID $pid)") |
| 98 | + fi |
| 99 | +} |
| 100 | + |
| 101 | +# Stop daemon in current project |
| 102 | +if [ -n "$_GIT_ROOT" ] && [ -f "$_GIT_ROOT/.gstack/browse.json" ]; then |
| 103 | + stop_browse_daemon "$_GIT_ROOT/.gstack/browse.json" |
| 104 | +fi |
| 105 | + |
| 106 | +# Stop daemons tracked in global projects directory |
| 107 | +if [ -d "$STATE_DIR/projects" ]; then |
| 108 | + while IFS= read -r browse_json; do |
| 109 | + stop_browse_daemon "$browse_json" |
| 110 | + done < <(find "$STATE_DIR/projects" -name browse.json -path '*/.gstack/*' 2>/dev/null || true) |
| 111 | +fi |
| 112 | + |
| 113 | +# ─── Remove Claude skills ─────────────────────────────────── |
| 114 | +CLAUDE_SKILLS="$HOME/.claude/skills" |
| 115 | +if [ -d "$CLAUDE_SKILLS/gstack" ] || [ -L "$CLAUDE_SKILLS/gstack" ]; then |
| 116 | + # Remove per-skill symlinks that point into gstack/ |
| 117 | + for link in "$CLAUDE_SKILLS"/*; do |
| 118 | + [ -L "$link" ] || continue |
| 119 | + name="$(basename "$link")" |
| 120 | + [ "$name" = "gstack" ] && continue |
| 121 | + target="$(readlink "$link" 2>/dev/null || true)" |
| 122 | + case "$target" in |
| 123 | + gstack/*|*/gstack/*) rm -f "$link"; removed+=("claude/$name") ;; |
| 124 | + esac |
| 125 | + done |
| 126 | + |
| 127 | + # Remove the gstack directory/symlink itself |
| 128 | + rm -rf "$CLAUDE_SKILLS/gstack" |
| 129 | + removed+=("~/.claude/skills/gstack") |
| 130 | +fi |
| 131 | + |
| 132 | +# ─── Remove Codex skills ──────────────────────────────────── |
| 133 | +CODEX_SKILLS="$HOME/.codex/skills" |
| 134 | +if [ -d "$CODEX_SKILLS" ]; then |
| 135 | + for item in "$CODEX_SKILLS"/gstack*; do |
| 136 | + [ -e "$item" ] || [ -L "$item" ] || continue |
| 137 | + rm -rf "$item" |
| 138 | + removed+=("codex/$(basename "$item")") |
| 139 | + done |
| 140 | +fi |
| 141 | + |
| 142 | +# ─── Remove Kiro skills ────────────────────────────────────── |
| 143 | +KIRO_SKILLS="$HOME/.kiro/skills" |
| 144 | +if [ -d "$KIRO_SKILLS" ]; then |
| 145 | + for item in "$KIRO_SKILLS"/gstack*; do |
| 146 | + [ -e "$item" ] || [ -L "$item" ] || continue |
| 147 | + rm -rf "$item" |
| 148 | + removed+=("kiro/$(basename "$item")") |
| 149 | + done |
| 150 | +fi |
| 151 | + |
| 152 | +# ─── Remove per-project .agents/ sidecar ───────────────────── |
| 153 | +if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.agents/skills" ]; then |
| 154 | + for item in "$_GIT_ROOT/.agents/skills"/gstack*; do |
| 155 | + [ -e "$item" ] || [ -L "$item" ] || continue |
| 156 | + rm -rf "$item" |
| 157 | + removed+=("agents/$(basename "$item")") |
| 158 | + done |
| 159 | + |
| 160 | + rmdir "$_GIT_ROOT/.agents/skills" 2>/dev/null || true |
| 161 | + rmdir "$_GIT_ROOT/.agents" 2>/dev/null || true |
| 162 | +fi |
| 163 | + |
| 164 | +# ─── Remove per-project .gstack/ state ─────────────────────── |
| 165 | +if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.gstack" ]; then |
| 166 | + rm -rf "$_GIT_ROOT/.gstack" |
| 167 | + removed+=("$_GIT_ROOT/.gstack/") |
| 168 | +fi |
| 169 | + |
| 170 | +# ─── Remove global state ──────────────────────────────────── |
| 171 | +if [ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ]; then |
| 172 | + rm -rf "$STATE_DIR" |
| 173 | + removed+=("$STATE_DIR") |
| 174 | +fi |
| 175 | + |
| 176 | +# ─── Clean up temp files ──────────────────────────────────── |
| 177 | +for tmp_file in /tmp/gstack-latest-version /tmp/gstack-sketch-*.html /tmp/gstack-sketch.png; do |
| 178 | + if [ -e "$tmp_file" ]; then |
| 179 | + rm -f "$tmp_file" |
| 180 | + removed+=("$(basename "$tmp_file")") |
| 181 | + fi |
| 182 | +done |
| 183 | + |
| 184 | +# ─── Summary ──────────────────────────────────────────────── |
| 185 | +if [ ${#removed[@]} -gt 0 ]; then |
| 186 | + echo "Removed: ${removed[*]}" |
| 187 | + echo "gstack uninstalled." |
| 188 | +else |
| 189 | + echo "Nothing to remove — gstack is not installed." |
| 190 | +fi |
0 commit comments