mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
fix: top-level skill dirs so Claude discovers unprefixed names (#761)
* fix: top-level skill dirs so Claude discovers unprefixed names Replace directory symlinks (gstack/qa → qa) with real directories containing a SKILL.md symlink. Claude Code auto-prefixes skills nested under a parent dir symlink, so /plan-ceo-review became "Unknown skill" even with skill_prefix=false. Real dirs fix this. Also syncs package.json version to match VERSION file and updates test assertions to match the new mkdir + ln approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update symlink references to new top-level directory pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: regression tests for top-level skill directory structure Verifies the invariant that setup/relink creates real directories (not symlinks) at the top level, with SKILL.md symlinks inside. This prevents Claude Code from auto-prefixing skills with gstack- when using --no-prefix. Tests added: - unprefixed skills must be real dirs with SKILL.md symlinks - prefixed skills must also be real dirs with SKILL.md symlinks - old directory symlinks get upgraded to real directories - cleanup functions handle both old symlinks and new dir pattern - link function removes old directory symlinks before mkdir Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: namespace isolation tests for first install + mode switching Verifies the core invariant: when you pick a prefix mode, ONLY that mode's entries exist. Zero pollution from the other mode. - first install --no-prefix: only flat names, zero gstack-* leaks - first install --prefix: only gstack-* names, zero flat leaks - non-TTY defaults to flat names - switching prefix→no-prefix removes ALL gstack-* entries - switching no-prefix→prefix removes ALL flat entries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: upgrade migration system — versioned fix scripts for broken state Adds gstack-upgrade/migrations/ directory with version-keyed bash scripts that run automatically during /gstack-upgrade (Step 4.75, after ./setup). Each script is idempotent and handles state fixes that setup alone can't cover. First migration: v0.15.2.0.sh runs gstack-relink to fix stale directory symlinks from pre-v0.15.2.0 installs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: migration script validation + v0.15.2.0 end-to-end fix test Tests that migration scripts are executable, parse without syntax errors, follow the v{VERSION}.sh naming convention, and that v0.15.2.0 actually fixes stale directory symlinks by converting them to real directories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: upgrade migration guide in CONTRIBUTING.md + CLAUDE.md pointer CONTRIBUTING.md: new "Upgrade migrations" section documenting when and how to add migration scripts for broken on-disk state. CLAUDE.md: added note under vendored symlink awareness pointing to CONTRIBUTING.md's migration section when worried about broken installs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -263,9 +263,11 @@ fi
|
||||
mkdir -p "$HOME/.gstack/projects"
|
||||
|
||||
# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
|
||||
# When SKILL_PREFIX=1 (default), symlinks are prefixed with "gstack-" to avoid
|
||||
# namespace pollution (e.g., gstack-review instead of review).
|
||||
# Use --no-prefix to restore the old flat names.
|
||||
# Creates real directories (not symlinks) at the top level with a SKILL.md symlink
|
||||
# inside. This ensures Claude discovers them as top-level skills, not nested under
|
||||
# gstack/ (which would auto-prefix them as gstack-*).
|
||||
# When SKILL_PREFIX=1, directories are prefixed with "gstack-".
|
||||
# Use --no-prefix to restore flat names.
|
||||
link_claude_skill_dirs() {
|
||||
local gstack_dir="$1"
|
||||
local skills_dir="$2"
|
||||
@@ -288,9 +290,14 @@ link_claude_skill_dirs() {
|
||||
link_name="$skill_name"
|
||||
fi
|
||||
target="$skills_dir/$link_name"
|
||||
# Create or update symlink; skip if a real file/directory exists
|
||||
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
||||
ln -snf "gstack/$dir_name" "$target"
|
||||
# Upgrade old directory symlinks to real directories
|
||||
if [ -L "$target" ]; then
|
||||
rm -f "$target"
|
||||
fi
|
||||
# Create real directory with symlinked SKILL.md (absolute path)
|
||||
if [ ! -e "$target" ] || [ -d "$target" ]; then
|
||||
mkdir -p "$target"
|
||||
ln -snf "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"
|
||||
linked+=("$link_name")
|
||||
fi
|
||||
fi
|
||||
@@ -300,9 +307,9 @@ link_claude_skill_dirs() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Helper: remove old unprefixed Claude skill symlinks ──────────────────────
|
||||
# ─── Helper: remove old unprefixed Claude skill entries ───────────────────────
|
||||
# Migration: when switching from flat names to gstack- prefixed names,
|
||||
# clean up stale symlinks that point into the gstack directory.
|
||||
# clean up stale symlinks or directories that point into the gstack directory.
|
||||
cleanup_old_claude_symlinks() {
|
||||
local gstack_dir="$1"
|
||||
local skills_dir="$2"
|
||||
@@ -314,7 +321,7 @@ cleanup_old_claude_symlinks() {
|
||||
# Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
|
||||
case "$skill_name" in gstack-*) continue ;; esac
|
||||
old_target="$skills_dir/$skill_name"
|
||||
# Only remove if it's a symlink pointing into gstack/
|
||||
# Remove directory symlinks pointing into gstack/
|
||||
if [ -L "$old_target" ]; then
|
||||
link_dest="$(readlink "$old_target" 2>/dev/null || true)"
|
||||
case "$link_dest" in
|
||||
@@ -323,17 +330,26 @@ cleanup_old_claude_symlinks() {
|
||||
removed+=("$skill_name")
|
||||
;;
|
||||
esac
|
||||
# Remove real directories with symlinked SKILL.md pointing into gstack/
|
||||
elif [ -d "$old_target" ] && [ -L "$old_target/SKILL.md" ]; then
|
||||
link_dest="$(readlink "$old_target/SKILL.md" 2>/dev/null || true)"
|
||||
case "$link_dest" in
|
||||
*gstack*)
|
||||
rm -rf "$old_target"
|
||||
removed+=("$skill_name")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ ${#removed[@]} -gt 0 ]; then
|
||||
echo " cleaned up old symlinks: ${removed[*]}"
|
||||
echo " cleaned up old entries: ${removed[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Helper: remove old prefixed Claude skill symlinks ────────────────────────
|
||||
# ─── Helper: remove old prefixed Claude skill entries ─────────────────────────
|
||||
# Reverse migration: when switching from gstack- prefixed names to flat names,
|
||||
# clean up stale gstack-* symlinks that point into the gstack directory.
|
||||
# clean up stale gstack-* symlinks or directories that point into the gstack directory.
|
||||
cleanup_prefixed_claude_symlinks() {
|
||||
local gstack_dir="$1"
|
||||
local skills_dir="$2"
|
||||
@@ -342,11 +358,11 @@ cleanup_prefixed_claude_symlinks() {
|
||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
[ "$skill_name" = "node_modules" ] && continue
|
||||
# Only clean up prefixed symlinks for dirs that AREN'T already prefixed
|
||||
# Only clean up prefixed entries for dirs that AREN'T already prefixed
|
||||
# (e.g., remove gstack-qa but NOT gstack-upgrade which is the real dir name)
|
||||
case "$skill_name" in gstack-*) continue ;; esac
|
||||
prefixed_target="$skills_dir/gstack-$skill_name"
|
||||
# Only remove if it's a symlink pointing into gstack/
|
||||
# Remove directory symlinks pointing into gstack/
|
||||
if [ -L "$prefixed_target" ]; then
|
||||
link_dest="$(readlink "$prefixed_target" 2>/dev/null || true)"
|
||||
case "$link_dest" in
|
||||
@@ -355,11 +371,20 @@ cleanup_prefixed_claude_symlinks() {
|
||||
removed+=("gstack-$skill_name")
|
||||
;;
|
||||
esac
|
||||
# Remove real directories with symlinked SKILL.md pointing into gstack/
|
||||
elif [ -d "$prefixed_target" ] && [ -L "$prefixed_target/SKILL.md" ]; then
|
||||
link_dest="$(readlink "$prefixed_target/SKILL.md" 2>/dev/null || true)"
|
||||
case "$link_dest" in
|
||||
*gstack*)
|
||||
rm -rf "$prefixed_target"
|
||||
removed+=("gstack-$skill_name")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ ${#removed[@]} -gt 0 ]; then
|
||||
echo " cleaned up prefixed symlinks: ${removed[*]}"
|
||||
echo " cleaned up prefixed entries: ${removed[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user