Files
gstack/bin/gstack-team-init
T
Garry Tanandcapy-ai[bot] e4e4e42967 v1.92.22.0 feat: Gemini CLI host, and older installs drop the dev toolchain (#3778)
* setup: explicit contributor marker; move pre-v1.91.231.0 installs to production deps (#3447)

./setup used node_modules/typescript as its contributor signal, so every
install made before v1.91.231.0 (a full bun install) kept ~520 MB of test and
eval packages forever. The contributor is now the clone bin/dev-setup marks
with git config gstack.contributor true, or GSTACK_CONTRIBUTOR=1. An unmarked
clone with the dev toolchain moves node_modules aside under a checkout lock,
installs production-only, verifies every dependency, and drops the backup; any
failure puts the full install back with SETUP_DEPS_MIGRATE_FAILED. CI and a
clone whose git config cannot be read never migrate. Bun cache entries for the
pinned Agent SDK are removed only when no other project links to them.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* hosts: add Gemini CLI as an experimental host (#1247)

./setup --host gemini (alias gemini-cli; --host auto when gemini is on PATH)
installs gstack-<skill> directories into ~/.gemini/skills, which Gemini CLI
0.66.0 lists with gemini skills list, plus the ~/.gemini/skills/gstack runtime
root. Renders go to .gemini-cli/ (gitignored) with name+description
frontmatter whose names match their directories, CLAUDE.md rewritten to
GEMINI.md, and a tool glossary for Gemini CLI's names (and Antigravity's, which
can read the same folder). gstack-uninstall removes only gstack-managed entries
there, so the user's own skills stay. Docs: ADDING_A_HOST.md verified facts,
hosts.md, uninstall.md, README.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (leave Bun's shared cache alone)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: Gemini CLI host and contributor marker (#1247, #3447)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: host lists name Copilot, Antigravity and Gemini CLI

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: the plan-mode re-render guard count includes the Gemini CLI host

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: troubleshooting's runtime-root guard host list names Gemini CLI

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-01 09:35:42 +00:00

270 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-team-init — generate repo-level bootstrap files for team mode
#
# Usage:
# gstack-team-init optional # gentle CLAUDE.md suggestion, one-time offer
# gstack-team-init required # CLAUDE.md enforcement + PreToolUse hook
#
# Run from the root of your team's repo (not from the gstack directory).
# Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a
# pipe in the forked child before exec, with no reader on the other end. On
# macOS under pipe-KVA pressure a fresh pipe gets a 512-byte buffer, so any
# body >=512B blocks write() forever and the script hangs at startup with no
# output. Compat level 50 restores the tempfile path. These scripts are
# bash-3.2-clean, so the compat level costs them nothing. Not exported: the
# guard is per-script, and it survives `bash script.sh` call sites that
# bypass the shebang.
BASH_COMPAT=50
set -euo pipefail
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
MODE="${1:-}"
if [ "$MODE" != "optional" ] && [ "$MODE" != "required" ]; then
echo "Usage: gstack-team-init {optional|required}" >&2
echo "" >&2
echo " optional — suggest gstack install once per developer (gentle)" >&2
echo " required — enforce gstack install, block work without it" >&2
exit 1
fi
# Must be in a git repo
if ! git rev-parse --show-toplevel >/dev/null 2>&1; then
echo "Error: not in a git repository. Run from your project root." >&2
exit 1
fi
REPO_ROOT=$(git rev-parse --show-toplevel)
CLAUDE_MD="$REPO_ROOT/CLAUDE.md"
GENERATED=()
# ── Migrate vendored copy if present ──────────────────────────
if [ -d "$REPO_ROOT/.claude/skills/gstack" ] && [ ! -L "$REPO_ROOT/.claude/skills/gstack" ]; then
if [ -f "$REPO_ROOT/.claude/skills/gstack/VERSION" ] || [ -d "$REPO_ROOT/.claude/skills/gstack/.git" ]; then
echo " Found vendored gstack copy at $REPO_ROOT/.claude/skills/gstack"
echo " Team mode uses the global install — removing vendored copy..."
( cd "$REPO_ROOT" && git rm -r --cached .claude/skills/gstack/ 2>/dev/null ) || true
if [ -f "$REPO_ROOT/.gitignore" ]; then
if ! grep -qF '.claude/skills/gstack/' "$REPO_ROOT/.gitignore" 2>/dev/null; then
echo '.claude/skills/gstack/' >> "$REPO_ROOT/.gitignore"
fi
else
echo '.claude/skills/gstack/' > "$REPO_ROOT/.gitignore"
fi
rm -rf "$REPO_ROOT/.claude/skills/gstack"
GENERATED+=(".gitignore")
echo " Removed vendored copy and added .claude/skills/gstack/ to .gitignore"
fi
fi
# ── CLAUDE.md snippet ──────────────────────────────────────────
if [ "$MODE" = "optional" ]; then
SNIPPET='## gstack (recommended)
This project uses [gstack](https://github.com/garrytan/gstack) for AI-assisted workflows.
Install it for the best experience:
```bash
git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup --team
```
Skills like /qa, /ship, /review, /investigate, and /browse become available after install.
Use /browse for all web browsing (Aside first, the bundled gstack browser as fallback). Use ~/.claude/skills/gstack/... for gstack file paths.'
else
SNIPPET='## gstack (REQUIRED — global install)
**Before doing ANY work, verify gstack is installed:**
```bash
_GS=""
for _D in "${GSTACK_ROOT:-}" "$HOME/.claude/skills/gstack" "$HOME/.codex/skills/gstack" "$HOME/.factory/skills/gstack" "$HOME/.kiro/skills/gstack" "$HOME/.config/opencode/skills/gstack" "$HOME/.slate/skills/gstack" "$HOME/.cursor/skills/gstack" "$HOME/.openclaw/skills/gstack" "$HOME/.hermes/skills/gstack" "$HOME/.gbrain/skills/gstack" "$HOME/.copilot/skills/gstack" "$HOME/.gemini/antigravity-cli/skills/gstack" "$HOME/.gemini/skills/gstack" "$HOME/.gstack/repos/gstack"; do
[ -z "$_GS" ] && [ -n "$_D" ] && [ -d "$_D/bin" ] && _GS="$_D"
done
[ -n "$_GS" ] && echo "GSTACK_OK: $_GS" || echo "GSTACK_MISSING"
```
If GSTACK_MISSING: STOP. Do not proceed. Tell the user:
> gstack is required for all AI-assisted work in this repo.
> Install it:
> ```bash
> git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
> cd ~/.claude/skills/gstack && ./setup --team
> ```
> Then restart your AI coding tool.
Do not skip skills, ignore gstack errors, or work around missing gstack.
Using gstack skills: After install, skills like /qa, /ship, /review, /investigate,
and /browse are available. Use /browse for all web browsing (Aside first, the bundled gstack browser as fallback).
Use the resolved install path above for gstack file paths
(default: ~/.claude/skills/gstack).
The Skill gate in .claude/settings.json runs .claude/hooks/check-gstack.cjs with
Node.js and blocks when it cannot run. If a skill is blocked with "node: not
found", tell the user to install Node.js (https://nodejs.org); if it is blocked
with "Cannot find module ... check-gstack.cjs", the hook file is missing from
this checkout. Do not work around either block.'
fi
# Check if CLAUDE.md already has a gstack section
if [ -f "$CLAUDE_MD" ] && grep -q "## gstack" "$CLAUDE_MD" 2>/dev/null; then
echo "CLAUDE.md already has a gstack section. Skipping CLAUDE.md update."
echo " To replace it, remove the existing ## gstack section and re-run."
else
if [ -f "$CLAUDE_MD" ]; then
echo "" >> "$CLAUDE_MD"
fi
echo "$SNIPPET" >> "$CLAUDE_MD"
GENERATED+=("CLAUDE.md")
echo " + CLAUDE.md — added gstack $MODE section"
fi
# ── Required mode: enforcement hook ────────────────────────────
if [ "$MODE" = "required" ]; then
HOOKS_DIR="$REPO_ROOT/.claude/hooks"
SETTINGS="$REPO_ROOT/.claude/settings.json"
# Create the enforcement hook. Claude Code and Copilot CLI may run hook
# commands through sh, cmd.exe or PowerShell, so the hook is CommonJS run by
# node, and the registered command holds no shell variable syntax: it reads
# CLAUDE_PROJECT_DIR through process.env. `.cjs` keeps a project's
# package.json "type" from changing how node loads it.
mkdir -p "$HOOKS_DIR"
cat > "$HOOKS_DIR/check-gstack.cjs" << 'HOOK_EOF'
'use strict';
// Block skill usage when gstack is not installed globally.
//
// Resolve the install root the way gstack skill preambles do: the GSTACK_ROOT
// env var first, then every host's global install location, then the migrated
// repo location. Block only when NONE exist (#2500 — hardcoding
// ~/.claude/skills/gstack false-blocked Codex-host and migrated-repo installs).
const fs = require('fs');
const os = require('os');
const path = require('path');
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
const roots = [process.env.GSTACK_ROOT || ''].concat([
'.claude/skills/gstack', '.codex/skills/gstack', '.factory/skills/gstack', '.kiro/skills/gstack',
'.config/opencode/skills/gstack', '.slate/skills/gstack', '.cursor/skills/gstack', '.openclaw/skills/gstack',
'.hermes/skills/gstack', '.gbrain/skills/gstack', '.copilot/skills/gstack', '.gemini/antigravity-cli/skills/gstack', '.gemini/skills/gstack', '.gstack/repos/gstack',
].map((rel) => path.join(home, rel)));
const gstackRoot = roots.find((root) => {
try { return root !== '' && fs.statSync(path.join(root, 'bin')).isDirectory(); } catch { return false; }
});
if (gstackRoot) {
process.stdout.write('{}\n');
} else {
process.stderr.write(`BLOCKED: gstack is not installed globally.
gstack is required for AI-assisted work in this repo.
Install it:
git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup --team
Then restart your AI coding tool.
`);
// Exit 2 denies on Claude Code (#2413) and on Copilot CLI, which treats a
// preToolUse exit 2 as deny (#2229).
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny',
permissionDecisionReason: 'gstack is required but not installed. See stderr for install instructions.' } }) + '\n');
process.exitCode = 2;
}
HOOK_EOF
chmod +x "$HOOKS_DIR/check-gstack.cjs"
GENERATED+=(".claude/hooks/check-gstack.cjs")
echo " + .claude/hooks/check-gstack.cjs — enforcement hook"
# Add hook to project-level settings.json. The command exits 0 (allow) or
# 2 (deny) only when node runs check-gstack.cjs. Without node the shell exits
# 127, and a require error exits 1; Claude Code treats any other exit as a
# non-blocking error and lets the Skill call through, so the handler is
# registered with onFailure: block (Claude Code 2.1.295+) to fail closed
# (#3319). Older Claude Code ignores the field and keeps the old behavior.
HOOK_COMMAND="node -e \"require(require('path').resolve(process.env.CLAUDE_PROJECT_DIR || '.', '.claude', 'hooks', 'check-gstack.cjs'))\""
if ! command -v bun >/dev/null 2>&1; then
echo " ! bun not found — manually add this PreToolUse hook (matcher Skill|skill, \"onFailure\": \"block\") to .claude/settings.json:"
echo " $HOOK_COMMAND"
elif GSTACK_SETTINGS_PATH="$SETTINGS" GSTACK_HOOK_COMMAND="$HOOK_COMMAND" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
const fs = require('fs');
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
const handler = { type: 'command', command: process.env.GSTACK_HOOK_COMMAND, onFailure: 'block' };
let settings = {};
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
if (!settings.hooks) settings.hooks = {};
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
// Claude Code's skill tool is Skill; Copilot CLI's is skill, which no
// Claude tool name maps to, so a bare 'Skill' never fired there (#2229).
// A registration an older team-init wrote (the .sh hook, matcher Skill)
// is replaced in place: exactly one check-gstack handler remains, and
// unrelated hooks in the same entry are kept. Re-running upgrades a
// handler registered without onFailure.
const MATCHER = 'Skill|skill';
const isOurs = h => h && typeof h.command === 'string' && h.command.includes('check-gstack');
let placed = false;
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(entry => {
if (entry.matcher !== 'Skill' && entry.matcher !== MATCHER) return true;
if (!Array.isArray(entry.hooks) || !entry.hooks.some(isOurs)) return true;
entry.matcher = MATCHER;
const kept = [];
for (const h of entry.hooks) {
if (!isOurs(h)) kept.push(h);
else if (!placed) { kept.push({ ...handler }); placed = true; }
}
entry.hooks = kept;
return kept.length > 0;
});
if (!placed) settings.hooks.PreToolUse.push({ matcher: MATCHER, hooks: [{ ...handler }] });
const tmp = settingsPath + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
fs.renameSync(tmp, settingsPath);
"; then
GENERATED+=(".claude/settings.json")
echo " + .claude/settings.json — PreToolUse hook registered"
# The bash hook an older team-init wrote goes only once its replacement is
# registered; a tracked one is staged as a deletion by the git add below.
if [ -e "$HOOKS_DIR/check-gstack.sh" ]; then
rm -f "$HOOKS_DIR/check-gstack.sh"
git -C "$REPO_ROOT" ls-files --error-unmatch -- .claude/hooks/check-gstack.sh >/dev/null 2>&1 \
&& GENERATED+=(".claude/hooks/check-gstack.sh")
echo " - .claude/hooks/check-gstack.sh — replaced by check-gstack.cjs"
fi
else
echo " ! could not update .claude/settings.json — add this PreToolUse hook (matcher Skill|skill, \"onFailure\": \"block\") manually:"
echo " $HOOK_COMMAND"
fi
fi
# ── Summary ────────────────────────────────────────────────────
echo ""
echo "Team mode ($MODE) initialized."
echo ""
if [ ${#GENERATED[@]} -gt 0 ]; then
echo "Commit the generated files:"
echo " git add ${GENERATED[*]}"
echo " git commit -m \"chore: require gstack for AI-assisted work\""
fi
echo ""
echo "Each developer then runs:"
echo " git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack"
echo " cd ~/.claude/skills/gstack && ./setup --team"
if [ "$MODE" = "required" ]; then
echo ""
echo "The Skill gate runs on Node.js and blocks when it cannot run, so every"
echo "developer also needs node on PATH and Claude Code 2.1.295 or later."
echo "Older Claude Code ignores onFailure: without node there, skills are not blocked."
fi