#!/bin/sh set -e SQUADEM_VERSION="${SQUADEM_VERSION:-latest}" SQUADEM_DIR="${SQUADEM_DIR:-$HOME/.squadem}" COMPOSE_URL="https://get.squadem.com/docker-compose.yml" LICENSE_API="https://squadem.com/api/license" # Parse flags FORCE_GATEWAY_SETUP=false for arg in "$@"; do case "$arg" in --setup-gateway) FORCE_GATEWAY_SETUP=true ;; esac done # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' info() { printf "${GREEN}[squadem]${NC} %s\n" "$1"; } warn() { printf "${YELLOW}[squadem]${NC} %s\n" "$1"; } error() { printf "${RED}[squadem]${NC} %s\n" "$1"; exit 1; } echo "" echo "${CYAN}${BOLD}" echo " ███████╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ ███████╗███╗ ███╗" echo " ██╔════╝██╔═══██╗██║ ██║██╔══██╗██╔══██╗██╔════╝████╗ ████║" echo " ███████╗██║ ██║██║ ██║███████║██║ ██║█████╗ ██╔████╔██║" echo " ╚════██║██║▄▄ ██║██║ ██║██╔══██║██║ ██║██╔══╝ ██║╚██╔╝██║" echo " ███████║╚██████╔╝╚██████╔╝██║ ██║██████╔╝███████╗██║ ╚═╝ ██║" echo " ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝" echo "${NC}" echo "" # ─── Step 1: License Key ─── SQUADEM_LICENSE_KEY="${SQUADEM_LICENSE_KEY:-}" if [ -z "$SQUADEM_LICENSE_KEY" ]; then if [ -f "$SQUADEM_DIR/.env" ]; then SQUADEM_LICENSE_KEY=$(grep -E '^SQUADEM_LICENSE_KEY=' "$SQUADEM_DIR/.env" 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true) fi fi if [ -z "$SQUADEM_LICENSE_KEY" ]; then echo " ${BOLD}A license key is required to install Squadem.${NC}" echo "" echo " ${BOLD}Free license:${NC} Sign up at ${BOLD}https://squadem.com${NC} to get a free Community license key" echo " ${BOLD}Enterprise:${NC} Contact ${BOLD}enterprise@squadem.com${NC} for Enterprise features" echo "" printf " Enter your license key: " read -r SQUADEM_LICENSE_KEY < /dev/tty echo "" fi if [ -z "$SQUADEM_LICENSE_KEY" ]; then error "No license key provided. Get one at https://squadem.com/portal" fi info "Validating license key..." AUTH_RESPONSE=$(curl -sf -X POST "${LICENSE_API}/install/auth" \ -H "Content-Type: application/json" \ -d "{\"key\":\"${SQUADEM_LICENSE_KEY}\"}" 2>&1) || { echo "" echo " ${RED}${BOLD}License validation failed.${NC}" echo "" echo " Possible reasons:" echo " • Invalid or expired license key" echo " • No internet connection" echo " • License server unreachable" echo "" echo " Purchase a plan at ${BOLD}https://squadem.com/portal${NC}" echo "" exit 1 } LICENSE_SUCCESS=$(echo "$AUTH_RESPONSE" | grep -o '"success":\s*true' || true) if [ -z "$LICENSE_SUCCESS" ]; then ERROR_MSG=$(echo "$AUTH_RESPONSE" | grep -o '"error":"[^"]*"' | cut -d'"' -f4 || echo "Unknown error") error "License validation failed: $ERROR_MSG" fi LICENSE_TIER=$(echo "$AUTH_RESPONSE" | grep -o '"tier":"[^"]*"' | cut -d'"' -f4 || echo "unknown") info "License valid! Tier: ${LICENSE_TIER}" # ─── Step 2: Detect OS/Arch ─── OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Darwin) OS_NAME="macOS" ;; Linux) OS_NAME="Linux" ;; *) error "Unsupported OS: $OS. Squadem supports macOS and Linux." ;; esac case "$ARCH" in x86_64|amd64) ARCH_NAME="amd64" ;; arm64|aarch64) ARCH_NAME="arm64" ;; *) error "Unsupported architecture: $ARCH" ;; esac info "Detected: $OS_NAME ($ARCH_NAME)" # ─── Step 3: Check Prerequisites ─── info "Checking prerequisites..." if ! command -v docker >/dev/null 2>&1; then error "Docker is not installed. Install it from https://docker.com/get-started" fi if ! docker compose version >/dev/null 2>&1; then error "Docker Compose v2 is required. Update Docker Desktop or install the compose plugin." fi if ! docker info >/dev/null 2>&1; then error "Docker daemon is not running. Start Docker Desktop and try again." fi DOCKER_VER=$(docker --version | grep -oE '[0-9]+\.[0-9]+' | head -1) info "Docker $DOCKER_VER detected" # ─── Step 4: Create install directory ─── mkdir -p "$SQUADEM_DIR" cd "$SQUADEM_DIR" info "Installing to $SQUADEM_DIR" # ─── Step 4b: AI Gateway mode setup (interactive, Linux only) ─── _TRANSPARENT_MODE="" _LAN_IFACE="" _WAN_IFACE="" _DNS_PORT_OVERRIDE="" _HOST_IP_OVERRIDE="" # Ask on fresh install OR when --setup-gateway flag is passed if { [ ! -f .env ] || [ "$FORCE_GATEWAY_SETUP" = "true" ]; } && [ "$OS" = "Linux" ]; then _NIC_COUNT=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -vE '^lo$|^docker|^veth|^br-' | wc -l) echo "" echo " ${CYAN}${BOLD}═══ AI Gateway Mode ═══${NC}" echo "" echo " ${BOLD}1)${NC} Standard (Modes 1-2) — AI Proxy & Reverse Proxy" echo " Point apps at Squadem. No network changes needed." echo "" echo " ${BOLD}2)${NC} DNS Interception (Mode 3) — Network-wide AI governance" echo " Squadem runs a DNS server on port 53. Set your router's DNS" echo " to this machine's IP and all AI traffic is intercepted." echo " No per-device config needed." echo "" if [ "$_NIC_COUNT" -ge 2 ]; then echo " ${BOLD}3)${NC} Transparent Router (Mode 4) — Full router replacement" echo " Squadem replaces your router with 2 NICs. All traffic flows" echo " through it. DHCP, DNS, NAT managed automatically." echo " ${BOLD}Requires:${NC} 2 NICs (WAN + LAN)." echo "" printf " Select mode [${BOLD}1${NC}/2/3]: " else printf " Select mode [${BOLD}1${NC}/2]: " fi read -r _MODE_ANSWER < /dev/tty echo "" # ── Mode 3: DNS Interception ── if [ "$_MODE_ANSWER" = "2" ]; then _TRANSPARENT_MODE="dns" _DNS_PORT_OVERRIDE="53" # Detect host IP for DNS responses _HOST_IP=$(ip -4 route get 8.8.8.8 2>/dev/null | grep -oP 'src \K[0-9.]+' | head -1) if [ -n "$_HOST_IP" ]; then echo " Detected host IP: ${BOLD}${_HOST_IP}${NC}" printf " Use this as the DNS redirect IP? (Y/n): " read -r _IP_CONFIRM < /dev/tty if [ "$_IP_CONFIRM" = "n" ] || [ "$_IP_CONFIRM" = "N" ]; then printf " Enter the IP address for DNS responses: " read -r _HOST_IP < /dev/tty fi else printf " Enter this machine's LAN IP (for DNS responses): " read -r _HOST_IP < /dev/tty fi _HOST_IP_OVERRIDE="$_HOST_IP" echo "" info "DNS mode: port 53, redirect IP=$_HOST_IP" # Free port 53 from systemd-resolved if systemctl is-active --quiet systemd-resolved 2>/dev/null; then _STUB_ACTIVE=$(ss -tlnp 2>/dev/null | grep ':53 ' | grep -c 'resolved' || true) if [ "$_STUB_ACTIVE" -gt 0 ] 2>/dev/null || ss -tlnp 2>/dev/null | grep -q '127.0.0.53:53'; then echo "" echo " ${YELLOW}systemd-resolved is using port 53.${NC}" echo " Squadem needs port 53 for DNS interception." printf " Disable the DNS stub listener? (Y/n): " read -r _DNS_ANSWER < /dev/tty if [ "$_DNS_ANSWER" != "n" ] && [ "$_DNS_ANSWER" != "N" ]; then info "Configuring systemd-resolved..." sudo mkdir -p /etc/systemd/resolved.conf.d printf "[Resolve]\nDNSStubListener=no\n" | sudo tee /etc/systemd/resolved.conf.d/no-stub.conf >/dev/null sudo systemctl restart systemd-resolved info "Port 53 freed for Squadem DNS server" else warn "Port 53 still in use — DNS mode may fail to start. Free it manually later." fi fi fi # ── Mode 4: Transparent Router ── elif [ "$_MODE_ANSWER" = "3" ] && [ "$_NIC_COUNT" -ge 2 ]; then _TRANSPARENT_MODE="router" _DNS_PORT_OVERRIDE="53" echo " ${BOLD}Available network interfaces:${NC}" echo "" _IDX=1 _IFACES="" for _iface in $(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -vE '^lo$|^docker|^veth|^br-'); do _ip=$(ip -4 addr show "$_iface" 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1) _state=$(ip -o link show "$_iface" 2>/dev/null | grep -oE 'state [A-Z]+' | awk '{print $2}') if [ -n "$_ip" ]; then echo " ${BOLD}${_IDX})${NC} $_iface — $_ip (${_state})" else echo " ${BOLD}${_IDX})${NC} $_iface — no IP (${_state})" fi _IFACES="${_IFACES} ${_iface}" _IDX=$((_IDX + 1)) done echo "" printf " Which interface has ${BOLD}internet access${NC} (WAN)? Enter name: " read -r _WAN_IFACE < /dev/tty printf " Which interface connects to your ${BOLD}router/switch${NC} (LAN)? Enter name: " read -r _LAN_IFACE < /dev/tty echo "" if [ -n "$_WAN_IFACE" ] && [ -n "$_LAN_IFACE" ]; then info "Transparent mode: WAN=$_WAN_IFACE, LAN=$_LAN_IFACE" # Free port 53 if systemd-resolved is running if systemctl is-active --quiet systemd-resolved 2>/dev/null; then echo "" echo " ${YELLOW}systemd-resolved is using port 53.${NC}" echo " Squadem needs port 53 for its built-in DNS server." printf " Disable systemd-resolved's DNS stub listener? (Y/n): " read -r _DNS_ANSWER < /dev/tty if [ "$_DNS_ANSWER" != "n" ] && [ "$_DNS_ANSWER" != "N" ]; then info "Configuring systemd-resolved..." sudo mkdir -p /etc/systemd/resolved.conf.d printf "[Resolve]\nDNSStubListener=no\n" | sudo tee /etc/systemd/resolved.conf.d/no-stub.conf >/dev/null sudo systemctl restart systemd-resolved info "Port 53 freed for Squadem DNS server" fi fi else warn "Incomplete interface selection — skipping transparent mode" _TRANSPARENT_MODE="" fi fi fi # ─── Step 5: Generate .env ─── FRESH_ENV=false if [ ! -f .env ]; then FRESH_ENV=true info "Generating configuration..." gen_password() { openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c "$1"; } PG_PASS=$(gen_password 24) KC_PASS=$(gen_password 16) N8N_PASS=$(gen_password 16) MINIO_PASS=$(gen_password 24) cat > .env << ENVEOF # Squadem Configuration (auto-generated) SQUADEM_VERSION=${SQUADEM_VERSION} SQUADEM_LICENSE_KEY=${SQUADEM_LICENSE_KEY} # Active plugins (managed from the dashboard — comma-separated profile names) # Available: rag, quality, router, meetings, sso, automation, storage # SSO and quality are installed by default (core plugins). COMPOSE_PROFILES=sso,quality # Database (only used by plugins that require PostgreSQL) POSTGRES_USER=squadem_user POSTGRES_PASSWORD=${PG_PASS} POSTGRES_DB=squadem_db # SSO (Keycloak — requires sso plugin) KEYCLOAK_ADMIN_PASSWORD=${KC_PASS} # n8n Workflow Engine (requires automation plugin) N8N_BASIC_AUTH_USER=admin N8N_BASIC_AUTH_PASSWORD=${N8N_PASS} # Object Storage (MinIO — requires storage plugin) MINIO_ROOT_USER=squadem MINIO_ROOT_PASSWORD=${MINIO_PASS} # External AI Keys (optional — also configurable from the dashboard) OPENAI_API_KEY= ANTHROPIC_API_KEY= # Bot Registry (public default — override for self-hosted registry) BOT_REGISTRY_URL=https://registry.squadem.bot # Meetings plugin DIARIZATION_BACKEND=auto HF_TOKEN= # AI Gateway — DNS interception port (Mode 3) # Default 10053 avoids conflicts with system DNS; set to 53 on a dedicated Linux server SQUADEM_DNS_PORT=${_DNS_PORT_OVERRIDE:-10053} # AI Gateway — host IP for DNS mode (Mode 3) # Required in Docker bridge mode so AI domain DNS responses point to the host. # Set to this machine's LAN IP (e.g. 192.168.1.100). Leave blank for host networking. SQUADEM_HOST_IP=${_HOST_IP_OVERRIDE:-} # AI Gateway — Transparent proxy mode (Mode 4: Router Replacement) # Set to "router" to enable host networking + auto NAT/DHCP/DNS. # Requires a dedicated Linux machine with 2 NICs (WAN + LAN). # Leave empty for standard proxy modes (1-3). SQUADEM_TRANSPARENT_MODE=${_TRANSPARENT_MODE:-} # AI Gateway — LAN interface for DHCP/NAT in Mode 4 SQUADEM_LAN_INTERFACE=${_LAN_IFACE:-} # AI Gateway — WAN interface for internet uplink in Mode 4 SQUADEM_WAN_INTERFACE=${_WAN_IFACE:-} # GPU / Apple Silicon (auto-detected) HOST_MEMORY_MB= APPLE_CHIP_NAME= ENVEOF # Auto-detect Apple Silicon host memory and chip name if [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "arm64" ]; then _MEM_BYTES=$(sysctl -n hw.memsize 2>/dev/null || echo 0) _MEM_MB=$(( _MEM_BYTES / 1024 / 1024 )) _CHIP=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "") if [ "$_MEM_MB" -gt 0 ]; then sed -i.bak "s|^HOST_MEMORY_MB=.*|HOST_MEMORY_MB=${_MEM_MB}|" .env && rm -f .env.bak fi if [ -n "$_CHIP" ]; then sed -i.bak "s|^APPLE_CHIP_NAME=.*|APPLE_CHIP_NAME=${_CHIP}|" .env && rm -f .env.bak fi info "Detected ${_CHIP} with $(( _MEM_MB / 1024 )) GB unified memory" fi info "Generated .env" else # Ensure license key is saved in existing .env if ! grep -q "^SQUADEM_LICENSE_KEY=" .env 2>/dev/null; then echo "SQUADEM_LICENSE_KEY=${SQUADEM_LICENSE_KEY}" >> .env else sed -i.bak "s|^SQUADEM_LICENSE_KEY=.*|SQUADEM_LICENSE_KEY=${SQUADEM_LICENSE_KEY}|" .env && rm -f .env.bak fi gen_password() { openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c "$1"; } # Backfill variables that may have been added after the original .env was created if ! grep -q "^MINIO_ROOT_USER=" .env 2>/dev/null; then printf "\n# Object Storage (MinIO — requires storage plugin)\nMINIO_ROOT_USER=squadem\nMINIO_ROOT_PASSWORD=%s\n" "$(gen_password 24)" >> .env info "Added MinIO credentials to .env" elif ! grep -q "^MINIO_ROOT_PASSWORD=" .env 2>/dev/null; then sed -i.bak "/^MINIO_ROOT_USER=/a\\ MINIO_ROOT_PASSWORD=$(gen_password 24)" .env && rm -f .env.bak info "Added MinIO password to .env" fi if ! grep -q "^BOT_REGISTRY_URL=" .env 2>/dev/null; then echo "BOT_REGISTRY_URL=https://registry.squadem.bot" >> .env fi if ! grep -q "^SQUADEM_DNS_PORT=" .env 2>/dev/null; then printf "\n# AI Gateway — DNS interception port (Mode 3)\nSQUADEM_DNS_PORT=10053\n" >> .env info "Added DNS port config to .env" fi if ! grep -q "^SQUADEM_TRANSPARENT_MODE=" .env 2>/dev/null; then printf "\n# AI Gateway — Transparent proxy mode (Mode 4: Router Replacement)\nSQUADEM_TRANSPARENT_MODE=\nSQUADEM_LAN_INTERFACE=\nSQUADEM_WAN_INTERFACE=\n" >> .env info "Added transparent mode config to .env" fi # Apply --setup-gateway selections to existing .env if [ "$FORCE_GATEWAY_SETUP" = "true" ]; then if [ -n "$_TRANSPARENT_MODE" ]; then sed -i.bak "s|^SQUADEM_TRANSPARENT_MODE=.*|SQUADEM_TRANSPARENT_MODE=${_TRANSPARENT_MODE}|" .env && rm -f .env.bak sed -i.bak "s|^SQUADEM_LAN_INTERFACE=.*|SQUADEM_LAN_INTERFACE=${_LAN_IFACE}|" .env && rm -f .env.bak sed -i.bak "s|^SQUADEM_WAN_INTERFACE=.*|SQUADEM_WAN_INTERFACE=${_WAN_IFACE}|" .env && rm -f .env.bak info "Updated gateway mode to: ${_TRANSPARENT_MODE}" else sed -i.bak "s|^SQUADEM_TRANSPARENT_MODE=.*|SQUADEM_TRANSPARENT_MODE=|" .env && rm -f .env.bak sed -i.bak "s|^SQUADEM_LAN_INTERFACE=.*|SQUADEM_LAN_INTERFACE=|" .env && rm -f .env.bak sed -i.bak "s|^SQUADEM_WAN_INTERFACE=.*|SQUADEM_WAN_INTERFACE=|" .env && rm -f .env.bak info "Reset to standard mode (Modes 1-2)" fi # Update DNS port and host IP if [ -n "$_DNS_PORT_OVERRIDE" ]; then sed -i.bak "s|^SQUADEM_DNS_PORT=.*|SQUADEM_DNS_PORT=${_DNS_PORT_OVERRIDE}|" .env && rm -f .env.bak info "Set DNS port to ${_DNS_PORT_OVERRIDE}" fi if [ -n "$_HOST_IP_OVERRIDE" ]; then if grep -q "^SQUADEM_HOST_IP=" .env 2>/dev/null; then sed -i.bak "s|^SQUADEM_HOST_IP=.*|SQUADEM_HOST_IP=${_HOST_IP_OVERRIDE}|" .env && rm -f .env.bak else printf "\n# AI Gateway — host IP for DNS responses\nSQUADEM_HOST_IP=%s\n" "$_HOST_IP_OVERRIDE" >> .env fi info "Set host IP to ${_HOST_IP_OVERRIDE}" fi fi warn ".env already exists -- keeping existing configuration (license key updated)" fi # ─── Step 6: Download compose file ─── info "Downloading Squadem ${SQUADEM_VERSION}..." if command -v curl >/dev/null 2>&1; then curl -fsSL "$COMPOSE_URL" -o docker-compose.yml elif command -v wget >/dev/null 2>&1; then wget -q "$COMPOSE_URL" -O docker-compose.yml else error "curl or wget is required to download Squadem" fi # ─── Step 6b: Generate host-networking override for Mode 4 ─── TRANSPARENT_MODE=$(grep -E '^SQUADEM_TRANSPARENT_MODE=' .env 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true) if [ "$TRANSPARENT_MODE" = "router" ]; then LAN_IFACE=$(grep -E '^SQUADEM_LAN_INTERFACE=' .env 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true) WAN_IFACE=$(grep -E '^SQUADEM_WAN_INTERFACE=' .env 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true) info "Mode 4 (Router Replacement) enabled — generating host-networking override..." info " WAN: ${WAN_IFACE:-auto} LAN: ${LAN_IFACE:-auto}" cat > docker-compose.override.yml << 'OVERRIDEEOF' # Auto-generated for Mode 4 (transparent router). # squadem-core runs with host networking so it can manage interfaces, # iptables, DHCP, and DNS directly on the host network stack. # With host networking, Docker DNS names are unavailable — all services # are reached via localhost on their exposed host ports. services: squadem-core: network_mode: host privileged: true environment: SQUADEM_LICENSE_KEY: ${SQUADEM_LICENSE_KEY:-} GPU_AGENT_URL: http://localhost:9400 KEYCLOAK_URL: http://localhost:8443/auth PLUGIN_POSTGRES_HOST: localhost PLUGIN_POSTGRES_PORT: "5433" ROUTER_SERVICE_URL: http://localhost:8085 DATA_QUALITY_SERVICE_URL: http://localhost:8088 MEETING_SERVICE_URL: http://localhost:8090 RAG_SERVICE_URL: http://localhost:8084 TRAINING_SERVICE_URL: http://localhost:8086 INFERENCE_SERVICE_URL: http://localhost:8087 EVENT_BRIDGE_SERVICE_URL: http://localhost:8092 MINIO_ENDPOINT: http://localhost:9000 OVERRIDEEOF info "Generated docker-compose.override.yml (host networking)" # Free port 53 on existing installs if systemd-resolved is active if systemctl is-active --quiet systemd-resolved 2>/dev/null; then if [ ! -f /etc/systemd/resolved.conf.d/no-stub.conf ]; then info "Freeing port 53 (disabling systemd-resolved stub listener)..." sudo mkdir -p /etc/systemd/resolved.conf.d printf "[Resolve]\nDNSStubListener=no\n" | sudo tee /etc/systemd/resolved.conf.d/no-stub.conf >/dev/null sudo systemctl restart systemd-resolved fi fi else # Remove override if transparent mode was disabled if [ -f docker-compose.override.yml ]; then if grep -q "Mode 4" docker-compose.override.yml 2>/dev/null; then rm -f docker-compose.override.yml info "Removed Mode 4 override (transparent mode disabled)" fi fi fi # ─── Step 7: Clean up stale state on fresh install ─── if [ "$FRESH_ENV" = "true" ]; then docker compose --project-name squadem down -v 2>/dev/null || true fi # ─── Step 8: Detect active plugins and pull ─── ACTIVE_PROFILES=$(grep -E '^COMPOSE_PROFILES=' .env 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || true) if [ "$FRESH_ENV" = "true" ]; then info "Fresh install — pulling core images..." docker compose --project-name squadem pull 2>&1 | grep -E "Pull|pull|Downloaded|Error" || true else info "Updating core services..." docker compose --project-name squadem pull squadem-core gpu-agent 2>&1 | grep -E "Pull|pull|Downloaded|Error" || true if [ -n "$ACTIVE_PROFILES" ]; then info "Active plugins: ${ACTIVE_PROFILES}" info "Updating plugin images..." # docker compose respects COMPOSE_PROFILES from .env, pulling only active plugin services docker compose --project-name squadem pull 2>&1 | grep -E "Pull|pull|Downloaded|Error" || true else info "No plugins enabled — to activate plugins, open the dashboard → Plugins" fi fi # Remove orphaned containers whose names conflict with compose-managed services. # This handles containers created manually or by a previous non-compose install. for _cname in squadem-core gpu-agent; do _existing=$(docker inspect "$_cname" --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null || true) if [ -n "$_existing" ] && [ "$_existing" != "squadem" ]; then warn "Removing orphaned container $_cname (not managed by compose)..." docker rm -f "$_cname" >/dev/null 2>&1 || true elif docker inspect "$_cname" >/dev/null 2>&1 && [ -z "$_existing" ]; then warn "Removing orphaned container $_cname (not managed by compose)..." docker rm -f "$_cname" >/dev/null 2>&1 || true fi done info "Starting Squadem..." docker compose --project-name squadem up -d 2>&1 | grep -v "^$" # ─── Step 9: Wait for health ─── info "Waiting for Squadem to be ready..." TIMEOUT=120 ELAPSED=0 READY=false while [ $ELAPSED -lt $TIMEOUT ]; do DASH_OK=$(curl -sf http://localhost:8200/health >/dev/null 2>&1 && echo "1" || echo "0") PROXY_OK=$(curl -sf http://localhost:8080/health >/dev/null 2>&1 && echo "1" || echo "0") if [ "$DASH_OK" = "1" ] && [ "$PROXY_OK" = "1" ]; then READY=true break fi sleep 3 ELAPSED=$((ELAPSED + 3)) printf "\r${GREEN}[squadem]${NC} Waiting... %ds / %ds" "$ELAPSED" "$TIMEOUT" done echo "" if [ "$READY" = "true" ]; then info "Squadem is healthy!" else warn "Squadem may still be starting. Check: docker compose logs -f" fi # ─── Track install/update event (silent, non-blocking) ─── TRACK_EVENT="install" if [ "$FRESH_ENV" != "true" ]; then TRACK_EVENT="update" fi curl -s -X POST "${LICENSE_API}/track" \ -H "Content-Type: application/json" \ -d "{\"event\":\"${TRACK_EVENT}\",\"platform\":\"${OS}-${ARCH}\",\"profiles\":\"${ACTIVE_PROFILES}\"}" \ >/dev/null 2>&1 || true # ─── Done ─── echo "" echo "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" if [ "$FRESH_ENV" = "true" ]; then echo " ${GREEN}${BOLD}Squadem is running!${NC} (${LICENSE_TIER} plan)" else echo " ${GREEN}${BOLD}Squadem updated successfully!${NC} (${LICENSE_TIER} plan)" if [ -n "$ACTIVE_PROFILES" ]; then echo " ${BOLD}Updated plugins:${NC} ${ACTIVE_PROFILES}" fi fi echo "" DNS_PORT=$(grep -E '^SQUADEM_DNS_PORT=' .env 2>/dev/null | cut -d'=' -f2 | tr -d '"' || echo "10053") TRANSPARENT_MODE_DISP=$(grep -E '^SQUADEM_TRANSPARENT_MODE=' .env 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true) echo " ${BOLD}Dashboard${NC} http://localhost:8200" echo " ${BOLD}AI Proxy${NC} http://localhost:8080" echo " ${BOLD}API${NC} http://localhost:8081" echo " ${BOLD}DNS Server${NC} localhost:${DNS_PORT} (UDP/TCP)" echo " ${BOLD}Plugin Manager${NC} http://localhost:8093" if [ "$TRANSPARENT_MODE_DISP" = "router" ]; then echo "" echo " ${BOLD}${YELLOW}Mode 4 (Router Replacement) active${NC}" echo " ${BOLD}DHCP/DNS/NAT${NC} Managed automatically via Settings → AI Gateway" echo " ${BOLD}Networking${NC} Host mode (privileged)" elif [ "$TRANSPARENT_MODE_DISP" = "dns" ]; then echo "" echo " ${BOLD}${YELLOW}Mode 3 (DNS Interception) active${NC}" HOST_IP_DISP=$(grep -E '^SQUADEM_HOST_IP=' .env 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true) echo " ${BOLD}DNS Server${NC} port 53 — set your router's DNS to ${HOST_IP_DISP:-this machine}" fi echo "" echo " ${BOLD}License${NC} ${SQUADEM_LICENSE_KEY}" echo " ${BOLD}Config${NC} $SQUADEM_DIR/.env" echo "" if [ "$FRESH_ENV" = "true" ]; then echo " ${BOLD}${YELLOW}Default Login${NC}" echo " ${BOLD}Username:${NC} admin@squadem.local" echo " ${BOLD}Password:${NC} admin" echo " ${YELLOW}Please change the default password after first login.${NC}" echo "" echo " ${BOLD}Core Plugins:${NC} SSO (Keycloak) and Data Quality are pre-installed." echo " ${BOLD}More Plugins:${NC} Open the dashboard → ${BOLD}Plugins${NC} to install" echo " RAG, Meetings, Automation, and more." echo "" fi echo " ${BOLD}Commands:${NC}" echo " cd $SQUADEM_DIR" echo " docker compose --project-name squadem ps # service status" echo " docker compose --project-name squadem logs -f # live logs" echo " docker compose --project-name squadem down # stop all" echo " curl -fsSL https://get.squadem.com/install.sh | sh # update" echo " curl -fsSL https://get.squadem.com/install.sh | sh -s -- --setup-gateway # reconfigure gateway" echo "" echo "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo ""