2386 lines
74 KiB
Bash
2386 lines
74 KiB
Bash
|
|
#!/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"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
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_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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 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
|
|||
|
|
|
|||
|
|
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
|