#!/usr/bin/env bash set -euo pipefail VM_NAME="Windows 11" SNAPSHOT_HINT="pre-openclaw-native-e2e-2026-03-12" MODE="both" PROVIDER="openai" API_KEY_ENV="" AUTH_CHOICE="" AUTH_KEY_FLAG="" MODEL_ID="" INSTALL_URL="https://openclaw.ai/install.ps1" HOST_PORT="18426" HOST_PORT_EXPLICIT=0 HOST_IP="" LATEST_VERSION="" INSTALL_VERSION="" TARGET_PACKAGE_SPEC="" UPGRADE_FROM_PACKED_MAIN=0 JSON_OUTPUT=0 KEEP_SERVER=0 CHECK_LATEST_REF=1 SNAPSHOT_ID="" SNAPSHOT_STATE="" SNAPSHOT_NAME="" PACKED_MAIN_COMMIT_SHORT="" MAIN_TGZ_DIR="$(mktemp -d)" MAIN_TGZ_PATH="" MINGIT_ZIP_PATH="" MINGIT_ZIP_NAME="" WINDOWS_LATEST_INSTALL_SCRIPT_PATH="" WINDOWS_BASELINE_INSTALL_SCRIPT_PATH="" WINDOWS_INSTALL_SCRIPT_PATH="" WINDOWS_ONBOARD_SCRIPT_PATH="" WINDOWS_DEV_UPDATE_SCRIPT_PATH="" SERVER_PID="" RUN_DIR="$(mktemp -d /tmp/openclaw-parallels-windows.XXXXXX)" BUILD_LOCK_DIR="${TMPDIR:-/tmp}/openclaw-parallels-build.lock" TIMEOUT_SNAPSHOT_S=240 TIMEOUT_INSTALL_S=1200 TIMEOUT_VERIFY_S=120 TIMEOUT_ONBOARD_S=240 TIMEOUT_ONBOARD_PHASE_S=$((TIMEOUT_ONBOARD_S + 60)) TIMEOUT_GATEWAY_S=120 TIMEOUT_AGENT_S=180 FRESH_MAIN_STATUS="skip" FRESH_MAIN_VERSION="skip" FRESH_GATEWAY_STATUS="skip" FRESH_AGENT_STATUS="skip" UPGRADE_STATUS="skip" UPGRADE_PRECHECK_STATUS="skip" LATEST_INSTALLED_VERSION="skip" UPGRADE_MAIN_VERSION="skip" UPGRADE_GATEWAY_STATUS="skip" UPGRADE_AGENT_STATUS="skip" say() { printf '==> %s\n' "$*" } artifact_label() { if [[ "$TARGET_PACKAGE_SPEC" == "" && "$MODE" == "upgrade" && "$UPGRADE_FROM_PACKED_MAIN" -eq 0 ]]; then printf 'Windows smoke artifacts' return fi if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then printf 'baseline package tgz' return fi if [[ "$UPGRADE_FROM_PACKED_MAIN" -eq 1 ]]; then printf 'packed main tgz' return fi printf 'current main tgz' } upgrade_uses_host_tgz() { [[ "$UPGRADE_FROM_PACKED_MAIN" -eq 1 || -n "$TARGET_PACKAGE_SPEC" ]] } needs_host_tgz() { [[ "$MODE" == "fresh" || "$MODE" == "both" ]] || upgrade_uses_host_tgz } upgrade_summary_label() { if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then printf 'target-package->dev' return fi if [[ "$UPGRADE_FROM_PACKED_MAIN" -eq 1 ]]; then printf 'packed-main->dev' return fi printf 'latest->dev' } extract_package_build_commit_from_tgz() { tar -xOf "$1" package/dist/build-info.json | python3 -c 'import json, sys; print(json.load(sys.stdin).get("commit", ""))' } 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-windows-smoke.sh [options] Options: --vm Parallels VM name. Default: "Windows 11" --snapshot-hint Snapshot name substring/fuzzy match. Default: "pre-openclaw-native-e2e-2026-03-12" --mode --provider Provider auth/model lane. Default: openai --api-key-env Host env var name for provider API key. Default: OPENAI_API_KEY for openai, ANTHROPIC_API_KEY for anthropic --openai-api-key-env Alias for --api-key-env (backward compatible) --install-url Installer URL for latest release. Default: https://openclaw.ai/install.ps1 --host-port Host HTTP port for current-main tgz. Default: 18426 --host-ip Override Parallels host IP. --latest-version Override npm latest version lookup. --install-version Pin site-installer version/dist-tag for the baseline lane. --upgrade-from-packed-main Upgrade lane: install the packed current-main npm tgz as baseline, then run openclaw update --channel dev. --target-package-spec Upgrade lane: install this npm package tarball as the baseline, then run openclaw update --channel dev. Fresh lane: install this npm package tarball instead of packing current main. Example: openclaw@2026.3.13-beta.1 Default upgrade lane without this flag: latest/site installer -> dev channel update. --skip-latest-ref-check Skip latest-release ref-mode precheck. --keep-server Leave temp host HTTP server running. --json Print machine-readable JSON summary. -h, --help Show help. EOF } while [[ $# -gt 0 ]]; do case "$1" in --) shift ;; --vm) VM_NAME="$2" shift 2 ;; --snapshot-hint) SNAPSHOT_HINT="$2" shift 2 ;; --mode) MODE="$2" shift 2 ;; --provider) PROVIDER="$2" shift 2 ;; --api-key-env|--openai-api-key-env) API_KEY_ENV="$2" shift 2 ;; --install-url) INSTALL_URL="$2" shift 2 ;; --host-port) HOST_PORT="$2" HOST_PORT_EXPLICIT=1 shift 2 ;; --host-ip) HOST_IP="$2" shift 2 ;; --latest-version) LATEST_VERSION="$2" shift 2 ;; --install-version) INSTALL_VERSION="$2" shift 2 ;; --upgrade-from-packed-main) UPGRADE_FROM_PACKED_MAIN=1 shift ;; --target-package-spec) TARGET_PACKAGE_SPEC="$2" shift 2 ;; --skip-latest-ref-check) CHECK_LATEST_REF=0 shift ;; --keep-server) KEEP_SERVER=1 shift ;; --json) JSON_OUTPUT=1 shift ;; -h|--help) usage exit 0 ;; *) die "unknown arg: $1" ;; esac done case "$MODE" in fresh|upgrade|both) ;; *) die "invalid --mode: $MODE" ;; esac case "$PROVIDER" in openai) AUTH_CHOICE="openai-api-key" AUTH_KEY_FLAG="openai-api-key" MODEL_ID="openai/gpt-5.4" [[ -n "$API_KEY_ENV" ]] || API_KEY_ENV="OPENAI_API_KEY" ;; anthropic) AUTH_CHOICE="apiKey" AUTH_KEY_FLAG="anthropic-api-key" MODEL_ID="anthropic/claude-sonnet-4-6" [[ -n "$API_KEY_ENV" ]] || API_KEY_ENV="ANTHROPIC_API_KEY" ;; minimax) AUTH_CHOICE="minimax-global-api" AUTH_KEY_FLAG="minimax-api-key" MODEL_ID="minimax/MiniMax-M2.7" [[ -n "$API_KEY_ENV" ]] || API_KEY_ENV="MINIMAX_API_KEY" ;; *) die "invalid --provider: $PROVIDER" ;; esac API_KEY_VALUE="${!API_KEY_ENV:-}" [[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required" ps_single_quote() { printf "%s" "$1" | sed "s/'/''/g" } ps_array_literal() { local arg quoted parts=() for arg in "$@"; do quoted="$(ps_single_quote "$arg")" parts+=("'$quoted'") done local joined="" local part for part in "${parts[@]}"; do if [[ -n "$joined" ]]; then joined+=", " fi joined+="$part" done printf '@(%s)' "$joined" } resolve_snapshot_info() { local json hint json="$(prlctl snapshot-list "$VM_NAME" --json)" hint="$SNAPSHOT_HINT" SNAPSHOT_JSON="$json" SNAPSHOT_HINT="$hint" python3 - <<'PY' import difflib import json import os import re import sys payload = json.loads(os.environ["SNAPSHOT_JSON"]) hint = os.environ["SNAPSHOT_HINT"].strip().lower() best_id = None best_meta = None best_score = -1.0 def aliases(name: str) -> list[str]: values = [name] for pattern in ( r"^(.*)-poweroff$", r"^(.*)-poweroff-\d{4}-\d{2}-\d{2}$", ): match = re.match(pattern, name) if match: values.append(match.group(1)) return values for snapshot_id, meta in payload.items(): name = str(meta.get("name", "")).strip() lowered = name.lower() score = 0.0 for alias in aliases(lowered): if alias == hint: score = max(score, 10.0) elif hint and hint in alias: score = max(score, 5.0 + len(hint) / max(len(alias), 1)) else: score = max(score, difflib.SequenceMatcher(None, hint, alias).ratio()) if str(meta.get("state", "")).lower() == "poweroff": score += 0.5 if score > best_score: best_score = score best_id = snapshot_id best_meta = meta if not best_id: sys.exit("no snapshot matched") print( "\t".join( [ best_id, str(best_meta.get("state", "")).strip(), str(best_meta.get("name", "")).strip(), ] ) ) PY } resolve_host_ip() { if [[ -n "$HOST_IP" ]]; then printf '%s\n' "$HOST_IP" return fi local detected detected="$(ifconfig | awk '/inet 10\.211\./ { print $2; exit }')" [[ -n "$detected" ]] || die "failed to detect Parallels host IP; pass --host-ip" printf '%s\n' "$detected" } is_host_port_free() { local port="$1" python3 - "$port" <<'PY' import socket import sys port = int(sys.argv[1]) sock = socket.socket() try: sock.bind(("0.0.0.0", port)) except OSError: raise SystemExit(1) finally: sock.close() PY } allocate_host_port() { python3 - <<'PY' import socket sock = socket.socket() sock.bind(("0.0.0.0", 0)) print(sock.getsockname()[1]) sock.close() PY } resolve_host_port() { if is_host_port_free "$HOST_PORT"; then printf '%s\n' "$HOST_PORT" return fi if [[ "$HOST_PORT_EXPLICIT" -eq 1 ]]; then die "host port $HOST_PORT already in use" fi HOST_PORT="$(allocate_host_port)" warn "host port 18426 busy; using $HOST_PORT" printf '%s\n' "$HOST_PORT" } guest_exec() { prlctl exec "$VM_NAME" --current-user "$@" } host_timeout_exec() { local timeout_s="$1" shift HOST_TIMEOUT_S="$timeout_s" python3 - "$@" <<'PY' import os import subprocess import sys timeout = int(os.environ["HOST_TIMEOUT_S"]) args = sys.argv[1:] try: completed = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout) except subprocess.TimeoutExpired as exc: if exc.stdout: sys.stdout.buffer.write(exc.stdout) if exc.stderr: sys.stderr.buffer.write(exc.stderr) sys.stderr.write(f"host timeout after {timeout}s\n") raise SystemExit(124) if completed.stdout: sys.stdout.buffer.write(completed.stdout) if completed.stderr: sys.stderr.buffer.write(completed.stderr) raise SystemExit(completed.returncode) 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 )" guest_exec powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "$encoded" } guest_powershell_poll() { local timeout_s="$1" local script="$2" 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 )" host_timeout_exec "$timeout_s" prlctl exec "$VM_NAME" --current-user powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "$encoded" } dump_latest_guest_npm_log_tail() { local label="${1:-guest npm debug log tail}" local npm_log rc set +e npm_log="$( guest_powershell_poll 20 "$(cat <<'EOF' $logDir = Join-Path $env:LOCALAPPDATA 'npm-cache\_logs' if (-not (Test-Path $logDir)) { exit 0 } $latest = Get-ChildItem $logDir -Filter '*-debug-0.log' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($null -eq $latest) { exit 0 } "==> npm-debug-log" $latest.FullName Get-Content $latest.FullName -Tail 80 EOF )" )" rc=$? set -e if [[ $rc -ne 0 || -z "$npm_log" ]]; then warn "$label unavailable" return 1 fi printf '==> %s\n' "$label" printf '%s\n' "$npm_log" } stream_latest_guest_npm_log_tail_delta() { local label="$1" local state_path="$2" local npm_log rc set +e npm_log="$( guest_powershell_poll 20 "$(cat <<'EOF' $logDir = Join-Path $env:LOCALAPPDATA 'npm-cache\_logs' if (-not (Test-Path $logDir)) { exit 0 } $latest = Get-ChildItem $logDir -Filter '*-debug-0.log' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($null -eq $latest) { exit 0 } "==> npm-debug-log" $latest.FullName Get-Content $latest.FullName -Tail 80 EOF )" )" rc=$? set -e if [[ $rc -ne 0 || -z "$npm_log" ]]; then return "$rc" fi GUEST_LOG="$npm_log" python3 - "$state_path" "$label" <<'PY' import os import pathlib import sys state_path = pathlib.Path(sys.argv[1]) label = sys.argv[2] previous = state_path.read_text(encoding="utf-8", errors="replace") current = os.environ["GUEST_LOG"].replace("\r\n", "\n").replace("\r", "\n") if current.startswith(previous): delta = current[len(previous):] else: delta = current if delta: sys.stdout.write(f"==> {label}\n") sys.stdout.write(delta) state_path.write_text(current, encoding="utf-8") PY } guest_run_openclaw() { local env_name="${1:-}" local env_value="${2:-}" shift 2 local args_literal env_name_q env_value_q args_literal="$(ps_array_literal "$@")" env_name_q="$(ps_single_quote "$env_name")" env_value_q="$(ps_single_quote "$env_value")" guest_powershell "$(cat <&1 if (\$null -ne \$output) { \$output | ForEach-Object { \$_ } } exit \$LASTEXITCODE EOF )" } ensure_vm_running_for_retry() { local status status="$(prlctl status "$VM_NAME" 2>/dev/null || true)" case "$status" in *" suspended") # Some Windows guest transport drops leave the VM suspended between retry # attempts; wake it before the next prlctl exec. warn "VM suspended during retry path; resuming $VM_NAME" prlctl resume "$VM_NAME" >/dev/null ;; *" stopped") warn "VM stopped during retry path; starting $VM_NAME" prlctl start "$VM_NAME" >/dev/null ;; esac } run_windows_retry() { local label="$1" local max_attempts="$2" shift 2 local attempt rc rc=0 for (( attempt = 1; attempt <= max_attempts; attempt++ )); do printf '%s attempt %d/%d\n' "$label" "$attempt" "$max_attempts" set +e "$@" rc=$? set -e if [[ $rc -eq 0 ]]; then return 0 fi warn "$label attempt $attempt failed (rc=$rc)" if (( attempt < max_attempts )); then if ! ensure_vm_running_for_retry >/dev/null 2>&1; then : fi if ! wait_for_guest_ready >/dev/null 2>&1; then : fi sleep 5 fi done return "$rc" } restore_snapshot() { local snapshot_id="$1" say "Restore snapshot $SNAPSHOT_HINT ($snapshot_id)" prlctl snapshot-switch "$VM_NAME" --id "$snapshot_id" >/dev/null if [[ "$SNAPSHOT_STATE" == "poweroff" ]]; then wait_for_vm_status "stopped" || die "restored poweroff snapshot did not reach stopped state in $VM_NAME" say "Start restored poweroff snapshot $SNAPSHOT_NAME" prlctl start "$VM_NAME" >/dev/null fi } verify_windows_user_ready() { guest_exec cmd.exe /d /s /c "echo ready" } wait_for_vm_status() { local expected="$1" local deadline status deadline=$((SECONDS + TIMEOUT_SNAPSHOT_S)) while (( SECONDS < deadline )); do status="$(prlctl status "$VM_NAME" 2>/dev/null || true)" if [[ "$status" == *" $expected" ]]; then return 0 fi sleep 1 done return 1 } wait_for_guest_ready() { local deadline deadline=$((SECONDS + TIMEOUT_SNAPSHOT_S)) while (( SECONDS < deadline )); do if verify_windows_user_ready >/dev/null 2>&1; then return 0 fi sleep 3 done return 1 } phase_log_path() { printf '%s/%s.log\n' "$RUN_DIR" "$1" } show_log_excerpt() { local log_path="$1" warn "log tail: $log_path" tail -n 80 "$log_path" >&2 || true } phase_run() { local phase_id="$1" local timeout_s="$2" shift 2 local log_path pid start rc timed_out log_path="$(phase_log_path "$phase_id")" say "$phase_id" start=$SECONDS timed_out=0 ( "$@" ) >"$log_path" 2>&1 & pid=$! while kill -0 "$pid" >/dev/null 2>&1; do if (( SECONDS - start >= timeout_s )); then timed_out=1 kill "$pid" >/dev/null 2>&1 || true sleep 2 kill -9 "$pid" >/dev/null 2>&1 || true break fi sleep 1 done set +e wait "$pid" rc=$? set -e if (( timed_out )); then warn "$phase_id timed out after ${timeout_s}s" printf 'timeout after %ss\n' "$timeout_s" >>"$log_path" show_log_excerpt "$log_path" return 124 fi if [[ $rc -ne 0 ]]; then warn "$phase_id failed (rc=$rc)" show_log_excerpt "$log_path" return "$rc" fi return 0 } 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(errors="replace") matches = re.findall(r"OpenClaw [^\r\n]+ \([0-9a-f]{7,}\)", text) print(matches[-1] if matches else "") PY } write_summary_json() { local summary_path="$RUN_DIR/summary.json" python3 - "$summary_path" <<'PY' import json import os import sys summary = { "vm": os.environ["SUMMARY_VM"], "snapshotHint": os.environ["SUMMARY_SNAPSHOT_HINT"], "snapshotId": os.environ["SUMMARY_SNAPSHOT_ID"], "mode": os.environ["SUMMARY_MODE"], "provider": os.environ["SUMMARY_PROVIDER"], "latestVersion": os.environ["SUMMARY_LATEST_VERSION"], "installVersion": os.environ["SUMMARY_INSTALL_VERSION"], "targetPackageSpec": os.environ["SUMMARY_TARGET_PACKAGE_SPEC"], "currentHead": os.environ["SUMMARY_CURRENT_HEAD"], "runDir": os.environ["SUMMARY_RUN_DIR"], "freshMain": { "status": os.environ["SUMMARY_FRESH_MAIN_STATUS"], "version": os.environ["SUMMARY_FRESH_MAIN_VERSION"], "gateway": os.environ["SUMMARY_FRESH_GATEWAY_STATUS"], "agent": os.environ["SUMMARY_FRESH_AGENT_STATUS"], }, "upgrade": { "precheck": os.environ["SUMMARY_UPGRADE_PRECHECK_STATUS"], "status": os.environ["SUMMARY_UPGRADE_STATUS"], "latestVersionInstalled": os.environ["SUMMARY_LATEST_INSTALLED_VERSION"], "mainVersion": os.environ["SUMMARY_UPGRADE_MAIN_VERSION"], "gateway": os.environ["SUMMARY_UPGRADE_GATEWAY_STATUS"], "agent": os.environ["SUMMARY_UPGRADE_AGENT_STATUS"], }, } with open(sys.argv[1], "w", encoding="utf-8") as handle: json.dump(summary, handle, indent=2, sort_keys=True) print(sys.argv[1]) PY } resolve_latest_version() { if [[ -n "$LATEST_VERSION" ]]; then printf '%s\n' "$LATEST_VERSION" return fi npm view openclaw version --userconfig "$(mktemp)" } baseline_install_version() { if [[ -n "$INSTALL_VERSION" ]]; then printf '%s\n' "$INSTALL_VERSION" return fi printf '%s\n' "$LATEST_VERSION" } resolve_mingit_download() { python3 - <<'PY' import json import urllib.request req = urllib.request.Request( "https://api.github.com/repos/git-for-windows/git/releases/latest", headers={ "User-Agent": "openclaw-parallels-smoke", "Accept": "application/vnd.github+json", }, ) with urllib.request.urlopen(req, timeout=30) as response: data = json.load(response) assets = data.get("assets", []) preferred_names = [ "MinGit-2.53.0.2-arm64.zip", "MinGit-2.53.0.2-64-bit.zip", ] best = None for wanted in preferred_names: for asset in assets: if asset.get("name") == wanted: best = asset break if best: break if best is None: for asset in assets: name = asset.get("name", "") if name.startswith("MinGit-") and name.endswith(".zip") and "busybox" not in name: best = asset break if best is None: raise SystemExit("no MinGit asset found") print(best["name"]) print(best["browser_download_url"]) PY } current_build_commit() { python3 - <<'PY' import json import pathlib path = pathlib.Path("dist/build-info.json") if not path.exists(): print("") else: print(json.loads(path.read_text()).get("commit", "")) PY } acquire_build_lock() { local owner_pid="" while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do if [[ -f "$BUILD_LOCK_DIR/pid" ]]; then owner_pid="$(cat "$BUILD_LOCK_DIR/pid" 2>/dev/null || true)" if [[ -n "$owner_pid" ]] && ! kill -0 "$owner_pid" >/dev/null 2>&1; then warn "Removing stale Parallels build lock" rm -rf "$BUILD_LOCK_DIR" continue fi fi sleep 1 done printf '%s\n' "$$" >"$BUILD_LOCK_DIR/pid" } release_build_lock() { if [[ -d "$BUILD_LOCK_DIR" ]]; then rm -rf "$BUILD_LOCK_DIR" fi } ensure_current_build() { local head build_commit acquire_build_lock head="$(git rev-parse HEAD)" build_commit="$(current_build_commit)" if [[ "$build_commit" == "$head" ]]; then release_build_lock return fi say "Build dist for current head" pnpm build build_commit="$(current_build_commit)" release_build_lock [[ "$build_commit" == "$head" ]] || die "dist/build-info.json still does not match HEAD after build" } ensure_guest_git() { local host_ip="$1" local mingit_url mingit_url_q mingit_name_q mingit_url="http://$host_ip:$HOST_PORT/$MINGIT_ZIP_NAME" if guest_exec cmd.exe /d /s /c "where git.exe >nul 2>nul && git.exe --version"; then return fi mingit_url_q="$(ps_single_quote "$mingit_url")" mingit_name_q="$(ps_single_quote "$MINGIT_ZIP_NAME")" guest_powershell "$(cat </tmp/openclaw-parallels-windows-http.log 2>&1 & SERVER_PID=$! sleep 1 probe_url="http://127.0.0.1:$HOST_PORT/$artifact" if kill -0 "$SERVER_PID" >/dev/null 2>&1 && curl -fsSI "$probe_url" >/dev/null 2>&1; then return 0 fi kill "$SERVER_PID" >/dev/null 2>&1 || true wait "$SERVER_PID" >/dev/null 2>&1 || true SERVER_PID="" if [[ "$HOST_PORT_EXPLICIT" -eq 1 || $attempt -ge 3 ]]; then die "failed to start reachable host HTTP server on port $HOST_PORT" fi HOST_PORT="$(allocate_host_port)" warn "retrying host HTTP server on port $HOST_PORT" done } write_latest_install_runner_script() { local install_url_q="$1" local version_flag_q="$2" WINDOWS_LATEST_INSTALL_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-install-latest.ps1" cat >"$WINDOWS_LATEST_INSTALL_SCRIPT_PATH" < \$Stage" | Tee-Object -FilePath \$LogPath -Append | Out-Null } try { \$script = Invoke-RestMethod -Uri '$install_url_q' Write-ProgressLog 'install.start' & ([scriptblock]::Create(\$script)) ${version_flag_q}-NoOnboard *>&1 | Tee-Object -FilePath \$LogPath -Append | Out-Null if (\$LASTEXITCODE -ne 0) { throw "installer failed with exit code \$LASTEXITCODE" } Write-ProgressLog 'install.version' & (Join-Path \$env:APPDATA 'npm\openclaw.cmd') --version *>&1 | Tee-Object -FilePath \$LogPath -Append | Out-Null if (\$LASTEXITCODE -ne 0) { throw "openclaw --version failed with exit code \$LASTEXITCODE" } Set-Content -Path \$DonePath -Value ([string]0) exit 0 } catch { if (Test-Path \$LogPath) { Add-Content -Path \$LogPath -Value (\$_ | Out-String) } else { (\$_ | Out-String) | Set-Content -Path \$LogPath } Set-Content -Path \$DonePath -Value '1' exit 1 } EOF } write_baseline_npm_install_runner_script() { WINDOWS_BASELINE_INSTALL_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-install-baseline-npm.ps1" cat >"$WINDOWS_BASELINE_INSTALL_SCRIPT_PATH" <<'EOF' param( [Parameter(Mandatory = $true)][string]$Version, [Parameter(Mandatory = $true)][string]$LogPath, [Parameter(Mandatory = $true)][string]$DonePath ) $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $false function Write-ProgressLog { param([Parameter(Mandatory = $true)][string]$Stage) "==> $Stage" | Tee-Object -FilePath $LogPath -Append | Out-Null } function Invoke-Logged { param( [Parameter(Mandatory = $true)][string]$Label, [Parameter(Mandatory = $true)][scriptblock]$Command ) $output = $null $previousErrorActionPreference = $ErrorActionPreference $previousNativeErrorPreference = $PSNativeCommandUseErrorActionPreference try { $ErrorActionPreference = 'Continue' $PSNativeCommandUseErrorActionPreference = $false $output = & $Command *>&1 $exitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $previousErrorActionPreference $PSNativeCommandUseErrorActionPreference = $previousNativeErrorPreference } if ($null -ne $output) { $output | Tee-Object -FilePath $LogPath -Append | Out-Null } if ($exitCode -ne 0) { throw "$Label failed with exit code $exitCode" } } try { $portableGit = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenClaw\deps') 'portable-git') '' $env:PATH = "$portableGit\cmd;$portableGit\mingw64\bin;$portableGit\usr\bin;$env:PATH" $openclaw = Join-Path $env:APPDATA 'npm\openclaw.cmd' Write-ProgressLog 'install.start' Invoke-Logged 'npm install baseline release' { & npm.cmd install -g "openclaw@$Version" --no-fund --no-audit --loglevel=error } Write-ProgressLog 'install.version' Invoke-Logged 'openclaw --version' { & $openclaw --version } Set-Content -Path $DonePath -Value ([string]0) exit 0 } catch { if (Test-Path $LogPath) { Add-Content -Path $LogPath -Value ($_ | Out-String) } else { ($_ | Out-String) | Set-Content -Path $LogPath } Set-Content -Path $DonePath -Value '1' exit 1 } EOF } install_baseline_npm_release() { local host_ip="$1" local version="$2" local script_url local runner_name log_name done_name done_status launcher_state guest_log local log_state_path npm_log_state_path local start_seconds poll_deadline startup_checked poll_rc state_rc log_rc last_npm_log_poll write_baseline_npm_install_runner_script script_url="http://$host_ip:$HOST_PORT/$(basename "$WINDOWS_BASELINE_INSTALL_SCRIPT_PATH")" runner_name="openclaw-install-baseline-$RANDOM-$RANDOM.ps1" log_name="openclaw-install-baseline-$RANDOM-$RANDOM.log" done_name="openclaw-install-baseline-$RANDOM-$RANDOM.done" log_state_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-install-baseline-log-state.XXXXXX")" npm_log_state_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-install-baseline-npm-log-state.XXXXXX")" : >"$log_state_path" : >"$npm_log_state_path" start_seconds="$SECONDS" poll_deadline=$((SECONDS + TIMEOUT_INSTALL_S + 60)) startup_checked=0 last_npm_log_poll=0 guest_powershell_poll 20 "$(cat <