#!/usr/bin/env bash set -euo pipefail CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" CLAUDE_HOME="${CLAUDE_HOME:-$HOME/.claude}" GSTACK_REPO_URL="${GSTACK_REPO_URL:-https://github.com/garrytan/gstack.git}" VERIFY_URL="${VERIFY_URL:-https://example.com}" SETUP_DOC="$CODEX_HOME/browser-control-setup.md" TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT log() { printf '[install-browser-control] %s\n' "$*" >&2 } fail() { printf '[install-browser-control] ERROR: %s\n' "$*" >&2 exit 1 } ensure_macos() { local os_name os_name="$(uname -s)" if [[ "$os_name" != "Darwin" ]]; then fail "当前脚本仅支持 macOS,检测到: $os_name" fi } ensure_dir() { mkdir -p "$1" } detect_codex_install() { if command -v codex >/dev/null 2>&1; then command -v codex else printf 'not-found' fi } detect_claude_install() { if command -v claude >/dev/null 2>&1; then command -v claude else printf 'not-found' fi } have_equivalent_gstack() { local target="$1" [[ -f "$target/connect-chrome/SKILL.md" ]] \ && [[ -f "$target/setup-browser-cookies/SKILL.md" ]] \ && [[ -f "$target/browse/src/cli.ts" || -x "$target/browse/dist/browse" ]] } clone_seed() { local seed_dir="$TMP_DIR/gstack-seed" if [[ -d "$seed_dir" ]]; then return fi log "cloning gstack from $GSTACK_REPO_URL" git clone --depth 1 "$GSTACK_REPO_URL" "$seed_dir" >/dev/null 2>&1 } backup_path() { local path="$1" if [[ -e "$path" ]]; then mv "$path" "$path.local.bak.$(date +%Y%m%d%H%M%S)" fi } ensure_gstack_home() { local home_root="$1" local target="$home_root/skills/gstack" ensure_dir "$home_root/skills" if have_equivalent_gstack "$target"; then printf '%s' "$target" return fi if [[ -e "$target" ]]; then backup_path "$target" fi clone_seed cp -R "$TMP_DIR/gstack-seed" "$target" printf '%s' "$target" } ensure_bun() { if command -v bun >/dev/null 2>&1; then return fi if command -v brew >/dev/null 2>&1; then log "installing bun with Homebrew" brew install bun >/dev/null else log "installing bun from bun.sh" curl -fsSL https://bun.sh/install | bash >/dev/null export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}" export PATH="$BUN_INSTALL/bin:$PATH" fi command -v bun >/dev/null 2>&1 || fail "bun 安装失败" } ensure_browse_runtime() { local gstack_root="$1" local browse_bin="$gstack_root/browse/dist/browse" if [[ ! -x "$browse_bin" ]]; then ensure_bun log "building browse binary in $gstack_root" ( cd "$gstack_root" bun install >/dev/null bun run build >/dev/null ) fi [[ -x "$browse_bin" ]] || fail "browse binary 不可用: $browse_bin" ensure_bun log "ensuring Playwright Chromium runtime" ( cd "$gstack_root" bunx playwright install chromium >/dev/null 2>&1 || true ) printf '%s' "$browse_bin" } ensure_skill_entry() { local home_root="$1" local skill_name="$2" local dest="$home_root/skills/$skill_name" if [[ -L "$dest" ]]; then printf '%s' "$dest" return fi if [[ -f "$dest/SKILL.md" ]] && grep -q "^name: $skill_name\$" "$dest/SKILL.md"; then printf '%s' "$dest" return fi if [[ -e "$dest" ]]; then backup_path "$dest" fi ln -sfn "gstack/$skill_name" "$dest" printf '%s' "$dest" } detect_browser_apps() { local apps=() [[ -d "/Applications/Google Chrome.app" ]] && apps+=("Google Chrome") [[ -d "/Applications/Chromium.app" ]] && apps+=("Chromium") [[ -d "/Applications/Microsoft Edge.app" ]] && apps+=("Microsoft Edge") [[ -d "/Applications/Brave Browser.app" ]] && apps+=("Brave Browser") [[ -d "/Applications/Arc.app" ]] && apps+=("Arc") if [[ ${#apps[@]} -eq 0 ]]; then printf 'none-detected' else local joined="" local app for app in "${apps[@]}"; do if [[ -n "$joined" ]]; then joined+=", " fi joined+="$app" done printf '%s' "$joined" fi } visible_browser_processes() { /usr/bin/osascript -e 'tell application "System Events" to get the name of every process whose visible is true' 2>/dev/null \ | tr ',' '\n' \ | sed 's/^ *//' \ | grep -E 'Chrome|Chromium' \ | paste -sd ', ' - \ || true } verify_browser_control() { local browse_bin="$1" local verify_tmp="$TMP_DIR/verify" local connect_out local newtab_out local title_out local heading_out local network_out local console_out local status_out local cookie_out="" local cookie_rc=0 local visible_out="" local cookie_status="" mkdir -p "$verify_tmp" "$browse_bin" stop >/dev/null 2>&1 || true "$browse_bin" connect >"$verify_tmp/connect.log" 2>&1 sleep 2 "$browse_bin" newtab "$VERIFY_URL" >"$verify_tmp/newtab.log" 2>&1 "$browse_bin" js 'document.title' >"$verify_tmp/title.log" 2>&1 "$browse_bin" text h1 >"$verify_tmp/heading.log" 2>&1 "$browse_bin" reload >/dev/null 2>&1 || true "$browse_bin" wait --load >/dev/null 2>&1 || true "$browse_bin" network >"$verify_tmp/network.log" 2>&1 "$browse_bin" console >"$verify_tmp/console.log" 2>&1 "$browse_bin" status >"$verify_tmp/status.log" 2>&1 visible_out="$(visible_browser_processes)" "$browse_bin" cookie-import-browser chrome --domain example.com >"$verify_tmp/cookie.log" 2>&1 || cookie_rc=$? connect_out="$(cat "$verify_tmp/connect.log")" newtab_out="$(cat "$verify_tmp/newtab.log")" title_out="$(cat "$verify_tmp/title.log")" heading_out="$(cat "$verify_tmp/heading.log")" network_out="$(cat "$verify_tmp/network.log")" console_out="$(cat "$verify_tmp/console.log")" status_out="$(cat "$verify_tmp/status.log")" cookie_out="$(cat "$verify_tmp/cookie.log" 2>/dev/null || true)" if [[ $cookie_rc -eq 0 ]]; then cookie_status="callable" elif [[ "$cookie_out" == *"Keychain"* ]]; then cookie_status="callable-requires-keychain-access" elif [[ "$cookie_out" == *"No Chromium browsers found"* ]]; then cookie_status="callable-no-cookie-source-browser" else cookie_status="error" fi BROWSE_CONNECT_OUT="$connect_out" BROWSE_NEWTAB_OUT="$newtab_out" BROWSE_TITLE_OUT="$title_out" BROWSE_HEADING_OUT="$heading_out" BROWSE_NETWORK_OUT="$network_out" BROWSE_CONSOLE_OUT="$console_out" BROWSE_STATUS_OUT="$status_out" BROWSE_VISIBLE_OUT="$visible_out" COOKIE_VERIFY_OUT="$cookie_out" COOKIE_VERIFY_STATUS="$cookie_status" } write_setup_doc() { local codex_connect="$1" local codex_browse="$2" local codex_cookie="$3" local claude_connect="$4" local claude_browse="$5" local claude_cookie="$6" local codex_gstack="$7" local claude_gstack="$8" local browse_bin="$9" local codex_install="${10}" local claude_install="${11}" local browser_apps="${12}" ensure_dir "$CODEX_HOME" cat >"$SETUP_DOC" <