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