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
|
# 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")
|
||||||
|
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)
|
# Helper: remove old skill entry (symlink or real directory with symlinked SKILL.md)
|
||||||
_cleanup_skill_entry() {
|
_cleanup_skill_entry() {
|
||||||
|
|
@ -46,6 +47,17 @@ _cleanup_skill_entry() {
|
||||||
fi
|
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() {
|
_link_root_skill_alias() {
|
||||||
local target="$SKILLS_DIR/_gstack-command"
|
local target="$SKILLS_DIR/_gstack-command"
|
||||||
|
|
||||||
|
|
@ -82,6 +94,12 @@ for skill_dir in "$INSTALL_DIR"/*/; do
|
||||||
*) _cleanup_skill_entry "$SKILLS_DIR/gstack-$skill" ;;
|
*) _cleanup_skill_entry "$SKILLS_DIR/gstack-$skill" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
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"
|
target="$SKILLS_DIR/$link_name"
|
||||||
# Upgrade old directory symlinks to real directories
|
# Upgrade old directory symlinks to real directories
|
||||||
[ -L "$target" ] && rm -f "$target"
|
[ -L "$target" ] && rm -f "$target"
|
||||||
|
|
|
||||||
23
setup
23
setup
|
|
@ -531,6 +531,20 @@ fi
|
||||||
# 3. Ensure ~/.gstack global state directory exists
|
# 3. Ensure ~/.gstack global state directory exists
|
||||||
mkdir -p "$HOME/.gstack/projects"
|
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 ──
|
# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
|
||||||
# Creates real directories (not symlinks) at the top level with a SKILL.md symlink
|
# 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
|
# 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"
|
link_name="$skill_name"
|
||||||
fi
|
fi
|
||||||
target="$skills_dir/$link_name"
|
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
|
# Upgrade old directory symlinks to real directories
|
||||||
if [ -L "$target" ]; then
|
if [ -L "$target" ]; then
|
||||||
rm -f "$target"
|
rm -f "$target"
|
||||||
|
|
@ -726,6 +748,7 @@ link_codex_skill_dirs() {
|
||||||
# browse/), not a skill. Linking it would overwrite the root gstack
|
# browse/), not a skill. Linking it would overwrite the root gstack
|
||||||
# symlink that Step 5 already pointed at the repo root.
|
# symlink that Step 5 already pointed at the repo root.
|
||||||
[ "$skill_name" = "gstack" ] && continue
|
[ "$skill_name" = "gstack" ] && continue
|
||||||
|
_gstack_skill_hidden "$skill_name" "$skill_name" && continue
|
||||||
target="$skills_dir/$skill_name"
|
target="$skills_dir/$skill_name"
|
||||||
# Create or update symlink
|
# Create or update symlink
|
||||||
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
||||||
|
|
|
||||||
|
|
@ -2318,6 +2318,7 @@ describe('setup script validation', () => {
|
||||||
const fnBody = setupContent.slice(fnStart, fnEnd);
|
const fnBody = setupContent.slice(fnStart, fnEnd);
|
||||||
expect(fnBody).toContain('.agents/skills');
|
expect(fnBody).toContain('.agents/skills');
|
||||||
expect(fnBody).toContain('gstack*');
|
expect(fnBody).toContain('gstack*');
|
||||||
|
expect(fnBody).toContain('_gstack_skill_hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('link_claude_skill_dirs creates real directories with absolute SKILL.md symlinks', () => {
|
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"');
|
expect(fnBody).toContain('mkdir -p "$target"');
|
||||||
// v1.36.0.0: routes through _link_or_copy helper for Windows fallback (cp on MSYS2/Git Bash).
|
// 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('_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
|
// 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');
|
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)
|
// REGRESSION: unprefixed skills must be real directories, not symlinks (#761)
|
||||||
// Claude Code auto-prefixes skills nested under a parent dir symlink.
|
// Claude Code auto-prefixes skills nested under a parent dir symlink.
|
||||||
// e.g., `qa -> gstack/qa` gets discovered as "gstack-qa", not "qa".
|
// e.g., `qa -> gstack/qa` gets discovered as "gstack-qa", not "qa".
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue