#!/usr/bin/env bash # gstack-uninstall — remove gstack skills, state, and browse daemons # # Usage: # gstack-uninstall — interactive uninstall (prompts before removing) # gstack-uninstall --force — remove everything without prompting # gstack-uninstall --keep-state — remove skills but keep ~/.gstack/ data # # What gets REMOVED: # ~/.claude/skills/gstack — global Claude skill install (git clone or vendored) # ~/.claude/skills/{skill} — per-skill symlinks created by setup # ~/.codex/skills/gstack* — Codex skill install + per-skill symlinks # ~/.factory/skills/gstack* — Factory Droid skill install + per-skill symlinks # ~/.kiro/skills/gstack* — Kiro skill install + per-skill symlinks # ~/.gstack/ — global state (config, analytics, sessions, projects, # repos, installation-id, browse error logs) # .claude/skills/gstack* — project-local skill install (--local installs) # .gstack/ — per-project browse state (in current git repo) # .gstack-worktrees/ — per-project test worktrees (in current git repo) # .agents/skills/gstack* — Codex/Gemini/Cursor sidecar (in current git repo) # Running browse daemons — stopped via SIGTERM before cleanup # # What is NOT REMOVED: # ~/Library/Caches/ms-playwright/ — Playwright Chromium (shared, may be used by other tools) # ~/.gstack-dev/ — developer eval artifacts (only present in gstack contributors) # # Env overrides (for testing): # GSTACK_DIR — override auto-detected gstack root # GSTACK_STATE_DIR — override ~/.gstack state directory # # NOTE: Uses set -uo pipefail (no -e) — uninstall must never abort partway. set -uo pipefail if [ -z "${HOME:-}" ]; then echo "ERROR: \$HOME is not set" >&2 exit 1 fi GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}" _GIT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" # ─── Parse flags ───────────────────────────────────────────── FORCE=0 KEEP_STATE=0 while [ $# -gt 0 ]; do case "$1" in --force) FORCE=1; shift ;; --keep-state) KEEP_STATE=1; shift ;; -h|--help) sed -n '2,/^[^#]/{ /^#/s/^# \{0,1\}//p; }' "$0" exit 0 ;; *) echo "Unknown option: $1" >&2 echo "Usage: gstack-uninstall [--force] [--keep-state]" >&2 exit 1 ;; esac done # ─── Confirmation ──────────────────────────────────────────── if [ "$FORCE" -eq 0 ]; then echo "This will remove gstack from your system:" { [ -d "$HOME/.claude/skills/gstack" ] || [ -L "$HOME/.claude/skills/gstack" ]; } && echo " ~/.claude/skills/gstack (+ per-skill symlinks)" [ -d "$HOME/.codex/skills" ] && echo " ~/.codex/skills/gstack*" [ -d "$HOME/.factory/skills" ] && echo " ~/.factory/skills/gstack*" [ -d "$HOME/.kiro/skills" ] && echo " ~/.kiro/skills/gstack*" [ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR" if [ -n "$_GIT_ROOT" ]; then [ -d "$_GIT_ROOT/.claude/skills/gstack" ] && echo " $_GIT_ROOT/.claude/skills/gstack (project-local)" [ -d "$_GIT_ROOT/.gstack" ] && echo " $_GIT_ROOT/.gstack/ (browse state + reports)" [ -d "$_GIT_ROOT/.gstack-worktrees" ] && echo " $_GIT_ROOT/.gstack-worktrees/" [ -d "$_GIT_ROOT/.agents/skills" ] && echo " $_GIT_ROOT/.agents/skills/gstack*" fi # Preview running daemons if [ -n "$_GIT_ROOT" ] && [ -f "$_GIT_ROOT/.gstack/browse.json" ]; then _PREVIEW_PID="$(awk -F'[:,]' '/"pid"/ { for(i=1;i<=NF;i++) if($i ~ /"pid"/) { gsub(/[^0-9]/, "", $(i+1)); print $(i+1); exit } }' "$_GIT_ROOT/.gstack/browse.json" 2>/dev/null || true)" [ -n "$_PREVIEW_PID" ] && kill -0 "$_PREVIEW_PID" 2>/dev/null && echo " browse daemon (PID $_PREVIEW_PID) will be stopped" fi printf "\nContinue? [y/N] " read -r REPLY case "$REPLY" in y|Y|yes|YES) ;; *) echo "Aborted."; exit 0 ;; esac fi REMOVED=() # ─── Stop running browse daemons ───────────────────────────── # Browse servers write PID to {project}/.gstack/browse.json. # Stop any we can find before removing state directories. stop_browse_daemon() { local state_file="$1" if [ ! -f "$state_file" ]; then return fi local pid 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)" if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true # Wait up to 2s for graceful shutdown local waited=0 while [ "$waited" -lt 4 ] && kill -0 "$pid" 2>/dev/null; do sleep 0.5 waited=$(( waited + 1 )) done if kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null || true fi REMOVED+=("browse daemon (PID $pid)") fi } # Stop daemon in current project if [ -n "$_GIT_ROOT" ] && [ -f "$_GIT_ROOT/.gstack/browse.json" ]; then stop_browse_daemon "$_GIT_ROOT/.gstack/browse.json" fi # Stop daemons tracked in global projects directory if [ -d "$STATE_DIR/projects" ]; then while IFS= read -r _BJ; do stop_browse_daemon "$_BJ" done < <(find "$STATE_DIR/projects" -name browse.json -path '*/.gstack/*' 2>/dev/null || true) fi # ─── Remove global Claude skills ──────────────────────────── CLAUDE_SKILLS="$HOME/.claude/skills" if [ -d "$CLAUDE_SKILLS/gstack" ] || [ -L "$CLAUDE_SKILLS/gstack" ]; then # Remove per-skill symlinks that point into gstack/ for _LINK in "$CLAUDE_SKILLS"/*; do [ -L "$_LINK" ] || continue _NAME="$(basename "$_LINK")" [ "$_NAME" = "gstack" ] && continue _TARGET="$(readlink "$_LINK" 2>/dev/null || true)" case "$_TARGET" in gstack/*|*/gstack/*) rm -f "$_LINK"; REMOVED+=("claude/$_NAME") ;; esac done rm -rf "$CLAUDE_SKILLS/gstack" REMOVED+=("~/.claude/skills/gstack") fi # ─── Remove project-local Claude skills (--local installs) ── if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.claude/skills" ]; then for _LINK in "$_GIT_ROOT/.claude/skills"/*; do [ -L "$_LINK" ] || continue _TARGET="$(readlink "$_LINK" 2>/dev/null || true)" case "$_TARGET" in gstack/*|*/gstack/*) rm -f "$_LINK"; REMOVED+=("local claude/$(basename "$_LINK")") ;; esac done if [ -d "$_GIT_ROOT/.claude/skills/gstack" ] || [ -L "$_GIT_ROOT/.claude/skills/gstack" ]; then rm -rf "$_GIT_ROOT/.claude/skills/gstack" REMOVED+=("$_GIT_ROOT/.claude/skills/gstack") fi fi # ─── Remove Codex skills ──────────────────────────────────── CODEX_SKILLS="$HOME/.codex/skills" if [ -d "$CODEX_SKILLS" ]; then for _ITEM in "$CODEX_SKILLS"/gstack*; do [ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue rm -rf "$_ITEM" REMOVED+=("codex/$(basename "$_ITEM")") done fi # ─── Remove Factory Droid skills ──────────────────────────── FACTORY_SKILLS="$HOME/.factory/skills" if [ -d "$FACTORY_SKILLS" ]; then for _ITEM in "$FACTORY_SKILLS"/gstack*; do [ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue rm -rf "$_ITEM" REMOVED+=("factory/$(basename "$_ITEM")") done fi # ─── Remove Kiro skills ───────────────────────────────────── KIRO_SKILLS="$HOME/.kiro/skills" if [ -d "$KIRO_SKILLS" ]; then for _ITEM in "$KIRO_SKILLS"/gstack*; do [ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue rm -rf "$_ITEM" REMOVED+=("kiro/$(basename "$_ITEM")") done fi # ─── Remove per-project .agents/ sidecar ───────────────────── if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.agents/skills" ]; then for _ITEM in "$_GIT_ROOT/.agents/skills"/gstack*; do [ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue rm -rf "$_ITEM" REMOVED+=("agents/$(basename "$_ITEM")") done rmdir "$_GIT_ROOT/.agents/skills" 2>/dev/null || true rmdir "$_GIT_ROOT/.agents" 2>/dev/null || true fi # ─── Remove per-project .factory/ sidecar ──────────────────── if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.factory/skills" ]; then for _ITEM in "$_GIT_ROOT/.factory/skills"/gstack*; do [ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue rm -rf "$_ITEM" REMOVED+=("factory/$(basename "$_ITEM")") done rmdir "$_GIT_ROOT/.factory/skills" 2>/dev/null || true rmdir "$_GIT_ROOT/.factory" 2>/dev/null || true fi # ─── Remove per-project state ─────────────────────────────── if [ -n "$_GIT_ROOT" ]; then if [ -d "$_GIT_ROOT/.gstack" ]; then rm -rf "$_GIT_ROOT/.gstack" REMOVED+=("$_GIT_ROOT/.gstack/") fi if [ -d "$_GIT_ROOT/.gstack-worktrees" ]; then rm -rf "$_GIT_ROOT/.gstack-worktrees" REMOVED+=("$_GIT_ROOT/.gstack-worktrees/") fi fi # ─── Remove global state ──────────────────────────────────── if [ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ]; then rm -rf "$STATE_DIR" REMOVED+=("$STATE_DIR") fi # ─── Clean up temp files ──────────────────────────────────── for _TMP in /tmp/gstack-latest-version /tmp/gstack-sketch-*.html /tmp/gstack-sketch.png /tmp/gstack-sync-*; do if [ -e "$_TMP" ]; then rm -f "$_TMP" REMOVED+=("$(basename "$_TMP")") fi done # ─── Summary ──────────────────────────────────────────────── if [ ${#REMOVED[@]} -gt 0 ]; then echo "Removed: ${REMOVED[*]}" echo "gstack uninstalled." else echo "Nothing to remove — gstack is not installed." fi exit 0