Skip to content

Commit 899320e

Browse files
committed
feat: add uninstall script
Closes #273. Adds bin/gstack-uninstall to cleanly remove gstack from a system: - Stops running browse daemons (SIGTERM with graceful fallback) - Removes Claude skills (~/.claude/skills/gstack + per-skill symlinks) - Removes Codex skills (~/.codex/skills/gstack*) - Removes per-project .agents/ sidecar and .gstack/ state - Removes global ~/.gstack/ state directory - Cleans up /tmp session files Supports --force (skip confirmation), --keep-state (preserve ~/.gstack/ data), and GSTACK_STATE_DIR env override for testing. Uses set -uo pipefail (no -e) so uninstall never aborts partway. Handles broken symlinks. Follows existing bin/ script conventions.
1 parent f4bbfaa commit 899320e

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

bin/gstack-uninstall

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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

Comments
 (0)