#!/usr/bin/env bash
#
# BeeOS CLI Installer (macOS / Linux)
#
# Usage:
#   curl -fsSL https://beeos.ai/install | bash
#   curl -fsSL https://beeos.ai/install | bash -s -- [beeos init args]
#   curl -fsSL https://beeos.ai/install | bash -s -- --device   # → beeos device attach
#
# Responsibilities (deliberately minimal — framework-agnostic):
#   1. Detect OS + CPU architecture, refuse unsupported targets early.
#   2. Make sure Node.js >= MIN_NODE_VERSION is available (install via
#      nvm / fnm / brew when possible). All heavier dependencies (adb,
#      scrcpy-bridge, the device-agent suite) are installed lazily by
#      the CLI itself when the user actually needs them.
#   3. `npm install -g @beeos-ai/cli`.
#   4. `exec beeos init` (or `beeos device attach` with `--device`).
#
# Anything else — "you already have BeeOS installed, what now?",
# framework selection, rebind variants — lives inside the CLI's own
# `beeos init` menu, not here. The bootstrap script stays small so it's
# easy to maintain and easy to diagnose on a customer machine.
#
# ── Exit codes ────────────────────────────────────────────────────
#   0   Success (or `exec` handed off — the CLI's own exit code wins)
#   1   Node bootstrap failure (Node missing / version too old / npm
#       not on PATH after Node install)
#   2   Unsupported OS / CPU architecture (and user declined override)
#   3   `npm install -g @beeos-ai/cli` failed (CLI didn't reach PATH)
#
# Automation can branch on these — `1` is "fix your Node install",
# `2` is "wrong machine", `3` is "your npm prefix / proxy / disk
# (after EACCES auto-recovery already attempted, see
# `BEEOS_NO_NPM_PREFIX_FIX` below)".
#
# ── Optional env vars ─────────────────────────────────────────────
#   BEEOS_API_URL            Public API base (this script uses it for
#                            telemetry; the CLI uses it for runtime).
#                            Defaults to production.
#   BEEOS_AGENT_GATEWAY_URL  Agent Gateway base (forwarded as-is to the
#                            spawned CLI).
#   BEEOS_DASHBOARD_URL      Dashboard base (OAuth / bind redirect).
#   BEEOS_INSTALL_LOG        Path to a forensic log of Node-bootstrap
#                            stderr; defaults to /tmp/beeos-install-$$.log.
#   BEEOS_INSTALL_DRY_RUN=1  CI smoke-test escape hatch — return 0
#                            from main() before any side effect.
#   BEEOS_NO_TELEMETRY=1     Disable the script-level telemetry POSTs.
#   BEEOS_NO_DESKTOP=1       Skip the TigerVNC pre-check (Linux) and
#                            forwarded to `beeos init` so the CLI's
#                            `getDesktopPipeline()` chokepoint also
#                            short-circuits, suppressing every macOS /
#                            Linux cold-start side effect. Mirrors
#                            install.ps1's same-named env var.
#   BEEOS_USE_NPX=1          Power-user throwaway install (beeos won't
#                            persist on PATH).
#   BEEOS_NO_NPM_PREFIX_FIX=1 Disable Phase 3 EACCES auto-recovery.
#                            Recovery (default ON) detects "EACCES on
#                            lib/node_modules" in the npm log, switches
#                            the npm global prefix to ~/.npm-global,
#                            persists $HOME/.npm-global/bin on PATH via
#                            the user's shell rc (zsh -> ~/.zshrc, bash
#                            -> ~/.bash_profile on macOS or ~/.bashrc
#                            on Linux), and retries `npm install -g`
#                            exactly once. The shell rc edit is wrapped
#                            in a sentinel block (`# >>> beeos
#                            npm-prefix >>>` ... `# <<< beeos
#                            npm-prefix <<<`) so it can be reverted by
#                            removing the block AND running
#                            `npm config delete prefix`. Set this var
#                            to 1 to fall back to the legacy
#                            "hint + exit 3" behaviour.
#
# IMPORTANT (env inheritance):
#   BEEOS_API_URL / BEEOS_AGENT_GATEWAY_URL / BEEOS_DASHBOARD_URL must
#   be set in THE SHELL THAT RUNS THIS SCRIPT before the curl pipe.
#   They reach `beeos init` only via the normal exec env passthrough
#   — there is NO auto-injection from the curl arguments.
#
#   Working:    `BEEOS_API_URL=... bash -c 'curl ...| bash'`
#   Working:    Set the var in your shell profile, then run normally.
#   Broken:     `bash -c "BEEOS_API_URL=... curl ... | bash"`
#               (the var is local to the inner bash, not exported).
#
# Bare-metal staging one-liner:
#
#   BEEOS_API_URL=https://public-api-staging.beeos.ai \
#   BEEOS_AGENT_GATEWAY_URL=https://agent-gw-staging.beeos.ai \
#   BEEOS_DASHBOARD_URL=https://beeos-staging-web.vercel.app \
#     curl -fsSL https://beeos.ai/install | bash -s -- --version 1.0.18

set -euo pipefail

BOLD="\033[1m"
DIM="\033[2m"
RESET="\033[0m"
GREEN="\033[32m"
YELLOW="\033[33m"
RED="\033[31m"
CYAN="\033[36m"

CLI_PACKAGE="@beeos-ai/cli@latest"
MIN_NODE_VERSION=20
NVM_VERSION="v0.40.1"
FNM_INSTALL_URL="https://fnm.vercel.app/install"

# ── Mirror / registry overrides ───────────────────────────────────────────────
# BEEOS_NPM_REGISTRY        Force a specific npm registry for all npm install -g
#                           calls in this script. Skips the fallback logic.
# BEEOS_NPM_FALLBACK_REGISTRY
#                           Registry to retry when the primary registry fails
#                           with a network error (ETIMEDOUT / ENOTFOUND /
#                           ECONNREFUSED). Defaults to npmmirror.
# BEEOS_NVM_NODE_MIRROR     NVM_NODEJS_ORG_MIRROR value to use when nodejs.org
#                           is unreachable. Applied only when nvm install fails.
# BEEOS_FNM_NODE_MIRROR     FNM_NODE_DIST_MIRROR value to use when the primary
#                           Node download fails via fnm.
BEEOS_NPM_REGISTRY="${BEEOS_NPM_REGISTRY:-}"
BEEOS_NPM_FALLBACK_REGISTRY="${BEEOS_NPM_FALLBACK_REGISTRY:-https://registry.npmmirror.com}"
BEEOS_NVM_NODE_MIRROR="${BEEOS_NVM_NODE_MIRROR:-https://npmmirror.com/mirrors/node}"
BEEOS_FNM_NODE_MIRROR="${BEEOS_FNM_NODE_MIRROR:-https://npmmirror.com/mirrors/node}"

BEEOS_HOME="${BEEOS_HOME:-$HOME/.beeos}"
# Default telemetry endpoint. `$BEEOS_API_URL` lets developers aim at
# staging / a local gateway; empty-or-unset falls back to production.
BEEOS_API_URL="${BEEOS_API_URL:-https://public-api.beeos.ai}"

# Where the installer writes verbose stderr for Node auto-install.
# Overridable via `BEEOS_INSTALL_LOG=/path/to/file.log` so support can
# ask "run the one-liner with `BEEOS_INSTALL_LOG=~/beeos-install.log`
# and share the log" rather than asking the user to recreate the
# failure with manual `bash -x`.
#
# Pre-P1-12 we buried every `2>/dev/null` in `try_install_node`, which
# meant bug reports like "nvm install --lts failed" were unreproducible
# from anything but the user's terminal. We now preserve stderr in a
# per-PID file and reference it in the terminal hint on failure.
BEEOS_INSTALL_LOG="${BEEOS_INSTALL_LOG:-/tmp/beeos-install-$$.log}"
# Truncate on every run so the log reflects only the current attempt.
: > "$BEEOS_INSTALL_LOG" 2>/dev/null || true

info()    { printf "${BOLD}${CYAN}[beeos]${RESET} %s\n" "$*"; }
warn()    { printf "${BOLD}${YELLOW}[beeos]${RESET} %s\n" "$*"; }
error()   { printf "${BOLD}${RED}[beeos]${RESET} %s\n" "$*" >&2; }
success() { printf "${BOLD}${GREEN}[beeos]${RESET} %s\n" "$*"; }

# ── Telemetry (script-level, pre-CLI) ────────────────────────
#
# Mirrors `packages/cli/src/telemetry.ts` (the Node-side single
# source of truth for event names + payload shape) so we can capture
# failures that happen BEFORE the Node CLI even starts (e.g. Node
# install failure, unsupported arch). Add a new event in BOTH places
# or the dashboard query and the script will silently disagree. Fire-and-forget with a 2 s cap —
# we must never block a real install on a flaky telemetry endpoint.
#
# Event names used here:
#   - install.bootstrap.start        — script entered `main()`
#   - install.bootstrap.node_fail    — could not install/find Node
#   - install.bootstrap.arch_fail    — unsupported arch
#   - install.bootstrap.success      — about to exec `beeos`
#
# `BEEOS_NO_TELEMETRY=1` short-circuits everything. This matches the
# Node CLI's opt-out env var verbatim so a user who disabled telemetry
# for the CLI doesn't unexpectedly get events from the bootstrap.
# Minimal JSON string-value escaper. Maps the four characters that
# can break a `"..."` JSON literal to their backslash escapes:
#
#   \  ->  \\
#   "  ->  \"
#   <CR>  ->  \r
#   <LF>  ->  \n
#
# We deliberately don't pull in jq (not always available) or full
# JSON encoding — telemetry payload values are short slugs and human
# strings, not arbitrary binary. Tab + control-char escaping is
# nice-to-have but the four cases above are the ones that actually
# show up (e.g. `error_code` strings carrying quoted reasons, or
# error messages with embedded newlines).
json_escape() {
  printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' \
    | tr '\r' ' ' | tr '\n' ' '
}

send_telemetry() {
  local event="$1"
  local error_code="${2:-}"
  local success_flag="${3:-true}"

  if [[ "${BEEOS_NO_TELEMETRY:-}" == "1" ]]; then
    return 0
  fi
  if ! command -v curl &>/dev/null; then
    return 0
  fi

  local os_kind="${OS_KIND:-unknown}"
  local os_arch="${OS_ARCH:-unknown}"
  local node_ver=""
  if command -v node &>/dev/null; then
    node_ver=$(node -v 2>/dev/null | sed 's/^v//' || true)
  fi
  local host_hint
  host_hint="$(hostname 2>/dev/null || echo unknown)-${os_arch}"

  # 1.0.19: every value below goes through `json_escape` so a stray
  # `"` or newline in any of these fields can't break the POST body.
  # `success_flag` is a bare boolean literal — never user-controlled —
  # so it stays unquoted/unescaped.
  local ev_e os_e arch_e node_e ec_e host_e
  ev_e=$(json_escape "$event")
  os_e=$(json_escape "$os_kind")
  arch_e=$(json_escape "$os_arch")
  node_e=$(json_escape "$node_ver")
  ec_e=$(json_escape "$error_code")
  host_e=$(json_escape "$host_hint")

  # POST body kept deliberately small — matches Node CLI payload.
  # Note: `cli_version` is empty until after npx install; we fill it
  # only when the bootstrap telemetry is fired post-install, from the
  # Node side. For pre-install events it stays "" and that's fine.
  local body
  body=$(cat <<JSON
{"event":"${ev_e}","cli_version":"","dist_tag":"","os":"${os_e}","arch":"${arch_e}","node_version":"${node_e}","success":${success_flag},"error_code":"${ec_e}","host_hint":"${host_e}"}
JSON
)

  # `--max-time 2` caps the whole request at 2 s. `-o /dev/null -s`
  # silences output. Swallow non-zero exit — telemetry MUST NOT break
  # an install.
  curl -fsS -o /dev/null \
    --max-time 2 \
    -H "Content-Type: application/json" \
    -X POST \
    -d "$body" \
    "${BEEOS_API_URL%/}/api/v1/telemetry/install" \
    2>/dev/null || true
}

# ── Parse arguments ──────────────────────────────────────────

CLI_VERSION=""
MODE="init"               # "init" | "device"
PASSTHROUGH_ARGS=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --version)
      CLI_VERSION="$2"
      shift 2
      ;;
    --device)
      MODE="device"
      shift
      ;;
    --init)
      MODE="init"
      shift
      ;;
    *)
      PASSTHROUGH_ARGS+=("$1")
      shift
      ;;
  esac
done

if [[ -n "$CLI_VERSION" ]]; then
  CLI_PACKAGE="@beeos-ai/cli@${CLI_VERSION}"
fi

# ── OS / architecture detection ──────────────────────────────

OS_KIND=""       # darwin | linux | *
OS_ARCH=""       # x86_64 | arm64
LIBC_KIND=""     # gnu | musl (Linux only, empty otherwise)

detect_platform() {
  local uname_s uname_m
  uname_s=$(uname -s 2>/dev/null || echo "unknown")
  uname_m=$(uname -m 2>/dev/null || echo "unknown")

  case "$uname_s" in
    Darwin) OS_KIND="darwin" ;;
    Linux)  OS_KIND="linux"  ;;
    *)      OS_KIND="$uname_s" ;;
  esac

  case "$uname_m" in
    x86_64|amd64) OS_ARCH="x86_64" ;;
    aarch64|arm64) OS_ARCH="arm64" ;;
    *) OS_ARCH="$uname_m" ;;
  esac

  if [[ "$OS_KIND" == "linux" ]]; then
    if ldd --version 2>&1 | head -n1 | grep -qi "musl"; then
      LIBC_KIND="musl"
    else
      LIBC_KIND="gnu"
    fi
  fi

  info "detected: ${OS_KIND} ${OS_ARCH}${LIBC_KIND:+ (${LIBC_KIND})}"

  # Architecture policy (kept in sync with install.ps1):
  #   - Non-interactive (no TTY): hard exit 2. The user cannot see a
  #     warning scroll past, and we must not silently install a CLI
  #     that will later fail to download its native sidecars.
  #   - Interactive TTY: warn and ask for confirmation. Power users
  #     sometimes run on odd arches (RISC-V Linux, IBM z, early
  #     Windows-on-ARM) for experimentation; bail out by default, but
  #     let them override when they know what they're doing.
  local interactive=0
  if [[ -t 0 ]] || [[ -t 1 ]]; then interactive=1; fi
  # Inner helper functions — `local` doesn't apply to function
  # declarations in bash, so these are scoped via the closure-style
  # convention of "only callers inside this function see them" plus a
  # naming prefix so they don't collide with outer scope.
  arch_bail() {
    local reason="$1"
    error "$reason"
    send_telemetry "install.bootstrap.arch_fail" "$reason" false
    exit 2
  }
  arch_confirm() {
    local reason="$1"
    warn "$reason"
    if (( interactive == 0 )); then
      arch_bail "$reason"
    fi
    local yn=""
    read -rp "Continue anyway? [y/N]: " yn </dev/tty 2>/dev/null || yn=""
    case "${yn,,}" in
      y|yes) info "Proceeding on unsupported arch by user confirmation." ;;
      *)     arch_bail "$reason" ;;
    esac
  }

  case "$OS_KIND" in
    darwin)
      if [[ "$OS_ARCH" != "arm64" && "$OS_ARCH" != "x86_64" ]]; then
        arch_confirm "Unsupported macOS architecture: $OS_ARCH (need arm64 or x86_64)"
      fi
      ;;
    linux)
      if [[ "$OS_ARCH" != "arm64" && "$OS_ARCH" != "x86_64" ]]; then
        arch_confirm "Unsupported Linux architecture: $OS_ARCH (need arm64 or x86_64)"
      fi
      if [[ "$LIBC_KIND" == "musl" ]]; then
        # No musl prebuilt for scrcpy-bridge / vnc-bridge yet. The CLI
        # still works (agents run on top of Node) but some sidecars
        # will need to be built from source.
        warn "musl libc detected — some Rust sidecar binaries may be unavailable."
      fi
      ;;
    *)
      arch_bail "Unsupported OS: $OS_KIND (use install.ps1 for Windows)"
      ;;
  esac
}

# ── Node.js detection ────────────────────────────────────────

check_node() {
  if ! command -v node &>/dev/null; then
    return 1
  fi

  local version major
  version=$(node -v 2>/dev/null | sed 's/^v//')
  major=$(echo "$version" | cut -d. -f1)

  if [ "$major" -lt "$MIN_NODE_VERSION" ] 2>/dev/null; then
    warn "Node.js $version found, but v${MIN_NODE_VERSION}+ is required."
    return 1
  fi

  return 0
}

# ── Auto-install Node.js (nvm -> fnm -> brew) ────────────────

try_install_node() {
  info "Node.js not found or too old. Attempting automatic installation..."
  info "Verbose output appended to: ${BEEOS_INSTALL_LOG}"
  echo ""

  # Every step below redirects stderr to $BEEOS_INSTALL_LOG (append)
  # so that a post-mortem has the full picture. We deliberately do NOT
  # use bare `2>/dev/null` anymore — it's the single biggest source of
  # unactionable bug reports in this layer. Steps still short-circuit
  # on success to avoid N-second retries when one tool works.
  {
    echo "=== try_install_node @ $(date -u +%FT%TZ) ==="
    echo "OS_KIND=${OS_KIND} OS_ARCH=${OS_ARCH} LIBC_KIND=${LIBC_KIND:-}"
  } >> "$BEEOS_INSTALL_LOG" 2>&1 || true

  # 1. Existing nvm
  export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
  if [[ -s "$NVM_DIR/nvm.sh" ]]; then
    info "Loading existing nvm..."
    # shellcheck source=/dev/null
    . "$NVM_DIR/nvm.sh"
    if nvm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1; then
      success "Node.js $(node -v) installed via existing nvm"
      persist_node_manager_shell_init "nvm" || true
      return 0
    fi
    # nodejs.org unreachable — retry with npmmirror Node mirror.
    info "nvm install failed, retrying with Node mirror: ${BEEOS_NVM_NODE_MIRROR}"
    if NVM_NODEJS_ORG_MIRROR="$BEEOS_NVM_NODE_MIRROR" \
         nvm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1; then
      success "Node.js $(node -v) installed via existing nvm (mirror)"
      persist_node_manager_shell_init "nvm" || true
      return 0
    fi
  fi

  # 2. Fresh nvm install
  info "Installing nvm ${NVM_VERSION}..."
  if curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" 2>>"$BEEOS_INSTALL_LOG" \
        | bash >> "$BEEOS_INSTALL_LOG" 2>&1; then
    # shellcheck source=/dev/null
    . "$NVM_DIR/nvm.sh" >> "$BEEOS_INSTALL_LOG" 2>&1 || true
    if nvm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1; then
      success "Node.js $(node -v) installed via nvm"
      persist_node_manager_shell_init "nvm" || true
      return 0
    fi
    # Retry with mirror when nodejs.org is slow/blocked.
    info "nvm install failed, retrying with Node mirror: ${BEEOS_NVM_NODE_MIRROR}"
    if NVM_NODEJS_ORG_MIRROR="$BEEOS_NVM_NODE_MIRROR" \
         nvm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1; then
      success "Node.js $(node -v) installed via nvm (mirror)"
      persist_node_manager_shell_init "nvm" || true
      return 0
    fi
  fi

  # 3. fnm — works on locked-down Linux without git/curl quirks AND
  # on macOS where users have neither Homebrew nor an existing nvm
  # (the historical 1.0.18 path required brew on darwin, which is not
  # always available — corporate-managed Macs, fresh users).
  if [[ "$OS_KIND" == "linux" || "$OS_KIND" == "darwin" ]] \
     && ! command -v fnm &>/dev/null; then
    info "Trying fnm (single-user, no sudo)..."
    if curl -fsSL "$FNM_INSTALL_URL" 2>>"$BEEOS_INSTALL_LOG" \
          | bash -s -- --skip-shell >> "$BEEOS_INSTALL_LOG" 2>&1; then
      export PATH="$HOME/.local/share/fnm:$PATH"
      eval "$(fnm env --shell bash 2>>"$BEEOS_INSTALL_LOG")" || true
      if fnm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1 \
         && fnm use lts-latest >> "$BEEOS_INSTALL_LOG" 2>&1; then
        success "Node.js $(node -v) installed via fnm"
        persist_node_manager_shell_init "fnm" || true
        return 0
      fi
      # Retry with mirror when nodejs.org is slow/blocked.
      info "fnm install failed, retrying with Node mirror: ${BEEOS_FNM_NODE_MIRROR}"
      if FNM_NODE_DIST_MIRROR="$BEEOS_FNM_NODE_MIRROR" \
           fnm install --lts >> "$BEEOS_INSTALL_LOG" 2>&1 \
         && fnm use lts-latest >> "$BEEOS_INSTALL_LOG" 2>&1; then
        success "Node.js $(node -v) installed via fnm (mirror)"
        persist_node_manager_shell_init "fnm" || true
        return 0
      fi
    fi
  fi

  # 4. macOS Homebrew (already-installed brew only — we don't install
  # brew itself).
  if [[ "$OS_KIND" == "darwin" ]] && command -v brew &>/dev/null; then
    info "Trying Homebrew..."
    if brew install node >> "$BEEOS_INSTALL_LOG" 2>&1; then
      success "Node.js installed via Homebrew"
      return 0
    fi
  fi

  # 5. Linux distro package manager (last resort).
  #
  # nvm + fnm both depend on `curl` to fetch the Node tarball from
  # nodejs.org. Locked-down corporate networks frequently block
  # `nodejs.org` while still allowing the distro mirror, so a system
  # package install is the only way through. We deliberately put this
  # AFTER nvm/fnm because:
  #
  #   1. distro Node LTS versions lag — Debian stable was on Node 18
  #      well into the Node 20 era, breaking device-agent's engines
  #      requirement. nvm/fnm pull current LTS regardless.
  #   2. apt/dnf/pacman normally need root, which is a different
  #      security posture than user-scope nvm. We require sudo -n
  #      (non-interactive) to detect "user is already root or has
  #      passwordless sudo" — anything else falls through to the
  #      hint block.
  #
  # Operators on hardened systems can disable this branch entirely
  # with BEEOS_NO_PKG_MANAGER_NODE=1 and rely on their own provisioning.
  if [[ "$OS_KIND" == "linux" ]] \
     && [[ "${BEEOS_NO_PKG_MANAGER_NODE:-}" != "1" ]]; then
    # `sudo -n true` returns 0 only when sudo is configured to skip the
    # password prompt for the current user, OR when we're already root.
    # No prompt = no surprise password dialog mid-install.
    local sudo_cmd=""
    if [[ $EUID -eq 0 ]]; then
      sudo_cmd=""
    elif command -v sudo &>/dev/null && sudo -n true 2>/dev/null; then
      sudo_cmd="sudo -n"
    else
      info "Linux package managers require root and sudo is not passwordless — skipping."
    fi

    if [[ -n "$sudo_cmd" || $EUID -eq 0 ]]; then
      # Probe in order of decreasing prevalence. Each branch is
      # idempotent and short-circuits the chain on success.
      if command -v apt-get &>/dev/null; then
        info "Trying apt-get (Debian / Ubuntu)..."
        # NodeSource setup script is the canonical way to get a
        # current Node LTS on Debian-family distros. Same retry/log
        # discipline as nvm above.
        if curl -fsSL https://deb.nodesource.com/setup_lts.x \
              | $sudo_cmd bash >> "$BEEOS_INSTALL_LOG" 2>&1 \
           && $sudo_cmd apt-get install -y nodejs >> "$BEEOS_INSTALL_LOG" 2>&1; then
          success "Node.js installed via apt-get"
          return 0
        fi
      elif command -v dnf &>/dev/null; then
        info "Trying dnf (Fedora / RHEL)..."
        if curl -fsSL https://rpm.nodesource.com/setup_lts.x \
              | $sudo_cmd bash >> "$BEEOS_INSTALL_LOG" 2>&1 \
           && $sudo_cmd dnf install -y nodejs >> "$BEEOS_INSTALL_LOG" 2>&1; then
          success "Node.js installed via dnf"
          return 0
        fi
      elif command -v yum &>/dev/null; then
        info "Trying yum (CentOS / Amazon Linux)..."
        if curl -fsSL https://rpm.nodesource.com/setup_lts.x \
              | $sudo_cmd bash >> "$BEEOS_INSTALL_LOG" 2>&1 \
           && $sudo_cmd yum install -y nodejs >> "$BEEOS_INSTALL_LOG" 2>&1; then
          success "Node.js installed via yum"
          return 0
        fi
      elif command -v pacman &>/dev/null; then
        info "Trying pacman (Arch)..."
        if $sudo_cmd pacman -Sy --noconfirm nodejs npm >> "$BEEOS_INSTALL_LOG" 2>&1; then
          success "Node.js installed via pacman"
          return 0
        fi
      elif command -v zypper &>/dev/null; then
        info "Trying zypper (openSUSE)..."
        if $sudo_cmd zypper --non-interactive install nodejs npm >> "$BEEOS_INSTALL_LOG" 2>&1; then
          success "Node.js installed via zypper"
          return 0
        fi
      elif command -v apk &>/dev/null; then
        info "Trying apk (Alpine)..."
        if $sudo_cmd apk add --no-cache nodejs npm >> "$BEEOS_INSTALL_LOG" 2>&1; then
          success "Node.js installed via apk"
          return 0
        fi
      fi
    fi
  fi

  return 1
}

# ── Manual install hints ─────────────────────────────────────

print_install_hints() {
  error "Could not install Node.js automatically."
  error "See ${BEEOS_INSTALL_LOG} for full stderr of every attempted method."
  echo ""
  info "Please install Node.js >= ${MIN_NODE_VERSION} manually:"
  echo ""

  case "$OS_KIND" in
    darwin)
      echo "  ${BOLD}Homebrew (recommended):${RESET}"
      echo "    brew install node"
      echo ""
      echo "  ${BOLD}Official installer:${RESET}"
      echo "    https://nodejs.org/en/download/"
      ;;
    linux)
      echo "  ${BOLD}nvm (recommended):${RESET}"
      echo "    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash"
      echo "    source ~/.bashrc && nvm install --lts"
      echo ""
      echo "  ${BOLD}fnm (single-user, no sudo):${RESET}"
      echo "    curl -fsSL ${FNM_INSTALL_URL} | bash"
      echo "    fnm install --lts"
      echo ""
      echo "  ${BOLD}NodeSource:${RESET}"
      echo "    https://github.com/nodesource/distributions"
      echo ""
      echo "  ${BOLD}Official binaries:${RESET}"
      echo "    https://nodejs.org/en/download/"
      ;;
    *)
      echo "  Visit https://nodejs.org/en/download/"
      ;;
  esac

  echo ""
  info "Then run this script again:"
  echo "  curl -fsSL https://beeos.ai/install | bash"
  echo ""
}

# ── TigerVNC pre-check (Linux desktop streaming) ─────────────
#
# Mirrors install.ps1's `Test-VncServer`. BeeOS desktop streaming on
# Linux uses TigerVNC + the `vnc-bridge` sidecar (same RFB pipeline
# as macOS / Windows). The CLI's `LinuxDesktopPlatform` adapter takes
# over once `beeos init` runs: it generates a viewer password,
# writes `~/.vnc/passwd` via `tigervncpasswd -f`, mirrors plaintext
# into `~/.beeos/vnc.password`, and starts `vncserver :1 -localhost
# yes` if not already listening. This script's only job at install
# time is the user-facing hint — pointing the user at the
# distro-appropriate package so the cold-start has something to
# configure on the next `beeos init`.
#
# Skipped entirely when:
#   - $BEEOS_NO_DESKTOP = "1" (hint AND CLI desktop pipeline off)
#   - the user invoked `--device` mode (device-attach handles its own
#     VNC story via `--vnc-host`).
#   - host is darwin (macOS Screen Sharing is built-in; the CLI's
#     `MacosDesktopPlatform` cold-start handles enable + password
#     generation, no install-time hint needed).
#
# Detection: command -v tigervncpasswd or vncpasswd. Both are part
# of standard distro packages (tigervnc-standalone-server on
# Debian/Ubuntu, tigervnc-server on Fedora/RHEL, tigervnc on Arch).
test_vnc_server() {
  if [[ "${BEEOS_NO_DESKTOP:-}" == "1" ]]; then
    info "BEEOS_NO_DESKTOP=1 — skipping TigerVNC pre-check."
    return 0
  fi
  if [[ "$MODE" == "device" ]]; then
    return 0
  fi
  if [[ "$OS_KIND" != "linux" ]]; then
    return 0
  fi
  if command -v tigervncpasswd &>/dev/null || command -v vncpasswd &>/dev/null; then
    success "TigerVNC detected — \`beeos init\` will configure it for desktop streaming."
    return 0
  fi
  echo ""
  info "Optional: BeeOS desktop streaming on Linux uses TigerVNC."
  echo "  Install with one of:"
  echo "    Debian/Ubuntu:  sudo apt install tigervnc-standalone-server"
  echo "    RHEL/Fedora:    sudo dnf install tigervnc-server"
  echo "    Arch:           sudo pacman -S tigervnc"
  echo ""
  echo "  Then re-run \`beeos init\`. Set BEEOS_NO_DESKTOP=1 to suppress this hint."
  echo ""
}

# ── EACCES auto-recovery (Phase 3) ────────────────────────────
#
# Phase 2 of the bootstrap pipeline (Node install) already has a
# multi-strategy auto-recovery loop in `try_install_node` (nvm → fnm
# → brew). Phase 3 (`npm install -g @beeos-ai/cli`) historically had
# none — it just printed a hint and exited 3. The single most common
# Phase 3 failure is EACCES on the system-owned global node_modules
# (typically `/usr/local/lib/node_modules` after a nodejs.org .pkg
# install or a sudo brew install). The two helpers below close that
# gap with the same shape Phase 2 uses: a small detect function +
# an idempotent recovery function. `run_cli` calls them only when
# the npm log carries the EACCES fingerprint; every other npm
# failure mode is rarer and needs user judgement we can't safely
# automate from a curl|bash one-liner.

# Detect the user's preferred shell init file so that an `export
# PATH=...` write survives a new terminal. We support zsh, bash, fish,
# and pwsh-on-Unix (~99% of macOS / Linux users); tcsh / nushell users
# still get the printed hint instead of a silent edit to a file we
# don't fully understand. `$SHELL` is the login shell as recorded in
# /etc/passwd, NOT the currently-running interpreter — that's exactly
# what we want for "what does the user's NEXT terminal source?".
#
# fish + pwsh emit different PATH syntaxes. The persisted block in
# `recover_eacces_prefix` below picks the right form by checking the
# rc path's basename, keeping shell detection isolated to this helper.
detect_shell_rc() {
  local shell_name
  shell_name="$(basename "${SHELL:-}")"
  case "$shell_name" in
    zsh)
      # zsh's per-user interactive rc. macOS Terminal.app sources it
      # for both login and non-login zsh sessions. We deliberately do
      # NOT touch ~/.zprofile (login-only) — keeping all our writes
      # in a single file makes "how do I revert?" a one-line answer.
      printf '%s' "$HOME/.zshrc"
      ;;
    bash)
      # macOS Terminal.app launches each window as a login bash, so
      # the canonical sourced file is ~/.bash_profile. Linux GUI
      # terminals run interactive non-login bash and source ~/.bashrc.
      if [[ "$OS_KIND" == "darwin" ]]; then
        printf '%s' "$HOME/.bash_profile"
      else
        printf '%s' "$HOME/.bashrc"
      fi
      ;;
    fish)
      # fish auto-sources every *.fish under ~/.config/fish/conf.d/,
      # so a single dropped-in file is the idiomatic way to add PATH
      # without touching the user's main config.fish. mkdir is
      # idempotent and ignored on permission errors.
      mkdir -p "$HOME/.config/fish/conf.d" 2>/dev/null || true
      printf '%s' "$HOME/.config/fish/conf.d/beeos.fish"
      ;;
    pwsh)
      # PowerShell on Linux / macOS reads
      # ~/.config/powershell/Microsoft.PowerShell_profile.ps1 by
      # default for the current user, all hosts. The exact filename
      # is case-sensitive on Linux — preserve it.
      mkdir -p "$HOME/.config/powershell" 2>/dev/null || true
      printf '%s' "$HOME/.config/powershell/Microsoft.PowerShell_profile.ps1"
      ;;
    *)
      # Empty string ⇒ caller falls back to a manual-paste hint.
      printf '%s' ""
      ;;
  esac
}

persist_node_manager_shell_init() {
  local manager="$1"
  local rc
  rc="$(detect_shell_rc)"
  if [[ -z "$rc" ]]; then
    warn "Unsupported login shell ($(basename "${SHELL:-unknown}")) — could not"
    warn "  persist ${manager} startup automatically. Open a new terminal, or"
    warn "  add your Node manager init block to your shell profile."
    return 0
  fi

  local rc_base marker_begin marker_end body
  rc_base="$(basename "$rc")"
  marker_begin="# >>> beeos ${manager} >>>"
  marker_end="# <<< beeos ${manager} <<<"

  if [[ -f "$rc" ]] && grep -qF "$marker_begin" "$rc"; then
    info "Node manager startup block already present in ${rc} — leaving as-is."
    export BEEOS_SHELL_RC_UPDATED="$rc"
    return 0
  fi

  case "$manager:$rc_base" in
    nvm:.zshrc|nvm:.bash_profile|nvm:.bashrc)
      body='export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"'
      ;;
    fnm:.zshrc)
      body='export PATH="$HOME/.local/share/fnm:$PATH"
eval "$(fnm env --shell zsh)"'
      ;;
    fnm:.bash_profile|fnm:.bashrc)
      body='export PATH="$HOME/.local/share/fnm:$PATH"
eval "$(fnm env --shell bash)"'
      ;;
    fnm:*.fish)
      body='set -gx PATH "$HOME/.local/share/fnm" $PATH
fnm env --shell fish | source'
      ;;
    *)
      warn "Could not persist ${manager} startup for $(basename "${SHELL:-unknown}")."
      warn "  If a new terminal cannot find beeos, add ${manager}'s shell init"
      warn "  block to ${rc} manually."
      return 0
      ;;
  esac

  {
    printf '\n%s\n' "$marker_begin"
    printf '# Added by https://beeos.ai/install on %s\n' "$(date -u +%FT%TZ)"
    printf '# Reason: BeeOS installed Node.js via %s and future terminals need it on PATH.\n' "$manager"
    printf '# To revert: delete this block.\n'
    printf '%s\n' "$body"
    printf '%s\n' "$marker_end"
  } >> "$rc"

  export BEEOS_SHELL_RC_UPDATED="$rc"
  info "Persisted ${manager} startup to ${rc}."
  return 0
}

# Run `npm install -g <pkg>` with automatic registry fallback.
#
# Behavior:
#   1. If BEEOS_NPM_REGISTRY is set: use it directly (enterprise override).
#   2. Otherwise: try the default npm registry (respects user's npm config).
#   3. If that fails with a network error (ETIMEDOUT / ENOTFOUND /
#      ECONNREFUSED / ECONNRESET): retry once with BEEOS_NPM_FALLBACK_REGISTRY
#      (defaults to registry.npmmirror.com).
#   4. Non-network failures (EACCES / ENOSPC / EBADENGINE) fall through
#      immediately — switching registries would not help those.
#
# The function writes all npm output to both the terminal and
# BEEOS_INSTALL_LOG (via tee), so the EACCES auto-recovery below can
# still grep the log for its fingerprint.
npm_install_global_with_fallback() {
  local pkg="$1"
  # Enterprise / CI override: honour explicitly-set registry, no fallback.
  if [[ -n "${BEEOS_NPM_REGISTRY:-}" ]]; then
    npm install -g "$pkg" --registry "$BEEOS_NPM_REGISTRY" 2>&1 \
      | tee -a "$BEEOS_INSTALL_LOG"
    return "${PIPESTATUS[0]}"
  fi
  # Primary attempt — respects user's local npm config (`.npmrc`, `npm config`).
  if npm install -g "$pkg" 2>&1 | tee -a "$BEEOS_INSTALL_LOG"; then
    return 0
  fi
  # Only retry on network-class errors; local errors (EACCES / ENOSPC /
  # EBADENGINE) will be handled further up the call stack.
  if grep -qE 'ETIMEDOUT|ENOTFOUND|ECONNREFUSED|ECONNRESET|network timeout|fetch failed' \
       "$BEEOS_INSTALL_LOG"; then
    warn "npm registry unreachable, retrying with fallback: ${BEEOS_NPM_FALLBACK_REGISTRY}"
    npm install -g "$pkg" --registry "$BEEOS_NPM_FALLBACK_REGISTRY" 2>&1 \
      | tee -a "$BEEOS_INSTALL_LOG"
    return "${PIPESTATUS[0]}"
  fi
  return 1
}

# Switch the npm global prefix to a user-owned directory and persist
# the resulting PATH change to the user's shell rc. Used as the EACCES
# auto-recovery in `run_cli`. Idempotent — running on an already-
# fixed host is a no-op (the sentinel block check below prevents
# duplicate appends).
#
# Why this approach (and not the alternatives):
#
#   - sudo retry: would leave /usr/local/lib/node_modules/@beeos-ai/cli
#     owned by root, breaking the next `npm update -g` and creating
#     ownership inconsistencies between the CLI binary and ~/.beeos
#     (which the user owns). It's the kind of "fix" that buries the
#     real problem deeper.
#
#   - force-install nvm: intrusive for a user who already has a
#     working Node toolchain, and risks breaking other tools that
#     depend on the system Node path.
#
#   - just print a hint: the original behaviour. Pushes the fix onto
#     the user, who has to switch contexts mid-install. Most users
#     give up here.
#
# The chosen path mirrors npm's own documentation:
# https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally
#
# Escape hatch: `BEEOS_NO_NPM_PREFIX_FIX=1` short-circuits this
# function so power users / strict-dotfiles operators can opt out.
recover_eacces_prefix() {
  if [[ "${BEEOS_NO_NPM_PREFIX_FIX:-}" == "1" ]]; then
    info "BEEOS_NO_NPM_PREFIX_FIX=1 — skipping auto npm prefix switch."
    return 1
  fi

  local prefix="$HOME/.npm-global"
  info "Switching npm global prefix to ${prefix} (user-owned)."

  if ! mkdir -p "$prefix" >> "$BEEOS_INSTALL_LOG" 2>&1; then
    error "Could not create ${prefix} — falling back to hint."
    return 1
  fi
  if ! npm config set prefix "$prefix" >> "$BEEOS_INSTALL_LOG" 2>&1; then
    error "npm config set prefix failed — falling back to hint."
    return 1
  fi

  # Make the new prefix usable for the upcoming retry IN this
  # session. Persistence (next terminal) happens via shell rc below.
  export PATH="$prefix/bin:$PATH"

  local rc
  rc="$(detect_shell_rc)"
  if [[ -z "$rc" ]]; then
    warn "Unsupported login shell ($(basename "${SHELL:-unknown}")) — could not"
    warn "  persist PATH automatically. Add this line to your shell init:"
    warn "    export PATH=\"$prefix/bin:\$PATH\""
    return 0
  fi

  # Sentinel block — grep + sed can remove it cleanly. We check for
  # the BEGIN marker so re-runs of the installer don't append the
  # block twice (the file may have been edited by hand between runs).
  local marker_begin="# >>> beeos npm-prefix >>>"
  local marker_end="# <<< beeos npm-prefix <<<"
  if [[ -f "$rc" ]] && grep -qF "$marker_begin" "$rc"; then
    info "Sentinel block already present in ${rc} — leaving as-is."
    return 0
  fi

  # Pick PATH-export syntax based on the rc file's basename. fish and
  # pwsh need different forms; sh-family rcs all share `export PATH=...`.
  local rc_base
  rc_base="$(basename "$rc")"
  local export_line
  case "$rc_base" in
    *.fish)
      # `fish_add_path -g` prepends to the global PATH list and is the
      # idiomatic way fish persists PATH (universal fish_user_paths).
      export_line="fish_add_path -g \"${prefix}/bin\""
      ;;
    Microsoft.PowerShell_profile.ps1)
      # PowerShell uses semicolon-delimited PATH on Windows but ':' on
      # Unix where pwsh runs. `$env:PATH` mutation in the profile is
      # the canonical way; we use the Unix separator literal because
      # pwsh-on-Unix is the only host that reads this rc.
      export_line="\$env:PATH = \"${prefix}/bin:\" + \$env:PATH"
      ;;
    *)
      # zsh / bash / dash-family — the historical contract.
      export_line="export PATH=\"${prefix}/bin:\$PATH\""
      ;;
  esac

  # `>>` never clobbers existing content. If the rc doesn't exist yet
  # (fresh user account), the redirect creates it with 0644 perms.
  {
    printf '\n%s\n' "$marker_begin"
    printf '# Added by https://beeos.ai/install on %s\n' "$(date -u +%FT%TZ)"
    printf '# Reason: system npm prefix was not writable (EACCES).\n'
    printf '# To revert: delete this block AND run `npm config delete prefix`.\n'
    printf '%s\n' "$export_line"
    printf '%s\n' "$marker_end"
  } >> "$rc"
  info "Persisted PATH to ${rc}. Set BEEOS_NO_NPM_PREFIX_FIX=1 to skip on re-runs."
  return 0
}

# ── Main ─────────────────────────────────────────────────────

run_cli() {
  local subcmd="$1"
  shift || true

  # When invoked as `curl ... | bash`, our own stdin is a pipe that has
  # already been fully consumed by the time we exec the CLI. Node's
  # readline treats a closed / non-TTY stdin as "no answer", silently
  # collapsing every interactive prompt to its default. Reopen stdin
  # against the controlling terminal so `beeos init` can still ask for
  # framework choice, bind confirmation, etc. `-t 1` guards us against
  # true headless runs (CI) where /dev/tty doesn't exist.
  local stdin_src="/dev/stdin"
  if [ -t 1 ] && [ -e /dev/tty ]; then
    stdin_src="/dev/tty"
  fi

  # ALWAYS install globally. We previously branched to `npx --yes` when
  # available so curl|bash users got a throwaway install, but that
  # silently broke the post-install UX: `beeos` was not on PATH after
  # the script exited, so the user's next step (`beeos device attach`)
  # failed with `command not found` and the obvious recovery (re-run
  # the one-liner) hit the "existing install" prompt because some
  # state in ~/.beeos persisted, leaving the user stuck.
  # Power users who explicitly want the old throwaway behaviour can set
  # `BEEOS_USE_NPX=1`. Default is global install.
  if [[ "${BEEOS_USE_NPX:-}" == "1" ]] && command -v npx &>/dev/null; then
    warn "BEEOS_USE_NPX=1 — using throwaway npx install (beeos won't persist on PATH)."
    exec npx --yes "$CLI_PACKAGE" "$subcmd" "$@" <"$stdin_src"
  fi

  if ! command -v npm &>/dev/null; then
    error "npm not found — install Node.js first (this script tries that for you;"
    error "see ${BEEOS_INSTALL_LOG} for why auto-install bailed out)."
    exit 1
  fi

  info "Installing ${CLI_PACKAGE} globally..."
  # npm output is teed into BEEOS_INSTALL_LOG so support engineers can
  # inspect the full error (registry URL, npm error code, proxy hint)
  # even after the terminal scrolls. `set -o pipefail` (file header)
  # ensures the pipe's exit status is `npm`'s, not `tee`'s.
  # `npm_install_global_with_fallback` additionally retries once with
  # BEEOS_NPM_FALLBACK_REGISTRY when the primary registry is unreachable.
  if ! npm_install_global_with_fallback "$CLI_PACKAGE"; then
    # ── EACCES auto-recovery ──────────────────────────────────────
    # The single most common Phase 3 failure: the system npm prefix
    # is owned by root, EACCES on rename. Detect the fingerprint in
    # the npm log we just teed; if it matches, swap the prefix to a
    # user-owned dir (see `recover_eacces_prefix` for the rationale)
    # and retry exactly once. Every other npm error class
    # (ENOSPC / ETIMEDOUT / EBADENGINE / ...) falls through to the
    # hint+exit path below — those are rarer in practice and need
    # human judgement we can't safely automate.
    if grep -qE 'EACCES.*permission denied.*node_modules' "$BEEOS_INSTALL_LOG"; then
      warn "Detected EACCES on global node_modules — attempting auto-recovery."
      if recover_eacces_prefix; then
        info "Retrying npm install with user-owned prefix..."
        if npm_install_global_with_fallback "$CLI_PACKAGE"; then
          send_telemetry "install.bootstrap.npm_recovered" "eacces_prefix"
          send_telemetry "install.bootstrap.cli_installed"
          local recovered_bin
          recovered_bin="$(resolve_beeos_bin)"
          exec "$recovered_bin" "$subcmd" "$@" <"$stdin_src"
        fi
      fi
    fi
    # ── End recovery ──────────────────────────────────────────────

    # P0-A of the install-link review: separate "Node ready" from
    # "CLI is actually on PATH" so the dashboard never reports a
    # success that the user can't reproduce. `install.bootstrap.npm_fail`
    # is the canonical fail-after-bootstrap marker.
    send_telemetry "install.bootstrap.npm_fail" "npm_install_cli_failed" false
    error "npm install -g ${CLI_PACKAGE} failed."
    error "  Full log: ${BEEOS_INSTALL_LOG}"
    error ""
    error "Common fixes:"
    error "  - EEXIST on /bin/beeos from another package:"
    error "      npm uninstall -g @beeos-ai/cli    # then re-run installer"
    error "  - EACCES permission error:"
    error "      auto-recovery already attempted; if it failed, manually"
    error "      run \`npm config set prefix \"\$HOME/.npm-global\"\` and"
    error "      add \$HOME/.npm-global/bin to PATH, then re-run installer."
    error "      (Set BEEOS_NO_NPM_PREFIX_FIX=1 to disable auto-recovery.)"
    exit 3
  fi
  # NPM succeeded — CLI is now on PATH. The device-agent suite is
  # deliberately NOT installed here; `beeos device attach` will
  # auto-install it on first use via `ensureDeviceAgent` (see
  # `packages/core/src/device-setup.ts`). OpenClaw-only users
  # never download those ~20MB. Fire the "CLI is reachable on PATH"
  # event before exec — exec replaces our process so this is the
  # last chance.
  send_telemetry "install.bootstrap.cli_installed"
  local beeos_bin
  beeos_bin="$(resolve_beeos_bin)"
  exec "$beeos_bin" "$subcmd" "$@" <"$stdin_src"
}

resolve_beeos_bin() {
  local found prefix candidate
  found="$(command -v beeos 2>/dev/null || true)"
  if [[ -n "$found" ]]; then
    printf '%s' "$found"
    return 0
  fi

  prefix="$(npm prefix -g 2>/dev/null || true)"
  if [[ -n "$prefix" ]]; then
    candidate="${prefix}/bin/beeos"
    if [[ -x "$candidate" ]]; then
      printf '%s' "$candidate"
      return 0
    fi
  fi

  error "npm install succeeded, but the beeos binary is not reachable on PATH."
  if [[ -n "${BEEOS_SHELL_RC_UPDATED:-}" ]]; then
    error "Reload your shell profile and try again:"
    error "  source \"${BEEOS_SHELL_RC_UPDATED}\""
  else
    error "Open a new terminal or add npm's global bin directory to PATH."
  fi
  exit 3
}

main() {
  # P1-F of the install-link review: dry-run hook for bats / CI smoke
  # tests. Mirrors `install.ps1`'s `BEEOS_INSTALL_DRY_RUN=1` early
  # exit. When set, every helper has been parsed and `main` was
  # entered — the script is "loadable" — but no telemetry, no Node
  # install, no `npm install -g`, no exec. This lets shellcheck +
  # bats catch parse-time and helper-availability regressions
  # without spawning a real Node bootstrap.
  if [[ "${BEEOS_INSTALL_DRY_RUN:-}" == "1" ]]; then
    echo "[beeos] BEEOS_INSTALL_DRY_RUN=1 — exiting early (smoke test mode)"
    return 0
  fi

  echo ""
  echo "  ${BOLD}BeeOS CLI Installer${RESET}"
  echo "  ${DIM}https://beeos.ai${RESET}"
  echo ""

  detect_platform
  send_telemetry "install.bootstrap.start"

  if ! check_node; then
    if ! try_install_node; then
      send_telemetry "install.bootstrap.node_fail" "node_install_failed" false
      print_install_hints
      exit 1
    fi
    if ! check_node; then
      send_telemetry "install.bootstrap.node_fail" "node_version_too_old" false
      print_install_hints
      exit 1
    fi
  fi

  success "Node.js $(node -v) detected"

  # Phase 3 of the multi-OS install-link refactor: TigerVNC pre-check
  # for Linux. Parallels install.ps1's `Test-VncServer`. Silent
  # success when the tool is on PATH; printable hint when missing.
  # Skipped on darwin (macOS Screen Sharing is configured by the
  # CLI's `MacosDesktopPlatform` cold-start) and in `--device` mode.
  test_vnc_server

  # Bootstrap is finished. Hand off to `run_cli` which performs
  # `npm install -g @beeos-ai/cli` (idempotent — npm short-circuits
  # when the requested version is already on disk) and then `exec`s
  # the freshly-installed binary. "What does the CLI do when it's
  # already installed?" is now `beeos init`'s problem; we don't
  # second-guess it from out here.
  info "Running BeeOS CLI..."
  echo ""

  # P0-A of the install-link review: this event marks "Node bootstrap
  # finished, about to dispatch into npm install -g + the CLI". The
  # follow-up `install.bootstrap.cli_installed` (fired inside run_cli
  # after npm succeeds) is the real "user has working `beeos` on PATH"
  # signal. The two are split so dashboards can distinguish bootstrap
  # failures from npm/CLI failures.
  send_telemetry "install.bootstrap.bootstrap_done"

  if [[ "$MODE" == "device" ]]; then
    run_cli "device" "attach" "${PASSTHROUGH_ARGS[@]+"${PASSTHROUGH_ARGS[@]}"}"
  else
    run_cli "init" "${PASSTHROUGH_ARGS[@]+"${PASSTHROUGH_ARGS[@]}"}"
  fi
}

main
