Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions bin/gstack-uninstall
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/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
# ~/.kiro/skills/gstack* — Kiro skill install + per-skill symlinks
# ~/.gstack/ — global state (config, analytics, sessions, projects, repos)
# .gstack/ — per-project browse state (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

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"
[ -d "$HOME/.codex/skills" ] && echo " ~/.codex/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/.gstack" ] && echo " $_GIT_ROOT/.gstack/"
[ -d "$_GIT_ROOT/.agents/skills" ] && echo " $_GIT_ROOT/.agents/skills/gstack*"
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 browse_json; do
stop_browse_daemon "$browse_json"
done < <(find "$STATE_DIR/projects" -name browse.json -path '*/.gstack/*' 2>/dev/null || true)
fi

# ─── Remove 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

# Remove the gstack directory/symlink itself
rm -rf "$CLAUDE_SKILLS/gstack"
removed+=("~/.claude/skills/gstack")
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 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 .gstack/ state ───────────────────────
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.gstack" ]; then
rm -rf "$_GIT_ROOT/.gstack"
removed+=("$_GIT_ROOT/.gstack/")
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_file in /tmp/gstack-latest-version /tmp/gstack-sketch-*.html /tmp/gstack-sketch.png; do
if [ -e "$tmp_file" ]; then
rm -f "$tmp_file"
removed+=("$(basename "$tmp_file")")
fi
done

# ─── Summary ────────────────────────────────────────────────
if [ ${#removed[@]} -gt 0 ]; then
echo "Removed: ${removed[*]}"
echo "gstack uninstalled."
else
echo "Nothing to remove — gstack is not installed."
fi