mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 23:56:07 +00:00
ci: verify and sync website installers (#80067)
* ci: verify and sync website installers * test: fix pi runner boundary test type cast * fix(installer): scope Windows legacy cleanup to git checkout * ci: install curl for minimal install-cli smoke * fix(installer): promote supported Linux node after install * test(cli): align command hint expectations * fix(installer): avoid shellcheck warning in node promotion * fix(installer): sync Linux path hardening * ci: raise build artifact testbox heap * test(installer): align PowerShell installer tests
This commit is contained in:
committed by
GitHub
parent
71ebedee95
commit
e60928d13c
@@ -147,6 +147,8 @@ jobs:
|
||||
|
||||
- name: Build dist on cache miss
|
||||
if: steps.dist-cache.outputs.cache-hit != 'true'
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
run: pnpm build:ci-artifacts
|
||||
|
||||
- name: Build Control UI on cache miss
|
||||
|
||||
188
.github/workflows/website-installer-sync.yml
vendored
Normal file
188
.github/workflows/website-installer-sync.yml
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
name: Website Installer Sync
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- scripts/install.sh
|
||||
- scripts/install-cli.sh
|
||||
- scripts/install.ps1
|
||||
- scripts/install.cmd
|
||||
- .github/workflows/website-installer-sync.yml
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- scripts/install.sh
|
||||
- scripts/install-cli.sh
|
||||
- scripts/install.ps1
|
||||
- scripts/install.cmd
|
||||
- .github/workflows/website-installer-sync.yml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sync_website:
|
||||
description: Sync openclaw.ai after verification
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: website-installer-sync-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
static:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: sudo apt-get update -y && sudo apt-get install -y shellcheck
|
||||
|
||||
- name: Shell syntax
|
||||
run: bash -n scripts/install.sh scripts/install-cli.sh
|
||||
|
||||
- name: ShellCheck
|
||||
run: shellcheck -e SC1091 scripts/install.sh scripts/install-cli.sh
|
||||
|
||||
- name: Installer help and dry-runs
|
||||
run: |
|
||||
bash scripts/install.sh --help >/tmp/install-help.txt
|
||||
bash scripts/install.sh --dry-run --no-onboard --no-prompt
|
||||
bash scripts/install-cli.sh --help >/tmp/install-cli-help.txt
|
||||
|
||||
- name: PowerShell syntax
|
||||
shell: pwsh
|
||||
run: |
|
||||
$errors = $null
|
||||
$null = [System.Management.Automation.PSParser]::Tokenize(
|
||||
(Get-Content -Raw scripts/install.ps1),
|
||||
[ref]$errors
|
||||
)
|
||||
if ($errors -and $errors.Count -gt 0) {
|
||||
$errors | Format-List | Out-String | Write-Error
|
||||
exit 1
|
||||
}
|
||||
|
||||
linux-docker:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: install.sh in Docker
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e OPENCLAW_NO_ONBOARD=1 \
|
||||
-e OPENCLAW_NO_PROMPT=1 \
|
||||
-v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
|
||||
node:24-bookworm-slim \
|
||||
bash -lc 'bash /tmp/install.sh --no-prompt --no-onboard --version latest && openclaw --version'
|
||||
|
||||
- name: install-cli.sh in Docker
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e OPENCLAW_NO_ONBOARD=1 \
|
||||
-e OPENCLAW_NO_PROMPT=1 \
|
||||
-v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \
|
||||
node:24-bookworm-slim \
|
||||
bash -lc 'apt-get update -y && apt-get install -y curl && bash /tmp/install-cli.sh --prefix /tmp/openclaw --no-onboard --version latest && /tmp/openclaw/bin/openclaw --version'
|
||||
|
||||
macos-installer:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: install.sh dry run
|
||||
run: bash scripts/install.sh --dry-run --no-onboard --no-prompt
|
||||
|
||||
windows-installer:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: install.ps1 dry run
|
||||
shell: pwsh
|
||||
run: .\scripts\install.ps1 -DryRun -NoOnboard -InstallMethod npm
|
||||
|
||||
- name: install.cmd dry run
|
||||
shell: cmd
|
||||
run: set "OPENCLAW_INSTALL_PS1_URL=%GITHUB_WORKSPACE%\scripts\install.ps1" && .\scripts\install.cmd --dry-run --no-onboard --npm
|
||||
|
||||
sync-website:
|
||||
needs: [static, linux-docker, macos-installer, windows-installer]
|
||||
if: >
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.sync_website)
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout OpenClaw
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: openclaw
|
||||
|
||||
- name: Checkout openclaw.ai
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/openclaw.ai
|
||||
token: ${{ secrets.OPENCLAW_GH_TOKEN }}
|
||||
path: openclaw.ai
|
||||
|
||||
- name: Sync installer scripts
|
||||
run: |
|
||||
cp openclaw/scripts/install.sh openclaw.ai/public/install.sh
|
||||
cp openclaw/scripts/install-cli.sh openclaw.ai/public/install-cli.sh
|
||||
cp openclaw/scripts/install.ps1 openclaw.ai/public/install.ps1
|
||||
cp openclaw/scripts/install.cmd openclaw.ai/public/install.cmd
|
||||
chmod +x openclaw.ai/public/install.sh openclaw.ai/public/install-cli.sh
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
working-directory: openclaw.ai
|
||||
run: |
|
||||
if git diff --quiet -- public/install.sh public/install-cli.sh public/install.ps1 public/install.cmd; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install ShellCheck
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: sudo apt-get update -y && sudo apt-get install -y shellcheck
|
||||
|
||||
- name: Verify website with synced installers
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: openclaw.ai
|
||||
run: |
|
||||
bash -n public/install.sh public/install-cli.sh
|
||||
shellcheck -e SC1091 public/install.sh public/install-cli.sh
|
||||
bun install --frozen-lockfile
|
||||
bun run build
|
||||
|
||||
- name: Commit and push website sync
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: openclaw.ai
|
||||
run: |
|
||||
git config user.name "openclaw-installer-sync[bot]"
|
||||
git config user.email "openclaw-installer-sync[bot]@users.noreply.github.com"
|
||||
git add public/install.sh public/install-cli.sh public/install.ps1 public/install.cmd
|
||||
git commit -m "chore: sync installers from openclaw ${GITHUB_SHA::12}"
|
||||
git push origin HEAD:main
|
||||
@@ -35,7 +35,7 @@ const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/;
|
||||
const NODE_SCOPE_RE =
|
||||
/^(src\/|test\/|extensions\/|packages\/|scripts\/|ui\/|\.github\/|openclaw\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig.*\.json$|vitest.*\.ts$|tsdown\.config\.ts$|\.oxlintrc\.json$|\.oxfmtrc\.jsonc$)/;
|
||||
const WINDOWS_SCOPE_RE =
|
||||
/^(src\/process\/|src\/infra\/windows-install-roots\.ts$|src\/plugins\/import-specifier(?:\.test)?\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|scripts\/(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|js)$|test\/scripts\/(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/ci\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/;
|
||||
/^(src\/process\/|src\/infra\/windows-install-roots\.ts$|src\/plugins\/import-specifier(?:\.test)?\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|scripts\/(?:install\.ps1|install\.cmd|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|js))$|test\/scripts\/(?:install-ps1|npm-runner|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/ci\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/;
|
||||
const WINDOWS_TEST_SCOPE_RE =
|
||||
/^(src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/windows-install-roots\.test\.ts$|src\/plugins\/import-specifier\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|test\/scripts\/(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/;
|
||||
const TEST_ONLY_PATH_RE =
|
||||
@@ -47,7 +47,7 @@ const NATIVE_ONLY_RE =
|
||||
const FAST_INSTALL_SMOKE_SCOPE_RE =
|
||||
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|agents-delete-shared-workspace-docker\.sh|gateway-network-docker\.sh)$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
|
||||
const FULL_INSTALL_SMOKE_SCOPE_RE =
|
||||
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install\.sh$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|qr-import-docker\.sh|bun-global-install-smoke\.sh)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
|
||||
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install(?:-cli)?\.sh$|scripts\/install\.ps1$|scripts\/install\.cmd$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|qr-import-docker\.sh|bun-global-install-smoke\.sh)$|\.github\/workflows\/(?:install-smoke|website-installer-sync)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
|
||||
const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE = /^src\/(?:channels|gateway|plugin-sdk|plugins)\//;
|
||||
const NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE =
|
||||
/^(src\/plugins\/contracts\/(?:inventory\/bundled-capability-metadata|registry|tts-contract-suites)\.ts$|scripts\/test-projects(?:\.test-support)?\.mjs$|test\/scripts\/test-projects\.test\.ts$)/;
|
||||
|
||||
748
scripts/install-cli.sh
Executable file
748
scripts/install-cli.sh
Executable file
@@ -0,0 +1,748 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# OpenClaw CLI installer (non-interactive, no onboarding)
|
||||
# Usage: curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash -s -- [--json] [--prefix <path>] [--version <ver>] [--node-version <ver>] [--onboard]
|
||||
|
||||
ensure_home_env() {
|
||||
if [[ -n "${HOME:-}" && "${HOME}" != "/" && -d "${HOME}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local user_name=""
|
||||
local home_dir=""
|
||||
user_name="$(id -un 2>/dev/null || true)"
|
||||
|
||||
if [[ -n "$user_name" ]]; then
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
home_dir="$(getent passwd "$user_name" 2>/dev/null | awk -F: '{print $6; exit}' || true)"
|
||||
fi
|
||||
if [[ -z "$home_dir" && "$(uname -s 2>/dev/null || true)" == "Darwin" ]] && command -v dscl >/dev/null 2>&1; then
|
||||
home_dir="$(dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null | awk '{print $2; exit}' || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$home_dir" && "$home_dir" != "/" && -d "$home_dir" ]]; then
|
||||
export HOME="$home_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_home_env
|
||||
|
||||
PREFIX="${OPENCLAW_PREFIX:-${HOME}/.openclaw}"
|
||||
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
|
||||
NODE_VERSION="${OPENCLAW_NODE_VERSION:-22.22.0}"
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}"
|
||||
NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}"
|
||||
INSTALL_METHOD="${OPENCLAW_INSTALL_METHOD:-npm}"
|
||||
GIT_DIR="${OPENCLAW_GIT_DIR:-${HOME}/openclaw}"
|
||||
GIT_UPDATE="${OPENCLAW_GIT_UPDATE:-1}"
|
||||
JSON=0
|
||||
RUN_ONBOARD=0
|
||||
SET_NPM_PREFIX=0
|
||||
PNPM_CMD=()
|
||||
|
||||
print_usage() {
|
||||
cat <<EOF
|
||||
Usage: install-cli.sh [options]
|
||||
--json Emit NDJSON events (no human output)
|
||||
--prefix <path> Install prefix (default: ~/.openclaw)
|
||||
--install-method, --method npm|git Install via npm (default) or from a git checkout
|
||||
--npm Shortcut for --install-method npm
|
||||
--git, --github Shortcut for --install-method git
|
||||
--git-dir, --dir <path> Checkout directory (default: ~/openclaw)
|
||||
--version <ver> OpenClaw version (default: latest)
|
||||
--node-version <ver> Node version (default: 22.22.0)
|
||||
--onboard Run "openclaw onboard" after install
|
||||
--no-onboard Skip onboarding (default)
|
||||
--set-npm-prefix Force npm prefix to ~/.npm-global if current prefix is not writable (Linux)
|
||||
|
||||
Environment variables:
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=0|1 Default: 1 (avoid sharp building against global libvips)
|
||||
OPENCLAW_NPM_LOGLEVEL=error|warn|notice Default: error (hide npm deprecation noise)
|
||||
OPENCLAW_INSTALL_METHOD=git|npm
|
||||
OPENCLAW_VERSION=latest|next|<semver>
|
||||
OPENCLAW_GIT_DIR=...
|
||||
OPENCLAW_GIT_UPDATE=0|1
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
if [[ "$JSON" -eq 0 ]]; then
|
||||
echo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
DOWNLOADER=""
|
||||
detect_downloader() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
DOWNLOADER="curl"
|
||||
return 0
|
||||
fi
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
DOWNLOADER="wget"
|
||||
return 0
|
||||
fi
|
||||
fail "Missing downloader (curl or wget required)"
|
||||
}
|
||||
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
if [[ -z "$DOWNLOADER" ]]; then
|
||||
detect_downloader
|
||||
fi
|
||||
if [[ "$DOWNLOADER" == "curl" ]]; then
|
||||
curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url"
|
||||
return
|
||||
fi
|
||||
wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url"
|
||||
}
|
||||
|
||||
cleanup_legacy_submodules() {
|
||||
local repo_dir="${1:-${OPENCLAW_GIT_DIR:-${HOME}/openclaw}}"
|
||||
local legacy_dir="${repo_dir}/Peekaboo"
|
||||
if [[ -d "$legacy_dir" ]]; then
|
||||
emit_json "{\"event\":\"step\",\"name\":\"legacy-submodule\",\"status\":\"start\",\"path\":\"${legacy_dir//\"/\\\"}\"}"
|
||||
log "Removing legacy submodule checkout: ${legacy_dir}"
|
||||
rm -rf "$legacy_dir"
|
||||
emit_json "{\"event\":\"step\",\"name\":\"legacy-submodule\",\"status\":\"ok\",\"path\":\"${legacy_dir//\"/\\\"}\"}"
|
||||
fi
|
||||
}
|
||||
|
||||
sha256_file() {
|
||||
local file="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$file" | awk '{print $1}'
|
||||
return 0
|
||||
fi
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" | awk '{print $1}'
|
||||
return 0
|
||||
fi
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl dgst -sha256 "$file" | awk '{print $NF}'
|
||||
return 0
|
||||
fi
|
||||
fail "Missing sha256 tool (need sha256sum, shasum, or openssl)"
|
||||
}
|
||||
|
||||
emit_json() {
|
||||
if [[ "$JSON" -eq 1 ]]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
fail() {
|
||||
local msg="$1"
|
||||
emit_json "{\"event\":\"error\",\"message\":\"${msg//\"/\\\"}\"}"
|
||||
log "ERROR: $msg"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_bin() {
|
||||
local name="$1"
|
||||
if ! command -v "$name" >/dev/null 2>&1; then
|
||||
fail "Missing required binary: $name"
|
||||
fi
|
||||
}
|
||||
|
||||
has_sudo() {
|
||||
command -v sudo >/dev/null 2>&1
|
||||
}
|
||||
|
||||
is_root() {
|
||||
[[ "$(id -u)" -eq 0 ]]
|
||||
}
|
||||
|
||||
ensure_git() {
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
emit_json '{"event":"step","name":"git","status":"ok"}'
|
||||
return
|
||||
fi
|
||||
|
||||
emit_json '{"event":"step","name":"git","status":"start"}'
|
||||
log "Installing Git (required for npm installs)..."
|
||||
|
||||
case "$(os_detect)" in
|
||||
linux)
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if is_root; then
|
||||
apt-get update -y
|
||||
apt-get install -y git
|
||||
elif has_sudo; then
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y git
|
||||
else
|
||||
fail "Git missing and sudo unavailable. Install git and retry."
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if is_root; then
|
||||
dnf install -y git
|
||||
elif has_sudo; then
|
||||
sudo dnf install -y git
|
||||
else
|
||||
fail "Git missing and sudo unavailable. Install git and retry."
|
||||
fi
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
if is_root; then
|
||||
yum install -y git
|
||||
elif has_sudo; then
|
||||
sudo yum install -y git
|
||||
else
|
||||
fail "Git missing and sudo unavailable. Install git and retry."
|
||||
fi
|
||||
else
|
||||
fail "Git missing and package manager not found. Install git and retry."
|
||||
fi
|
||||
;;
|
||||
darwin)
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew install git
|
||||
else
|
||||
fail "Git missing. Install Xcode Command Line Tools or Homebrew Git, then retry."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
fail "Git install failed. Install git manually and retry."
|
||||
fi
|
||||
|
||||
emit_json '{"event":"step","name":"git","status":"ok"}'
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--json)
|
||||
JSON=1
|
||||
shift
|
||||
;;
|
||||
--prefix)
|
||||
PREFIX="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
OPENCLAW_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--node-version)
|
||||
NODE_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--install-method|--method)
|
||||
INSTALL_METHOD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--npm)
|
||||
INSTALL_METHOD="npm"
|
||||
shift
|
||||
;;
|
||||
--git|--github)
|
||||
INSTALL_METHOD="git"
|
||||
shift
|
||||
;;
|
||||
--git-dir|--dir)
|
||||
GIT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-git-update)
|
||||
GIT_UPDATE=0
|
||||
shift
|
||||
;;
|
||||
--onboard)
|
||||
RUN_ONBOARD=1
|
||||
shift
|
||||
;;
|
||||
--no-onboard)
|
||||
RUN_ONBOARD=0
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
print_usage
|
||||
exit 0
|
||||
;;
|
||||
--set-npm-prefix)
|
||||
SET_NPM_PREFIX=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
fail "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
os_detect() {
|
||||
local os
|
||||
os="$(uname -s)"
|
||||
case "$os" in
|
||||
Darwin) echo "darwin" ;;
|
||||
Linux) echo "linux" ;;
|
||||
*) fail "Unsupported OS: $os" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
arch_detect() {
|
||||
local arch
|
||||
arch="$(uname -m)"
|
||||
case "$arch" in
|
||||
arm64|aarch64) echo "arm64" ;;
|
||||
x86_64|amd64) echo "x64" ;;
|
||||
*) fail "Unsupported architecture: $arch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
node_dir() {
|
||||
echo "${PREFIX}/tools/node-v${NODE_VERSION}"
|
||||
}
|
||||
|
||||
node_bin() {
|
||||
echo "$(node_dir)/bin/node"
|
||||
}
|
||||
|
||||
npm_bin() {
|
||||
echo "$(node_dir)/bin/npm"
|
||||
}
|
||||
|
||||
set_pnpm_cmd() {
|
||||
PNPM_CMD=("$@")
|
||||
}
|
||||
|
||||
pnpm_cmd_is_ready() {
|
||||
if [[ ${#PNPM_CMD[@]} -eq 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
"${PNPM_CMD[@]}" --version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
detect_pnpm_cmd() {
|
||||
if [[ -x "${PREFIX}/bin/pnpm" ]]; then
|
||||
set_pnpm_cmd "${PREFIX}/bin/pnpm"
|
||||
return 0
|
||||
fi
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
set_pnpm_cmd pnpm
|
||||
return 0
|
||||
fi
|
||||
if [[ -x "$(node_dir)/bin/corepack" ]] && "$(node_dir)/bin/corepack" pnpm --version >/dev/null 2>&1; then
|
||||
set_pnpm_cmd "$(node_dir)/bin/corepack" pnpm
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_pnpm_binary_for_scripts() {
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ${#PNPM_CMD[@]} -eq 2 && "${PNPM_CMD[1]}" == "pnpm" ]] && [[ "$(basename "${PNPM_CMD[0]}")" == "corepack" ]]; then
|
||||
mkdir -p "${PREFIX}/bin"
|
||||
cat > "${PREFIX}/bin/pnpm" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec "${PNPM_CMD[0]}" pnpm "\$@"
|
||||
EOF
|
||||
chmod +x "${PREFIX}/bin/pnpm"
|
||||
export PATH="${PREFIX}/bin:${PATH}"
|
||||
hash -r 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if command -v pnpm >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail "pnpm command not available on PATH"
|
||||
}
|
||||
|
||||
run_pnpm() {
|
||||
if ! pnpm_cmd_is_ready; then
|
||||
ensure_pnpm
|
||||
fi
|
||||
"${PNPM_CMD[@]}" "$@"
|
||||
}
|
||||
|
||||
install_node() {
|
||||
local os
|
||||
local arch
|
||||
local url
|
||||
local tmp
|
||||
local dir
|
||||
local current_major
|
||||
local base_url
|
||||
local tarball
|
||||
local expected_sha
|
||||
local actual_sha
|
||||
|
||||
os="$(os_detect)"
|
||||
arch="$(arch_detect)"
|
||||
dir="$(node_dir)"
|
||||
|
||||
if [[ -x "$(node_bin)" ]]; then
|
||||
current_major="$("$(node_bin)" -v 2>/dev/null | tr -d 'v' | cut -d'.' -f1 || echo "")"
|
||||
if [[ -n "$current_major" && "$current_major" -ge 22 ]]; then
|
||||
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"skip\",\"path\":\"${dir//\"/\\\\\\\"}\"}"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"start\",\"version\":\"${NODE_VERSION}\"}"
|
||||
log "Installing Node ${NODE_VERSION} (user-space)..."
|
||||
|
||||
mkdir -p "${PREFIX}/tools"
|
||||
tmp="$(mktemp -d)"
|
||||
base_url="https://nodejs.org/dist/v${NODE_VERSION}"
|
||||
tarball="node-v${NODE_VERSION}-${os}-${arch}.tar.gz"
|
||||
url="${base_url}/${tarball}"
|
||||
|
||||
detect_downloader
|
||||
require_bin tar
|
||||
|
||||
download_file "${base_url}/SHASUMS256.txt" "$tmp/SHASUMS256.txt"
|
||||
expected_sha="$(grep " ${tarball}$" "$tmp/SHASUMS256.txt" | awk '{print $1}' | head -n 1 || true)"
|
||||
if [[ -z "${expected_sha}" ]]; then
|
||||
fail "Failed to resolve Node shasum for ${tarball}"
|
||||
fi
|
||||
|
||||
download_file "$url" "$tmp/node.tgz"
|
||||
actual_sha="$(sha256_file "$tmp/node.tgz")"
|
||||
if [[ "$actual_sha" != "$expected_sha" ]]; then
|
||||
fail "Node tarball sha256 mismatch for ${tarball} (expected ${expected_sha}, got ${actual_sha})"
|
||||
fi
|
||||
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir"
|
||||
tar -xzf "$tmp/node.tgz" -C "$dir" --strip-components=1
|
||||
rm -rf "$tmp"
|
||||
|
||||
ln -sfn "$dir" "${PREFIX}/tools/node"
|
||||
|
||||
if ! "$(node_bin)" -e "require('node:sqlite')" >/dev/null 2>&1; then
|
||||
fail "Installed Node ${NODE_VERSION} is missing node:sqlite; re-run with --node-version 22.22.0 (or newer)"
|
||||
fi
|
||||
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"ok\",\"version\":\"${NODE_VERSION}\"}"
|
||||
}
|
||||
|
||||
ensure_pnpm() {
|
||||
if detect_pnpm_cmd && pnpm_cmd_is_ready; then
|
||||
local current_version
|
||||
current_version="$("${PNPM_CMD[@]}" --version 2>/dev/null || true)"
|
||||
if [[ "$current_version" =~ ^10\. ]]; then
|
||||
return 0
|
||||
fi
|
||||
log "Found pnpm ${current_version:-unknown}; upgrading to pnpm@10..."
|
||||
fi
|
||||
|
||||
if [[ -x "$(node_dir)/bin/corepack" ]]; then
|
||||
emit_json "{\"event\":\"step\",\"name\":\"pnpm\",\"status\":\"start\",\"method\":\"corepack\"}"
|
||||
log "Installing pnpm via Corepack..."
|
||||
"$(node_dir)/bin/corepack" enable >/dev/null 2>&1 || true
|
||||
"$(node_dir)/bin/corepack" prepare pnpm@10 --activate
|
||||
if detect_pnpm_cmd && pnpm_cmd_is_ready && [[ "$("${PNPM_CMD[@]}" --version 2>/dev/null || true)" =~ ^10\. ]]; then
|
||||
emit_json "{\"event\":\"step\",\"name\":\"pnpm\",\"status\":\"ok\"}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
emit_json "{\"event\":\"step\",\"name\":\"pnpm\",\"status\":\"start\",\"method\":\"npm\"}"
|
||||
log "Installing pnpm via npm..."
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" "$(npm_bin)" install -g --prefix "$PREFIX" pnpm@10
|
||||
detect_pnpm_cmd || true
|
||||
emit_json "{\"event\":\"step\",\"name\":\"pnpm\",\"status\":\"ok\"}"
|
||||
return 0
|
||||
}
|
||||
|
||||
fix_npm_prefix_if_needed() {
|
||||
# only meaningful on Linux, non-root installs
|
||||
if [[ "$(os_detect)" != "linux" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local prefix
|
||||
prefix="$("$(npm_bin)" config get prefix 2>/dev/null || true)"
|
||||
if [[ -z "$prefix" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -w "$prefix" || -w "${prefix}/lib" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local target="${HOME}/.npm-global"
|
||||
mkdir -p "$target"
|
||||
"$(npm_bin)" config set prefix "$target"
|
||||
|
||||
local path_line="export PATH=\\\"${target}/bin:\\$PATH\\\""
|
||||
for rc in "${HOME}/.bashrc" "${HOME}/.zshrc"; do
|
||||
if [[ -f "$rc" ]] && ! grep -q ".npm-global" "$rc"; then
|
||||
echo "$path_line" >> "$rc"
|
||||
fi
|
||||
done
|
||||
|
||||
export PATH="${target}/bin:${PATH}"
|
||||
emit_json "{\"event\":\"step\",\"name\":\"npm-prefix\",\"status\":\"ok\",\"prefix\":\"${target//\"/\\\"}\"}"
|
||||
log "Configured npm prefix to ${target}"
|
||||
}
|
||||
|
||||
install_openclaw() {
|
||||
local requested="${OPENCLAW_VERSION:-latest}"
|
||||
local npm_args=(
|
||||
--loglevel "$NPM_LOGLEVEL"
|
||||
--no-fund
|
||||
--no-audit
|
||||
)
|
||||
emit_json "{\"event\":\"step\",\"name\":\"openclaw\",\"status\":\"start\",\"version\":\"${requested}\"}"
|
||||
log "Installing OpenClaw (${requested})..."
|
||||
if [[ "$SET_NPM_PREFIX" -eq 1 ]]; then
|
||||
fix_npm_prefix_if_needed
|
||||
fi
|
||||
|
||||
if [[ "${requested}" == "latest" ]]; then
|
||||
if ! SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" "$(npm_bin)" install -g --prefix "$(node_dir)" "${npm_args[@]}" "openclaw@latest"; then
|
||||
log "npm install openclaw@latest failed; retrying openclaw@next"
|
||||
emit_json "{\"event\":\"step\",\"name\":\"openclaw\",\"status\":\"retry\",\"version\":\"next\"}"
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" "$(npm_bin)" install -g --prefix "$(node_dir)" "${npm_args[@]}" "openclaw@next"
|
||||
requested="next"
|
||||
fi
|
||||
else
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" "$(npm_bin)" install -g --prefix "$(node_dir)" "${npm_args[@]}" "openclaw@${requested}"
|
||||
fi
|
||||
|
||||
mkdir -p "${PREFIX}/bin"
|
||||
rm -f "${PREFIX}/bin/openclaw"
|
||||
cat > "${PREFIX}/bin/openclaw" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec "${PREFIX}/tools/node/bin/node" "$(node_dir)/lib/node_modules/openclaw/dist/entry.js" "\$@"
|
||||
EOF
|
||||
chmod +x "${PREFIX}/bin/openclaw"
|
||||
emit_json "{\"event\":\"step\",\"name\":\"openclaw\",\"status\":\"ok\",\"version\":\"${requested}\"}"
|
||||
}
|
||||
|
||||
ensure_pnpm_git_prepare_allowlist() {
|
||||
local repo_dir="$1"
|
||||
local workspace_file="${repo_dir}/pnpm-workspace.yaml"
|
||||
local package_file="${repo_dir}/package.json"
|
||||
local dep="@tloncorp/api"
|
||||
local tmp
|
||||
|
||||
if [[ -f "$workspace_file" ]] && ! grep -Fq "\"${dep}\"" "$workspace_file" && ! grep -Fq -- "- ${dep}" "$workspace_file"; then
|
||||
tmp="$(mktemp)"
|
||||
if grep -q '^onlyBuiltDependencies:[[:space:]]*$' "$workspace_file"; then
|
||||
awk -v dep="$dep" '
|
||||
BEGIN { inserted = 0 }
|
||||
{
|
||||
print
|
||||
if (!inserted && $0 ~ /^onlyBuiltDependencies:[[:space:]]*$/) {
|
||||
print " - \"" dep "\""
|
||||
inserted = 1
|
||||
}
|
||||
}
|
||||
' "$workspace_file" >"$tmp"
|
||||
else
|
||||
cat "$workspace_file" >"$tmp"
|
||||
printf '\nonlyBuiltDependencies:\n - "%s"\n' "$dep" >>"$tmp"
|
||||
fi
|
||||
mv "$tmp" "$workspace_file"
|
||||
fi
|
||||
|
||||
if [[ -f "$package_file" ]]; then
|
||||
"$(node_bin)" - "$package_file" "$dep" <<'EOF'
|
||||
const fs = require("node:fs");
|
||||
|
||||
const [packageFile, dep] = process.argv.slice(2);
|
||||
const data = JSON.parse(fs.readFileSync(packageFile, "utf8"));
|
||||
const list = data.pnpm?.onlyBuiltDependencies;
|
||||
if (Array.isArray(list)) {
|
||||
if (!list.includes(dep)) {
|
||||
list.unshift(dep);
|
||||
fs.writeFileSync(packageFile, `${JSON.stringify(data, null, 2)}\n`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!data.pnpm || typeof data.pnpm !== "object") {
|
||||
data.pnpm = {};
|
||||
}
|
||||
data.pnpm.onlyBuiltDependencies = [dep];
|
||||
fs.writeFileSync(packageFile, `${JSON.stringify(data, null, 2)}\n`);
|
||||
EOF
|
||||
fi
|
||||
|
||||
log "Updated pnpm allowlist for git-hosted build dependency: ${dep}"
|
||||
}
|
||||
|
||||
install_openclaw_from_git() {
|
||||
local repo_dir="$1"
|
||||
local repo_url="https://github.com/openclaw/openclaw.git"
|
||||
|
||||
if [[ -z "$repo_dir" ]]; then
|
||||
fail "Git install dir cannot be empty"
|
||||
fi
|
||||
if [[ "$repo_dir" != /* ]]; then
|
||||
repo_dir="$(pwd)/$repo_dir"
|
||||
fi
|
||||
mkdir -p "$(dirname "$repo_dir")"
|
||||
repo_dir="$(cd "$(dirname "$repo_dir")" && pwd)/$(basename "$repo_dir")"
|
||||
|
||||
emit_json "{\"event\":\"step\",\"name\":\"openclaw\",\"status\":\"start\",\"method\":\"git\",\"repo\":\"${repo_url//\"/\\\"}\"}"
|
||||
if [[ -d "$repo_dir/.git" ]]; then
|
||||
log "Installing Openclaw from git checkout: ${repo_dir}"
|
||||
else
|
||||
log "Installing Openclaw from GitHub (${repo_url})..."
|
||||
fi
|
||||
|
||||
ensure_git
|
||||
ensure_pnpm
|
||||
ensure_pnpm_binary_for_scripts
|
||||
|
||||
if [[ -d "$repo_dir/.git" ]]; then
|
||||
:
|
||||
elif [[ -d "$repo_dir" ]]; then
|
||||
if [[ -z "$(ls -A "$repo_dir" 2>/dev/null || true)" ]]; then
|
||||
git clone "$repo_url" "$repo_dir"
|
||||
else
|
||||
fail "Git install dir exists but is not a git repo: ${repo_dir}"
|
||||
fi
|
||||
else
|
||||
git clone "$repo_url" "$repo_dir"
|
||||
fi
|
||||
|
||||
if [[ "$GIT_UPDATE" == "1" ]]; then
|
||||
if [[ -z "$(git -C "$repo_dir" status --porcelain 2>/dev/null || true)" ]]; then
|
||||
git -C "$repo_dir" pull --rebase || true
|
||||
else
|
||||
log "Repo is dirty; skipping git pull"
|
||||
fi
|
||||
fi
|
||||
|
||||
cleanup_legacy_submodules "$repo_dir"
|
||||
ensure_pnpm_git_prepare_allowlist "$repo_dir"
|
||||
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" run_pnpm -C "$repo_dir" install
|
||||
|
||||
if ! run_pnpm -C "$repo_dir" ui:build; then
|
||||
log "UI build failed; continuing (CLI may still work)"
|
||||
fi
|
||||
run_pnpm -C "$repo_dir" build
|
||||
|
||||
mkdir -p "${PREFIX}/bin"
|
||||
cat > "${PREFIX}/bin/openclaw" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec "${PREFIX}/tools/node/bin/node" "${repo_dir}/dist/entry.js" "\$@"
|
||||
EOF
|
||||
chmod +x "${PREFIX}/bin/openclaw"
|
||||
emit_json "{\"event\":\"step\",\"name\":\"openclaw\",\"status\":\"ok\",\"method\":\"git\"}"
|
||||
}
|
||||
|
||||
resolve_openclaw_version() {
|
||||
local version=""
|
||||
if [[ -x "${PREFIX}/bin/openclaw" ]]; then
|
||||
version="$("${PREFIX}/bin/openclaw" --version 2>/dev/null | head -n 1 | tr -d '\r')"
|
||||
fi
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
is_gateway_daemon_loaded() {
|
||||
local claw="$1"
|
||||
if [[ -z "$claw" || ! -x "$claw" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local status_json=""
|
||||
status_json="$("$claw" daemon status --json 2>/dev/null || true)"
|
||||
if [[ -z "$status_json" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s' "$status_json" | node -e '
|
||||
const fs = require("fs");
|
||||
const raw = fs.readFileSync(0, "utf8").trim();
|
||||
if (!raw) process.exit(1);
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
process.exit(data?.service?.loaded ? 0 : 1);
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
refresh_gateway_service_if_loaded() {
|
||||
local claw="${PREFIX}/bin/openclaw"
|
||||
if [[ ! -x "$claw" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! is_gateway_daemon_loaded "$claw"; then
|
||||
emit_json '{"event":"step","name":"gateway-service","status":"skip","reason":"not-loaded"}'
|
||||
return 0
|
||||
fi
|
||||
|
||||
emit_json '{"event":"step","name":"gateway-service","status":"start"}'
|
||||
log "Refreshing loaded gateway service..."
|
||||
|
||||
if ! "$claw" gateway install --force >/dev/null 2>&1; then
|
||||
emit_json '{"event":"step","name":"gateway-service","status":"warn","reason":"install-failed"}'
|
||||
log "Warning: gateway service refresh failed; continuing."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! "$claw" gateway restart >/dev/null 2>&1; then
|
||||
emit_json '{"event":"step","name":"gateway-service","status":"warn","reason":"restart-failed"}'
|
||||
log "Warning: gateway service restart failed; continuing."
|
||||
return 0
|
||||
fi
|
||||
|
||||
"$claw" gateway status --probe --json >/dev/null 2>&1 || true
|
||||
emit_json '{"event":"step","name":"gateway-service","status":"ok"}'
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
if [[ "${OPENCLAW_NO_ONBOARD:-0}" == "1" ]]; then
|
||||
RUN_ONBOARD=0
|
||||
fi
|
||||
|
||||
cleanup_legacy_submodules
|
||||
|
||||
PATH="$(node_dir)/bin:${PREFIX}/bin:${PATH}"
|
||||
export PATH
|
||||
|
||||
install_node
|
||||
if [[ "$INSTALL_METHOD" == "git" ]]; then
|
||||
install_openclaw_from_git "$GIT_DIR"
|
||||
elif [[ "$INSTALL_METHOD" == "npm" ]]; then
|
||||
ensure_git
|
||||
if [[ "$SET_NPM_PREFIX" -eq 1 ]]; then
|
||||
fix_npm_prefix_if_needed
|
||||
fi
|
||||
install_openclaw
|
||||
else
|
||||
fail "Unknown install method: ${INSTALL_METHOD} (use npm or git)"
|
||||
fi
|
||||
|
||||
refresh_gateway_service_if_loaded
|
||||
|
||||
local installed_version
|
||||
installed_version="$(resolve_openclaw_version)"
|
||||
if [[ -n "$installed_version" ]]; then
|
||||
emit_json "{\"event\":\"done\",\"ok\":true,\"version\":\"${installed_version//\"/\\\"}\"}"
|
||||
log "OpenClaw installed (${installed_version})."
|
||||
else
|
||||
emit_json "{\"event\":\"done\",\"ok\":true}"
|
||||
log "OpenClaw installed."
|
||||
fi
|
||||
|
||||
if [[ "$RUN_ONBOARD" -eq 1 ]]; then
|
||||
"${PREFIX}/bin/openclaw" onboard
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${OPENCLAW_INSTALL_CLI_SH_NO_RUN:-0}" != "1" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
102
scripts/install.cmd
Normal file
102
scripts/install.cmd
Normal file
@@ -0,0 +1,102 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM OpenClaw Windows CMD installer
|
||||
REM Usage:
|
||||
REM curl -fsSL https://openclaw.ai/install.cmd -o install.cmd && install.cmd --no-onboard && del install.cmd
|
||||
|
||||
set "TAG=latest"
|
||||
set "INSTALL_METHOD=npm"
|
||||
set "NO_ONBOARD=0"
|
||||
set "NO_GIT_UPDATE=0"
|
||||
set "DRY_RUN=0"
|
||||
set "TAG_SET=0"
|
||||
set "INSTALL_PS1_URL="
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto :args_done
|
||||
|
||||
if /i "%~1"=="--help" goto :usage
|
||||
if /i "%~1"=="--git" set "INSTALL_METHOD=git"
|
||||
if /i "%~1"=="--npm" set "INSTALL_METHOD=npm"
|
||||
if /i "%~1"=="--no-onboard" set "NO_ONBOARD=1"
|
||||
if /i "%~1"=="--no-git-update" set "NO_GIT_UPDATE=1"
|
||||
if /i "%~1"=="--dry-run" set "DRY_RUN=1"
|
||||
|
||||
if /i "%~1"=="--tag" (
|
||||
if not "%~2"=="" (
|
||||
set "TAG=%~2"
|
||||
set "TAG_SET=1"
|
||||
shift
|
||||
)
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
|
||||
set "ARG=%~1"
|
||||
if not "%ARG%"=="" (
|
||||
if not "%ARG:~0,1%"=="-" (
|
||||
if "%TAG_SET%"=="0" (
|
||||
set "TAG=%ARG%"
|
||||
set "TAG_SET=1"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
shift
|
||||
goto :parse_args
|
||||
|
||||
:args_done
|
||||
|
||||
curl --version >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo curl is required but not available. Please install curl or use PowerShell installer. >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
powershell -NoProfile -Command "$PSVersionTable.PSVersion.Major" >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo PowerShell is required but not available. Use install.ps1 directly or install PowerShell. >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "TMP=%TEMP%\openclaw-install.ps1"
|
||||
REM TMP may include spaces; always quote "%TMP%" when used.
|
||||
if not "%OPENCLAW_INSTALL_PS1_URL%"=="" set "INSTALL_PS1_URL=%OPENCLAW_INSTALL_PS1_URL%"
|
||||
if "%INSTALL_PS1_URL%"=="" set "INSTALL_PS1_URL=https://openclaw.ai/install.ps1"
|
||||
|
||||
if exist "%INSTALL_PS1_URL%" (
|
||||
copy /Y "%INSTALL_PS1_URL%" "%TMP%" >nul
|
||||
) else (
|
||||
curl -fsSL "%INSTALL_PS1_URL%" -o "%TMP%"
|
||||
)
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Failed to download install.ps1 >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "PS_ARGS=-Tag ""%TAG%"" -InstallMethod ""%INSTALL_METHOD%"""
|
||||
if "%NO_ONBOARD%"=="1" set "PS_ARGS=%PS_ARGS% -NoOnboard"
|
||||
if "%NO_GIT_UPDATE%"=="1" set "PS_ARGS=%PS_ARGS% -NoGitUpdate"
|
||||
if "%DRY_RUN%"=="1" set "PS_ARGS=%PS_ARGS% -DryRun"
|
||||
|
||||
if "%DRY_RUN%"=="1" echo [OK] Dry run ^(delegating to install.ps1^)
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%TMP%" %PS_ARGS%
|
||||
set "RESULT=%ERRORLEVEL%"
|
||||
|
||||
del /f "%TMP%" >nul 2>&1
|
||||
|
||||
if %RESULT% neq 0 exit /b %RESULT%
|
||||
exit /b 0
|
||||
|
||||
:usage
|
||||
echo Usage: install.cmd [options] [tag]
|
||||
echo.
|
||||
echo Options:
|
||||
echo --git Install from git checkout
|
||||
echo --npm Install via npm ^(default^)
|
||||
echo --tag <ver> Tag/version to install ^(default: latest^)
|
||||
echo --no-onboard Skip onboarding
|
||||
echo --no-git-update Skip git pull for existing checkout
|
||||
echo --dry-run Print what would happen ^(no changes^)
|
||||
exit /b 0
|
||||
1268
scripts/install.ps1
1268
scripts/install.ps1
File diff suppressed because it is too large
Load Diff
@@ -1275,12 +1275,13 @@ install_homebrew() {
|
||||
}
|
||||
|
||||
# Check Node.js version
|
||||
parse_node_version_components() {
|
||||
if ! command -v node &> /dev/null; then
|
||||
parse_node_version_components_for_binary() {
|
||||
local node_bin="${1:-node}"
|
||||
if ! command -v "$node_bin" &> /dev/null && [[ ! -x "$node_bin" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local version major minor
|
||||
version="$(node -v 2>/dev/null || true)"
|
||||
version="$("$node_bin" -v 2>/dev/null || true)"
|
||||
major="${version#v}"
|
||||
major="${major%%.*}"
|
||||
minor="${version#v}"
|
||||
@@ -1297,6 +1298,13 @@ parse_node_version_components() {
|
||||
return 0
|
||||
}
|
||||
|
||||
parse_node_version_components() {
|
||||
if ! command -v node &> /dev/null; then
|
||||
return 1
|
||||
fi
|
||||
parse_node_version_components_for_binary node
|
||||
}
|
||||
|
||||
node_major_version() {
|
||||
local version_components major minor
|
||||
version_components="$(parse_node_version_components || true)"
|
||||
@@ -1324,6 +1332,83 @@ node_is_at_least_required() {
|
||||
return 1
|
||||
}
|
||||
|
||||
node_binary_is_at_least_required() {
|
||||
local node_bin="$1"
|
||||
local version_components major minor
|
||||
version_components="$(parse_node_version_components_for_binary "$node_bin" || true)"
|
||||
read -r major minor <<< "$version_components"
|
||||
if [[ ! "$major" =~ ^[0-9]+$ || ! "$minor" =~ ^[0-9]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$major" -gt "$NODE_MIN_MAJOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$major" -eq "$NODE_MIN_MAJOR" && "$minor" -ge "$NODE_MIN_MINOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
prepend_path_dir() {
|
||||
local dir="${1%/}"
|
||||
if [[ -z "$dir" || ! -d "$dir" ]]; then
|
||||
return 1
|
||||
fi
|
||||
local current=":${PATH:-}:"
|
||||
current="${current//:${dir}:/:}"
|
||||
current="${current#:}"
|
||||
current="${current%:}"
|
||||
if [[ -n "$current" ]]; then
|
||||
export PATH="${dir}:${current}"
|
||||
else
|
||||
export PATH="${dir}"
|
||||
fi
|
||||
refresh_shell_command_cache
|
||||
}
|
||||
|
||||
promote_supported_node_binary() {
|
||||
local candidates=()
|
||||
local candidate dir seen_dirs=":"
|
||||
|
||||
while IFS= read -r candidate; do
|
||||
candidates+=("$candidate")
|
||||
done < <(type -P -a node 2>/dev/null || true)
|
||||
|
||||
candidates+=(
|
||||
"/usr/bin/node"
|
||||
"/usr/local/bin/node"
|
||||
"/opt/homebrew/bin/node"
|
||||
"/opt/homebrew/opt/node@${NODE_DEFAULT_MAJOR}/bin/node"
|
||||
"/usr/local/opt/node@${NODE_DEFAULT_MAJOR}/bin/node"
|
||||
)
|
||||
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if [[ -z "$candidate" || ! -x "$candidate" ]]; then
|
||||
continue
|
||||
fi
|
||||
if dir="$(cd "$(dirname "$candidate")" && pwd 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
dir=""
|
||||
fi
|
||||
if [[ -z "$dir" || "$seen_dirs" == *":$dir:"* ]]; then
|
||||
continue
|
||||
fi
|
||||
seen_dirs="${seen_dirs}${dir}:"
|
||||
if node_binary_is_at_least_required "$candidate"; then
|
||||
prepend_path_dir "$dir" || continue
|
||||
ui_info "Using Node.js runtime at ${candidate}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
activate_supported_node_on_path() {
|
||||
promote_supported_node_binary "$@"
|
||||
}
|
||||
|
||||
print_active_node_paths() {
|
||||
if ! command -v node &> /dev/null; then
|
||||
return 1
|
||||
@@ -1378,7 +1463,12 @@ ensure_macos_default_node_active() {
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_macos_node22_active() {
|
||||
ensure_macos_default_node_active "$@"
|
||||
}
|
||||
|
||||
ensure_default_node_active_shell() {
|
||||
promote_supported_node_binary || true
|
||||
if node_is_at_least_required; then
|
||||
return 0
|
||||
fi
|
||||
@@ -1426,7 +1516,7 @@ load_nvm_for_node_detection() {
|
||||
fi
|
||||
|
||||
export NVM_DIR="$nvm_dir"
|
||||
# shellcheck disable=SC1090
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$NVM_DIR/nvm.sh" --no-use >/dev/null 2>&1 || . "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
|
||||
if command -v nvm >/dev/null 2>&1; then
|
||||
nvm use default --silent >/dev/null 2>&1 || nvm use node --silent >/dev/null 2>&1 || true
|
||||
@@ -1487,6 +1577,7 @@ install_node() {
|
||||
else
|
||||
run_quiet_step "Installing Node.js" sudo pacman -Sy --noconfirm nodejs npm
|
||||
fi
|
||||
promote_supported_node_binary || true
|
||||
ui_success "Node.js v${NODE_DEFAULT_MAJOR} installed"
|
||||
print_active_node_paths || true
|
||||
return 0
|
||||
@@ -1532,6 +1623,7 @@ install_node() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
promote_supported_node_binary || true
|
||||
ui_success "Node.js v${NODE_DEFAULT_MAJOR} installed"
|
||||
print_active_node_paths || true
|
||||
fi
|
||||
@@ -1642,11 +1734,18 @@ fix_npm_permissions() {
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
local path_line='export PATH="$HOME/.npm-global/bin:$PATH"'
|
||||
local wrote_rc=0
|
||||
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
||||
if [[ -f "$rc" ]] && ! grep -q ".npm-global" "$rc"; then
|
||||
echo "$path_line" >> "$rc"
|
||||
if [[ -f "$rc" ]]; then
|
||||
if ! grep -q ".npm-global" "$rc"; then
|
||||
echo "$path_line" >> "$rc"
|
||||
fi
|
||||
wrote_rc=1
|
||||
fi
|
||||
done
|
||||
if [[ "$wrote_rc" -eq 0 ]]; then
|
||||
printf '%s\n' "$path_line" >> "$HOME/.bashrc"
|
||||
fi
|
||||
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
ui_success "npm configured for user installs"
|
||||
@@ -2350,10 +2449,15 @@ load_install_version_helpers() {
|
||||
if [[ -z "$source_path" || ! -f "$source_path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
script_dir="$(cd "$(dirname "$source_path")" && pwd 2>/dev/null || true)"
|
||||
if script_dir="$(cd "$(dirname "$source_path")" && pwd 2>/dev/null)"; then
|
||||
:
|
||||
else
|
||||
script_dir=""
|
||||
fi
|
||||
helper_path="${script_dir}/docker/install-sh-common/version-parse.sh"
|
||||
if [[ -n "$script_dir" && -r "$helper_path" ]]; then
|
||||
# shellcheck source=docker/install-sh-common/version-parse.sh
|
||||
# shellcheck disable=SC1091
|
||||
source "$helper_path"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ describe("normalizeMessagesForLlmBoundary", () => {
|
||||
|
||||
const output = normalizeMessagesForLlmBoundary(
|
||||
input as Parameters<typeof normalizeMessagesForLlmBoundary>[0],
|
||||
) as Array<Record<string, unknown>>;
|
||||
) as unknown as Array<Record<string, unknown>>;
|
||||
|
||||
expect(output[0]?.content).toEqual([
|
||||
{
|
||||
|
||||
@@ -353,7 +353,7 @@ describe("channel-auth", () => {
|
||||
});
|
||||
|
||||
await expect(runChannelLogin({ channel: "whatsapp" }, runtime)).rejects.toThrow(
|
||||
'Channel "whatsapp" does not support login. Run openclaw channels status --channel whatsapp to inspect supported actions.',
|
||||
'Channel "whatsapp" does not support login. Run `openclaw channels status --channel whatsapp` to inspect supported actions.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -564,7 +564,7 @@ describe("channel-auth", () => {
|
||||
});
|
||||
|
||||
await expect(runChannelLogout({ channel: "whatsapp" }, runtime)).rejects.toThrow(
|
||||
'Channel "whatsapp" does not support logout. Run openclaw channels status --channel whatsapp to inspect supported actions.',
|
||||
'Channel "whatsapp" does not support logout. Run `openclaw channels status --channel whatsapp` to inspect supported actions.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,7 +189,7 @@ describe("plugins cli policy mutations", () => {
|
||||
);
|
||||
|
||||
expect(runtimeErrors).toContain(
|
||||
"Plugin not found: missing-plugin. Run openclaw plugins list to see installed plugins, or openclaw plugins search missing-plugin to look for installable plugins.",
|
||||
"Plugin not found: missing-plugin. Run `openclaw plugins list` to see installed plugins, or `openclaw plugins search missing-plugin` to look for installable plugins.",
|
||||
);
|
||||
expect(enablePluginInConfig).not.toHaveBeenCalled();
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
|
||||
@@ -300,6 +300,24 @@ describe("detectChangedScope", () => {
|
||||
runChangedSmoke: false,
|
||||
runControlUiI18n: false,
|
||||
});
|
||||
expect(detectChangedScope(["scripts/install.ps1"])).toEqual({
|
||||
runNode: true,
|
||||
runMacos: false,
|
||||
runAndroid: false,
|
||||
runWindows: true,
|
||||
runSkillsPython: false,
|
||||
runChangedSmoke: true,
|
||||
runControlUiI18n: false,
|
||||
});
|
||||
expect(detectChangedScope(["scripts/install.cmd"])).toEqual({
|
||||
runNode: true,
|
||||
runMacos: false,
|
||||
runAndroid: false,
|
||||
runWindows: true,
|
||||
runSkillsPython: false,
|
||||
runChangedSmoke: true,
|
||||
runControlUiI18n: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("runs changed-smoke for install and packaging surfaces", () => {
|
||||
@@ -312,6 +330,15 @@ describe("detectChangedScope", () => {
|
||||
runChangedSmoke: true,
|
||||
runControlUiI18n: false,
|
||||
});
|
||||
expect(detectChangedScope(["scripts/install-cli.sh"])).toEqual({
|
||||
runNode: true,
|
||||
runMacos: false,
|
||||
runAndroid: false,
|
||||
runWindows: false,
|
||||
runSkillsPython: false,
|
||||
runChangedSmoke: true,
|
||||
runControlUiI18n: false,
|
||||
});
|
||||
expect(detectChangedScope([bundledPluginFile("matrix", "package.json")])).toEqual({
|
||||
runNode: true,
|
||||
runMacos: false,
|
||||
@@ -456,6 +483,18 @@ describe("detectChangedScope", () => {
|
||||
runFastInstallSmoke: true,
|
||||
runFullInstallSmoke: true,
|
||||
});
|
||||
expect(detectInstallSmokeScope(["scripts/install-cli.sh"])).toEqual({
|
||||
runFastInstallSmoke: true,
|
||||
runFullInstallSmoke: true,
|
||||
});
|
||||
expect(detectInstallSmokeScope(["scripts/install.ps1"])).toEqual({
|
||||
runFastInstallSmoke: true,
|
||||
runFullInstallSmoke: true,
|
||||
});
|
||||
expect(detectInstallSmokeScope(["scripts/install.cmd"])).toEqual({
|
||||
runFastInstallSmoke: true,
|
||||
runFullInstallSmoke: true,
|
||||
});
|
||||
expect(detectInstallSmokeScope(["Dockerfile"])).toEqual({
|
||||
runFastInstallSmoke: true,
|
||||
runFullInstallSmoke: true,
|
||||
|
||||
@@ -47,7 +47,8 @@ function createFailingNodeFixture(source: string): string {
|
||||
"",
|
||||
"function Write-Banner { }",
|
||||
"function Ensure-ExecutionPolicy { return $true }",
|
||||
"function Ensure-Node { return $false }",
|
||||
"function Check-Node { return $false }",
|
||||
"function Install-Node { return $false }",
|
||||
"",
|
||||
"$mainResults = @(Main)",
|
||||
"$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true",
|
||||
@@ -81,45 +82,26 @@ describe("install.ps1 failure handling", () => {
|
||||
expect(completeInstallBody).toMatch(/\bthrow "OpenClaw installation failed with exit code/);
|
||||
});
|
||||
|
||||
it("runs npm capture commands from a writable installer temp directory", () => {
|
||||
const nativeCaptureBody = extractFunctionBody(source, "Invoke-NativeCommandCapture");
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClawNpm");
|
||||
const mainBody = extractFunctionBody(source, "Main");
|
||||
expect(source).toContain("function Get-NpmWorkingDirectory {");
|
||||
expect(nativeCaptureBody).toContain('[string]$WorkingDirectory = ""');
|
||||
expect(nativeCaptureBody).toContain("$startProcessArgs.WorkingDirectory = $WorkingDirectory");
|
||||
expect(npmInstallBody).toContain("-WorkingDirectory (Get-NpmWorkingDirectory)");
|
||||
expect(mainBody).toContain("-WorkingDirectory (Get-NpmWorkingDirectory)");
|
||||
it("runs npm install through the resolved command with quiet CI defaults", () => {
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
|
||||
expect(npmInstallBody).toContain("$npmOutput = & (Get-NpmCommandPath) install -g");
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_LOGLEVEL = "error"');
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_UPDATE_NOTIFIER = "false"');
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_FUND = "false"');
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_AUDIT = "false"');
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe"');
|
||||
expect(npmInstallBody).toContain('$env:NODE_LLAMA_CPP_SKIP_DOWNLOAD = "1"');
|
||||
expect(npmInstallBody).toContain("$env:NPM_CONFIG_LOGLEVEL = $prevLogLevel");
|
||||
expect(npmInstallBody).toContain(
|
||||
"$env:NODE_LLAMA_CPP_SKIP_DOWNLOAD = $prevNodeLlamaSkipDownload",
|
||||
);
|
||||
});
|
||||
|
||||
runIfPowerShell("creates a temp npm working directory", () => {
|
||||
const tempDir = harness.createTempDir("openclaw-install-ps1-");
|
||||
const scriptPath = join(tempDir, "install.ps1");
|
||||
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
|
||||
writeFileSync(
|
||||
scriptPath,
|
||||
[
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"$result = Get-NpmWorkingDirectory",
|
||||
'if (!(Test-Path -LiteralPath $result)) { throw "missing $result" }',
|
||||
'if ($result -notmatch "openclaw-installer") { throw "unexpected $result" }',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(scriptPath, 0o755);
|
||||
|
||||
const result = runPowerShell([
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
scriptPath,
|
||||
]);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
it("cleans legacy git submodules only from the selected git checkout", () => {
|
||||
const gitInstallBody = extractFunctionBody(source, "Install-OpenClawFromGit");
|
||||
const mainBody = extractFunctionBody(source, "Main");
|
||||
expect(gitInstallBody).toContain("Remove-LegacySubmodule -RepoDir $RepoDir");
|
||||
expect(mainBody).not.toContain("Remove-LegacySubmodule");
|
||||
});
|
||||
|
||||
runIfPowerShell("exits non-zero when run as a script file", () => {
|
||||
@@ -172,12 +154,13 @@ describe("install.ps1 failure handling", () => {
|
||||
"",
|
||||
"function Write-Banner { }",
|
||||
"function Ensure-ExecutionPolicy { return $true }",
|
||||
"function Ensure-Node { return $true }",
|
||||
"function Check-Node { return $true }",
|
||||
"function Check-ExistingOpenClaw { return $false }",
|
||||
"function Add-ToPath { param([string]$Path) }",
|
||||
"function Invoke-NativeCommandCapture {",
|
||||
" param([string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory = '')",
|
||||
" return @{ ExitCode = 0; Stdout = 'npm stdout'; Stderr = 'npm stderr' }",
|
||||
"}",
|
||||
"function Install-OpenClaw { Write-Output 'npm stdout'; return $true }",
|
||||
"function Ensure-OpenClawOnPath { return $true }",
|
||||
"function Refresh-GatewayServiceIfLoaded { }",
|
||||
"function Invoke-OpenClawCommand { return 'OpenClaw test-version' }",
|
||||
"$NoOnboard = $true",
|
||||
"$result = Main",
|
||||
"if ($result -is [array]) { throw 'Main returned an array' }",
|
||||
@@ -211,18 +194,16 @@ describe("install.ps1 failure handling", () => {
|
||||
"",
|
||||
"function Write-Banner { }",
|
||||
"function Ensure-ExecutionPolicy { return $true }",
|
||||
"function Ensure-Node { return $true }",
|
||||
"function Ensure-Git { return $true }",
|
||||
"function Check-Node { return $true }",
|
||||
"function Check-ExistingOpenClaw { return $false }",
|
||||
"function Add-ToPath { param([string]$Path) }",
|
||||
"function Install-OpenClawNpm {",
|
||||
" param([string]$Target = 'latest')",
|
||||
"function Install-OpenClaw {",
|
||||
" Write-Output 'native chatter'",
|
||||
" return $true",
|
||||
"}",
|
||||
"function Invoke-NativeCommandCapture {",
|
||||
" param([string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory = '')",
|
||||
" return @{ ExitCode = 0; Stdout = 'npm prefix'; Stderr = '' }",
|
||||
"}",
|
||||
"function Ensure-OpenClawOnPath { return $true }",
|
||||
"function Refresh-GatewayServiceIfLoaded { }",
|
||||
"function Invoke-OpenClawCommand { return 'OpenClaw test-version' }",
|
||||
"$NoOnboard = $true",
|
||||
"$mainResults = @(Main)",
|
||||
"$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true",
|
||||
|
||||
@@ -109,6 +109,52 @@ describe("install.sh", () => {
|
||||
expect(output).toContain("version=v22.22.1");
|
||||
});
|
||||
|
||||
it("promotes a supported Linux Node binary over stale PATH entries", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-node-promote-"));
|
||||
const staleBin = join(tmp, "usr-local-bin");
|
||||
const supportedBin = join(tmp, "usr-bin");
|
||||
mkdirSync(staleBin, { recursive: true });
|
||||
mkdirSync(supportedBin, { recursive: true });
|
||||
|
||||
const staleNode = join(staleBin, "node");
|
||||
const supportedNode = join(supportedBin, "node");
|
||||
writeFileSync(staleNode, "#!/bin/sh\necho v20.20.0\n");
|
||||
writeFileSync(supportedNode, "#!/bin/sh\necho v22.22.0\n");
|
||||
chmodSync(staleNode, 0o755);
|
||||
chmodSync(supportedNode, 0o755);
|
||||
|
||||
let result: ReturnType<typeof runInstallShell> | undefined;
|
||||
try {
|
||||
result = runInstallShell(
|
||||
[
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"set +e",
|
||||
"OS=linux",
|
||||
"promote_supported_node_binary",
|
||||
"promote_status=$?",
|
||||
"ensure_default_node_active_shell",
|
||||
"active_status=$?",
|
||||
'printf "promote=%s\\nactive=%s\\npath=%s\\nversion=%s\\n" "$promote_status" "$active_status" "$(command -v node)" "$(node -v)"',
|
||||
"exit $active_status",
|
||||
].join("\n"),
|
||||
{
|
||||
PATH: `${staleBin}:${supportedBin}:/usr/bin:/bin`,
|
||||
TERM: "dumb",
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
expect(result?.status).toBe(0);
|
||||
const output = result?.stdout ?? "";
|
||||
expect(output).toContain("promote=0");
|
||||
expect(output).toContain("active=0");
|
||||
expect(output).toContain(`path=${supportedNode}`);
|
||||
expect(output).toContain("version=v22.22.0");
|
||||
});
|
||||
|
||||
it("warns before redirecting an unwritable npm prefix", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-prefix-"));
|
||||
const home = join(tmp, "home");
|
||||
|
||||
55
test/scripts/website-installer-sync-workflow.test.ts
Normal file
55
test/scripts/website-installer-sync-workflow.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const { detectInstallSmokeScope } = (await import("../../scripts/ci-changed-scope.mjs")) as {
|
||||
detectInstallSmokeScope: (paths: string[]) => {
|
||||
runFastInstallSmoke: boolean;
|
||||
runFullInstallSmoke: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const WORKFLOW_PATH = ".github/workflows/website-installer-sync.yml";
|
||||
|
||||
describe("website installer sync workflow", () => {
|
||||
const workflow = readFileSync(WORKFLOW_PATH, "utf8");
|
||||
|
||||
it("treats all website installer scripts as OpenClaw-owned inputs", () => {
|
||||
for (const path of [
|
||||
"scripts/install.sh",
|
||||
"scripts/install-cli.sh",
|
||||
"scripts/install.ps1",
|
||||
"scripts/install.cmd",
|
||||
]) {
|
||||
expect(workflow).toContain(path);
|
||||
expect(detectInstallSmokeScope([path]).runFullInstallSmoke).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("verifies installers on Linux Docker plus native macOS and Windows runners", () => {
|
||||
expect(workflow).toContain("linux-docker:");
|
||||
expect(workflow).toContain("docker run --rm");
|
||||
expect(workflow).toContain("bash /tmp/install.sh --no-prompt --no-onboard");
|
||||
expect(workflow).toContain("bash /tmp/install-cli.sh --prefix /tmp/openclaw");
|
||||
expect(workflow).toContain("macos-installer:");
|
||||
expect(workflow).toContain("runs-on: macos-latest");
|
||||
expect(workflow).toContain("windows-installer:");
|
||||
expect(workflow).toContain("runs-on: windows-latest");
|
||||
expect(workflow).toContain(".\\scripts\\install.ps1 -DryRun");
|
||||
expect(workflow).toContain("OPENCLAW_INSTALL_PS1_URL=%GITHUB_WORKSPACE%\\scripts\\install.ps1");
|
||||
expect(workflow).toContain(".\\scripts\\install.cmd --dry-run");
|
||||
});
|
||||
|
||||
it("syncs verified scripts to openclaw.ai only after all installer checks pass", () => {
|
||||
expect(workflow).toContain("needs: [static, linux-docker, macos-installer, windows-installer]");
|
||||
expect(workflow).toContain("repository: openclaw/openclaw.ai");
|
||||
expect(workflow).toContain("token: ${{ secrets.OPENCLAW_GH_TOKEN }}");
|
||||
expect(workflow).toContain("cp openclaw/scripts/install.sh openclaw.ai/public/install.sh");
|
||||
expect(workflow).toContain(
|
||||
"cp openclaw/scripts/install-cli.sh openclaw.ai/public/install-cli.sh",
|
||||
);
|
||||
expect(workflow).toContain("cp openclaw/scripts/install.ps1 openclaw.ai/public/install.ps1");
|
||||
expect(workflow).toContain("cp openclaw/scripts/install.cmd openclaw.ai/public/install.cmd");
|
||||
expect(workflow).toContain("bun run build");
|
||||
expect(workflow).toContain("git push origin HEAD:main");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user