diff --git a/.github/workflows/install-matrix.yml b/.github/workflows/install-matrix.yml new file mode 100644 index 0000000..cdcfb8d --- /dev/null +++ b/.github/workflows/install-matrix.yml @@ -0,0 +1,51 @@ +name: Install Matrix + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + inputs: + mode: + description: Matrix mode + required: true + default: dry-run + type: choice + options: + - dry-run + - full + gum: + description: Enable gum UI in matrix runs (may produce control codes in logs) + required: true + default: false + type: boolean + +jobs: + linux-install-matrix: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + image: + - ubuntu:22.04 + - ubuntu:24.04 + - debian:12 + - fedora:40 + + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4 + + - name: Run matrix (push/pr) + if: github.event_name != 'workflow_dispatch' + run: bash scripts/test-install-matrix.sh --mode dry-run --image "${{ matrix.image }}" --no-gum + + - name: Run matrix (manual) + if: github.event_name == 'workflow_dispatch' + run: | + if [[ "${{ inputs.gum }}" == "true" ]]; then + GUM_FLAG="--gum" + else + GUM_FLAG="--no-gum" + fi + bash scripts/test-install-matrix.sh --mode "${{ inputs.mode }}" --image "${{ matrix.image }}" "$GUM_FLAG" diff --git a/.gitignore b/.gitignore index 26c80b6..f017ed3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ yarn-debug.log* yarn-error.log* .vercel .env*.local + +# Local files +.local/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c639b1..b7c4628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2026-02-10 + +- Installer: modernize `install.sh` UX with staged progress, quieter command output, optional gum UI controls (`--gum`, `--no-gum`, `OPENCLAW_USE_GUM`, `CLAWDBOT_USE_GUM`), and verified-only temporary gum bootstrap (#50, thanks @sebslight). +- CI: add Linux installer matrix workflow and runner script for dry-run/full validation across distro images (#50, thanks @sebslight). + ## 2026-01-27 - Home page: keep testimonial links clickable while skipping keyboard focus (#18, thanks @wilfriedladenhauf). diff --git a/README.md b/README.md index 0090d9e..2553bad 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,11 @@ The landing page hosts installer scripts: - **macOS/Linux (CLI only, no onboarding)**: `curl -fsSL --proto '=https' --tlsv1.2 https://clawd.bot/install-cli.sh | bash` - **Windows**: `iwr -useb https://clawd.bot/install.ps1 | iex` +Installer UI controls (macOS/Linux `install.sh`): +- Pass `--gum` to force gum UI when supported, or `--no-gum` to disable gum output. +- Set `OPENCLAW_USE_GUM=auto|1|0` to control gum behavior in automation. +- Compatibility alias: `CLAWDBOT_USE_GUM` (mapped to `OPENCLAW_USE_GUM`). + These scripts: 1. Install Homebrew (macOS) or detect package managers (Windows) 2. Install Node.js 22+ if needed diff --git a/public/install.sh b/public/install.sh old mode 100644 new mode 100755 index 86c4d5a..bfea5bc --- a/public/install.sh +++ b/public/install.sh @@ -5,15 +5,15 @@ set -euo pipefail # Usage: curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash BOLD='\033[1m' -ACCENT='\033[38;2;255;90;45m' +ACCENT='\033[38;2;255;77;77m' # coral-bright #ff4d4d # shellcheck disable=SC2034 -ACCENT_BRIGHT='\033[38;2;255;122;61m' -ACCENT_DIM='\033[38;2;209;74;34m' -INFO='\033[38;2;255;138;91m' -SUCCESS='\033[38;2;47;191;113m' -WARN='\033[38;2;255;176;32m' -ERROR='\033[38;2;226;61;45m' -MUTED='\033[38;2;139;127;119m' +ACCENT_BRIGHT='\033[38;2;255;110;110m' # lighter coral +ACCENT_DIM='\033[38;2;153;27;27m' # coral-dark #991b1b +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." @@ -24,7 +24,7 @@ TMPFILES=() cleanup_tmpfiles() { local f for f in "${TMPFILES[@]:-}"; do - rm -f "$f" 2>/dev/null || true + rm -rf "$f" 2>/dev/null || true done } trap cleanup_tmpfiles EXIT @@ -46,7 +46,7 @@ detect_downloader() { DOWNLOADER="wget" return 0 fi - echo -e "${ERROR}Error: Missing downloader (curl or wget required)${NC}" + ui_error "Missing downloader (curl or wget required)" exit 1 } @@ -71,11 +71,379 @@ run_remote_bash() { /bin/bash "$tmp" } +GUM_VERSION="${OPENCLAW_GUM_VERSION:-0.17.0}" +GUM="" +GUM_STATUS="skipped" +GUM_REASON="" + +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="" + + case "$OPENCLAW_USE_GUM" in + 0|false|False|FALSE|off|OFF|no|NO) + GUM_REASON="disabled via OPENCLAW_USE_GUM" + return 1 + ;; + esac + + if ! gum_is_tty; then + GUM_REASON="not a TTY" + 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 [[ "$OPENCLAW_USE_GUM" != "1" && "$OPENCLAW_USE_GUM" != "true" && "$OPENCLAW_USE_GUM" != "TRUE" ]]; then + if [[ "$OPENCLAW_USE_GUM" != "auto" ]]; then + GUM_REASON="invalid OPENCLAW_USE_GUM value: $OPENCLAW_USE_GUM" + return 1 + fi + 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" ]]; 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 +} + +run_with_spinner() { + local title="$1" + shift + + if [[ -n "$GUM" ]] && gum_is_tty && ! is_shell_function "${1:-}"; then + "$GUM" spin --spinner dot --title "$title" -- "$@" + return $? + 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 - echo -e "${WARN}โ†’${NC} Removing legacy submodule checkout: ${INFO}${legacy_dir}${NC}" + ui_info "Removing legacy submodule checkout: ${legacy_dir}" rm -rf "$legacy_dir" fi } @@ -124,7 +492,7 @@ cleanup_openclaw_bin_conflict() { target="$(readlink "$bin_path" 2>/dev/null || true)" if [[ "$target" == *"/node_modules/openclaw/"* ]]; then rm -f "$bin_path" - echo -e "${WARN}โ†’${NC} Removed stale openclaw symlink at ${INFO}${bin_path}${NC}" + ui_info "Removed stale openclaw symlink at ${bin_path}" return 0 fi return 1 @@ -132,38 +500,78 @@ cleanup_openclaw_bin_conflict() { local backup="" backup="${bin_path}.bak-$(date +%Y%m%d-%H%M%S)" if mv "$bin_path" "$backup"; then - echo -e "${WARN}โ†’${NC} Moved existing openclaw binary to ${INFO}${backup}${NC}" + ui_info "Moved existing openclaw binary to ${backup}" return 0 fi return 1 } +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") + + 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 +} + install_openclaw_npm() { local spec="$1" local log log="$(mktempfile)" - if ! SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" npm --loglevel "$NPM_LOGLEVEL" ${NPM_SILENT_FLAG:+$NPM_SILENT_FLAG} --no-fund --no-audit install -g "$spec" 2>&1 | tee "$log"; then + if ! run_npm_global_install "$spec" "$log"; then + if [[ "$VERBOSE" != "1" ]]; then + ui_warn "npm install failed; showing last log lines" + tail -n 80 "$log" >&2 || true + fi + if grep -q "ENOTEMPTY: directory not empty, rename .*openclaw" "$log"; then - echo -e "${WARN}โ†’${NC} npm left a stale openclaw directory; cleaning and retrying..." + ui_warn "npm left stale directory; cleaning and retrying" cleanup_npm_openclaw_paths - SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" npm --loglevel "$NPM_LOGLEVEL" ${NPM_SILENT_FLAG:+$NPM_SILENT_FLAG} --no-fund --no-audit install -g "$spec" - return $? + 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 - SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" npm --loglevel "$NPM_LOGLEVEL" ${NPM_SILENT_FLAG:+$NPM_SILENT_FLAG} --no-fund --no-audit install -g "$spec" - return $? + if run_npm_global_install "$spec" "$log"; then + ui_success "OpenClaw npm package installed" + return 0 + fi + return 1 fi - echo -e "${ERROR}npm failed because an openclaw binary already exists.${NC}" + ui_error "npm failed because an openclaw binary already exists" if [[ -n "$conflict" ]]; then - echo -e "${INFO}i${NC} Remove or move ${INFO}${conflict}${NC}, then retry." + ui_info "Remove or move ${conflict}, then retry" fi - echo -e "${INFO}i${NC} Or rerun with ${INFO}npm install -g --force ${spec}${NC} (overwrites)." + ui_info "Or rerun with: npm install -g --force ${spec}" fi return 1 fi + ui_success "OpenClaw npm package installed" return 0 } @@ -288,6 +696,7 @@ 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_USE_GUM" "CLAWDBOT_USE_GUM" map_legacy_env "OPENCLAW_INSTALL_SH_NO_RUN" "CLAWDBOT_INSTALL_SH_NO_RUN" pick_tagline() { @@ -323,6 +732,7 @@ SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}" NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}" NPM_SILENT_FLAG="--silent" VERBOSE="${OPENCLAW_VERBOSE:-0}" +OPENCLAW_USE_GUM="${OPENCLAW_USE_GUM:-auto}" OPENCLAW_BIN="" HELP=0 @@ -345,6 +755,8 @@ Options: --no-prompt Disable prompts (required in CI/automation) --dry-run Print what would happen (no changes) --verbose Print debug output (set -x, npm verbose) + --gum Force gum UI if possible + --no-gum Disable gum UI --help, -h Show this help Environment variables: @@ -357,6 +769,7 @@ Environment variables: OPENCLAW_DRY_RUN=1 OPENCLAW_NO_ONBOARD=1 OPENCLAW_VERBOSE=1 + OPENCLAW_USE_GUM=auto|1|0 Default: auto (try gum on interactive TTY) 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) @@ -386,6 +799,14 @@ parse_args() { VERBOSE=1 shift ;; + --gum) + OPENCLAW_USE_GUM=1 + shift + ;; + --no-gum) + OPENCLAW_USE_GUM=0 + shift + ;; --no-prompt) NO_PROMPT=1 shift @@ -461,6 +882,59 @@ prompt_choice() { 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}\nChoose install method" + selection="$("$GUM" choose \ + --header "$header" \ + --cursor-prefix "โฏ " \ + "git ยท update this checkout and use it" \ + "npm ยท install globally via npm" < /dev/tty || true)" + + case "$selection" in + git*) + echo "git" + return 0 + ;; + npm*) + echo "npm" + return 0 + ;; + esac + return 1 + fi + + local choice="" + choice="$(prompt_choice "$(cat < /dev/null; then - echo -e "${WARN}โ†’${NC} Installing Homebrew..." - run_remote_bash "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + 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 @@ -511,9 +963,9 @@ install_homebrew() { elif [[ -f "/usr/local/bin/brew" ]]; then eval "$(/usr/local/bin/brew shellenv)" fi - echo -e "${SUCCESS}โœ“${NC} Homebrew installed" + ui_success "Homebrew installed" else - echo -e "${SUCCESS}โœ“${NC} Homebrew already installed" + ui_success "Homebrew already installed" fi fi } @@ -523,14 +975,14 @@ check_node() { if command -v node &> /dev/null; then NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) if [[ "$NODE_VERSION" -ge 22 ]]; then - echo -e "${SUCCESS}โœ“${NC} Node.js v$(node -v | cut -d'v' -f2) found" + ui_success "Node.js v$(node -v | cut -d'v' -f2) found" return 0 else - echo -e "${WARN}โ†’${NC} Node.js $(node -v) found, but v22+ required" + ui_info "Node.js $(node -v) found, upgrading to v22+" return 1 fi else - echo -e "${WARN}โ†’${NC} Node.js not found" + ui_info "Node.js not found, installing it now" return 1 fi } @@ -538,47 +990,64 @@ check_node() { # Install Node.js install_node() { if [[ "$OS" == "macos" ]]; then - echo -e "${WARN}โ†’${NC} Installing Node.js via Homebrew..." - brew install node@22 + 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 - echo -e "${SUCCESS}โœ“${NC} Node.js installed" - elif [[ "$OS" == "linux" ]]; then - echo -e "${WARN}โ†’${NC} Installing Node.js via NodeSource..." - require_sudo - if command -v apt-get &> /dev/null; then - local tmp - tmp="$(mktempfile)" - download_file "https://deb.nodesource.com/setup_22.x" "$tmp" - maybe_sudo -E bash "$tmp" - maybe_sudo apt-get install -y nodejs - elif command -v dnf &> /dev/null; then - local tmp - tmp="$(mktempfile)" - download_file "https://rpm.nodesource.com/setup_22.x" "$tmp" - maybe_sudo bash "$tmp" - maybe_sudo dnf install -y nodejs - elif command -v yum &> /dev/null; then - local tmp - tmp="$(mktempfile)" - download_file "https://rpm.nodesource.com/setup_22.x" "$tmp" - maybe_sudo bash "$tmp" - maybe_sudo yum install -y nodejs - else - echo -e "${ERROR}Error: Could not detect package manager${NC}" - echo "Please install Node.js 22+ manually: https://nodejs.org" + ui_success "Node.js installed" + elif [[ "$OS" == "linux" ]]; then + ui_info "Installing Node.js via NodeSource" + require_sudo + + 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 - echo -e "${SUCCESS}โœ“${NC} Node.js installed" + + ui_success "Node.js v22 installed" fi } # Check Git check_git() { if command -v git &> /dev/null; then - echo -e "${SUCCESS}โœ“${NC} Git already installed" + ui_success "Git already installed" return 0 fi - echo -e "${WARN}โ†’${NC} Git not found" + ui_info "Git not found, installing it now" return 1 } @@ -607,32 +1076,48 @@ require_sudo() { 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 - echo -e "${ERROR}Error: sudo is required for system installs on Linux${NC}" - echo "Install sudo or re-run as root." + ui_error "sudo is required for system installs on Linux" + echo " Install sudo or re-run as root." exit 1 } install_git() { - echo -e "${WARN}โ†’${NC} Installing Git..." if [[ "$OS" == "macos" ]]; then - brew install git + run_quiet_step "Installing Git" brew install git elif [[ "$OS" == "linux" ]]; then require_sudo if command -v apt-get &> /dev/null; then - maybe_sudo apt-get update -y - maybe_sudo apt-get install -y git + if is_root; then + run_quiet_step "Updating package index" apt-get update -qq + run_quiet_step "Installing Git" apt-get install -y -qq git + else + run_quiet_step "Updating package index" sudo apt-get update -qq + run_quiet_step "Installing Git" sudo apt-get install -y -qq git + fi elif command -v dnf &> /dev/null; then - maybe_sudo dnf install -y git + 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 - maybe_sudo yum install -y git + 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 - echo -e "${ERROR}Error: Could not detect package manager for Git${NC}" + ui_error "Could not detect package manager for Git" exit 1 fi fi - echo -e "${SUCCESS}โœ“${NC} Git installed" + ui_success "Git installed" } # Fix npm permissions for global installs (Linux) @@ -651,7 +1136,7 @@ fix_npm_permissions() { return 0 fi - echo -e "${WARN}โ†’${NC} Configuring npm for user-local installs..." + ui_info "Configuring npm for user-local installs" mkdir -p "$HOME/.npm-global" npm config set prefix "$HOME/.npm-global" @@ -664,7 +1149,7 @@ fix_npm_permissions() { done export PATH="$HOME/.npm-global/bin:$PATH" - echo -e "${SUCCESS}โœ“${NC} npm configured for user installs" + ui_success "npm configured for user installs" } resolve_openclaw_bin() { @@ -695,7 +1180,7 @@ ensure_openclaw_bin_link() { mkdir -p "$npm_bin" if [[ ! -x "${npm_bin}/openclaw" ]]; then ln -sf "$npm_root/openclaw/dist/entry.js" "${npm_bin}/openclaw" - echo -e "${WARN}โ†’${NC} Installed openclaw bin link at ${INFO}${npm_bin}/openclaw${NC}" + ui_info "Created openclaw bin link at ${npm_bin}/openclaw" fi return 0 } @@ -703,7 +1188,7 @@ ensure_openclaw_bin_link() { # Check for existing OpenClaw installation check_existing_openclaw() { if [[ -n "$(type -P openclaw 2>/dev/null || true)" ]]; then - echo -e "${WARN}โ†’${NC} Existing OpenClaw installation detected" + ui_info "Existing OpenClaw installation detected, upgrading" return 0 fi return 1 @@ -711,21 +1196,22 @@ check_existing_openclaw() { ensure_pnpm() { if command -v pnpm &> /dev/null; then + ui_success "pnpm already installed" return 0 fi if command -v corepack &> /dev/null; then - echo -e "${WARN}โ†’${NC} Installing pnpm via Corepack..." + ui_info "Installing pnpm via Corepack" corepack enable >/dev/null 2>&1 || true - corepack prepare pnpm@10 --activate - echo -e "${SUCCESS}โœ“${NC} pnpm installed" + run_quiet_step "Activating pnpm" corepack prepare pnpm@10 --activate + ui_success "pnpm installed" return 0 fi - echo -e "${WARN}โ†’${NC} Installing pnpm via npm..." + ui_info "Installing pnpm via npm" fix_npm_permissions - npm install -g pnpm@10 - echo -e "${SUCCESS}โœ“${NC} pnpm installed" + run_quiet_step "Installing pnpm" npm install -g pnpm@10 + ui_success "pnpm installed" return 0 } @@ -793,11 +1279,10 @@ warn_shell_path_missing_dir() { fi echo "" - echo -e "${WARN}โ†’${NC} PATH warning: missing ${label}: ${INFO}${dir}${NC}" - echo -e "This can make ${INFO}openclaw${NC} show as \"command not found\" in new terminals." - echo -e "Fix (zsh: ~/.zshrc, bash: ~/.bashrc):" - echo -e " export PATH=\"${dir}:\\$PATH\"" - echo -e "Docs: ${INFO}https://docs.openclaw.ai/install#nodejs--npm-path-sanity${NC}" + 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() { @@ -815,13 +1300,12 @@ maybe_nodenv_rehash() { } warn_openclaw_not_found() { - echo -e "${WARN}โ†’${NC} Installed, but ${INFO}openclaw${NC} is not discoverable on PATH in this shell." - echo -e "Try: ${INFO}hash -r${NC} (bash) or ${INFO}rehash${NC} (zsh), then retry." - echo -e "Docs: ${INFO}https://docs.openclaw.ai/install#nodejs--npm-path-sanity${NC}" + 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 - echo -e "${WARN}โ†’${NC} Found a shell ${INFO}${t}${NC} named ${INFO}openclaw${NC}; it may shadow the real binary." + 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}" @@ -886,9 +1370,9 @@ install_openclaw_from_git() { local repo_url="https://github.com/openclaw/openclaw.git" if [[ -d "$repo_dir/.git" ]]; then - echo -e "${WARN}โ†’${NC} Installing OpenClaw from git checkout: ${INFO}${repo_dir}${NC}" + ui_info "Installing OpenClaw from git checkout: ${repo_dir}" else - echo -e "${WARN}โ†’${NC} Installing OpenClaw from GitHub (${repo_url})..." + ui_info "Installing OpenClaw from GitHub (${repo_url})" fi if ! check_git; then @@ -898,25 +1382,25 @@ install_openclaw_from_git() { ensure_pnpm if [[ ! -d "$repo_dir" ]]; then - git clone "$repo_url" "$repo_dir" + 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 - git -C "$repo_dir" pull --rebase || true + run_quiet_step "Updating repository" git -C "$repo_dir" pull --rebase || true else - echo -e "${WARN}โ†’${NC} Repo is dirty; skipping git pull" + ui_info "Repo has local changes; skipping git pull" fi fi cleanup_legacy_submodules "$repo_dir" - SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" pnpm -C "$repo_dir" install + run_quiet_step "Installing dependencies" env SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" pnpm -C "$repo_dir" install - if ! pnpm -C "$repo_dir" ui:build; then - echo -e "${WARN}โ†’${NC} UI build failed; continuing (CLI may still work)" + if ! run_quiet_step "Building UI" pnpm -C "$repo_dir" ui:build; then + ui_warn "UI build failed; continuing (CLI may still work)" fi - pnpm -C "$repo_dir" build + run_quiet_step "Building OpenClaw" pnpm -C "$repo_dir" build ensure_user_local_bin_on_path @@ -926,8 +1410,8 @@ set -euo pipefail exec node "${repo_dir}/dist/entry.js" "\$@" EOF chmod +x "$HOME/.local/bin/openclaw" - echo -e "${SUCCESS}โœ“${NC} OpenClaw wrapper installed to \$HOME/.local/bin/openclaw" - echo -e "${INFO}i${NC} This checkout uses pnpm. For deps, run: ${INFO}pnpm install${NC} (avoid npm install in the repo)." + ui_success "OpenClaw wrapper installed to \$HOME/.local/bin/openclaw" + ui_info "This checkout uses pnpm โ€” run pnpm install for deps (not npm install)" } # Install OpenClaw @@ -947,11 +1431,11 @@ install_openclaw() { beta_version="$(resolve_beta_version || true)" if [[ -n "$beta_version" ]]; then OPENCLAW_VERSION="$beta_version" - echo -e "${INFO}i${NC} Beta tag detected (${beta_version}); installing beta." + ui_info "Beta tag detected (${beta_version})" package_name="openclaw" else OPENCLAW_VERSION="latest" - echo -e "${INFO}i${NC} No beta tag found; installing latest." + ui_info "No beta tag found; using latest" fi fi @@ -962,9 +1446,9 @@ install_openclaw() { local resolved_version="" resolved_version="$(npm view "${package_name}@${OPENCLAW_VERSION}" version 2>/dev/null || true)" if [[ -n "$resolved_version" ]]; then - echo -e "${WARN}โ†’${NC} Installing OpenClaw ${INFO}${resolved_version}${NC}..." + ui_info "Installing OpenClaw v${resolved_version}" else - echo -e "${WARN}โ†’${NC} Installing OpenClaw (${INFO}${OPENCLAW_VERSION}${NC})..." + ui_info "Installing OpenClaw (${OPENCLAW_VERSION})" fi local install_spec="" if [[ "${OPENCLAW_VERSION}" == "latest" ]]; then @@ -974,14 +1458,14 @@ install_openclaw() { fi if ! install_openclaw_npm "${install_spec}"; then - echo -e "${WARN}โ†’${NC} npm install failed; cleaning up and retrying..." + 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 - echo -e "${WARN}โ†’${NC} npm install openclaw@latest failed; retrying openclaw@next" + ui_warn "npm install openclaw@latest failed; retrying openclaw@next" cleanup_npm_openclaw_paths install_openclaw_npm "openclaw@next" fi @@ -989,23 +1473,23 @@ install_openclaw() { ensure_openclaw_bin_link || true - echo -e "${SUCCESS}โœ“${NC} OpenClaw installed" + ui_success "OpenClaw installed" } # Run doctor for migrations (safe, non-interactive) run_doctor() { - echo -e "${WARN}โ†’${NC} Running doctor to migrate settings..." + 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 - echo -e "${WARN}โ†’${NC} Skipping doctor: ${INFO}openclaw${NC} not on PATH yet." + ui_info "Skipping doctor (openclaw not on PATH yet)" warn_openclaw_not_found return 0 fi - "$claw" doctor --non-interactive || true - echo -e "${SUCCESS}โœ“${NC} Migration complete" + run_quiet_step "Running doctor" "$claw" doctor --non-interactive || true + ui_success "Doctor complete" } maybe_open_dashboard() { @@ -1050,24 +1534,23 @@ run_bootstrap_onboarding_if_needed() { fi if [[ ! -r /dev/tty || ! -w /dev/tty ]]; then - echo -e "${WARN}โ†’${NC} BOOTSTRAP.md found at ${INFO}${bootstrap}${NC}; no TTY, skipping onboarding." - echo -e "Run ${INFO}openclaw onboard${NC} later to finish setup." + ui_info "BOOTSTRAP.md found but no TTY; run openclaw onboard to finish setup" return fi - echo -e "${WARN}โ†’${NC} BOOTSTRAP.md found at ${INFO}${bootstrap}${NC}; starting onboarding..." + 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 - echo -e "${WARN}โ†’${NC} BOOTSTRAP.md found, but ${INFO}openclaw${NC} not on PATH yet; skipping onboarding." + ui_info "BOOTSTRAP.md found but openclaw not on PATH; skipping onboarding" warn_openclaw_not_found return fi "$claw" onboard || { - echo -e "${ERROR}Onboarding failed; BOOTSTRAP.md still present. Re-run ${INFO}openclaw onboard${ERROR}.${NC}" + ui_error "Onboarding failed; run openclaw onboard to retry" return } } @@ -1123,29 +1606,27 @@ main() { 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 - echo -e "${WARN}โ†’${NC} Found a OpenClaw checkout, but no TTY; defaulting to npm install." + ui_info "Found OpenClaw checkout but no TTY; defaulting to npm install" INSTALL_METHOD="npm" else - local choice="" - choice="$(prompt_choice "$(cat </dev/null; then - echo -e "${WARN}โ†’${NC} Removing npm global install (switching to git)..." + ui_info "Removing npm global install (switching to git)" npm uninstall -g openclaw 2>/dev/null || true - echo -e "${SUCCESS}โœ“${NC} npm global install removed" + ui_success "npm global install removed" fi local repo_dir="$GIT_DIR" @@ -1211,9 +1689,9 @@ EOF else # Clean up git wrapper if switching to npm if [[ -x "$HOME/.local/bin/openclaw" ]]; then - echo -e "${WARN}โ†’${NC} Removing git wrapper (switching to npm)..." + ui_info "Removing git wrapper (switching to npm)" rm -f "$HOME/.local/bin/openclaw" - echo -e "${SUCCESS}โœ“${NC} git wrapper removed" + ui_success "git wrapper removed" fi # Step 3: Git (required for npm installs that may fetch from git or apply patches) @@ -1228,6 +1706,8 @@ EOF 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. @@ -1260,9 +1740,9 @@ EOF echo "" if [[ -n "$installed_version" ]]; then - echo -e "${SUCCESS}${BOLD}๐Ÿฆž OpenClaw installed successfully (${installed_version})!${NC}" + ui_celebrate "๐Ÿฆž OpenClaw installed successfully (${installed_version})!" else - echo -e "${SUCCESS}${BOLD}๐Ÿฆž OpenClaw installed successfully!${NC}" + ui_celebrate "๐Ÿฆž OpenClaw installed successfully!" fi if [[ "$is_upgrade" == "true" ]]; then local update_messages=( @@ -1310,19 +1790,20 @@ EOF echo "" if [[ "$INSTALL_METHOD" == "git" && -n "$final_git_dir" ]]; then - echo -e "Source checkout: ${INFO}${final_git_dir}${NC}" - echo -e "Wrapper: ${INFO}\$HOME/.local/bin/openclaw${NC}" - echo -e "Installed from source. To update later, run: ${INFO}openclaw update --restart${NC}" - echo -e "Switch to global install later: ${INFO}curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method npm${NC}" + 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 - echo -e "Upgrade complete." + 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 - echo -e "${WARN}โ†’${NC} Skipping doctor: ${INFO}openclaw${NC} not on PATH yet." + ui_info "Skipping doctor (openclaw not on PATH yet)" warn_openclaw_not_found return 0 fi @@ -1332,7 +1813,7 @@ EOF doctor_args+=("--non-interactive") fi fi - echo -e "Running ${INFO}openclaw doctor${NC}..." + ui_info "Running openclaw doctor" local doctor_ok=0 if (( ${#doctor_args[@]} )); then OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" doctor "${doctor_args[@]}" /dev/null 2>&1; then - echo -e "${SUCCESS}โœ“${NC} Gateway restarted." + ui_success "Gateway restarted" else - echo -e "${WARN}โ†’${NC} Gateway restart failed; try: ${INFO}openclaw daemon restart${NC}" + ui_warn "Gateway restart failed; try: openclaw daemon restart" fi fi fi @@ -1405,8 +1884,7 @@ EOF maybe_open_dashboard fi - echo "" - echo -e "FAQ: ${INFO}https://docs.openclaw.ai/start/faq${NC}" + show_footer_links } if [[ "${OPENCLAW_INSTALL_SH_NO_RUN:-0}" != "1" ]]; then diff --git a/scripts/test-install-matrix.sh b/scripts/test-install-matrix.sh new file mode 100755 index 0000000..d5efbf0 --- /dev/null +++ b/scripts/test-install-matrix.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +set -euo pipefail + +MODE="dry-run" +USE_GUM=0 +IMAGES=() + +usage() { + cat <<'EOF' +Run install.sh in clean Linux containers (matrix style). + +Note: gum UI is disabled by default because CI/container logs often render +raw ANSI/OSC control sequences. Use --gum only in a real interactive terminal. + +Usage: + bash scripts/test-install-matrix.sh [options] + +Options: + --mode dry-run|full Test mode (default: dry-run) + --image Image to test (repeatable) + --gum Force gum UI + --no-gum Disable gum UI (default) + --help, -h Show this help + +Examples: + bash scripts/test-install-matrix.sh + bash scripts/test-install-matrix.sh --mode full + bash scripts/test-install-matrix.sh --image ubuntu:24.04 --image fedora:40 +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --mode) + MODE="$2" + shift 2 + ;; + --image) + IMAGES+=("$2") + shift 2 + ;; + --gum) + USE_GUM=1 + shift + ;; + --no-gum) + USE_GUM=0 + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ "$MODE" != "dry-run" && "$MODE" != "full" ]]; then + echo "Invalid --mode: $MODE (expected dry-run|full)" >&2 + exit 2 +fi + +if [[ ${#IMAGES[@]} -eq 0 ]]; then + IMAGES=( + "ubuntu:22.04" + "ubuntu:24.04" + "debian:12" + "fedora:40" + ) +fi + +setup_cmd_for_image() { + local image="$1" + case "$image" in + ubuntu:*|debian:*) + cat <<'EOF' +apt-get update -qq +DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ + bash ca-certificates curl sudo tar util-linux +EOF + ;; + fedora:*|rockylinux:*|almalinux:*) + cat <<'EOF' +dnf install -y -q \ + bash ca-certificates curl sudo tar util-linux +EOF + ;; + *) + return 1 + ;; + esac +} + +INSTALL_ARGS=() +build_install_args() { + INSTALL_ARGS=(--no-onboard --no-prompt) + if [[ "$MODE" == "dry-run" ]]; then + INSTALL_ARGS+=(--dry-run) + fi + if [[ "$USE_GUM" == "1" ]]; then + INSTALL_ARGS+=(--gum) + else + INSTALL_ARGS+=(--no-gum) + fi +} + +run_case() { + local image="$1" + local setup_cmd="" + setup_cmd="$(setup_cmd_for_image "$image")" || { + echo "Unsupported image for setup bootstrap: $image" >&2 + return 2 + } + + build_install_args + + local install_cmd="" + printf -v install_cmd '%q ' bash public/install.sh "${INSTALL_ARGS[@]}" + + echo "" + echo "============================================================" + echo "Image: $image" + echo "Mode: $MODE" + echo "GUM: $([[ "$USE_GUM" == "1" ]] && echo on || echo off)" + echo "============================================================" + + docker run --rm -t \ + -e TERM=xterm-256color \ + -v "$PWD":/work \ + -w /work \ + "$image" \ + bash -lc "set -euo pipefail; if ! { ${setup_cmd}; } >/tmp/install-matrix-setup.log 2>&1; then echo 'bootstrap setup failed' >&2; tail -n 120 /tmp/install-matrix-setup.log >&2; exit 1; fi; ${install_cmd}" +} + +for image in "${IMAGES[@]}"; do + run_case "$image" +done + +echo "" +echo "โœ… install matrix completed"