#!/usr/bin/env bash set -euo pipefail info() { echo "==> $*" } warn() { echo "warning: $*" >&2 } error() { echo "error: $*" >&2 } usage() { cat <<'USAGE' ZeroClaw one-click bootstrap Usage: ./bootstrap.sh [options] Modes: Default mode installs/builds ZeroClaw only (requires existing Rust toolchain). Optional bootstrap mode can also install system dependencies and Rust. Options: --docker Run bootstrap in Docker and launch onboarding inside the container --install-system-deps Install build dependencies (Linux/macOS) --install-rust Install Rust via rustup if missing --prefer-prebuilt Try latest release binary first; fallback to source build on miss --prebuilt-only Install only from latest release binary (no source build fallback) --force-source-build Disable prebuilt flow and always build from source --onboard Run onboarding after install --interactive-onboard Run interactive onboarding (implies --onboard) --api-key API key for non-interactive onboarding --provider Provider for non-interactive onboarding (default: openrouter) --model Model for non-interactive onboarding (optional) --skip-build Skip `cargo build --release --locked` --skip-install Skip `cargo install --path . --force --locked` -h, --help Show help Examples: ./bootstrap.sh ./bootstrap.sh --docker ./bootstrap.sh --install-system-deps --install-rust ./bootstrap.sh --prefer-prebuilt ./bootstrap.sh --prebuilt-only ./bootstrap.sh --onboard --api-key "sk-..." --provider openrouter [--model "openrouter/auto"] ./bootstrap.sh --interactive-onboard # Remote one-liner curl -fsSL https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/main/scripts/bootstrap.sh | bash Environment: ZEROCLAW_DOCKER_DATA_DIR Host path for Docker config/workspace persistence ZEROCLAW_DOCKER_IMAGE Docker image tag to build/run (default: zeroclaw-bootstrap:local) ZEROCLAW_API_KEY Used when --api-key is not provided ZEROCLAW_PROVIDER Used when --provider is not provided (default: openrouter) ZEROCLAW_MODEL Used when --model is not provided ZEROCLAW_BOOTSTRAP_MIN_RAM_MB Minimum RAM threshold for source build preflight (default: 2048) ZEROCLAW_BOOTSTRAP_MIN_DISK_MB Minimum free disk threshold for source build preflight (default: 6144) USAGE } have_cmd() { command -v "$1" >/dev/null 2>&1 } get_total_memory_mb() { case "$(uname -s)" in Linux) if [[ -r /proc/meminfo ]]; then awk '/MemTotal:/ {printf "%d\n", $2 / 1024}' /proc/meminfo fi ;; Darwin) if have_cmd sysctl; then local bytes bytes="$(sysctl -n hw.memsize 2>/dev/null || true)" if [[ "$bytes" =~ ^[0-9]+$ ]]; then echo $((bytes / 1024 / 1024)) fi fi ;; esac } get_available_disk_mb() { local path="${1:-.}" local free_kb free_kb="$(df -Pk "$path" 2>/dev/null | awk 'NR==2 {print $4}')" if [[ "$free_kb" =~ ^[0-9]+$ ]]; then echo $((free_kb / 1024)) fi } detect_release_target() { local os arch os="$(uname -s)" arch="$(uname -m)" case "$os:$arch" in Linux:x86_64) echo "x86_64-unknown-linux-gnu" ;; Linux:aarch64|Linux:arm64) echo "aarch64-unknown-linux-gnu" ;; Linux:armv7l|Linux:armv6l) echo "armv7-unknown-linux-gnueabihf" ;; Darwin:x86_64) echo "x86_64-apple-darwin" ;; Darwin:arm64|Darwin:aarch64) echo "aarch64-apple-darwin" ;; *) return 1 ;; esac } should_attempt_prebuilt_for_resources() { local workspace="${1:-.}" local min_ram_mb min_disk_mb total_ram_mb free_disk_mb low_resource min_ram_mb="${ZEROCLAW_BOOTSTRAP_MIN_RAM_MB:-2048}" min_disk_mb="${ZEROCLAW_BOOTSTRAP_MIN_DISK_MB:-6144}" total_ram_mb="$(get_total_memory_mb || true)" free_disk_mb="$(get_available_disk_mb "$workspace" || true)" low_resource=false if [[ "$total_ram_mb" =~ ^[0-9]+$ && "$total_ram_mb" -lt "$min_ram_mb" ]]; then low_resource=true fi if [[ "$free_disk_mb" =~ ^[0-9]+$ && "$free_disk_mb" -lt "$min_disk_mb" ]]; then low_resource=true fi if [[ "$low_resource" == true ]]; then warn "Source build preflight indicates constrained resources." if [[ "$total_ram_mb" =~ ^[0-9]+$ ]]; then warn "Detected RAM: ${total_ram_mb}MB (recommended >= ${min_ram_mb}MB for local source builds)." else warn "Unable to detect total RAM automatically." fi if [[ "$free_disk_mb" =~ ^[0-9]+$ ]]; then warn "Detected free disk: ${free_disk_mb}MB (recommended >= ${min_disk_mb}MB)." else warn "Unable to detect free disk space automatically." fi return 0 fi return 1 } install_prebuilt_binary() { local target archive_url temp_dir archive_path extracted_bin install_dir if ! have_cmd curl; then warn "curl is required for pre-built binary installation." return 1 fi if ! have_cmd tar; then warn "tar is required for pre-built binary installation." return 1 fi target="$(detect_release_target || true)" if [[ -z "$target" ]]; then warn "No pre-built binary target mapping for $(uname -s)/$(uname -m)." return 1 fi archive_url="https://github.com/zeroclaw-labs/zeroclaw/releases/latest/download/zeroclaw-${target}.tar.gz" temp_dir="$(mktemp -d -t zeroclaw-prebuilt-XXXXXX)" archive_path="$temp_dir/zeroclaw-${target}.tar.gz" info "Attempting pre-built binary install for target: $target" if ! curl -fsSL "$archive_url" -o "$archive_path"; then warn "Could not download release asset: $archive_url" rm -rf "$temp_dir" return 1 fi if ! tar -xzf "$archive_path" -C "$temp_dir"; then warn "Failed to extract pre-built archive." rm -rf "$temp_dir" return 1 fi extracted_bin="$temp_dir/zeroclaw" if [[ ! -x "$extracted_bin" ]]; then extracted_bin="$(find "$temp_dir" -maxdepth 2 -type f -name zeroclaw -perm -u+x | head -n 1 || true)" fi if [[ -z "$extracted_bin" || ! -x "$extracted_bin" ]]; then warn "Archive did not contain an executable zeroclaw binary." rm -rf "$temp_dir" return 1 fi install_dir="$HOME/.cargo/bin" mkdir -p "$install_dir" install -m 0755 "$extracted_bin" "$install_dir/zeroclaw" rm -rf "$temp_dir" info "Installed pre-built binary to $install_dir/zeroclaw" if [[ ":$PATH:" != *":$install_dir:"* ]]; then warn "$install_dir is not in PATH for this shell." warn "Run: export PATH=\"$install_dir:\$PATH\"" fi return 0 } run_privileged() { if [[ "$(id -u)" -eq 0 ]]; then "$@" elif have_cmd sudo; then sudo "$@" else error "sudo is required to install system dependencies." return 1 fi } install_system_deps() { info "Installing system dependencies" case "$(uname -s)" in Linux) if have_cmd apt-get; then run_privileged apt-get update -qq run_privileged apt-get install -y build-essential pkg-config git curl elif have_cmd dnf; then run_privileged dnf group install -y development-tools run_privileged dnf install -y pkg-config git curl else warn "Unsupported Linux distribution. Install compiler toolchain + pkg-config + git + curl manually." fi ;; Darwin) if ! xcode-select -p >/dev/null 2>&1; then info "Installing Xcode Command Line Tools" xcode-select --install || true cat <<'MSG' Please complete the Xcode Command Line Tools installation dialog, then re-run bootstrap. MSG exit 0 fi if ! have_cmd git; then warn "git is not available. Install git (e.g., Homebrew) and re-run bootstrap." fi ;; *) warn "Unsupported OS for automatic dependency install. Continuing without changes." ;; esac } install_rust_toolchain() { if have_cmd cargo && have_cmd rustc; then info "Rust already installed: $(rustc --version)" return fi if ! have_cmd curl; then error "curl is required to install Rust via rustup." exit 1 fi info "Installing Rust via rustup" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y if [[ -f "$HOME/.cargo/env" ]]; then # shellcheck disable=SC1090 source "$HOME/.cargo/env" fi if ! have_cmd cargo; then error "Rust installation completed but cargo is still unavailable in PATH." error "Run: source \"$HOME/.cargo/env\"" exit 1 fi } ensure_docker_ready() { if ! have_cmd docker; then error "docker is not installed." cat <<'MSG' >&2 Install Docker first, then re-run with: ./bootstrap.sh --docker MSG exit 1 fi if ! docker info >/dev/null 2>&1; then error "Docker daemon is not reachable." error "Start Docker and re-run bootstrap." exit 1 fi } run_docker_bootstrap() { local docker_image docker_data_dir default_data_dir docker_image="${ZEROCLAW_DOCKER_IMAGE:-zeroclaw-bootstrap:local}" if [[ "$TEMP_CLONE" == true ]]; then default_data_dir="$HOME/.zeroclaw-docker" else default_data_dir="$WORK_DIR/.zeroclaw-docker" fi docker_data_dir="${ZEROCLAW_DOCKER_DATA_DIR:-$default_data_dir}" DOCKER_DATA_DIR="$docker_data_dir" mkdir -p "$docker_data_dir/.zeroclaw" "$docker_data_dir/workspace" if [[ "$SKIP_INSTALL" == true ]]; then warn "--skip-install has no effect with --docker." fi if [[ "$SKIP_BUILD" == false ]]; then info "Building Docker image ($docker_image)" docker build --target release -t "$docker_image" "$WORK_DIR" else info "Skipping Docker image build" fi info "Docker data directory: $docker_data_dir" local onboard_cmd=() if [[ "$INTERACTIVE_ONBOARD" == true ]]; then info "Launching interactive onboarding in container" onboard_cmd=(onboard --interactive) else if [[ -z "$API_KEY" ]]; then cat <<'MSG' ==> Onboarding requested, but API key not provided. Use either: --api-key "sk-..." or: ZEROCLAW_API_KEY="sk-..." ./bootstrap.sh --docker or run interactive: ./bootstrap.sh --docker --interactive-onboard MSG exit 1 fi if [[ -n "$MODEL" ]]; then info "Launching quick onboarding in container (provider: $PROVIDER, model: $MODEL)" else info "Launching quick onboarding in container (provider: $PROVIDER)" fi onboard_cmd=(onboard --api-key "$API_KEY" --provider "$PROVIDER") if [[ -n "$MODEL" ]]; then onboard_cmd+=(--model "$MODEL") fi fi docker run --rm -it \ --user "$(id -u):$(id -g)" \ -e HOME=/zeroclaw-data \ -e ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace \ -v "$docker_data_dir/.zeroclaw:/zeroclaw-data/.zeroclaw" \ -v "$docker_data_dir/workspace:/zeroclaw-data/workspace" \ "$docker_image" \ "${onboard_cmd[@]}" } SCRIPT_PATH="${BASH_SOURCE[0]:-$0}" SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd || pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd || pwd)" REPO_URL="https://github.com/zeroclaw-labs/zeroclaw.git" DOCKER_MODE=false INSTALL_SYSTEM_DEPS=false INSTALL_RUST=false PREFER_PREBUILT=false PREBUILT_ONLY=false FORCE_SOURCE_BUILD=false RUN_ONBOARD=false INTERACTIVE_ONBOARD=false SKIP_BUILD=false SKIP_INSTALL=false PREBUILT_INSTALLED=false API_KEY="${ZEROCLAW_API_KEY:-}" PROVIDER="${ZEROCLAW_PROVIDER:-openrouter}" MODEL="${ZEROCLAW_MODEL:-}" while [[ $# -gt 0 ]]; do case "$1" in --docker) DOCKER_MODE=true shift ;; --install-system-deps) INSTALL_SYSTEM_DEPS=true shift ;; --install-rust) INSTALL_RUST=true shift ;; --prefer-prebuilt) PREFER_PREBUILT=true shift ;; --prebuilt-only) PREBUILT_ONLY=true shift ;; --force-source-build) FORCE_SOURCE_BUILD=true shift ;; --onboard) RUN_ONBOARD=true shift ;; --interactive-onboard) RUN_ONBOARD=true INTERACTIVE_ONBOARD=true shift ;; --api-key) API_KEY="${2:-}" [[ -n "$API_KEY" ]] || { error "--api-key requires a value" exit 1 } shift 2 ;; --provider) PROVIDER="${2:-}" [[ -n "$PROVIDER" ]] || { error "--provider requires a value" exit 1 } shift 2 ;; --model) MODEL="${2:-}" [[ -n "$MODEL" ]] || { error "--model requires a value" exit 1 } shift 2 ;; --skip-build) SKIP_BUILD=true shift ;; --skip-install) SKIP_INSTALL=true shift ;; -h|--help) usage exit 0 ;; *) error "unknown option: $1" echo usage exit 1 ;; esac done if [[ "$DOCKER_MODE" == true ]]; then if [[ "$INSTALL_SYSTEM_DEPS" == true ]]; then warn "--install-system-deps is ignored with --docker." fi if [[ "$INSTALL_RUST" == true ]]; then warn "--install-rust is ignored with --docker." fi else if [[ "$INSTALL_SYSTEM_DEPS" == true ]]; then install_system_deps fi if [[ "$INSTALL_RUST" == true ]]; then install_rust_toolchain fi fi WORK_DIR="$ROOT_DIR" TEMP_CLONE=false TEMP_DIR="" cleanup() { if [[ "$TEMP_CLONE" == true && -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi } trap cleanup EXIT # Support three launch modes: # 1) ./bootstrap.sh from repo root # 2) scripts/bootstrap.sh from repo # 3) curl | bash (no local repo => temporary clone) if [[ ! -f "$WORK_DIR/Cargo.toml" ]]; then if [[ -f "$(pwd)/Cargo.toml" ]]; then WORK_DIR="$(pwd)" else if ! have_cmd git; then error "git is required when running bootstrap outside a local repository checkout." if [[ "$INSTALL_SYSTEM_DEPS" == false ]]; then error "Re-run with --install-system-deps or install git manually." fi exit 1 fi TEMP_DIR="$(mktemp -d -t zeroclaw-bootstrap-XXXXXX)" info "No local repository detected; cloning latest main branch" git clone --depth 1 "$REPO_URL" "$TEMP_DIR" WORK_DIR="$TEMP_DIR" TEMP_CLONE=true fi fi info "ZeroClaw bootstrap" echo " workspace: $WORK_DIR" cd "$WORK_DIR" if [[ "$FORCE_SOURCE_BUILD" == true ]]; then PREFER_PREBUILT=false PREBUILT_ONLY=false fi if [[ "$PREBUILT_ONLY" == true ]]; then PREFER_PREBUILT=true fi if [[ "$DOCKER_MODE" == true ]]; then ensure_docker_ready if [[ "$RUN_ONBOARD" == false ]]; then RUN_ONBOARD=true if [[ -z "$API_KEY" ]]; then INTERACTIVE_ONBOARD=true fi fi run_docker_bootstrap cat <<'DONE' ✅ Docker bootstrap complete. Your containerized ZeroClaw data is persisted under: DONE echo " $DOCKER_DATA_DIR" cat <<'DONE' Next steps: ./bootstrap.sh --docker --interactive-onboard ./bootstrap.sh --docker --api-key "sk-..." --provider openrouter DONE exit 0 fi if [[ "$FORCE_SOURCE_BUILD" == false ]]; then if [[ "$PREFER_PREBUILT" == false && "$PREBUILT_ONLY" == false ]]; then if should_attempt_prebuilt_for_resources "$WORK_DIR"; then info "Attempting pre-built binary first due to resource preflight." PREFER_PREBUILT=true fi fi if [[ "$PREFER_PREBUILT" == true ]]; then if install_prebuilt_binary; then PREBUILT_INSTALLED=true SKIP_BUILD=true SKIP_INSTALL=true elif [[ "$PREBUILT_ONLY" == true ]]; then error "Pre-built-only mode requested, but no compatible release asset is available." error "Try again later, or run with --force-source-build on a machine with enough RAM/disk." exit 1 else warn "Pre-built install unavailable; falling back to source build." fi fi fi if [[ "$PREBUILT_INSTALLED" == false && ( "$SKIP_BUILD" == false || "$SKIP_INSTALL" == false ) ]] && ! have_cmd cargo; then error "cargo is not installed." cat <<'MSG' >&2 Install Rust first: https://rustup.rs/ or re-run with: ./bootstrap.sh --install-rust MSG exit 1 fi if [[ "$SKIP_BUILD" == false ]]; then info "Building release binary" cargo build --release --locked else info "Skipping build" fi if [[ "$SKIP_INSTALL" == false ]]; then info "Installing zeroclaw to cargo bin" cargo install --path "$WORK_DIR" --force --locked else info "Skipping install" fi ZEROCLAW_BIN="" if have_cmd zeroclaw; then ZEROCLAW_BIN="zeroclaw" elif [[ -x "$HOME/.cargo/bin/zeroclaw" ]]; then ZEROCLAW_BIN="$HOME/.cargo/bin/zeroclaw" elif [[ -x "$WORK_DIR/target/release/zeroclaw" ]]; then ZEROCLAW_BIN="$WORK_DIR/target/release/zeroclaw" fi if [[ "$RUN_ONBOARD" == true ]]; then if [[ -z "$ZEROCLAW_BIN" ]]; then error "onboarding requested but zeroclaw binary is not available." error "Run without --skip-install, or ensure zeroclaw is in PATH." exit 1 fi if [[ "$INTERACTIVE_ONBOARD" == true ]]; then info "Running interactive onboarding" "$ZEROCLAW_BIN" onboard --interactive else if [[ -z "$API_KEY" ]]; then cat <<'MSG' ==> Onboarding requested, but API key not provided. Use either: --api-key "sk-..." or: ZEROCLAW_API_KEY="sk-..." ./bootstrap.sh --onboard or run interactive: ./bootstrap.sh --interactive-onboard MSG exit 1 fi if [[ -n "$MODEL" ]]; then info "Running quick onboarding (provider: $PROVIDER, model: $MODEL)" else info "Running quick onboarding (provider: $PROVIDER)" fi ONBOARD_CMD=("$ZEROCLAW_BIN" onboard --api-key "$API_KEY" --provider "$PROVIDER") if [[ -n "$MODEL" ]]; then ONBOARD_CMD+=(--model "$MODEL") fi "${ONBOARD_CMD[@]}" fi fi cat <<'DONE' ✅ Bootstrap complete. Next steps: zeroclaw status zeroclaw agent -m "Hello, ZeroClaw!" zeroclaw gateway DONE