From 820af2597b884b03fda92346fd12f7f97734921f Mon Sep 17 00:00:00 2001 From: Malik Salim Date: Sun, 22 Mar 2026 18:56:03 -0400 Subject: [PATCH] feat: add optional prefix for claude skill installs --- README.md | 2 ++ setup | 21 ++++++++++++++++++--- test/gen-skill-docs.test.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f9d65195..eeca3a10 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Open Claude Code and paste this. Claude does the rest. > Install gstack: run **`git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack && cd ~/.claude/skills/gstack && ./setup`** then add a "gstack" section to CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, and lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /review, /ship, /land-and-deploy, /canary, /benchmark, /browse, /qa, /qa-only, /design-review, /setup-browser-cookies, /setup-deploy, /retro, /investigate, /document-release, /codex, /cso, /autoplan, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade. Then ask the user if they also want to add gstack to the current project so teammates get it. +If you want prefixed Claude skill names in `~/.claude/skills/`, run `./setup --prefix gstack-`. That creates entries like `gstack-review` and `gstack-ship`. Codex installs already use the `gstack-` prefix by default. + ### Step 2: Add to your repo so teammates get it (optional) > Add gstack to this project: run **`cp -Rf ~/.claude/skills/gstack .claude/skills/gstack && rm -rf .claude/skills/gstack/.git && cd .claude/skills/gstack && ./setup`** then add a "gstack" section to this project's CLAUDE.md that says to use the /browse skill from gstack for all web browsing, never use mcp\_\_claude-in-chrome\_\_\* tools, lists the available skills: /office-hours, /plan-ceo-review, /plan-eng-review, /plan-design-review, /design-consultation, /review, /ship, /land-and-deploy, /canary, /benchmark, /browse, /qa, /qa-only, /design-review, /setup-browser-cookies, /setup-deploy, /retro, /investigate, /document-release, /codex, /cso, /careful, /freeze, /guard, /unfreeze, /gstack-upgrade, and tells Claude that if gstack skills aren't working, run `cd .claude/skills/gstack && ./setup` to build the binary and register skills. diff --git a/setup b/setup index d67bdec1..cf95e91b 100755 --- a/setup +++ b/setup @@ -19,10 +19,20 @@ esac # ─── Parse --host flag ───────────────────────────────────────── HOST="claude" +CLAUDE_PREFIX="" while [ $# -gt 0 ]; do case "$1" in --host) HOST="$2"; shift 2 ;; --host=*) HOST="${1#--host=}"; shift ;; + --prefix) + [ $# -lt 2 ] && echo "Missing value for --prefix" >&2 && exit 1 + CLAUDE_PREFIX="$2" + shift 2 + ;; + --prefix=*) + CLAUDE_PREFIX="${1#--prefix=}" + shift + ;; *) shift ;; esac done @@ -137,17 +147,22 @@ mkdir -p "$HOME/.gstack/projects" link_claude_skill_dirs() { local gstack_dir="$1" local skills_dir="$2" + local skill_prefix="${3:-}" local linked=() for skill_dir in "$gstack_dir"/*/; do if [ -f "$skill_dir/SKILL.md" ]; then skill_name="$(basename "$skill_dir")" + target_name="$skill_name" # Skip node_modules [ "$skill_name" = "node_modules" ] && continue - target="$skills_dir/$skill_name" + if [ -n "$skill_prefix" ] && [ "${skill_name#"$skill_prefix"}" = "$skill_name" ]; then + target_name="${skill_prefix}$skill_name" + fi + target="$skills_dir/$target_name" # Create or update symlink; skip if a real file/directory exists if [ -L "$target" ] || [ ! -e "$target" ]; then ln -snf "gstack/$skill_name" "$target" - linked+=("$skill_name") + linked+=("$target_name") fi fi done @@ -222,7 +237,7 @@ create_agents_sidecar() { SKILLS_BASENAME="$(basename "$SKILLS_DIR")" if [ "$INSTALL_CLAUDE" -eq 1 ]; then if [ "$SKILLS_BASENAME" = "skills" ]; then - link_claude_skill_dirs "$GSTACK_DIR" "$SKILLS_DIR" + link_claude_skill_dirs "$GSTACK_DIR" "$SKILLS_DIR" "$CLAUDE_PREFIX" echo "gstack ready (claude)." echo " browse: $BROWSE_BIN" else diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index d1d907a0..03b4bbea 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -982,11 +982,41 @@ describe('setup script validation', () => { expect(setupContent).toContain('claude|codex|auto'); }); + test('setup supports --prefix for Claude skill symlinks', () => { + expect(setupContent).toContain('--prefix'); + expect(setupContent).toContain('CLAUDE_PREFIX'); + }); + test('auto mode detects claude and codex binaries', () => { expect(setupContent).toContain('command -v claude'); expect(setupContent).toContain('command -v codex'); }); + test('link_claude_skill_dirs applies optional prefix to target names', () => { + const fnStart = setupContent.indexOf('link_claude_skill_dirs()'); + const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart)); + const fnBody = setupContent.slice(fnStart, fnEnd); + expect(fnBody).toContain('local skill_prefix="${3:-}"'); + expect(fnBody).toContain('target_name="$skill_name"'); + expect(fnBody).toContain('target="$skills_dir/$target_name"'); + }); + + test('Claude install passes CLAUDE_PREFIX into link_claude_skill_dirs', () => { + const claudeSection = setupContent.slice( + setupContent.indexOf('# 4. Install for Claude'), + setupContent.indexOf('# 5. Install for Codex') + ); + expect(claudeSection).toContain('link_claude_skill_dirs "$GSTACK_DIR" "$SKILLS_DIR" "$CLAUDE_PREFIX"'); + }); + + test('link_claude_skill_dirs avoids double-prefixing already-prefixed skills', () => { + const fnStart = setupContent.indexOf('link_claude_skill_dirs()'); + const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart)); + const fnBody = setupContent.slice(fnStart, fnEnd); + expect(fnBody).toContain('if [ -n "$skill_prefix" ] && [ "${skill_name#"$skill_prefix"}" = "$skill_name" ]; then'); + expect(fnBody).toContain('target_name="${skill_prefix}$skill_name"'); + }); + test('create_agents_sidecar links runtime assets', () => { // Sidecar must link bin, browse, review, qa const fnStart = setupContent.indexOf('create_agents_sidecar()');