Files
gstack/bin/gstack-team-init
T
Garry Tanandcapy-ai[bot] 9dc1dfb729 v1.91.227.0 fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415) (#3431)
* fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415)

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

* fix(windows): detect Git Bash/MSYS2/Cygwin from OSTYPE, as gstack-doctor does, so uname stubs in setup fixtures stay untouched

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

* fix: pre-landing review fixes (#3415)

- Choose NUL only when cygpath is also on PATH, so an OSTYPE=msys inherited
  by a POSIX bash keeps /dev/null instead of a cwd-relative NUL file.
- gstack-developer-profile --log-session reports a Bun failure instead of
  skipping it as invalid input, like gstack-timeline-log.
- learnings-injection tests check that their anchored bun -e block exists.
- CHANGELOG names the callers this fix does not cover; curation reason fixed.

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

* fix: pre-landing review fixes, cycle 2 (#3415)

- timeline-log and developer-profile validators print their verdict; any
  non-zero Bun exit is a reported failure (a Bun crash on Windows exits 3).
- timeline-log keeps the original event when ts stamping fails.
- The definition ratchet scopes a local GSTACK_BUN_CONFIG to its function.
- The shell-bins fixture clears its preload marker per test.
- CHANGELOG states the callers that discard the report and the Bun 1.3.x case.

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

* docs: CHANGELOG notes Bun 1.3.10 evidence and remaining literal /dev/null callers (#3415)

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-10-26 04:32:27 +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/.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', '.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