#!/usr/bin/env bash # scripts/update.sh — pull + rebuild + restart daemon for collaborators # # Usage (from repo root or anywhere inside the clone): # bash scripts/update.sh # # Idempotent. Aborts on a dirty working tree so local changes are never # clobbered. Re-runs safely — each step detects whether it is needed. set -euo pipefail # Resolve repo root no matter where the script is invoked from. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" cd "${REPO_ROOT}" step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; } ok() { printf ' \033[0;32m✓\033[0m %s\n' "$*"; } warn() { printf ' \033[0;33m!\033[0m %s\n' "$*"; } die() { printf '\n\033[0;31m✗ %s\033[0m\n' "$*" >&2; exit 1; } # --------------------------------------------------------------------------- # 0. Preconditions # --------------------------------------------------------------------------- step "preflight" [ -d .git ] || die "not a git repository (run from an iai-mcp clone)" # Require a clean working tree — never trample local edits. if [ -n "$(git status --porcelain)" ]; then git status --short die "working tree is dirty. commit or stash first, then re-run." fi ok "working tree clean" VENV_PY="${REPO_ROOT}/.venv/bin/python" [ -x "${VENV_PY}" ] || die ".venv/bin/python not found — run 'python3 -m venv .venv && .venv/bin/pip install -e .' once, then rerun" ok "venv detected" # --------------------------------------------------------------------------- # 1. git pull (fast-forward only — never merge surprises) # --------------------------------------------------------------------------- step "git pull --ff-only origin main" BEFORE="$(git rev-parse HEAD)" git fetch --quiet origin main git pull --ff-only --quiet origin main AFTER="$(git rev-parse HEAD)" if [ "${BEFORE}" = "${AFTER}" ]; then ok "already at $(git rev-parse --short HEAD) — no upstream commits" NOOP=1 else ok "advanced $(git rev-parse --short "${BEFORE}") → $(git rev-parse --short "${AFTER}")" NOOP=0 fi # --------------------------------------------------------------------------- # 2. Python package (editable reinstall — picks up deps or entry-point drift) # --------------------------------------------------------------------------- step "python package refresh (editable)" "${VENV_PY}" -m pip install --quiet -e . || die "pip install -e failed" ok "iai-mcp python package up to date" # --------------------------------------------------------------------------- # 3. TypeScript MCP wrapper # --------------------------------------------------------------------------- step "TS wrapper build" if [ -d mcp-wrapper ]; then pushd mcp-wrapper >/dev/null if [ -f package-lock.json ]; then npm ci --silent --no-audit --no-fund else npm install --silent --no-audit --no-fund fi npm run build --silent popd >/dev/null ok "mcp-wrapper/dist rebuilt" else warn "mcp-wrapper/ missing — skipping" fi # --------------------------------------------------------------------------- # 4. Global CLI symlink (idempotent — ensures ~/.local/bin/iai-mcp exists) # --------------------------------------------------------------------------- step "global CLI symlink" LOCAL_BIN="${HOME}/.local/bin" LINK_PATH="${LOCAL_BIN}/iai-mcp" TARGET="${REPO_ROOT}/.venv/bin/iai-mcp" if [ -e "${LINK_PATH}" ] && [ ! -L "${LINK_PATH}" ]; then warn "${LINK_PATH} exists as a regular file — skipping symlink refresh" else mkdir -p "${LOCAL_BIN}" ln -sf "${TARGET}" "${LINK_PATH}" ok "${LINK_PATH} -> ${TARGET}" fi # --------------------------------------------------------------------------- # 5. Daemon (restart only if currently running; plist drift advisory) # --------------------------------------------------------------------------- step "daemon lifecycle" IAI_MCP="${REPO_ROOT}/.venv/bin/iai-mcp" # Check template drift using a python one-liner (avoids shell grep, which is # hook-blocked in this repo's dev env). TEMPLATE_CHECK="$("${VENV_PY}" - <<'PY' import pathlib, sys home = pathlib.Path.home() installed = home / "Library/LaunchAgents/com.iai-mcp.daemon.plist" template = pathlib.Path.cwd() / "deploy/launchd/com.iai-mcp.daemon.plist" if not installed.exists() or not template.exists(): print("none"); sys.exit(0) # Substitute USERNAME placeholder and compare env-var + args payload. rendered = template.read_text().replace("{USERNAME}", home.name) a_env = "IAI_MCP_STORE" in installed.read_text() and home.as_posix() + "/.iai-mcp" in installed.read_text() b_env = "IAI_MCP_STORE" in rendered and home.as_posix() + "/.iai-mcp" in rendered print("drift" if a_env != b_env else "same") PY )" if [ "${TEMPLATE_CHECK}" = "drift" ]; then warn "launchd plist template drift detected" warn "run: '${IAI_MCP} daemon uninstall --yes && ${IAI_MCP} daemon install --yes' to pick up the new plist" fi if "${IAI_MCP}" daemon status >/dev/null 2>&1; then # daemon status exits 0 only when running "${IAI_MCP}" daemon stop >/dev/null 2>&1 || true sleep 2 "${IAI_MCP}" daemon start >/dev/null 2>&1 || warn "daemon start returned non-zero; check 'iai-mcp daemon status'" ok "daemon restarted on new code" else ok "daemon not running — nothing to restart" fi # --------------------------------------------------------------------------- # 5. Summary # --------------------------------------------------------------------------- step "done" if [ "${NOOP}" = "1" ]; then ok "no-op — everything already current" else ok "updated to $(git rev-parse --short HEAD)" echo git log --oneline "${BEFORE}..${AFTER}" fi