mirror of https://github.com/garrytan/gstack.git
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>
This commit is contained in:
parent
6169273d16
commit
4aac13baae
|
|
@ -36,6 +36,16 @@ SKILLS_DIR="${GSTACK_SKILLS_DIR:-$(dirname "$INSTALL_DIR")}"
|
||||||
# Read prefix setting
|
# Read prefix setting
|
||||||
PREFIX=$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || echo "false")
|
PREFIX=$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || echo "false")
|
||||||
|
|
||||||
|
# Helper: remove old skill entry (symlink or real directory with symlinked SKILL.md)
|
||||||
|
_cleanup_skill_entry() {
|
||||||
|
local entry="$1"
|
||||||
|
if [ -L "$entry" ]; then
|
||||||
|
rm -f "$entry"
|
||||||
|
elif [ -d "$entry" ] && [ -L "$entry/SKILL.md" ]; then
|
||||||
|
rm -rf "$entry"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Discover skills (directories with SKILL.md, excluding meta dirs)
|
# Discover skills (directories with SKILL.md, excluding meta dirs)
|
||||||
SKILL_COUNT=0
|
SKILL_COUNT=0
|
||||||
for skill_dir in "$INSTALL_DIR"/*/; do
|
for skill_dir in "$INSTALL_DIR"/*/; do
|
||||||
|
|
@ -51,18 +61,22 @@ for skill_dir in "$INSTALL_DIR"/*/; do
|
||||||
gstack-*) link_name="$skill" ;;
|
gstack-*) link_name="$skill" ;;
|
||||||
*) link_name="gstack-$skill" ;;
|
*) link_name="gstack-$skill" ;;
|
||||||
esac
|
esac
|
||||||
ln -sfn "$INSTALL_DIR/$skill" "$SKILLS_DIR/$link_name"
|
# Remove old flat entry if it exists (and isn't the same as the new link)
|
||||||
# Remove old flat symlink if it exists (and isn't the same as the new link)
|
[ "$link_name" != "$skill" ] && _cleanup_skill_entry "$SKILLS_DIR/$skill"
|
||||||
[ "$link_name" != "$skill" ] && [ -L "$SKILLS_DIR/$skill" ] && rm -f "$SKILLS_DIR/$skill"
|
|
||||||
else
|
else
|
||||||
# Create flat symlink, remove gstack-* if exists
|
link_name="$skill"
|
||||||
ln -sfn "$INSTALL_DIR/$skill" "$SKILLS_DIR/$skill"
|
|
||||||
# Don't remove gstack-* dirs that are their real name (e.g., gstack-upgrade)
|
# Don't remove gstack-* dirs that are their real name (e.g., gstack-upgrade)
|
||||||
case "$skill" in
|
case "$skill" in
|
||||||
gstack-*) ;; # Already the real name, no old prefixed link to clean
|
gstack-*) ;; # Already the real name, no old prefixed link to clean
|
||||||
*) [ -L "$SKILLS_DIR/gstack-$skill" ] && rm -f "$SKILLS_DIR/gstack-$skill" ;;
|
*) _cleanup_skill_entry "$SKILLS_DIR/gstack-$skill" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
target="$SKILLS_DIR/$link_name"
|
||||||
|
# Upgrade old directory symlinks to real directories
|
||||||
|
[ -L "$target" ] && rm -f "$target"
|
||||||
|
# Create real directory with symlinked SKILL.md (absolute path)
|
||||||
|
mkdir -p "$target"
|
||||||
|
ln -snf "$INSTALL_DIR/$skill/SKILL.md" "$target/SKILL.md"
|
||||||
SKILL_COUNT=$((SKILL_COUNT + 1))
|
SKILL_COUNT=$((SKILL_COUNT + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gstack",
|
"name": "gstack",
|
||||||
"version": "0.15.0.0",
|
"version": "0.15.1.0",
|
||||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
55
setup
55
setup
|
|
@ -263,9 +263,11 @@ fi
|
||||||
mkdir -p "$HOME/.gstack/projects"
|
mkdir -p "$HOME/.gstack/projects"
|
||||||
|
|
||||||
# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
|
# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
|
||||||
# When SKILL_PREFIX=1 (default), symlinks are prefixed with "gstack-" to avoid
|
# Creates real directories (not symlinks) at the top level with a SKILL.md symlink
|
||||||
# namespace pollution (e.g., gstack-review instead of review).
|
# inside. This ensures Claude discovers them as top-level skills, not nested under
|
||||||
# Use --no-prefix to restore the old flat names.
|
# 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() {
|
link_claude_skill_dirs() {
|
||||||
local gstack_dir="$1"
|
local gstack_dir="$1"
|
||||||
local skills_dir="$2"
|
local skills_dir="$2"
|
||||||
|
|
@ -288,9 +290,14 @@ link_claude_skill_dirs() {
|
||||||
link_name="$skill_name"
|
link_name="$skill_name"
|
||||||
fi
|
fi
|
||||||
target="$skills_dir/$link_name"
|
target="$skills_dir/$link_name"
|
||||||
# Create or update symlink; skip if a real file/directory exists
|
# Upgrade old directory symlinks to real directories
|
||||||
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
if [ -L "$target" ]; then
|
||||||
ln -snf "gstack/$dir_name" "$target"
|
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")
|
linked+=("$link_name")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -300,9 +307,9 @@ link_claude_skill_dirs() {
|
||||||
fi
|
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,
|
# 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() {
|
cleanup_old_claude_symlinks() {
|
||||||
local gstack_dir="$1"
|
local gstack_dir="$1"
|
||||||
local skills_dir="$2"
|
local skills_dir="$2"
|
||||||
|
|
@ -314,7 +321,7 @@ cleanup_old_claude_symlinks() {
|
||||||
# Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
|
# Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
|
||||||
case "$skill_name" in gstack-*) continue ;; esac
|
case "$skill_name" in gstack-*) continue ;; esac
|
||||||
old_target="$skills_dir/$skill_name"
|
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
|
if [ -L "$old_target" ]; then
|
||||||
link_dest="$(readlink "$old_target" 2>/dev/null || true)"
|
link_dest="$(readlink "$old_target" 2>/dev/null || true)"
|
||||||
case "$link_dest" in
|
case "$link_dest" in
|
||||||
|
|
@ -323,17 +330,26 @@ cleanup_old_claude_symlinks() {
|
||||||
removed+=("$skill_name")
|
removed+=("$skill_name")
|
||||||
;;
|
;;
|
||||||
esac
|
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
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ ${#removed[@]} -gt 0 ]; then
|
if [ ${#removed[@]} -gt 0 ]; then
|
||||||
echo " cleaned up old symlinks: ${removed[*]}"
|
echo " cleaned up old entries: ${removed[*]}"
|
||||||
fi
|
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,
|
# 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() {
|
cleanup_prefixed_claude_symlinks() {
|
||||||
local gstack_dir="$1"
|
local gstack_dir="$1"
|
||||||
local skills_dir="$2"
|
local skills_dir="$2"
|
||||||
|
|
@ -342,11 +358,11 @@ cleanup_prefixed_claude_symlinks() {
|
||||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||||
skill_name="$(basename "$skill_dir")"
|
skill_name="$(basename "$skill_dir")"
|
||||||
[ "$skill_name" = "node_modules" ] && continue
|
[ "$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)
|
# (e.g., remove gstack-qa but NOT gstack-upgrade which is the real dir name)
|
||||||
case "$skill_name" in gstack-*) continue ;; esac
|
case "$skill_name" in gstack-*) continue ;; esac
|
||||||
prefixed_target="$skills_dir/gstack-$skill_name"
|
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
|
if [ -L "$prefixed_target" ]; then
|
||||||
link_dest="$(readlink "$prefixed_target" 2>/dev/null || true)"
|
link_dest="$(readlink "$prefixed_target" 2>/dev/null || true)"
|
||||||
case "$link_dest" in
|
case "$link_dest" in
|
||||||
|
|
@ -355,11 +371,20 @@ cleanup_prefixed_claude_symlinks() {
|
||||||
removed+=("gstack-$skill_name")
|
removed+=("gstack-$skill_name")
|
||||||
;;
|
;;
|
||||||
esac
|
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
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ ${#removed[@]} -gt 0 ]; then
|
if [ ${#removed[@]} -gt 0 ]; then
|
||||||
echo " cleaned up prefixed symlinks: ${removed[*]}"
|
echo " cleaned up prefixed entries: ${removed[*]}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1969,13 +1969,14 @@ describe('setup script validation', () => {
|
||||||
expect(fnBody).toContain('gstack*');
|
expect(fnBody).toContain('gstack*');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('link_claude_skill_dirs creates relative symlinks', () => {
|
test('link_claude_skill_dirs creates real directories with absolute SKILL.md symlinks', () => {
|
||||||
// Claude links should be relative: ln -snf "gstack/$dir_name"
|
// Claude links should be real directories with absolute SKILL.md symlinks
|
||||||
// Uses dir_name (not skill_name) because symlink target must point to the physical directory
|
// to ensure Claude Code discovers them as top-level skills (not nested under gstack/)
|
||||||
const fnStart = setupContent.indexOf('link_claude_skill_dirs()');
|
const fnStart = setupContent.indexOf('link_claude_skill_dirs()');
|
||||||
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
|
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
|
||||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||||
expect(fnBody).toContain('ln -snf "gstack/$dir_name"');
|
expect(fnBody).toContain('mkdir -p "$target"');
|
||||||
|
expect(fnBody).toContain('ln -snf "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('setup supports --host auto|claude|codex|kiro', () => {
|
test('setup supports --host auto|claude|codex|kiro', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue