#!/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 # ─── Symlink-or-copy helper ─────────────────────────────────── # On macOS/Linux: create a symlink (existing behavior). # On Windows without Developer Mode (MSYS2/Git Bash): plain ln -snf silently # creates a frozen file copy that doesn't refresh after `git pull`. We use # explicit `cp -R` / `cp -f` so the user gets a real copy and the staleness # is reportable (re-run ./setup after pull). Auto-detects file vs dir. # # INVARIANT: every symlink in this script MUST route through this helper. # A raw ln call here will be caught by test/setup-windows-fallback.test.ts # (the static-invariant assertion D7). _link_or_copy() { local src="$1" local dst="$2" if [ "$IS_WINDOWS" -eq 1 ]; then rm -rf "$dst" # Unix `ln -snf` accepts a name-only or relative-path source even when the # target doesn't resolve from CWD (e.g. the connect-chrome alias points at # the sibling-relative "gstack/open-gstack-browser"). On Windows the # equivalent semantics don't exist — we'd need a real source on disk to # copy. Skip the alias quietly rather than aborting setup under `set -e`. if [ ! -e "$src" ]; then return 0 fi if [ -d "$src" ]; then cp -R "$src" "$dst" else cp -f "$src" "$dst" fi else ln -snf "$src" "$dst" fi } _WINDOWS_COPY_NOTE_PRINTED=0 _print_windows_copy_note_once() { if [ "$IS_WINDOWS" -eq 1 ] && [ "$_WINDOWS_COPY_NOTE_PRINTED" -eq 0 ]; then echo " note: Windows install uses file copies (no Developer Mode required). Re-run ./setup after every 'git pull' to refresh skill files." _WINDOWS_COPY_NOTE_PRINTED=1 fi } # ─── 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 PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit 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 ;; --plan-tune-hooks) PLAN_TUNE_HOOKS_MODE="yes"; shift ;; --no-plan-tune-hooks) PLAN_TUNE_HOOKS_MODE="no"; shift ;; --plan-tune-hooks=*) PLAN_TUNE_HOOKS_MODE="${1#--plan-tune-hooks=}"; 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/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 } # Ensure a color-emoji font is installed (Linux only). # # Chromium renders emoji code points as .notdef "tofu" (▯) when no color-emoji # font is installed. macOS ships "Apple Color Emoji" and Windows ships "Segoe UI # Emoji", so they're fine out of the box. Most Linux distros and containers ship # NO color-emoji font, which is why make-pdf output shows tofu in headers/tables # that contain emoji. Install Noto Color Emoji to fix it. # # Best-effort: warn (don't fail) if we can't install — PDFs still generate, they # just fall back to tofu for emoji as before. Skip entirely with # GSTACK_SKIP_FONTS=1 (CI without sudo, managed machines, offline envs). # # Returns 0 and sets EMOJI_FONT_INSTALLED=1 when it actually installs a font. EMOJI_FONT_INSTALLED=0 ensure_emoji_font() { # macOS/Windows ship a color-emoji font; nothing to do. [ "$(uname -s)" = "Linux" ] || return 0 [ "${GSTACK_SKIP_FONTS:-0}" = "1" ] && return 0 # Idempotency: a real COLOR emoji font that resolves for an actual emoji code # point (U+1F600). `fc-list :lang=und-zsye` is too broad — it matches symbol # and last-resort fallback fonts — so we use fc-match and require color=True. if command -v fc-match >/dev/null 2>&1; then if fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' 2>/dev/null | grep -qi 'True'; then return 0 fi fi local sudo="" if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then # -n: never prompt. If a password is required we fail fast into the # warn-not-fail path below instead of hanging a non-interactive setup. sudo="sudo -n" fi # Every package-manager call is wrapped in `timeout` so a stuck dpkg/rpm lock # or a wedged mirror fails fast into the warn path instead of hanging setup. if command -v apt-get >/dev/null 2>&1; then echo "Installing color-emoji font (fonts-noto-color-emoji) so make-pdf emoji render (set GSTACK_SKIP_FONTS=1 to skip)..." DEBIAN_FRONTEND=noninteractive timeout 30 $sudo apt-get update -qq >/dev/null 2>&1 || true DEBIAN_FRONTEND=noninteractive timeout 120 $sudo apt-get install -y -qq fonts-noto-color-emoji >/dev/null 2>&1 || return 1 elif command -v dnf >/dev/null 2>&1; then echo "Installing color-emoji font (google-noto-color-emoji-fonts)..." timeout 120 $sudo dnf install -y google-noto-color-emoji-fonts >/dev/null 2>&1 || return 1 elif command -v pacman >/dev/null 2>&1; then echo "Installing color-emoji font (noto-fonts-emoji)..." timeout 120 $sudo pacman -Sy --noconfirm noto-fonts-emoji >/dev/null 2>&1 || return 1 elif command -v apk >/dev/null 2>&1; then echo "Installing color-emoji font (font-noto-emoji)..." timeout 120 $sudo apk add --no-cache font-noto-emoji >/dev/null 2>&1 || return 1 else return 1 fi # Refresh fontconfig cache so Chromium picks up the new font. Run under sudo # for the system cache dirs (unprivileged fc-cache fails on unwritable dirs). if command -v fc-cache >/dev/null 2>&1; then $sudo fc-cache -f >/dev/null 2>&1 || fc-cache -f >/dev/null 2>&1 || true fi EMOJI_FONT_INSTALLED=1 return 0 } # After a fresh font install, stop any running browse render daemon so the next # make-pdf render spawns a fresh Chromium that sees the new font. Chromium # caches its font list at process start, so a daemon that was alive before the # install would keep emitting tofu. `browse stop` is the graceful API; the # daemon auto-respawns on the next render. Best-effort and per-project-root, so # we also print a note for daemons in other roots. refresh_browse_daemon_for_fonts() { [ "$EMOJI_FONT_INSTALLED" -eq 1 ] || return 0 if [ -x "$BROWSE_BIN" ]; then "$BROWSE_BIN" stop >/dev/null 2>&1 || true fi echo " Installed a color-emoji font. The next make-pdf render will show emoji." echo " If a gstack browser is running in another project, restart it to pick up the font." } prepare_bun_for_windows_compile() { BUN_CMD="bun" BUN_CMD_WAS_COPIED=0 [ "$IS_WINDOWS" -eq 1 ] || return 0 local bun_path bun_path="$(command -v bun 2>/dev/null || true)" case "$bun_path" in *[![:ascii:]]*) local bun_copy_dir="$SOURCE_GSTACK_DIR/.tmp-bun-bin" mkdir -p "$bun_copy_dir" cp -f "$bun_path" "$bun_copy_dir/bun.exe" BUN_CMD="$bun_copy_dir/bun.exe" BUN_CMD_WAS_COPIED=1 ;; esac } bun_cmd() { "$BUN_CMD" "$@" } cleanup_copied_bun() { if [ "${BUN_CMD_WAS_COPIED:-0}" -eq 1 ]; then rm -rf "$SOURCE_GSTACK_DIR/.tmp-bun-bin" fi } prepare_bun_for_windows_compile trap cleanup_copied_bun EXIT # 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_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install bun_cmd 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 make-pdf/dist/pdf 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_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install bun_cmd 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_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install bun_cmd 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_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install bun_cmd 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 # 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux). # Best-effort: warn instead of failing if it can't install. if ! ensure_emoji_font; then echo " Note: could not auto-install a color-emoji font. Emoji in make-pdf" >&2 echo " output may render as boxes (▯). Install one manually, e.g.:" >&2 echo " Debian/Ubuntu: sudo apt-get install fonts-noto-color-emoji" >&2 echo " Fedora: sudo dnf install google-noto-color-emoji-fonts" >&2 echo " Arch: sudo pacman -S noto-fonts-emoji" >&2 echo " Alpine: sudo apk add font-noto-emoji" >&2 else refresh_browse_daemon_for_fonts 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 _link_or_copy "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md" # Link the sections/ subdir for carved skills (v2 plan T9). The prefixed # Claude skill dir otherwise holds only SKILL.md, so a runtime # "Read sections/.md" 404s. Route through _link_or_copy so Windows # gets a fresh copy (and re-copies on every ./setup, refreshing staleness). if [ -d "$gstack_dir/$dir_name/sections" ]; then if [ -e "$target/sections" ] || [ -L "$target/sections" ]; then rm -rf "$target/sections"; fi _link_or_copy "$gstack_dir/$dir_name/sections" "$target/sections" fi # Also link bin/ when the source skill ships one. PreToolUse hook # frontmatter references $CLAUDE_SKILL_DIR/bin/