#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" MACOS_VM="macOS Tahoe" WINDOWS_VM="Windows 11" LINUX_VM="Ubuntu 24.04.3 ARM64" OPENAI_API_KEY_ENV="OPENAI_API_KEY" PACKAGE_SPEC="" JSON_OUTPUT=0 RUN_DIR="$(mktemp -d /tmp/openclaw-parallels-npm-update.XXXXXX)" MAIN_TGZ_DIR="$(mktemp -d)" MAIN_TGZ_PATH="" SERVER_PID="" HOST_IP="" HOST_PORT="" LATEST_VERSION="" CURRENT_HEAD="" CURRENT_HEAD_SHORT="" OPENAI_API_KEY_VALUE="" MACOS_FRESH_STATUS="skip" WINDOWS_FRESH_STATUS="skip" LINUX_FRESH_STATUS="skip" MACOS_UPDATE_STATUS="skip" WINDOWS_UPDATE_STATUS="skip" LINUX_UPDATE_STATUS="skip" MACOS_UPDATE_VERSION="skip" WINDOWS_UPDATE_VERSION="skip" LINUX_UPDATE_VERSION="skip" say() { printf '==> %s\n' "$*" } warn() { printf 'warn: %s\n' "$*" >&2 } die() { printf 'error: %s\n' "$*" >&2 exit 1 } cleanup() { if [[ -n "${SERVER_PID:-}" ]]; then kill "$SERVER_PID" >/dev/null 2>&1 || true fi rm -rf "$MAIN_TGZ_DIR" } trap cleanup EXIT usage() { cat <<'EOF' Usage: bash scripts/e2e/parallels-npm-update-smoke.sh [options] Options: --package-spec Baseline npm package spec. Default: openclaw@latest --openai-api-key-env Host env var name for OpenAI API key. Default: OPENAI_API_KEY --json Print machine-readable JSON summary. -h, --help Show help. EOF } while [[ $# -gt 0 ]]; do case "$1" in --) shift ;; --package-spec) PACKAGE_SPEC="$2" shift 2 ;; --openai-api-key-env) OPENAI_API_KEY_ENV="$2" shift 2 ;; --json) JSON_OUTPUT=1 shift ;; -h|--help) usage exit 0 ;; *) die "unknown arg: $1" ;; esac done OPENAI_API_KEY_VALUE="${!OPENAI_API_KEY_ENV:-}" [[ -n "$OPENAI_API_KEY_VALUE" ]] || die "$OPENAI_API_KEY_ENV is required" resolve_linux_vm_name() { local json requested json="$(prlctl list --all --json)" requested="$LINUX_VM" PRL_VM_JSON="$json" REQUESTED_VM_NAME="$requested" python3 - <<'PY' import difflib import json import os import sys payload = json.loads(os.environ["PRL_VM_JSON"]) requested = os.environ["REQUESTED_VM_NAME"].strip() requested_lower = requested.lower() names = [str(item.get("name", "")).strip() for item in payload if str(item.get("name", "")).strip()] if requested in names: print(requested) raise SystemExit(0) ubuntu_names = [name for name in names if "ubuntu" in name.lower()] if not ubuntu_names: sys.exit(f"default vm not found and no Ubuntu fallback available: {requested}") best_name = max( ubuntu_names, key=lambda name: difflib.SequenceMatcher(None, requested_lower, name.lower()).ratio(), ) print(best_name) PY } resolve_latest_version() { npm view openclaw version --userconfig "$(mktemp)" } resolve_host_ip() { local detected detected="$(ifconfig | awk '/inet 10\.211\./ { print $2; exit }')" [[ -n "$detected" ]] || die "failed to detect Parallels host IP" printf '%s\n' "$detected" } allocate_host_port() { python3 - <<'PY' import socket sock = socket.socket() sock.bind(("0.0.0.0", 0)) print(sock.getsockname()[1]) sock.close() PY } ensure_current_build() { say "Build dist for current head" pnpm build } pack_main_tgz() { local pkg CURRENT_HEAD="$(git rev-parse HEAD)" CURRENT_HEAD_SHORT="$(git rev-parse --short=7 HEAD)" ensure_current_build pkg="$( npm pack --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \ | python3 -c 'import json, sys; data = json.load(sys.stdin); print(data[-1]["filename"])' )" MAIN_TGZ_PATH="$MAIN_TGZ_DIR/openclaw-main-$CURRENT_HEAD_SHORT.tgz" cp "$MAIN_TGZ_DIR/$pkg" "$MAIN_TGZ_PATH" } start_server() { HOST_IP="$(resolve_host_ip)" HOST_PORT="$(allocate_host_port)" say "Serve current main tgz on $HOST_IP:$HOST_PORT" ( cd "$MAIN_TGZ_DIR" exec python3 -m http.server "$HOST_PORT" --bind 0.0.0.0 ) >/tmp/openclaw-parallels-npm-update-http.log 2>&1 & SERVER_PID=$! sleep 1 kill -0 "$SERVER_PID" >/dev/null 2>&1 || die "failed to start host HTTP server" } wait_job() { local label="$1" local pid="$2" if wait "$pid"; then return 0 fi warn "$label failed" return 1 } extract_last_version() { local log_path="$1" python3 - "$log_path" <<'PY' import pathlib import re import sys text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace") matches = re.findall(r"OpenClaw [^\r\n]+", text) print(matches[-1] if matches else "") PY } guest_powershell() { local script="$1" local encoded encoded="$( SCRIPT_CONTENT="$script" python3 - <<'PY' import base64 import os script = "$ProgressPreference = 'SilentlyContinue'\n" + os.environ["SCRIPT_CONTENT"] payload = script.encode("utf-16le") print(base64.b64encode(payload).decode("ascii")) PY )" prlctl exec "$WINDOWS_VM" --current-user powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "$encoded" } run_windows_script_via_log() { local script_body="$1" local runner_name log_name done_name done_status runner_name="openclaw-update-$RANDOM-$RANDOM.ps1" log_name="openclaw-update-$RANDOM-$RANDOM.log" done_name="openclaw-update-$RANDOM-$RANDOM.done" guest_powershell "$(cat <