#!/bin/bash # ============================================================================ # ODS Installer — Orchestrator # ============================================================================ # Unified installer - voice-enabled by default, uses docker-compose.yml # profiles for optional features. # Mission: M5 (Clonable ODS Setup Server) # # This file sources library modules (pure functions, no side effects) then # runs each install phase in order. Individual modules live under: # installers/lib/ — reusable function libraries # installers/phases/ — sequential install steps (execute on source) # # See each module's header for what it expects and provides. # ============================================================================ set -euo pipefail #============================================================================= # Cleanup on Failure #============================================================================= # Track what phases have completed so we can provide useful context on failure. export INSTALL_PHASE="init" cleanup_on_error() { local exit_code=$? echo "" echo -e "\033[0;31m[ERROR] Installation failed during phase: ${INSTALL_PHASE}\033[0m" echo -e "\033[0;33m Log file: ${LOG_FILE:-/tmp/ods-install.log}\033[0m" echo "" echo "The install did not complete. Partial state may exist at:" echo " ${INSTALL_DIR:-~/ods}" echo "" echo "To retry, run the installer again. It will resume safely." echo "To start fresh, remove the install directory first:" echo " rm -rf ${INSTALL_DIR:-~/ods} && ./install.sh" exit "$exit_code" } trap cleanup_on_error ERR #============================================================================= # Interrupt Protection #============================================================================= # Accidental keypresses (Ctrl+C, Ctrl+Z) shouldn't silently kill the install. # We require a double-tap of Ctrl+C within 3 seconds to actually abort. LAST_SIGINT=0 interrupt_handler() { local now now=$(date +%s) if (( now - LAST_SIGINT <= 3 )); then echo "" echo -e "\033[0;33m[!] Install cancelled by user.\033[0m" echo -e "\033[0;32m Log file: ${LOG_FILE:-/tmp/ods-install.log}\033[0m" exit 130 fi LAST_SIGINT=$now echo "" echo -e "\033[0;33m[!] Press Ctrl+C again within 3 seconds to cancel the install.\033[0m" } trap interrupt_handler INT # Ignore Ctrl+Z (SIGTSTP) entirely — backgrounding the installer breaks things trap '' TSTP #============================================================================= # Load libraries (pure functions, no side effects) #============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ "$(uname -s 2>/dev/null || true)" == "Linux" ]]; then export ODS_PYTHON_PREFER_SYSTEM="${ODS_PYTHON_PREFER_SYSTEM:-1}" fi source "$SCRIPT_DIR/installers/lib/constants.sh" source "$SCRIPT_DIR/installers/lib/logging.sh" source "$SCRIPT_DIR/installers/lib/ui.sh" source "$SCRIPT_DIR/installers/lib/sudo.sh" source "$SCRIPT_DIR/installers/lib/detection.sh" source "$SCRIPT_DIR/installers/lib/host-arch.sh" source "$SCRIPT_DIR/installers/lib/tier-map.sh" source "$SCRIPT_DIR/installers/lib/docker-images.sh" source "$SCRIPT_DIR/installers/lib/compose-images.sh" source "$SCRIPT_DIR/installers/lib/compose-select.sh" source "$SCRIPT_DIR/installers/lib/compose-failure-report.sh" source "$SCRIPT_DIR/installers/lib/readiness-summary.sh" source "$SCRIPT_DIR/installers/lib/packaging.sh" source "$SCRIPT_DIR/installers/lib/python-runtime.sh" source "$SCRIPT_DIR/installers/lib/progress.sh" if [[ -f "$SCRIPT_DIR/lib/service-registry.sh" ]]; then source "$SCRIPT_DIR/lib/service-registry.sh" fi #============================================================================= # Command Line Args #============================================================================= DRY_RUN=false SKIP_DOCKER=false FORCE=false TIER="" ENABLE_VOICE=true ENABLE_WORKFLOWS=true ENABLE_RAG=true ENABLE_RECOMMENDED=true # Default agent flipped to Hermes Agent (Nous Research) on 2026-05-12. # OpenClaw is deprecated and will be removed in the next release; new # installs no longer enable it by default. Users who explicitly pass # --openclaw or upgrade an existing install with OpenClaw enabled keep # it working until the removal release. See docs/MIGRATION-OPENCLAW-TO-HERMES.md. ENABLE_HERMES=true ENABLE_OPENCLAW=false OPENCLAW_EXPLICIT=false ENABLE_COMFYUI=true ENABLE_APE=true ENABLE_PERPLEXICA=true ENABLE_PRIVACY_SHIELD=true ENABLE_ODS_PROXY=false ENABLE_TAILSCALE=false ENABLE_BRAVE_SEARCH=false # Langfuse (LLM observability) defaults OFF on all tiers because its # clickhouse + postgres + minio stack adds ~500MB baseline memory that is # nontrivial even on Tier 3+ systems. Users opt in via --langfuse, --all, # the Custom menu, or post-install `ods enable langfuse`. ENABLE_LANGFUSE=false INTERACTIVE=true ODS_MODE="${ODS_MODE:-local}" LEMONADE_EXTERNAL="${LEMONADE_EXTERNAL:-false}" LEMONADE_BASE_URL="${LEMONADE_BASE_URL:-}" LEMONADE_API_KEY="${LEMONADE_API_KEY:-}" OFFLINE_MODE=false # M1 integration: fully air-gapped operation NO_BOOTSTRAP=false # Skip bootstrap fast-start, download full model in foreground BIND_ADDRESS_EXPLICIT=false [[ -n "${BIND_ADDRESS:-}" ]] && BIND_ADDRESS_EXPLICIT=true BIND_ADDRESS="${BIND_ADDRESS:-127.0.0.1}" SUMMARY_JSON_FILE="${SUMMARY_JSON_FILE:-}" usage() { cat << EOF ODS Installer v${VERSION} Usage: $0 [OPTIONS] Options: --dry-run Show what would be done without making changes --skip-docker Skip Docker installation (assume already installed) --force Overwrite existing installation --tier N Force specific tier (1-4) instead of auto-detect --cloud Cloud mode: skip GPU detection, use LiteLLM + cloud APIs --use-existing-lemonade Use an already-running Lemonade SDK server as the AMD LLM runtime --lemonade-url U Lemonade server URL for --use-existing-lemonade (auto-detects localhost:13305, then localhost:8000 when omitted) --lemonade-api-key K API key LiteLLM should send to the existing Lemonade server --voice Enable voice services (Whisper + Kokoro) --no-voice Disable voice services --workflows Enable n8n workflow automation --no-workflows Disable n8n workflow automation --rag Enable RAG with Qdrant vector database --no-rag Disable RAG / Qdrant --recommended Enable LiteLLM + SearXNG + Token Spy support services --no-recommended Disable recommended support services --hermes Enable Hermes Agent (default; new default agent as of 2026-05-12) --no-hermes Disable Hermes Agent --openclaw Enable OpenClaw (DEPRECATED — see docs/MIGRATION-OPENCLAW-TO-HERMES.md) --no-openclaw Disable OpenClaw --comfyui Enable ComfyUI image generation --no-comfyui Disable ComfyUI image generation (saves ~34GB) --odsforge Deprecated no-op; ODSForge has been removed --no-odsforge Deprecated no-op; ODSForge has been removed --langfuse Enable Langfuse LLM observability (off by default) --no-langfuse Explicitly disable Langfuse (for --all overrides) --all Enable all optional services (including Langfuse) --non-interactive Run without prompts (use defaults or flags) --offline M1 mode: Configure for fully offline/air-gapped operation --lan Bind services to 0.0.0.0 for LAN access (headless servers) --no-bootstrap Skip bootstrap fast-start (download full model in foreground) --summary-json P Write machine-readable install summary JSON to path P -h, --help Show this help Tiers: 1 - Entry Level (8GB+ VRAM, 7B models) 2 - Prosumer (12GB+ VRAM, 14B-32B AWQ models) 3 - Pro (24GB+ VRAM, 32B models) 4 - Enterprise (48GB+ VRAM or dual GPU, 72B models) Port Configuration: All service ports are configurable via .env (see .env.example). Example: WEBUI_PORT=8080 OLLAMA_PORT=11435 ./install.sh Examples: $0 # Interactive setup $0 --tier 2 --voice # Tier 2 with voice $0 --all --non-interactive # Full stack, no prompts $0 --cloud # Cloud mode (no GPU needed, uses API keys) $0 --use-existing-lemonade # Wrap an existing Lemonade SDK runtime $0 --offline --all # Fully offline (M1 mode) with all services $0 --dry-run # Preview installation EOF exit 0 } while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true; shift ;; --skip-docker) SKIP_DOCKER=true; shift ;; --force) FORCE=true; shift ;; --tier) TIER="$2"; shift 2 ;; --cloud) ODS_MODE="cloud"; shift ;; --use-existing-lemonade) LEMONADE_EXTERNAL=true; ODS_MODE="lemonade"; shift ;; --lemonade-url) LEMONADE_EXTERNAL=true; ODS_MODE="lemonade"; LEMONADE_BASE_URL="$2"; shift 2 ;; --lemonade-api-key) LEMONADE_API_KEY="$2"; shift 2 ;; --voice) ENABLE_VOICE=true; shift ;; --no-voice) ENABLE_VOICE=false; shift ;; --workflows) ENABLE_WORKFLOWS=true; shift ;; --no-workflows) ENABLE_WORKFLOWS=false; shift ;; --rag) ENABLE_RAG=true; shift ;; --no-rag) ENABLE_RAG=false; shift ;; --recommended) ENABLE_RECOMMENDED=true; shift ;; --no-recommended) ENABLE_RECOMMENDED=false; shift ;; --hermes) ENABLE_HERMES=true; shift ;; --no-hermes) ENABLE_HERMES=false; shift ;; --openclaw) ENABLE_OPENCLAW=true; OPENCLAW_EXPLICIT=true; shift ;; --no-openclaw) ENABLE_OPENCLAW=false; OPENCLAW_EXPLICIT=true; shift ;; --comfyui) ENABLE_COMFYUI=true; shift ;; --no-comfyui) ENABLE_COMFYUI=false; shift ;; --odsforge) warn "ODSForge has been removed; ignoring --odsforge"; shift ;; --no-odsforge) warn "ODSForge has been removed; ignoring --no-odsforge"; shift ;; --langfuse) ENABLE_LANGFUSE=true; shift ;; # NOTE: with --all, --no-langfuse must appear AFTER --all on the command # line (flag processing is case-loop ordered, matching comfyui). --no-langfuse) ENABLE_LANGFUSE=false; shift ;; # --all enables Hermes (the new default agent) but NOT OpenClaw — # the deprecated agent is opt-in via --openclaw for the deprecation # release. Will be dropped entirely in the removal release. # ENABLE_ODS_PROXY is included so magic-link invite URLs # (http://auth..local/magic-link/) actually resolve. # Without ods-proxy on host :80, mDNS publishes the hostname but # nothing serves it, and a phone clicking the invite gets # "site can't be reached." Operators who don't want the LAN-facing # surface can set ENABLE_ODS_PROXY=false in .env after install. --all) ENABLE_VOICE=true; ENABLE_WORKFLOWS=true; ENABLE_RAG=true; ENABLE_RECOMMENDED=true; ENABLE_HERMES=true; ENABLE_OPENCLAW=false; ENABLE_COMFYUI=true; ENABLE_APE=true; ENABLE_PERPLEXICA=true; ENABLE_PRIVACY_SHIELD=true; ENABLE_LANGFUSE=true; ENABLE_ODS_PROXY=true; shift ;; --non-interactive) INTERACTIVE=false; shift ;; --offline) OFFLINE_MODE=true; shift ;; --lan) BIND_ADDRESS="0.0.0.0"; BIND_ADDRESS_EXPLICIT=true; shift ;; --no-bootstrap) NO_BOOTSTRAP=true; shift ;; --summary-json) SUMMARY_JSON_FILE="$2"; shift 2 ;; -h|--help) usage ;; *) error "Unknown option: $1" ;; esac done if [[ "${LEMONADE_EXTERNAL,,}" == "true" ]]; then ODS_MODE="lemonade" ENABLE_RECOMMENDED=true export LEMONADE_EXTERNAL LEMONADE_BASE_URL LEMONADE_API_KEY fi # OpenClaw deprecation back-compat: preserve OpenClaw on UPGRADES of installs # that previously had it enabled. The earlier heuristic — "does the compose # file exist on disk?" — was wrong: extensions/services/openclaw/compose.yaml # is part of the source tree, so every fresh install (including `--all` which # explicitly sets ENABLE_OPENCLAW=false) was being silently re-enabled. That # tacked ~20 minutes onto every install (a slow OpenClaw container blocking # the phase-12 health-link loop) and contradicted both the deprecation policy # AND what `--all` claims to do. # # Correct heuristic: there's an actual OpenClaw container on this host (running # or stopped from a prior install), OR there's persisted OpenClaw data on disk. # Either signal means the user already opted in once, so preserve their choice # through the deprecation window. A fresh install matches neither and leaves # ENABLE_OPENCLAW at its --no-openclaw / --all / default-false value. if [[ "$OPENCLAW_EXPLICIT" != "true" ]]; then _existing_openclaw=false if command -v docker >/dev/null 2>&1 \ && docker ps -a --filter "name=^/ods-openclaw$" --format '{{.Names}}' 2>/dev/null \ | grep -q '^ods-openclaw$'; then _existing_openclaw=true fi if [[ -d "$INSTALL_DIR/data/openclaw" ]] \ && [[ -n "$(ls -A "$INSTALL_DIR/data/openclaw" 2>/dev/null)" ]]; then _existing_openclaw=true fi if $_existing_openclaw; then ENABLE_OPENCLAW=true log "Existing OpenClaw install detected; preserving it for this deprecation release" fi unset _existing_openclaw fi # Detect distro + package manager (after arg parsing so --help still shows # the correct VERSION before /etc/os-release overwrites it) detect_pkg_manager log "Installer run started: pid=$$, script=$0" ods_prepare_sudo "ODS installer setup" export ODS_SR_AUTO_INSTALL_PYYAML=1 ods_ensure_python_module yaml python3-pyyaml pyyaml PyYAML if declare -f sr_load >/dev/null 2>&1; then sr_load fi #============================================================================= # Splash #============================================================================= show_stranger_boot [[ "$INTERACTIVE" == "true" ]] && sleep 5 $DRY_RUN && echo -e "${AMB}>>> DRY RUN MODE — I will simulate everything. No changes made. <<<${NC}\n" #============================================================================= # Run phases #============================================================================= INSTALL_PHASE="01-preflight"; source "$SCRIPT_DIR/installers/phases/01-preflight.sh" INSTALL_PHASE="02-detection"; source "$SCRIPT_DIR/installers/phases/02-detection.sh" INSTALL_PHASE="03-features"; source "$SCRIPT_DIR/installers/phases/03-features.sh" INSTALL_PHASE="04-requirements"; source "$SCRIPT_DIR/installers/phases/04-requirements.sh" INSTALL_PHASE="05-docker"; source "$SCRIPT_DIR/installers/phases/05-docker.sh" INSTALL_PHASE="06-directories"; source "$SCRIPT_DIR/installers/phases/06-directories.sh" INSTALL_PHASE="07-devtools"; source "$SCRIPT_DIR/installers/phases/07-devtools.sh" INSTALL_PHASE="08-images"; source "$SCRIPT_DIR/installers/phases/08-images.sh" INSTALL_PHASE="09-offline"; source "$SCRIPT_DIR/installers/phases/09-offline.sh" INSTALL_PHASE="10-amd-tuning"; source "$SCRIPT_DIR/installers/phases/10-amd-tuning.sh" INSTALL_PHASE="11-services"; source "$SCRIPT_DIR/installers/phases/11-services.sh" INSTALL_PHASE="12-health"; source "$SCRIPT_DIR/installers/phases/12-health.sh" # Phase 13 is informational (URLs, shortcuts, preflight). It must never fail # the install — any error here is cosmetic. Run with set +e to prevent # stray non-zero exit codes (e.g., a crashing privacy-shield health probe) # from triggering the cleanup_on_error trap. INSTALL_PHASE="13-summary" set +e source "$SCRIPT_DIR/installers/phases/13-summary.sh" set -e