diff --git a/bin/gstack-uninstall b/bin/gstack-uninstall new file mode 100755 index 000000000..7ac316aa0 --- /dev/null +++ b/bin/gstack-uninstall @@ -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