mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
2461 lines
76 KiB
Bash
Executable File
2461 lines
76 KiB
Bash
Executable File
#!/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."
|
||
NODE_MIN_MAJOR=22
|
||
NODE_MIN_MINOR=12
|
||
NODE_MIN_VERSION="${NODE_MIN_MAJOR}.${NODE_MIN_MINOR}"
|
||
|
||
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"
|
||
}
|
||
|
||
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: iwr -useb 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"
|
||
}
|
||
|
||
# Detect Arch-based distributions (Arch Linux, Manjaro, EndeavourOS, etc.)
|
||
is_arch_linux() {
|
||
if [[ -f /etc/os-release ]]; then
|
||
local os_id
|
||
os_id="$(grep -E '^ID=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true)"
|
||
case "$os_id" in
|
||
arch|manjaro|endeavouros|arcolinux|garuda|archarm|cachyos|archcraft)
|
||
return 0
|
||
;;
|
||
esac
|
||
# Also check ID_LIKE for Arch derivatives
|
||
local os_id_like
|
||
os_id_like="$(grep -E '^ID_LIKE=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true)"
|
||
if [[ "$os_id_like" == *arch* ]]; then
|
||
return 0
|
||
fi
|
||
fi
|
||
# Fallback: check for pacman
|
||
if command -v pacman &> /dev/null; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
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 pacman &> /dev/null || is_arch_linux; then
|
||
if is_root; then
|
||
run_quiet_step "Installing build tools" pacman -Sy --noconfirm base-devel python make cmake gcc
|
||
else
|
||
run_quiet_step "Installing build tools" sudo pacman -Sy --noconfirm base-devel python make cmake gcc
|
||
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
|
||
|
||
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
|
||
if command -v brew >/dev/null 2>&1; then
|
||
run_quiet_step "Installing cmake" brew 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=""
|
||
PNPM_CMD=()
|
||
HELP=0
|
||
|
||
print_usage() {
|
||
cat <<EOF
|
||
OpenClaw installer (macOS + Linux)
|
||
|
||
Usage:
|
||
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- [options]
|
||
|
||
Options:
|
||
--install-method, --method npm|git Install via npm (default) or from a git checkout
|
||
--npm Shortcut for --install-method npm
|
||
--git, --github Shortcut for --install-method git
|
||
--version <version|dist-tag> npm install: version (default: latest)
|
||
--beta Use beta if available, else latest
|
||
--git-dir, --dir <path> 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|<semver>
|
||
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 <<EOF
|
||
${WARN}→${NC} Detected a OpenClaw source checkout in: ${INFO}${detected_checkout}${NC}
|
||
Choose install method:
|
||
1) Update this checkout (git) and use it
|
||
2) Install global via npm (migrate away from git)
|
||
Enter 1 or 2:
|
||
EOF
|
||
)" || true)"
|
||
|
||
case "$choice" in
|
||
1)
|
||
echo "git"
|
||
return 0
|
||
;;
|
||
2)
|
||
echo "npm"
|
||
return 0
|
||
;;
|
||
esac
|
||
|
||
return 1
|
||
}
|
||
|
||
detect_openclaw_checkout() {
|
||
local dir="$1"
|
||
if [[ ! -f "$dir/package.json" ]]; then
|
||
return 1
|
||
fi
|
||
if [[ ! -f "$dir/pnpm-workspace.yaml" ]]; then
|
||
return 1
|
||
fi
|
||
if ! grep -q '"name"[[:space:]]*:[[:space:]]*"openclaw"' "$dir/package.json" 2>/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() {
|
||
if [[ "$OS" == "macos" ]]; then
|
||
if ! command -v brew &> /dev/null; 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 [[ -f "/opt/homebrew/bin/brew" ]]; then
|
||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||
elif [[ -f "/usr/local/bin/brew" ]]; then
|
||
eval "$(/usr/local/bin/brew shellenv)"
|
||
fi
|
||
ui_success "Homebrew installed"
|
||
else
|
||
ui_success "Homebrew already installed"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Check Node.js version
|
||
parse_node_version_components() {
|
||
if ! command -v node &> /dev/null; then
|
||
return 1
|
||
fi
|
||
local version major minor
|
||
version="$(node -v 2>/dev/null || true)"
|
||
major="${version#v}"
|
||
major="${major%%.*}"
|
||
minor="${version#v}"
|
||
minor="${minor#*.}"
|
||
minor="${minor%%.*}"
|
||
|
||
if [[ ! "$major" =~ ^[0-9]+$ ]]; then
|
||
return 1
|
||
fi
|
||
if [[ ! "$minor" =~ ^[0-9]+$ ]]; then
|
||
return 1
|
||
fi
|
||
echo "${major} ${minor}"
|
||
return 0
|
||
}
|
||
|
||
node_major_version() {
|
||
local version_components major minor
|
||
version_components="$(parse_node_version_components || true)"
|
||
read -r major minor <<< "$version_components"
|
||
if [[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ ]]; then
|
||
echo "$major"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
node_is_at_least_required() {
|
||
local version_components major minor
|
||
version_components="$(parse_node_version_components || true)"
|
||
read -r major minor <<< "$version_components"
|
||
if [[ ! "$major" =~ ^[0-9]+$ || ! "$minor" =~ ^[0-9]+$ ]]; then
|
||
return 1
|
||
fi
|
||
if [[ "$major" -gt "$NODE_MIN_MAJOR" ]]; then
|
||
return 0
|
||
fi
|
||
if [[ "$major" -eq "$NODE_MIN_MAJOR" && "$minor" -ge "$NODE_MIN_MINOR" ]]; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
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_node_prefix=""
|
||
if command -v brew &> /dev/null; then
|
||
brew_node_prefix="$(brew --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
|
||
}
|
||
|
||
ensure_node22_active_shell() {
|
||
if node_is_at_least_required; 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 "Active Node.js must be v${NODE_MIN_VERSION}+ but this shell is using ${active_version} (${active_path})"
|
||
print_active_node_paths || true
|
||
|
||
local nvm_detected=0
|
||
if [[ -n "${NVM_DIR:-}" || "$active_path" == *"/.nvm/"* ]]; then
|
||
nvm_detected=1
|
||
fi
|
||
if command -v nvm >/dev/null 2>&1; then
|
||
nvm_detected=1
|
||
fi
|
||
|
||
if [[ "$nvm_detected" -eq 1 ]]; then
|
||
echo "nvm appears to be managing Node for this shell."
|
||
echo "Run:"
|
||
echo " nvm install 22"
|
||
echo " nvm use 22"
|
||
echo " nvm alias default 22"
|
||
echo "Then open a new shell and rerun:"
|
||
echo " curl -fsSL https://openclaw.ai/install.sh | bash"
|
||
else
|
||
echo "Install/select Node.js 22+ and ensure it 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 node_is_at_least_required; 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 v${NODE_MIN_VERSION}+"
|
||
else
|
||
ui_info "Node.js found but version could not be parsed; reinstalling v${NODE_MIN_VERSION}+"
|
||
fi
|
||
return 1
|
||
fi
|
||
else
|
||
ui_info "Node.js not found, installing it now"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 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
|
||
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
|
||
|
||
# Arch-based distros: use pacman with official repos
|
||
if command -v pacman &> /dev/null || is_arch_linux; then
|
||
ui_info "Installing Node.js via pacman (Arch-based distribution detected)"
|
||
if is_root; then
|
||
run_quiet_step "Installing Node.js" pacman -Sy --noconfirm nodejs npm
|
||
else
|
||
run_quiet_step "Installing Node.js" sudo pacman -Sy --noconfirm nodejs npm
|
||
fi
|
||
ui_success "Node.js v22 installed"
|
||
print_active_node_paths || true
|
||
return 0
|
||
fi
|
||
|
||
ui_info "Installing Node.js via NodeSource"
|
||
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
|
||
}
|
||
|
||
# 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 pacman &> /dev/null || is_arch_linux; then
|
||
if is_root; then
|
||
run_quiet_step "Installing Git" pacman -Sy --noconfirm git
|
||
else
|
||
run_quiet_step "Installing Git" sudo pacman -Sy --noconfirm 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"
|
||
|
||
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" <<EOF
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
exec node "${repo_dir}/dist/entry.js" "\$@"
|
||
EOF
|
||
chmod +x "$HOME/.local/bin/openclaw"
|
||
ui_success "OpenClaw wrapper installed to \$HOME/.local/bin/openclaw"
|
||
ui_info "This checkout uses pnpm — run pnpm install (or corepack pnpm install) for deps"
|
||
}
|
||
|
||
# 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"
|
||
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 --probe --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
|
||
if ! ensure_node22_active_shell; then
|
||
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
|
||
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/tty && doctor_ok=1
|
||
else
|
||
OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" doctor </dev/tty && doctor_ok=1
|
||
fi
|
||
if (( doctor_ok )); then
|
||
ui_info "Updating plugins"
|
||
OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" plugins update --all || true
|
||
else
|
||
ui_warn "Doctor failed; skipping plugin updates"
|
||
fi
|
||
else
|
||
ui_info "No TTY; run openclaw doctor and openclaw plugins update --all manually"
|
||
fi
|
||
else
|
||
if [[ "$NO_ONBOARD" == "1" || "$skip_onboard" == "true" ]]; then
|
||
ui_info "Skipping onboard (requested); run openclaw onboard later"
|
||
else
|
||
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
|
||
ui_info "Config already present; running doctor"
|
||
run_doctor
|
||
should_open_dashboard=true
|
||
ui_info "Config already present; skipping onboarding"
|
||
skip_onboard=true
|
||
fi
|
||
ui_info "Starting setup"
|
||
echo ""
|
||
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 onboarding (openclaw not on PATH yet)"
|
||
warn_openclaw_not_found
|
||
return 0
|
||
fi
|
||
exec </dev/tty
|
||
exec "$claw" onboard
|
||
fi
|
||
ui_info "No TTY; run openclaw onboard to finish setup"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
if command -v openclaw &> /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
|