mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-12 16:08:59 +02:00
fix(uninstall): remove real-directory skill installs, gated on provenance
On Windows, setup installs skills as REAL directory copies (cp -R via _link_or_copy). gstack-uninstall's per-skill loop filtered on [ -L ], so every copy was skipped: --force exited 0 and printed 'gstack uninstalled.' while leaving ~52 gstack-* directories plus _gstack-command/ behind in ~/.claude/skills. The same filter also missed the standard Unix shape (real dir + symlinked SKILL.md), which was left as a dangling-symlink husk. Fix: the loop now handles all three install shapes. Symlink entries keep the existing readlink check. Real dirs with a SYMLINKED SKILL.md are removed when the link points into gstack (same semantics as setup's cleanup helpers). Real dirs with a REAL-FILE SKILL.md — the Windows copy shape — are removed ONLY when both provenance gates pass (F8): (a) the directory name is in gstack's skill inventory (source dir names, frontmatter names, gstack- prefixed variants, and the alias dirs), and (b) the SKILL.md carries the existing generated banner '<!-- AUTO-GENERATED from' (ENG-OV10: every pre-v1.67 copy already carries it; a NEW marker would refuse to delete legitimate old installs, recreating the bug). Anything failing a gate is listed to stderr and never deleted — a user's own skill that happens to share a name with a gstack skill survives. Tests: a fake-tree fixture covers removed/kept/listed for every shape (including the F8 name-collision row), and a census test asserts every installable skill's generated SKILL.md carries the banner so the gate can't strand a bannerless skill. Registered in the Windows-safe curated list — the copy shape is exactly what windows-latest exercises. Fixes #2563 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
52006feac4
commit
c84246845e
+64
-8
@@ -130,22 +130,78 @@ fi
|
||||
|
||||
# ─── Remove global Claude skills ────────────────────────────
|
||||
CLAUDE_SKILLS="$HOME/.claude/skills"
|
||||
|
||||
# Skill-name inventory (#2563 gate a): every name setup could have installed —
|
||||
# each source skill's directory name, its frontmatter name, their gstack-
|
||||
# prefixed variants, and the alias dirs. Built BEFORE the install root is
|
||||
# removed. A real directory in ~/.claude/skills is only deletable when its
|
||||
# name is in this inventory AND its SKILL.md carries the generated banner.
|
||||
_INVENTORY=" _gstack-command connect-chrome gstack-connect-chrome "
|
||||
if [ -d "$GSTACK_DIR" ]; then
|
||||
for _SRC in "$GSTACK_DIR"/*/; do
|
||||
[ -f "$_SRC/SKILL.md" ] || continue
|
||||
_SRC_NAME="$(basename "$_SRC")"
|
||||
_FM_NAME=$(grep -m1 '^name:' "$_SRC/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]' || true)
|
||||
for _N in "$_SRC_NAME" "$_FM_NAME"; do
|
||||
[ -n "$_N" ] || continue
|
||||
case "$_INVENTORY" in *" $_N "*) ;; *) _INVENTORY="$_INVENTORY$_N gstack-$_N " ;; esac
|
||||
done
|
||||
done
|
||||
fi
|
||||
_in_skill_inventory() { case "$_INVENTORY" in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||
|
||||
_SKIPPED_DIRS=()
|
||||
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")"
|
||||
# Remove per-skill entries created by setup. Three install shapes exist:
|
||||
# 1. symlink entry (oldest installs)
|
||||
# 2. real dir + SYMLINKED SKILL.md (standard Unix install)
|
||||
# 3. real dir + REAL-FILE SKILL.md (Windows copy install, #2563)
|
||||
# Shape 3 was skipped entirely — gstack-uninstall exited 0 and reported
|
||||
# success while leaving ~52 gstack-* directories behind on Windows.
|
||||
for _ENTRY in "$CLAUDE_SKILLS"/*; do
|
||||
_NAME="$(basename "$_ENTRY")"
|
||||
[ "$_NAME" = "gstack" ] && continue
|
||||
_TARGET="$(readlink "$_LINK" 2>/dev/null || true)"
|
||||
case "$_TARGET" in
|
||||
gstack/*|*/gstack/*) rm -f "$_LINK"; REMOVED+=("claude/$_NAME") ;;
|
||||
esac
|
||||
if [ -L "$_ENTRY" ]; then
|
||||
_TARGET="$(readlink "$_ENTRY" 2>/dev/null || true)"
|
||||
case "$_TARGET" in
|
||||
gstack/*|*/gstack/*) rm -f "$_ENTRY"; REMOVED+=("claude/$_NAME") ;;
|
||||
esac
|
||||
elif [ -d "$_ENTRY" ] && { [ -f "$_ENTRY/SKILL.md" ] || [ -L "$_ENTRY/SKILL.md" ]; }; then
|
||||
if [ -L "$_ENTRY/SKILL.md" ]; then
|
||||
# Shape 2: provenance readable from the symlink target itself
|
||||
# (mirrors setup's cleanup_old_claude_symlinks semantics).
|
||||
_TARGET="$(readlink "$_ENTRY/SKILL.md" 2>/dev/null || true)"
|
||||
case "$_TARGET" in
|
||||
*gstack*) rm -rf "$_ENTRY"; REMOVED+=("claude/$_NAME") ;;
|
||||
*) _SKIPPED_DIRS+=("$_ENTRY") ;;
|
||||
esac
|
||||
elif _in_skill_inventory "$_NAME" && grep -q '<!-- AUTO-GENERATED from' "$_ENTRY/SKILL.md" 2>/dev/null; then
|
||||
# Shape 3: delete ONLY when BOTH gates pass (F8) — the name is in
|
||||
# gstack's skill inventory AND the SKILL.md carries the existing
|
||||
# generated banner. ENG-OV10: the banner IS the provenance marker —
|
||||
# every pre-v1.67 copy already carries it; inventing a new marker
|
||||
# would refuse to delete legitimate old installs, recreating #2563.
|
||||
rm -rf "$_ENTRY"
|
||||
REMOVED+=("claude/$_NAME")
|
||||
else
|
||||
# A real dir we cannot prove is gstack-managed (name collision with a
|
||||
# user's own skill, or a hand-written SKILL.md): NEVER delete — list.
|
||||
_SKIPPED_DIRS+=("$_ENTRY")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf "$CLAUDE_SKILLS/gstack"
|
||||
REMOVED+=("~/.claude/skills/gstack")
|
||||
fi
|
||||
|
||||
if [ ${#_SKIPPED_DIRS[@]} -gt 0 ]; then
|
||||
echo "left in place (not provably gstack-managed — remove by hand if they are yours):" >&2
|
||||
for _D in "${_SKIPPED_DIRS[@]}"; do
|
||||
echo " $_D" >&2
|
||||
done
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user