feat: support repo-local Codex installs (#317)

Changes gen-skill-docs.ts to use dynamic $GSTACK_ROOT/$GSTACK_BIN/$GSTACK_BROWSE
variables in generated Codex preambles instead of hardcoded ~/.codex/ paths.
Renames GSTACK_DIR → SOURCE_GSTACK_DIR/INSTALL_GSTACK_DIR throughout setup for
clarity. Supports both global (~/.codex/skills/) and repo-local (.agents/skills/)
Codex installs.

Co-authored-by: pengwk <pengwk@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-22 14:44:37 -07:00
parent 5db711e113
commit 530d276466
6 changed files with 101 additions and 581 deletions
+42 -34
View File
@@ -8,9 +8,10 @@ if ! command -v bun >/dev/null 2>&1; then
exit 1
fi
GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILLS_DIR="$(dirname "$GSTACK_DIR")"
BROWSE_BIN="$GSTACK_DIR/browse/dist/browse"
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"
@@ -67,13 +68,14 @@ migrate_direct_codex_install() {
echo "Migrating direct Codex install to $migrated_dir to avoid duplicate skill discovery..."
mv "$gstack_dir" "$migrated_dir"
GSTACK_DIR="$migrated_dir"
SKILLS_DIR="$(dirname "$GSTACK_DIR")"
BROWSE_BIN="$GSTACK_DIR/browse/dist/browse"
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 "$GSTACK_DIR" "$CODEX_GSTACK"
migrate_direct_codex_install "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
fi
ensure_playwright_browser() {
@@ -81,12 +83,12 @@ ensure_playwright_browser() {
# 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"
cd "$SOURCE_GSTACK_DIR"
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
)
else
(
cd "$GSTACK_DIR"
cd "$SOURCE_GSTACK_DIR"
bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();'
) >/dev/null 2>&1
fi
@@ -96,24 +98,24 @@ ensure_playwright_browser() {
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
elif [ -n "$(find "$SOURCE_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
elif [ "$SOURCE_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
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
echo "Building browse binary..."
(
cd "$GSTACK_DIR"
cd "$SOURCE_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
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
fi
@@ -126,18 +128,18 @@ fi
# .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
# but .agents/ hasn't been generated yet, e.g., fresh clone).
AGENTS_DIR="$GSTACK_DIR/.agents/skills"
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
NEEDS_AGENTS_GEN=0
if [ ! -d "$AGENTS_DIR" ]; then
NEEDS_AGENTS_GEN=1
elif [ -n "$(find "$GSTACK_DIR" -maxdepth 2 -name 'SKILL.md.tmpl' -newer "$AGENTS_DIR" -print -quit 2>/dev/null)" ]; then
elif [ -n "$(find "$SOURCE_GSTACK_DIR" -maxdepth 2 -name 'SKILL.md.tmpl' -newer "$AGENTS_DIR" -print -quit 2>/dev/null)" ]; then
NEEDS_AGENTS_GEN=1
fi
if [ "$NEEDS_AGENTS_GEN" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
echo "Generating .agents/ skill docs..."
(
cd "$GSTACK_DIR"
cd "$SOURCE_GSTACK_DIR"
bun install --frozen-lockfile 2>/dev/null || bun install
bun run gen:skill-docs --host codex
)
@@ -147,7 +149,7 @@ fi
if ! ensure_playwright_browser; then
echo "Installing Playwright Chromium..."
(
cd "$GSTACK_DIR"
cd "$SOURCE_GSTACK_DIR"
bunx playwright install chromium
)
@@ -161,7 +163,7 @@ if ! ensure_playwright_browser; then
fi
echo "Windows detected — verifying Node.js can load Playwright..."
(
cd "$GSTACK_DIR"
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
)
@@ -255,7 +257,7 @@ create_agents_sidecar() {
# Sidecar directories that skills reference at runtime
for asset in bin browse review qa; do
local src="$GSTACK_DIR/$asset"
local src="$SOURCE_GSTACK_DIR/$asset"
local dst="$agents_gstack/$asset"
if [ -d "$src" ] || [ -f "$src" ]; then
if [ -L "$dst" ] || [ ! -e "$dst" ]; then
@@ -266,7 +268,7 @@ create_agents_sidecar() {
# Sidecar files that skills reference at runtime
for file in ETHOS.md; do
local src="$GSTACK_DIR/$file"
local src="$SOURCE_GSTACK_DIR/$file"
local dst="$agents_gstack/$file"
if [ -f "$src" ]; then
if [ -L "$dst" ] || [ ! -e "$dst" ]; then
@@ -315,10 +317,16 @@ create_codex_runtime_root() {
}
# 4. Install for Claude (default)
SKILLS_BASENAME="$(basename "$SKILLS_DIR")"
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
link_claude_skill_dirs "$GSTACK_DIR" "$SKILLS_DIR"
link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
echo "gstack ready (claude)."
echo " browse: $BROWSE_BIN"
else
@@ -330,26 +338,26 @@ 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"
create_codex_runtime_root "$GSTACK_DIR" "$CODEX_GSTACK"
create_codex_runtime_root "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
# Install generated Codex-format skills (not Claude source dirs)
link_codex_skill_dirs "$GSTACK_DIR" "$CODEX_SKILLS"
link_codex_skill_dirs "$SOURCE_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)
# 6. 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
# 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"
create_agents_sidecar "$SOURCE_GSTACK_DIR"
fi
# 7. First-time welcome + legacy cleanup