diff --git a/.claude/skills/install-browser-control/SKILL.md b/.claude/skills/install-browser-control/SKILL.md new file mode 100644 index 0000000..33feb92 --- /dev/null +++ b/.claude/skills/install-browser-control/SKILL.md @@ -0,0 +1,45 @@ +--- +name: install-browser-control +description: 在新 Mac 上为 Codex 和 Claude 全局安装或整理真实 Chrome/Chromium 调试能力时使用;适用于需要 connect-chrome、browse、setup-browser-cookies 三个入口、可见浏览器接管、DOM 操作、network/console 检查与 cookie 导入支持的场景。 +--- + +# Install Browser Control + +当用户要求在新机器上把浏览器接管调试能力装完整时,直接执行本 skill。 + +## 目标 + +- 自动识别 `CODEX_HOME`,默认回退到 `~/.codex` +- 在 Codex / Claude 两侧全局目录补齐或标准化: + - `connect-chrome` + - `browse` + - `setup-browser-cookies` +- 优先复用现有等价能力;缺失时从公开官方来源安装 +- 生成本地说明文件:`$CODEX_HOME/browser-control-setup.md` +- 做真实验证:可见 Chromium 窗口、`https://example.com`、DOM 读取、network/console、cookie 导入入口调用 + +## 执行 + +先运行安装脚本: + +```bash +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +bash "$SCRIPT_DIR/install-browser-control.sh" +``` + +## 交付 + +安装脚本结束后,读取并引用: + +```bash +CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" +sed -n '1,240p' "$CODEX_HOME/browser-control-setup.md" +``` + +汇报时只保留高信号结果: + +- 是否安装完成 +- 3 个入口的最终路径 +- 本地说明文件路径 +- 实际验证结果 +- 如果未完成,唯一阻塞点 diff --git a/.claude/skills/install-browser-control/install-browser-control.sh b/.claude/skills/install-browser-control/install-browser-control.sh new file mode 100755 index 0000000..5fbcbc0 --- /dev/null +++ b/.claude/skills/install-browser-control/install-browser-control.sh @@ -0,0 +1,433 @@ +#!/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" <&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" <