mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
9ec4ab7eb9
* fix: ad-hoc codesign compiled binaries on Apple Silicon after build On some Apple Silicon machines, Bun's --compile produces a corrupt or linker-only code signature. macOS kills these binaries with SIGKILL (exit 137, zsh: killed) before they execute a single instruction. Add a post-build codesign step to setup that runs only on Darwin arm64: 1. Remove the corrupt/linker-only signature (required — a direct re-sign fails with 'invalid or unsupported format for signature') 2. Apply a fresh ad-hoc signature The step is idempotent, costs <1s, and is what Bun's own docs recommend for distributed standalone executables. All four compiled binaries are covered: browse, find-browse, design, and gstack-global-discover. Failure is a non-fatal warning so Intel/CI builds are unaffected. Fixes #997 * fix: prevent codex exec stdin deadlock with </dev/null redirect codex CLI 0.120.0+ blocks indefinitely when stdin is a non-TTY pipe (Claude Code Bash tool, background bash, CI). The CLI sees a non-TTY stdin and waits for EOF to append it as a <stdin> block, even when the prompt is passed as a positional argument. Fix: add < /dev/null to every codex exec and codex review invocation in the source-of-truth files (scripts/resolvers/*.ts and *.md.tmpl). Generated SKILL.md files will be produced by bun run gen:skill-docs in a subsequent commit (Tension D: template+resolver only, generator is authoritative, not cherry-picked artifacts). Affected source files (16 total invocations): - scripts/resolvers/review.ts (4) - scripts/resolvers/design.ts (3) - codex/SKILL.md.tmpl (5) - autoplan/SKILL.md.tmpl (4) Fixes #971 Co-Authored-By: loning <loning@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: codex/autoplan hardening + Apple Silicon coreutils auto-install Hardens /codex and /autoplan against silent failures surfaced by the #972 stdin fix and #1003 Apple Silicon codesign. Six-layer defense: 1. **Multi-signal auth probe** (new Step 0.5 / Phase 0.5): env-based auth ($CODEX_API_KEY, $OPENAI_API_KEY) OR file-based auth (${CODEX_HOME:-~/.codex}/auth.json). Rejects false negatives that the old file-only check produced for CI / platform-engineer users. 2. **Timeout wrapper** around every codex exec / codex review invocation: gtimeout → timeout → unwrapped fallback chain. On exit 124, surfaces common causes + actionable next step. Guards against model-API stalls not covered by the #972 stdin fix. 3. **Stderr capture in Challenge mode** (codex/SKILL.md.tmpl:208): 2>/dev/null → 2>$TMPERR. Post-invocation grep for auth/login/unauthorized surfaces errors that were previously dropped silently. 4. **Completeness check** in the Python JSON parser: tracks turn.completed events and warns on zero (possible mid-stream disconnect). 5. **Version warning** for known-bad Codex CLI (0.120.0-0.120.2, the range that introduced the stdin deadlock #972 fixes). Anchored regex `(^|[^0-9.])0\.120\.(0|1|2)([^0-9.]|$)` prevents 0.120.10 / 0.120.20 false positives. 6. **Failure telemetry + operational learnings**: codex_timeout, codex_auth_failed, codex_cli_missing, codex_version_warning events land in ~/.gstack/analytics/skill-usage.jsonl behind the existing telemetry opt-in. On timeout (exit 124), auto-logs an operational learning via gstack-learnings-log so future /investigate sessions surface prior hang patterns automatically. **Shared helper** (bin/gstack-codex-probe): consolidates all four pieces (auth probe, version check, timeout wrapper, telemetry logger) into one bash file that /codex and /autoplan source. Namespace-prefixed (_gstack_codex_*) with a unit test that verifies sourcing does not leak shell options into the caller. pathRewrites in host configs rewrite ~/.claude/skills/gstack → $GSTACK_ROOT for Codex, $GSTACK_BIN for Factory/Cursor/etc. **Apple Silicon coreutils auto-install** (setup:264): macOS lacks GNU timeout by default; Homebrew's coreutils installs it as gtimeout to avoid shadowing BSD utilities. ./setup now auto-installs coreutils on Darwin (arch-agnostic — applies to Intel + Apple Silicon) when neither gtimeout nor timeout is present. Opt-out via GSTACK_SKIP_COREUTILS=1 for CI, managed machines, or offline envs. **25 deterministic unit tests** (test/codex-hardening.test.ts): - 8 auth probe combinations (env precedence, whitespace, alternate $CODEX_HOME, corrupt file paths) - 10 version regex cases including 0.120.10 false-positive guards and v-prefixed / multiline output - 4 timeout wrapper + namespace hygiene (bash -n, gtimeout preference, set-option leak check) - 3 telemetry payload schema checks (confirms env values + auth tokens never leak into emitted events) **1 periodic-tier E2E** (test/skill-e2e-autoplan-dual-voice.test.ts): gates the /autoplan dual-voice path — asserts both Claude subagent and Codex voices produce output in Phase 1, OR that [codex-unavailable] is logged when Codex is absent. ~\$1/run, not a CI gate. Golden baseline + gen-skill-docs exclusion list updated for the new codex path references and the 16 < /dev/null redirects from #972. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: plan-review right-sized diff counterbalance (not minimal-diff default) /plan-ceo-review and /plan-eng-review listed "minimal diff" as an engineering preference without counterbalancing language. Reviewers picked up on that and rejected rewrites that should have been approved. The preference is now framed as "right-sized diff" with explicit permission to recommend a rewrite when the existing foundation is broken. Implementation alternatives section in CEO review gets an equal-weight clarification: don't default to minimal viable just because it is smaller. Recommend whichever best serves the user's goal; if the right answer is a rewrite, say so. Three-line tone edit per template, no voice / ETHOS / YC / promotional content change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * release: v0.18.4.0 — codex + Apple Silicon hardening wave - Apple Silicon codesign fix (#1003 @voidborne-d) - Codex stdin deadlock fix (#972 @loning) - Codex timeout wrapper (gtimeout → timeout → unwrapped fallback) - Multi-signal auth gate for /codex + /autoplan - Codex version warning for known-bad CLI (0.120.0-0.120.2) - Challenge mode stderr capture + completeness check - Plan-review right-sized diff counterbalance - Failure telemetry + auto-log timeout as operational learning - 25 deterministic unit tests + dual-voice periodic E2E Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com> Co-authored-by: loning <loning@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1012 lines
39 KiB
Bash
Executable File
1012 lines
39 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack setup — build browser binary + register skills with Claude Code / Codex
|
|
set -e
|
|
umask 077 # Restrict new files to owner-only (0o600 files, 0o700 dirs)
|
|
|
|
if ! command -v bun >/dev/null 2>&1; then
|
|
echo "Error: bun is required but not installed." >&2
|
|
echo "Install with checksum verification:" >&2
|
|
echo ' BUN_VERSION="1.3.10"' >&2
|
|
echo ' tmpfile=$(mktemp)' >&2
|
|
echo ' curl -fsSL "https://bun.sh/install" -o "$tmpfile"' >&2
|
|
echo ' echo "Verify checksum before running: shasum -a 256 $tmpfile"' >&2
|
|
echo ' BUN_VERSION="$BUN_VERSION" bash "$tmpfile" && rm "$tmpfile"' >&2
|
|
exit 1
|
|
fi
|
|
|
|
INSTALL_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
|
|
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
|
|
CODEX_SKILLS="$HOME/.codex/skills"
|
|
CODEX_GSTACK="$CODEX_SKILLS/gstack"
|
|
FACTORY_SKILLS="$HOME/.factory/skills"
|
|
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
|
|
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
|
|
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
|
|
|
|
IS_WINDOWS=0
|
|
case "$(uname -s)" in
|
|
MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
|
|
esac
|
|
|
|
# ─── Quiet mode helper ────────────────────────────────────────
|
|
QUIET=0
|
|
log() { [ "$QUIET" -eq 0 ] && echo "$@" || true; }
|
|
|
|
# ─── Parse flags ──────────────────────────────────────────────
|
|
HOST="claude"
|
|
LOCAL_INSTALL=0
|
|
SKILL_PREFIX=1
|
|
SKILL_PREFIX_FLAG=0
|
|
TEAM_MODE=0
|
|
NO_TEAM_MODE=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
|
|
--host=*) HOST="${1#--host=}"; shift ;;
|
|
--local) LOCAL_INSTALL=1; shift ;;
|
|
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
|
|
--no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;;
|
|
--team) TEAM_MODE=1; shift ;;
|
|
--no-team) NO_TEAM_MODE=1; shift ;;
|
|
-q|--quiet) QUIET=1; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
case "$HOST" in
|
|
claude|codex|kiro|factory|opencode|auto) ;;
|
|
openclaw)
|
|
echo ""
|
|
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
|
|
echo "sessions natively via ACP. gstack provides methodology artifacts, not a"
|
|
echo "full skill installation."
|
|
echo ""
|
|
echo "To integrate gstack with OpenClaw:"
|
|
echo " 1. Tell your OpenClaw agent: 'install gstack for openclaw'"
|
|
echo " 2. Or generate artifacts: bun run gen:skill-docs --host openclaw"
|
|
echo " 3. See docs/OPENCLAW.md for the full architecture"
|
|
echo ""
|
|
exit 0 ;;
|
|
hermes)
|
|
echo ""
|
|
echo "Hermes integration uses the same model as OpenClaw — Hermes spawns"
|
|
echo "Claude Code sessions, and gstack provides methodology artifacts."
|
|
echo ""
|
|
echo "To integrate gstack with Hermes:"
|
|
echo " 1. Tell your Hermes agent: 'install gstack for hermes'"
|
|
echo " 2. Or generate artifacts: bun run gen:skill-docs --host hermes"
|
|
echo ""
|
|
exit 0 ;;
|
|
gbrain)
|
|
echo ""
|
|
echo "GBrain is a mod for gstack — it makes coding skills brain-aware."
|
|
echo "GBrain generates brain-enhanced skill variants that search your brain"
|
|
echo "for context before starting and save results after finishing."
|
|
echo ""
|
|
echo "To generate brain-aware skills:"
|
|
echo " bun run gen:skill-docs --host gbrain"
|
|
echo ""
|
|
echo "GBrain setup and brain skills ship from the GBrain repo."
|
|
echo ""
|
|
exit 0 ;;
|
|
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
|
|
esac
|
|
|
|
# ─── Resolve skill prefix preference ─────────────────────────
|
|
# Priority: CLI flag > saved config > interactive prompt (or flat default for non-TTY)
|
|
GSTACK_CONFIG="$SOURCE_GSTACK_DIR/bin/gstack-config"
|
|
export GSTACK_SETUP_RUNNING=1 # Prevent gstack-config post-set hook from triggering relink mid-setup
|
|
if [ "$SKILL_PREFIX_FLAG" -eq 0 ]; then
|
|
_saved_prefix="$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || true)"
|
|
if [ "$_saved_prefix" = "true" ]; then
|
|
SKILL_PREFIX=1
|
|
elif [ "$_saved_prefix" = "false" ]; then
|
|
SKILL_PREFIX=0
|
|
else
|
|
# No saved preference — prompt interactively (or default flat for non-TTY/quiet)
|
|
if [ "$QUIET" -eq 1 ]; then
|
|
SKILL_PREFIX=0
|
|
elif [ -t 0 ]; then
|
|
echo ""
|
|
echo "Skill naming: how should gstack skills appear?"
|
|
echo ""
|
|
echo " 1) Short names: /qa, /ship, /review"
|
|
echo " Recommended. Clean and fast to type."
|
|
echo ""
|
|
echo " 2) Namespaced: /gstack-qa, /gstack-ship, /gstack-review"
|
|
echo " Use this if you run other skill packs alongside gstack to avoid conflicts."
|
|
echo ""
|
|
printf "Choice [1/2] (default: 1, auto-selects in 10s): "
|
|
read -t 10 -r _prefix_choice </dev/tty 2>/dev/null || _prefix_choice=""
|
|
case "$_prefix_choice" in
|
|
2) SKILL_PREFIX=1 ;;
|
|
*) SKILL_PREFIX=0 ;;
|
|
esac
|
|
else
|
|
SKILL_PREFIX=0
|
|
fi
|
|
# Save the choice for future runs
|
|
"$GSTACK_CONFIG" set skill_prefix "$([ "$SKILL_PREFIX" -eq 1 ] && echo true || echo false)" 2>/dev/null || true
|
|
fi
|
|
else
|
|
# Flag was passed explicitly — persist the choice
|
|
"$GSTACK_CONFIG" set skill_prefix "$([ "$SKILL_PREFIX" -eq 1 ] && echo true || echo false)" 2>/dev/null || true
|
|
fi
|
|
|
|
# --local: install to .claude/skills/ in the current working directory (deprecated)
|
|
if [ "$LOCAL_INSTALL" -eq 1 ]; then
|
|
echo "Warning: --local is deprecated. Use global install + --team instead." >&2
|
|
echo " See: https://github.com/garrytan/gstack#team-mode" >&2
|
|
if [ "$HOST" = "codex" ]; then
|
|
echo "Error: --local is only supported for Claude Code (not Codex)." >&2
|
|
exit 1
|
|
fi
|
|
INSTALL_SKILLS_DIR="$(pwd)/.claude/skills"
|
|
mkdir -p "$INSTALL_SKILLS_DIR"
|
|
HOST="claude"
|
|
INSTALL_CODEX=0
|
|
fi
|
|
|
|
# For auto: detect which agents are installed
|
|
INSTALL_CLAUDE=0
|
|
INSTALL_CODEX=0
|
|
INSTALL_KIRO=0
|
|
INSTALL_FACTORY=0
|
|
INSTALL_OPENCODE=0
|
|
if [ "$HOST" = "auto" ]; then
|
|
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
|
|
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
|
|
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
|
|
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
|
|
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
|
|
# If none found, default to claude
|
|
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
|
|
INSTALL_CLAUDE=1
|
|
fi
|
|
elif [ "$HOST" = "claude" ]; then
|
|
INSTALL_CLAUDE=1
|
|
elif [ "$HOST" = "codex" ]; then
|
|
INSTALL_CODEX=1
|
|
elif [ "$HOST" = "kiro" ]; then
|
|
INSTALL_KIRO=1
|
|
elif [ "$HOST" = "factory" ]; then
|
|
INSTALL_FACTORY=1
|
|
elif [ "$HOST" = "opencode" ]; then
|
|
INSTALL_OPENCODE=1
|
|
fi
|
|
|
|
migrate_direct_codex_install() {
|
|
local gstack_dir="$1"
|
|
local codex_gstack="$2"
|
|
local migrated_dir="$HOME/.gstack/repos/gstack"
|
|
|
|
[ "$gstack_dir" = "$codex_gstack" ] || return 0
|
|
[ -L "$gstack_dir" ] && return 0
|
|
|
|
mkdir -p "$(dirname "$migrated_dir")"
|
|
if [ -e "$migrated_dir" ] && [ "$migrated_dir" != "$gstack_dir" ]; then
|
|
echo "gstack setup failed: direct Codex install detected at $gstack_dir" >&2
|
|
echo "A migrated repo already exists at $migrated_dir; move one of them aside and rerun setup." >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "Migrating direct Codex install to $migrated_dir to avoid duplicate skill discovery..."
|
|
mv "$gstack_dir" "$migrated_dir"
|
|
SOURCE_GSTACK_DIR="$migrated_dir"
|
|
INSTALL_GSTACK_DIR="$migrated_dir"
|
|
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
|
|
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
|
|
}
|
|
|
|
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
migrate_direct_codex_install "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
|
|
fi
|
|
|
|
ensure_playwright_browser() {
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
# On Windows, Bun can't launch Chromium due to broken pipe handling
|
|
# (oven-sh/bun#4253). Use Node.js to verify Chromium works instead.
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
|
|
)
|
|
else
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();'
|
|
) >/dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
# 1. Build browse binary if needed (smart rebuild: stale sources, package.json, lock)
|
|
NEEDS_BUILD=0
|
|
if [ ! -x "$BROWSE_BIN" ]; then
|
|
NEEDS_BUILD=1
|
|
elif [ -n "$(find "$SOURCE_GSTACK_DIR/browse/src" -type f -newer "$BROWSE_BIN" -print -quit 2>/dev/null)" ]; then
|
|
NEEDS_BUILD=1
|
|
elif [ "$SOURCE_GSTACK_DIR/package.json" -nt "$BROWSE_BIN" ]; then
|
|
NEEDS_BUILD=1
|
|
elif [ -f "$SOURCE_GSTACK_DIR/bun.lock" ] && [ "$SOURCE_GSTACK_DIR/bun.lock" -nt "$BROWSE_BIN" ]; then
|
|
NEEDS_BUILD=1
|
|
fi
|
|
|
|
if [ "$NEEDS_BUILD" -eq 1 ]; then
|
|
log "Building browse binary..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun install --frozen-lockfile 2>/dev/null || bun install
|
|
bun run build
|
|
)
|
|
# Safety net: write .version if build script didn't (e.g., git not available during build)
|
|
if [ ! -f "$SOURCE_GSTACK_DIR/browse/dist/.version" ]; then
|
|
git -C "$SOURCE_GSTACK_DIR" rev-parse HEAD > "$SOURCE_GSTACK_DIR/browse/dist/.version" 2>/dev/null || true
|
|
fi
|
|
|
|
# macOS Apple Silicon: ad-hoc codesign compiled binaries.
|
|
# Bun's --compile can produce a corrupt or linker-only code signature that
|
|
# macOS kills with SIGKILL (exit 137). The two-step remove+re-sign is
|
|
# required because a naive `codesign -s - -f` fails when the existing
|
|
# signature block is corrupt. This is idempotent and costs <1s.
|
|
# See: https://github.com/garrytan/gstack/issues/997
|
|
if [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "arm64" ]; then
|
|
for _bin in browse/dist/browse browse/dist/find-browse design/dist/design bin/gstack-global-discover; do
|
|
_bin_path="$SOURCE_GSTACK_DIR/$_bin"
|
|
[ -f "$_bin_path" ] && [ -x "$_bin_path" ] || continue
|
|
codesign --remove-signature "$_bin_path" 2>/dev/null || true
|
|
if ! codesign -s - -f "$_bin_path" 2>/dev/null; then
|
|
log "warning: codesign failed for $_bin (binary may not run on Apple Silicon)"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# macOS: install coreutils for `gtimeout` (Codex hang protection in /codex + /autoplan).
|
|
# macOS ships BSD `timeout`-less; Homebrew's coreutils installs GNU timeout as
|
|
# `gtimeout` to avoid shadowing BSD utilities. The /codex and /autoplan skills
|
|
# fall back to unwrapped codex invocations when neither is available — this
|
|
# auto-install upgrades them to hang-protected where possible.
|
|
# Skip entirely with GSTACK_SKIP_COREUTILS=1 (CI, managed machines, offline envs).
|
|
if [ "$(uname -s)" = "Darwin" ] && [ "${GSTACK_SKIP_COREUTILS:-0}" != "1" ]; then
|
|
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
|
if command -v brew >/dev/null 2>&1; then
|
|
log "Installing coreutils for Codex hang protection (set GSTACK_SKIP_COREUTILS=1 to skip)..."
|
|
brew install coreutils >/dev/null 2>&1 || log "warning: brew install coreutils failed; /codex will run without hang protection"
|
|
else
|
|
log "warning: Homebrew not found. /codex will run without hang protection. Install coreutils manually or set GSTACK_SKIP_COREUTILS=1."
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ ! -x "$BROWSE_BIN" ]; then
|
|
echo "gstack setup failed: browse binary missing at $BROWSE_BIN" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 1b. Generate .agents/ Codex skill docs — always regenerate to prevent stale descriptions.
|
|
# .agents/ is no longer committed — generated at setup time from .tmpl templates.
|
|
# bun run build already does this, but we need it when NEEDS_BUILD=0 (binary is fresh).
|
|
# Always regenerate: generation is fast (<2s) and mtime-based staleness checks are fragile
|
|
# (miss stale files when timestamps match after clone/checkout/upgrade).
|
|
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
|
|
NEEDS_AGENTS_GEN=1
|
|
|
|
if [ "$NEEDS_AGENTS_GEN" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
|
log "Generating .agents/ skill docs..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun install --frozen-lockfile 2>/dev/null || bun install
|
|
bun run gen:skill-docs --host codex
|
|
)
|
|
fi
|
|
|
|
# 1c. Generate .factory/ Factory Droid skill docs
|
|
if [ "$INSTALL_FACTORY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
|
log "Generating .factory/ skill docs..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun install --frozen-lockfile 2>/dev/null || bun install
|
|
bun run gen:skill-docs --host factory
|
|
)
|
|
fi
|
|
|
|
# 1d. Generate .opencode/ OpenCode skill docs
|
|
if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
|
log "Generating .opencode/ skill docs..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bun install --frozen-lockfile 2>/dev/null || bun install
|
|
bun run gen:skill-docs --host opencode
|
|
)
|
|
fi
|
|
|
|
# 2. Ensure Playwright's Chromium is available
|
|
if ! ensure_playwright_browser; then
|
|
echo "Installing Playwright Chromium..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
bunx playwright install chromium
|
|
)
|
|
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
# On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
|
|
# Ensure playwright is importable by Node from the gstack directory.
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
echo "gstack setup failed: Node.js is required on Windows (Bun cannot launch Chromium due to a pipe bug)" >&2
|
|
echo " Install Node.js: https://nodejs.org/" >&2
|
|
exit 1
|
|
fi
|
|
echo "Windows detected — verifying Node.js can load Playwright..."
|
|
(
|
|
cd "$SOURCE_GSTACK_DIR"
|
|
# Bun's node_modules already has playwright; verify Node can require it
|
|
node -e "require('playwright')" 2>/dev/null || npm install --no-save playwright
|
|
# @ngrok/ngrok is externalized in server-node.mjs and resolved at runtime.
|
|
# Verify the platform-specific native binary is installed so /pair-agent
|
|
# tunnels don't fail later with a cryptic module-not-found error.
|
|
node -e "require('@ngrok/ngrok')" 2>/dev/null || npm install --no-save @ngrok/ngrok
|
|
)
|
|
fi
|
|
fi
|
|
|
|
if ! ensure_playwright_browser; then
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
echo "gstack setup failed: Playwright Chromium could not be launched via Node.js" >&2
|
|
echo " This is a known issue with Bun on Windows (oven-sh/bun#4253)." >&2
|
|
echo " Ensure Node.js is installed and 'node -e \"require('playwright')\"' works." >&2
|
|
else
|
|
echo "gstack setup failed: Playwright Chromium could not be launched" >&2
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
# 3. Ensure ~/.gstack global state directory exists
|
|
mkdir -p "$HOME/.gstack/projects"
|
|
|
|
# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
|
|
# 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
|
|
# gstack/ (which would auto-prefix them as gstack-*).
|
|
# When SKILL_PREFIX=1, directories are prefixed with "gstack-".
|
|
# Use --no-prefix to restore flat names.
|
|
link_claude_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local linked=()
|
|
for skill_dir in "$gstack_dir"/*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
dir_name="$(basename "$skill_dir")"
|
|
# Skip node_modules
|
|
[ "$dir_name" = "node_modules" ] && continue
|
|
# Use frontmatter name: if present (e.g., run-tests/ with name: test → symlink as "test")
|
|
skill_name=$(grep -m1 '^name:' "$skill_dir/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]')
|
|
[ -z "$skill_name" ] && skill_name="$dir_name"
|
|
# Apply gstack- prefix unless --no-prefix or already prefixed
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
case "$skill_name" in
|
|
gstack-*) link_name="$skill_name" ;;
|
|
*) link_name="gstack-$skill_name" ;;
|
|
esac
|
|
else
|
|
link_name="$skill_name"
|
|
fi
|
|
target="$skills_dir/$link_name"
|
|
# Upgrade old directory symlinks to real directories
|
|
if [ -L "$target" ]; then
|
|
rm -f "$target"
|
|
fi
|
|
# Create real directory with symlinked SKILL.md (absolute path)
|
|
# Use mkdir -p unconditionally (idempotent) to avoid TOCTOU race
|
|
mkdir -p "$target"
|
|
# Validate target isn't a symlink before creating the link
|
|
if [ -L "$target/SKILL.md" ]; then rm "$target/SKILL.md"; fi
|
|
ln -snf "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"
|
|
linked+=("$link_name")
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
fi
|
|
}
|
|
|
|
# ─── 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.
|
|
cleanup_old_claude_symlinks() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local removed=()
|
|
for skill_dir in "$gstack_dir"/*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "node_modules" ] && continue
|
|
# Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
|
|
case "$skill_name" in gstack-*) continue ;; esac
|
|
old_target="$skills_dir/$skill_name"
|
|
# Remove directory symlinks pointing into gstack/
|
|
if [ -L "$old_target" ]; then
|
|
link_dest="$(readlink "$old_target" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
gstack/*|*/gstack/*)
|
|
rm -f "$old_target"
|
|
removed+=("$skill_name")
|
|
;;
|
|
esac
|
|
# Remove real directories with symlinked SKILL.md pointing into gstack/
|
|
elif [ -d "$old_target" ] && [ -L "$old_target/SKILL.md" ]; then
|
|
link_dest="$(readlink "$old_target/SKILL.md" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
*gstack*)
|
|
rm -rf "$old_target"
|
|
removed+=("$skill_name")
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#removed[@]} -gt 0 ]; then
|
|
echo " cleaned up old entries: ${removed[*]}"
|
|
fi
|
|
}
|
|
|
|
# ─── Helper: remove old prefixed Claude skill entries ─────────────────────────
|
|
# Reverse migration: when switching from gstack- prefixed names to flat names,
|
|
# clean up stale gstack-* symlinks or directories that point into the gstack directory.
|
|
cleanup_prefixed_claude_symlinks() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local removed=()
|
|
for skill_dir in "$gstack_dir"/*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "node_modules" ] && continue
|
|
# Only clean up prefixed entries for dirs that AREN'T already prefixed
|
|
# (e.g., remove gstack-qa but NOT gstack-upgrade which is the real dir name)
|
|
case "$skill_name" in gstack-*) continue ;; esac
|
|
prefixed_target="$skills_dir/gstack-$skill_name"
|
|
# Remove directory symlinks pointing into gstack/
|
|
if [ -L "$prefixed_target" ]; then
|
|
link_dest="$(readlink "$prefixed_target" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
gstack/*|*/gstack/*)
|
|
rm -f "$prefixed_target"
|
|
removed+=("gstack-$skill_name")
|
|
;;
|
|
esac
|
|
# Remove real directories with symlinked SKILL.md pointing into gstack/
|
|
elif [ -d "$prefixed_target" ] && [ -L "$prefixed_target/SKILL.md" ]; then
|
|
link_dest="$(readlink "$prefixed_target/SKILL.md" 2>/dev/null || true)"
|
|
case "$link_dest" in
|
|
*gstack*)
|
|
rm -rf "$prefixed_target"
|
|
removed+=("gstack-$skill_name")
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#removed[@]} -gt 0 ]; then
|
|
echo " cleaned up prefixed entries: ${removed[*]}"
|
|
fi
|
|
}
|
|
|
|
# ─── Helper: link generated Codex skills into a skills parent directory ──
|
|
# Installs from .agents/skills/gstack-* (the generated Codex-format skills)
|
|
# instead of source dirs (which have Claude paths).
|
|
link_codex_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local agents_dir="$gstack_dir/.agents/skills"
|
|
local linked=()
|
|
|
|
if [ ! -d "$agents_dir" ]; then
|
|
echo " Generating .agents/ skill docs..."
|
|
( cd "$gstack_dir" && bun run gen:skill-docs --host codex )
|
|
fi
|
|
|
|
if [ ! -d "$agents_dir" ]; then
|
|
echo " warning: .agents/skills/ generation failed — run 'bun run gen:skill-docs --host codex' manually" >&2
|
|
return 1
|
|
fi
|
|
|
|
for skill_dir in "$agents_dir"/gstack*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
# Skip the sidecar directory — it contains runtime asset symlinks (bin/,
|
|
# browse/), not a skill. Linking it would overwrite the root gstack
|
|
# symlink that Step 5 already pointed at the repo root.
|
|
[ "$skill_name" = "gstack" ] && continue
|
|
target="$skills_dir/$skill_name"
|
|
# Create or update symlink
|
|
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
|
ln -snf "$skill_dir" "$target"
|
|
linked+=("$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
fi
|
|
}
|
|
|
|
# ─── Helper: create .agents/skills/gstack/ sidecar symlinks ──────────
|
|
# Codex/Gemini/Cursor read skills from .agents/skills/. We link runtime
|
|
# assets (bin/, browse/dist/, review/, qa/, etc.) so skill templates can
|
|
# resolve paths like $SKILL_ROOT/review/design-checklist.md.
|
|
create_agents_sidecar() {
|
|
local repo_root="$1"
|
|
local agents_gstack="$repo_root/.agents/skills/gstack"
|
|
mkdir -p "$agents_gstack"
|
|
|
|
# Sidecar directories that skills reference at runtime
|
|
for asset in bin browse review qa; do
|
|
local src="$SOURCE_GSTACK_DIR/$asset"
|
|
local dst="$agents_gstack/$asset"
|
|
if [ -d "$src" ] || [ -f "$src" ]; then
|
|
if [ -L "$dst" ] || [ ! -e "$dst" ]; then
|
|
ln -snf "$src" "$dst"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Sidecar files that skills reference at runtime
|
|
for file in ETHOS.md; do
|
|
local src="$SOURCE_GSTACK_DIR/$file"
|
|
local dst="$agents_gstack/$file"
|
|
if [ -f "$src" ]; then
|
|
if [ -L "$dst" ] || [ ! -e "$dst" ]; then
|
|
ln -snf "$src" "$dst"
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ─── Helper: create a minimal ~/.codex/skills/gstack runtime root ───────────
|
|
# Codex scans ~/.codex/skills recursively. Exposing the whole repo here causes
|
|
# duplicate skills because source SKILL.md files and generated Codex skills are
|
|
# both discoverable. Keep this directory limited to runtime assets + root skill.
|
|
create_codex_runtime_root() {
|
|
local gstack_dir="$1"
|
|
local codex_gstack="$2"
|
|
local agents_dir="$gstack_dir/.agents/skills"
|
|
|
|
if [ -L "$codex_gstack" ]; then
|
|
rm -f "$codex_gstack"
|
|
elif [ -d "$codex_gstack" ] && [ "$codex_gstack" != "$gstack_dir" ]; then
|
|
# Old direct installs left a real directory here with stale source skills.
|
|
# Remove it so we start fresh with only the minimal runtime assets.
|
|
rm -rf "$codex_gstack"
|
|
fi
|
|
|
|
mkdir -p "$codex_gstack" "$codex_gstack/browse" "$codex_gstack/gstack-upgrade" "$codex_gstack/review"
|
|
|
|
if [ -f "$agents_dir/gstack/SKILL.md" ]; then
|
|
ln -snf "$agents_dir/gstack/SKILL.md" "$codex_gstack/SKILL.md"
|
|
fi
|
|
if [ -d "$gstack_dir/bin" ]; then
|
|
ln -snf "$gstack_dir/bin" "$codex_gstack/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/dist" ]; then
|
|
ln -snf "$gstack_dir/browse/dist" "$codex_gstack/browse/dist"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/bin" ]; then
|
|
ln -snf "$gstack_dir/browse/bin" "$codex_gstack/browse/bin"
|
|
fi
|
|
if [ -f "$agents_dir/gstack-upgrade/SKILL.md" ]; then
|
|
ln -snf "$agents_dir/gstack-upgrade/SKILL.md" "$codex_gstack/gstack-upgrade/SKILL.md"
|
|
fi
|
|
# Review runtime assets (individual files, NOT the whole review/ dir which has SKILL.md)
|
|
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
|
if [ -f "$gstack_dir/review/$f" ]; then
|
|
ln -snf "$gstack_dir/review/$f" "$codex_gstack/review/$f"
|
|
fi
|
|
done
|
|
# ETHOS.md — referenced by "Search Before Building" in all skill preambles
|
|
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
|
ln -snf "$gstack_dir/ETHOS.md" "$codex_gstack/ETHOS.md"
|
|
fi
|
|
}
|
|
|
|
create_factory_runtime_root() {
|
|
local gstack_dir="$1"
|
|
local factory_gstack="$2"
|
|
local factory_dir="$gstack_dir/.factory/skills"
|
|
|
|
if [ -L "$factory_gstack" ]; then
|
|
rm -f "$factory_gstack"
|
|
elif [ -d "$factory_gstack" ] && [ "$factory_gstack" != "$gstack_dir" ]; then
|
|
rm -rf "$factory_gstack"
|
|
fi
|
|
|
|
mkdir -p "$factory_gstack" "$factory_gstack/browse" "$factory_gstack/gstack-upgrade" "$factory_gstack/review"
|
|
|
|
if [ -f "$factory_dir/gstack/SKILL.md" ]; then
|
|
ln -snf "$factory_dir/gstack/SKILL.md" "$factory_gstack/SKILL.md"
|
|
fi
|
|
if [ -d "$gstack_dir/bin" ]; then
|
|
ln -snf "$gstack_dir/bin" "$factory_gstack/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/dist" ]; then
|
|
ln -snf "$gstack_dir/browse/dist" "$factory_gstack/browse/dist"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/bin" ]; then
|
|
ln -snf "$gstack_dir/browse/bin" "$factory_gstack/browse/bin"
|
|
fi
|
|
if [ -f "$factory_dir/gstack-upgrade/SKILL.md" ]; then
|
|
ln -snf "$factory_dir/gstack-upgrade/SKILL.md" "$factory_gstack/gstack-upgrade/SKILL.md"
|
|
fi
|
|
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
|
if [ -f "$gstack_dir/review/$f" ]; then
|
|
ln -snf "$gstack_dir/review/$f" "$factory_gstack/review/$f"
|
|
fi
|
|
done
|
|
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
|
ln -snf "$gstack_dir/ETHOS.md" "$factory_gstack/ETHOS.md"
|
|
fi
|
|
}
|
|
|
|
create_opencode_runtime_root() {
|
|
local gstack_dir="$1"
|
|
local opencode_gstack="$2"
|
|
local opencode_dir="$gstack_dir/.opencode/skills"
|
|
|
|
if [ -L "$opencode_gstack" ]; then
|
|
rm -f "$opencode_gstack"
|
|
elif [ -d "$opencode_gstack" ] && [ "$opencode_gstack" != "$gstack_dir" ]; then
|
|
rm -rf "$opencode_gstack"
|
|
fi
|
|
|
|
mkdir -p "$opencode_gstack" "$opencode_gstack/browse" "$opencode_gstack/design" "$opencode_gstack/gstack-upgrade" "$opencode_gstack/review" "$opencode_gstack/qa" "$opencode_gstack/plan-devex-review"
|
|
|
|
if [ -f "$opencode_dir/gstack/SKILL.md" ]; then
|
|
ln -snf "$opencode_dir/gstack/SKILL.md" "$opencode_gstack/SKILL.md"
|
|
fi
|
|
if [ -d "$gstack_dir/bin" ]; then
|
|
ln -snf "$gstack_dir/bin" "$opencode_gstack/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/dist" ]; then
|
|
ln -snf "$gstack_dir/browse/dist" "$opencode_gstack/browse/dist"
|
|
fi
|
|
if [ -d "$gstack_dir/browse/bin" ]; then
|
|
ln -snf "$gstack_dir/browse/bin" "$opencode_gstack/browse/bin"
|
|
fi
|
|
if [ -d "$gstack_dir/design/dist" ]; then
|
|
ln -snf "$gstack_dir/design/dist" "$opencode_gstack/design/dist"
|
|
fi
|
|
if [ -f "$opencode_dir/gstack-upgrade/SKILL.md" ]; then
|
|
ln -snf "$opencode_dir/gstack-upgrade/SKILL.md" "$opencode_gstack/gstack-upgrade/SKILL.md"
|
|
fi
|
|
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
|
if [ -f "$gstack_dir/review/$f" ]; then
|
|
ln -snf "$gstack_dir/review/$f" "$opencode_gstack/review/$f"
|
|
fi
|
|
done
|
|
if [ -d "$gstack_dir/review/specialists" ]; then
|
|
ln -snf "$gstack_dir/review/specialists" "$opencode_gstack/review/specialists"
|
|
fi
|
|
if [ -d "$gstack_dir/qa/templates" ]; then
|
|
ln -snf "$gstack_dir/qa/templates" "$opencode_gstack/qa/templates"
|
|
fi
|
|
if [ -d "$gstack_dir/qa/references" ]; then
|
|
ln -snf "$gstack_dir/qa/references" "$opencode_gstack/qa/references"
|
|
fi
|
|
if [ -f "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" ]; then
|
|
ln -snf "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" "$opencode_gstack/plan-devex-review/dx-hall-of-fame.md"
|
|
fi
|
|
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
|
ln -snf "$gstack_dir/ETHOS.md" "$opencode_gstack/ETHOS.md"
|
|
fi
|
|
}
|
|
|
|
link_factory_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local factory_dir="$gstack_dir/.factory/skills"
|
|
local linked=()
|
|
|
|
if [ ! -d "$factory_dir" ]; then
|
|
echo " Generating .factory/ skill docs..."
|
|
( cd "$gstack_dir" && bun run gen:skill-docs --host factory )
|
|
fi
|
|
|
|
if [ ! -d "$factory_dir" ]; then
|
|
echo " warning: .factory/skills/ generation failed — run 'bun run gen:skill-docs --host factory' manually" >&2
|
|
return 1
|
|
fi
|
|
|
|
for skill_dir in "$factory_dir"/gstack*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "gstack" ] && continue
|
|
target="$skills_dir/$skill_name"
|
|
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
|
ln -snf "$skill_dir" "$target"
|
|
linked+=("$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
fi
|
|
}
|
|
|
|
link_opencode_skill_dirs() {
|
|
local gstack_dir="$1"
|
|
local skills_dir="$2"
|
|
local opencode_dir="$gstack_dir/.opencode/skills"
|
|
local linked=()
|
|
|
|
if [ ! -d "$opencode_dir" ]; then
|
|
echo " Generating .opencode/ skill docs..."
|
|
( cd "$gstack_dir" && bun run gen:skill-docs --host opencode )
|
|
fi
|
|
|
|
if [ ! -d "$opencode_dir" ]; then
|
|
echo " warning: .opencode/skills/ generation failed — run 'bun run gen:skill-docs --host opencode' manually" >&2
|
|
return 1
|
|
fi
|
|
|
|
for skill_dir in "$opencode_dir"/gstack*/; do
|
|
if [ -f "$skill_dir/SKILL.md" ]; then
|
|
skill_name="$(basename "$skill_dir")"
|
|
[ "$skill_name" = "gstack" ] && continue
|
|
target="$skills_dir/$skill_name"
|
|
if [ -L "$target" ] || [ ! -e "$target" ]; then
|
|
ln -snf "$skill_dir" "$target"
|
|
linked+=("$skill_name")
|
|
fi
|
|
fi
|
|
done
|
|
if [ ${#linked[@]} -gt 0 ]; then
|
|
echo " linked skills: ${linked[*]}"
|
|
fi
|
|
}
|
|
|
|
# 4. Install for Claude (default)
|
|
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
|
|
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
|
|
CODEX_REPO_LOCAL=0
|
|
if [ "$SKILLS_BASENAME" = "skills" ] && [ "$SKILLS_PARENT_BASENAME" = ".agents" ]; then
|
|
CODEX_REPO_LOCAL=1
|
|
fi
|
|
|
|
if [ "$INSTALL_CLAUDE" -eq 1 ]; then
|
|
if [ "$SKILLS_BASENAME" = "skills" ]; then
|
|
# Clean up stale symlinks from the opposite prefix mode
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
cleanup_old_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
else
|
|
cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
fi
|
|
# Patch name: fields BEFORE creating symlinks so link_claude_skill_dirs
|
|
# 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"
|
|
# 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.
|
|
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
|
|
fi
|
|
# Backwards-compat alias: /connect-chrome → /open-gstack-browser
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome"
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome"
|
|
fi
|
|
if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then
|
|
ln -snf "gstack/open-gstack-browser" "$_OGB_LINK"
|
|
fi
|
|
if [ "$LOCAL_INSTALL" -eq 1 ]; then
|
|
log "gstack ready (project-local)."
|
|
log " skills: $INSTALL_SKILLS_DIR"
|
|
else
|
|
log "gstack ready (claude)."
|
|
fi
|
|
log " browse: $BROWSE_BIN"
|
|
else
|
|
# Not inside a skills/ directory — symlink into ~/.claude/skills/ and retry
|
|
CLAUDE_SKILLS_DIR="$HOME/.claude/skills"
|
|
CLAUDE_GSTACK_LINK="$CLAUDE_SKILLS_DIR/gstack"
|
|
mkdir -p "$CLAUDE_SKILLS_DIR"
|
|
ln -snf "$SOURCE_GSTACK_DIR" "$CLAUDE_GSTACK_LINK"
|
|
log " symlinked $CLAUDE_GSTACK_LINK -> $SOURCE_GSTACK_DIR"
|
|
INSTALL_SKILLS_DIR="$CLAUDE_SKILLS_DIR"
|
|
INSTALL_GSTACK_DIR="$CLAUDE_GSTACK_LINK"
|
|
# Clean up stale symlinks from the opposite prefix mode
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
cleanup_old_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
else
|
|
cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
|
|
fi
|
|
"$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX"
|
|
link_claude_skill_dirs "$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
|
|
fi
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/connect-chrome"
|
|
if [ "$SKILL_PREFIX" -eq 1 ]; then
|
|
_OGB_LINK="$INSTALL_SKILLS_DIR/gstack-connect-chrome"
|
|
fi
|
|
if [ -L "$_OGB_LINK" ] || [ ! -e "$_OGB_LINK" ]; then
|
|
ln -snf "gstack/open-gstack-browser" "$_OGB_LINK"
|
|
fi
|
|
log "gstack ready (claude)."
|
|
log " browse: $BROWSE_BIN"
|
|
fi
|
|
fi
|
|
|
|
# 5. Install for Codex
|
|
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
if [ "$CODEX_REPO_LOCAL" -eq 1 ]; then
|
|
CODEX_SKILLS="$INSTALL_SKILLS_DIR"
|
|
CODEX_GSTACK="$INSTALL_GSTACK_DIR"
|
|
fi
|
|
mkdir -p "$CODEX_SKILLS"
|
|
|
|
# Skip runtime root creation for repo-local installs — the checkout IS the runtime root.
|
|
# create_codex_runtime_root would create self-referential symlinks (bin → bin, etc.).
|
|
if [ "$CODEX_REPO_LOCAL" -eq 0 ]; then
|
|
create_codex_runtime_root "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
|
|
fi
|
|
# Install generated Codex-format skills (not Claude source dirs)
|
|
link_codex_skill_dirs "$SOURCE_GSTACK_DIR" "$CODEX_SKILLS"
|
|
|
|
log "gstack ready (codex)."
|
|
log " browse: $BROWSE_BIN"
|
|
log " codex skills: $CODEX_SKILLS"
|
|
fi
|
|
|
|
# 6. Install for Kiro CLI (copy from .agents/skills, rewrite paths)
|
|
if [ "$INSTALL_KIRO" -eq 1 ]; then
|
|
KIRO_SKILLS="$HOME/.kiro/skills"
|
|
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
|
|
mkdir -p "$KIRO_SKILLS"
|
|
|
|
# Create gstack dir with symlinks for runtime assets, copy+sed for SKILL.md
|
|
KIRO_GSTACK="$KIRO_SKILLS/gstack"
|
|
# Remove old whole-dir symlink from previous installs
|
|
[ -L "$KIRO_GSTACK" ] && rm -f "$KIRO_GSTACK"
|
|
mkdir -p "$KIRO_GSTACK" "$KIRO_GSTACK/browse" "$KIRO_GSTACK/gstack-upgrade" "$KIRO_GSTACK/review"
|
|
ln -snf "$SOURCE_GSTACK_DIR/bin" "$KIRO_GSTACK/bin"
|
|
ln -snf "$SOURCE_GSTACK_DIR/browse/dist" "$KIRO_GSTACK/browse/dist"
|
|
ln -snf "$SOURCE_GSTACK_DIR/browse/bin" "$KIRO_GSTACK/browse/bin"
|
|
# ETHOS.md — referenced by "Search Before Building" in all skill preambles
|
|
if [ -f "$SOURCE_GSTACK_DIR/ETHOS.md" ]; then
|
|
ln -snf "$SOURCE_GSTACK_DIR/ETHOS.md" "$KIRO_GSTACK/ETHOS.md"
|
|
fi
|
|
# gstack-upgrade skill
|
|
if [ -f "$AGENTS_DIR/gstack-upgrade/SKILL.md" ]; then
|
|
ln -snf "$AGENTS_DIR/gstack-upgrade/SKILL.md" "$KIRO_GSTACK/gstack-upgrade/SKILL.md"
|
|
fi
|
|
# Review runtime assets (individual files, not whole dir)
|
|
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
|
|
if [ -f "$SOURCE_GSTACK_DIR/review/$f" ]; then
|
|
ln -snf "$SOURCE_GSTACK_DIR/review/$f" "$KIRO_GSTACK/review/$f"
|
|
fi
|
|
done
|
|
|
|
# Rewrite root SKILL.md paths for Kiro
|
|
sed -e "s|~/.claude/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
-e "s|\.claude/skills/gstack|.kiro/skills/gstack|g" \
|
|
-e "s|\.claude/skills|.kiro/skills|g" \
|
|
"$SOURCE_GSTACK_DIR/SKILL.md" > "$KIRO_GSTACK/SKILL.md"
|
|
|
|
if [ ! -d "$AGENTS_DIR" ]; then
|
|
echo " warning: no .agents/skills/ directory found — run 'bun run build' first" >&2
|
|
else
|
|
for skill_dir in "$AGENTS_DIR"/gstack*/; do
|
|
[ -f "$skill_dir/SKILL.md" ] || continue
|
|
skill_name="$(basename "$skill_dir")"
|
|
target_dir="$KIRO_SKILLS/$skill_name"
|
|
mkdir -p "$target_dir"
|
|
# Generated Codex skills use $HOME/.codex (not ~/), plus $GSTACK_ROOT variables.
|
|
# Rewrite the default GSTACK_ROOT value and any remaining literal paths.
|
|
sed -e 's|\$HOME/.codex/skills/gstack|$HOME/.kiro/skills/gstack|g' \
|
|
-e "s|~/.codex/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
-e "s|~/.claude/skills/gstack|~/.kiro/skills/gstack|g" \
|
|
"$skill_dir/SKILL.md" > "$target_dir/SKILL.md"
|
|
done
|
|
echo "gstack ready (kiro)."
|
|
echo " browse: $BROWSE_BIN"
|
|
echo " kiro skills: $KIRO_SKILLS"
|
|
fi
|
|
fi
|
|
|
|
# 6b. Install for Factory Droid
|
|
if [ "$INSTALL_FACTORY" -eq 1 ]; then
|
|
mkdir -p "$FACTORY_SKILLS"
|
|
create_factory_runtime_root "$SOURCE_GSTACK_DIR" "$FACTORY_GSTACK"
|
|
link_factory_skill_dirs "$SOURCE_GSTACK_DIR" "$FACTORY_SKILLS"
|
|
echo "gstack ready (factory)."
|
|
echo " browse: $BROWSE_BIN"
|
|
echo " factory skills: $FACTORY_SKILLS"
|
|
fi
|
|
|
|
# 6c. Install for OpenCode
|
|
if [ "$INSTALL_OPENCODE" -eq 1 ]; then
|
|
mkdir -p "$OPENCODE_SKILLS"
|
|
create_opencode_runtime_root "$SOURCE_GSTACK_DIR" "$OPENCODE_GSTACK"
|
|
link_opencode_skill_dirs "$SOURCE_GSTACK_DIR" "$OPENCODE_SKILLS"
|
|
echo "gstack ready (opencode)."
|
|
echo " browse: $BROWSE_BIN"
|
|
echo " opencode skills: $OPENCODE_SKILLS"
|
|
fi
|
|
|
|
# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
|
|
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
|
|
# so the runtime assets must live there for both global and repo-local installs.
|
|
if [ "$INSTALL_CODEX" -eq 1 ]; then
|
|
create_agents_sidecar "$SOURCE_GSTACK_DIR"
|
|
fi
|
|
|
|
# 8. Run pending version migrations
|
|
# Migrations handle state fixes that ./setup alone can't cover (stale config,
|
|
# orphaned files, directory structure changes). Each migration is idempotent.
|
|
MIGRATIONS_DIR="$SOURCE_GSTACK_DIR/gstack-upgrade/migrations"
|
|
CURRENT_VERSION=$(cat "$SOURCE_GSTACK_DIR/VERSION" 2>/dev/null || echo "unknown")
|
|
LAST_SETUP_VERSION=$(cat "$HOME/.gstack/.last-setup-version" 2>/dev/null || echo "0.0.0.0")
|
|
if [ -d "$MIGRATIONS_DIR" ] && [ "$CURRENT_VERSION" != "unknown" ] && [ "$LAST_SETUP_VERSION" != "$CURRENT_VERSION" ]; then
|
|
# Fresh install (no marker file) — skip migrations, just write marker
|
|
if [ ! -f "$HOME/.gstack/.last-setup-version" ]; then
|
|
: # fall through to marker write below
|
|
else
|
|
find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V | while IFS= read -r migration; do
|
|
m_ver="$(basename "$migration" .sh | sed 's/^v//')"
|
|
# Run if migration is newer than last setup version AND not newer than current version
|
|
if [ "$(printf '%s\n%s' "$LAST_SETUP_VERSION" "$m_ver" | sort -V | head -1)" = "$LAST_SETUP_VERSION" ] && [ "$LAST_SETUP_VERSION" != "$m_ver" ] \
|
|
&& [ "$(printf '%s\n%s' "$m_ver" "$CURRENT_VERSION" | sort -V | tail -1)" = "$CURRENT_VERSION" ]; then
|
|
echo " running migration $m_ver..."
|
|
bash "$migration" || echo " warning: migration $m_ver had errors (non-fatal)"
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
mkdir -p "$HOME/.gstack"
|
|
if [ "$CURRENT_VERSION" != "unknown" ]; then
|
|
echo "$CURRENT_VERSION" > "$HOME/.gstack/.last-setup-version"
|
|
fi
|
|
|
|
# 9. First-time welcome + legacy cleanup
|
|
if [ ! -f "$HOME/.gstack/.welcome-seen" ]; then
|
|
log " Welcome! Run /gstack-upgrade anytime to stay current."
|
|
touch "$HOME/.gstack/.welcome-seen"
|
|
fi
|
|
rm -f /tmp/gstack-latest-version
|
|
|
|
# 10. Team mode: register/unregister SessionStart hook
|
|
SETTINGS_HOOK="$SOURCE_GSTACK_DIR/bin/gstack-settings-hook"
|
|
HOOK_CMD="$SOURCE_GSTACK_DIR/bin/gstack-session-update"
|
|
|
|
if [ "$TEAM_MODE" -eq 1 ]; then
|
|
"$GSTACK_CONFIG" set auto_upgrade true 2>/dev/null || true
|
|
"$GSTACK_CONFIG" set team_mode true 2>/dev/null || true
|
|
|
|
# Register SessionStart hook in Claude Code settings
|
|
if [ -x "$SETTINGS_HOOK" ]; then
|
|
"$SETTINGS_HOOK" add "$HOOK_CMD" 2>/dev/null || true
|
|
fi
|
|
|
|
log ""
|
|
log "Team mode enabled: gstack will auto-update at the start of each Claude Code session."
|
|
log " Hook: $HOOK_CMD"
|
|
log " To disable: ./setup --no-team"
|
|
log ""
|
|
log "Bootstrap your repo:"
|
|
log " cd <your-repo> && $SOURCE_GSTACK_DIR/bin/gstack-team-init required"
|
|
fi
|
|
|
|
if [ "$NO_TEAM_MODE" -eq 1 ]; then
|
|
"$GSTACK_CONFIG" set auto_upgrade false 2>/dev/null || true
|
|
"$GSTACK_CONFIG" set team_mode false 2>/dev/null || true
|
|
|
|
# Remove SessionStart hook from Claude Code settings
|
|
if [ -x "$SETTINGS_HOOK" ]; then
|
|
"$SETTINGS_HOOK" remove "$HOOK_CMD" 2>/dev/null || true
|
|
fi
|
|
|
|
log "Team mode disabled: auto-update hook removed."
|
|
fi
|