~cytrogen/gstack

ref: 709bed9f4d7d419ef4f806f8b3e91fa53f6c0945 gstack/setup -rwxr-xr-x 8.2 KiB
709bed9f — Garry Tan feat: CEO review handoff context for /office-hours chaining (v0.9.5.0) (#288) a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#!/usr/bin/env bash
# gstack setup — build browser binary + register skills with Claude Code / Codex
set -e

if ! command -v bun >/dev/null 2>&1; then
  echo "Error: bun is required but not installed." >&2
  echo "Install it: curl -fsSL https://bun.sh/install | bash" >&2
  exit 1
fi

GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILLS_DIR="$(dirname "$GSTACK_DIR")"
BROWSE_BIN="$GSTACK_DIR/browse/dist/browse"

IS_WINDOWS=0
case "$(uname -s)" in
  MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
esac

# ─── 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() {
  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 "$GSTACK_DIR"
      node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
    )
  else
    (
      cd "$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 "$GSTACK_DIR/browse/src" -type f -newer "$BROWSE_BIN" -print -quit 2>/dev/null)" ]; then
  NEEDS_BUILD=1
elif [ "$GSTACK_DIR/package.json" -nt "$BROWSE_BIN" ]; then
  NEEDS_BUILD=1
elif [ -f "$GSTACK_DIR/bun.lock" ] && [ "$GSTACK_DIR/bun.lock" -nt "$BROWSE_BIN" ]; then
  NEEDS_BUILD=1
fi

if [ "$NEEDS_BUILD" -eq 1 ]; then
  echo "Building browse binary..."
  (
    cd "$GSTACK_DIR"
    bun install
    bun run build
  )
  # Safety net: write .version if build script didn't (e.g., git not available during build)
  if [ ! -f "$GSTACK_DIR/browse/dist/.version" ]; then
    git -C "$GSTACK_DIR" rev-parse HEAD > "$GSTACK_DIR/browse/dist/.version" 2>/dev/null || true
  fi
fi

if [ ! -x "$BROWSE_BIN" ]; then
  echo "gstack setup failed: browse binary missing at $BROWSE_BIN" >&2
  exit 1
fi

# 2. Ensure Playwright's Chromium is available
if ! ensure_playwright_browser; then
  echo "Installing Playwright Chromium..."
  (
    cd "$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 "$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
    )
  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 ──
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
      skill_name="$(basename "$skill_dir")"
      # Skip node_modules
      [ "$skill_name" = "node_modules" ] && continue
      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"
        linked+=("$skill_name")
      fi
    fi
  done
  if [ ${#linked[@]} -gt 0 ]; then
    echo "  linked skills: ${linked[*]}"
  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 "  warning: no .agents/skills/ directory found — run 'bun run build' first" >&2
    return 1
  fi

  for skill_dir in "$agents_dir"/gstack*/; do
    if [ -f "$skill_dir/SKILL.md" ]; then
      skill_name="$(basename "$skill_dir")"
      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="$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_claude_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

# 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 gstack source for runtime assets (bin/, browse/dist/)
  if [ -L "$CODEX_GSTACK" ] || [ ! -e "$CODEX_GSTACK" ]; then
    ln -snf "$GSTACK_DIR" "$CODEX_GSTACK"
  fi
  # Install generated Codex-format skills (not Claude source dirs)
  link_codex_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."
fi
rm -f /tmp/gstack-latest-version