#!/usr/bin/env bash # Bootstrap or re-sync the OpenCode harness on this Mac. # # Idempotent — re-run after pulling changes to opencode.json or the # Phoenix bridge plugin. Each step checks before doing work. # # What this does: # 1. Verify Homebrew is present # 2. Install node, uv, opencode, jq (skips if already at latest) # 3. Pre-cache Playwright's chromium so the first MCP call is instant # 4. Install the Phoenix bridge plugin's OTel deps # 5. Generate ~/.config/opencode/opencode.json from the repo's # opencode.json with relative plugin paths rewritten to absolute, # so opencode loads the plugin regardless of where it's launched. # # Usage: ./install.sh (from this directory) set -euo pipefail cd "$(dirname "$0")" HERE="$(pwd)" # --- Pretty printing --------------------------------------------------------- bold() { printf '\033[1m%s\033[0m\n' "$*"; } ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } info() { printf ' → %s\n' "$*"; } warn() { printf ' \033[33m!\033[0m %s\n' "$*"; } fail() { printf ' \033[31m✗\033[0m %s\n' "$*"; exit 1; } # --- 1. Homebrew ------------------------------------------------------------- bold "[1/5] Homebrew" if ! command -v brew >/dev/null 2>&1; then fail "brew not found. Install from https://brew.sh, then re-run." fi ok "brew $(brew --version | head -1 | awk '{print $2}')" # --- 2. CLI deps ------------------------------------------------------------- bold "[2/5] CLI dependencies" brew_install_if_missing() { local pkg="$1" local bin="${2:-$1}" if command -v "$bin" >/dev/null 2>&1; then ok "$pkg already installed ($(command -v "$bin"))" else info "installing $pkg" brew install "$pkg" ok "$pkg installed" fi } brew_install_if_missing node node brew_install_if_missing uv uv brew_install_if_missing jq jq # opencode is in a tap; check the binary, not the formula name. if command -v opencode >/dev/null 2>&1; then ok "opencode already installed ($(command -v opencode))" else info "tapping sst/tap and installing opencode" brew install sst/tap/opencode ok "opencode installed" fi # --- 3. Playwright browsers -------------------------------------------------- bold "[3/5] Playwright browser cache" PW_CACHE="${HOME}/Library/Caches/ms-playwright" if [[ -d "$PW_CACHE" ]] && find "$PW_CACHE" -name "chrome" -o -name "Chromium*" 2>/dev/null | grep -q .; then ok "browsers already cached at $PW_CACHE" else info "downloading chromium (~200 MB) — first run only" npx -y @playwright/mcp@latest --help >/dev/null 2>&1 || true ok "browsers cached" fi # --- 4. Phoenix bridge plugin deps ------------------------------------------ bold "[4/5] Phoenix bridge plugin deps" if [[ -d ".opencode/plugin/node_modules" && -f ".opencode/plugin/package-lock.json" ]]; then # Re-run npm install if package.json is newer than the lockfile, otherwise skip. if [[ ".opencode/plugin/package.json" -nt ".opencode/plugin/package-lock.json" ]]; then info "package.json is newer than lockfile — running npm install" ( cd .opencode/plugin && npm install ) ok "deps updated" else ok "deps already installed" fi else info "installing OTel deps (one-time, ~40 MB)" ( cd .opencode/plugin && npm install ) ok "deps installed" fi # --- 5. Generate ~/.config/opencode/opencode.json --------------------------- # The repo's opencode.json uses relative plugin paths so it stays valid # in-place. Rewriting them to absolute paths here makes opencode find the # plugin regardless of which directory it was launched from. Re-run this # script after editing opencode.json. bold "[5/5] Deploy global config" mkdir -p "${HOME}/.config/opencode" src="${HERE}/opencode.json" dst="${HOME}/.config/opencode/opencode.json" # If the user previously had a symlink from the old install.sh, replace it. if [[ -L "$dst" ]]; then info "removing stale symlink at $dst" rm "$dst" fi # And the old .opencode dir symlink — no longer needed now that plugin # paths are absolute. if [[ -L "${HOME}/.config/opencode/.opencode" ]]; then info "removing stale ~/.config/opencode/.opencode symlink" rm "${HOME}/.config/opencode/.opencode" fi # Rewrite any relative plugin path (./foo, ../foo) to an absolute path # rooted at this directory. Absolute paths and npm-package refs pass # through untouched. jq --arg here "$HERE" ' .plugin = ( (.plugin // []) | map( if type == "string" and (startswith("./") or startswith("../")) then ($here + "/" + ltrimstr("./") | gsub("/\\./"; "/")) else . end ) ) ' "$src" > "$dst.tmp" mv "$dst.tmp" "$dst" ok "wrote $dst" info "plugin paths resolved to:" jq -r '.plugin[]?' "$dst" | sed 's/^/ /' echo bold "Done." cat <