mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +02:00
The two back-compat alias dirs — _gstack-command (root router) and connect-chrome (→ open-gstack-browser) — symlinked the canonical SKILL.md verbatim, so each alias re-served the canonical frontmatter name:. Claude Code keys skills on that name and requires global uniqueness: the connect-chrome duplicate silently shadowed /open-gstack-browser (whichever readdir returned first won), and the _gstack-command duplicate could drop the ENTIRE personal-skills set — every /gstack command vanished until the user hand-deleted the alias dirs, and the next setup re-broke it. Fix: copy-then-rewrite. A shared _install_alias_skill_md helper reads the SOURCE SKILL.md and writes a fresh copy with name: rewritten to the alias dir's own name (_gstack-command / connect-chrome / gstack-connect-chrome). sed never edits in place: on Unix the old install was a symlink into the repo, and an in-place rewrite through it would have corrupted the generated source (eng review E2). bin/gstack-relink gets the same treatment for its root-alias helper, and its discovery loop now skips symlinked source dirs so the connect-chrome repo symlink can't re-mint the duplicate. Tests assert: installed aliases are NOT symlinks, carry their own unique names, all installed frontmatter names are globally unique, re-runs refresh cleanly, legacy symlinked aliases are replaced not written through, and the source files stay byte-intact. Fixes #2511 Fixes #2201 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.0 KiB
Bash
Executable File
113 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-relink — re-create skill symlinks based on skill_prefix config
|
|
#
|
|
# Usage:
|
|
# gstack-relink
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
|
# GSTACK_INSTALL_DIR — override gstack install directory
|
|
# GSTACK_SKILLS_DIR — override target skills directory
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
GSTACK_CONFIG="${SCRIPT_DIR}/gstack-config"
|
|
|
|
# Detect install dir
|
|
INSTALL_DIR="${GSTACK_INSTALL_DIR:-}"
|
|
if [ -z "$INSTALL_DIR" ]; then
|
|
if [ -d "$HOME/.claude/skills/gstack" ]; then
|
|
INSTALL_DIR="$HOME/.claude/skills/gstack"
|
|
elif [ -d "${SCRIPT_DIR}/.." ] && [ -f "${SCRIPT_DIR}/../setup" ]; then
|
|
INSTALL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$INSTALL_DIR" ] || [ ! -d "$INSTALL_DIR" ]; then
|
|
echo "Error: gstack install directory not found." >&2
|
|
echo "Run: cd ~/.claude/skills/gstack && ./setup" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Detect target skills dir
|
|
SKILLS_DIR="${GSTACK_SKILLS_DIR:-$(dirname "$INSTALL_DIR")}"
|
|
[ -d "$SKILLS_DIR" ] || mkdir -p "$SKILLS_DIR"
|
|
|
|
# Read prefix setting
|
|
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
|
|
}
|
|
|
|
_link_root_skill_alias() {
|
|
local target="$SKILLS_DIR/_gstack-command"
|
|
|
|
[ -f "$INSTALL_DIR/SKILL.md" ] || return 0
|
|
[ -L "$target" ] && rm -f "$target"
|
|
mkdir -p "$target"
|
|
# Copy-then-rewrite, never a symlink (#2511): a symlinked alias re-serves
|
|
# the canonical `name: gstack`, Claude Code sees a duplicate skill name,
|
|
# and drops the ENTIRE personal-skills set. sed reads the source and writes
|
|
# a fresh copy — remove any prior symlink first so the redirect can never
|
|
# write through it into the generated source.
|
|
rm -f "$target/SKILL.md"
|
|
sed "1,/^---\$/ s/^name:[[:space:]].*/name: _gstack-command/" "$INSTALL_DIR/SKILL.md" > "$target/SKILL.md"
|
|
}
|
|
|
|
_link_root_skill_alias
|
|
|
|
# Discover skills (directories with SKILL.md, excluding meta dirs)
|
|
SKILL_COUNT=0
|
|
for skill_dir in "$INSTALL_DIR"/*/; do
|
|
[ -d "$skill_dir" ] || continue
|
|
# Skip symlinked skill dirs (connect-chrome → open-gstack-browser): linking
|
|
# one under the symlink's basename would duplicate the canonical frontmatter
|
|
# name and collide in Claude Code's skill registry (#2201). setup owns the
|
|
# rewritten-copy alias for those.
|
|
[ -L "${skill_dir%/}" ] && continue
|
|
skill=$(basename "$skill_dir")
|
|
# Skip non-skill directories
|
|
case "$skill" in bin|browse|design|docs|extension|lib|node_modules|scripts|test|.git|.github) continue ;; esac
|
|
[ -f "$skill_dir/SKILL.md" ] || continue
|
|
|
|
if [ "$PREFIX" = "true" ]; then
|
|
# Don't double-prefix directories already named gstack-*
|
|
case "$skill" in
|
|
gstack-*) link_name="$skill" ;;
|
|
*) link_name="gstack-$skill" ;;
|
|
esac
|
|
# Remove old flat entry if it exists (and isn't the same as the new link)
|
|
[ "$link_name" != "$skill" ] && _cleanup_skill_entry "$SKILLS_DIR/$skill"
|
|
else
|
|
link_name="$skill"
|
|
# Don't remove gstack-* dirs that are their real name (e.g., gstack-upgrade)
|
|
case "$skill" in
|
|
gstack-*) ;; # Already the real name, no old prefixed link to clean
|
|
*) _cleanup_skill_entry "$SKILLS_DIR/gstack-$skill" ;;
|
|
esac
|
|
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))
|
|
done
|
|
|
|
# Patch SKILL.md name: fields to match prefix setting
|
|
"$INSTALL_DIR/bin/gstack-patch-names" "$INSTALL_DIR" "$PREFIX"
|
|
|
|
if [ "$PREFIX" = "true" ]; then
|
|
echo "Relinked $SKILL_COUNT skills as gstack-*"
|
|
else
|
|
echo "Relinked $SKILL_COUNT skills as flat names"
|
|
fi
|