#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" ENV_FILE="${1:-$ROOT_DIR/.env}" COMPOSE_FILE="$ROOT_DIR/docker-compose.yml" DATA_DIR="$ROOT_DIR/data" INIT_DB_SCRIPT="$ROOT_DIR/offline/init-full-db-offline.sh" AGENT_TEMPLATES_FILE="$DATA_DIR/templates/agent_md_templates.json" TOPIC_PRESETS_FILE="$DATA_DIR/templates/topic_presets.json" SKILLS_DIR="$DATA_DIR/skills" if [[ ! -f "$ENV_FILE" && -f "$ROOT_DIR/.env.full" ]]; then ENV_FILE="$ROOT_DIR/.env.full" fi if [[ ! -f "$COMPOSE_FILE" && -f "$ROOT_DIR/docker-compose.full.yml" ]]; then COMPOSE_FILE="$ROOT_DIR/docker-compose.full.yml" fi require_file() { local path="$1" local hint="${2:-}" if [[ -f "$path" ]]; then return 0 fi echo "Missing file: $path" [[ -n "$hint" ]] && echo "$hint" exit 1 } require_dir() { local path="$1" local hint="${2:-}" if [[ -d "$path" ]]; then return 0 fi echo "Missing directory: $path" [[ -n "$hint" ]] && echo "$hint" exit 1 } require_env() { local name="$1" [[ -n "${!name:-}" ]] || { echo "Missing required env: $name"; exit 1; } } read_env_value() { local key="$1" local line="" local value="" while IFS= read -r line || [[ -n "$line" ]]; do line="${line%$'\r'}" [[ -z "${line//[[:space:]]/}" ]] && continue [[ "${line#\#}" != "$line" ]] && continue [[ "${line#export }" != "$line" ]] && line="${line#export }" [[ "$line" == "$key="* ]] || continue value="${line#*=}" if [[ "$value" =~ ^\"(.*)\"$ ]]; then value="${BASH_REMATCH[1]}" elif [[ "$value" =~ ^\'(.*)\'$ ]]; then value="${BASH_REMATCH[1]}" fi printf '%s' "$value" return 0 done < "$ENV_FILE" return 1 } load_env_var() { local name="$1" local default_value="${2:-}" local value="" value="$(read_env_value "$name" || true)" if [[ -z "$value" ]]; then value="$default_value" fi printf -v "$name" '%s' "$value" } wait_for_health() { local container_name="$1" local timeout_seconds="$2" local elapsed=0 local status="" while (( elapsed < timeout_seconds )); do status="$( docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_name" 2>/dev/null || true )" if [[ "$status" == "healthy" || "$status" == "running" ]]; then echo "[deploy-full-offline] $container_name is $status" return 0 fi sleep 2 elapsed=$((elapsed + 2)) done echo "[deploy-full-offline] timed out waiting for $container_name (last status: ${status:-unknown})" docker logs --tail 80 "$container_name" 2>/dev/null || true exit 1 } require_file "$ENV_FILE" "Expected bundle config file such as .env" require_file "$COMPOSE_FILE" require_file "$INIT_DB_SCRIPT" require_file "$AGENT_TEMPLATES_FILE" require_file "$TOPIC_PRESETS_FILE" require_dir "$SKILLS_DIR" load_env_var HOST_BOTS_WORKSPACE_ROOT load_env_var POSTGRES_SUPERUSER postgres load_env_var POSTGRES_SUPERPASSWORD load_env_var POSTGRES_BOOTSTRAP_DB postgres load_env_var POSTGRES_APP_DB load_env_var POSTGRES_APP_USER load_env_var POSTGRES_APP_PASSWORD load_env_var NGINX_PORT 8080 load_env_var PUBLIC_HOST "" require_env HOST_BOTS_WORKSPACE_ROOT require_env POSTGRES_SUPERUSER require_env POSTGRES_SUPERPASSWORD require_env POSTGRES_BOOTSTRAP_DB require_env POSTGRES_APP_DB require_env POSTGRES_APP_USER require_env POSTGRES_APP_PASSWORD require_env NGINX_PORT case "$HOST_BOTS_WORKSPACE_ROOT" in /*) ;; *) echo "HOST_BOTS_WORKSPACE_ROOT must be an absolute host path: $HOST_BOTS_WORKSPACE_ROOT" exit 1 ;; esac if [[ ! -S /var/run/docker.sock ]]; then echo "Missing required mount source: /var/run/docker.sock" exit 1 fi echo "[deploy-full-offline] using env: $ENV_FILE" mkdir -p \ "$DATA_DIR" \ "$DATA_DIR/postgres" \ "$DATA_DIR/redis" \ "$DATA_DIR/model" \ "$HOST_BOTS_WORKSPACE_ROOT" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config -q echo "[deploy-full-offline] starting postgres and redis" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d postgres redis wait_for_health "dashboard-nanobot-postgres" 120 wait_for_health "dashboard-nanobot-redis" 60 echo "[deploy-full-offline] initializing application database" "$INIT_DB_SCRIPT" "$ENV_FILE" echo "[deploy-full-offline] starting backend and nginx" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d backend nginx wait_for_health "dashboard-nanobot-backend" 180 wait_for_health "dashboard-nanobot-nginx" 120 echo "[deploy-full-offline] service status" docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" ps if [[ -n "$PUBLIC_HOST" ]]; then echo "[deploy-full-offline] open: http://${PUBLIC_HOST}:${NGINX_PORT}" fi echo "[deploy-full-offline] done"