#!/usr/bin/env bash # ═══════════════════════════════════════════════════════════════════════ # ByteLyst Single-VM Bootstrap Script # ═══════════════════════════════════════════════════════════════════════ # Deploys the ENTIRE ByteLyst ecosystem on a **raw** Ubuntu Azure VM. # Installs ALL dependencies from scratch — nothing pre-installed required. # # What gets installed: # - Docker CE + Docker Compose + BuildKit # - Node.js 22 LTS + pnpm 10.6.5 # - Gitea (Docker container — npm package registry on :3300) # - Ollama (local LLM inference for LocalMemGPT on :11434) # - All 11 ByteLyst repos (cloned from GitHub) # - All @bytelyst/* packages (built + published to Gitea) # - Full 27-service ecosystem (via docker-compose.ecosystem.yml) # # Usage: sudo ./setup.sh [OPTIONS] # # Options: # --resume Auto-resume from last completed phase # --resume-from=N Resume from phase N (1-8) # --phase=N Run ONLY phase N (useful for retrying a single phase) # --reset Clear phase markers and start fresh # # Optional env vars: # GITHUB_USER — GitHub org/user to clone from (default: saravanakumardb1) # GITHUB_TOKEN — If repos are private, set this for HTTPS auth # GITEA_ADMIN — Gitea admin username (default: bytelyst-admin) # GITEA_PASS — Gitea admin password (default: ByteLyst2026!) # OLLAMA_MODEL — Default LLM model to pull (default: llama3.2:3b) # SKIP_CLONE — Set to 1 to skip cloning (repos already exist) # SKIP_BUILD — Set to 1 to skip package build+publish # ═══════════════════════════════════════════════════════════════════════ set -euo pipefail # ── Configuration ──────────────────────────────────────────────────── INSTALL_DIR="/opt/bytelyst" GITHUB_USER="${GITHUB_USER:-saravanakumardb1}" GITEA_ADMIN="${GITEA_ADMIN:-bytelyst-admin}" GITEA_PASS="${GITEA_PASS:-ByteLyst2026!}" GITEA_PORT=3300 NODE_VERSION=22 PNPM_VERSION="10.6.5" COMPOSE_FILE="docker-compose.ecosystem.yml" OLLAMA_MODEL="${OLLAMA_MODEL:-llama3.2:3b}" # Well-known emulator keys (public, safe to embed) COSMOS_EMULATOR_KEY="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" AZURITE_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" REPOS=( learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_jarvis_jr learning_ai_fastgap learning_ai_peakpulse learning_ai_flowmonk learning_ai_notes learning_ai_trails learning_ai_local_memory_gpt ) # ── Helpers ────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' log() { echo -e "${CYAN}[$(date +%H:%M:%S)]${NC} $*"; } ok() { echo -e "${GREEN}[$(date +%H:%M:%S)] ✓${NC} $*"; } warn() { echo -e "${YELLOW}[$(date +%H:%M:%S)] ⚠${NC} $*"; } fail() { echo -e "${RED}[$(date +%H:%M:%S)] ✗${NC} $*"; exit 1; } wait_for_url() { local url="$1" max="${2:-60}" i=0 while ! curl -sf "$url" > /dev/null 2>&1; do sleep 2; i=$((i + 2)) [ "$i" -ge "$max" ] && fail "Timeout waiting for $url" done } # Detect the host IP that Docker containers can reach detect_docker_host_ip() { # On Linux, the Docker bridge gateway (172.17.0.1) is reachable from containers ip -4 addr show docker0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || echo "172.17.0.1" } # ── Phase tracking (resume/retry support) ────────────────────────── STATE_DIR="${INSTALL_DIR}/.setup-state" mark_phase_done() { mkdir -p "$STATE_DIR" date -Iseconds > "${STATE_DIR}/phase${1}.done" } is_phase_done() { [ -f "${STATE_DIR}/phase${1}.done" ] } last_completed_phase() { local last=0 for i in 1 2 3 4 5 6 7 8; do is_phase_done "$i" && last=$i done echo "$last" } reset_phase_markers() { rm -rf "$STATE_DIR" ok "Phase markers cleared." } # Restore GITEA_NPM_TOKEN from saved state (needed when resuming after phase 2) restore_gitea_token() { if [ -z "${GITEA_NPM_TOKEN:-}" ] && [ -f "${INSTALL_DIR}/.gitea_token" ]; then GITEA_NPM_TOKEN=$(cat "${INSTALL_DIR}/.gitea_token") export GITEA_NPM_TOKEN log "Restored GITEA_NPM_TOKEN from saved state." fi } # ═══════════════════════════════════════════════════════════════════════ # PHASE 1: System Dependencies # ═══════════════════════════════════════════════════════════════════════ phase1_system() { log "Phase 1: Installing system dependencies..." export DEBIAN_FRONTEND=noninteractive # Update package index apt-get update -qq # Install essentials apt-get install -y -qq \ ca-certificates curl gnupg lsb-release git jq unzip iproute2 # ── Docker ───────────────────────────────────────────────────────── if ! command -v docker &>/dev/null; then log "Installing Docker..." install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-buildx-plugin systemctl enable --now docker ok "Docker installed: $(docker --version)" else ok "Docker already installed: $(docker --version)" fi # Enable BuildKit globally mkdir -p /etc/docker cat > /etc/docker/daemon.json <<'DJSON' { "features": { "buildkit": true }, "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } } DJSON systemctl restart docker # ── Node.js ──────────────────────────────────────────────────────── if ! command -v node &>/dev/null || ! node -v | grep -q "v${NODE_VERSION}"; then log "Installing Node.js ${NODE_VERSION}..." curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - apt-get install -y -qq nodejs ok "Node.js installed: $(node -v)" else ok "Node.js already installed: $(node -v)" fi # ── pnpm ─────────────────────────────────────────────────────────── if ! command -v pnpm &>/dev/null; then log "Installing pnpm ${PNPM_VERSION}..." npm install -g "pnpm@${PNPM_VERSION}" ok "pnpm installed: $(pnpm -v)" else ok "pnpm already installed: $(pnpm -v)" fi # ── Ollama (local LLM inference) ────────────────────────────────── if ! command -v ollama &>/dev/null; then log "Installing Ollama..." curl -fsSL https://ollama.com/install.sh | sh ok "Ollama installed: $(ollama --version 2>&1 || echo 'ok')" else ok "Ollama already installed: $(ollama --version 2>&1 || echo 'ok')" fi # Start Ollama service (systemd) if ! systemctl is-active --quiet ollama 2>/dev/null; then systemctl enable --now ollama 2>/dev/null || true # Fallback: start manually if not using systemd if ! curl -sf http://localhost:11434/api/version > /dev/null 2>&1; then nohup ollama serve > /var/log/ollama.log 2>&1 & sleep 3 fi fi # Wait for Ollama API log "Waiting for Ollama API..." wait_for_url "http://localhost:11434/api/version" 30 # Pull default model log "Pulling Ollama model: ${OLLAMA_MODEL} (this may take a few minutes)..." ollama pull "$OLLAMA_MODEL" 2>&1 | tail -3 ok "Ollama ready with model: ${OLLAMA_MODEL}" # ── Create install directory ─────────────────────────────────────── mkdir -p "$INSTALL_DIR" ok "Phase 1 complete." } # ═══════════════════════════════════════════════════════════════════════ # PHASE 2: Gitea (npm Registry) # ═══════════════════════════════════════════════════════════════════════ phase2_gitea() { log "Phase 2: Setting up Gitea npm registry on port ${GITEA_PORT}..." local GITEA_CONTAINER="gitea-npm-registry" # Check if already running if docker ps --format '{{.Names}}' | grep -q "^${GITEA_CONTAINER}$"; then ok "Gitea already running." else # Remove stopped container if exists docker rm -f "$GITEA_CONTAINER" 2>/dev/null || true docker run -d \ --name "$GITEA_CONTAINER" \ --restart unless-stopped \ -p "${GITEA_PORT}:3000" \ -v gitea-data:/data \ -e GITEA__server__ROOT_URL="http://localhost:${GITEA_PORT}/" \ -e GITEA__server__HTTP_PORT=3000 \ -e GITEA__packages__ENABLED=true \ -e INSTALL_LOCK=true \ -e GITEA__security__INSTALL_LOCK=true \ gitea/gitea:1.22 ok "Gitea container started." fi # Wait for Gitea to become ready log "Waiting for Gitea to start..." wait_for_url "http://localhost:${GITEA_PORT}/api/v1/version" 90 # ── Create admin user (idempotent) ───────────────────────────────── log "Creating Gitea admin user..." docker exec "$GITEA_CONTAINER" gitea admin user create \ --username "$GITEA_ADMIN" \ --password "$GITEA_PASS" \ --email "admin@bytelyst.local" \ --admin \ --must-change-password=false 2>/dev/null || true # ── Create "bytelyst" organization ───────────────────────────────── local GITEA_URL="http://localhost:${GITEA_PORT}" local AUTH_HEADER="Authorization: Basic $(echo -n "${GITEA_ADMIN}:${GITEA_PASS}" | base64)" # Check if org exists local org_status org_status=$(curl -sf -o /dev/null -w "%{http_code}" \ -H "$AUTH_HEADER" "${GITEA_URL}/api/v1/orgs/bytelyst") if [ "$org_status" != "200" ]; then curl -sf -X POST "${GITEA_URL}/api/v1/orgs" \ -H "$AUTH_HEADER" \ -H "Content-Type: application/json" \ -d '{"username":"bytelyst","visibility":"public"}' > /dev/null ok "Created 'bytelyst' organization." else ok "'bytelyst' organization already exists." fi # ── Create API token ─────────────────────────────────────────────── # Delete old token if exists, then create fresh curl -sf -X DELETE "${GITEA_URL}/api/v1/users/${GITEA_ADMIN}/tokens/vm-deploy" \ -H "$AUTH_HEADER" > /dev/null 2>&1 || true local token_response token_response=$(curl -sf -X POST "${GITEA_URL}/api/v1/users/${GITEA_ADMIN}/tokens" \ -H "$AUTH_HEADER" \ -H "Content-Type: application/json" \ -d '{"name":"vm-deploy","scopes":["write:package","read:package","write:organization","read:organization"]}') GITEA_NPM_TOKEN=$(echo "$token_response" | jq -r '.sha1 // .token') if [ -z "$GITEA_NPM_TOKEN" ] || [ "$GITEA_NPM_TOKEN" = "null" ]; then fail "Failed to create Gitea API token. Response: $token_response" fi # Export for later phases export GITEA_NPM_TOKEN echo "$GITEA_NPM_TOKEN" > "${INSTALL_DIR}/.gitea_token" chmod 600 "${INSTALL_DIR}/.gitea_token" ok "Phase 2 complete. Gitea running at http://localhost:${GITEA_PORT}" } # ═══════════════════════════════════════════════════════════════════════ # PHASE 3: Clone Repositories # ═══════════════════════════════════════════════════════════════════════ phase3_clone() { if [ "${SKIP_CLONE:-0}" = "1" ]; then warn "Skipping clone (SKIP_CLONE=1)." return fi log "Phase 3: Cloning ${#REPOS[@]} repositories..." local clone_base="https://github.com/${GITHUB_USER}" if [ -n "${GITHUB_TOKEN:-}" ]; then clone_base="https://${GITHUB_TOKEN}@github.com/${GITHUB_USER}" fi for repo in "${REPOS[@]}"; do local target="${INSTALL_DIR}/${repo}" if [ -d "$target/.git" ]; then log " Pulling latest: $repo" git -C "$target" pull --ff-only 2>/dev/null || true else log " Cloning: $repo" git clone --depth 1 "$clone_base/${repo}.git" "$target" fi done # NOTE: Corporate proxy was removed from all Dockerfiles at source # (commit fix(docker) across all 10 product repos + dashboards). # No runtime sed stripping needed. ok "Phase 3 complete. All repos in ${INSTALL_DIR}/" } # ═══════════════════════════════════════════════════════════════════════ # PHASE 4: Build @bytelyst/* Packages # ═══════════════════════════════════════════════════════════════════════ phase4_build() { if [ "${SKIP_BUILD:-0}" = "1" ]; then warn "Skipping build (SKIP_BUILD=1)." return fi log "Phase 4: Building @bytelyst/* packages..." local plat_dir="${INSTALL_DIR}/learning_ai_common_plat" # Configure .npmrc for the common-plat workspace (publish target) cat > "${plat_dir}/.npmrc" <&1 | tail -3 # Build all packages log " Building all packages..." pnpm -r build 2>&1 | tail -5 ok "Phase 4 complete. All packages built." } # ═══════════════════════════════════════════════════════════════════════ # PHASE 5: Publish Packages to Gitea npm Registry # ═══════════════════════════════════════════════════════════════════════ phase5_publish() { if [ "${SKIP_BUILD:-0}" = "1" ]; then warn "Skipping publish (SKIP_BUILD=1)." return fi log "Phase 5: Publishing @bytelyst/* packages to Gitea..." local plat_dir="${INSTALL_DIR}/learning_ai_common_plat" cd "$plat_dir" local published=0 skipped=0 local registry_url="http://localhost:${GITEA_PORT}/api/packages/bytelyst/npm/" for pkg_dir in packages/*/; do local pkg_json="${pkg_dir}package.json" [ -f "$pkg_json" ] || continue local pkg_name pkg_name=$(jq -r '.name // ""' "$pkg_json") # Skip non-@bytelyst packages and private packages [[ "$pkg_name" == @bytelyst/* ]] || continue [ "$(jq -r '.private // false' "$pkg_json")" = "true" ] && continue # Skip packages without a build output [ -d "${pkg_dir}dist" ] || { skipped=$((skipped + 1)) continue } # Single publish attempt — "already exists" (409) errors are expected and OK local pub_output if pub_output=$(cd "$pkg_dir" && pnpm publish --registry "$registry_url" --no-git-checks 2>&1); then published=$((published + 1)) elif echo "$pub_output" | grep -qi 'already exists\|409\|conflict'; then published=$((published + 1)) else warn " Failed to publish ${pkg_name}: $(echo "$pub_output" | tail -1)" skipped=$((skipped + 1)) fi done ok "Phase 5 complete. Published: ${published}, Skipped: ${skipped}" } # ═══════════════════════════════════════════════════════════════════════ # PHASE 6: Generate .env.ecosystem # ═══════════════════════════════════════════════════════════════════════ phase6_env() { log "Phase 6: Generating .env.ecosystem..." local plat_dir="${INSTALL_DIR}/learning_ai_common_plat" local env_file="${plat_dir}/.env.ecosystem" # Generate a random JWT secret local jwt_secret jwt_secret=$(openssl rand -base64 32) cat > "$env_file" <&1 | tail -5 } phase7_deploy() { log "Phase 7: Deploying ecosystem (per-service build + fallback)..." setup_compose_env local plat_dir="${INSTALL_DIR}/learning_ai_common_plat" local build_ok=() build_fail=() build_skip=() mkdir -p "${STATE_DIR}/builds" # ── 7a: Build each service individually ────────────────────────── local all_services=("${INFRA_SERVICES[@]}" "${PLATFORM_SERVICES[@]}" "${DASHBOARD_SERVICES[@]}" "${BACKEND_SERVICES[@]}" "${WEB_SERVICES[@]}") local total=${#all_services[@]} local idx=0 log " Building ${total} service images individually..." echo "" for svc in "${all_services[@]}"; do idx=$((idx + 1)) # Infrastructure services use pre-built images (no build step) local has_build has_build=$(docker compose -f "${plat_dir}/${COMPOSE_FILE}" config --format json 2>/dev/null \ | jq -r ".services.\"${svc}\".build // empty" 2>/dev/null || true) if [ -z "$has_build" ] || [ "$has_build" = "null" ]; then build_skip+=("$svc") ok " [${idx}/${total}] ${svc} — pre-built image (skip build)" continue fi log " [${idx}/${total}] Building ${svc}..." if build_one_service "$svc"; then build_ok+=("$svc") date -Iseconds > "${STATE_DIR}/builds/${svc}.ok" ok " [${idx}/${total}] ${svc} — build OK" else build_fail+=("$svc") date -Iseconds > "${STATE_DIR}/builds/${svc}.fail" warn " [${idx}/${total}] ${svc} — BUILD FAILED (will skip)" fi done # ── Build summary ──────────────────────────────────────────────── echo "" log " Build results: ${#build_ok[@]} OK, ${#build_fail[@]} FAILED, ${#build_skip[@]} pre-built" if [ ${#build_fail[@]} -gt 0 ]; then warn " Failed services: ${build_fail[*]}" fi # ── 7b: Start services (skip failed builds) ───────────────────── # Compose up only the services that built successfully + pre-built infra local start_services=() for svc in "${build_skip[@]}"; do start_services+=("$svc"); done for svc in "${build_ok[@]}"; do start_services+=("$svc"); done if [ ${#start_services[@]} -eq 0 ]; then fail "No services to start — all builds failed." fi log " Starting ${#start_services[@]} services..." docker compose \ -f "${plat_dir}/${COMPOSE_FILE}" \ --env-file "${plat_dir}/.env.ecosystem" \ up -d "${start_services[@]}" 2>&1 | tail -10 if [ ${#build_fail[@]} -gt 0 ]; then warn "Phase 7 complete with ${#build_fail[@]} failed builds: ${build_fail[*]}" warn " Fix and re-run: sudo ./setup.sh --phase=7" else ok "Phase 7 complete. All ${#start_services[@]} services started." fi } # ═══════════════════════════════════════════════════════════════════════ # PHASE 8: Health Check # ═══════════════════════════════════════════════════════════════════════ phase8_verify() { log "Phase 8: Verifying service health..." local plat_dir="${INSTALL_DIR}/learning_ai_common_plat" # Wait for platform-service (everything else depends on it) log " Waiting for platform-service..." wait_for_url "http://localhost:4003/health" 120 # Create a reusable health-check script cat > "${INSTALL_DIR}/check-health.sh" <<'HEALTH' #!/usr/bin/env bash RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m' check() { local name="$1" url="$2" if curl -sf "$url" > /dev/null 2>&1; then echo -e "${GREEN} ✓ ${name}${NC} ${url}" else echo -e "${RED} ✗ ${name}${NC} ${url}" fi } echo "" echo "═══ Infrastructure ═══" check "Gitea (npm)" "http://localhost:3300/api/v1/version" check "Ollama (LLM)" "http://localhost:11434/api/version" check "Cosmos Explorer" "http://localhost:1234" check "Mailpit" "http://localhost:8025" check "Grafana" "http://localhost:3000/api/health" check "Traefik" "http://localhost:8080/api/overview" echo "" echo "═══ Platform Services ═══" check "platform-service" "http://localhost:4003/health" check "extraction-service" "http://localhost:4005/health" check "mcp-server" "http://localhost:4007/health" echo "" echo "═══ Dashboards ═══" check "admin-web" "http://localhost:3001" check "tracker-web" "http://localhost:3003" echo "" echo "═══ Product Backends ═══" check "peakpulse" "http://localhost:4010/health" check "chronomind" "http://localhost:4011/health" check "jarvisjr" "http://localhost:4012/health" check "nomgap" "http://localhost:4013/health" check "mindlyst" "http://localhost:4014/health" check "lysnrai" "http://localhost:4015/health" check "notelett" "http://localhost:4016/health" check "flowmonk" "http://localhost:4017/health" check "actiontrail" "http://localhost:4018/health" check "localmemgpt" "http://localhost:4019/health" echo "" echo "═══ Product Web Apps ═══" check "lysnrai-dashboard" "http://localhost:3002" check "chronomind-web" "http://localhost:3030" check "jarvisjr-web" "http://localhost:3035" check "flowmonk-web" "http://localhost:3040" check "notelett-web" "http://localhost:3045" check "mindlyst-web" "http://localhost:3050" check "nomgap-web" "http://localhost:3055" check "actiontrail-web" "http://localhost:3060" check "localmemgpt-web" "http://localhost:3070" echo "" HEALTH chmod +x "${INSTALL_DIR}/check-health.sh" # Give services a moment to start, then run health check log " Waiting 30s for services to stabilize..." sleep 30 # Run the health check bash "${INSTALL_DIR}/check-health.sh" ok "Phase 8 complete." } # ═══════════════════════════════════════════════════════════════════════ # MAIN # ═══════════════════════════════════════════════════════════════════════ run_phase() { local phase_num="$1" case "$phase_num" in 1) phase1_system ;; 2) phase2_gitea ;; 3) phase3_clone ;; 4) phase4_build ;; 5) phase5_publish ;; 6) phase6_env ;; 7) phase7_deploy ;; 8) phase8_verify ;; *) fail "Unknown phase: $phase_num" ;; esac mark_phase_done "$phase_num" } usage() { echo "Usage: sudo ./setup.sh [OPTIONS]" echo "" echo "Options:" echo " --resume Auto-resume from last completed phase" echo " --resume-from=N Resume starting at phase N (1-8)" echo " --phase=N Run ONLY phase N" echo " --reset Clear phase markers and start fresh" echo " --status Show completed phases and exit" echo " -h, --help Show this help" echo "" echo "Phases:" echo " 1 System dependencies (Docker, Node, pnpm, Ollama)" echo " 2 Gitea npm registry" echo " 3 Clone repositories" echo " 4 Build @bytelyst/* packages" echo " 5 Publish packages to Gitea" echo " 6 Generate .env.ecosystem" echo " 7 Build + deploy Docker services (per-service, with fallback)" echo " 8 Health check" } main() { # Parse CLI arguments local mode="full" start_phase=1 only_phase=0 for arg in "$@"; do case "$arg" in --resume) mode="resume" ;; --resume-from=*) mode="resume-from" start_phase="${arg#*=}" ;; --phase=*) mode="single" only_phase="${arg#*=}" ;; --reset) mkdir -p "$INSTALL_DIR" reset_phase_markers exit 0 ;; --status) mkdir -p "$INSTALL_DIR" echo "Phase completion status:" for i in 1 2 3 4 5 6 7 8; do if is_phase_done "$i"; then echo " Phase $i: DONE ($(cat "${STATE_DIR}/phase${i}.done"))" else echo " Phase $i: pending" fi done exit 0 ;; -h|--help) usage; exit 0 ;; *) warn "Unknown option: $arg" usage; exit 1 ;; esac done # Tee all output to a log file so SSH disconnection doesn't lose context mkdir -p "$INSTALL_DIR" exec > >(tee -a "${INSTALL_DIR}/setup.log") 2>&1 echo "" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ ByteLyst Single-VM Deployment (raw Ubuntu) ║" echo "║ 27 services · 10 products · Ollama · Gitea · 1 VM ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo "" log "Log file: ${INSTALL_DIR}/setup.log" [ "$(id -u)" -eq 0 ] || fail "This script must be run as root (sudo)." local start_time start_time=$(date +%s) log "Target OS: $(lsb_release -ds 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')" log "Target arch: $(uname -m)" # ── Single-phase mode ──────────────────────────────────────────── if [ "$mode" = "single" ]; then log "Running ONLY phase ${only_phase}..." restore_gitea_token run_phase "$only_phase" ok "Phase ${only_phase} complete." exit 0 fi # ── Auto-resume mode ───────────────────────────────────────────── if [ "$mode" = "resume" ]; then local last last=$(last_completed_phase) if [ "$last" -eq 0 ]; then log "No completed phases found. Starting from phase 1." start_phase=1 elif [ "$last" -ge 8 ]; then ok "All phases already completed. Use --reset to start over." exit 0 else start_phase=$((last + 1)) log "Resuming from phase ${start_phase} (phases 1-${last} already done)." fi elif [ "$mode" = "resume-from" ]; then log "Resuming from phase ${start_phase} (as requested)." fi # Restore token if resuming past phase 2 if [ "$start_phase" -gt 2 ]; then restore_gitea_token fi echo "" # ── Run phases ─────────────────────────────────────────────────── for phase_num in 1 2 3 4 5 6 7 8; do [ "$phase_num" -ge "$start_phase" ] || continue run_phase "$phase_num" done local elapsed=$(( $(date +%s) - start_time )) local minutes=$(( elapsed / 60 )) local seconds=$(( elapsed % 60 )) echo "" echo "╔═══════════════════════════════════════════════════════════════╗" echo "║ Deployment complete in ${minutes}m ${seconds}s ║" echo "║ ║" echo "║ Health check: /opt/bytelyst/check-health.sh ║" echo "║ Compose logs: docker compose -f ${COMPOSE_FILE} logs -f ║" echo "║ Retry failed: sudo ./setup.sh --phase=7 ║" echo "║ Resume: sudo ./setup.sh --resume ║" echo "║ Gitea UI: http://localhost:3300 ║" echo "║ Ollama API: http://localhost:11434 ║" echo "║ Grafana: http://localhost:3000 (admin / bytelyst) ║" echo "║ Mailpit: http://localhost:8025 ║" echo "╚═══════════════════════════════════════════════════════════════╝" echo "" } main "$@"