#!/usr/bin/env bash set -euo pipefail OPENCLAW_PUBLIC_BASE_URL="https://pub-644ad95b9d504aa8a79aeb428f44c923.r2.dev" MIN_NODE_VERSION="22.16.0" WORKSPACE="${HOME}/.openclaw/workspace" LOG_FILE="/tmp/openclaw-install-$(date +%Y%m%d-%H%M%S).log" PROXY="" NODE_PACKAGE_URL="" OPENCLAW_PACKAGE_URL="" PROVIDER_BASE_URL="" PROVIDER_API_KEY="" PROVIDER_MODEL_ID="" PROVIDER_COMPATIBILITY="openai" SKIP_PROVIDER_PROMPT=0 STAGE="init" PLATFORM="" ARCH="" exec > >(tee -a "$LOG_FILE") 2>&1 log() { printf '\n==> %s\n' "$1" } warn() { printf 'WARN: %s\n' "$1" >&2 } fail() { printf '\n[FAILED] stage=%s\n%s\n' "$STAGE" "$1" >&2 printf 'Log file: %s\n' "$LOG_FILE" >&2 exit 1 } command_exists() { command -v "$1" >/dev/null 2>&1 } version_ge() { local a b i ai bi IFS=. read -r -a a <<<"${1#v}" IFS=. read -r -a b <<<"${2#v}" for i in 0 1 2; do ai=${a[$i]:-0} bi=${b[$i]:-0} ((10#$ai > 10#$bi)) && return 0 ((10#$ai < 10#$bi)) && return 1 done return 0 } add_runtime_paths() { local base="$HOME/.openclaw/runtime/node" if [[ -d "$base" ]]; then while IFS= read -r dir; do PATH="$dir/bin:$PATH" done < <(find "$base" -mindepth 1 -maxdepth 1 -type d | sort) fi export PATH } set_proxy_if_needed() { if [[ -z "$PROXY" ]]; then return fi log "Setting proxy" export HTTP_PROXY="$PROXY" export HTTPS_PROXY="$PROXY" export http_proxy="$PROXY" export https_proxy="$PROXY" } retry() { local attempts="$1" local delay="$2" shift 2 local n for n in $(seq 1 "$attempts"); do if "$@"; then return 0 fi if [[ "$n" -lt "$attempts" ]]; then warn "attempt $n failed, retrying in ${delay}s: $*" sleep "$delay" fi done return 1 } detect_platform() { case "$(uname -s)" in Darwin) PLATFORM="macos" ;; Linux) PLATFORM="linux" ;; *) fail "Unsupported OS: $(uname -s)" ;; esac case "$(uname -m)" in arm64|aarch64) ARCH="arm64" ;; x86_64|amd64) ARCH="x64" ;; *) fail "Unsupported architecture: $(uname -m)" ;; esac } resolve_default_node_package_url() { case "$PLATFORM/$ARCH" in macos/arm64) printf '%s/prod/macos/arm64/runtime/node/node-lts-arm64.tar.gz' "$OPENCLAW_PUBLIC_BASE_URL" ;; macos/x64) printf '%s/prod/macos/x64/runtime/node/node-lts-x64.tar.gz' "$OPENCLAW_PUBLIC_BASE_URL" ;; linux/x64) printf '%s/prod/linux/x64/runtime/node/node-lts-x64.tar.xz' "$OPENCLAW_PUBLIC_BASE_URL" ;; *) fail "No default Node package URL for $PLATFORM/$ARCH. Pass --node-package-url manually." ;; esac } resolve_default_openclaw_package_url() { case "$PLATFORM/$ARCH" in macos/arm64) printf '%s/prod/macos/arm64/runtime/openclaw/openclaw-latest.tgz' "$OPENCLAW_PUBLIC_BASE_URL" ;; macos/x64) printf '%s/prod/macos/x64/runtime/openclaw/openclaw-latest.tgz' "$OPENCLAW_PUBLIC_BASE_URL" ;; linux/x64) printf '%s/prod/linux/x64/runtime/openclaw/openclaw-latest.tgz' "$OPENCLAW_PUBLIC_BASE_URL" ;; *) fail "No default OpenClaw package URL for $PLATFORM/$ARCH. Pass --openclaw-package-url manually." ;; esac } node_version_ok() { add_runtime_paths if ! command_exists node; then return 1 fi local version version="$(node -v 2>/dev/null || true)" [[ -n "$version" ]] || return 1 version_ge "$version" "$MIN_NODE_VERSION" } download_file() { local url="$1" local out="$2" curl -fsSL --retry 5 --retry-delay 2 --connect-timeout 15 -o "$out" "$url" } install_node() { STAGE="install-node" add_runtime_paths if node_version_ok; then log "Node already exists and the version is supported" node -v return fi local url archive runtime_root extracted_dir url="${NODE_PACKAGE_URL:-$(resolve_default_node_package_url)}" archive="$(mktemp /tmp/openclaw-node.XXXXXX)" runtime_root="$HOME/.openclaw/runtime/node" mkdir -p "$runtime_root" log "Installing Node from $url" retry 4 3 download_file "$url" "$archive" || fail "Failed to download Node package" local unpack_dir unpack_dir="$(mktemp -d /tmp/openclaw-node-unpack.XXXXXX)" case "$url" in *.tar.gz|*.tgz) tar -xzf "$archive" -C "$unpack_dir" ;; *.tar.xz) tar -xJf "$archive" -C "$unpack_dir" ;; *) fail "Unsupported Node package format: $url" ;; esac extracted_dir="$(find "$unpack_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)" [[ -n "$extracted_dir" ]] || fail "Unable to find extracted Node directory" local target_dir="$runtime_root/${PLATFORM}-${ARCH}" rm -rf "$target_dir" mv "$extracted_dir" "$target_dir" add_runtime_paths node_version_ok || fail "Node was not detected after installation" node -v } prompt_provider_settings() { if [[ -n "$PROVIDER_BASE_URL" ]]; then return fi if [[ "$SKIP_PROVIDER_PROMPT" -eq 1 ]]; then return fi log "Model provider setup" read -r -p "Configure model provider now? (y/N) " choice choice="${choice:-n}" case "${choice,,}" in y|yes) ;; *) return ;; esac while [[ -z "$PROVIDER_BASE_URL" ]]; do read -r -p "Provider base URL: " PROVIDER_BASE_URL PROVIDER_BASE_URL="${PROVIDER_BASE_URL//[$'\r\n']/}" done read -r -p "Provider compatibility [openai/anthropic] (default: $PROVIDER_COMPATIBILITY): " compat compat="${compat:-$PROVIDER_COMPATIBILITY}" case "$compat" in openai|anthropic) PROVIDER_COMPATIBILITY="$compat" ;; *) warn "Invalid compatibility '$compat', keeping $PROVIDER_COMPATIBILITY" ;; esac read -r -s -p "Provider API key (hidden): " PROVIDER_API_KEY printf '\n' read -r -p "Provider model ID (required for provider setup, leave empty to skip provider config): " PROVIDER_MODEL_ID PROVIDER_MODEL_ID="${PROVIDER_MODEL_ID//[$'\r\n']/}" if [[ -z "$PROVIDER_MODEL_ID" ]]; then warn "Provider model ID empty; provider config will be skipped" PROVIDER_BASE_URL="" PROVIDER_API_KEY="" fi } get_openclaw_cmd() { add_runtime_paths if command_exists openclaw; then command -v openclaw return fi fail "openclaw not found in PATH" } install_openclaw() { STAGE="install-openclaw" local npm_cmd tgz url add_runtime_paths npm_cmd="$(command -v npm || true)" [[ -n "$npm_cmd" ]] || fail "npm not found" url="${OPENCLAW_PACKAGE_URL:-$(resolve_default_openclaw_package_url)}" tgz="$(mktemp /tmp/openclaw-package.XXXXXX.tgz)" log "Installing OpenClaw from $url" retry 4 3 download_file "$url" "$tgz" || fail "Failed to download OpenClaw package" retry 4 3 "$npm_cmd" install -g "$tgz" --fetch-retries=5 --fetch-retry-mintimeout=2000 --fetch-retry-maxtimeout=20000 || fail "npm install -g failed" local openclaw openclaw="$(get_openclaw_cmd)" "$openclaw" --version } initialize_openclaw() { STAGE="initialize-openclaw" local openclaw openclaw="$(get_openclaw_cmd)" prompt_provider_settings local args=( onboard --non-interactive --accept-risk --workspace "$WORKSPACE" --skip-health --skip-channels --skip-skills --skip-search --skip-ui --json ) if [[ -n "$PROVIDER_BASE_URL" || -n "$PROVIDER_MODEL_ID" || -n "$PROVIDER_API_KEY" ]]; then [[ -n "$PROVIDER_BASE_URL" ]] || fail "Provider base URL is required when provider settings are used" [[ -n "$PROVIDER_MODEL_ID" ]] || fail "Provider model ID is required when provider settings are used" args+=( --auth-choice custom-api-key --custom-base-url "$PROVIDER_BASE_URL" --custom-model-id "$PROVIDER_MODEL_ID" --custom-compatibility "$PROVIDER_COMPATIBILITY" ) if [[ -n "$PROVIDER_API_KEY" ]]; then args+=(--custom-api-key "$PROVIDER_API_KEY") fi else args+=(--auth-choice skip) fi log "Running minimal non-interactive onboarding" retry 3 3 "$openclaw" "${args[@]}" || fail "openclaw onboard failed" } install_gateway() { STAGE="install-gateway" local openclaw openclaw="$(get_openclaw_cmd)" log "Setting gateway.mode=local" "$openclaw" config set gateway.mode local || warn "setting gateway.mode failed" log "Installing Gateway auto-start" "$openclaw" gateway install --runtime node --json || warn "gateway install failed" log "Starting Gateway now" "$openclaw" gateway start || warn "gateway start failed" log "Checking Gateway status" "$openclaw" gateway status --json || warn "gateway status check failed" } show_dashboard_access() { STAGE="dashboard-access" local openclaw openclaw="$(get_openclaw_cmd)" log "Dashboard access" printf 'Trying to open the dashboard in your default browser...\n' printf 'Do not open http://127.0.0.1:18789 directly. It requires a tokenized dashboard URL.\n' if ! "$openclaw" dashboard; then warn "dashboard auto-open failed" printf 'Falling back to printing the dashboard URL:\n' printf 'openclaw dashboard --no-open\n' "$openclaw" dashboard --no-open || warn "dashboard URL generation failed" fi } usage() { cat < --node-package-url --openclaw-package-url --provider-base-url --provider-api-key --provider-model-id --provider-compatibility --skip-provider-prompt -h, --help EOF } while [[ $# -gt 0 ]]; do case "$1" in --proxy) PROXY="$2"; shift 2 ;; --node-package-url) NODE_PACKAGE_URL="$2"; shift 2 ;; --openclaw-package-url) OPENCLAW_PACKAGE_URL="$2"; shift 2 ;; --provider-base-url) PROVIDER_BASE_URL="$2"; shift 2 ;; --provider-api-key) PROVIDER_API_KEY="$2"; shift 2 ;; --provider-model-id) PROVIDER_MODEL_ID="$2"; shift 2 ;; --provider-compatibility) PROVIDER_COMPATIBILITY="$2"; shift 2 ;; --skip-provider-prompt) SKIP_PROVIDER_PROMPT=1; shift ;; -h|--help) usage; exit 0 ;; *) fail "Unknown argument: $1" ;; esac done detect_platform set_proxy_if_needed add_runtime_paths log "Log file: $LOG_FILE" install_node install_openclaw initialize_openclaw install_gateway show_dashboard_access log "Installation finished" printf 'OpenClaw installation is complete.\n'