mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
feat: community PRs — faster install, skill namespacing, uninstall, Codex fallback, Windows fix, Python patterns (v0.12.9.0) (#561)
* fix: sync package.json version with VERSION file (0.12.7.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * perf: shallow clone for faster install (#484) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Python/async/SSRF patterns in review checklist (#531) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: namespace skill symlinks with gstack- prefix (#503) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add uninstall script (#323) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: office-hours Claude subagent fallback when Codex unavailable (#464) Updates generateCodexSecondOpinion resolver to always offer second opinion and fall back to Claude subagent when Codex is unavailable or errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: findPort() race condition via net.createServer (#490) Replaces Bun.serve() port probing with net.createServer() for proper async bind/close semantics. Fixes Windows EADDRINUSE race condition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add tests for uninstall, setup prefix, and resolver fallback - Uninstall integration tests: syntax, flags, mock install layout, upgrade path - Setup prefix tests: gstack-* prefixing, --no-prefix, cleanup migration - Resolver tests: Claude subagent fallback in generated SKILL.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.12.9.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/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, 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/.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 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 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
|
||||
Reference in New Issue
Block a user