if [[ -n "$PYTHON_BIN" ]]; then
[[ -x "$PYTHON_BIN" ]] || die "PYTHON_BIN is not executable: $PYTHON_BIN"
python_bin_usable "$PYTHON_BIN" || die "PYTHON_BIN must be Python 3.10+: $PYTHON_BIN"
return fi
for candidate in "$(command -v python3 || true)" /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do
[[ -n "$candidate" && -x "$candidate" ]] || continue if python_bin_usable "$candidate"; then
PYTHON_BIN="$candidate"
return fi done
platform_enabled() {
local platform="$1"
[[ "$RUN_PLATFORMS" == "all" ]] && return 0
case ",$RUN_PLATFORMS," in
*,"$platform",*) return 0 ;;
*) return 1 ;;
esac
}
validate_platforms() {
local normalized entry valid_any
local -a entries
normalized="${RUN_PLATFORMS// /}"
[[ -n "$normalized" ]] || die "--platform must not be empty"
RUN_PLATFORMS="$normalized" if [[ "$RUN_PLATFORMS" == "all" ]]; then
return fi
valid_any=0
IFS=',' read -ra entries <<<"$RUN_PLATFORMS" for entry in "${entries[@]}"; do
case "$entry" in
macos|windows|linux)
valid_any=1
;;
*)
die "invalid --platform entry: $entry"
;;
esac done
[[ "$valid_any" -eq 1 ]] || die "--platform must include at least one platform"
}
API_KEY_VALUE="${!API_KEY_ENV:-}"
[[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required"
resolve_python_bin
resolve_linux_vm_name() {
local json requested
json="$(prlctl list --all --json)"
requested="$LINUX_VM"
PRL_VM_JSON="$json" REQUESTED_VM_NAME="$requested""$PYTHON_BIN" - <<'PY' import difflib import json import os import re import sys
from typing import Optional
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()]
def parse_ubuntu_version(name: str) -> Optional[tuple[int, ...]]:
match = re.search(r"ubuntu\s+(\d+(?:\.\d+)*)", name, re.IGNORECASE) if not match:
return None
return tuple(int(part) for part in match.group(1).split("."))
def version_distance(version: tuple[int, ...], target: tuple[int, ...]) -> tuple[int, ...]:
width = max(len(version), len(target))
padded_version = version + (0,) * (width - len(version))
padded_target = target + (0,) * (width - len(target))
return tuple(abs(a - b) for a, b in zip(padded_version, padded_target))
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}")
requested_version = parse_ubuntu_version(requested) or (24,)
ubuntu_with_versions = [
(name, parse_ubuntu_version(name)) for name in ubuntu_names
]
ubuntu_ge_24 = [
(name, version) for name, version in ubuntu_with_versions if version and version[0] >= 24
] if ubuntu_ge_24:
best_name = min(
ubuntu_ge_24,
key=lambda item: (
version_distance(item[1], requested_version),
-len(item[1]),
item[0].lower(),
),
)[0]
print(best_name)
raise SystemExit(0)
resolve_latest_version() {
npm view openclaw version --userconfig "$(mktemp)"
}
vm_status() {
local json vm_name
vm_name="$1"
json="$(prlctl list --all --json)"
PRL_VM_JSON="$json" VM_NAME="$vm_name""$PYTHON_BIN" - <<'PY' import json import os
name = os.environ["VM_NAME"] for vm in json.loads(os.environ["PRL_VM_JSON"]): if vm.get("name") == name:
print(vm.get("status", "unknown"))
break else:
print("missing")
PY
}
ensure_vm_running_for_update() {
local vm_name status deadline
vm_name="$1"
deadline=$((SECONDS + 180))
while :; do
status="$(vm_status "$vm_name")"
case "$status" in
running)
return 0
;;
stopped)
say "Start $vm_name before update phase"
prlctl start "$vm_name" >/dev/null
;;
suspended|paused)
say "Resume $vm_name before update phase"
prlctl resume "$vm_name" >/dev/null
;;
restoring|stopping|starting|pausing|suspending|resuming)
;;
missing)
die "VM not found before update phase: $vm_name"
;;
*)
warn "unexpected VM state for $vm_name before update phase: $status"
;;
esac
if (( SECONDS >= deadline )); then
die "VM did not become running before update phase: $vm_name ($status)" fi
sleep 5 done
}
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"
}
ensure_current_build() {
local build_commit head rc
head="$(git rev-parse HEAD)"
build_commit="$(current_build_commit)" if [[ "$build_commit" == "$head" ]] && ! source_tree_dirty_for_build && current_build_has_control_ui; then
return 0 fi
say "Build dist for current head"
pnpm build
rc=$? if [[ $rc -eq 0 ]]; then
pnpm ui:build
rc=$? fi if [[ $rc -eq 0 ]]; then
parallels_package_assert_no_generated_drift
rc=$? fi
return "$rc"
}
resolve_registry_target_version() {
local target="$1"
local spec="$target" if [[ "$spec" != openclaw@* ]]; then
spec="openclaw@$spec" fi
npm view "$spec" version 2>/dev/null || true
}
try {
$env:PATH = "$env:LOCALAPPDATA\OpenClaw\deps\portable-git\cmd;$env:LOCALAPPDATA\OpenClaw\deps\portable-git\mingw64\bin;$env:LOCALAPPDATA\OpenClaw\deps\portable-git\usr\bin;$env:PATH"
Remove-Item $LogPath, $DonePath -Force -ErrorAction SilentlyContinue
Write-ProgressLog 'update.start' if ($ProviderKeyFile) {
$ProviderKey = [Text.Encoding]::UTF8.GetString([IO.File]::ReadAllBytes($ProviderKeyFile))
Remove-Item $ProviderKeyFile -Force -ErrorAction SilentlyContinue
} if (-not $ProviderKey) {
throw "$ProviderKeyEnv is required"
}
Set-Item -Path ('Env:' + $ProviderKeyEnv) -Value $ProviderKey
$openclaw = Join-Path $env:APPDATA 'npm\openclaw.cmd'
Remove-FuturePluginEntries
Stop-OpenClawGatewayProcesses
Write-ProgressLog 'update.openclaw-update'
Invoke-OpenClawUpdateWithTimeout -OpenClawPath $openclaw -UpdateTarget $UpdateTarget
Write-ProgressLog 'update.verify-version'
$version = Invoke-CaptureLogged 'openclaw --version' { & $openclaw --version } if ($ExpectedNeedle -and $version -notmatch [regex]::Escape($ExpectedNeedle)) {
throw "version mismatch: expected substring $ExpectedNeedle"
}
Write-ProgressLog $version
Write-ProgressLog 'update.status'
Invoke-Logged 'openclaw update status' { & $openclaw update status --json }
Write-ProgressLog 'update.set-model'
Invoke-Logged 'openclaw models set' { & $openclaw models set $ModelId } # Windows can keep the old hashed dist modules alive across in-place global npm upgrades. # Restart the gateway/service before verifying status or the next agent turn. # Current login-item restarts can report failure before the background service # is fully observable again, so verify readiness separately and fall back to # an explicit start only if the RPC endpoint never returns.
Write-ProgressLog 'update.restart-gateway'
Restart-GatewayWithRecovery -OpenClawPath $openclaw
Stop-OpenClawGatewayProcesses
Complete-WorkspaceSetup
Write-ProgressLog 'update.agent-turn'
$exitCode = Invoke-OpenClawAgentWithTimeout -OpenClawPath $openclaw -SessionId $SessionId
Write-ProgressLog 'update.done'
Set-Content -Path $DonePath -Value ([string]$exitCode)
exit $exitCode
} 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
}
start_server() {
HOST_IP="$(resolve_host_ip)"
HOST_PORT="$(allocate_host_port)"
say "Serve update helper artifacts on $HOST_IP:$HOST_PORT"
(
cd "$MAIN_TGZ_DIR"
exec "$PYTHON_BIN" -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"
local log_path="${3:-}" if wait "$pid"; then
return 0 fi if [[ -n "$log_path" && "$label" == *"update"* ]] && update_log_completed "$log_path"; then
warn "$label exited nonzero after completion markers; treating as pass"
return 0 fi if [[ "$label" == "macOS update" ]] && verify_macos_update_after_transport_loss "$UPDATE_EXPECTED_NEEDLE"; then
warn "$label transport failed after product verification passed; treating as pass"
return 0 fi if [[ "$label" == "Windows update" ]] && verify_windows_update_after_transport_loss "$UPDATE_EXPECTED_NEEDLE"; then
warn "$label transport failed after product verification passed; treating as pass"
return 0 fi
warn "$label failed" if [[ -n "$log_path" ]]; then
dump_log_tail "$label""$log_path" fi
return 1
}
text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace") if"==> update.done" in text:
raise SystemExit(0) if'"finalAssistantRawText": "OK"' in text:
raise SystemExit(0) if'"finalAssistantVisibleText": "OK"' in text:
raise SystemExit(0)
raise SystemExit(1)
PY
}
verify_macos_update_after_transport_loss() {
local expected_needle="$1"
local script_path="/tmp/openclaw-npm-update-macos-recover.sh" cat <<EOF | prlctl exec "$MACOS_VM" /usr/bin/tee "$script_path" >/dev/null
set -euo pipefail export PATH=/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin export OPENCLAW_PLUGIN_STAGE_DIR="\$HOME/.openclaw/plugin-runtime-deps-parallels"
busy="\$(/bin/ps -axo command | /usr/bin/egrep 'openclaw update|npm install|pnpm install|pnpm run build' | /usr/bin/egrep -v 'egrep|openclaw-npm-update-macos-recover' || true)"
gateway_listener_ready() {
/usr/sbin/lsof -tiTCP:18789 -sTCP:LISTEN >/dev/null 2>&1
}
gateway_log_ready() {
latest="\$(/bin/ls -t /tmp/openclaw/openclaw-*.log 2>/dev/null | /usr/bin/head -n 1 || true)"
[ -n "\$latest" ] || return 1
/usr/bin/tail -n 160 "\$latest" | /usr/bin/grep -q 'ready ('
}
gateway_smoke_ready() {
gateway_listener_ready && gateway_log_ready
} if [ -n "\$busy" ]; then
printf 'update still has active npm/pnpm/openclaw processes\n%s\n'"\$busy" >&2
exit 1 fi
version="\$(/opt/homebrew/bin/openclaw --version)"
printf '%s\n'"\$version" if [ -n "$expected_needle" ]; then
case "\$version" in
*"$expected_needle"*) ;;
*) echo"version mismatch after transport loss: expected substring $expected_needle" >&2
exit 1
;;
esac fi
gateway_smoke_ready || /opt/homebrew/bin/openclaw gateway restart || true
gateway_ready=0 for _ in 1 2 3 4 5 6; do if gateway_smoke_ready; then
gateway_ready=1
break fi
sleep 2 done if [ "\$gateway_ready" != "1" ]; then
/opt/homebrew/bin/openclaw gateway start || true for _ in 1 2 3 4 5 6; do if gateway_smoke_ready; then
gateway_ready=1
break fi
sleep 2 done fi if [ "\$gateway_ready" != "1" ]; then echo"gateway did not become ready after transport recovery" >&2
exit 1 fi
workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}"
mkdir -p "\$workspace/.openclaw" cat > "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' # Identity
- Name: OpenClaw
- Purpose: Parallels npm update smoke test assistant.
IDENTITY_EOF cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF'
{ "version": 1, "setupCompletedAt": "2026-01-01T00:00:00.000Z"
}
STATE_EOF rm -f "\$workspace/BOOTSTRAP.md"
/opt/homebrew/bin/openclaw models set "$MODEL_ID"
/opt/homebrew/bin/openclaw agent --agent main --session-id "parallels-npm-update-macos-transport-recovery-$expected_needle" --message "Reply with exact ASCII text OK only." --json
EOF
macos_desktop_user_exec /bin/bash "$script_path"
}
verify_windows_update_after_transport_loss() {
local expected_needle="$1"
local provider_key_b64
provider_key_b64="$(
PROVIDER_KEY="$API_KEY_VALUE""$PYTHON_BIN" - <<'PY' import base64 import os
start_timeout_guard() {
local label="$1"
local timeout_s="$2"
local pid="$3"
local log_path="${4:-}"
(
sleep "$timeout_s" if kill -0 "$pid" >/dev/null 2>&1; then
warn "$label exceeded ${timeout_s}s; stopping" if [[ -n "$log_path" ]]; then
dump_log_tail "$label""$log_path" fi
terminate_process_tree "$pid" TERM
sleep 2
terminate_process_tree "$pid" KILL fi
) >&2 &
printf '%s\n'"$!"
}
terminate_process_tree() {
local pid="$1"
local signal_name="${2:-TERM}"
local child
pgrep -P "$pid" 2>/dev/null | while read -r child; do
terminate_process_tree "$child""$signal_name" done
kill "-$signal_name""$pid" >/dev/null 2>&1 || true
}
monitor_jobs_progress() {
local group="$1"
shift
parallels_monitor_jobs_progress "$group""$PROGRESS_INTERVAL_S""$PROGRESS_STALE_S""$PYTHON_BIN""$$""$@"
}
extract_last_version() {
local log_path="$1" "$PYTHON_BIN" - "$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)
matches = [match for match in matches if re.search(r"OpenClaw \d", match)]
print(matches[-1] if matches else"")
PY
}
guest_powershell() {
local script="$1"
local encoded
encoded="$(
SCRIPT_CONTENT="$script""$PYTHON_BIN" - <<'PY' import base64 import os
guest_powershell_poll() {
local timeout_s="$1"
local script="$2"
local encoded
encoded="$(
SCRIPT_CONTENT="$script""$PYTHON_BIN" - <<'PY' import base64 import os
run_windows_script_via_log() {
local script_url="$1"
local update_target="$2"
local expected_needle="$3"
local session_id="$4"
local model_id="$5"
local provider_key_env="$6"
local provider_key="$7"
local runner_name log_name done_name done_status launcher_state guest_log
local start_seconds poll_deadline startup_checked poll_rc state_rc log_rc
local log_state_path provider_key_b64
runner_name="openclaw-update-$RANDOM-$RANDOM.ps1"
log_name="openclaw-update-$RANDOM-$RANDOM.log"
done_name="openclaw-update-$RANDOM-$RANDOM.done"
log_state_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-update-log-state.XXXXXX")"
: >"$log_state_path"
provider_key_b64="$(
PROVIDER_KEY="$provider_key""$PYTHON_BIN" - <<'PY' import base64 import os
while :; do
set +e
done_status="$(
guest_powershell_poll 60 "\$done = Join-Path \$env:TEMP '$done_name'; if (Test-Path \$done) { (Get-Content \$done -Raw).Trim() }"
)"
poll_rc=$?
set -e
done_status="${done_status//$'\r'/}" if [[ $poll_rc -ne 0 ]]; then
warn "windows update helper poll failed; retrying" if (( SECONDS >= poll_deadline )); then
warn "windows update helper timed out while polling done file"
return 1 fi
sleep 2
continue fi
set +e
stream_windows_update_log
log_rc=$?
set -e if [[ $log_rc -ne 0 ]]; then
warn "windows update helper live log poll failed; retrying" fi if [[ -n "$done_status" ]]; then if ! stream_windows_update_log; then
warn "windows update helper log drain failed after completion" fi rm -f "$log_state_path"
[[ "$done_status" == "0" ]]
return $? fi if [[ "$startup_checked" -eq 0 && $((SECONDS - start_seconds)) -ge 20 ]]; then
set +e
launcher_state="$(
guest_powershell_poll 60 "\$runner = Join-Path \$env:TEMP '$runner_name'; \$log = Join-Path \$env:TEMP '$log_name'; \$done = Join-Path \$env:TEMP '$done_name'; 'runner=' + (Test-Path \$runner) + ' log=' + (Test-Path \$log) + ' done=' + (Test-Path \$done)"
)"
state_rc=$?
set -e
launcher_state="${launcher_state//$'\r'/}"
startup_checked=1 if [[ $state_rc -eq 0 && "$launcher_state" == *"runner=False"* && "$launcher_state" == *"log=False"* && "$launcher_state" == *"done=False"* ]]; then
warn "windows update helper failed to materialize guest files"
return 1 fi fi if (( SECONDS >= poll_deadline )); then if ! stream_windows_update_log; then
warn "windows update helper log drain failed after timeout" fi rm -f "$log_state_path"
warn "windows update helper timed out waiting for done file"
return 1 fi
sleep 2 done
}
run_macos_update() {
local update_target="$1"
local expected_needle="$2" cat <<EOF | prlctl exec "$MACOS_VM" /usr/bin/tee /tmp/openclaw-main-update.sh >/dev/null
set -euo pipefail export PATH=/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin if [ -z "\${HOME:-}" ]; thenexport HOME="/Users/\$(id -un)"; fi export OPENCLAW_PLUGIN_STAGE_DIR="\$HOME/.openclaw/plugin-runtime-deps-parallels" if [ -z "\${$API_KEY_ENV:-}" ]; then echo"$API_KEY_ENV is required in the macOS update environment" >&2
exit 1 fi
cd "\$HOME"
gateway_listener_ready() {
/usr/sbin/lsof -tiTCP:18789 -sTCP:LISTEN >/dev/null 2>&1
}
gateway_log_ready() {
latest="\$(/bin/ls -t /tmp/openclaw/openclaw-*.log 2>/dev/null | /usr/bin/head -n 1 || true)"
[ -n "\$latest" ] || return 1
/usr/bin/tail -n 160 "\$latest" | /usr/bin/grep -q 'ready ('
}
gateway_smoke_ready() {
gateway_listener_ready && gateway_log_ready
}
scrub_future_plugin_entries() {
node - <<'JS' || true
const fs = require("fs");
const os = require("os");
const path = require("path");
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json"); if (!fs.existsSync(configPath)) process.exit(0);
let config;
try {
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
} catch {
process.exit(0);
}
const plugins = config?.plugins; if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) process.exit(0);
const entries = plugins.entries; if (entries && typeof entries === "object" && !Array.isArray(entries)) { delete entries.feishu; delete entries.whatsapp;
} if (Array.isArray(plugins.allow)) {
plugins.allow = plugins.allow.filter((pluginId) => pluginId !== "feishu" && pluginId !== "whatsapp");
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\\n");
JS
}
stop_openclaw_gateway_processes() {
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw gateway stop >/dev/null 2>&1 || true
/usr/bin/pkill -9 -f openclaw-gateway || true
/usr/bin/pkill -9 -f 'openclaw gateway run' || true
/usr/bin/pkill -9 -f 'openclaw.mjs gateway' || true for pid in \$(/usr/sbin/lsof -tiTCP:18789 -sTCP:LISTEN 2>/dev/null || true); do
/bin/kill -9 "\$pid" 2>/dev/null || true done
} # Stop the pre-update gateway before replacing the package. Otherwise the old # host can observe new plugin metadata mid-update and abort config validation.
scrub_future_plugin_entries
stop_openclaw_gateway_processes
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw update --tag "$update_target" --yes --json # Same-guest npm upgrades can leave the old gateway process holding the old # bundled plugin host version. Stop it before post-update config commands.
stop_openclaw_gateway_processes
version="\$(/opt/homebrew/bin/openclaw --version)"
printf '%s\n'"\$version" if [ -n "$expected_needle" ]; then
case "\$version" in
*"$expected_needle"*) ;;
*) echo"version mismatch: expected substring $expected_needle" >&2
exit 1
;;
esac fi
/opt/homebrew/bin/openclaw update status --json
/opt/homebrew/bin/openclaw models set "$MODEL_ID" # Same-guest npm upgrades can leave launchd holding the old gateway process or # module graph briefly; wait for a fresh RPC-ready restart before the agent turn. # Fresh npm installs may not have a launchd service yet, so fall back to the # same manual gateway launch used by the fresh macOS lane.
/opt/homebrew/bin/openclaw gateway restart || true
gateway_ready=0 for _ in 1 2 3 4 5 6 7 8; do if gateway_smoke_ready; then
gateway_ready=1
break fi
sleep 2 done if [ "\$gateway_ready" != "1" ]; then
stop_openclaw_gateway_processes
/opt/homebrew/bin/openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-npm-update-macos-gateway.log 2>&1 </dev/null & for _ in 1 2 3 4 5 6 7 8; do if gateway_smoke_ready; then
gateway_ready=1
break fi
sleep 2 done fi if [ "\$gateway_ready" != "1" ]; then
tail -n 120 /tmp/openclaw-parallels-npm-update-macos-gateway.log 2>/dev/null || true fi if [ "\$gateway_ready" != "1" ]; then
/opt/homebrew/bin/openclaw gateway status --deep --require-rpc fi
workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}"
mkdir -p "\$workspace/.openclaw" cat > "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' # Identity
- Name: OpenClaw
- Purpose: Parallels npm update smoke test assistant.
IDENTITY_EOF cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF'
{ "version": 1, "setupCompletedAt": "2026-01-01T00:00:00.000Z"
}
STATE_EOF rm -f "\$workspace/BOOTSTRAP.md"
/opt/homebrew/bin/openclaw agent --agent main --session-id parallels-npm-update-macos-$expected_needle --message "Reply with exact ASCII text OK only." --json
EOF
macos_desktop_user_exec /bin/bash /tmp/openclaw-main-update.sh
}
run_windows_update() {
local update_target="$1"
local expected_needle="$2"
local script_url="$3"
run_windows_script_via_log \ "$script_url" \ "$update_target" \ "$expected_needle" \ "parallels-npm-update-windows-$expected_needle" \ "$MODEL_ID" \ "$API_KEY_ENV" \ "$API_KEY_VALUE"
}
run_linux_update() {
local update_target="$1"
local expected_needle="$2" cat <<EOF | prlctl exec "$LINUX_VM" /usr/bin/tee /tmp/openclaw-main-update.sh >/dev/null
set -euo pipefail export HOME=/root
cd "\$HOME"
scrub_future_plugin_entries() {
node - <<'JS' || true
const fs = require("fs");
const os = require("os");
const path = require("path");
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json"); if (!fs.existsSync(configPath)) process.exit(0);
let config;
try {
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
} catch {
process.exit(0);
}
const plugins = config?.plugins; if (!plugins || typeof plugins !== "object" || Array.isArray(plugins)) process.exit(0);
const entries = plugins.entries; if (entries && typeof entries === "object" && !Array.isArray(entries)) { delete entries.feishu; delete entries.whatsapp;
} if (Array.isArray(plugins.allow)) {
plugins.allow = plugins.allow.filter((pluginId) => pluginId !== "feishu" && pluginId !== "whatsapp");
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\\n");
JS
}
stop_openclaw_gateway_processes() {
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw gateway stop >/dev/null 2>&1 || true
pkill -9 -f openclaw-gateway || true
pkill -9 -f 'openclaw gateway run' || true
pkill -9 -f 'openclaw.mjs gateway' || true if command -v fuser >/dev/null 2>&1; then
fuser -k 18789/tcp >/dev/null 2>&1 || true fi if command -v lsof >/dev/null 2>&1; then for pid in \$(lsof -tiTCP:18789 -sTCP:LISTEN 2>/dev/null || true); do
kill -9 "\$pid" 2>/dev/null || true done fi
} # Stop the pre-update manual gateway before replacing the package. Otherwise # the old host can observe new plugin metadata mid-update and abort validation.
scrub_future_plugin_entries
stop_openclaw_gateway_processes
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 openclaw update --tag "$update_target" --yes --json # The fresh Linux lane starts a manual gateway; stop the old process before # post-update config validation sees mixed old-host/new-plugin metadata.
stop_openclaw_gateway_processes
version="\$(openclaw --version)"
printf '%s\n'"\$version" if [ -n "$expected_needle" ]; then
case "\$version" in
*"$expected_needle"*) ;;
*) echo"version mismatch: expected substring $expected_needle" >&2
exit 1
;;
esac fi
openclaw update status --json
openclaw models set "$MODEL_ID"
workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}"
mkdir -p "\$workspace/.openclaw" cat > "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' # Identity
LATEST_VERSION="$(resolve_latest_version)" if [[ -z "$PACKAGE_SPEC" ]]; then
PACKAGE_SPEC="openclaw@$LATEST_VERSION" fi
resolve_current_head
if platform_enabled linux; then
RESOLVED_LINUX_VM="$(resolve_linux_vm_name)" if [[ "$RESOLVED_LINUX_VM" != "$LINUX_VM" ]]; then
warn "requested VM $LINUX_VM not found; using $RESOLVED_LINUX_VM"
LINUX_VM="$RESOLVED_LINUX_VM" fi fi
say "Run fresh npm baseline: $PACKAGE_SPEC"
say "Platforms: $RUN_PLATFORMS"
say "Run dir: $RUN_DIR"
fresh_monitor_args=() if platform_enabled macos; then
bash "$ROOT_DIR/scripts/e2e/parallels-macos-smoke.sh" \
--mode fresh \
--provider "$PROVIDER" \
--api-key-env "$API_KEY_ENV" \
--target-package-spec "$PACKAGE_SPEC" \
--json >"$RUN_DIR/macos-fresh.log" 2>&1 &
macos_fresh_pid=$!
fresh_monitor_args+=("macOS""$macos_fresh_pid""$RUN_DIR/macos-fresh.log") fi
if platform_enabled windows; then
bash "$ROOT_DIR/scripts/e2e/parallels-windows-smoke.sh" \
--mode fresh \
--provider "$PROVIDER" \
--api-key-env "$API_KEY_ENV" \
--target-package-spec "$PACKAGE_SPEC" \
--json >"$RUN_DIR/windows-fresh.log" 2>&1 &
windows_fresh_pid=$!
fresh_monitor_args+=("Windows""$windows_fresh_pid""$RUN_DIR/windows-fresh.log") fi
if platform_enabled linux; then
bash "$ROOT_DIR/scripts/e2e/parallels-linux-smoke.sh" \
--mode fresh \
--provider "$PROVIDER" \
--api-key-env "$API_KEY_ENV" \
--target-package-spec "$PACKAGE_SPEC" \
--json >"$RUN_DIR/linux-fresh.log" 2>&1 &
linux_fresh_pid=$!
fresh_monitor_args+=("Linux""$linux_fresh_pid""$RUN_DIR/linux-fresh.log") fi
if platform_enabled macos; then
wait_job "macOS fresh""$macos_fresh_pid""$RUN_DIR/macos-fresh.log" && MACOS_FRESH_STATUS="pass" || MACOS_FRESH_STATUS="fail"
[[ "$MACOS_FRESH_STATUS" == "pass" ]] || die "macOS fresh baseline failed" fi if platform_enabled windows; then
wait_job "Windows fresh""$windows_fresh_pid""$RUN_DIR/windows-fresh.log" && WINDOWS_FRESH_STATUS="pass" || WINDOWS_FRESH_STATUS="fail"
[[ "$WINDOWS_FRESH_STATUS" == "pass" ]] || die "Windows fresh baseline failed" fi if platform_enabled linux; then
wait_job "Linux fresh""$linux_fresh_pid""$RUN_DIR/linux-fresh.log" && LINUX_FRESH_STATUS="pass" || LINUX_FRESH_STATUS="fail"
[[ "$LINUX_FRESH_STATUS" == "pass" ]] || die "Linux fresh baseline failed" fi
if [[ -z "$UPDATE_TARGET" || "$UPDATE_TARGET" == "local-main" ]]; then
pack_main_tgz
UPDATE_TARGET_EFFECTIVE="http://$HOST_IP:$HOST_PORT/$(basename "$MAIN_TGZ_PATH")"
UPDATE_EXPECTED_NEEDLE="$CURRENT_HEAD_SHORT" else
UPDATE_TARGET_EFFECTIVE="$UPDATE_TARGET" if is_explicit_package_target "$UPDATE_TARGET_EFFECTIVE"; then
UPDATE_EXPECTED_NEEDLE="" else
UPDATE_EXPECTED_NEEDLE="$(resolve_registry_target_version "$UPDATE_TARGET_EFFECTIVE")"
[[ -n "$UPDATE_EXPECTED_NEEDLE" ]] || UPDATE_EXPECTED_NEEDLE="$UPDATE_TARGET_EFFECTIVE" fi fi if platform_enabled windows; then
write_windows_update_script fi if [[ -n "$MAIN_TGZ_PATH" ]] || platform_enabled windows; then
start_server fi
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.