mirror of https://github.com/garrytan/gstack.git
fix(setup): preserve hidden skill visibility
This commit is contained in:
parent
38ff0793c3
commit
f4b3b714e5
|
|
@ -35,6 +35,7 @@ SKILLS_DIR="${GSTACK_SKILLS_DIR:-$(dirname "$INSTALL_DIR")}"
|
|||
|
||||
# Read prefix setting
|
||||
PREFIX=$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || echo "false")
|
||||
GSTACK_HIDDEN_SKILLS_FILE="${GSTACK_HIDDEN_SKILLS_FILE:-$HOME/.gstack/hidden-skills}"
|
||||
|
||||
# Helper: remove old skill entry (symlink or real directory with symlinked SKILL.md)
|
||||
_cleanup_skill_entry() {
|
||||
|
|
@ -46,6 +47,17 @@ _cleanup_skill_entry() {
|
|||
fi
|
||||
}
|
||||
|
||||
_skill_hidden() {
|
||||
local source_name="$1" projected_name="$2"
|
||||
[ -f "$GSTACK_HIDDEN_SKILLS_FILE" ] || return 1
|
||||
grep -Fqx "$source_name" "$GSTACK_HIDDEN_SKILLS_FILE" 2>/dev/null && return 0
|
||||
case "$source_name" in
|
||||
gstack-*) ;;
|
||||
*) grep -Fqx "gstack-$source_name" "$GSTACK_HIDDEN_SKILLS_FILE" 2>/dev/null && return 0 ;;
|
||||
esac
|
||||
grep -Fqx "$projected_name" "$GSTACK_HIDDEN_SKILLS_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
_link_root_skill_alias() {
|
||||
local target="$SKILLS_DIR/_gstack-command"
|
||||
|
||||
|
|
@ -82,6 +94,12 @@ for skill_dir in "$INSTALL_DIR"/*/; do
|
|||
*) _cleanup_skill_entry "$SKILLS_DIR/gstack-$skill" ;;
|
||||
esac
|
||||
fi
|
||||
if _skill_hidden "$skill" "$link_name"; then
|
||||
_cleanup_skill_entry "$SKILLS_DIR/$skill"
|
||||
_cleanup_skill_entry "$SKILLS_DIR/gstack-$skill"
|
||||
_cleanup_skill_entry "$SKILLS_DIR/$link_name"
|
||||
continue
|
||||
fi
|
||||
target="$SKILLS_DIR/$link_name"
|
||||
# Upgrade old directory symlinks to real directories
|
||||
[ -L "$target" ] && rm -f "$target"
|
||||
|
|
|
|||
23
setup
23
setup
|
|
@ -531,6 +531,20 @@ fi
|
|||
# 3. Ensure ~/.gstack global state directory exists
|
||||
mkdir -p "$HOME/.gstack/projects"
|
||||
|
||||
# Optional user-level visibility denylist. One exact skill name per line;
|
||||
# either source form (context-save) or projected form (gstack-context-save).
|
||||
GSTACK_HIDDEN_SKILLS_FILE="${GSTACK_HIDDEN_SKILLS_FILE:-$HOME/.gstack/hidden-skills}"
|
||||
_gstack_skill_hidden() {
|
||||
local source_name="$1" projected_name="${2:-$1}"
|
||||
[ -f "$GSTACK_HIDDEN_SKILLS_FILE" ] || return 1
|
||||
grep -Fqx "$source_name" "$GSTACK_HIDDEN_SKILLS_FILE" 2>/dev/null && return 0
|
||||
case "$source_name" in
|
||||
gstack-*) ;;
|
||||
*) grep -Fqx "gstack-$source_name" "$GSTACK_HIDDEN_SKILLS_FILE" 2>/dev/null && return 0 ;;
|
||||
esac
|
||||
grep -Fqx "$projected_name" "$GSTACK_HIDDEN_SKILLS_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
|
||||
# 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
|
||||
|
|
@ -559,6 +573,14 @@ link_claude_skill_dirs() {
|
|||
link_name="$skill_name"
|
||||
fi
|
||||
target="$skills_dir/$link_name"
|
||||
if _gstack_skill_hidden "$dir_name" "$link_name"; then
|
||||
if [ -L "$target" ]; then
|
||||
rm -f "$target"
|
||||
elif [ -d "$target" ] && [ -L "$target/SKILL.md" ]; then
|
||||
rm -rf "$target"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
# Upgrade old directory symlinks to real directories
|
||||
if [ -L "$target" ]; then
|
||||
rm -f "$target"
|
||||
|
|
@ -726,6 +748,7 @@ link_codex_skill_dirs() {
|
|||
# browse/), not a skill. Linking it would overwrite the root gstack
|
||||
# symlink that Step 5 already pointed at the repo root.
|
||||
[ "$skill_name" = "gstack" ] && continue
|
||||
_gstack_skill_hidden "$skill_name" "$skill_name" && continue
|
||||
target="$skills_dir/$skill_name"
|
||||
# Create or update symlink
|
||||
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
||||
|
|
|
|||
|
|
@ -2318,6 +2318,7 @@ describe('setup script validation', () => {
|
|||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||
expect(fnBody).toContain('.agents/skills');
|
||||
expect(fnBody).toContain('gstack*');
|
||||
expect(fnBody).toContain('_gstack_skill_hidden');
|
||||
});
|
||||
|
||||
test('link_claude_skill_dirs creates real directories with absolute SKILL.md symlinks', () => {
|
||||
|
|
@ -2329,6 +2330,7 @@ describe('setup script validation', () => {
|
|||
expect(fnBody).toContain('mkdir -p "$target"');
|
||||
// v1.36.0.0: routes through _link_or_copy helper for Windows fallback (cp on MSYS2/Git Bash).
|
||||
expect(fnBody).toContain('_link_or_copy "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"');
|
||||
expect(fnBody).toContain('_gstack_skill_hidden');
|
||||
});
|
||||
|
||||
// REGRESSION: cleanup functions must handle both old symlinks AND new real-directory pattern
|
||||
|
|
|
|||
|
|
@ -113,6 +113,44 @@ describe('gstack-relink (#578)', () => {
|
|||
expect(output).toContain('flat');
|
||||
});
|
||||
|
||||
test('does not relink a skill listed in the visibility denylist', () => {
|
||||
setupMockInstall(['context-save', 'qa']);
|
||||
const hiddenFile = path.join(tmpDir, 'hidden-skills');
|
||||
fs.writeFileSync(hiddenFile, 'gstack-context-save\n');
|
||||
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix true`, {
|
||||
GSTACK_INSTALL_DIR: installDir,
|
||||
GSTACK_SKILLS_DIR: skillsDir,
|
||||
});
|
||||
|
||||
run(`${path.join(installDir, 'bin', 'gstack-relink')}`, {
|
||||
GSTACK_INSTALL_DIR: installDir,
|
||||
GSTACK_SKILLS_DIR: skillsDir,
|
||||
GSTACK_HIDDEN_SKILLS_FILE: hiddenFile,
|
||||
});
|
||||
|
||||
expect(fs.existsSync(path.join(skillsDir, 'gstack-context-save'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(skillsDir, 'gstack-qa'))).toBe(true);
|
||||
});
|
||||
|
||||
test('visibility denylist also applies in no-prefix mode', () => {
|
||||
setupMockInstall(['context-save', 'qa']);
|
||||
const hiddenFile = path.join(tmpDir, 'hidden-skills');
|
||||
fs.writeFileSync(hiddenFile, 'gstack-context-save\n');
|
||||
run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix false`, {
|
||||
GSTACK_INSTALL_DIR: installDir,
|
||||
GSTACK_SKILLS_DIR: skillsDir,
|
||||
});
|
||||
|
||||
run(`${path.join(installDir, 'bin', 'gstack-relink')}`, {
|
||||
GSTACK_INSTALL_DIR: installDir,
|
||||
GSTACK_SKILLS_DIR: skillsDir,
|
||||
GSTACK_HIDDEN_SKILLS_FILE: hiddenFile,
|
||||
});
|
||||
|
||||
expect(fs.existsSync(path.join(skillsDir, 'context-save'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(skillsDir, 'qa'))).toBe(true);
|
||||
});
|
||||
|
||||
// REGRESSION: unprefixed skills must be real directories, not symlinks (#761)
|
||||
// Claude Code auto-prefixes skills nested under a parent dir symlink.
|
||||
// e.g., `qa -> gstack/qa` gets discovered as "gstack-qa", not "qa".
|
||||
|
|
|
|||
Loading…
Reference in New Issue