#!/bin/bash # install_full.sh — OpenClacky full installer (macOS + Linux, with Homebrew) # Generated from scripts/build/src/install_full.sh.cc — DO NOT EDIT DIRECTLY set -e BRAND_NAME="" BRAND_COMMAND="" RESTORE_MIRRORS=false # ---[ @include lib/colors.sh ]--- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' print_info() { echo -e "${BLUE}ℹ${NC} $1"; } print_success() { echo -e "${GREEN}✓${NC} $1"; } print_warning() { echo -e "${YELLOW}⚠${NC} $1"; } print_error() { echo -e "${RED}✗${NC} $1"; } print_step() { echo -e "\n${BLUE}==>${NC} $1"; } # ---[ @include lib/os.sh ]--- # Sets OS (macOS | Linux | Windows | Unknown) and DISTRO (ubuntu | debian | …) detect_os() { case "$(uname -s)" in Linux*) OS=Linux ;; Darwin*) OS=macOS ;; CYGWIN*) OS=Windows ;; MINGW*) OS=Windows ;; *) OS=Unknown ;; esac print_info "Detected OS: $OS" if [ "$OS" = "Linux" ] && [ -f /etc/os-release ]; then # shellcheck source=/dev/null . /etc/os-release DISTRO=$ID print_info "Detected Linux distribution: $DISTRO" else DISTRO=unknown fi } # Returns 0 if the given command is on PATH command_exists() { command -v "$1" >/dev/null 2>&1; } # Boolean helpers — use these in business logic instead of inline string comparisons is_macos() { [ "$OS" = "macOS" ]; } is_linux_apt() { [ "$OS" = "Linux" ] && { [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; }; } # Returns 0 (true) if $1 >= $2 (semantic version comparison) version_ge() { printf '%s\n%s\n' "$2" "$1" | sort -V -C; } # Assert that the current OS/distro is supported (macOS or Ubuntu/Debian). # Optional $1: hint message printed on failure (e.g. manual install instructions). # Exits with code 1 on unsupported OS or distro. assert_supported_os() { local hint="${1:-}" if [ "$OS" = "Linux" ]; then if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then return 0 fi print_error "Unsupported Linux distribution: $DISTRO" [ -n "$hint" ] && print_info "$hint" exit 1 elif [ "$OS" = "macOS" ]; then return 0 else print_error "Unsupported OS: $OS" [ -n "$hint" ] && print_info "$hint" exit 1 fi } # ---[ @include lib/shell.sh ]--- # Sets CURRENT_SHELL (zsh | bash | fish) and SHELL_RC (path to rc file) detect_shell() { local shell_name shell_name=$(basename "$SHELL") case "$shell_name" in zsh) CURRENT_SHELL="zsh" SHELL_RC="$HOME/.zshrc" ;; fish) CURRENT_SHELL="fish" SHELL_RC="$HOME/.config/fish/config.fish" ;; bash) CURRENT_SHELL="bash" # macOS uses ~/.bash_profile; Linux uses ~/.bashrc if is_macos; then SHELL_RC="$HOME/.bash_profile" else SHELL_RC="$HOME/.bashrc" fi ;; *) CURRENT_SHELL="bash" SHELL_RC="$HOME/.bashrc" ;; esac print_info "Detected shell: $CURRENT_SHELL (rc file: $SHELL_RC)" } # ---[ @include lib/network.sh ]--- # -------------------------------------------------------------------------- # Mirror variables — overridden by detect_network_region() # -------------------------------------------------------------------------- SLOW_THRESHOLD_MS=5000 NETWORK_REGION="global" # china | global | unknown USE_CN_MIRRORS=false GITHUB_RAW_BASE_URL="https://raw.githubusercontent.com" DEFAULT_RUBYGEMS_URL="https://rubygems.org" DEFAULT_NPM_REGISTRY="https://registry.npmjs.org" DEFAULT_MISE_INSTALL_URL="https://mise.run" CN_CDN_BASE_URL="https://oss.1024code.com" CN_CDN_FALLBACK_URL="https://clackyai-1258723534.cos.ap-guangzhou.myqcloud.com" CN_ALIYUN_MIRROR="https://mirrors.aliyun.com" CN_RUBYGEMS_URL="${CN_ALIYUN_MIRROR}/rubygems/" CN_NPM_REGISTRY="https://registry.npmmirror.com" CN_NODE_MIRROR_URL="https://cdn.npmmirror.com/binaries/node/" # Derived from CN_CDN_BASE_URL — always set via _apply_cdn_base_url() CN_MISE_INSTALL_URL="${CN_CDN_BASE_URL}/mise.sh" CN_RUBY_PRECOMPILED_URL="${CN_CDN_BASE_URL}/ruby/ruby-{version}.{platform}.tar.gz" CN_GEM_BASE_URL="${CN_CDN_BASE_URL}/openclacky" CN_GEM_LATEST_URL="${CN_GEM_BASE_URL}/latest.txt" # Active values (set by detect_network_region) MISE_INSTALL_URL="$DEFAULT_MISE_INSTALL_URL" RUBYGEMS_INSTALL_URL="$DEFAULT_RUBYGEMS_URL" NPM_REGISTRY_URL="$DEFAULT_NPM_REGISTRY" NODE_MIRROR_URL="" # empty = mise default (nodejs.org) RUBY_VERSION_SPEC="ruby@3" # CN mode pins to a specific precompiled build # -------------------------------------------------------------------------- # Internal probe helpers # -------------------------------------------------------------------------- # Probe a single URL; echoes round-trip time in ms, or "timeout" _probe_url() { local url="$1" local out http_code total_time out=$(curl -s -o /dev/null -w "%{http_code} %{time_total}" \ --connect-timeout 5 --max-time 5 "$url" 2>/dev/null) || true http_code="${out%% *}" total_time="${out#* }" if [ -z "$http_code" ] || [ "$http_code" = "000" ] || [ "$http_code" = "$out" ]; then echo "timeout"; return fi awk -v s="$total_time" 'BEGIN { printf "%d", s * 1000 }' } # Returns 0 (true) if result is slow or unreachable _is_slow_or_unreachable() { local r="$1" [ "$r" = "timeout" ] && return 0 [ "${r:-9999}" -ge "$SLOW_THRESHOLD_MS" ] 2>/dev/null } _format_probe_time() { local r="$1" [ "$r" = "timeout" ] && echo "timeout" && return awk -v ms="$r" 'BEGIN { printf "%.1fs", ms / 1000 }' } _print_probe_result() { local label="$1" result="$2" if [ "$result" = "timeout" ]; then print_warning "UNREACHABLE ${label}" elif _is_slow_or_unreachable "$result"; then print_warning "SLOW ($(_format_probe_time "$result")) ${label}" else print_success "OK ($(_format_probe_time "$result")) ${label}" fi } # Probe URL up to max_retries times; returns first fast result or last result _probe_url_with_retry() { local url="$1" max="${2:-2}" result for _ in $(seq 1 "$max"); do result=$(_probe_url "$url") ! _is_slow_or_unreachable "$result" && { echo "$result"; return 0; } done echo "$result" } # -------------------------------------------------------------------------- # CN CDN resolution helpers # -------------------------------------------------------------------------- # Probe primary CDN then fallback; echoes the reachable base URL or exits 1 _resolve_cdn_base_url() { local result result=$(_probe_url_with_retry "$CN_CDN_BASE_URL") _print_probe_result "CN CDN (oss.1024code.com)" "$result" ! _is_slow_or_unreachable "$result" && return 0 print_warning "CN CDN unreachable — trying fallback..." result=$(_probe_url_with_retry "$CN_CDN_FALLBACK_URL") _print_probe_result "CN CDN fallback" "$result" if _is_slow_or_unreachable "$result"; then print_error "CN CDN and fallback both unreachable — cannot install." exit 2 fi CN_CDN_BASE_URL="$CN_CDN_FALLBACK_URL" } # Apply a resolved base URL to all CDN-derived variables _apply_cdn_base_url() { local base="$1" CN_CDN_BASE_URL="$base" CN_MISE_INSTALL_URL="${base}/mise.sh" CN_RUBY_PRECOMPILED_URL="${base}/ruby/ruby-{version}.{platform}.tar.gz" CN_GEM_BASE_URL="${base}/openclacky" CN_GEM_LATEST_URL="${CN_GEM_BASE_URL}/latest.txt" } # -------------------------------------------------------------------------- # detect_network_region — sets USE_CN_MIRRORS and active mirror variables # -------------------------------------------------------------------------- detect_network_region() { print_step "Network pre-flight check..." echo "" local probe_google="https://www.google.com" local probe_github="https://raw.githubusercontent.com" local probe_baidu="https://www.baidu.com" local google_result github_result baidu_result google_result=$(_probe_url "$probe_google") github_result=$(_probe_url "$probe_github") baidu_result=$(_probe_url "$probe_baidu") _print_probe_result "google.com" "$google_result" _print_probe_result "raw.githubusercontent.com" "$github_result" _print_probe_result "baidu.com" "$baidu_result" local google_ok=false github_ok=false baidu_ok=false ! _is_slow_or_unreachable "$google_result" && google_ok=true ! _is_slow_or_unreachable "$github_result" && github_ok=true ! _is_slow_or_unreachable "$baidu_result" && baidu_ok=true if [ "$google_ok" = true ] && [ "$github_ok" = true ]; then NETWORK_REGION="global" print_success "Region: global" elif [ "$baidu_ok" = true ]; then NETWORK_REGION="china" print_success "Region: china" else print_error "Region: unknown (all unreachable) — cannot install." exit 2 fi echo "" if [ "$NETWORK_REGION" = "china" ]; then _resolve_cdn_base_url _apply_cdn_base_url "$CN_CDN_BASE_URL" local mirror_result mirror_result=$(_probe_url_with_retry "$CN_ALIYUN_MIRROR") _print_probe_result "Aliyun mirror" "$mirror_result" local mirror_ok=false ! _is_slow_or_unreachable "$mirror_result" && mirror_ok=true if [ "$mirror_ok" = true ]; then USE_CN_MIRRORS=true MISE_INSTALL_URL="$CN_MISE_INSTALL_URL" RUBYGEMS_INSTALL_URL="$CN_RUBYGEMS_URL" NPM_REGISTRY_URL="$CN_NPM_REGISTRY" NODE_MIRROR_URL="$CN_NODE_MIRROR_URL" RUBY_VERSION_SPEC="ruby@3.4.8" print_info "CN mirrors applied" else print_error "CN mirrors unreachable — cannot install." exit 2 fi else USE_CN_MIRRORS=false RUBY_VERSION_SPEC="ruby@3" fi echo "" } # ---[ @include lib/brew.sh ]--- # -------------------------------------------------------------------------- # Homebrew CN mirror URLs (Aliyun) # -------------------------------------------------------------------------- CN_HOMEBREW_INSTALL_SCRIPT_URL="${CN_CDN_BASE_URL}/Homebrew/install/HEAD/install.sh" CN_HOMEBREW_BREW_GIT_REMOTE="https://mirrors.aliyun.com/homebrew/brew.git" CN_HOMEBREW_CORE_GIT_REMOTE="https://mirrors.aliyun.com/homebrew/homebrew-core.git" CN_HOMEBREW_BOTTLE_DOMAIN="https://mirrors.aliyun.com/homebrew/homebrew-bottles" CN_HOMEBREW_API_DOMAIN="https://mirrors.aliyun.com/homebrew-bottles/api" HOMEBREW_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" # -------------------------------------------------------------------------- # configure_homebrew_cn_mirrors — export env vars and persist to SHELL_RC # Only runs when USE_CN_MIRRORS=true # -------------------------------------------------------------------------- configure_homebrew_cn_mirrors() { [ "$USE_CN_MIRRORS" = true ] || return 0 print_info "Configuring Homebrew CN mirrors..." export HOMEBREW_INSTALL_FROM_API=1 export HOMEBREW_API_DOMAIN="$CN_HOMEBREW_API_DOMAIN" export HOMEBREW_BREW_GIT_REMOTE="$CN_HOMEBREW_BREW_GIT_REMOTE" export HOMEBREW_CORE_GIT_REMOTE="$CN_HOMEBREW_CORE_GIT_REMOTE" export HOMEBREW_BOTTLE_DOMAIN="$CN_HOMEBREW_BOTTLE_DOMAIN" if ! grep -q "HOMEBREW_BOTTLE_DOMAIN" "$SHELL_RC" 2>/dev/null; then { echo "" echo "# Homebrew CN mirrors (added by openclacky installer)" echo "export HOMEBREW_INSTALL_FROM_API=1" echo "export HOMEBREW_API_DOMAIN=\"${CN_HOMEBREW_API_DOMAIN}\"" echo "export HOMEBREW_BREW_GIT_REMOTE=\"${CN_HOMEBREW_BREW_GIT_REMOTE}\"" echo "export HOMEBREW_CORE_GIT_REMOTE=\"${CN_HOMEBREW_CORE_GIT_REMOTE}\"" echo "export HOMEBREW_BOTTLE_DOMAIN=\"${CN_HOMEBREW_BOTTLE_DOMAIN}\"" } >> "$SHELL_RC" print_success "Homebrew CN mirrors written to $SHELL_RC" else print_success "Homebrew CN mirrors already configured in $SHELL_RC" fi } # -------------------------------------------------------------------------- # restore_homebrew_cn_mirrors — remove CN mirror lines from SHELL_RC # -------------------------------------------------------------------------- restore_homebrew_cn_mirrors() { if [ -n "$SHELL_RC" ] && [ -f "$SHELL_RC" ] && grep -q "HOMEBREW_BOTTLE_DOMAIN" "$SHELL_RC" 2>/dev/null; then sed -i.bak '/# Homebrew CN mirrors (added by openclacky installer)/d' "$SHELL_RC" sed -i.bak '/HOMEBREW_INSTALL_FROM_API/d' "$SHELL_RC" sed -i.bak '/HOMEBREW_API_DOMAIN/d' "$SHELL_RC" sed -i.bak '/HOMEBREW_BREW_GIT_REMOTE/d' "$SHELL_RC" sed -i.bak '/HOMEBREW_CORE_GIT_REMOTE/d' "$SHELL_RC" sed -i.bak '/HOMEBREW_BOTTLE_DOMAIN/d' "$SHELL_RC" rm -f "${SHELL_RC}.bak" unset HOMEBREW_BREW_GIT_REMOTE HOMEBREW_CORE_GIT_REMOTE HOMEBREW_BOTTLE_DOMAIN print_success "Homebrew CN mirrors removed from $SHELL_RC" else print_info "Homebrew CN mirrors — nothing to restore" fi } # -------------------------------------------------------------------------- # ensure_homebrew — install Homebrew if missing, add to PATH # -------------------------------------------------------------------------- ensure_homebrew() { if command_exists brew; then print_success "Homebrew already installed" return 0 fi print_info "Installing Homebrew..." local brew_url="$HOMEBREW_INSTALL_SCRIPT_URL" [ "$USE_CN_MIRRORS" = true ] && brew_url="$CN_HOMEBREW_INSTALL_SCRIPT_URL" if /bin/bash -c "$(curl -fsSL "$brew_url")"; then # Add Homebrew to PATH (Apple Silicon default path) echo 'export PATH="/opt/homebrew/bin:$PATH"' >> "$SHELL_RC" export PATH="/opt/homebrew/bin:$PATH" print_success "Homebrew installed" else print_error "Failed to install Homebrew" return 1 fi } # ---[ @include lib/mise.sh ]--- # -------------------------------------------------------------------------- # Internal helper — locate the mise binary # -------------------------------------------------------------------------- _mise_bin() { if [ -x "$HOME/.local/bin/mise" ]; then echo "$HOME/.local/bin/mise" elif command_exists mise; then command -v mise else echo "" fi } # -------------------------------------------------------------------------- # ensure_mise — install mise if missing, activate it in the current session # -------------------------------------------------------------------------- ensure_mise() { local mise mise=$(_mise_bin) if [ -n "$mise" ]; then print_success "mise already installed: $($mise --version 2>/dev/null || echo 'n/a')" export PATH="$HOME/.local/bin:$PATH" eval "$($mise activate bash 2>/dev/null)" 2>/dev/null || true MISE_BIN="$mise" return 0 fi print_step "Installing mise..." if curl -fsSL "$MISE_INSTALL_URL" | sh; then export PATH="$HOME/.local/bin:$PATH" eval "$(~/.local/bin/mise activate bash 2>/dev/null)" 2>/dev/null || true # Persist mise activation to shell rc local init_line='eval "$(~/.local/bin/mise activate '"$CURRENT_SHELL"')"' if ! grep -q "mise activate" "$SHELL_RC" 2>/dev/null; then echo "$init_line" >> "$SHELL_RC" print_info "Added mise activation to $SHELL_RC" fi MISE_BIN="$HOME/.local/bin/mise" print_success "mise installed" else print_error "Failed to install mise" return 1 fi } # -------------------------------------------------------------------------- # install_ruby_via_mise — install Ruby via mise # CN mode: precompiled binary from oss.1024code.com # Global: mise default (precompiled where available) # -------------------------------------------------------------------------- install_ruby_via_mise() { local mise="${MISE_BIN:-$(_mise_bin)}" if [ -z "$mise" ]; then print_error "mise not found — call ensure_mise first" return 1 fi print_info "Installing Ruby via mise ($RUBY_VERSION_SPEC)..." if [ "$USE_CN_MIRRORS" = true ]; then "$mise" settings ruby.compile=false 2>/dev/null || true "$mise" settings ruby.precompiled_url="$CN_RUBY_PRECOMPILED_URL" 2>/dev/null || true print_info "Using precompiled Ruby from CN CDN" else # Enable precompiled binaries globally (mise supports this on common platforms) "$mise" settings ruby.compile=false 2>/dev/null || true "$mise" settings unset ruby.precompiled_url 2>/dev/null || true fi if "$mise" use -g "$RUBY_VERSION_SPEC"; then eval "$($mise activate bash 2>/dev/null)" 2>/dev/null || true local installed installed=$(ruby -e 'puts RUBY_VERSION' 2>/dev/null || echo "unknown") print_success "Ruby $installed installed" else print_error "Failed to install Ruby via mise" return 1 fi } # -------------------------------------------------------------------------- # install_node_via_mise — install Node.js 22 via mise # -------------------------------------------------------------------------- install_node_via_mise() { local mise="${MISE_BIN:-$(_mise_bin)}" if [ -z "$mise" ]; then print_error "mise not found — call ensure_mise first" return 1 fi # Skip if compatible Node is already active if command_exists node; then local ver major ver=$(node --version 2>/dev/null | sed 's/v//') major="${ver%%.*}" if [ "${major:-0}" -ge 22 ] 2>/dev/null; then print_success "Node.js v${ver} already satisfies >= 22 — skipping" return 0 fi fi # Apply CN Node mirror if needed if [ "$USE_CN_MIRRORS" = true ] && [ -n "$NODE_MIRROR_URL" ]; then "$mise" settings node.mirror_url="$NODE_MIRROR_URL" 2>/dev/null || true print_info "Node mirror → ${NODE_MIRROR_URL}" fi print_info "Installing Node.js 22 via mise..." if "$mise" use -g node@22; then eval "$($mise activate bash 2>/dev/null)" 2>/dev/null || true print_success "Node.js $(node --version 2>/dev/null) installed" else print_error "Failed to install Node.js via mise" return 1 fi } # ---[ @include lib/gem.sh ]--- configure_gem_source() { local gemrc="$HOME/.gemrc" if [ "$USE_CN_MIRRORS" = true ]; then if grep -q "${CN_RUBYGEMS_URL}" "$gemrc" 2>/dev/null; then print_success "gem source already → ${CN_RUBYGEMS_URL}" else [ -f "$gemrc" ] && mv "$gemrc" "$HOME/.gemrc_clackybak" cat > "$gemrc" </dev/null; then if [ -f "$HOME/.gemrc_clackybak" ]; then mv "$HOME/.gemrc_clackybak" "$gemrc" print_info "gem source restored from backup" else rm "$gemrc" print_info "gem source restored to default" fi fi fi } restore_gemrc() { local gemrc="$HOME/.gemrc" local gemrc_bak="$HOME/.gemrc_clackybak" if [ -f "$gemrc_bak" ]; then mv "$gemrc_bak" "$gemrc" print_success "~/.gemrc restored from backup" elif [ -f "$gemrc" ]; then rm "$gemrc" print_success "~/.gemrc removed" else print_info "~/.gemrc — nothing to restore" fi } # -------------------------------------------------------------------------- # Redirect GEM_HOME to user dir when system Ruby gem dir is not writable # -------------------------------------------------------------------------- setup_gem_home() { local gem_dir gem_dir=$(gem environment gemdir 2>/dev/null || true) # gemdir writable → no workaround needed, clean up old RC entries if [ -w "$gem_dir" ]; then restore_gem_home return 0 fi local ruby_api ruby_api=$(ruby -e 'puts RbConfig::CONFIG["ruby_version"]' 2>/dev/null) export GEM_HOME="$HOME/.gem/ruby/${ruby_api}" export GEM_PATH="$HOME/.gem/ruby/${ruby_api}" export PATH="$HOME/.gem/ruby/${ruby_api}/bin:$PATH" print_info "System Ruby detected — gems will install to ~/.gem/ruby/${ruby_api}" # Clean old RC entries (could be from a different Ruby version), then write new ones restore_gem_home if [ -n "$SHELL_RC" ]; then { echo "" echo "# Ruby user gem dir (added by openclacky installer)" echo "export GEM_HOME=\"\$HOME/.gem/ruby/${ruby_api}\"" echo "export GEM_PATH=\"\$HOME/.gem/ruby/${ruby_api}\"" echo "export PATH=\"\$HOME/.gem/ruby/${ruby_api}/bin:\$PATH\"" } >> "$SHELL_RC" print_info "GEM_HOME written to $SHELL_RC" fi } restore_gem_home() { [ -n "$SHELL_RC" ] && [ -f "$SHELL_RC" ] || return 0 grep -q "GEM_HOME" "$SHELL_RC" 2>/dev/null || return 0 # Remove the block written by setup_gem_home (comment + 3 export lines) local tmp tmp=$(mktemp) grep -v "# Ruby user gem dir (added by openclacky installer)" "$SHELL_RC" \ | grep -v "export GEM_HOME=" \ | grep -v "export GEM_PATH=" \ | grep -v "/.gem/ruby/" \ > "$tmp" && mv "$tmp" "$SHELL_RC" print_success "GEM_HOME removed from $SHELL_RC" } # -------------------------------------------------------------------------- # CN mirror config — gem, npm (persisted to dotfiles) # -------------------------------------------------------------------------- configure_cn_mirrors() { [ "$USE_CN_MIRRORS" = true ] || return 0 print_step "Configuring CN mirrors (permanent)..." # gem: ~/.gemrc configure_gem_source # npm: ~/.npmrc local npmrc="$HOME/.npmrc" if grep -q "${NPM_REGISTRY_URL}" "$npmrc" 2>/dev/null; then print_success "npm registry already set → ${NPM_REGISTRY_URL}" else [ -f "$npmrc" ] && [ ! -f "$HOME/.npmrc_clackybak" ] && cp "$npmrc" "$HOME/.npmrc_clackybak" if command_exists npm; then npm config set registry "$NPM_REGISTRY_URL" 2>/dev/null && \ print_success "npm registry → ${NPM_REGISTRY_URL}" else echo "registry=${NPM_REGISTRY_URL}" >> "$npmrc" print_success "npm registry → ${NPM_REGISTRY_URL} (pre-set)" fi fi echo "" } restore_mirrors() { print_step "Restoring original mirror settings..." # gem restore_gemrc # npm local npmrc="$HOME/.npmrc" npmrc_bak="$HOME/.npmrc_clackybak" if [ -f "$npmrc_bak" ]; then mv "$npmrc_bak" "$npmrc"; print_success "~/.npmrc restored from backup" elif [ -f "$npmrc" ]; then rm "$npmrc"; print_success "~/.npmrc removed" else print_info "~/.npmrc — nothing to restore" fi # mise node.mirror_url local mise_bin="" command_exists mise && mise_bin="mise" [ -x "$HOME/.local/bin/mise" ] && mise_bin="$HOME/.local/bin/mise" [ -n "$mise_bin" ] && "$mise_bin" settings unset node.mirror_url 2>/dev/null && \ print_success "mise node.mirror_url unset" restore_homebrew_cn_mirrors echo "" print_success "Done. All mirror settings restored." echo "" } # -------------------------------------------------------------------------- # Ruby check # -------------------------------------------------------------------------- check_ruby() { command_exists ruby || return 1 local ver; ver=$(ruby -e 'puts RUBY_VERSION' 2>/dev/null) version_ge "$ver" "3.1.0" && { print_success "Ruby $ver — OK (>= 3.1.0)"; return 0; } print_warning "Ruby $ver too old (need >= 3.1.0)"; return 1 } # -------------------------------------------------------------------------- # macOS: Homebrew + build deps + Ruby via mise # -------------------------------------------------------------------------- install_macos_dependencies() { print_step "Installing macOS dependencies and Ruby..." echo "" configure_homebrew_cn_mirrors ensure_homebrew || return 1 print_info "Installing build dependencies..." brew install openssl@3 libyaml gmp || { print_error "Failed to install build deps"; return 1; } print_success "Build dependencies installed" ensure_mise || return 1 install_ruby_via_mise || return 1 check_ruby || { print_error "Ruby installation verification failed"; return 1; } } # -------------------------------------------------------------------------- # Linux (Ubuntu/Debian): apt mirror + build deps + Ruby via mise # -------------------------------------------------------------------------- install_ubuntu_dependencies() { print_step "Installing Ubuntu dependencies and Ruby..." echo "" if [ "$USE_CN_MIRRORS" = true ]; then print_info "Configuring apt mirror (Aliyun)..." local codename="${VERSION_CODENAME:-jammy}" local components="main restricted universe multiverse" local arch; arch=$(dpkg --print-architecture 2>/dev/null || uname -m) if [ "$arch" = "arm64" ] || [ "$arch" = "aarch64" ]; then local mirror_base="https://mirrors.aliyun.com/ubuntu-ports/" else local mirror_base="https://mirrors.aliyun.com/ubuntu/" fi sudo tee /etc/apt/sources.list > /dev/null </dev/null) version_ge "$ver" "3.1.0" || { print_error "Ruby $ver too old (>= 3.1.0 required)"; return 1; } setup_gem_home print_info "Installing ${DISPLAY_NAME}..." if gem install openclacky --no-document; then print_success "${DISPLAY_NAME} installed!" install_chrome_devtools_mcp return 0 else print_error "gem install failed"; return 1 fi } # -------------------------------------------------------------------------- # Optional: chrome-devtools-mcp (browser automation) # -------------------------------------------------------------------------- install_chrome_devtools_mcp() { print_step "Installing chrome-devtools-mcp..." if ! command_exists npm; then local mise_bin="" command_exists mise && mise_bin="mise" [ -x "$HOME/.local/bin/mise" ] && mise_bin="$HOME/.local/bin/mise" if [ -n "$mise_bin" ]; then "$mise_bin" install node@22 >/dev/null 2>&1 || true "$mise_bin" use -g node@22 >/dev/null 2>&1 || true eval "$($mise_bin activate bash 2>/dev/null)" 2>/dev/null || true fi fi if ! command_exists npm; then print_warning "npm not found — browser automation unavailable" print_info "Install Node.js then run: npm install -g chrome-devtools-mcp" return 0 fi if npm install -g chrome-devtools-mcp >/dev/null 2>&1; then print_success "chrome-devtools-mcp installed" else print_warning "chrome-devtools-mcp install failed" print_info "Run manually: npm install -g chrome-devtools-mcp" fi } # -------------------------------------------------------------------------- # Parse args # -------------------------------------------------------------------------- parse_args() { for arg in "$0" "$@"; do case "$arg" in --brand-name=*) BRAND_NAME="${arg#--brand-name=}" ;; --command=*) BRAND_COMMAND="${arg#--command=}" ;; --restore-mirrors) RESTORE_MIRRORS=true ;; esac done DISPLAY_NAME="${BRAND_NAME:-OpenClacky}" } # -------------------------------------------------------------------------- # Brand setup # -------------------------------------------------------------------------- setup_brand() { [ -z "$BRAND_NAME" ] && return 0 local brand_file="$HOME/.clacky/brand.yml" mkdir -p "$HOME/.clacky" print_step "Configuring brand: $BRAND_NAME" cat > "$brand_file" < "$wrapper" <