diff --git a/openClaw/openClaw浏览器工具配置.md b/openClaw/openClaw浏览器工具配置.md new file mode 100644 index 0000000..51366ea --- /dev/null +++ b/openClaw/openClaw浏览器工具配置.md @@ -0,0 +1,100 @@ +# 浏览器工具配置 + +## 一、结论先行 + +可以使用如下配置(仅供参考): + +```json +{ + "browser": { + + // 是否启用 browser 工具模块 + // 关闭后 OpenClaw 将不会提供浏览器自动化能力 + "enabled": true, + + // 调用远程 CDP(Chrome DevTools Protocol)接口时 + // 单次请求的超时时间(毫秒) + "remoteCdpTimeoutMs": 1500, + + // 与远程 CDP 建立 WebSocket 连接时的握手超时时间(毫秒) + "remoteCdpHandshakeTimeoutMs": 3000, + + // 浏览器实例在 UI 或日志中的默认标识颜色 + // 用于区分不同浏览器实例 + "color": "#FF4500", + + // OpenClaw 启动浏览器时使用的可执行文件路径 + // 只有在 OpenClaw 托管浏览器(spawn browser)模式下才会使用 + "executablePath": "/usr/bin/chromium-browser", + + // 是否以无头模式启动浏览器 + // true = 不显示浏览器窗口 + // false = 显示浏览器 UI + "headless": false, + + // 是否添加 --no-sandbox 启动参数 + // 在 Docker / WSL / CI 环境中通常需要开启 + "noSandbox": true, + + // 是否只允许“附加到已有浏览器” + // true = OpenClaw 不会启动新浏览器,只能连接已有 CDP + // false = OpenClaw 可以自行启动浏览器 + "attachOnly": false, + + // 默认使用的浏览器 profile 名称 + // 如果没有指定 profile,则使用这个 + "defaultProfile": "chrome", + + // 浏览器 profile 列表 + // 每个 profile 代表一个可控制的浏览器实例 + "profiles": { + + // OpenClaw 默认自动化浏览器实例 + "openclaw": { + + // 该浏览器实例的 CDP 调试端口 + // OpenClaw 会通过这个端口连接浏览器 + "cdpPort": 18800, + + // 该浏览器实例在 UI 或日志中的颜色标识 + "color": "#FF4500" + }, + + // 第二个浏览器实例(例如工作环境浏览器) + "work": { + + // 该实例的 CDP 调试端口 + "cdpPort": 18801, + + // UI / 日志中的颜色标识 + "color": "#0066CC" + } + } + } +} +``` + +## 二、实用场景 + +### 1.1 让 openclaw 控制个人浏览器实现自动化(使用个人用户态、登录态) + +实现方案: + +1. 插件安装 + 1. 使用 openclaw 官方 chrome 插件 + 1. 下载 openclaw 官方 chrome 插件 `openclaw browser extension install` + 2. 查看插件安装路径 `openclaw browser extension path` + 3. 打开 chrome 浏览器,访问 `chrome://extensions/`,开启开发者模式 + 4. 点击 `加载未打包的扩展程序`,选择 openclaw 插件安装路径 + 5. 中继器链接,使用中继器端口 `18792` 和 网关令牌 `token`,保存即可 + 2. 自行在 chrome 商店下载安装 OpenClaw Borwser Relay 插件 + 1. 登录谷歌插件商店 `https://chromewebstore.google.com/category/extensions` + 2. 搜索 `OpenClaw Browser Relay`,选一个高分的就行,都不是官方的 + 3. 点击 `添加到 Chrome`,安装插件 +2. 配置 openclaw 浏览器工具 + 1. 打开 openclaw 浏览器工具配置文件 `openclaw browser.json` + 2. 配置 `cdpPort` 为 `18792` + 3. 配置 `gatewayToken` 为 `token` + 4. 保存配置文件 + +### 1.2让 openclaw 控制浏览器自行实现自动化(使用 openclaw 同一环境如 linux 浏览器) diff --git a/openClaw/scripts/mac-install.sh b/openClaw/scripts/mac-install.sh new file mode 100755 index 0000000..bc54eb8 --- /dev/null +++ b/openClaw/scripts/mac-install.sh @@ -0,0 +1,2636 @@ +#!/bin/bash +set -euo pipefail + +# OpenClaw Installer for macOS and Linux +# Usage: curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash + +BOLD='\033[1m' +ACCENT='\033[38;2;255;77;77m' # coral-bright #ff4d4d +# shellcheck disable=SC2034 +ACCENT_BRIGHT='\033[38;2;255;110;110m' # lighter coral +INFO='\033[38;2;136;146;176m' # text-secondary #8892b0 +SUCCESS='\033[38;2;0;229;204m' # cyan-bright #00e5cc +WARN='\033[38;2;255;176;32m' # amber (no site equiv, keep warm) +ERROR='\033[38;2;230;57;70m' # coral-mid #e63946 +MUTED='\033[38;2;90;100;128m' # text-muted #5a6480 +NC='\033[0m' # No Color + +DEFAULT_TAGLINE="All your chats, one OpenClaw." + +ORIGINAL_PATH="${PATH:-}" + +TMPFILES=() +cleanup_tmpfiles() { + local f + for f in "${TMPFILES[@]:-}"; do + rm -rf "$f" 2>/dev/null || true + done +} +trap cleanup_tmpfiles EXIT + +mktempfile() { + local f + f="$(mktemp)" + TMPFILES+=("$f") + echo "$f" +} + +resolve_brew_bin() { + local brew_bin="" + brew_bin="$(command -v brew 2>/dev/null || true)" + if [[ -n "$brew_bin" ]]; then + echo "$brew_bin" + return 0 + fi + if [[ -x "/opt/homebrew/bin/brew" ]]; then + echo "/opt/homebrew/bin/brew" + return 0 + fi + if [[ -x "/usr/local/bin/brew" ]]; then + echo "/usr/local/bin/brew" + return 0 + fi + return 1 +} + +activate_brew_for_session() { + local brew_bin="" + brew_bin="$(resolve_brew_bin || true)" + if [[ -z "$brew_bin" ]]; then + return 1 + fi + if [[ -z "$(command -v brew 2>/dev/null || true)" && "${BREW_SHELLENV_ANNOUNCED:-0}" != "1" ]]; then + ui_info "Found Homebrew at ${brew_bin}; exporting shellenv" + BREW_SHELLENV_ANNOUNCED=1 + fi + eval "$("$brew_bin" shellenv)" + return 0 +} + +DOWNLOADER="" +detect_downloader() { + if command -v curl &> /dev/null; then + DOWNLOADER="curl" + return 0 + fi + if command -v wget &> /dev/null; then + DOWNLOADER="wget" + return 0 + fi + ui_error "Missing downloader (curl or wget required)" + exit 1 +} + +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" +} + +run_remote_bash() { + local url="$1" + local tmp + tmp="$(mktempfile)" + download_file "$url" "$tmp" + /bin/bash "$tmp" +} + +GUM_VERSION="${OPENCLAW_GUM_VERSION:-0.17.0}" +GUM="" +GUM_STATUS="skipped" +GUM_REASON="" +LAST_NPM_INSTALL_CMD="" + +is_non_interactive_shell() { + if [[ "${NO_PROMPT:-0}" == "1" ]]; then + return 0 + fi + if [[ ! -t 0 || ! -t 1 ]]; then + return 0 + fi + return 1 +} + +gum_is_tty() { + if [[ -n "${NO_COLOR:-}" ]]; then + return 1 + fi + if [[ "${TERM:-dumb}" == "dumb" ]]; then + return 1 + fi + if [[ -t 2 || -t 1 ]]; then + return 0 + fi + if [[ -r /dev/tty && -w /dev/tty ]]; then + return 0 + fi + return 1 +} + +gum_detect_os() { + case "$(uname -s 2>/dev/null || true)" in + Darwin) echo "Darwin" ;; + Linux) echo "Linux" ;; + *) echo "unsupported" ;; + esac +} + +gum_detect_arch() { + case "$(uname -m 2>/dev/null || true)" in + x86_64|amd64) echo "x86_64" ;; + arm64|aarch64) echo "arm64" ;; + i386|i686) echo "i386" ;; + armv7l|armv7) echo "armv7" ;; + armv6l|armv6) echo "armv6" ;; + *) echo "unknown" ;; + esac +} + +verify_sha256sum_file() { + local checksums="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum --ignore-missing -c "$checksums" >/dev/null 2>&1 + return $? + fi + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 --ignore-missing -c "$checksums" >/dev/null 2>&1 + return $? + fi + return 1 +} + +bootstrap_gum_temp() { + GUM="" + GUM_STATUS="skipped" + GUM_REASON="" + + if is_non_interactive_shell; then + GUM_REASON="non-interactive shell (auto-disabled)" + return 1 + fi + + if ! gum_is_tty; then + GUM_REASON="terminal does not support gum UI" + return 1 + fi + + if command -v gum >/dev/null 2>&1; then + GUM="gum" + GUM_STATUS="found" + GUM_REASON="already installed" + return 0 + fi + + if ! command -v tar >/dev/null 2>&1; then + GUM_REASON="tar not found" + return 1 + fi + + local os arch asset base gum_tmpdir gum_path + os="$(gum_detect_os)" + arch="$(gum_detect_arch)" + if [[ "$os" == "unsupported" || "$arch" == "unknown" ]]; then + GUM_REASON="unsupported os/arch ($os/$arch)" + return 1 + fi + + asset="gum_${GUM_VERSION}_${os}_${arch}.tar.gz" + base="https://github.com/charmbracelet/gum/releases/download/v${GUM_VERSION}" + + gum_tmpdir="$(mktemp -d)" + TMPFILES+=("$gum_tmpdir") + + if ! download_file "${base}/${asset}" "$gum_tmpdir/$asset"; then + GUM_REASON="download failed" + return 1 + fi + + if ! download_file "${base}/checksums.txt" "$gum_tmpdir/checksums.txt"; then + GUM_REASON="checksum unavailable or failed" + return 1 + fi + + if ! (cd "$gum_tmpdir" && verify_sha256sum_file "checksums.txt"); then + GUM_REASON="checksum unavailable or failed" + return 1 + fi + + if ! tar -xzf "$gum_tmpdir/$asset" -C "$gum_tmpdir" >/dev/null 2>&1; then + GUM_REASON="extract failed" + return 1 + fi + + gum_path="$(find "$gum_tmpdir" -type f -name gum 2>/dev/null | head -n1 || true)" + if [[ -z "$gum_path" ]]; then + GUM_REASON="gum binary missing after extract" + return 1 + fi + + chmod +x "$gum_path" >/dev/null 2>&1 || true + if [[ ! -x "$gum_path" ]]; then + GUM_REASON="gum binary is not executable" + return 1 + fi + + GUM="$gum_path" + GUM_STATUS="installed" + GUM_REASON="temp, verified" + return 0 +} + +print_gum_status() { + case "$GUM_STATUS" in + found) + ui_success "gum available (${GUM_REASON})" + ;; + installed) + ui_success "gum bootstrapped (${GUM_REASON}, v${GUM_VERSION})" + ;; + *) + if [[ -n "$GUM_REASON" && "$GUM_REASON" != "non-interactive shell (auto-disabled)" ]]; then + ui_info "gum skipped (${GUM_REASON})" + fi + ;; + esac +} + +print_installer_banner() { + if [[ -n "$GUM" ]]; then + local title tagline hint card + title="$("$GUM" style --foreground "#ff4d4d" --bold "🦞 OpenClaw Installer")" + tagline="$("$GUM" style --foreground "#8892b0" "$TAGLINE")" + hint="$("$GUM" style --foreground "#5a6480" "modern installer mode")" + card="$(printf '%s\n%s\n%s' "$title" "$tagline" "$hint")" + "$GUM" style --border rounded --border-foreground "#ff4d4d" --padding "1 2" "$card" + echo "" + return + fi + + echo -e "${ACCENT}${BOLD}" + echo " 🦞 OpenClaw Installer" + echo -e "${NC}${INFO} ${TAGLINE}${NC}" + echo "" +} + +detect_os_or_die() { + OS="unknown" + if [[ "$OSTYPE" == "darwin"* ]]; then + OS="macos" + elif [[ "$OSTYPE" == "linux-gnu"* ]] || [[ -n "${WSL_DISTRO_NAME:-}" ]]; then + OS="linux" + fi + + if [[ "$OS" == "unknown" ]]; then + ui_error "Unsupported operating system" + echo "This installer supports macOS and Linux (including WSL)." + echo "For Windows, use: powershell -c \"irm https://openclaw.ai/install.ps1 | iex\"" + exit 1 + fi + + ui_success "Detected: $OS" +} + +ui_info() { + local msg="$*" + if [[ -n "$GUM" ]]; then + "$GUM" log --level info "$msg" + else + echo -e "${MUTED}·${NC} ${msg}" + fi +} + +ui_warn() { + local msg="$*" + if [[ -n "$GUM" ]]; then + "$GUM" log --level warn "$msg" + else + echo -e "${WARN}!${NC} ${msg}" + fi +} + +ui_success() { + local msg="$*" + if [[ -n "$GUM" ]]; then + local mark + mark="$("$GUM" style --foreground "#00e5cc" --bold "✓")" + echo "${mark} ${msg}" + else + echo -e "${SUCCESS}✓${NC} ${msg}" + fi +} + +ui_error() { + local msg="$*" + if [[ -n "$GUM" ]]; then + "$GUM" log --level error "$msg" + else + echo -e "${ERROR}✗${NC} ${msg}" + fi +} + +INSTALL_STAGE_TOTAL=3 +INSTALL_STAGE_CURRENT=0 + +ui_section() { + local title="$1" + if [[ -n "$GUM" ]]; then + "$GUM" style --bold --foreground "#ff4d4d" --padding "1 0" "$title" + else + echo "" + echo -e "${ACCENT}${BOLD}${title}${NC}" + fi +} + +ui_stage() { + local title="$1" + INSTALL_STAGE_CURRENT=$((INSTALL_STAGE_CURRENT + 1)) + ui_section "[${INSTALL_STAGE_CURRENT}/${INSTALL_STAGE_TOTAL}] ${title}" +} + +ui_kv() { + local key="$1" + local value="$2" + if [[ -n "$GUM" ]]; then + local key_part value_part + key_part="$("$GUM" style --foreground "#5a6480" --width 20 "$key")" + value_part="$("$GUM" style --bold "$value")" + "$GUM" join --horizontal "$key_part" "$value_part" + else + echo -e "${MUTED}${key}:${NC} ${value}" + fi +} + +ui_panel() { + local content="$1" + if [[ -n "$GUM" ]]; then + "$GUM" style --border rounded --border-foreground "#5a6480" --padding "0 1" "$content" + else + echo "$content" + fi +} + +show_install_plan() { + local detected_checkout="$1" + + ui_section "Install plan" + ui_kv "OS" "$OS" + ui_kv "Install method" "$INSTALL_METHOD" + ui_kv "Requested version" "$OPENCLAW_VERSION" + if [[ "$USE_BETA" == "1" ]]; then + ui_kv "Beta channel" "enabled" + fi + if [[ "$INSTALL_METHOD" == "git" ]]; then + ui_kv "Git directory" "$GIT_DIR" + ui_kv "Git update" "$GIT_UPDATE" + fi + if [[ -n "$detected_checkout" ]]; then + ui_kv "Detected checkout" "$detected_checkout" + fi + if [[ "$DRY_RUN" == "1" ]]; then + ui_kv "Dry run" "yes" + fi + if [[ "$NO_ONBOARD" == "1" ]]; then + ui_kv "Onboarding" "skipped" + fi +} + +show_footer_links() { + local faq_url="https://docs.openclaw.ai/start/faq" + if [[ -n "$GUM" ]]; then + local content + content="$(printf '%s\n%s' "Need help?" "FAQ: ${faq_url}")" + ui_panel "$content" + else + echo "" + echo -e "FAQ: ${INFO}${faq_url}${NC}" + fi +} + +ui_celebrate() { + local msg="$1" + if [[ -n "$GUM" ]]; then + "$GUM" style --bold --foreground "#00e5cc" "$msg" + else + echo -e "${SUCCESS}${BOLD}${msg}${NC}" + fi +} + +is_shell_function() { + local name="${1:-}" + [[ -n "$name" ]] && declare -F "$name" >/dev/null 2>&1 +} + +is_gum_raw_mode_failure() { + local err_log="$1" + [[ -s "$err_log" ]] || return 1 + grep -Eiq 'setrawmode' "$err_log" +} + +run_with_spinner() { + local title="$1" + shift + + if [[ -n "$GUM" ]] && gum_is_tty && ! is_shell_function "${1:-}"; then + local gum_err + gum_err="$(mktempfile)" + if "$GUM" spin --spinner dot --title "$title" -- "$@" 2>"$gum_err"; then + return 0 + fi + local gum_status=$? + if is_gum_raw_mode_failure "$gum_err"; then + GUM="" + GUM_STATUS="skipped" + GUM_REASON="gum raw mode unavailable" + ui_warn "Spinner unavailable in this terminal; continuing without spinner" + "$@" + return $? + fi + if [[ -s "$gum_err" ]]; then + cat "$gum_err" >&2 + fi + return "$gum_status" + fi + + "$@" +} + +run_quiet_step() { + local title="$1" + shift + + if [[ "$VERBOSE" == "1" ]]; then + run_with_spinner "$title" "$@" + return $? + fi + + local log + log="$(mktempfile)" + + if [[ -n "$GUM" ]] && gum_is_tty && ! is_shell_function "${1:-}"; then + local cmd_quoted="" + local log_quoted="" + printf -v cmd_quoted '%q ' "$@" + printf -v log_quoted '%q' "$log" + if run_with_spinner "$title" bash -c "${cmd_quoted}>${log_quoted} 2>&1"; then + return 0 + fi + else + if "$@" >"$log" 2>&1; then + return 0 + fi + fi + + ui_error "${title} failed — re-run with --verbose for details" + if [[ -s "$log" ]]; then + tail -n 80 "$log" >&2 || true + fi + return 1 +} + +cleanup_legacy_submodules() { + local repo_dir="$1" + local legacy_dir="$repo_dir/Peekaboo" + if [[ -d "$legacy_dir" ]]; then + ui_info "Removing legacy submodule checkout: ${legacy_dir}" + rm -rf "$legacy_dir" + fi +} + +cleanup_npm_openclaw_paths() { + local npm_root="" + npm_root="$(npm root -g 2>/dev/null || true)" + if [[ -z "$npm_root" || "$npm_root" != *node_modules* ]]; then + return 1 + fi + rm -rf "$npm_root"/.openclaw-* "$npm_root"/openclaw 2>/dev/null || true +} + +extract_openclaw_conflict_path() { + local log="$1" + local path="" + path="$(sed -n 's/.*File exists: //p' "$log" | head -n1)" + if [[ -z "$path" ]]; then + path="$(sed -n 's/.*EEXIST: file already exists, //p' "$log" | head -n1)" + fi + if [[ -n "$path" ]]; then + echo "$path" + return 0 + fi + return 1 +} + +cleanup_openclaw_bin_conflict() { + local bin_path="$1" + if [[ -z "$bin_path" || ( ! -e "$bin_path" && ! -L "$bin_path" ) ]]; then + return 1 + fi + local npm_bin="" + npm_bin="$(npm_global_bin_dir 2>/dev/null || true)" + if [[ -n "$npm_bin" && "$bin_path" != "$npm_bin/openclaw" ]]; then + case "$bin_path" in + "/opt/homebrew/bin/openclaw"|"/usr/local/bin/openclaw") + ;; + *) + return 1 + ;; + esac + fi + if [[ -L "$bin_path" ]]; then + local target="" + target="$(readlink "$bin_path" 2>/dev/null || true)" + if [[ "$target" == *"/node_modules/openclaw/"* ]]; then + rm -f "$bin_path" + ui_info "Removed stale openclaw symlink at ${bin_path}" + return 0 + fi + return 1 + fi + local backup="" + backup="${bin_path}.bak-$(date +%Y%m%d-%H%M%S)" + if mv "$bin_path" "$backup"; then + ui_info "Moved existing openclaw binary to ${backup}" + return 0 + fi + return 1 +} + +npm_log_indicates_missing_build_tools() { + local log="$1" + if [[ -z "$log" || ! -f "$log" ]]; then + return 1 + fi + + grep -Eiq "(not found: make|make: command not found|cmake: command not found|CMAKE_MAKE_PROGRAM is not set|Could not find CMAKE|gyp ERR! find Python|no developer tools were found|is not able to compile a simple test program|Failed to build llama\\.cpp|It seems that \"make\" is not installed in your system|It seems that the used \"cmake\" doesn't work properly)" "$log" +} + +install_build_tools_linux() { + require_sudo + + if command -v apt-get &> /dev/null; then + if is_root; then + run_quiet_step "Updating package index" apt-get update -qq + run_quiet_step "Installing build tools" apt-get install -y -qq build-essential python3 make g++ cmake + else + run_quiet_step "Updating package index" sudo apt-get update -qq + run_quiet_step "Installing build tools" sudo apt-get install -y -qq build-essential python3 make g++ cmake + fi + return 0 + fi + + if command -v dnf &> /dev/null; then + if is_root; then + run_quiet_step "Installing build tools" dnf install -y -q gcc gcc-c++ make cmake python3 + else + run_quiet_step "Installing build tools" sudo dnf install -y -q gcc gcc-c++ make cmake python3 + fi + return 0 + fi + + if command -v yum &> /dev/null; then + if is_root; then + run_quiet_step "Installing build tools" yum install -y -q gcc gcc-c++ make cmake python3 + else + run_quiet_step "Installing build tools" sudo yum install -y -q gcc gcc-c++ make cmake python3 + fi + return 0 + fi + + if command -v apk &> /dev/null; then + if is_root; then + run_quiet_step "Installing build tools" apk add --no-cache build-base python3 cmake + else + run_quiet_step "Installing build tools" sudo apk add --no-cache build-base python3 cmake + fi + return 0 + fi + + ui_warn "Could not detect package manager for auto-installing build tools" + return 1 +} + +install_build_tools_macos() { + local ok=true + local brew_bin="" + + if ! xcode-select -p >/dev/null 2>&1; then + ui_info "Installing Xcode Command Line Tools (required for make/clang)" + xcode-select --install >/dev/null 2>&1 || true + if ! xcode-select -p >/dev/null 2>&1; then + ui_warn "Xcode Command Line Tools are not ready yet" + ui_info "Complete the installer dialog, then re-run this installer" + ok=false + fi + fi + + if ! command -v cmake >/dev/null 2>&1; then + brew_bin="$(resolve_brew_bin || true)" + if [[ -n "$brew_bin" ]]; then + activate_brew_for_session || true + run_quiet_step "Installing cmake" "$brew_bin" install cmake + else + ui_warn "Homebrew not available; cannot auto-install cmake" + ok=false + fi + fi + + if ! command -v make >/dev/null 2>&1; then + ui_warn "make is still unavailable" + ok=false + fi + if ! command -v cmake >/dev/null 2>&1; then + ui_warn "cmake is still unavailable" + ok=false + fi + + [[ "$ok" == "true" ]] +} + +auto_install_build_tools_for_npm_failure() { + local log="$1" + if ! npm_log_indicates_missing_build_tools "$log"; then + return 1 + fi + + ui_warn "Detected missing native build tools; attempting automatic setup" + if [[ "$OS" == "linux" ]]; then + install_build_tools_linux || return 1 + elif [[ "$OS" == "macos" ]]; then + install_build_tools_macos || return 1 + else + return 1 + fi + ui_success "Build tools setup complete" + return 0 +} + +run_npm_global_install() { + local spec="$1" + local log="$2" + + local -a cmd + cmd=(env "SHARP_IGNORE_GLOBAL_LIBVIPS=$SHARP_IGNORE_GLOBAL_LIBVIPS" npm --loglevel "$NPM_LOGLEVEL") + if [[ -n "$NPM_SILENT_FLAG" ]]; then + cmd+=("$NPM_SILENT_FLAG") + fi + cmd+=(--no-fund --no-audit install -g "$spec") + local cmd_display="" + printf -v cmd_display '%q ' "${cmd[@]}" + LAST_NPM_INSTALL_CMD="${cmd_display% }" + + if [[ "$VERBOSE" == "1" ]]; then + "${cmd[@]}" 2>&1 | tee "$log" + return $? + fi + + if [[ -n "$GUM" ]] && gum_is_tty; then + local cmd_quoted="" + local log_quoted="" + printf -v cmd_quoted '%q ' "${cmd[@]}" + printf -v log_quoted '%q' "$log" + run_with_spinner "Installing OpenClaw package" bash -c "${cmd_quoted}>${log_quoted} 2>&1" + return $? + fi + + "${cmd[@]}" >"$log" 2>&1 +} + +extract_npm_debug_log_path() { + local log="$1" + local path="" + path="$(sed -n -E 's/.*A complete log of this run can be found in:[[:space:]]*//p' "$log" | tail -n1)" + if [[ -n "$path" ]]; then + echo "$path" + return 0 + fi + + path="$(grep -Eo '/[^[:space:]]+_logs/[^[:space:]]+debug[^[:space:]]*\.log' "$log" | tail -n1 || true)" + if [[ -n "$path" ]]; then + echo "$path" + return 0 + fi + + return 1 +} + +extract_first_npm_error_line() { + local log="$1" + grep -E 'npm (ERR!|error)|ERR!' "$log" | head -n1 || true +} + +extract_npm_error_code() { + local log="$1" + sed -n -E 's/^npm (ERR!|error) code[[:space:]]+([^[:space:]]+).*$/\2/p' "$log" | head -n1 +} + +extract_npm_error_syscall() { + local log="$1" + sed -n -E 's/^npm (ERR!|error) syscall[[:space:]]+(.+)$/\2/p' "$log" | head -n1 +} + +extract_npm_error_errno() { + local log="$1" + sed -n -E 's/^npm (ERR!|error) errno[[:space:]]+(.+)$/\2/p' "$log" | head -n1 +} + +print_npm_failure_diagnostics() { + local spec="$1" + local log="$2" + local debug_log="" + local first_error="" + local error_code="" + local error_syscall="" + local error_errno="" + + ui_warn "npm install failed for ${spec}" + if [[ -n "${LAST_NPM_INSTALL_CMD}" ]]; then + echo " Command: ${LAST_NPM_INSTALL_CMD}" + fi + echo " Installer log: ${log}" + + error_code="$(extract_npm_error_code "$log")" + if [[ -n "$error_code" ]]; then + echo " npm code: ${error_code}" + fi + + error_syscall="$(extract_npm_error_syscall "$log")" + if [[ -n "$error_syscall" ]]; then + echo " npm syscall: ${error_syscall}" + fi + + error_errno="$(extract_npm_error_errno "$log")" + if [[ -n "$error_errno" ]]; then + echo " npm errno: ${error_errno}" + fi + + debug_log="$(extract_npm_debug_log_path "$log" || true)" + if [[ -n "$debug_log" ]]; then + echo " npm debug log: ${debug_log}" + fi + + first_error="$(extract_first_npm_error_line "$log")" + if [[ -n "$first_error" ]]; then + echo " First npm error: ${first_error}" + fi +} + +install_openclaw_npm() { + local spec="$1" + local log + log="$(mktempfile)" + if ! run_npm_global_install "$spec" "$log"; then + local attempted_build_tool_fix=false + if auto_install_build_tools_for_npm_failure "$log"; then + attempted_build_tool_fix=true + ui_info "Retrying npm install after build tools setup" + if run_npm_global_install "$spec" "$log"; then + ui_success "OpenClaw npm package installed" + return 0 + fi + fi + + print_npm_failure_diagnostics "$spec" "$log" + + if [[ "$VERBOSE" != "1" ]]; then + if [[ "$attempted_build_tool_fix" == "true" ]]; then + ui_warn "npm install still failed after build tools setup; showing last log lines" + else + ui_warn "npm install failed; showing last log lines" + fi + tail -n 80 "$log" >&2 || true + fi + + if grep -q "ENOTEMPTY: directory not empty, rename .*openclaw" "$log"; then + ui_warn "npm left stale directory; cleaning and retrying" + cleanup_npm_openclaw_paths + if run_npm_global_install "$spec" "$log"; then + ui_success "OpenClaw npm package installed" + return 0 + fi + return 1 + fi + if grep -q "EEXIST" "$log"; then + local conflict="" + conflict="$(extract_openclaw_conflict_path "$log" || true)" + if [[ -n "$conflict" ]] && cleanup_openclaw_bin_conflict "$conflict"; then + if run_npm_global_install "$spec" "$log"; then + ui_success "OpenClaw npm package installed" + return 0 + fi + return 1 + fi + ui_error "npm failed because an openclaw binary already exists" + if [[ -n "$conflict" ]]; then + ui_info "Remove or move ${conflict}, then retry" + fi + ui_info "Or rerun with: npm install -g --force ${spec}" + fi + return 1 + fi + ui_success "OpenClaw npm package installed" + return 0 +} + +TAGLINES=() +TAGLINES+=("Your terminal just grew claws—type something and let the bot pinch the busywork.") +TAGLINES+=("Welcome to the command line: where dreams compile and confidence segfaults.") +TAGLINES+=("I run on caffeine, JSON5, and the audacity of \"it worked on my machine.\"") +TAGLINES+=("Gateway online—please keep hands, feet, and appendages inside the shell at all times.") +TAGLINES+=("I speak fluent bash, mild sarcasm, and aggressive tab-completion energy.") +TAGLINES+=("One CLI to rule them all, and one more restart because you changed the port.") +TAGLINES+=("If it works, it's automation; if it breaks, it's a \"learning opportunity.\"") +TAGLINES+=("Pairing codes exist because even bots believe in consent—and good security hygiene.") +TAGLINES+=("Your .env is showing; don't worry, I'll pretend I didn't see it.") +TAGLINES+=("I'll do the boring stuff while you dramatically stare at the logs like it's cinema.") +TAGLINES+=("I'm not saying your workflow is chaotic... I'm just bringing a linter and a helmet.") +TAGLINES+=("Type the command with confidence—nature will provide the stack trace if needed.") +TAGLINES+=("I don't judge, but your missing API keys are absolutely judging you.") +TAGLINES+=("I can grep it, git blame it, and gently roast it—pick your coping mechanism.") +TAGLINES+=("Hot reload for config, cold sweat for deploys.") +TAGLINES+=("I'm the assistant your terminal demanded, not the one your sleep schedule requested.") +TAGLINES+=("I keep secrets like a vault... unless you print them in debug logs again.") +TAGLINES+=("Automation with claws: minimal fuss, maximal pinch.") +TAGLINES+=("I'm basically a Swiss Army knife, but with more opinions and fewer sharp edges.") +TAGLINES+=("If you're lost, run doctor; if you're brave, run prod; if you're wise, run tests.") +TAGLINES+=("Your task has been queued; your dignity has been deprecated.") +TAGLINES+=("I can't fix your code taste, but I can fix your build and your backlog.") +TAGLINES+=("I'm not magic—I'm just extremely persistent with retries and coping strategies.") +TAGLINES+=("It's not \"failing,\" it's \"discovering new ways to configure the same thing wrong.\"") +TAGLINES+=("Give me a workspace and I'll give you fewer tabs, fewer toggles, and more oxygen.") +TAGLINES+=("I read logs so you can keep pretending you don't have to.") +TAGLINES+=("If something's on fire, I can't extinguish it—but I can write a beautiful postmortem.") +TAGLINES+=("I'll refactor your busywork like it owes me money.") +TAGLINES+=("Say \"stop\" and I'll stop—say \"ship\" and we'll both learn a lesson.") +TAGLINES+=("I'm the reason your shell history looks like a hacker-movie montage.") +TAGLINES+=("I'm like tmux: confusing at first, then suddenly you can't live without me.") +TAGLINES+=("I can run local, remote, or purely on vibes—results may vary with DNS.") +TAGLINES+=("If you can describe it, I can probably automate it—or at least make it funnier.") +TAGLINES+=("Your config is valid, your assumptions are not.") +TAGLINES+=("I don't just autocomplete—I auto-commit (emotionally), then ask you to review (logically).") +TAGLINES+=("Less clicking, more shipping, fewer \"where did that file go\" moments.") +TAGLINES+=("Claws out, commit in—let's ship something mildly responsible.") +TAGLINES+=("I'll butter your workflow like a lobster roll: messy, delicious, effective.") +TAGLINES+=("Shell yeah—I'm here to pinch the toil and leave you the glory.") +TAGLINES+=("If it's repetitive, I'll automate it; if it's hard, I'll bring jokes and a rollback plan.") +TAGLINES+=("Because texting yourself reminders is so 2024.") +TAGLINES+=("WhatsApp, but make it ✨engineering✨.") +TAGLINES+=("Turning \"I'll reply later\" into \"my bot replied instantly\".") +TAGLINES+=("The only crab in your contacts you actually want to hear from. 🦞") +TAGLINES+=("Chat automation for people who peaked at IRC.") +TAGLINES+=("Because Siri wasn't answering at 3AM.") +TAGLINES+=("IPC, but it's your phone.") +TAGLINES+=("The UNIX philosophy meets your DMs.") +TAGLINES+=("curl for conversations.") +TAGLINES+=("WhatsApp Business, but without the business.") +TAGLINES+=("Meta wishes they shipped this fast.") +TAGLINES+=("End-to-end encrypted, Zuck-to-Zuck excluded.") +TAGLINES+=("The only bot Mark can't train on your DMs.") +TAGLINES+=("WhatsApp automation without the \"please accept our new privacy policy\".") +TAGLINES+=("Chat APIs that don't require a Senate hearing.") +TAGLINES+=("Because Threads wasn't the answer either.") +TAGLINES+=("Your messages, your servers, Meta's tears.") +TAGLINES+=("iMessage green bubble energy, but for everyone.") +TAGLINES+=("Siri's competent cousin.") +TAGLINES+=("Works on Android. Crazy concept, we know.") +TAGLINES+=("No \$999 stand required.") +TAGLINES+=("We ship features faster than Apple ships calculator updates.") +TAGLINES+=("Your AI assistant, now without the \$3,499 headset.") +TAGLINES+=("Think different. Actually think.") +TAGLINES+=("Ah, the fruit tree company! 🍎") + +HOLIDAY_NEW_YEAR="New Year's Day: New year, new config—same old EADDRINUSE, but this time we resolve it like grown-ups." +HOLIDAY_LUNAR_NEW_YEAR="Lunar New Year: May your builds be lucky, your branches prosperous, and your merge conflicts chased away with fireworks." +HOLIDAY_CHRISTMAS="Christmas: Ho ho ho—Santa's little claw-sistant is here to ship joy, roll back chaos, and stash the keys safely." +HOLIDAY_EID="Eid al-Fitr: Celebration mode: queues cleared, tasks completed, and good vibes committed to main with clean history." +HOLIDAY_DIWALI="Diwali: Let the logs sparkle and the bugs flee—today we light up the terminal and ship with pride." +HOLIDAY_EASTER="Easter: I found your missing environment variable—consider it a tiny CLI egg hunt with fewer jellybeans." +HOLIDAY_HANUKKAH="Hanukkah: Eight nights, eight retries, zero shame—may your gateway stay lit and your deployments stay peaceful." +HOLIDAY_HALLOWEEN="Halloween: Spooky season: beware haunted dependencies, cursed caches, and the ghost of node_modules past." +HOLIDAY_THANKSGIVING="Thanksgiving: Grateful for stable ports, working DNS, and a bot that reads the logs so nobody has to." +HOLIDAY_VALENTINES="Valentine's Day: Roses are typed, violets are piped—I'll automate the chores so you can spend time with humans." + +append_holiday_taglines() { + local today + local month_day + today="$(date -u +%Y-%m-%d 2>/dev/null || date +%Y-%m-%d)" + month_day="$(date -u +%m-%d 2>/dev/null || date +%m-%d)" + + case "$month_day" in + "01-01") TAGLINES+=("$HOLIDAY_NEW_YEAR") ;; + "02-14") TAGLINES+=("$HOLIDAY_VALENTINES") ;; + "10-31") TAGLINES+=("$HOLIDAY_HALLOWEEN") ;; + "12-25") TAGLINES+=("$HOLIDAY_CHRISTMAS") ;; + esac + + case "$today" in + "2025-01-29"|"2026-02-17"|"2027-02-06") TAGLINES+=("$HOLIDAY_LUNAR_NEW_YEAR") ;; + "2025-03-30"|"2025-03-31"|"2026-03-20"|"2027-03-10") TAGLINES+=("$HOLIDAY_EID") ;; + "2025-10-20"|"2026-11-08"|"2027-10-28") TAGLINES+=("$HOLIDAY_DIWALI") ;; + "2025-04-20"|"2026-04-05"|"2027-03-28") TAGLINES+=("$HOLIDAY_EASTER") ;; + "2025-11-27"|"2026-11-26"|"2027-11-25") TAGLINES+=("$HOLIDAY_THANKSGIVING") ;; + "2025-12-15"|"2025-12-16"|"2025-12-17"|"2025-12-18"|"2025-12-19"|"2025-12-20"|"2025-12-21"|"2025-12-22"|"2026-12-05"|"2026-12-06"|"2026-12-07"|"2026-12-08"|"2026-12-09"|"2026-12-10"|"2026-12-11"|"2026-12-12"|"2027-12-25"|"2027-12-26"|"2027-12-27"|"2027-12-28"|"2027-12-29"|"2027-12-30"|"2027-12-31"|"2028-01-01") TAGLINES+=("$HOLIDAY_HANUKKAH") ;; + esac +} + +map_legacy_env() { + local key="$1" + local legacy="$2" + if [[ -z "${!key:-}" && -n "${!legacy:-}" ]]; then + printf -v "$key" '%s' "${!legacy}" + fi +} + +map_legacy_env "OPENCLAW_TAGLINE_INDEX" "CLAWDBOT_TAGLINE_INDEX" +map_legacy_env "OPENCLAW_NO_ONBOARD" "CLAWDBOT_NO_ONBOARD" +map_legacy_env "OPENCLAW_NO_PROMPT" "CLAWDBOT_NO_PROMPT" +map_legacy_env "OPENCLAW_DRY_RUN" "CLAWDBOT_DRY_RUN" +map_legacy_env "OPENCLAW_INSTALL_METHOD" "CLAWDBOT_INSTALL_METHOD" +map_legacy_env "OPENCLAW_VERSION" "CLAWDBOT_VERSION" +map_legacy_env "OPENCLAW_BETA" "CLAWDBOT_BETA" +map_legacy_env "OPENCLAW_GIT_DIR" "CLAWDBOT_GIT_DIR" +map_legacy_env "OPENCLAW_GIT_UPDATE" "CLAWDBOT_GIT_UPDATE" +map_legacy_env "OPENCLAW_NPM_LOGLEVEL" "CLAWDBOT_NPM_LOGLEVEL" +map_legacy_env "OPENCLAW_VERBOSE" "CLAWDBOT_VERBOSE" +map_legacy_env "OPENCLAW_PROFILE" "CLAWDBOT_PROFILE" +map_legacy_env "OPENCLAW_INSTALL_SH_NO_RUN" "CLAWDBOT_INSTALL_SH_NO_RUN" + +pick_tagline() { + append_holiday_taglines + local count=${#TAGLINES[@]} + if [[ "$count" -eq 0 ]]; then + echo "$DEFAULT_TAGLINE" + return + fi + if [[ -n "${OPENCLAW_TAGLINE_INDEX:-}" ]]; then + if [[ "${OPENCLAW_TAGLINE_INDEX}" =~ ^[0-9]+$ ]]; then + local idx=$((OPENCLAW_TAGLINE_INDEX % count)) + echo "${TAGLINES[$idx]}" + return + fi + fi + local idx=$((RANDOM % count)) + echo "${TAGLINES[$idx]}" +} + +TAGLINE=$(pick_tagline) + +NO_ONBOARD=${OPENCLAW_NO_ONBOARD:-0} +NO_PROMPT=${OPENCLAW_NO_PROMPT:-0} +DRY_RUN=${OPENCLAW_DRY_RUN:-0} +INSTALL_METHOD=${OPENCLAW_INSTALL_METHOD:-} +OPENCLAW_VERSION=${OPENCLAW_VERSION:-latest} +USE_BETA=${OPENCLAW_BETA:-0} +GIT_DIR_DEFAULT="${HOME}/openclaw" +GIT_DIR=${OPENCLAW_GIT_DIR:-$GIT_DIR_DEFAULT} +GIT_UPDATE=${OPENCLAW_GIT_UPDATE:-1} +SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}" +NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}" +NPM_SILENT_FLAG="--silent" +VERBOSE="${OPENCLAW_VERBOSE:-0}" +OPENCLAW_BIN="" +SELECTED_NODE_BIN="" +PNPM_CMD=() +HELP=0 + +print_usage() { + cat < npm install: version (default: latest) + --beta Use beta if available, else latest + --git-dir, --dir Checkout directory (default: ~/openclaw) + --no-git-update Skip git pull for existing checkout + --no-onboard Skip onboarding (non-interactive) + --no-prompt Disable prompts (required in CI/automation) + --dry-run Print what would happen (no changes) + --verbose Print debug output (set -x, npm verbose) + --help, -h Show this help + +Environment variables: + OPENCLAW_INSTALL_METHOD=git|npm + OPENCLAW_VERSION=latest|next| + OPENCLAW_BETA=0|1 + OPENCLAW_GIT_DIR=... + OPENCLAW_GIT_UPDATE=0|1 + OPENCLAW_NO_PROMPT=1 + OPENCLAW_DRY_RUN=1 + OPENCLAW_NO_ONBOARD=1 + OPENCLAW_VERBOSE=1 + OPENCLAW_NPM_LOGLEVEL=error|warn|notice Default: error (hide npm deprecation noise) + SHARP_IGNORE_GLOBAL_LIBVIPS=0|1 Default: 1 (avoid sharp building against global libvips) + +Examples: + curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash + curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard + curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboard +EOF +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --no-onboard) + NO_ONBOARD=1 + shift + ;; + --onboard) + NO_ONBOARD=0 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + --verbose) + VERBOSE=1 + shift + ;; + --no-prompt) + NO_PROMPT=1 + shift + ;; + --help|-h) + HELP=1 + shift + ;; + --install-method|--method) + INSTALL_METHOD="$2" + shift 2 + ;; + --version) + OPENCLAW_VERSION="$2" + shift 2 + ;; + --beta) + USE_BETA=1 + shift + ;; + --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 + ;; + *) + shift + ;; + esac + done +} + +configure_verbose() { + if [[ "$VERBOSE" != "1" ]]; then + return 0 + fi + if [[ "$NPM_LOGLEVEL" == "error" ]]; then + NPM_LOGLEVEL="notice" + fi + NPM_SILENT_FLAG="" + set -x +} + +is_promptable() { + if [[ "$NO_PROMPT" == "1" ]]; then + return 1 + fi + if [[ -r /dev/tty && -w /dev/tty ]]; then + return 0 + fi + return 1 +} + +prompt_choice() { + local prompt="$1" + local answer="" + if ! is_promptable; then + return 1 + fi + echo -e "$prompt" > /dev/tty + read -r answer < /dev/tty || true + echo "$answer" +} + +choose_install_method_interactive() { + local detected_checkout="$1" + + if ! is_promptable; then + return 1 + fi + + if [[ -n "$GUM" ]] && gum_is_tty; then + local header selection + header="Detected OpenClaw checkout in: ${detected_checkout} +Choose install method" + selection="$("$GUM" choose \ + --header "$header" \ + --cursor-prefix "❯ " \ + "git · update this checkout and use it" \ + "npm · install globally via npm" < /dev/tty || true)" + + case "$selection" in + git*) + echo "git" + return 0 + ;; + npm*) + echo "npm" + return 0 + ;; + esac + return 1 + fi + + local choice="" + choice="$(prompt_choice "$(cat </dev/null; then + return 1 + fi + echo "$dir" + return 0 +} + +# Check for Homebrew on macOS +is_macos_admin_user() { + if [[ "$OS" != "macos" ]]; then + return 0 + fi + if is_root; then + return 0 + fi + id -Gn "$(id -un)" 2>/dev/null | grep -qw "admin" +} + +print_homebrew_admin_fix() { + local current_user + current_user="$(id -un 2>/dev/null || echo "${USER:-current user}")" + ui_error "Homebrew installation requires a macOS Administrator account" + echo "Current user (${current_user}) is not in the admin group." + echo "Fix options:" + echo " 1) Use an Administrator account and re-run the installer." + echo " 2) Ask an Administrator to grant admin rights, then sign out/in:" + echo " sudo dseditgroup -o edit -a ${current_user} -t user admin" + echo "Then retry:" + echo " curl -fsSL https://openclaw.ai/install.sh | bash" +} + +install_homebrew() { + local brew_bin="" + if [[ "$OS" == "macos" ]]; then + brew_bin="$(resolve_brew_bin || true)" + if [[ -z "$brew_bin" ]]; then + if ! is_macos_admin_user; then + print_homebrew_admin_fix + exit 1 + fi + ui_info "Homebrew not found, installing" + run_quiet_step "Installing Homebrew" run_remote_bash "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + + # Add Homebrew to PATH for this session + if ! activate_brew_for_session; then + ui_warn "Homebrew install completed but brew is still unavailable in this shell" + fi + ui_success "Homebrew installed" + else + activate_brew_for_session || true + ui_success "Homebrew already installed" + fi + fi +} + +# Check Node.js version +node_major_version() { + if ! command -v node &> /dev/null; then + return 1 + fi + local version major + version="$(node -v 2>/dev/null || true)" + major="${version#v}" + major="${major%%.*}" + if [[ "$major" =~ ^[0-9]+$ ]]; then + echo "$major" + return 0 + fi + return 1 +} + +print_active_node_paths() { + if ! command -v node &> /dev/null; then + return 1 + fi + local node_path node_version npm_path npm_version + node_path="$(command -v node 2>/dev/null || true)" + node_version="$(node -v 2>/dev/null || true)" + ui_info "Active Node.js: ${node_version:-unknown} (${node_path:-unknown})" + + if command -v npm &> /dev/null; then + npm_path="$(command -v npm 2>/dev/null || true)" + npm_version="$(npm -v 2>/dev/null || true)" + ui_info "Active npm: ${npm_version:-unknown} (${npm_path:-unknown})" + fi + return 0 +} + +ensure_macos_node22_active() { + if [[ "$OS" != "macos" ]]; then + return 0 + fi + + local brew_bin="" + local brew_node_prefix="" + brew_bin="$(resolve_brew_bin || true)" + if [[ -n "$brew_bin" ]]; then + activate_brew_for_session || true + brew_node_prefix="$("$brew_bin" --prefix node@22 2>/dev/null || true)" + if [[ -n "$brew_node_prefix" && -x "${brew_node_prefix}/bin/node" ]]; then + export PATH="${brew_node_prefix}/bin:$PATH" + refresh_shell_command_cache + fi + fi + + local major="" + major="$(node_major_version || true)" + if [[ -n "$major" && "$major" -ge 22 ]]; then + return 0 + fi + + local active_path active_version + active_path="$(command -v node 2>/dev/null || echo "not found")" + active_version="$(node -v 2>/dev/null || echo "missing")" + + ui_error "Node.js v22 was installed but this shell is using ${active_version} (${active_path})" + if [[ -n "$brew_node_prefix" ]]; then + echo "Add this to your shell profile and restart shell:" + echo " export PATH=\"${brew_node_prefix}/bin:\$PATH\"" + else + echo "Ensure Homebrew node@22 is first on PATH, then rerun installer." + fi + return 1 +} + +check_node() { + if command -v node &> /dev/null; then + NODE_VERSION="$(node_major_version || true)" + if [[ -n "$NODE_VERSION" && "$NODE_VERSION" -ge 22 ]]; then + ui_success "Node.js v$(node -v | cut -d'v' -f2) found" + print_active_node_paths || true + return 0 + else + if [[ -n "$NODE_VERSION" ]]; then + ui_info "Node.js $(node -v) found, upgrading to v22+" + else + ui_info "Node.js found but version could not be parsed; reinstalling v22+" + fi + return 1 + fi + else + ui_info "Node.js not found, installing it now" + return 1 + fi +} + +node_major_from_binary() { + local node_bin="$1" + if [[ -z "$node_bin" || ! -x "$node_bin" ]]; then + return 1 + fi + "$node_bin" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true +} + +node_is_supported_binary() { + local node_bin="$1" + local major="" + major="$(node_major_from_binary "$node_bin")" + if [[ ! "$major" =~ ^[0-9]+$ ]]; then + return 1 + fi + [[ "$major" -ge 22 ]] +} + +has_supported_node() { + local node_bin="" + node_bin="$(command -v node 2>/dev/null || true)" + if [[ -z "$node_bin" ]]; then + return 1 + fi + node_is_supported_binary "$node_bin" +} + +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 + hash -r 2>/dev/null || true +} + +ensure_supported_node_on_path() { + if has_supported_node; then + SELECTED_NODE_BIN="$(command -v node 2>/dev/null || true)" + return 0 + fi + + local -a candidates=() + local candidate="" + while IFS= read -r candidate; do + [[ -n "$candidate" ]] && candidates+=("$candidate") + done < <(type -aP node 2>/dev/null || true) + candidates+=( + "/usr/bin/node" + "/usr/local/bin/node" + "/opt/homebrew/bin/node" + "/opt/homebrew/opt/node@22/bin/node" + "/usr/local/opt/node@22/bin/node" + ) + + local seen=":" + for candidate in "${candidates[@]}"; do + if [[ -z "$candidate" || ! -x "$candidate" ]]; then + continue + fi + case "$seen" in + *":$candidate:"*) continue ;; + esac + seen="${seen}${candidate}:" + + if node_is_supported_binary "$candidate"; then + prepend_path_dir "$(dirname "$candidate")" || continue + SELECTED_NODE_BIN="$candidate" + ui_info "Using Node.js runtime at ${candidate}" + return 0 + fi + done + + return 1 +} + +original_path_node_bin() { + if [[ -z "${ORIGINAL_PATH:-}" ]]; then + return 1 + fi + PATH="$ORIGINAL_PATH" command -v node 2>/dev/null || true +} + +original_path_has_supported_node() { + local node_bin="" + node_bin="$(original_path_node_bin)" + if [[ -z "$node_bin" ]]; then + return 1 + fi + node_is_supported_binary "$node_bin" +} + +find_openclaw_entry_path() { + local npm_root="" + npm_root="$(npm root -g 2>/dev/null || true)" + if [[ -z "$npm_root" ]]; then + return 1 + fi + local entry_js="${npm_root}/openclaw/dist/entry.js" + if [[ -f "$entry_js" ]]; then + echo "$entry_js" + return 0 + fi + local entry_mjs="${npm_root}/openclaw/dist/entry.mjs" + if [[ -f "$entry_mjs" ]]; then + echo "$entry_mjs" + return 0 + fi + return 1 +} + +install_openclaw_compat_shim() { + if [[ "$INSTALL_METHOD" != "npm" ]]; then + return 0 + fi + if original_path_has_supported_node; then + return 0 + fi + + local node_bin="${SELECTED_NODE_BIN:-}" + if [[ -z "$node_bin" ]]; then + node_bin="$(command -v node 2>/dev/null || true)" + fi + if [[ -z "$node_bin" || ! -x "$node_bin" ]] || ! node_is_supported_binary "$node_bin"; then + return 1 + fi + + local entry_path="" + entry_path="$(find_openclaw_entry_path || true)" + if [[ -z "$entry_path" ]]; then + return 1 + fi + + local target_dir="$HOME/.local/bin" + ensure_user_local_bin_on_path + + mkdir -p "$target_dir" + local shim_path="${target_dir}/openclaw" + cat > "$shim_path" </dev/null || echo '22+')" + return 0 +} + +# Install Node.js +install_node() { + if [[ "$OS" == "macos" ]]; then + ui_info "Installing Node.js via Homebrew" + run_quiet_step "Installing node@22" brew install node@22 + brew link node@22 --overwrite --force 2>/dev/null || true + if ! ensure_macos_node22_active; then + exit 1 + fi + ui_success "Node.js installed" + print_active_node_paths || true + elif [[ "$OS" == "linux" ]]; then + ui_info "Installing Node.js via NodeSource" + require_sudo + + ui_info "Installing Linux build tools (make/g++/cmake/python3)" + if install_build_tools_linux; then + ui_success "Build tools installed" + else + ui_warn "Continuing without auto-installing build tools" + fi + + if command -v apt-get &> /dev/null; then + local tmp + tmp="$(mktempfile)" + download_file "https://deb.nodesource.com/setup_22.x" "$tmp" + if is_root; then + run_quiet_step "Configuring NodeSource repository" bash "$tmp" + run_quiet_step "Installing Node.js" apt-get install -y -qq nodejs + else + run_quiet_step "Configuring NodeSource repository" sudo -E bash "$tmp" + run_quiet_step "Installing Node.js" sudo apt-get install -y -qq nodejs + fi + elif command -v dnf &> /dev/null; then + local tmp + tmp="$(mktempfile)" + download_file "https://rpm.nodesource.com/setup_22.x" "$tmp" + if is_root; then + run_quiet_step "Configuring NodeSource repository" bash "$tmp" + run_quiet_step "Installing Node.js" dnf install -y -q nodejs + else + run_quiet_step "Configuring NodeSource repository" sudo bash "$tmp" + run_quiet_step "Installing Node.js" sudo dnf install -y -q nodejs + fi + elif command -v yum &> /dev/null; then + local tmp + tmp="$(mktempfile)" + download_file "https://rpm.nodesource.com/setup_22.x" "$tmp" + if is_root; then + run_quiet_step "Configuring NodeSource repository" bash "$tmp" + run_quiet_step "Installing Node.js" yum install -y -q nodejs + else + run_quiet_step "Configuring NodeSource repository" sudo bash "$tmp" + run_quiet_step "Installing Node.js" sudo yum install -y -q nodejs + fi + else + ui_error "Could not detect package manager" + echo "Please install Node.js 22+ manually: https://nodejs.org" + exit 1 + fi + + ui_success "Node.js v22 installed" + print_active_node_paths || true + fi + + # Detect NVM and warn if the active Node is still from NVM with old version + detect_nvm_and_warn +} + +# Detect NVM and warn user if they need to switch Node version +detect_nvm_and_warn() { + # Check if NVM is installed (look for NVM_DIR or nvm script) + local nvm_dir="${NVM_DIR:-}" + if [[ -z "$nvm_dir" ]] && [[ -f "${HOME}/.nvm/nvm.sh" ]]; then + nvm_dir="${HOME}/.nvm" + fi + + # If NVM not found, nothing to do + if [[ -z "$nvm_dir" ]]; then + return 0 + fi + + # NVM is present - check if current node is from NVM and old + local node_path + node_path="$(command -v node 2>/dev/null || true)" + + if [[ -n "$node_path" && "$node_path" == *".nvm"* ]]; then + local current_version + current_version="$(node -v 2>/dev/null || true)" + local major="${current_version#v}" + major="${major%%.*}" + + if [[ -n "$major" && "$major" -lt 22 ]]; then + ui_warn "" + ui_warn "⚠️ NVM detected with old default Node version" + ui_warn " Your shell is using NVM's Node ${current_version}, but OpenClaw requires Node 22+" + ui_warn "" + ui_info "To fix this, run:" + ui_info " nvm install 22" + ui_info " nvm use 22" + ui_info " nvm alias default 22" + ui_warn "" + ui_warn "Then restart your terminal and run the installer again." + exit 1 + fi + fi +} + +# Check Git +check_git() { + if command -v git &> /dev/null; then + ui_success "Git already installed" + return 0 + fi + ui_info "Git not found, installing it now" + return 1 +} + +is_root() { + [[ "$(id -u)" -eq 0 ]] +} + +# Run a command with sudo only if not already root +maybe_sudo() { + if is_root; then + # Skip -E flag when root (env is already preserved) + if [[ "${1:-}" == "-E" ]]; then + shift + fi + "$@" + else + sudo "$@" + fi +} + +require_sudo() { + if [[ "$OS" != "linux" ]]; then + return 0 + fi + if is_root; then + return 0 + fi + if command -v sudo &> /dev/null; then + if ! sudo -n true >/dev/null 2>&1; then + ui_info "Administrator privileges required; enter your password" + sudo -v + fi + return 0 + fi + ui_error "sudo is required for system installs on Linux" + echo " Install sudo or re-run as root." + exit 1 +} + +install_git() { + if [[ "$OS" == "macos" ]]; then + run_quiet_step "Installing Git" brew install git + elif [[ "$OS" == "linux" ]]; then + require_sudo + if command -v apt-get &> /dev/null; then + if is_root; then + run_quiet_step "Updating package index" apt-get update -qq + run_quiet_step "Installing Git" apt-get install -y -qq git + else + run_quiet_step "Updating package index" sudo apt-get update -qq + run_quiet_step "Installing Git" sudo apt-get install -y -qq git + fi + elif command -v dnf &> /dev/null; then + if is_root; then + run_quiet_step "Installing Git" dnf install -y -q git + else + run_quiet_step "Installing Git" sudo dnf install -y -q git + fi + elif command -v yum &> /dev/null; then + if is_root; then + run_quiet_step "Installing Git" yum install -y -q git + else + run_quiet_step "Installing Git" sudo yum install -y -q git + fi + else + ui_error "Could not detect package manager for Git" + exit 1 + fi + fi + ui_success "Git installed" +} + +# Fix npm permissions for global installs (Linux) +fix_npm_permissions() { + if [[ "$OS" != "linux" ]]; then + return 0 + fi + + local npm_prefix + npm_prefix="$(npm config get prefix 2>/dev/null || true)" + if [[ -z "$npm_prefix" ]]; then + return 0 + fi + + if [[ -w "$npm_prefix" || -w "$npm_prefix/lib" ]]; then + return 0 + fi + + ui_info "Configuring npm for user-local installs" + mkdir -p "$HOME/.npm-global" + npm config set prefix "$HOME/.npm-global" + + # shellcheck disable=SC2016 + local path_line='export PATH="$HOME/.npm-global/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="$HOME/.npm-global/bin:$PATH" + ui_success "npm configured for user installs" +} + +ensure_openclaw_bin_link() { + local npm_root="" + npm_root="$(npm root -g 2>/dev/null || true)" + if [[ -z "$npm_root" || ! -d "$npm_root/openclaw" ]]; then + return 1 + fi + local npm_bin="" + npm_bin="$(npm_global_bin_dir || true)" + if [[ -z "$npm_bin" ]]; then + return 1 + fi + mkdir -p "$npm_bin" + if [[ ! -x "${npm_bin}/openclaw" ]]; then + ln -sf "$npm_root/openclaw/dist/entry.js" "${npm_bin}/openclaw" + ui_info "Created openclaw bin link at ${npm_bin}/openclaw" + fi + return 0 +} + +# Check for existing OpenClaw installation +check_existing_openclaw() { + if [[ -n "$(type -P openclaw 2>/dev/null || true)" ]]; then + ui_info "Existing OpenClaw installation detected, upgrading" + return 0 + fi + return 1 +} + +set_pnpm_cmd() { + PNPM_CMD=("$@") +} + +pnpm_cmd_pretty() { + if [[ ${#PNPM_CMD[@]} -eq 0 ]]; then + echo "" + return 1 + fi + printf '%s' "${PNPM_CMD[*]}" + return 0 +} + +pnpm_cmd_is_ready() { + if [[ ${#PNPM_CMD[@]} -eq 0 ]]; then + return 1 + fi + "${PNPM_CMD[@]}" --version >/dev/null 2>&1 +} + +detect_pnpm_cmd() { + if command -v pnpm &> /dev/null; then + set_pnpm_cmd pnpm + return 0 + fi + if command -v corepack &> /dev/null; then + if corepack pnpm --version >/dev/null 2>&1; then + set_pnpm_cmd corepack pnpm + return 0 + fi + fi + return 1 +} + +ensure_pnpm() { + if detect_pnpm_cmd && pnpm_cmd_is_ready; then + ui_success "pnpm ready ($(pnpm_cmd_pretty))" + return 0 + fi + + if command -v corepack &> /dev/null; then + ui_info "Configuring pnpm via Corepack" + corepack enable >/dev/null 2>&1 || true + if ! run_quiet_step "Activating pnpm" corepack prepare pnpm@10 --activate; then + ui_warn "Corepack pnpm activation failed; falling back" + fi + refresh_shell_command_cache + if detect_pnpm_cmd && pnpm_cmd_is_ready; then + if [[ "${PNPM_CMD[*]}" == "corepack pnpm" ]]; then + ui_warn "pnpm shim not on PATH; using corepack pnpm fallback" + fi + ui_success "pnpm ready ($(pnpm_cmd_pretty))" + return 0 + fi + fi + + ui_info "Installing pnpm via npm" + fix_npm_permissions + run_quiet_step "Installing pnpm" npm install -g pnpm@10 + refresh_shell_command_cache + if detect_pnpm_cmd && pnpm_cmd_is_ready; then + ui_success "pnpm ready ($(pnpm_cmd_pretty))" + return 0 + fi + + ui_error "pnpm installation failed" + return 1 +} + +ensure_pnpm_binary_for_scripts() { + if command -v pnpm >/dev/null 2>&1; then + return 0 + fi + + if command -v corepack >/dev/null 2>&1; then + ui_info "Ensuring pnpm command is available" + corepack enable >/dev/null 2>&1 || true + corepack prepare pnpm@10 --activate >/dev/null 2>&1 || true + refresh_shell_command_cache + if command -v pnpm >/dev/null 2>&1; then + ui_success "pnpm command enabled via Corepack" + return 0 + fi + fi + + if [[ "${PNPM_CMD[*]}" == "corepack pnpm" ]] && command -v corepack >/dev/null 2>&1; then + ensure_user_local_bin_on_path + local user_pnpm="${HOME}/.local/bin/pnpm" + cat >"${user_pnpm}" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +exec corepack pnpm "$@" +EOF + chmod +x "${user_pnpm}" + refresh_shell_command_cache + + if command -v pnpm >/dev/null 2>&1; then + ui_warn "pnpm shim not on PATH; installed user-local wrapper at ${user_pnpm}" + return 0 + fi + fi + + ui_error "pnpm command not available on PATH" + ui_info "Install pnpm globally (npm install -g pnpm@10) and retry" + return 1 +} + +run_pnpm() { + if ! pnpm_cmd_is_ready; then + ensure_pnpm + fi + "${PNPM_CMD[@]}" "$@" +} + +ensure_user_local_bin_on_path() { + local target="$HOME/.local/bin" + mkdir -p "$target" + + export PATH="$target:$PATH" + + # shellcheck disable=SC2016 + local path_line='export PATH="$HOME/.local/bin:$PATH"' + for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do + if [[ -f "$rc" ]] && ! grep -q ".local/bin" "$rc"; then + echo "$path_line" >> "$rc" + fi + done +} + +npm_global_bin_dir() { + local prefix="" + prefix="$(npm prefix -g 2>/dev/null || true)" + if [[ -n "$prefix" ]]; then + if [[ "$prefix" == /* ]]; then + echo "${prefix%/}/bin" + return 0 + fi + fi + + prefix="$(npm config get prefix 2>/dev/null || true)" + if [[ -n "$prefix" && "$prefix" != "undefined" && "$prefix" != "null" ]]; then + if [[ "$prefix" == /* ]]; then + echo "${prefix%/}/bin" + return 0 + fi + fi + + echo "" + return 1 +} + +refresh_shell_command_cache() { + hash -r 2>/dev/null || true +} + +path_has_dir() { + local path="$1" + local dir="${2%/}" + if [[ -z "$dir" ]]; then + return 1 + fi + case ":${path}:" in + *":${dir}:"*) return 0 ;; + *) return 1 ;; + esac +} + +warn_shell_path_missing_dir() { + local dir="${1%/}" + local label="$2" + if [[ -z "$dir" ]]; then + return 0 + fi + if path_has_dir "$ORIGINAL_PATH" "$dir"; then + return 0 + fi + + echo "" + ui_warn "PATH missing ${label}: ${dir}" + echo " This can make openclaw show as \"command not found\" in new terminals." + echo " Fix (zsh: ~/.zshrc, bash: ~/.bashrc):" + echo " export PATH=\"${dir}:\$PATH\"" +} + +ensure_npm_global_bin_on_path() { + local bin_dir="" + bin_dir="$(npm_global_bin_dir || true)" + if [[ -n "$bin_dir" ]]; then + export PATH="${bin_dir}:$PATH" + fi +} + +maybe_nodenv_rehash() { + if command -v nodenv &> /dev/null; then + nodenv rehash >/dev/null 2>&1 || true + fi +} + +warn_openclaw_not_found() { + ui_warn "Installed, but openclaw is not discoverable on PATH in this shell" + echo " Try: hash -r (bash) or rehash (zsh), then retry." + local t="" + t="$(type -t openclaw 2>/dev/null || true)" + if [[ "$t" == "alias" || "$t" == "function" ]]; then + ui_warn "Found a shell ${t} named openclaw; it may shadow the real binary" + fi + if command -v nodenv &> /dev/null; then + echo -e "Using nodenv? Run: ${INFO}nodenv rehash${NC}" + fi + + local npm_prefix="" + npm_prefix="$(npm prefix -g 2>/dev/null || true)" + local npm_bin="" + npm_bin="$(npm_global_bin_dir 2>/dev/null || true)" + if [[ -n "$npm_prefix" ]]; then + echo -e "npm prefix -g: ${INFO}${npm_prefix}${NC}" + fi + if [[ -n "$npm_bin" ]]; then + echo -e "npm bin -g: ${INFO}${npm_bin}${NC}" + echo -e "If needed: ${INFO}export PATH=\"${npm_bin}:\\$PATH\"${NC}" + fi +} + +resolve_openclaw_bin() { + refresh_shell_command_cache + local resolved="" + resolved="$(type -P openclaw 2>/dev/null || true)" + if [[ -n "$resolved" && -x "$resolved" ]]; then + echo "$resolved" + return 0 + fi + + ensure_npm_global_bin_on_path + refresh_shell_command_cache + resolved="$(type -P openclaw 2>/dev/null || true)" + if [[ -n "$resolved" && -x "$resolved" ]]; then + echo "$resolved" + return 0 + fi + + local npm_bin="" + npm_bin="$(npm_global_bin_dir || true)" + if [[ -n "$npm_bin" && -x "${npm_bin}/openclaw" ]]; then + echo "${npm_bin}/openclaw" + return 0 + fi + + maybe_nodenv_rehash + refresh_shell_command_cache + resolved="$(type -P openclaw 2>/dev/null || true)" + if [[ -n "$resolved" && -x "$resolved" ]]; then + echo "$resolved" + return 0 + fi + + if [[ -n "$npm_bin" && -x "${npm_bin}/openclaw" ]]; then + echo "${npm_bin}/openclaw" + return 0 + fi + + echo "" + return 1 +} + +install_openclaw_from_git() { + local repo_dir="$1" + local repo_url="https://github.com/openclaw/openclaw.git" + + if [[ -d "$repo_dir/.git" ]]; then + ui_info "Installing OpenClaw from git checkout: ${repo_dir}" + else + ui_info "Installing OpenClaw from GitHub (${repo_url})" + fi + + if ! check_git; then + install_git + fi + + ensure_pnpm + ensure_pnpm_binary_for_scripts + + if [[ ! -d "$repo_dir" ]]; then + run_quiet_step "Cloning OpenClaw" 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 + run_quiet_step "Updating repository" git -C "$repo_dir" pull --rebase || true + else + ui_info "Repo has local changes; 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_quiet_step "Installing dependencies" run_pnpm -C "$repo_dir" install + + if ! run_quiet_step "Building UI" run_pnpm -C "$repo_dir" ui:build; then + ui_warn "UI build failed; continuing (CLI may still work)" + fi + run_quiet_step "Building OpenClaw" run_pnpm -C "$repo_dir" build + + ensure_user_local_bin_on_path + + cat > "$HOME/.local/bin/openclaw" <"$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 - "$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 + + ui_info "Updated pnpm allowlist for git-hosted build dependency: ${dep}" +} + +# Install OpenClaw +resolve_beta_version() { + local beta="" + beta="$(npm view openclaw dist-tags.beta 2>/dev/null || true)" + if [[ -z "$beta" || "$beta" == "undefined" || "$beta" == "null" ]]; then + return 1 + fi + echo "$beta" +} + +install_openclaw() { + local package_name="openclaw" + if [[ "$USE_BETA" == "1" ]]; then + local beta_version="" + beta_version="$(resolve_beta_version || true)" + if [[ -n "$beta_version" ]]; then + OPENCLAW_VERSION="$beta_version" + ui_info "Beta tag detected (${beta_version})" + package_name="openclaw" + else + OPENCLAW_VERSION="latest" + ui_info "No beta tag found; using latest" + fi + fi + + if [[ -z "${OPENCLAW_VERSION}" ]]; then + OPENCLAW_VERSION="latest" + fi + + local resolved_version="" + resolved_version="$(npm view "${package_name}@${OPENCLAW_VERSION}" version 2>/dev/null || true)" + if [[ -n "$resolved_version" ]]; then + ui_info "Installing OpenClaw v${resolved_version}" + else + ui_info "Installing OpenClaw (${OPENCLAW_VERSION})" + fi + local install_spec="" + if [[ "${OPENCLAW_VERSION}" == "latest" ]]; then + install_spec="${package_name}@latest" + else + install_spec="${package_name}@${OPENCLAW_VERSION}" + fi + + if ! install_openclaw_npm "${install_spec}"; then + ui_warn "npm install failed; retrying" + cleanup_npm_openclaw_paths + install_openclaw_npm "${install_spec}" + fi + + if [[ "${OPENCLAW_VERSION}" == "latest" && "${package_name}" == "openclaw" ]]; then + if ! resolve_openclaw_bin &> /dev/null; then + ui_warn "npm install openclaw@latest failed; retrying openclaw@next" + cleanup_npm_openclaw_paths + install_openclaw_npm "openclaw@next" + fi + fi + + ensure_openclaw_bin_link || true + + ui_success "OpenClaw installed" +} + +# Run doctor for migrations (safe, non-interactive) +run_doctor() { + ui_info "Running doctor to migrate settings" + local claw="${OPENCLAW_BIN:-}" + if [[ -z "$claw" ]]; then + claw="$(resolve_openclaw_bin || true)" + fi + if [[ -z "$claw" ]]; then + ui_info "Skipping doctor (openclaw not on PATH yet)" + warn_openclaw_not_found + return 0 + fi + run_quiet_step "Running doctor" "$claw" doctor --non-interactive || true + ui_success "Doctor complete" +} + +maybe_open_dashboard() { + local claw="${OPENCLAW_BIN:-}" + if [[ -z "$claw" ]]; then + claw="$(resolve_openclaw_bin || true)" + fi + if [[ -z "$claw" ]]; then + return 0 + fi + if ! "$claw" dashboard --help >/dev/null 2>&1; then + return 0 + fi + "$claw" dashboard || true +} + +resolve_workspace_dir() { + local profile="${OPENCLAW_PROFILE:-default}" + if [[ "${profile}" != "default" ]]; then + echo "${HOME}/.openclaw/workspace-${profile}" + else + echo "${HOME}/.openclaw/workspace" + fi +} + +run_bootstrap_onboarding_if_needed() { + if [[ "${NO_ONBOARD}" == "1" ]]; then + return + fi + + local config_path="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}" + if [[ -f "${config_path}" || -f "$HOME/.clawdbot/clawdbot.json" || -f "$HOME/.moltbot/moltbot.json" || -f "$HOME/.moldbot/moldbot.json" ]]; then + return + fi + + local workspace + workspace="$(resolve_workspace_dir)" + local bootstrap="${workspace}/BOOTSTRAP.md" + + if [[ ! -f "${bootstrap}" ]]; then + return + fi + + if [[ ! -r /dev/tty || ! -w /dev/tty ]]; then + ui_info "BOOTSTRAP.md found but no TTY; run openclaw onboard to finish setup" + return + fi + + ui_info "BOOTSTRAP.md found; starting onboarding" + local claw="${OPENCLAW_BIN:-}" + if [[ -z "$claw" ]]; then + claw="$(resolve_openclaw_bin || true)" + fi + if [[ -z "$claw" ]]; then + ui_info "BOOTSTRAP.md found but openclaw not on PATH; skipping onboarding" + warn_openclaw_not_found + return + fi + + "$claw" onboard || { + ui_error "Onboarding failed; run openclaw onboard to retry" + ui_info "If gateway startup looks unhealthy, run: openclaw gateway status --deep" + return + } +} + +resolve_openclaw_version() { + local version="" + local claw="${OPENCLAW_BIN:-}" + if [[ -z "$claw" ]] && command -v openclaw &> /dev/null; then + claw="$(command -v openclaw)" + fi + if [[ -n "$claw" ]]; then + version=$("$claw" --version 2>/dev/null | head -n 1 | tr -d '\r') + fi + if [[ -z "$version" ]]; then + local npm_root="" + npm_root=$(npm root -g 2>/dev/null || true) + if [[ -n "$npm_root" && -f "$npm_root/openclaw/package.json" ]]; then + version=$(node -e "console.log(require('${npm_root}/openclaw/package.json').version)" 2>/dev/null || true) + fi + fi + echo "$version" +} + +is_gateway_daemon_loaded() { + local claw="$1" + if [[ -z "$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="${OPENCLAW_BIN:-}" + if [[ -z "$claw" ]]; then + claw="$(resolve_openclaw_bin || true)" + fi + if [[ -z "$claw" ]]; then + return 0 + fi + + if ! is_gateway_daemon_loaded "$claw"; then + return 0 + fi + + ui_info "Refreshing loaded gateway service" + if run_quiet_step "Refreshing gateway service" "$claw" gateway install --force; then + ui_success "Gateway service metadata refreshed" + else + ui_warn "Gateway service refresh failed; continuing" + return 0 + fi + + if run_quiet_step "Restarting gateway service" "$claw" gateway restart; then + ui_success "Gateway service restarted" + else + ui_warn "Gateway service restart failed; continuing" + return 0 + fi + + run_quiet_step "Probing gateway service" "$claw" gateway status --deep || true +} + +# Main installation flow +main() { + if [[ "$HELP" == "1" ]]; then + print_usage + return 0 + fi + + bootstrap_gum_temp || true + print_installer_banner + print_gum_status + detect_os_or_die + + local detected_checkout="" + detected_checkout="$(detect_openclaw_checkout "$PWD" || true)" + + if [[ -z "$INSTALL_METHOD" && -n "$detected_checkout" ]]; then + if ! is_promptable; then + ui_info "Found OpenClaw checkout but no TTY; defaulting to npm install" + INSTALL_METHOD="npm" + else + local selected_method="" + selected_method="$(choose_install_method_interactive "$detected_checkout" || true)" + case "$selected_method" in + git|npm) + INSTALL_METHOD="$selected_method" + ;; + *) + ui_error "no install method selected" + echo "Re-run with: --install-method git|npm (or set OPENCLAW_INSTALL_METHOD)." + exit 2 + ;; + esac + fi + fi + + if [[ -z "$INSTALL_METHOD" ]]; then + INSTALL_METHOD="npm" + fi + + if [[ "$INSTALL_METHOD" != "npm" && "$INSTALL_METHOD" != "git" ]]; then + ui_error "invalid --install-method: ${INSTALL_METHOD}" + echo "Use: --install-method npm|git" + exit 2 + fi + + show_install_plan "$detected_checkout" + + if [[ "$DRY_RUN" == "1" ]]; then + ui_success "Dry run complete (no changes made)" + return 0 + fi + + # Check for existing installation + local is_upgrade=false + if check_existing_openclaw; then + is_upgrade=true + fi + local should_open_dashboard=false + local skip_onboard=false + + ui_stage "Preparing environment" + + # Step 1: Homebrew (macOS only) + install_homebrew + + # Step 2: Node.js + if ! check_node; then + install_node + fi + ensure_supported_node_on_path || true + if ! has_supported_node; then + ui_error "Node.js v22+ is required but could not be activated on PATH" + echo "Detected node: $(command -v node 2>/dev/null || echo '(not found)')" + echo "Current version: $(node -v 2>/dev/null || echo 'unknown')" + echo "Install Node.js 22+ manually: https://nodejs.org" + exit 1 + fi + + ui_stage "Installing OpenClaw" + + local final_git_dir="" + if [[ "$INSTALL_METHOD" == "git" ]]; then + # Clean up npm global install if switching to git + if npm list -g openclaw &>/dev/null; then + ui_info "Removing npm global install (switching to git)" + npm uninstall -g openclaw 2>/dev/null || true + ui_success "npm global install removed" + fi + + local repo_dir="$GIT_DIR" + if [[ -n "$detected_checkout" ]]; then + repo_dir="$detected_checkout" + fi + final_git_dir="$repo_dir" + install_openclaw_from_git "$repo_dir" + else + # Clean up git wrapper if switching to npm + if [[ -x "$HOME/.local/bin/openclaw" ]]; then + ui_info "Removing git wrapper (switching to npm)" + rm -f "$HOME/.local/bin/openclaw" + ui_success "git wrapper removed" + fi + + # Step 3: Git (required for npm installs that may fetch from git or apply patches) + if ! check_git; then + install_git + fi + + # Step 4: npm permissions (Linux) + fix_npm_permissions + + # Step 5: OpenClaw + install_openclaw + install_openclaw_compat_shim || true + fi + + ui_stage "Finalizing setup" + + OPENCLAW_BIN="$(resolve_openclaw_bin || true)" + + # PATH warning: installs can succeed while the user's login shell still lacks npm's global bin dir. + local npm_bin="" + npm_bin="$(npm_global_bin_dir || true)" + if [[ "$INSTALL_METHOD" == "npm" ]]; then + warn_shell_path_missing_dir "$npm_bin" "npm global bin dir" + fi + if [[ "$INSTALL_METHOD" == "git" ]]; then + if [[ -x "$HOME/.local/bin/openclaw" ]]; then + warn_shell_path_missing_dir "$HOME/.local/bin" "user-local bin dir (~/.local/bin)" + fi + fi + + refresh_gateway_service_if_loaded + + # Step 6: Run doctor for migrations on upgrades and git installs + local run_doctor_after=false + if [[ "$is_upgrade" == "true" || "$INSTALL_METHOD" == "git" ]]; then + run_doctor_after=true + fi + if [[ "$run_doctor_after" == "true" ]]; then + run_doctor + should_open_dashboard=true + fi + + # Step 7: If BOOTSTRAP.md is still present in the workspace, resume onboarding + run_bootstrap_onboarding_if_needed + + local installed_version + installed_version=$(resolve_openclaw_version) + + echo "" + if [[ -n "$installed_version" ]]; then + ui_celebrate "🦞 OpenClaw installed successfully (${installed_version})!" + else + ui_celebrate "🦞 OpenClaw installed successfully!" + fi + if [[ "$is_upgrade" == "true" ]]; then + local update_messages=( + "Leveled up! New skills unlocked. You're welcome." + "Fresh code, same lobster. Miss me?" + "Back and better. Did you even notice I was gone?" + "Update complete. I learned some new tricks while I was out." + "Upgraded! Now with 23% more sass." + "I've evolved. Try to keep up. 🦞" + "New version, who dis? Oh right, still me but shinier." + "Patched, polished, and ready to pinch. Let's go." + "The lobster has molted. Harder shell, sharper claws." + "Update done! Check the changelog or just trust me, it's good." + "Reborn from the boiling waters of npm. Stronger now." + "I went away and came back smarter. You should try it sometime." + "Update complete. The bugs feared me, so they left." + "New version installed. Old version sends its regards." + "Firmware fresh. Brain wrinkles: increased." + "I've seen things you wouldn't believe. Anyway, I'm updated." + "Back online. The changelog is long but our friendship is longer." + "Upgraded! Peter fixed stuff. Blame him if it breaks." + "Molting complete. Please don't look at my soft shell phase." + "Version bump! Same chaos energy, fewer crashes (probably)." + ) + local update_message + update_message="${update_messages[RANDOM % ${#update_messages[@]}]}" + echo -e "${MUTED}${update_message}${NC}" + else + local completion_messages=( + "Ahh nice, I like it here. Got any snacks? " + "Home sweet home. Don't worry, I won't rearrange the furniture." + "I'm in. Let's cause some responsible chaos." + "Installation complete. Your productivity is about to get weird." + "Settled in. Time to automate your life whether you're ready or not." + "Cozy. I've already read your calendar. We need to talk." + "Finally unpacked. Now point me at your problems." + "cracks claws Alright, what are we building?" + "The lobster has landed. Your terminal will never be the same." + "All done! I promise to only judge your code a little bit." + ) + local completion_message + completion_message="${completion_messages[RANDOM % ${#completion_messages[@]}]}" + echo -e "${MUTED}${completion_message}${NC}" + fi + echo "" + + if [[ "$INSTALL_METHOD" == "git" && -n "$final_git_dir" ]]; then + ui_section "Source install details" + ui_kv "Checkout" "$final_git_dir" + ui_kv "Wrapper" "$HOME/.local/bin/openclaw" + ui_kv "Update command" "openclaw update --restart" + ui_kv "Switch to npm" "curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method npm" + elif [[ "$is_upgrade" == "true" ]]; then + ui_info "Upgrade complete" + if [[ -r /dev/tty && -w /dev/tty ]]; then + local claw="${OPENCLAW_BIN:-}" + if [[ -z "$claw" ]]; then + claw="$(resolve_openclaw_bin || true)" + fi + if [[ -z "$claw" ]]; then + ui_info "Skipping doctor (openclaw not on PATH yet)" + warn_openclaw_not_found + return 0 + fi + local -a doctor_args=() + if [[ "$NO_ONBOARD" == "1" ]]; then + if "$claw" doctor --help 2>/dev/null | grep -q -- "--non-interactive"; then + doctor_args+=("--non-interactive") + fi + fi + ui_info "Running openclaw doctor" + local doctor_ok=0 + if (( ${#doctor_args[@]} )); then + OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" doctor "${doctor_args[@]}" /dev/null; then + local claw="${OPENCLAW_BIN:-}" + if [[ -z "$claw" ]]; then + claw="$(resolve_openclaw_bin || true)" + fi + if [[ -n "$claw" ]] && is_gateway_daemon_loaded "$claw"; then + if [[ "$DRY_RUN" == "1" ]]; then + ui_info "Gateway daemon detected; would restart (openclaw daemon restart)" + else + ui_info "Gateway daemon detected; restarting" + if OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" daemon restart >/dev/null 2>&1; then + ui_success "Gateway restarted" + else + ui_warn "Gateway restart failed; try: openclaw daemon restart" + fi + fi + fi + fi + + if [[ "$should_open_dashboard" == "true" ]]; then + maybe_open_dashboard + fi + + show_footer_links +} + +if [[ "${OPENCLAW_INSTALL_SH_NO_RUN:-0}" != "1" ]]; then + parse_args "$@" + configure_verbose + main +fi diff --git a/openClaw/scripts/win-install.ps1 b/openClaw/scripts/win-install.ps1 new file mode 100644 index 0000000..2289c9d --- /dev/null +++ b/openClaw/scripts/win-install.ps1 @@ -0,0 +1,670 @@ +# OpenClaw Installer for Windows +# Usage: iwr -useb https://openclaw.ai/install.ps1 | iex +# & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -Tag beta -NoOnboard -DryRun + +param( + [string]$Tag = "latest", + [ValidateSet("npm", "git")] + [string]$InstallMethod = "npm", + [string]$GitDir, + [switch]$NoOnboard, + [switch]$NoGitUpdate, + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" + +Write-Host "" +Write-Host " OpenClaw Installer" -ForegroundColor Cyan +Write-Host "" + +# Check if running in PowerShell +if ($PSVersionTable.PSVersion.Major -lt 5) { + Write-Host "Error: PowerShell 5+ required" -ForegroundColor Red + exit 1 +} + +Write-Host "[OK] Windows detected" -ForegroundColor Green + +if (-not $PSBoundParameters.ContainsKey("InstallMethod")) { + if (-not [string]::IsNullOrWhiteSpace($env:OPENCLAW_INSTALL_METHOD)) { + $InstallMethod = $env:OPENCLAW_INSTALL_METHOD + } +} +if (-not $PSBoundParameters.ContainsKey("GitDir")) { + if (-not [string]::IsNullOrWhiteSpace($env:OPENCLAW_GIT_DIR)) { + $GitDir = $env:OPENCLAW_GIT_DIR + } +} +if (-not $PSBoundParameters.ContainsKey("NoOnboard")) { + if ($env:OPENCLAW_NO_ONBOARD -eq "1") { + $NoOnboard = $true + } +} +if (-not $PSBoundParameters.ContainsKey("NoGitUpdate")) { + if ($env:OPENCLAW_GIT_UPDATE -eq "0") { + $NoGitUpdate = $true + } +} +if (-not $PSBoundParameters.ContainsKey("DryRun")) { + if ($env:OPENCLAW_DRY_RUN -eq "1") { + $DryRun = $true + } +} + +if ([string]::IsNullOrWhiteSpace($GitDir)) { + $userHome = [Environment]::GetFolderPath("UserProfile") + $GitDir = (Join-Path $userHome "openclaw") +} + +# Check for Node.js +function Check-Node { + try { + $nodeVersion = (node -v 2>$null) + if ($nodeVersion) { + $version = [int]($nodeVersion -replace 'v(\d+)\..*', '$1') + if ($version -ge 22) { + Write-Host "[OK] Node.js $nodeVersion found" -ForegroundColor Green + return $true + } else { + Write-Host "[!] Node.js $nodeVersion found, but v22+ required" -ForegroundColor Yellow + return $false + } + } + } catch { + Write-Host "[!] Node.js not found" -ForegroundColor Yellow + return $false + } + return $false +} + +# Install Node.js +function Install-Node { + Write-Host "[*] Installing Node.js..." -ForegroundColor Yellow + + # Try winget first (Windows 11 / Windows 10 with App Installer) + if (Get-Command winget -ErrorAction SilentlyContinue) { + Write-Host " Using winget..." -ForegroundColor Gray + winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + Write-Host "[OK] Node.js installed via winget" -ForegroundColor Green + return + } + + # Try Chocolatey + if (Get-Command choco -ErrorAction SilentlyContinue) { + Write-Host " Using Chocolatey..." -ForegroundColor Gray + choco install nodejs-lts -y + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + Write-Host "[OK] Node.js installed via Chocolatey" -ForegroundColor Green + return + } + + # Try Scoop + if (Get-Command scoop -ErrorAction SilentlyContinue) { + Write-Host " Using Scoop..." -ForegroundColor Gray + scoop install nodejs-lts + Write-Host "[OK] Node.js installed via Scoop" -ForegroundColor Green + return + } + + # Manual download fallback + Write-Host "" + Write-Host "Error: Could not find a package manager (winget, choco, or scoop)" -ForegroundColor Red + Write-Host "" + Write-Host "Please install Node.js 22+ manually:" -ForegroundColor Yellow + Write-Host " https://nodejs.org/en/download/" -ForegroundColor Cyan + Write-Host "" + Write-Host "Or install winget (App Installer) from the Microsoft Store." -ForegroundColor Gray + exit 1 +} + +# Check for existing OpenClaw installation +function Check-ExistingOpenClaw { + if (Get-OpenClawCommandPath) { + Write-Host "[*] Existing OpenClaw installation detected" -ForegroundColor Yellow + return $true + } + return $false +} + +function Check-Git { + try { + $null = Get-Command git -ErrorAction Stop + return $true + } catch { + return $false + } +} + +function Install-Git { + Write-Host "[*] Installing Git..." -ForegroundColor Yellow + + # 优先从国内镜像下载(无梯子也能快速下载) + Write-Host " Downloading from Chinese mirror..." -ForegroundColor Gray + $gitInstallerUrl = "https://registry.npmmirror.com/-/binary/git-for-windows/v2.47.0.windows.1/Git-2.47.0-64-bit.exe" + $installerPath = Join-Path $env:TEMP "Git-installer.exe" + + try { + Invoke-WebRequest -Uri $gitInstallerUrl -OutFile $installerPath -UseBasicParsing + Write-Host " Download complete, running installer..." -ForegroundColor Gray + Start-Process -FilePath $installerPath -ArgumentList "/VERYSILENT", "/NORESTART", "/NOCANCEL", "/SP-", "/COMPONENTS=icons,ext\shellhere,assoc,assoc_sh" -Wait + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + Write-Host "[OK] Git installed via Chinese mirror" -ForegroundColor Green + return + } catch { + Write-Host " [!] Mirror download failed, falling back to package managers..." -ForegroundColor Yellow + } + + # Try winget (Windows 11 / Windows 10 with App Installer) + if (Get-Command winget -ErrorAction SilentlyContinue) { + Write-Host " Using winget..." -ForegroundColor Gray + winget install Git.Git --accept-package-agreements --accept-source-agreements + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + Write-Host "[OK] Git installed via winget" -ForegroundColor Green + return + } + + # Try Chocolatey + if (Get-Command choco -ErrorAction SilentlyContinue) { + Write-Host " Using Chocolatey..." -ForegroundColor Gray + choco install git -y + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + Write-Host "[OK] Git installed via Chocolatey" -ForegroundColor Green + return + } + + # Try Scoop + if (Get-Command scoop -ErrorAction SilentlyContinue) { + Write-Host " Using Scoop..." -ForegroundColor Gray + scoop install git + Write-Host "[OK] Git installed via Scoop" -ForegroundColor Green + return + } + + Write-Host "" + Write-Host "Error: Git installation failed" -ForegroundColor Red + Write-Host "" + Write-Host "Please install Git for Windows manually:" -ForegroundColor Yellow + Write-Host " https://registry.npmmirror.com/-/binary/git-for-windows/" -ForegroundColor Cyan + Write-Host "" + exit 1 +} + +function Require-Git { + if (Check-Git) { return } + Install-Git + + # Verify installation + if (-not (Check-Git)) { + Write-Host "" + Write-Host "Error: Git installation may require a terminal restart" -ForegroundColor Red + Write-Host "Please close this terminal, open a new one, and run this installer again." -ForegroundColor Yellow + exit 1 + } +} + +function Get-OpenClawCommandPath { + $openclawCmd = Get-Command openclaw.cmd -ErrorAction SilentlyContinue + if ($openclawCmd -and $openclawCmd.Source) { + return $openclawCmd.Source + } + + $openclaw = Get-Command openclaw -ErrorAction SilentlyContinue + if ($openclaw -and $openclaw.Source) { + return $openclaw.Source + } + + return $null +} + +function Invoke-OpenClawCommand { + param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Arguments + ) + + $commandPath = Get-OpenClawCommandPath + if (-not $commandPath) { + throw "openclaw command not found on PATH." + } + + & $commandPath @Arguments +} + +function Get-NpmGlobalBinCandidates { + param( + [string]$NpmPrefix + ) + + $candidates = @() + if (-not [string]::IsNullOrWhiteSpace($NpmPrefix)) { + $candidates += $NpmPrefix + $candidates += (Join-Path $NpmPrefix "bin") + } + if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) { + $candidates += (Join-Path $env:APPDATA "npm") + } + + return $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique +} + +function Ensure-OpenClawOnPath { + if (Get-OpenClawCommandPath) { + return $true + } + + $npmPrefix = $null + try { + $npmPrefix = (npm config get prefix 2>$null).Trim() + } catch { + $npmPrefix = $null + } + + $npmBins = Get-NpmGlobalBinCandidates -NpmPrefix $npmPrefix + foreach ($npmBin in $npmBins) { + if (-not (Test-Path (Join-Path $npmBin "openclaw.cmd"))) { + continue + } + + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if (-not ($userPath -split ";" | Where-Object { $_ -ieq $npmBin })) { + [Environment]::SetEnvironmentVariable("Path", "$userPath;$npmBin", "User") + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + Write-Host "[!] Added $npmBin to user PATH (restart terminal if command not found)" -ForegroundColor Yellow + } + return $true + } + + Write-Host "[!] openclaw is not on PATH yet." -ForegroundColor Yellow + Write-Host "Restart PowerShell or add the npm global install folder to PATH." -ForegroundColor Yellow + if ($npmBins.Count -gt 0) { + Write-Host "Expected path (one of):" -ForegroundColor Gray + foreach ($npmBin in $npmBins) { + Write-Host " $npmBin" -ForegroundColor Cyan + } + } else { + Write-Host "Hint: run \"npm config get prefix\" to find your npm global path." -ForegroundColor Gray + } + return $false +} + +function Ensure-Pnpm { + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + return + } + if (Get-Command corepack -ErrorAction SilentlyContinue) { + try { + corepack enable | Out-Null + corepack prepare pnpm@latest --activate | Out-Null + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + Write-Host "[OK] pnpm installed via corepack" -ForegroundColor Green + return + } + } catch { + # fallthrough to npm install + } + } + Write-Host "[*] Installing pnpm..." -ForegroundColor Yellow + $prevScriptShell = $env:NPM_CONFIG_SCRIPT_SHELL + $env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe" + try { + npm install -g pnpm + } finally { + $env:NPM_CONFIG_SCRIPT_SHELL = $prevScriptShell + } + Write-Host "[OK] pnpm installed" -ForegroundColor Green +} + +# Install OpenClaw +function Install-OpenClaw { + if ([string]::IsNullOrWhiteSpace($Tag)) { + $Tag = "latest" + } + Require-Git + + # 国内环境适配:自动替换GitHub SSH地址为HTTPS+镜像加速 + git config --global url."https://github.com/".insteadOf ssh://git@github.com/ + ## 暂时停用,频繁出现错误,不如直接拉github仓库 + # git config --global url."https://mirror.ghproxy.com/https://github.com/".insteadOf https://github.com/ + + # Use openclaw package for beta, openclaw for stable + $packageName = "openclaw" + if ($Tag -eq "beta" -or $Tag -match "^beta\.") { + $packageName = "openclaw" + } + Write-Host "[*] Installing OpenClaw ($packageName@$Tag)..." -ForegroundColor Yellow + $prevLogLevel = $env:NPM_CONFIG_LOGLEVEL + $prevUpdateNotifier = $env:NPM_CONFIG_UPDATE_NOTIFIER + $prevFund = $env:NPM_CONFIG_FUND + $prevAudit = $env:NPM_CONFIG_AUDIT + $prevScriptShell = $env:NPM_CONFIG_SCRIPT_SHELL + $env:NPM_CONFIG_LOGLEVEL = "error" + $env:NPM_CONFIG_UPDATE_NOTIFIER = "false" + $env:NPM_CONFIG_FUND = "false" + $env:NPM_CONFIG_AUDIT = "false" + $env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe" + try { + # $npmOutput = npm install -g "$packageName@$Tag" 2>&1 + $npmOutput = npm install -g "$packageName@$Tag" --ignore-scripts --registry=https://registry.npmmirror.com 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "[!] npm install failed" -ForegroundColor Red + if ($npmOutput -match "spawn git" -or $npmOutput -match "ENOENT.*git") { + Write-Host "Error: git is missing from PATH." -ForegroundColor Red + Write-Host "Install Git for Windows, then reopen PowerShell and retry:" -ForegroundColor Yellow + Write-Host " https://git-scm.com/download/win" -ForegroundColor Cyan + } else { + Write-Host "Re-run with verbose output to see the full error:" -ForegroundColor Yellow + Write-Host " iwr -useb https://openclaw.ai/install.ps1 | iex" -ForegroundColor Cyan + } + $npmOutput | ForEach-Object { Write-Host $_ } + exit 1 + } + } finally { + $env:NPM_CONFIG_LOGLEVEL = $prevLogLevel + $env:NPM_CONFIG_UPDATE_NOTIFIER = $prevUpdateNotifier + $env:NPM_CONFIG_FUND = $prevFund + $env:NPM_CONFIG_AUDIT = $prevAudit + $env:NPM_CONFIG_SCRIPT_SHELL = $prevScriptShell + } + Write-Host "[OK] OpenClaw installed" -ForegroundColor Green +} + +# Install OpenClaw from GitHub +function Install-OpenClawFromGit { + param( + [string]$RepoDir, + [switch]$SkipUpdate + ) + Require-Git + Ensure-Pnpm + + $repoUrl = "https://github.com/openclaw/openclaw.git" + Write-Host "[*] Installing OpenClaw from GitHub ($repoUrl)..." -ForegroundColor Yellow + + if (-not (Test-Path $RepoDir)) { + git clone $repoUrl $RepoDir + } + + if (-not $SkipUpdate) { + if (-not (git -C $RepoDir status --porcelain 2>$null)) { + git -C $RepoDir pull --rebase 2>$null + } else { + Write-Host "[!] Repo is dirty; skipping git pull" -ForegroundColor Yellow + } + } else { + Write-Host "[!] Git update disabled; skipping git pull" -ForegroundColor Yellow + } + + Remove-LegacySubmodule -RepoDir $RepoDir + + $prevPnpmScriptShell = $env:NPM_CONFIG_SCRIPT_SHELL + $env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe" + try { + pnpm -C $RepoDir install + if (-not (pnpm -C $RepoDir ui:build)) { + Write-Host "[!] UI build failed; continuing (CLI may still work)" -ForegroundColor Yellow + } + pnpm -C $RepoDir build + } finally { + $env:NPM_CONFIG_SCRIPT_SHELL = $prevPnpmScriptShell + } + + $binDir = Join-Path $env:USERPROFILE ".local\\bin" + if (-not (Test-Path $binDir)) { + New-Item -ItemType Directory -Force -Path $binDir | Out-Null + } + $cmdPath = Join-Path $binDir "openclaw.cmd" + $cmdContents = "@echo off`r`nnode ""$RepoDir\\dist\\entry.js"" %*`r`n" + Set-Content -Path $cmdPath -Value $cmdContents -NoNewline + + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if (-not ($userPath -split ";" | Where-Object { $_ -ieq $binDir })) { + [Environment]::SetEnvironmentVariable("Path", "$userPath;$binDir", "User") + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + Write-Host "[!] Added $binDir to user PATH (restart terminal if command not found)" -ForegroundColor Yellow + } + + Write-Host "[OK] OpenClaw wrapper installed to $cmdPath" -ForegroundColor Green + Write-Host "[i] This checkout uses pnpm. For deps, run: pnpm install (avoid npm install in the repo)." -ForegroundColor Gray +} + +# Run doctor for migrations (safe, non-interactive) +function Run-Doctor { + Write-Host "[*] Running doctor to migrate settings..." -ForegroundColor Yellow + try { + Invoke-OpenClawCommand doctor --non-interactive + } catch { + # Ignore errors from doctor + } + Write-Host "[OK] Migration complete" -ForegroundColor Green +} + +function Test-GatewayServiceLoaded { + try { + $statusJson = (Invoke-OpenClawCommand daemon status --json 2>$null) + if ([string]::IsNullOrWhiteSpace($statusJson)) { + return $false + } + $parsed = $statusJson | ConvertFrom-Json + if ($parsed -and $parsed.service -and $parsed.service.loaded) { + return $true + } + } catch { + return $false + } + return $false +} + +function Refresh-GatewayServiceIfLoaded { + if (-not (Get-OpenClawCommandPath)) { + return + } + if (-not (Test-GatewayServiceLoaded)) { + return + } + + Write-Host "[*] Refreshing loaded gateway service..." -ForegroundColor Yellow + try { + Invoke-OpenClawCommand gateway install --force | Out-Null + } catch { + Write-Host "[!] Gateway service refresh failed; continuing." -ForegroundColor Yellow + return + } + + try { + Invoke-OpenClawCommand gateway restart | Out-Null + Invoke-OpenClawCommand gateway status --probe --json | Out-Null + Write-Host "[OK] Gateway service refreshed" -ForegroundColor Green + } catch { + Write-Host "[!] Gateway service restart failed; continuing." -ForegroundColor Yellow + } +} + +function Get-LegacyRepoDir { + if (-not [string]::IsNullOrWhiteSpace($env:OPENCLAW_GIT_DIR)) { + return $env:OPENCLAW_GIT_DIR + } + $userHome = [Environment]::GetFolderPath("UserProfile") + return (Join-Path $userHome "openclaw") +} + +function Remove-LegacySubmodule { + param( + [string]$RepoDir + ) + if ([string]::IsNullOrWhiteSpace($RepoDir)) { + $RepoDir = Get-LegacyRepoDir + } + $legacyDir = Join-Path $RepoDir "Peekaboo" + if (Test-Path $legacyDir) { + Write-Host "[!] Removing legacy submodule checkout: $legacyDir" -ForegroundColor Yellow + Remove-Item -Recurse -Force $legacyDir + } +} + +# Main installation flow +function Main { + if ($InstallMethod -ne "npm" -and $InstallMethod -ne "git") { + Write-Host "Error: invalid -InstallMethod (use npm or git)." -ForegroundColor Red + exit 2 + } + + if ($DryRun) { + Write-Host "[OK] Dry run" -ForegroundColor Green + Write-Host "[OK] Install method: $InstallMethod" -ForegroundColor Green + if ($InstallMethod -eq "git") { + Write-Host "[OK] Git dir: $GitDir" -ForegroundColor Green + if ($NoGitUpdate) { + Write-Host "[OK] Git update: disabled" -ForegroundColor Green + } else { + Write-Host "[OK] Git update: enabled" -ForegroundColor Green + } + } + if ($NoOnboard) { + Write-Host "[OK] Onboard: skipped" -ForegroundColor Green + } + return + } + + Remove-LegacySubmodule -RepoDir $RepoDir + + # Check for existing installation + $isUpgrade = Check-ExistingOpenClaw + + # Step 1: Node.js + if (-not (Check-Node)) { + Install-Node + + # Verify installation + if (-not (Check-Node)) { + Write-Host "" + Write-Host "Error: Node.js installation may require a terminal restart" -ForegroundColor Red + Write-Host "Please close this terminal, open a new one, and run this installer again." -ForegroundColor Yellow + exit 1 + } + } + + $finalGitDir = $null + + # Step 2: OpenClaw + if ($InstallMethod -eq "git") { + $finalGitDir = $GitDir + Install-OpenClawFromGit -RepoDir $GitDir -SkipUpdate:$NoGitUpdate + } else { + Install-OpenClaw + } + + if (-not (Ensure-OpenClawOnPath)) { + Write-Host "Install completed, but OpenClaw is not on PATH yet." -ForegroundColor Yellow + Write-Host "Open a new terminal, then run: openclaw doctor" -ForegroundColor Cyan + return + } + + Refresh-GatewayServiceIfLoaded + + # Step 3: Run doctor for migrations if upgrading or git install + if ($isUpgrade -or $InstallMethod -eq "git") { + Run-Doctor + } + + $installedVersion = $null + try { + $installedVersion = (Invoke-OpenClawCommand --version 2>$null).Trim() + } catch { + $installedVersion = $null + } + if (-not $installedVersion) { + try { + $npmList = npm list -g --depth 0 --json 2>$null | ConvertFrom-Json + if ($npmList -and $npmList.dependencies -and $npmList.dependencies.openclaw -and $npmList.dependencies.openclaw.version) { + $installedVersion = $npmList.dependencies.openclaw.version + } + } catch { + $installedVersion = $null + } + } + + Write-Host "" + if ($installedVersion) { + Write-Host "OpenClaw installed successfully ($installedVersion)!" -ForegroundColor Green + } else { + Write-Host "OpenClaw installed successfully!" -ForegroundColor Green + } + Write-Host "" + if ($isUpgrade) { + $updateMessages = @( + "Leveled up! New skills unlocked. You're welcome.", + "Fresh code, same lobster. Miss me?", + "Back and better. Did you even notice I was gone?", + "Update complete. I learned some new tricks while I was out.", + "Upgraded! Now with 23% more sass.", + "I've evolved. Try to keep up.", + "New version, who dis? Oh right, still me but shinier.", + "Patched, polished, and ready to pinch. Let's go.", + "The lobster has molted. Harder shell, sharper claws.", + "Update done! Check the changelog or just trust me, it's good.", + "Reborn from the boiling waters of npm. Stronger now.", + "I went away and came back smarter. You should try it sometime.", + "Update complete. The bugs feared me, so they left.", + "New version installed. Old version sends its regards.", + "Firmware fresh. Brain wrinkles: increased.", + "I've seen things you wouldn't believe. Anyway, I'm updated.", + "Back online. The changelog is long but our friendship is longer.", + "Upgraded! Peter fixed stuff. Blame him if it breaks.", + "Molting complete. Please don't look at my soft shell phase.", + "Version bump! Same chaos energy, fewer crashes (probably)." + ) + Write-Host (Get-Random -InputObject $updateMessages) -ForegroundColor Gray + Write-Host "" + } else { + $completionMessages = @( + "Ahh nice, I like it here. Got any snacks? ", + "Home sweet home. Don't worry, I won't rearrange the furniture.", + "I'm in. Let's cause some responsible chaos.", + "Installation complete. Your productivity is about to get weird.", + "Settled in. Time to automate your life whether you're ready or not.", + "Cozy. I've already read your calendar. We need to talk.", + "Finally unpacked. Now point me at your problems.", + "cracks claws Alright, what are we building?", + "The lobster has landed. Your terminal will never be the same.", + "All done! I promise to only judge your code a little bit." + ) + Write-Host (Get-Random -InputObject $completionMessages) -ForegroundColor Gray + Write-Host "" + } + + if ($InstallMethod -eq "git") { + Write-Host "Source checkout: $finalGitDir" -ForegroundColor Cyan + Write-Host "Wrapper: $env:USERPROFILE\\.local\\bin\\openclaw.cmd" -ForegroundColor Cyan + Write-Host "" + } + + if ($isUpgrade) { + Write-Host "Upgrade complete. Run " -NoNewline + Write-Host "openclaw doctor" -ForegroundColor Cyan -NoNewline + Write-Host " to check for additional migrations." + } else { + if ($NoOnboard) { + Write-Host "Skipping onboard (requested). Run " -NoNewline + Write-Host "openclaw onboard" -ForegroundColor Cyan -NoNewline + Write-Host " later." + } else { + Write-Host "Starting setup..." -ForegroundColor Cyan + Write-Host "" + Invoke-OpenClawCommand onboard + } + } +} + +Main