#!/usr/bin/env bash
# Probe0 CLI installer.
#
#   curl -fsSL https://probe0.xyz/install | bash
#
# Installs and turns on Probe0 from the terminal. No app and no signing:
#   1. the `probe0` CLI into ~/.probe0 (no sudo)
#   2. a local root certificate, trusted in your login keychain (one password
#      prompt on macOS) so agents accept Probe0's TLS
#   3. the Probe0 block in your shell profile, so agents started from a new
#      terminal route through it (it only applies while Probe0 is running)
#   4. the daemon, kept running by a user LaunchAgent (macOS) or a systemd
#      user service (Linux), with the dashboard at http://127.0.0.1:8789
#   5. `probe0 bench`: what your Claude Code history cost, and what Probe0
#      would have saved on it
#
# A Probe0 account is required: right after the CLI is installed, your browser
# opens to sign in with Google or GitHub (`probe0 login`). Nothing else is set
# up until that finishes. Already signed in on this machine? It is skipped.
#
# This installs the terminal command only, never the Mac app. The Mac app runs
# this same script in the background to add the `probe0` command
# (PROBE0_FROM_APP=1); the app keeps running the proxy and has its own sign-in.
#
# Undo everything: probe0 uninstall   (add --purge to delete recorded data)
#
# Uses your Node.js if it is 22.14+ or 24+; otherwise downloads a private copy of
# Node 22 into ~/.probe0/runtime (checksum-verified), leaving your system alone.
#
# Environment:
#   PROBE0_NO_BENCH=1        skip running `probe0 bench` at the end
#   PROBE0_BENCH_ONLY=1      install the CLI and run bench, but do not turn the proxy on
#   PROBE0_NO_TRUST=1        skip trusting the certificate (agents on Node still work)
#   PROBE0_NO_MODIFY_PATH=1  do not add ~/.probe0/bin to your shell profile
#   PROBE0_CLI_URL=...       install from a different tarball (testing)
#   PROBE0_NO_TELEMETRY=1    do not send the anonymous install count (DO_NOT_TRACK=1 works too)
#   PROBE0_NO_AUTO_UPDATE=1  do not let the daemon update the CLI by itself
#   PROBE0_FROM_APP=1        set by the Mac app: CLI only, no sign-in, the app runs the proxy
#
# Re-running this is also how you update: it installs the newest CLI over the
# old one, keeps your data, and refreshes the shell block. Installed daemons
# also update themselves (probe0 update --auto off to stop that).
#
# Privacy: at the end this sends one anonymous count (a random install id made
# here, OS, CPU arch, CLI version, and how the install ended). Never anything
# about you or your code. The daemon then sends per-day request counts; turn it
# all off with: probe0 telemetry off
set -euo pipefail

# Each CLI build is its own release asset, so GitHub download counts add up per
# version instead of resetting when a file is replaced.
CLI_VERSION="0.1.15"
CLI_URL="${PROBE0_CLI_URL:-https://github.com/shlok-madhekar/probe0-releases/releases/download/cli-v${CLI_VERSION}/probe0-cli-${CLI_VERSION}.tgz}"
TELEMETRY_URL="https://bybilsridrakqdrydzwz.supabase.co/rest/v1/rpc/probe0_cli_install"
TELEMETRY_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ5Ymlsc3JpZHJha3Fkcnlkend6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjM1NTQ2MzYsImV4cCI6MjA3OTEzMDYzNn0.mIaSDEIoFil3x4sAtreMSPXIOn98tiQ7zil7eL0AfQA"
NODE_LINE="22"
P0_HOME="${HOME}/.probe0"
BIN_DIR="${P0_HOME}/bin"
CLI_DIR="${P0_HOME}/cli"
RUNTIME_DIR="${P0_HOME}/runtime"

if [ -t 1 ]; then
  BOLD=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GREEN=$'\033[32m'; RESET=$'\033[0m'
else
  BOLD=""; DIM=""; RED=""; GREEN=""; RESET=""
fi
say() { printf '%s\n' "$*"; }
step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; }
die() { printf '%serror:%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }

# Runs a command with a spinner and a label, so a quiet step does not look hung.
# Output of the command itself is discarded; its exit code is kept.
spin() {
  local label="$1"; shift
  if [ ! -t 1 ]; then
    say "    ${label}"
    "$@" >/dev/null 2>&1
    return $?
  fi
  "$@" >/dev/null 2>&1 &
  local pid=$! i=0 frames='|/-\' started=$SECONDS
  while kill -0 "$pid" 2>/dev/null; do
    printf '\r    %s %s %s(%ss)%s' "${frames:i%4:1}" "$label" "$DIM" "$((SECONDS - started))" "$RESET"
    i=$((i + 1))
    sleep 0.1
  done
  printf '\r\033[2K'
  wait "$pid"
}

# Runs a command for at most $1 seconds. Returns 124 if it had to be stopped.
with_timeout() {
  local secs="$1"; shift
  "$@" &
  local pid=$! ticks=0
  while kill -0 "$pid" 2>/dev/null; do
    if [ "$ticks" -ge $((secs * 2)) ]; then
      kill "$pid" 2>/dev/null || true
      wait "$pid" 2>/dev/null || true
      return 124
    fi
    sleep 0.5
    ticks=$((ticks + 1))
  done
  wait "$pid"
}

need() { command -v "$1" >/dev/null 2>&1 || die "this installer needs '$1'"; }
need curl
need tar
need uname

# ---------------------------------------------------------------- platform --
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
  Darwin) node_os="darwin" ;;
  Linux) node_os="linux" ;;
  *) die "unsupported OS: $os (macOS and Linux only for now)" ;;
esac
case "$arch" in
  arm64 | aarch64) node_arch="arm64" ;;
  x86_64 | amd64) node_arch="x64" ;;
  *) die "unsupported CPU: $arch" ;;
esac

say ""
say "${BOLD}probe0${RESET} ${DIM}installer${RESET}"
say ""

mkdir -p "$P0_HOME" "$BIN_DIR"
chmod 700 "$P0_HOME" 2>/dev/null || true

# -------------------------------------------------------------------- node --
node_ok() {
  local bin="$1"
  [ -x "$bin" ] || return 1
  local ver major minor
  ver="$("$bin" -p 'process.versions.node' 2>/dev/null)" || return 1
  major="${ver%%.*}"; minor="${ver#*.}"; minor="${minor%%.*}"
  case "$major$minor" in '' | *[!0-9]*) return 1 ;; esac
  # better-sqlite3's prebuilt binary segfaults on Node 22.0-22.13 and every
  # 23.x (verified 2026-10-29), so only 22.14+ or 24+ count as usable.
  [ "$major" -ge 24 ] || { [ "$major" -eq 22 ] && [ "$minor" -ge 14 ]; }
}

NODE_BIN=""
if command -v node >/dev/null 2>&1 && node_ok "$(command -v node)"; then
  NODE_BIN="$(command -v node)"
  step "Using your Node.js $("$NODE_BIN" -v)"
elif node_ok "${RUNTIME_DIR}/node/bin/node"; then
  NODE_BIN="${RUNTIME_DIR}/node/bin/node"
  step "Using Probe0's Node.js $("$NODE_BIN" -v)"
else
  step "Downloading Node.js ${NODE_LINE} for Probe0 (yours is missing or older than 22.14)"
  base="https://nodejs.org/dist/latest-v${NODE_LINE}.x"
  sums="$(curl -fsSL "${base}/SHASUMS256.txt")" || die "could not reach nodejs.org"
  file="$(printf '%s\n' "$sums" | awk '{print $2}' | grep -E "^node-v[0-9.]+-${node_os}-${node_arch}\.tar\.gz$" | head -n1)"
  [ -n "$file" ] || die "no Node.js ${NODE_LINE} build for ${node_os}-${node_arch}"
  want="$(printf '%s\n' "$sums" | awk -v f="$file" '$2 == f {print $1}')"
  tmp="$(mktemp -d)"
  trap 'rm -rf "$tmp"' EXIT
  if [ -t 2 ]; then
    curl -fL --progress-bar "${base}/${file}" -o "${tmp}/${file}" || die "Node.js download failed"
  else
    curl -fsSL "${base}/${file}" -o "${tmp}/${file}" || die "Node.js download failed"
  fi
  if command -v shasum >/dev/null 2>&1; then
    got="$(shasum -a 256 "${tmp}/${file}" | awk '{print $1}')"
  else
    got="$(sha256sum "${tmp}/${file}" | awk '{print $1}')"
  fi
  [ "$got" = "$want" ] || die "Node.js checksum mismatch; not installing"
  rm -rf "${RUNTIME_DIR}/node"
  mkdir -p "${RUNTIME_DIR}"
  tar -xzf "${tmp}/${file}" -C "$tmp"
  mv "${tmp}/${file%.tar.gz}" "${RUNTIME_DIR}/node"
  NODE_BIN="${RUNTIME_DIR}/node/bin/node"
  node_ok "$NODE_BIN" || die "downloaded Node.js does not run"
  say "    ${DIM}installed $("$NODE_BIN" -v) to ~/.probe0/runtime/node${RESET}"
fi

NODE_DIR="$(dirname "$NODE_BIN")"
NPM_CLI=""
for candidate in "${NODE_DIR}/../lib/node_modules/npm/bin/npm-cli.js" "$(dirname "$(readlink -f "$NODE_BIN" 2>/dev/null || echo "$NODE_BIN")")/../lib/node_modules/npm/bin/npm-cli.js"; do
  if [ -f "$candidate" ]; then NPM_CLI="$candidate"; break; fi
done
run_npm() {
  if [ -n "$NPM_CLI" ]; then
    "$NODE_BIN" "$NPM_CLI" "$@"
  elif command -v npm >/dev/null 2>&1; then
    PATH="${NODE_DIR}:${PATH}" npm "$@"
  else
    die "npm not found next to Node.js"
  fi
}

# --------------------------------------------------------------------- cli --
previous=""
if [ -f "${CLI_DIR}/node_modules/probe0/package.json" ]; then
  previous="$("$NODE_BIN" -p 'require(process.argv[1]).version' "${CLI_DIR}/node_modules/probe0/package.json" 2>/dev/null || true)"
fi
if [ -n "$previous" ] && [ "$previous" != "$CLI_VERSION" ]; then
  step "Updating the probe0 CLI (${previous} -> ${CLI_VERSION})"
elif [ -n "$previous" ]; then
  step "Reinstalling the probe0 CLI ${CLI_VERSION}"
else
  step "Installing the probe0 CLI"
fi
mkdir -p "$CLI_DIR"
[ -f "${CLI_DIR}/package.json" ] || printf '{"private":true}\n' > "${CLI_DIR}/package.json"
# The CLI's own PATH puts this Node first, so native modules build for it.
npm_install() {
  NPM_CONFIG_UPDATE_NOTIFIER=false PATH="${NODE_DIR}:${PATH}" run_npm install --prefix "$CLI_DIR" --no-audit --no-fund --loglevel=error \
    --omit=dev "$CLI_URL"
}
spin "downloading and installing (usually 5-15s)" npm_install || {
  say "    ${RED}npm install failed${RESET}; the output follows"
  npm_install
  die "npm install failed"
}

PKG="${CLI_DIR}/node_modules/probe0"
ENTRY="${PKG}/dist/index.js"
[ -f "$ENTRY" ] || die "install finished but ${ENTRY} is missing"

cat > "${BIN_DIR}/probe0" <<EOF
#!/bin/sh
# Installed by https://probe0.xyz/install
export PROBE0_UI_DIR="\${PROBE0_UI_DIR:-${PKG}/ui}"
exec "${NODE_BIN}" "${ENTRY}" "\$@"
EOF
chmod 755 "${BIN_DIR}/probe0"
version="$("${BIN_DIR}/probe0" --version)" || die "probe0 does not run"
say "    ${DIM}probe0 ${version} -> ~/.probe0/bin/probe0${RESET}"

# -------------------------------------------------------------------- PATH --
on_path=0
case ":${PATH}:" in *":${BIN_DIR}:"*) on_path=1 ;; esac

if [ "$on_path" = 0 ] && [ "${PROBE0_NO_MODIFY_PATH:-}" != "1" ]; then
  shell_name="$(basename "${SHELL:-}")"
  line='export PATH="$HOME/.probe0/bin:$PATH"'
  case "$shell_name" in
    zsh) profile="${ZDOTDIR:-$HOME}/.zshrc" ;;
    bash) if [ "$os" = "Darwin" ]; then profile="$HOME/.bash_profile"; else profile="$HOME/.bashrc"; fi ;;
    fish) profile="$HOME/.config/fish/conf.d/probe0-path.fish"; line='fish_add_path -g $HOME/.probe0/bin' ;;
    *) profile="$HOME/.profile" ;;
  esac
  mkdir -p "$(dirname "$profile")"
  if ! grep -qs '.probe0/bin' "$profile"; then
    printf '\n# probe0 CLI\n%s\n' "$line" >> "$profile"
    say "    ${DIM}added ~/.probe0/bin to PATH in ${profile/#$HOME/~}${RESET}"
  fi
fi

P0="${BIN_DIR}/probe0"

# The Mac app embeds the same daemon on the same ports. Two of them would
# fight over 8788, so if the app is here (or this run came from the app), it
# stays in charge and this only adds the `probe0` command.
app_owns_it=0
if [ "${PROBE0_FROM_APP:-}" = "1" ]; then
  app_owns_it=1
elif [ "${PROBE0_IGNORE_APP:-}" != "1" ] && [ "$os" = "Darwin" ] && { [ -d "/Applications/Probe0.app" ] || [ -d "$HOME/Applications/Probe0.app" ]; }; then
  app_owns_it=1
fi

# ------------------------------------------------------------------ sign in --
# Required for a terminal install. The app has its own sign-in, so a run the
# app started skips this.
signed_in=0
if [ "${PROBE0_FROM_APP:-}" = "1" ]; then
  signed_in=1
elif "$P0" whoami --json 2>/dev/null | grep -q '"signedIn": *true'; then
  signed_in=1
  who="$("$P0" whoami --json 2>/dev/null | "$NODE_BIN" -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const u=JSON.parse(s).user||{};process.stdout.write(u.email||u.name||"")}catch{}})' || true)"
  say "    ${DIM}signed in${who:+ as ${who}}${RESET}"
else
  say ""
  step "Sign in to Probe0"
  say "    A Probe0 account is required. Your browser is opening to sign in with Google or GitHub."
  if "$P0" login </dev/null; then
    signed_in=1
  fi
fi

send_install_count() {
  # One anonymous ping so terminal installs show up in the user count. Never
  # blocks or fails the install.
  local mode="$1" proxy_ok="$2" app_present="$3"
  local telemetry_off=0
  case "${PROBE0_NO_TELEMETRY:-}" in "" | 0 | false) ;; *) telemetry_off=1 ;; esac
  case "${DO_NOT_TRACK:-}" in "" | 0 | false) ;; *) telemetry_off=1 ;; esac
  if grep -qs '"enabled": *false' "${P0_HOME}/telemetry.json"; then telemetry_off=1; fi
  [ "$telemetry_off" = 0 ] || return 0
  local install_id=""
  [ -f "${P0_HOME}/install-id" ] && install_id="$(tr -d '[:space:]' < "${P0_HOME}/install-id" 2>/dev/null || true)"
  case "$install_id" in
    [0-9a-f]*-*-*-*-*) ;;
    *)
      install_id="$("$NODE_BIN" -p 'require("crypto").randomUUID()' 2>/dev/null || true)"
      [ -n "$install_id" ] && { printf '%s\n' "$install_id" > "${P0_HOME}/install-id"; chmod 600 "${P0_HOME}/install-id" 2>/dev/null || true; }
      ;;
  esac
  local channel="${PROBE0_TELEMETRY_CHANNEL:-stable}"
  [ "${PROBE0_FROM_APP:-}" = "1" ] && channel="app"
  if [ -n "$install_id" ]; then
    curl -fsS -m 5 -o /dev/null -X POST "$TELEMETRY_URL" \
      -H "apikey: ${TELEMETRY_KEY}" -H "Authorization: Bearer ${TELEMETRY_KEY}" \
      -H "Content-Type: application/json" \
      -d "{\"p_install_id\":\"${install_id}\",\"p_os\":\"${node_os}\",\"p_arch\":\"${node_arch}\",\"p_cli_version\":\"${version}\",\"p_mode\":\"${mode}\",\"p_proxy_ok\":${proxy_ok},\"p_app_present\":${app_present},\"p_channel\":\"${channel}\"}" \
      >/dev/null 2>&1 || true
  fi
}

if [ "$signed_in" != 1 ]; then
  send_install_count login_required false "$([ "$app_owns_it" = 1 ] && echo true || echo false)"
  say ""
  say "${RED}Sign-in did not finish, so Probe0 is not turned on.${RESET}"
  say "The probe0 command is installed, nothing else was changed. To finish:"
  say "  ${BOLD}~/.probe0/bin/probe0 login${RESET}   then run the installer again:"
  say "  curl -fsSL https://probe0.xyz/install | bash"
  exit 1
fi

PROXY_PORT="${PROBE0_PROXY_PORT:-8788}"
API_PORT="${PROBE0_API_PORT:-8789}"
SERVICE_LABEL="xyz.probe0.cli"
API_URL="http://127.0.0.1:${API_PORT}"

# Is anything other than our own daemon listening on a port?
port_taken() {
  local port="$1"
  if command -v lsof >/dev/null 2>&1; then
    lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
  else
    (exec 3<>"/dev/tcp/127.0.0.1/${port}") >/dev/null 2>&1
  fi
}

# ------------------------------------------------------------------ proxy --
activated=0
if [ "${PROBE0_BENCH_ONLY:-}" != "1" ]; then
  if [ "$app_owns_it" = 1 ]; then
    say ""
    say "    ${DIM}The Probe0 app is installed and runs the proxy itself; leaving it in charge.${RESET}"
  else
    step "Setting up the proxy"
    # Stop any daemon an earlier run of this installer started, so the port
    # check below only sees programs that are not ours.
    if [ "$os" = "Darwin" ]; then
      launchctl bootout "gui/$(id -u)/${SERVICE_LABEL}" >/dev/null 2>&1 || true
    elif command -v systemctl >/dev/null 2>&1; then
      systemctl --user stop probe0.service >/dev/null 2>&1 || true
    fi
    "$P0" stop >/dev/null 2>&1 || true
    sleep 1

    # Two proxies on one port is how agent traffic breaks. If something else
    # already holds Probe0's ports (the Mac app, or anything at all), stop here
    # and change nothing about how the machine routes traffic.
    for p in "$PROXY_PORT" "$API_PORT"; do
      if port_taken "$p"; then
        say ""
        say "    ${RED}Port ${p} is already in use by another program.${RESET}"
        say "    Probe0 needs ports ${PROXY_PORT} and ${API_PORT}. If the Probe0 app is running, it already"
        say "    does the proxy work; otherwise close whatever is using port ${p} and re-run."
        say "    Nothing about your agents or shell was changed. The probe0 CLI is installed."
        port_conflict=1
        PROBE0_BENCH_ONLY=1
        break
      fi
    done
  fi
fi

if [ "${PROBE0_BENCH_ONLY:-}" != "1" ] && [ "$app_owns_it" = 0 ]; then
  if true; then
    "$P0" init --quiet >/dev/null || die "probe0 init failed (run: ~/.probe0/bin/probe0 init --verbose)"
    if [ "$PROXY_PORT" != 8788 ] || [ "$API_PORT" != 8789 ]; then
      "$NODE_BIN" -e '
        const fs = require("fs"); const f = process.argv[1];
        const c = JSON.parse(fs.readFileSync(f, "utf8"));
        c.server = { ...(c.server || {}), proxyPort: +process.argv[2], apiPort: +process.argv[3] };
        fs.writeFileSync(f, JSON.stringify(c, null, 2));' "${P0_HOME}/config.json" "$PROXY_PORT" "$API_PORT" ||
        die "could not set ports in ~/.probe0/config.json"
    fi
    say "    ${DIM}local certificate and database in ~/.probe0${RESET}"

    # Route Claude Code everywhere: VS Code / Cursor extensions, the Claude app
    # and terminals that are already open, not only new terminals. On by
    # default for macOS terminal installs (the crash guard is a LaunchAgent).
    # Applied once per machine: if the user turns it off later, re-running the
    # installer leaves it off. Opt out up front with PROBE0_NO_CLAUDE_ROUTE=1.
    route_marker="${P0_HOME}/.claude-route-default"
    if [ "$os" = "Darwin" ] && [ "${PROBE0_NO_CLAUDE_ROUTE:-}" != "1" ] && [ ! -e "$route_marker" ]; then
      if "$NODE_BIN" -e '
        const fs = require("fs"); const f = process.argv[1];
        const c = JSON.parse(fs.readFileSync(f, "utf8"));
        c.integrations = c.integrations || {};
        c.integrations.claudeCode = { ...(c.integrations.claudeCode || {}), routeAllSessions: true };
        fs.writeFileSync(f, JSON.stringify(c, null, 2));' "${P0_HOME}/config.json" 2>/dev/null; then
        : > "$route_marker"
        say "    ${DIM}Claude Code routed everywhere: terminals, VS Code / Cursor, the Claude app${RESET}"
      else
        say "    ${DIM}could not turn on routing for all Claude Code sessions; new terminals still work${RESET}"
      fi
    fi

    # Trust: one macOS password prompt (login keychain). Agents built on Node
    # (Claude Code) also get the certificate through NODE_EXTRA_CA_CERTS, so a
    # skipped or refused prompt degrades rather than breaks.
    if [ "${PROBE0_NO_TRUST:-}" != "1" ]; then
      trust_failed=0
      trust_timed_out=0
      if "$P0" trust-ca --check --json 2>/dev/null | grep -q '"trusted": *true'; then
        say "    ${DIM}certificate already trusted${RESET}"
      else
        trust_run() {
          if [ "$os" = "Darwin" ]; then
            if [ -r /dev/tty ]; then
              "$P0" trust-ca --yes --scope user </dev/tty >/dev/null 2>&1
            else
              "$P0" trust-ca --yes --scope user >/dev/null 2>&1
            fi
          else
            [ -r /dev/tty ] || return 1
            "$P0" trust-ca --yes </dev/tty >/dev/null 2>&1
          fi
        }
        if [ "$os" = "Darwin" ]; then
          say ""
          say "    ${BOLD}A macOS password popup is opening now${RESET} to trust the Probe0 certificate."
          say "    It can open behind other windows; check the Dock if you do not see it."
          say "    ${DIM}Waiting up to 2 minutes, then continuing without it...${RESET}"
        else
          say "    Enter your password if asked, to trust the Probe0 certificate (up to 2 minutes)."
        fi
        with_timeout 120 trust_run || {
          rc=$?
          trust_failed=1
          [ "$rc" = 124 ] && trust_timed_out=1
        }
        if [ "$trust_failed" = 1 ]; then
          if [ "$trust_timed_out" = 1 ]; then
            say "    ${DIM}no answer to the password popup, skipped it (Claude Code still works); later: probe0 trust-ca${RESET}"
          else
            say "    ${DIM}certificate not trusted system-wide (Claude Code still works); retry: probe0 trust-ca${RESET}"
          fi
        else
          say "    ${DIM}certificate trusted${RESET}"
        fi
      fi
    fi

    # Shell block: new terminals route agents through Probe0 while it runs.
    shell_name="$(basename "${SHELL:-}")"
    case "$shell_name" in
      zsh) block_shell=zsh; block_file="${ZDOTDIR:-$HOME}/.zshrc" ;;
      bash) block_shell=bash; if [ "$os" = "Darwin" ]; then block_file="$HOME/.bash_profile"; else block_file="$HOME/.bashrc"; fi ;;
      fish) block_shell=fish; block_file="$HOME/.config/fish/config.fish" ;;
      *) block_shell=bash; block_file="$HOME/.profile" ;;
    esac
    mkdir -p "$(dirname "$block_file")"
    # Adds the block, or replaces an older one so fixes reach existing installs.
    block_outcome="$("$P0" env --write-profile --shell "$block_shell" --proxy-port "$PROXY_PORT")" ||
      die "could not write the shell block to ${block_file}"
    case "$block_outcome" in
      added) say "    ${DIM}routing block added to ${block_file/#$HOME/~}${RESET}" ;;
      updated) say "    ${DIM}routing block in ${block_file/#$HOME/~} updated (old file kept as .probe0.bak)${RESET}" ;;
      *) say "    ${DIM}routing block in ${block_file/#$HOME/~} is current${RESET}" ;;
    esac

    # Keep the daemon running across logouts and reboots.
    mkdir -p "${P0_HOME}/logs"
    if [ "$os" = "Darwin" ]; then
      label="$SERVICE_LABEL"
      plist="$HOME/Library/LaunchAgents/${label}.plist"
      mkdir -p "$HOME/Library/LaunchAgents"
      launchctl bootout "gui/$(id -u)/${label}" >/dev/null 2>&1 || true
      cat > "$plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>${label}</string>
  <key>ProgramArguments</key>
  <array><string>${P0}</string><string>start</string></array>
  <key>EnvironmentVariables</key><dict><key>HOME</key><string>${HOME}</string></dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>ThrottleInterval</key><integer>10</integer>
  <key>StandardOutPath</key><string>${P0_HOME}/logs/launchd.log</string>
  <key>StandardErrorPath</key><string>${P0_HOME}/logs/launchd.log</string>
</dict>
</plist>
PLIST
      launchctl bootstrap "gui/$(id -u)" "$plist" 2>/dev/null ||
        launchctl load -w "$plist" 2>/dev/null ||
        "$P0" start --detach --quiet >/dev/null 2>&1 || true
    elif command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
      unit_dir="$HOME/.config/systemd/user"
      mkdir -p "$unit_dir"
      cat > "${unit_dir}/probe0.service" <<UNIT
[Unit]
Description=Probe0 proxy for AI coding agents

[Service]
ExecStart=${P0} start
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
UNIT
      systemctl --user daemon-reload
      systemctl --user enable --now probe0.service >/dev/null 2>&1 ||
        "$P0" start --detach --quiet >/dev/null 2>&1 || true
    else
      "$P0" start --detach --quiet >/dev/null 2>&1 || true
    fi

    # Wait for it to answer.
    say "    ${DIM}starting the daemon...${RESET}"
    for _ in $(seq 1 40); do
      if curl -fsS -o /dev/null "${API_URL}/api/health" 2>/dev/null || curl -fsS -o /dev/null "${API_URL}/" 2>/dev/null; then
        activated=1
        break
      fi
      sleep 0.5
    done
    if [ "$activated" = 1 ]; then
      say "    ${DIM}daemon running (proxy 127.0.0.1:${PROXY_PORT}, dashboard ${API_URL})${RESET}"
      # Prove interception end to end: an HTTPS call to Anthropic through the
      # proxy, verified against Probe0's own certificate. Any HTTP status (401
      # without a key) means the TLS handshake with Probe0 worked.
      code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 15 \
        -x "http://127.0.0.1:${PROXY_PORT}" --cacert "${P0_HOME}/ca/probe0-ca.crt" \
        https://api.anthropic.com/v1/models 2>/dev/null || true)"
      if [ -n "$code" ] && [ "$code" != "000" ]; then
        say "    ${DIM}self-test: HTTPS to api.anthropic.com through Probe0 works (HTTP ${code})${RESET}"
      else
        say "    ${RED}self-test failed${RESET}: HTTPS through the proxy did not complete; run ~/.probe0/bin/probe0 doctor"
        activated=0
      fi
    else
      say "    ${RED}the daemon did not come up${RESET}; see: ~/.probe0/bin/probe0 doctor  and  ~/.probe0/bin/probe0 logs"
    fi
    # Never leave a half-working proxy behind: if it is not verified, stop the
    # service so nothing keeps running on these ports.
    if [ "$activated" != 1 ]; then
      if [ "$os" = "Darwin" ]; then
        launchctl bootout "gui/$(id -u)/${SERVICE_LABEL}" >/dev/null 2>&1 || true
        rm -f "$HOME/Library/LaunchAgents/${SERVICE_LABEL}.plist"
      elif command -v systemctl >/dev/null 2>&1; then
        systemctl --user disable --now probe0.service >/dev/null 2>&1 || true
      fi
      "$P0" stop >/dev/null 2>&1 || true
      say "    ${DIM}stopped it again so nothing is left running; the shell block only applies while Probe0 runs${RESET}"
    fi
  fi
fi

say ""
say "${GREEN}Installed.${RESET}"

# --------------------------------------------------------------- count it --
if [ "${port_conflict:-0}" = 1 ]; then mode="port_conflict"
elif [ "$app_owns_it" = 1 ]; then mode="app_owns"
elif [ "${PROBE0_BENCH_ONLY:-}" = "1" ]; then mode="bench_only"
elif [ "$activated" = 1 ]; then mode="full"
else mode="proxy_failed"; fi
send_install_count "$mode" "$([ "$activated" = 1 ] && echo true || echo false)" "$([ "$app_owns_it" = 1 ] && echo true || echo false)"

# ------------------------------------------------------------------- bench --
# An update skips it (it was shown on the first install); `probe0 bench` any time.
if [ "${PROBE0_NO_BENCH:-}" != "1" ] && { [ -z "$previous" ] || [ "${PROBE0_BENCH_ONLY:-}" = "1" ]; }; then
  say ""
  step "probe0 bench: your last 7 days of Claude Code, priced"
  say "    ${DIM}read-only; probe0 bench --days 30 for a longer window${RESET}"
  "$P0" bench --days 7 || true
fi

say "Next:"
if [ "$activated" = 1 ]; then
  if grep -qs '"routeAllSessions": *true' "${P0_HOME}/config.json"; then
    say "  ${BOLD}use Claude Code as usual${RESET}, anywhere (sessions open right now switch over in a few seconds)"
    say "  ${BOLD}open a new terminal${RESET} for Codex and other agents"
  else
    say "  ${BOLD}open a new terminal${RESET}, then use Claude Code / Codex as usual; Probe0 is in the path"
  fi
  say "  probe0 ui                  open the dashboard (${API_URL})"
  say "  probe0 status              what it is doing right now"
elif [ "$app_owns_it" = 1 ]; then
  say "  open the Probe0 app to manage the proxy"
fi
if [ "$on_path" = 0 ]; then
  say "  ${DIM}(the probe0 command works in new terminals; or run: export PATH=\"\$HOME/.probe0/bin:\$PATH\")${RESET}"
fi
say "  probe0 bench               what your Claude Code history cost"
say "  probe0 doctor              check everything"
say "  ${DIM}probe0 updates itself in the background (probe0 update --auto off to stop)${RESET}"
say "  probe0 uninstall           remove it all"
say ""
