From 10e6d39f2707fa3a2934f9fd548cf06e2ad6a346 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 19 Mar 2026 00:57:43 -0700 Subject: [PATCH] feat: dual-host setup + find-browse for Codex/Gemini/Cursor - setup: add --host codex|claude|auto flag, install to ~/.codex/skills/ when targeting Codex, auto-detect installed agents - find-browse: priority chain .codex > .agents > .claude (both workspace-local and global) - dev-setup/teardown: create .agents/skills/gstack symlinks for dev mode Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/dev-setup | 16 +++++- bin/dev-teardown | 61 +++++++++++++------- browse/bin/find-browse | 22 ++++--- browse/src/find-browse.ts | 13 +++-- setup | 118 +++++++++++++++++++++++++++++++++----- 5 files changed, 179 insertions(+), 51 deletions(-) diff --git a/bin/dev-setup b/bin/dev-setup index 6c5619d2..a5bd4827 100755 --- a/bin/dev-setup +++ b/bin/dev-setup @@ -44,11 +44,25 @@ elif [ -d "$GSTACK_LINK" ]; then fi ln -s "$REPO_ROOT" "$GSTACK_LINK" -# 5. Run setup via the symlink so it detects .claude/skills/ as its parent +# 5. Create .agents/skills/gstack → repo root (for Codex/Gemini/Cursor) +mkdir -p "$REPO_ROOT/.agents/skills" +AGENTS_LINK="$REPO_ROOT/.agents/skills/gstack" +if [ -L "$AGENTS_LINK" ]; then + rm "$AGENTS_LINK" +elif [ -d "$AGENTS_LINK" ]; then + echo "Warning: .agents/skills/gstack is a real directory, skipping." >&2 +fi +if [ ! -e "$AGENTS_LINK" ]; then + ln -s "$REPO_ROOT" "$AGENTS_LINK" +fi + +# 6. Run setup via the symlink so it detects .claude/skills/ as its parent "$GSTACK_LINK/setup" echo "" echo "Dev mode active. Skills resolve from this working tree." +echo " .claude/skills/gstack → $REPO_ROOT" +echo " .agents/skills/gstack → $REPO_ROOT" echo "Edit any SKILL.md and test immediately — no copy/deploy needed." echo "" echo "To tear down: bin/dev-teardown" diff --git a/bin/dev-teardown b/bin/dev-teardown index e333a75d..dc8f7426 100755 --- a/bin/dev-teardown +++ b/bin/dev-teardown @@ -3,33 +3,50 @@ set -e REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -SKILLS_DIR="$REPO_ROOT/.claude/skills" -if [ ! -d "$SKILLS_DIR" ]; then - echo "Nothing to tear down — .claude/skills/ doesn't exist." - exit 0 -fi - -# Remove individual skill symlinks removed=() -for link in "$SKILLS_DIR"/*/; do - name="$(basename "$link")" - [ "$name" = "gstack" ] && continue - if [ -L "${link%/}" ]; then - rm "${link%/}" - removed+=("$name") - fi -done -# Remove the gstack symlink -if [ -L "$SKILLS_DIR/gstack" ]; then - rm "$SKILLS_DIR/gstack" - removed+=("gstack") +# ─── Clean up .claude/skills/ ───────────────────────────────── +CLAUDE_SKILLS="$REPO_ROOT/.claude/skills" +if [ -d "$CLAUDE_SKILLS" ]; then + for link in "$CLAUDE_SKILLS"/*/; do + name="$(basename "$link")" + [ "$name" = "gstack" ] && continue + if [ -L "${link%/}" ]; then + rm "${link%/}" + removed+=("claude/$name") + fi + done + + if [ -L "$CLAUDE_SKILLS/gstack" ]; then + rm "$CLAUDE_SKILLS/gstack" + removed+=("claude/gstack") + fi + + rmdir "$CLAUDE_SKILLS" 2>/dev/null || true + rmdir "$REPO_ROOT/.claude" 2>/dev/null || true fi -# Clean up empty dirs -rmdir "$SKILLS_DIR" 2>/dev/null || true -rmdir "$REPO_ROOT/.claude" 2>/dev/null || true +# ─── Clean up .agents/skills/ ──────────────────────────────── +AGENTS_SKILLS="$REPO_ROOT/.agents/skills" +if [ -d "$AGENTS_SKILLS" ]; then + for link in "$AGENTS_SKILLS"/*/; do + name="$(basename "$link")" + [ "$name" = "gstack" ] && continue + if [ -L "${link%/}" ]; then + rm "${link%/}" + removed+=("agents/$name") + fi + done + + if [ -L "$AGENTS_SKILLS/gstack" ]; then + rm "$AGENTS_SKILLS/gstack" + removed+=("agents/gstack") + fi + + rmdir "$AGENTS_SKILLS" 2>/dev/null || true + rmdir "$REPO_ROOT/.agents" 2>/dev/null || true +fi if [ ${#removed[@]} -gt 0 ]; then echo "Removed: ${removed[*]}" diff --git a/browse/bin/find-browse b/browse/bin/find-browse index 9cbd7f81..8f441b49 100755 --- a/browse/bin/find-browse +++ b/browse/bin/find-browse @@ -5,13 +5,17 @@ DIR="$(cd "$(dirname "$0")/.." && pwd)/dist" if test -x "$DIR/find-browse"; then exec "$DIR/find-browse" "$@" fi -# Fallback: basic discovery +# Fallback: basic discovery with priority chain ROOT=$(git rev-parse --show-toplevel 2>/dev/null) -if [ -n "$ROOT" ] && test -x "$ROOT/.claude/skills/gstack/browse/dist/browse"; then - echo "$ROOT/.claude/skills/gstack/browse/dist/browse" -elif test -x "$HOME/.claude/skills/gstack/browse/dist/browse"; then - echo "$HOME/.claude/skills/gstack/browse/dist/browse" -else - echo "ERROR: browse binary not found. Run: cd && ./setup" >&2 - exit 1 -fi +for MARKER in .codex .agents .claude; do + if [ -n "$ROOT" ] && test -x "$ROOT/$MARKER/skills/gstack/browse/dist/browse"; then + echo "$ROOT/$MARKER/skills/gstack/browse/dist/browse" + exit 0 + fi + if test -x "$HOME/$MARKER/skills/gstack/browse/dist/browse"; then + echo "$HOME/$MARKER/skills/gstack/browse/dist/browse" + exit 0 + fi +done +echo "ERROR: browse binary not found. Run: cd && ./setup" >&2 +exit 1 diff --git a/browse/src/find-browse.ts b/browse/src/find-browse.ts index 44d76b4c..93c4a26e 100644 --- a/browse/src/find-browse.ts +++ b/browse/src/find-browse.ts @@ -27,16 +27,21 @@ function getGitRoot(): string | null { export function locateBinary(): string | null { const root = getGitRoot(); const home = homedir(); + const markers = ['.codex', '.agents', '.claude']; // Workspace-local takes priority (for development) if (root) { - const local = join(root, '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'); - if (existsSync(local)) return local; + for (const m of markers) { + const local = join(root, m, 'skills', 'gstack', 'browse', 'dist', 'browse'); + if (existsSync(local)) return local; + } } // Global fallback - const global = join(home, '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'); - if (existsSync(global)) return global; + for (const m of markers) { + const global = join(home, m, 'skills', 'gstack', 'browse', 'dist', 'browse'); + if (existsSync(global)) return global; + } return null; } diff --git a/setup b/setup index 607c2772..10fd6742 100755 --- a/setup +++ b/setup @@ -1,11 +1,42 @@ #!/usr/bin/env bash -# gstack setup — build browser binary + register all skills with Claude Code +# gstack setup — build browser binary + register skills with Claude Code / Codex set -e GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)" SKILLS_DIR="$(dirname "$GSTACK_DIR")" BROWSE_BIN="$GSTACK_DIR/browse/dist/browse" +# ─── Parse --host flag ───────────────────────────────────────── +HOST="claude" +while [ $# -gt 0 ]; do + case "$1" in + --host) HOST="$2"; shift 2 ;; + --host=*) HOST="${1#--host=}"; shift ;; + *) shift ;; + esac +done + +case "$HOST" in + claude|codex|auto) ;; + *) echo "Unknown --host value: $HOST (expected claude, codex, or auto)" >&2; exit 1 ;; +esac + +# For auto: detect which agents are installed +INSTALL_CLAUDE=0 +INSTALL_CODEX=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 + # If neither found, default to claude + if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ]; then + INSTALL_CLAUDE=1 + fi +elif [ "$HOST" = "claude" ]; then + INSTALL_CLAUDE=1 +elif [ "$HOST" = "codex" ]; then + INSTALL_CODEX=1 +fi + ensure_playwright_browser() { ( cd "$GSTACK_DIR" @@ -60,16 +91,17 @@ fi # 3. Ensure ~/.gstack global state directory exists mkdir -p "$HOME/.gstack/projects" -# 4. Only create skill symlinks if we're inside a .claude/skills directory -SKILLS_BASENAME="$(basename "$SKILLS_DIR")" -if [ "$SKILLS_BASENAME" = "skills" ]; then - linked=() - for skill_dir in "$GSTACK_DIR"/*/; do +# ─── Helper: link skill subdirectories into a skills parent directory ── +link_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 skill_name="$(basename "$skill_dir")" # Skip node_modules [ "$skill_name" = "node_modules" ] && continue - target="$SKILLS_DIR/$skill_name" + target="$skills_dir/$skill_name" # Create or update symlink; skip if a real file/directory exists if [ -L "$target" ] || [ ! -e "$target" ]; then ln -snf "gstack/$skill_name" "$target" @@ -77,19 +109,75 @@ if [ "$SKILLS_BASENAME" = "skills" ]; then fi fi done - - echo "gstack ready." - echo " browse: $BROWSE_BIN" if [ ${#linked[@]} -gt 0 ]; then echo " linked skills: ${linked[*]}" fi -else - echo "gstack ready." - echo " browse: $BROWSE_BIN" - echo " (skipped skill symlinks — not inside .claude/skills/)" +} + +# ─── 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="$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 +} + +# 4. Install for Claude (default) +SKILLS_BASENAME="$(basename "$SKILLS_DIR")" +if [ "$INSTALL_CLAUDE" -eq 1 ]; then + if [ "$SKILLS_BASENAME" = "skills" ]; then + link_skill_dirs "$GSTACK_DIR" "$SKILLS_DIR" + echo "gstack ready (claude)." + echo " browse: $BROWSE_BIN" + else + echo "gstack ready (claude)." + echo " browse: $BROWSE_BIN" + echo " (skipped skill symlinks — not inside .claude/skills/)" + fi fi -# 4. First-time welcome + legacy cleanup +# 5. Install for Codex +if [ "$INSTALL_CODEX" -eq 1 ]; then + CODEX_SKILLS="$HOME/.codex/skills" + CODEX_GSTACK="$CODEX_SKILLS/gstack" + mkdir -p "$CODEX_SKILLS" + + # Symlink or copy gstack into ~/.codex/skills/gstack + if [ -L "$CODEX_GSTACK" ] || [ ! -e "$CODEX_GSTACK" ]; then + ln -snf "$GSTACK_DIR" "$CODEX_GSTACK" + fi + link_skill_dirs "$GSTACK_DIR" "$CODEX_SKILLS" + + echo "gstack ready (codex)." + echo " browse: $BROWSE_BIN" + echo " codex skills: $CODEX_SKILLS" +fi + +# 6. Create .agents/ sidecar symlinks (useful for Codex/Gemini/Cursor workspace-local) +if [ "$INSTALL_CODEX" -eq 1 ]; then + # Detect repo root: if we're inside a skills directory, go up two levels + if [ "$SKILLS_BASENAME" = "skills" ]; then + REPO_ROOT="$(dirname "$SKILLS_DIR")" + else + REPO_ROOT="$GSTACK_DIR" + fi + create_agents_sidecar "$REPO_ROOT" +fi + +# 7. First-time welcome + legacy cleanup if [ ! -d "$HOME/.gstack" ]; then mkdir -p "$HOME/.gstack" echo " Welcome! Run /gstack-upgrade anytime to stay current."