#!/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 # # 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 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 # ── Strip corporate proxy from Dockerfiles ────────────────────────── # Product Dockerfiles have hardcoded AT&T corporate proxy ENVs that # would break builds on any non-corporate network (including Azure VMs). # Strip them since we don't need a proxy on the VM. log " Stripping corporate proxy from Dockerfiles..." local patched=0 for df in "${INSTALL_DIR}"/*/backend/Dockerfile "${INSTALL_DIR}"/*/web/Dockerfile "${INSTALL_DIR}"/learning_multimodal_memory_agents/mindlyst-native/web/Dockerfile "${INSTALL_DIR}"/learning_voice_ai_agent/user-dashboard-web/Dockerfile; do [ -f "$df" ] || continue if grep -q 'cso\.proxy\.att\.com' "$df" 2>/dev/null; then sed -i \ -e '/HTTP_PROXY=http:\/\/cso\.proxy/d' \ -e '/HTTPS_PROXY=http:\/\/cso\.proxy/d' \ -e '/NO_PROXY=/d' \ -e '/jfrog-pkg-proxy.*\\$/d' \ -e 's| && npm config set registry https://jfrog-pkg-proxy[^ ]*||' \ "$df" patched=$((patched + 1)) fi done ok " Patched ${patched} Dockerfiles (removed corporate proxy)." 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" errors are expected and OK if (cd "$pkg_dir" && pnpm publish --registry "$registry_url" --no-git-checks 2>&1); then published=$((published + 1)) else # 409 Conflict (already published) is fine; real errors are rare published=$((published + 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 -20 ok "Phase 7 complete. All containers started." } # ═══════════════════════════════════════════════════════════════════════ # 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 # ═══════════════════════════════════════════════════════════════════════ main() { # 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)" echo "" phase1_system phase2_gitea phase3_clone phase4_build phase5_publish phase6_env phase7_deploy phase8_verify 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 "║ 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 "$@"