From 033b58b6ff2ba8cdbfbcc6d126908586cf975e64 Mon Sep 17 00:00:00 2001 From: Jayesh Betala Date: Mon, 18 May 2026 12:14:30 +0530 Subject: [PATCH] fix(setup): register root gstack slash alias --- bin/gstack-relink | 11 +++++++++++ setup | 22 ++++++++++++++++++++++ test/gen-skill-docs.test.ts | 14 ++++++++++++++ test/relink.test.ts | 31 +++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/bin/gstack-relink b/bin/gstack-relink index 31e6b82f06..dd2a681fa4 100755 --- a/bin/gstack-relink +++ b/bin/gstack-relink @@ -46,6 +46,17 @@ _cleanup_skill_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" + ln -snf "$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 diff --git a/setup b/setup index b51fed83df..0c196bd813 100755 --- a/setup +++ b/setup @@ -452,6 +452,26 @@ link_claude_skill_dirs() { fi } +# Claude Code skips the repo-shaped ~/.claude/skills/gstack directory when +# building the user-facing slash-command list. Keep the repo path for runtime +# assets, and add a separate thin wrapper whose frontmatter name remains +# `gstack` so `/gstack` can autocomplete. +link_claude_root_skill_alias() { + local gstack_dir="$1" + local skills_dir="$2" + local target="$skills_dir/_gstack-command" + + [ -f "$gstack_dir/SKILL.md" ] || return 0 + if [ -L "$target" ]; then + rm -f "$target" + fi + mkdir -p "$target" + if [ -L "$target/SKILL.md" ]; then rm "$target/SKILL.md"; fi + _link_or_copy "$gstack_dir/SKILL.md" "$target/SKILL.md" + echo " linked root skill alias: gstack" + _print_windows_copy_note_once +} + # ─── Helper: remove old unprefixed Claude skill entries ─────────────────────── # Migration: when switching from flat names to gstack- prefixed names, # clean up stale symlinks or directories that point into the gstack directory. @@ -838,6 +858,7 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then # reads the correct (patched) name: values for symlink naming "$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX" link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" + link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" # Self-healing: re-run gstack-relink to ensure name: fields and directory # names are consistent with the config. This catches cases where an interrupted # setup, stale git state, or gen:skill-docs left name: fields out of sync. @@ -909,6 +930,7 @@ if [ "$INSTALL_CLAUDE" -eq 1 ]; then fi "$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX" link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" + link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR" GSTACK_RELINK="$SOURCE_GSTACK_DIR/bin/gstack-relink" if [ -x "$GSTACK_RELINK" ]; then GSTACK_SKILLS_DIR="$INSTALL_SKILLS_DIR" GSTACK_INSTALL_DIR="$SOURCE_GSTACK_DIR" "$GSTACK_RELINK" >/dev/null 2>&1 || true diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 8e6b8b486a..090f09aac4 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -2273,6 +2273,20 @@ describe('setup script validation', () => { expect(fnBody).toContain('rm -f "$target"'); }); + test('setup links root gstack skill through a thin Claude wrapper alias', () => { + const fnStart = setupContent.indexOf('link_claude_root_skill_alias()'); + const fnEnd = setupContent.indexOf('# ─── Helper: remove old unprefixed Claude skill entries', fnStart); + const fnBody = setupContent.slice(fnStart, fnEnd); + expect(fnBody).toContain('_gstack-command'); + expect(fnBody).toContain('_link_or_copy "$gstack_dir/SKILL.md" "$target/SKILL.md"'); + + const claudeSection = setupContent.slice( + setupContent.indexOf('# 4. Install for Claude'), + setupContent.indexOf('# 5. Install for Codex') + ); + expect(claudeSection).toContain('link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"'); + }); + test('setup supports --host auto|claude|codex|kiro|opencode', () => { expect(setupContent).toContain('--host'); expect(setupContent).toContain('claude|codex|kiro|factory|opencode|auto'); diff --git a/test/relink.test.ts b/test/relink.test.ts index e5cd52061e..d83c4cd378 100644 --- a/test/relink.test.ts +++ b/test/relink.test.ts @@ -187,6 +187,37 @@ describe('gstack-relink (#578)', () => { expect(fs.lstatSync(path.join(skillsDir, 'qa', 'SKILL.md')).isSymbolicLink()).toBe(true); }); + test('creates a thin root alias wrapper for the /gstack slash command', () => { + setupMockInstall(['qa']); + fs.writeFileSync( + path.join(installDir, 'SKILL.md'), + '---\nname: gstack\ndescription: root\n---\n# gstack', + ); + + 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, + }); + + const aliasDir = path.join(skillsDir, '_gstack-command'); + const aliasSkill = path.join(aliasDir, 'SKILL.md'); + expect(fs.lstatSync(aliasDir).isDirectory()).toBe(true); + expect(fs.lstatSync(aliasDir).isSymbolicLink()).toBe(false); + expect(fs.lstatSync(aliasSkill).isSymbolicLink()).toBe(true); + expect(fs.readlinkSync(aliasSkill)).toBe(path.join(installDir, 'SKILL.md')); + expect(fs.readFileSync(aliasSkill, 'utf-8')).toContain('name: gstack'); + + run(`${path.join(installDir, 'bin', 'gstack-config')} set skill_prefix true`, { + GSTACK_INSTALL_DIR: installDir, + GSTACK_SKILLS_DIR: skillsDir, + }); + expect(fs.existsSync(aliasSkill)).toBe(true); + }); + // FIRST INSTALL: --no-prefix must create ONLY flat names, zero gstack-* pollution test('first install --no-prefix: only flat names exist, zero gstack-* entries', () => { setupMockInstall(['qa', 'ship', 'review', 'plan-ceo-review', 'gstack-upgrade']);