#!/bin/sh # Squadem Installer — free to self-host for everyone # # Squadem is open-core: the full platform is free forever. # Enterprise adds SSO, SQ-Xray, and dedicated support. # Visit https://squadem.com/enterprise for details. # # Two components ship from the same release; pick what this host should run: # # • core — Squadem control plane (dashboard, AI proxy, plugin # manager, in-process Agent IDE / GPU manager). This is # what you install on the box that operators log into. # # • gpu-agent — Headless companion that turns a remote NVIDIA / Apple # Silicon machine into a GPU node for an existing core. # No dashboard, no license prompt — it joins your core # with a registration token. # # Selection order: --component= flag → SQUADEM_COMPONENT env → # interactive prompt → defaults to "core". # # Examples: # # Core, native binary (interactive license prompt) # curl -fsSL https://get.squadem.com/install.sh | sh # # # Core, with license key (scripted / CI) # curl -fsSL https://get.squadem.com/install.sh | \ # SQUADEM_LICENSE_KEY=SQD-XXXX-XXXX-XXXX sh # # # Core, dockerized # curl -fsSL https://get.squadem.com/install.sh | \ # SQUADEM_LICENSE_KEY=SQD-XXXX-XXXX-XXXX sh -s -- --mode=docker # # # GPU agent on a remote box (interactive prompts) # curl -fsSL https://get.squadem.com/install.sh | \ # SQUADEM_LICENSE_KEY=SQD-XXXX-XXXX-XXXX sh -s -- --gpu-agent # # # GPU agent, fully scripted (no prompts) # curl -fsSL https://get.squadem.com/install.sh | \ # SQUADEM_LICENSE_KEY=SQD-XXXX-XXXX-XXXX \ # SQUADEM_CENTRAL_URL=http://core.lan:8081 \ # SQUADEM_AGENT_TOKEN=sqd-... sh -s -- --gpu-agent # # Plugins (RAG, meetings, training, automation, SSO, adapters) ship as # containers but are managed by core. After install, open the dashboard # → Plugins, or run: squadem compose extract && docker compose --profile rag up -d # # Override knobs (env vars): # SQUADEM_COMPONENT core | gpu-agent (default: prompt, then core) # SQUADEM_VERSION release tag to pin (default: latest) # SQUADEM_DIR install dir (default: ~/.squadem) # SQUADEM_LICENSE_KEY core only — license key (free at squadem.com/sign-up) # SQUADEM_CENTRAL_URL gpu-agent only — URL of the core (e.g. http://core:8081) # SQUADEM_AGENT_TOKEN gpu-agent only — registration token from core dashboard # SQUADEM_AGENT_PORT gpu-agent only — listening port (default: 9400) # (Binaries are fetched via signed S3 URLs after license validation) # SQUADEM_DOCKER_IMAGE core+docker only — image (default squadem/squadem-core:) # SQUADEM_MODE core only — binary | docker (default: binary) set -e SQUADEM_VERSION="${SQUADEM_VERSION:-latest}" SQUADEM_DIR="${SQUADEM_DIR:-$HOME/.squadem}" SQUADEM_DOCKER_IMAGE="${SQUADEM_DOCKER_IMAGE:-squadem/squadem-core:${SQUADEM_VERSION}}" LICENSE_API="https://squadem.com/api/license" # License key is required for downloads (free at squadem.com/sign-up) SQUADEM_LICENSE_KEY="${SQUADEM_LICENSE_KEY:-}" COMPONENT="${SQUADEM_COMPONENT:-}" MODE="${SQUADEM_MODE:-binary}" for arg in "$@"; do case "$arg" in --component=core|--core) COMPONENT="core" ;; --component=gpu-agent|--gpu-agent) COMPONENT="gpu-agent" ;; --update) COMPONENT="update" ;; --uninstall) COMPONENT="uninstall" ;; --restart) COMPONENT="restart" ;; --logs) COMPONENT="logs" ;; --kill) COMPONENT="kill" ;; --mode=binary|--binary) MODE="binary" ;; --mode=docker|--docker) MODE="docker" ;; -h|--help) # Dump the leading comment block. Stop at the first non-comment # line so the help text auto-grows when we extend the header. awk 'NR==1{next} /^[^#]/{exit} {sub(/^# ?/,""); print}' "$0" exit 0 ;; esac done # ── Pretty output ── # We resolve the ESC byte once via printf so the color vars hold a # real 0x1B character, not the literal four-byte string "\033". This # makes `cat <&2; exit 1; } # ── License-gated download URLs ──────────────────────────────────── # All installations require a valid license key (free or enterprise). # We call the license server to validate the key and get signed # download URLs. This ensures binaries are not publicly accessible. DOWNLOAD_URLS_JSON="" fetch_download_urls() { local component="$1" local platform="$2" if [ -z "$SQUADEM_LICENSE_KEY" ]; then if [ -r /dev/tty ]; then echo "" echo " ${BOLD}A license key is required to install Squadem.${NC}" echo " Enter your license key (get one free at https://squadem.com/sign-up)" echo "" printf " License key: " read -r SQUADEM_LICENSE_KEY < /dev/tty || SQUADEM_LICENSE_KEY="" echo "" fi if [ -z "$SQUADEM_LICENSE_KEY" ]; then error "License key required. Set SQUADEM_LICENSE_KEY or register free at https://squadem.com/sign-up" fi fi info "Validating license and fetching download URLs..." local payload payload=$(cat </dev/null 2>&1; then response=$(curl -fsSL -X POST \ -H "Content-Type: application/json" \ -d "$payload" \ "${LICENSE_API}/download-urls" 2>&1) || { # Try to extract error message from response if echo "$response" | grep -q '"error"'; then local err_msg err_msg=$(echo "$response" | sed 's/.*"error":"\([^"]*\)".*/\1/') error "License validation failed: $err_msg" else error "Failed to contact license server. Check your internet connection." fi } elif command -v wget >/dev/null 2>&1; then response=$(wget -qO- --post-data="$payload" \ --header="Content-Type: application/json" \ "${LICENSE_API}/download-urls" 2>&1) || { error "Failed to contact license server. Check your internet connection." } else error "curl or wget is required" fi # Check for error in response if echo "$response" | grep -q '"error"'; then local err_msg err_msg=$(echo "$response" | sed 's/.*"error":"\([^"]*\)".*/\1/') error "License validation failed: $err_msg" fi DOWNLOAD_URLS_JSON="$response" # Extract resolved version from response RESOLVED_VERSION=$(echo "$response" | grep -o '"version":"[^"]*"' | cut -d'"' -f4 || echo "") if [ -n "$RESOLVED_VERSION" ] && [ "$SQUADEM_VERSION" = "latest" ]; then SQUADEM_VERSION="$RESOLVED_VERSION" fi info "License validated - downloading ${SQUADEM_VERSION}" } # Extract URL from JSON response (simple grep-based parsing for POSIX sh) get_download_url() { local filename="$1" echo "$DOWNLOAD_URLS_JSON" | grep -o "\"$filename\":\"[^\"]*\"" | sed 's/.*:"\([^"]*\)"/\1/' | sed 's/\\u0026/\&/g' } # ── Checksum verification ───────────────────────────────────────── # verify_checksum # # Aborts unless the file matches its published SHA256. Every failure to # verify is fatal, including a missing hashing tool, an unreachable # SHA256SUMS, or a missing entry for this artifact. Previously each of # those merely printed a warning and installed the binary anyway, which # meant an attacker who could tamper with the download only had to also # suppress SHA256SUMS to defeat the check entirely. # # Set SQUADEM_ALLOW_UNVERIFIED=1 to install without verification. That is # only appropriate when you have already established the file's integrity # by other means. verify_checksum() { local file="$1" local name="$2" local sha_tool="" if command -v sha256sum >/dev/null 2>&1; then sha_tool="sha256sum" elif command -v shasum >/dev/null 2>&1; then sha_tool="shasum -a 256" fi if [ "${SQUADEM_ALLOW_UNVERIFIED:-0}" = "1" ]; then warn "SQUADEM_ALLOW_UNVERIFIED=1 — installing ${name} without checksum verification" return 0 fi if [ -z "$sha_tool" ]; then error "Cannot verify ${name}: neither sha256sum nor shasum is available. Install one, or set SQUADEM_ALLOW_UNVERIFIED=1 to override." fi local sums_url sums_url=$(get_download_url "SHA256SUMS") if [ -z "$sums_url" ]; then error "Cannot verify ${name}: no SHA256SUMS URL was returned for this license. Set SQUADEM_ALLOW_UNVERIFIED=1 to override." fi local sums_tmp sums_tmp="$(mktemp)" if ! curl -fsSL "$sums_url" -o "$sums_tmp" 2>/dev/null; then rm -f "$sums_tmp" error "Cannot verify ${name}: SHA256SUMS could not be downloaded. Set SQUADEM_ALLOW_UNVERIFIED=1 to override." fi local expected expected=$(grep " ${name}\$" "$sums_tmp" | awk '{print $1}' | head -1) rm -f "$sums_tmp" if [ -z "$expected" ]; then error "Cannot verify ${name}: SHA256SUMS has no entry for it. Set SQUADEM_ALLOW_UNVERIFIED=1 to override." fi local actual actual=$($sha_tool "$file" | awk '{print $1}') if [ "$expected" != "$actual" ]; then rm -f "$file" error "SHA256 mismatch for ${name} — refusing to install. Expected $expected, got $actual" fi info "Checksum verified: ${name}" } cat </dev/null 2>&1; then termux-wake-lock 2>/dev/null || true info "Acquired Termux wake lock (release with termux-wake-unlock)" else warn "termux-wake-lock not found — Android may kill Squadem when the screen turns off" fi SQUADEM_ENV_FILE="$_asc_dir/.env" nohup "$_asc_bin" --data-dir="$_asc_data" \ --enable-sandbox=false --enable-local-gpu=false \ > "$_asc_dir/squadem.log" 2>&1 & ANDROID_START_PID=$! } # Drop the wake lock on stop/uninstall. Leaving it held means the phone # keeps paying for a daemon that is no longer running. android_release_wake_lock() { if command -v termux-wake-unlock >/dev/null 2>&1; then termux-wake-unlock 2>/dev/null || true info "Released Termux wake lock" fi } # ── Component selection ─────────────────────────────────────────── # We ask up-front because the rest of the script (license validation, # binary URL, service unit, post-install message) all branches off this # choice. When piped from `curl … | sh` we still get an interactive # prompt by reading directly from /dev/tty (same trick used for the # license key below). If /dev/tty is unavailable we silently fall back # to "core" — that's the 99% case for unattended provisioning runs. if [ -z "$COMPONENT" ]; then if [ -r /dev/tty ]; then echo "" echo " ${BOLD}What would you like to do?${NC}" echo " 1) install — install Squadem core" echo " 2) gpu-agent — install remote GPU node that joins an existing core" echo " 3) update — update existing Squadem to the latest version" echo " 4) restart — restart the running Squadem binary" echo " 5) logs — tail live logs of the running Squadem service" echo " 6) kill — stop all running Squadem processes" echo " 7) uninstall — stop and remove Squadem from this machine" echo "" printf " Selection [1]: " _ans="" read -r _ans < /dev/tty || _ans="" case "$_ans" in 2|gpu-agent|gpu) COMPONENT="gpu-agent" ;; 3|update) COMPONENT="update" ;; 4|restart) COMPONENT="restart" ;; 5|logs) COMPONENT="logs" ;; 6|kill|stop) COMPONENT="kill" ;; 7|uninstall) COMPONENT="uninstall" ;; *) COMPONENT="core" ;; esac echo "" else COMPONENT="core" fi fi # ── Handle uninstall ────────────────────────────────────────────── if [ "$COMPONENT" = "uninstall" ]; then echo "" info "Stopping Squadem services..." # Stop core service if [ "$IS_ANDROID" = "true" ]; then android_release_wake_lock elif [ "$(uname)" = "Darwin" ]; then launchctl unload "$HOME/Library/LaunchAgents/com.squadem.core.plist" 2>/dev/null || true launchctl unload "$HOME/Library/LaunchAgents/com.squadem.gpu-agent.plist" 2>/dev/null || true else systemctl --user stop squadem.service 2>/dev/null || true systemctl --user disable squadem.service 2>/dev/null || true systemctl --user stop sq-gpu-agent.service 2>/dev/null || true systemctl --user disable sq-gpu-agent.service 2>/dev/null || true fi # Kill any running processes pkill -9 -x squadem 2>/dev/null || true pkill -9 -x sq-gpu-agent 2>/dev/null || true # Remove files info "Removing Squadem files..." rm -rf "${SQUADEM_DIR:-$HOME/.squadem}" rm -f "$HOME/Library/LaunchAgents/com.squadem.core.plist" 2>/dev/null rm -f "$HOME/Library/LaunchAgents/com.squadem.gpu-agent.plist" 2>/dev/null rm -f "$HOME/.config/systemd/user/squadem.service" 2>/dev/null rm -f "$HOME/.config/systemd/user/sq-gpu-agent.service" 2>/dev/null # Symlinks the installer may have placed on PATH. Left behind they are # dangling links that shadow a later reinstall. rm -f /usr/local/bin/sq /usr/local/bin/squadem 2>/dev/null || true if [ -n "${PREFIX:-}" ]; then rm -f "$PREFIX/bin/sq" "$PREFIX/bin/squadem" 2>/dev/null || true fi if [ "$(uname)" != "Darwin" ] && [ "$IS_ANDROID" != "true" ]; then systemctl --user daemon-reload 2>/dev/null || true fi echo "" echo " ${GREEN}${BOLD}Squadem has been completely removed.${NC}" echo "" exit 0 fi # ── Handle restart ──────────────────────────────────────────────── if [ "$COMPONENT" = "restart" ]; then echo "" SQUADEM_DIR="${SQUADEM_DIR:-$HOME/.squadem}" if [ "$IS_ANDROID" = "true" ]; then # No service manager to delegate to, so do it by hand. Restarting a # daemon that is already gone still means "start it" here: on Android # the usual reason it is not running is that the phantom-process # reaper killed it, which is precisely when someone runs this. [ -x "$SQUADEM_DIR/bin/squadem" ] || error "Squadem is not installed at $SQUADEM_DIR/bin/squadem" if pgrep -x squadem >/dev/null 2>&1; then info "Restarting Squadem (no service manager on Android)..." pkill -x squadem 2>/dev/null || true sleep 1 pkill -9 -x squadem 2>/dev/null || true else warn "No running Squadem process found — starting it" fi android_start_core echo " ${GREEN}${BOLD}Squadem restarted (PID ${ANDROID_START_PID}).${NC}" elif [ "$(uname)" = "Darwin" ]; then # macOS — restart via launchctl if launchctl list 2>/dev/null | grep -q com.squadem.core; then info "Restarting Squadem core..." launchctl unload "$HOME/Library/LaunchAgents/com.squadem.core.plist" 2>/dev/null || true sleep 1 launchctl load "$HOME/Library/LaunchAgents/com.squadem.core.plist" 2>/dev/null || true echo " ${GREEN}${BOLD}Squadem core restarted.${NC}" elif launchctl list 2>/dev/null | grep -q com.squadem.gpu-agent; then info "Restarting Squadem GPU agent..." launchctl unload "$HOME/Library/LaunchAgents/com.squadem.gpu-agent.plist" 2>/dev/null || true sleep 1 launchctl load "$HOME/Library/LaunchAgents/com.squadem.gpu-agent.plist" 2>/dev/null || true echo " ${GREEN}${BOLD}Squadem GPU agent restarted.${NC}" else # No service registered — try killing and restarting manually if pgrep -x squadem >/dev/null 2>&1; then info "Restarting Squadem binary (no launchd service)..." pkill -x squadem 2>/dev/null || true sleep 1 nohup "$SQUADEM_DIR/bin/squadem" --data-dir="$SQUADEM_DIR/data" > "$SQUADEM_DIR/squadem.log" 2>&1 & echo " ${GREEN}${BOLD}Squadem restarted (PID $!).${NC}" elif pgrep -x sq-gpu-agent >/dev/null 2>&1; then info "Restarting GPU agent binary..." pkill -x sq-gpu-agent 2>/dev/null || true sleep 1 nohup "$SQUADEM_DIR/bin/sq-gpu-agent" > "$SQUADEM_DIR/sq-gpu-agent.log" 2>&1 & echo " ${GREEN}${BOLD}GPU agent restarted (PID $!).${NC}" else error "No running Squadem process found to restart." fi fi else # Linux — restart via systemd if systemctl --user is-active squadem.service >/dev/null 2>&1; then info "Restarting Squadem core..." systemctl --user restart squadem.service echo " ${GREEN}${BOLD}Squadem core restarted.${NC}" elif systemctl --user is-active sq-gpu-agent.service >/dev/null 2>&1; then info "Restarting Squadem GPU agent..." systemctl --user restart sq-gpu-agent.service echo " ${GREEN}${BOLD}Squadem GPU agent restarted.${NC}" else if pgrep -x squadem >/dev/null 2>&1; then info "Restarting Squadem binary (no systemd service)..." pkill -x squadem 2>/dev/null || true sleep 1 nohup "$SQUADEM_DIR/bin/squadem" --data-dir="$SQUADEM_DIR/data" > "$SQUADEM_DIR/squadem.log" 2>&1 & echo " ${GREEN}${BOLD}Squadem restarted (PID $!).${NC}" elif pgrep -x sq-gpu-agent >/dev/null 2>&1; then info "Restarting GPU agent binary..." pkill -x sq-gpu-agent 2>/dev/null || true sleep 1 nohup "$SQUADEM_DIR/bin/sq-gpu-agent" > "$SQUADEM_DIR/sq-gpu-agent.log" 2>&1 & echo " ${GREEN}${BOLD}GPU agent restarted (PID $!).${NC}" else error "No running Squadem process found to restart." fi fi fi echo "" exit 0 fi # ── Handle logs ─────────────────────────────────────────────────── if [ "$COMPONENT" = "logs" ]; then echo "" SQUADEM_DIR="${SQUADEM_DIR:-$HOME/.squadem}" if [ "$IS_ANDROID" = "true" ]; then # No journald on Android — the daemon's output goes to this file # because that is where android_start_core redirects it. [ -f "$SQUADEM_DIR/squadem.log" ] || error "No log file at $SQUADEM_DIR/squadem.log" info "Tailing ${SQUADEM_DIR}/squadem.log (Ctrl+C to stop)" echo "" tail -f "$SQUADEM_DIR/squadem.log" elif [ "$(uname)" = "Darwin" ]; then # macOS — check for log file or use launchd stdout if launchctl list 2>/dev/null | grep -q com.squadem.core; then LOG_FILE="$SQUADEM_DIR/squadem.log" elif launchctl list 2>/dev/null | grep -q com.squadem.gpu-agent; then LOG_FILE="$SQUADEM_DIR/sq-gpu-agent.log" elif [ -f "$SQUADEM_DIR/squadem.log" ]; then LOG_FILE="$SQUADEM_DIR/squadem.log" elif [ -f "$SQUADEM_DIR/sq-gpu-agent.log" ]; then LOG_FILE="$SQUADEM_DIR/sq-gpu-agent.log" else error "No Squadem log file found in $SQUADEM_DIR" fi info "Tailing ${LOG_FILE} (Ctrl+C to stop)" echo "" tail -f "$LOG_FILE" else # Linux — prefer journalctl for systemd services if systemctl --user is-active squadem.service >/dev/null 2>&1; then info "Tailing Squadem core logs (Ctrl+C to stop)" echo "" journalctl --user -u squadem -f --no-pager elif systemctl --user is-active sq-gpu-agent.service >/dev/null 2>&1; then info "Tailing GPU agent logs (Ctrl+C to stop)" echo "" journalctl --user -u sq-gpu-agent -f --no-pager elif [ -f "$SQUADEM_DIR/squadem.log" ]; then info "Tailing ${SQUADEM_DIR}/squadem.log (Ctrl+C to stop)" echo "" tail -f "$SQUADEM_DIR/squadem.log" elif [ -f "$SQUADEM_DIR/sq-gpu-agent.log" ]; then info "Tailing ${SQUADEM_DIR}/sq-gpu-agent.log (Ctrl+C to stop)" echo "" tail -f "$SQUADEM_DIR/sq-gpu-agent.log" else error "No Squadem service or log file found." fi fi exit 0 fi # ── Handle kill ─────────────────────────────────────────────────── if [ "$COMPONENT" = "kill" ]; then echo "" SQUADEM_DIR="${SQUADEM_DIR:-$HOME/.squadem}" killed_any=false if [ "$IS_ANDROID" = "true" ]; then # Nothing to unregister: the daemon is a plain background process, # so the pkill sweep below is the whole story. : elif [ "$(uname)" = "Darwin" ]; then # macOS — unload launchd services first, then kill any stray processes if launchctl list 2>/dev/null | grep -q com.squadem.core; then info "Stopping Squadem core service..." launchctl unload "$HOME/Library/LaunchAgents/com.squadem.core.plist" 2>/dev/null || true killed_any=true fi if launchctl list 2>/dev/null | grep -q com.squadem.gpu-agent; then info "Stopping Squadem GPU agent service..." launchctl unload "$HOME/Library/LaunchAgents/com.squadem.gpu-agent.plist" 2>/dev/null || true killed_any=true fi else # Linux — stop systemd services first if systemctl --user is-active squadem.service >/dev/null 2>&1; then info "Stopping Squadem core service..." systemctl --user stop squadem.service 2>/dev/null || true killed_any=true fi if systemctl --user is-active sq-gpu-agent.service >/dev/null 2>&1; then info "Stopping Squadem GPU agent service..." systemctl --user stop sq-gpu-agent.service 2>/dev/null || true killed_any=true fi fi # Kill any remaining Squadem processes if pgrep -x squadem >/dev/null 2>&1; then info "Killing squadem process..." pkill -9 -x squadem 2>/dev/null || true killed_any=true fi if pgrep -x sq-gpu-agent >/dev/null 2>&1; then info "Killing sq-gpu-agent process..." pkill -9 -x sq-gpu-agent 2>/dev/null || true killed_any=true fi # Also kill any MLX server processes spawned by Squadem if pgrep -f "mlx_lm.server" >/dev/null 2>&1; then info "Killing MLX server processes..." pkill -9 -f "mlx_lm.server" 2>/dev/null || true killed_any=true fi if [ "$IS_ANDROID" = "true" ]; then android_release_wake_lock fi if [ "$killed_any" = true ]; then echo "" echo " ${GREEN}${BOLD}All Squadem processes stopped.${NC}" else echo " ${YELLOW}No running Squadem processes found.${NC}" fi echo "" exit 0 fi # ── Handle update ───────────────────────────────────────────────── if [ "$COMPONENT" = "update" ]; then # Detect what's installed if [ -x "${SQUADEM_DIR:-$HOME/.squadem}/bin/squadem" ]; then COMPONENT="core" info "Updating Squadem core..." elif [ -x "${SQUADEM_DIR:-$HOME/.squadem}/bin/sq-gpu-agent" ]; then COMPONENT="gpu-agent" info "Updating Squadem GPU agent..." else error "No existing Squadem installation found in ${SQUADEM_DIR:-$HOME/.squadem}" fi fi case "$COMPONENT" in core|gpu-agent) ;; *) error "Unknown --component=${COMPONENT} (use core, gpu-agent, update, or uninstall)" ;; esac # `--mode=docker` only ships for core. Silently coerce gpu-agent back # to native binary so a user passing both flags doesn't get a confusing # "image not found" later. if [ "$COMPONENT" = "gpu-agent" ] && [ "$MODE" = "docker" ]; then warn "Docker mode is not supported for gpu-agent — falling back to native binary" MODE="binary" fi if [ "$COMPONENT" = "gpu-agent" ] && [ "$IS_ANDROID" = "true" ]; then error "The GPU agent has no Android build — mobile GPUs have no CUDA, ROCm, or Metal path for model serving." fi # Guard: prevent installing standalone gpu-agent on a machine that already # runs Squadem core (which has a built-in GPU agent on :9400). if [ "$COMPONENT" = "gpu-agent" ]; then _core_running=false if [ -x "${SQUADEM_DIR:-$HOME/.squadem}/bin/squadem" ]; then _core_running=true elif [ -f "$HOME/Library/LaunchAgents/com.squadem.core.plist" ]; then _core_running=true elif [ -f "$HOME/.config/systemd/user/squadem.service" ]; then _core_running=true fi if [ "$_core_running" = "true" ]; then echo "" error "Squadem core is already installed on this machine." echo " Core includes a built-in GPU agent — no separate install needed." echo " The standalone gpu-agent is only for REMOTE machines that join" echo " an existing core over the network." echo "" echo " If you want to add a remote GPU node, run this script on that" echo " remote machine instead." exit 1 fi fi if [ "$COMPONENT" = "core" ]; then info "Platform: ${BIN_OS}/${ARCH_NAME} Component: ${BOLD}core${NC} Mode: ${BOLD}${MODE}${NC}" else info "Platform: ${BIN_OS}/${ARCH_NAME} Component: ${BOLD}gpu-agent${NC}" fi # ────────────────────────────────────────────────────────────────── # CORE PATH — license, .env, control-plane install # ────────────────────────────────────────────────────────────────── if [ "$COMPONENT" = "core" ]; then # ── License key ─────────────────────────────────────────────────── ENV_FILE="$SQUADEM_DIR/.env" SQUADEM_LICENSE_KEY="${SQUADEM_LICENSE_KEY:-}" if [ -z "$SQUADEM_LICENSE_KEY" ] && [ -f "$ENV_FILE" ]; then SQUADEM_LICENSE_KEY=$(grep -E '^SQUADEM_LICENSE_KEY=' "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | tr -d '"' || true) fi if [ -z "$SQUADEM_LICENSE_KEY" ]; then echo "" echo " By providing your email, you agree to Squadem's Terms of Service" echo " (https://squadem.com/terms) and Privacy Policy (https://squadem.com/privacy)." echo "" printf " Email address: " read -r REG_EMAIL < /dev/tty echo "" [ -z "$REG_EMAIL" ] && error "Email address is required." printf " License key (or Enter to register): " read -r SQUADEM_LICENSE_KEY < /dev/tty echo "" # If no key provided, register for a free license if [ -z "$SQUADEM_LICENSE_KEY" ]; then info "Registering free license..." # No -f: we need the body and status of 4xx replies, not just a failure. REG_RESPONSE=$(curl -s -w '\n%{http_code}' -X POST "${LICENSE_API}/register" \ -H "Content-Type: application/json" \ -d "{\"email\":\"${REG_EMAIL}\"}" 2>/dev/null) || \ error "Registration failed: could not reach the license server." REG_STATUS=$(printf '%s' "$REG_RESPONSE" | tail -n1) REG_BODY=$(printf '%s' "$REG_RESPONSE" | sed '$d') if [ "$REG_STATUS" = "409" ]; then # Already registered. The server mails the key to the address on file # instead of returning it, so nobody can harvest a stranger's key by # guessing their email — which means we have to ask for it here. echo "" echo " ${BOLD}This email already has a Squadem license.${NC}" echo " We've emailed the key to ${REG_EMAIL} — check your inbox and paste it below." echo " You can also find it at https://squadem.com/portal" echo "" printf " License key: " read -r SQUADEM_LICENSE_KEY < /dev/tty echo "" [ -z "$SQUADEM_LICENSE_KEY" ] && error "License key required. Retrieve it at https://squadem.com/portal" elif [ "$REG_STATUS" = "429" ]; then error "Too many registration attempts for this email. Wait a few minutes, or get your key at https://squadem.com/portal" elif [ "$REG_STATUS" != "200" ] && [ "$REG_STATUS" != "201" ]; then REG_ERR=$(printf '%s' "$REG_BODY" | grep -o '"error":"[^"]*"' | cut -d'"' -f4) error "Registration failed${REG_ERR:+: $REG_ERR}. Visit https://squadem.com/portal to register." else SQUADEM_LICENSE_KEY=$(printf '%s' "$REG_BODY" | grep -o '"license_key":"[^"]*"' | cut -d'"' -f4) [ -z "$SQUADEM_LICENSE_KEY" ] && error "Registration failed: no license key returned" info "Free license registered! Key: $(printf '%.12s' "$SQUADEM_LICENSE_KEY")..." fi fi fi [ -z "$SQUADEM_LICENSE_KEY" ] && error "No license key provided" info "Validating license..." AUTH_RESPONSE=$(curl -sf -X POST "${LICENSE_API}/install/auth" \ -H "Content-Type: application/json" \ -d "{\"key\":\"${SQUADEM_LICENSE_KEY}\"}" 2>&1) || \ error "License validation failed (offline or invalid key). Register at https://squadem.com/sign-up" echo "$AUTH_RESPONSE" | grep -q '"success":\s*true' || \ error "License validation failed: $(echo "$AUTH_RESPONSE" | grep -o '"error":"[^"]*"' | cut -d'"' -f4)" LICENSE_TIER=$(echo "$AUTH_RESPONSE" | grep -o '"tier":"[^"]*"' | cut -d'"' -f4 || echo "unknown") # All tiers can self-host if [ "$LICENSE_TIER" = "unknown" ] || [ -z "$LICENSE_TIER" ]; then error "Could not determine license tier. Please check your license key or register at https://squadem.com/portal" fi info "License valid (${LICENSE_TIER} plan)" # ── Layout + minimal .env ───────────────────────────────────────── DATA_DIR="${SQUADEM_DATA_DIR:-$SQUADEM_DIR/data}" mkdir -p "$SQUADEM_DIR" "$DATA_DIR" # Single-source-of-truth .env. The binary fills in everything else # (passwords, plugin URLs, host hints) at first boot or via the # dashboard's Settings page — install.sh stays thin on purpose. if [ ! -f "$ENV_FILE" ]; then # If email wasn't collected during registration, ask now (needed for admin account) if [ -z "$REG_EMAIL" ] && [ -r /dev/tty ]; then printf " Admin email address: " read -r REG_EMAIL < /dev/tty echo "" fi cat > "$ENV_FILE" </dev/null 2>&1; then xattr -d com.apple.quarantine "$BIN_PATH" 2>/dev/null || true xattr -d com.apple.quarantine "$SQ_PATH" 2>/dev/null || true fi # Display installed version INSTALLED_VERSION=$("$BIN_PATH" --version 2>/dev/null || echo "unknown") info "Installed: ${INSTALLED_VERSION}" # Add sq CLI and squadem binary to PATH via symlink if [ -x "$SQ_PATH" ]; then if [ "$IS_ANDROID" = "true" ] && [ -w "${PREFIX:-/nonexistent}/bin" ]; then # Termux has no /usr/local/bin, but $PREFIX/bin is writable and # already on PATH — linking there saves a manual export. ln -sf "$SQ_PATH" "$PREFIX/bin/sq" 2>/dev/null || true ln -sf "$BIN_PATH" "$PREFIX/bin/squadem" 2>/dev/null || true info "Linked sq + squadem into ${PREFIX}/bin" elif [ -w /usr/local/bin ]; then ln -sf "$SQ_PATH" /usr/local/bin/sq 2>/dev/null || true ln -sf "$BIN_PATH" /usr/local/bin/squadem 2>/dev/null || true else info "Add to your PATH: export PATH=\"$BIN_DIR:\$PATH\"" fi fi if [ "$IS_ANDROID" = "true" ]; then # Android has neither systemd nor launchd, so there is nothing to # register the daemon with — start it detached and leave supervision # to the operator. android_start_core "$BIN_PATH" "$DATA_DIR" info "Started Squadem (PID ${ANDROID_START_PID})" SVC_HINT="sh install.sh --restart | --kill | --logs • tail -f ${SQUADEM_DIR}/squadem.log" elif [ "$OS" = "Linux" ]; then UNIT_PATH="$HOME/.config/systemd/user/squadem.service" mkdir -p "$(dirname "$UNIT_PATH")" # Build a PATH that includes the standard system bin dirs plus the # locations Docker / docker-ce installs the CLI on most distros. # systemd --user otherwise inherits a stripped PATH that omits # /usr/local/bin and /snap/bin, which breaks every downstream # `docker ...` exec the binary issues for plugins, adapters, agent # containers, and GPU model deployments. SERVICE_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/opt/docker/bin" SERVICE_LD_PATH="" # Under WSL the Windows NVIDIA driver mounts nvidia-smi and libcuda into # /usr/lib/wsl/lib. WSL puts that on PATH for interactive shells only, so a # service started from this unit would see a machine with no GPU while the # user's own terminal runs nvidia-smi fine. if [ -d /usr/lib/wsl/lib ]; then SERVICE_PATH="${SERVICE_PATH}:/usr/lib/wsl/lib" SERVICE_LD_PATH="Environment=LD_LIBRARY_PATH=/usr/lib/wsl/lib" info "WSL detected — adding /usr/lib/wsl/lib to the service environment for GPU access" fi cat > "$UNIT_PATH" </dev/null || true systemctl --user enable --now squadem.service 2>/dev/null || \ warn "Run manually: systemctl --user enable --now squadem.service" loginctl enable-linger "$USER" >/dev/null 2>&1 || true SVC_HINT="systemctl --user {status|restart|stop} squadem • journalctl --user -u squadem -f" else # macOS PLIST_PATH="$HOME/Library/LaunchAgents/com.squadem.core.plist" mkdir -p "$(dirname "$PLIST_PATH")" # launchd hands child processes a stripped PATH (/usr/bin:/bin:/usr/sbin:/sbin) # that omits /usr/local/bin (x86 Homebrew + manual docker), /opt/homebrew/bin # (Apple-Silicon Homebrew), and Docker Desktop's own CLI dir. Without an # explicit PATH here every `docker ...` exec issued by the binary # (plugins, adapters, agent containers, GPU model deployments) # fails with "executable file not found in \$PATH". The binary # also runs EnsureDockerOnPATH() at startup as a belt-and-suspenders # fallback, but baking the right PATH into the plist is what makes # the first boot work cleanly. cat > "$PLIST_PATH" < Labelcom.squadem.core ProgramArguments ${BIN_PATH}--data-dir=${DATA_DIR} EnvironmentVariables SQUADEM_ENV_FILE${ENV_FILE} PATH/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin WorkingDirectory${SQUADEM_DIR} RunAtLoad KeepAlive StandardOutPath${SQUADEM_DIR}/squadem.log StandardErrorPath${SQUADEM_DIR}/squadem.log PLIST launchctl unload "$PLIST_PATH" 2>/dev/null || true launchctl load "$PLIST_PATH" 2>/dev/null || \ warn "Run manually: launchctl load $PLIST_PATH" SVC_HINT="launchctl {load|unload} ${PLIST_PATH} • tail -f ${SQUADEM_DIR}/squadem.log" fi fi # ══════════════════════════════════════════════════════════════════ # OPTION 2 — Same binary, in a Docker container # ══════════════════════════════════════════════════════════════════ if [ "$MODE" = "docker" ]; then command -v docker >/dev/null 2>&1 || \ error "Docker is not installed. Get it at https://docker.com — or rerun without --mode=docker for the native binary." docker info >/dev/null 2>&1 || error "Docker daemon is not running. Start Docker and try again." info "Pulling ${SQUADEM_DOCKER_IMAGE}..." docker pull "$SQUADEM_DOCKER_IMAGE" || error "Pull failed for $SQUADEM_DOCKER_IMAGE" # Replace any previous container of the same name. We mount the # host docker socket so the in-process Agent IDE sandbox can spawn # ephemeral containers, and we expose only the user-facing ports # (dashboard, AI proxy, REST API, plugin manager). Transparent / # gateway listeners (DNS:53, transparent 8180/443) are configured # later from the dashboard if needed. docker rm -f squadem-core >/dev/null 2>&1 || true docker run -d \ --name squadem-core \ --restart unless-stopped \ --env-file "$ENV_FILE" \ -v "$DATA_DIR:/data" \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host=host.docker.internal:host-gateway \ -p 8080:8080 \ -p 8081:8081 \ -p 8085:8085 \ -p 8088:8088 \ -p 8093:8093 \ -p 8200:8200 \ "$SQUADEM_DOCKER_IMAGE" >/dev/null SVC_HINT="docker {logs|restart|stop} squadem-core" fi fi # end COMPONENT=core # ══════════════════════════════════════════════════════════════════ # GPU-AGENT PATH — companion binary that joins an existing core # ══════════════════════════════════════════════════════════════════ if [ "$COMPONENT" = "gpu-agent" ]; then AGENT_ENV_FILE="$SQUADEM_DIR/gpu-agent.env" mkdir -p "$SQUADEM_DIR" # Hydrate prior values so a re-run is a real upgrade (preserve URL + # token without re-prompting). Env vars passed on the current run # always win — that's how `SQUADEM_AGENT_TOKEN=… sh` rotates secrets. if [ -f "$AGENT_ENV_FILE" ]; then SQUADEM_CENTRAL_URL="${SQUADEM_CENTRAL_URL:-$(grep -E '^SQUADEM_CENTRAL_URL=' "$AGENT_ENV_FILE" | cut -d'=' -f2- | tr -d '"' || true)}" SQUADEM_AGENT_TOKEN="${SQUADEM_AGENT_TOKEN:-$(grep -E '^SQUADEM_AGENT_TOKEN=' "$AGENT_ENV_FILE" | cut -d'=' -f2- | tr -d '"' || true)}" SQUADEM_AGENT_PORT="${SQUADEM_AGENT_PORT:-$(grep -E '^SQUADEM_AGENT_PORT=' "$AGENT_ENV_FILE" | cut -d'=' -f2- | tr -d '"' || true)}" fi SQUADEM_AGENT_PORT="${SQUADEM_AGENT_PORT:-9400}" if [ -z "$SQUADEM_CENTRAL_URL" ]; then echo "" echo " ${BOLD}Squadem core URL${NC}" echo " Where this GPU agent should register (e.g. http://core.lan:8081)." echo "" printf " Central URL: " read -r SQUADEM_CENTRAL_URL < /dev/tty echo "" fi [ -z "$SQUADEM_CENTRAL_URL" ] && error "No central URL provided" if [ -z "$SQUADEM_AGENT_TOKEN" ]; then echo " ${BOLD}Registration token${NC}" echo " Generate one in the dashboard: ${BOLD}Settings → GPU → Add agent${NC}" echo "" printf " Token: " read -r SQUADEM_AGENT_TOKEN < /dev/tty echo "" fi [ -z "$SQUADEM_AGENT_TOKEN" ] && error "No registration token provided" cat > "$AGENT_ENV_FILE" </dev/null 2>&1; then info "Stopping existing GPU agent service for upgrade..." systemctl --user stop sq-gpu-agent.service 2>/dev/null || true STOPPED_SERVICE="systemd" elif [ "$OS" = "Darwin" ]; then PLIST_PATH="$HOME/Library/LaunchAgents/com.squadem.gpu-agent.plist" if launchctl list | grep -q com.squadem.gpu-agent 2>/dev/null; then info "Stopping existing GPU agent service for upgrade..." launchctl unload "$PLIST_PATH" 2>/dev/null || true STOPPED_SERVICE="launchd" fi fi # Kill any stray sq-gpu-agent processes not managed by the service if pgrep -x sq-gpu-agent >/dev/null 2>&1; then info "Killing existing sq-gpu-agent processes..." pkill -x sq-gpu-agent 2>/dev/null || true sleep 1 # Force kill if still running pkill -9 -x sq-gpu-agent 2>/dev/null || true fi # Download to a temp file first, then move (handles locked binary edge case) BIN_TMP="${BIN_PATH}.new" info "Downloading sq-gpu-agent ${SQUADEM_VERSION}..." if command -v curl >/dev/null 2>&1; then curl -fL --progress-bar "$BIN_URL" -o "$BIN_TMP" || error "Download failed" elif command -v wget >/dev/null 2>&1; then wget -q --show-progress "$BIN_URL" -O "$BIN_TMP" || error "Download failed" else error "curl or wget is required" fi verify_checksum "$BIN_TMP" "${AGENT_FILE}" mv -f "$BIN_TMP" "$BIN_PATH" chmod +x "$BIN_PATH" if [ "$OS" = "Darwin" ] && command -v xattr >/dev/null 2>&1; then xattr -d com.apple.quarantine "$BIN_PATH" 2>/dev/null || true fi if [ "$OS" = "Linux" ]; then UNIT_PATH="$HOME/.config/systemd/user/sq-gpu-agent.service" mkdir -p "$(dirname "$UNIT_PATH")" # Pull cred values from EnvironmentFile rather than baking them # into the unit file itself so token rotation is one `sed` away # without rewriting the unit (and so `journalctl` doesn't leak the # token in the ExecStart line on `systemctl status`). cat > "$UNIT_PATH" </dev/null || true systemctl --user enable --now sq-gpu-agent.service 2>/dev/null || \ warn "Run manually: systemctl --user enable --now sq-gpu-agent.service" loginctl enable-linger "$USER" >/dev/null 2>&1 || true SVC_HINT="systemctl --user {status|restart|stop} sq-gpu-agent • journalctl --user -u sq-gpu-agent -f" else # macOS PLIST_PATH="$HOME/Library/LaunchAgents/com.squadem.gpu-agent.plist" mkdir -p "$(dirname "$PLIST_PATH")" cat > "$PLIST_PATH" < Labelcom.squadem.gpu-agent ProgramArguments ${BIN_PATH} --central=${SQUADEM_CENTRAL_URL} --token=${SQUADEM_AGENT_TOKEN} --port=${SQUADEM_AGENT_PORT} WorkingDirectory${SQUADEM_DIR} RunAtLoad KeepAlive StandardOutPath${SQUADEM_DIR}/sq-gpu-agent.log StandardErrorPath${SQUADEM_DIR}/sq-gpu-agent.log PLIST launchctl unload "$PLIST_PATH" 2>/dev/null || true launchctl load "$PLIST_PATH" 2>/dev/null || \ warn "Run manually: launchctl load $PLIST_PATH" SVC_HINT="launchctl {load|unload} ${PLIST_PATH} • tail -f ${SQUADEM_DIR}/sq-gpu-agent.log" fi fi # end COMPONENT=gpu-agent # ── Wait for health ─────────────────────────────────────────────── if [ "$COMPONENT" = "core" ]; then HEALTH_URL="http://localhost:8200/health"; READY_LABEL="Squadem" else HEALTH_URL="http://localhost:${SQUADEM_AGENT_PORT}/health"; READY_LABEL="GPU agent" fi info "Waiting for ${READY_LABEL} to be ready..." TIMEOUT=60; ELAPSED=0; READY=false while [ $ELAPSED -lt $TIMEOUT ]; do if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then READY=true; break; fi sleep 2; ELAPSED=$((ELAPSED + 2)) done if [ "$READY" = "true" ]; then info "${READY_LABEL} is healthy" else warn "Not responding at ${HEALTH_URL} yet. Check service logs (see commands below)." fi # ── Best-effort install ping ────────────────────────────────────── curl -s -X POST "${LICENSE_API}/track" \ -H "Content-Type: application/json" \ -d "{\"event\":\"install\",\"platform\":\"${OS}-${ARCH}\",\"component\":\"${COMPONENT}\",\"mode\":\"${MODE}\",\"source\":\"installer\",\"version\":\"${SQUADEM_VERSION}\",\"license_key\":\"${SQUADEM_LICENSE_KEY}\"}" \ >/dev/null 2>&1 || true # ── Done ────────────────────────────────────────────────────────── echo "" echo "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" if [ "$COMPONENT" = "core" ]; then if [ "$READY" = "true" ]; then echo " ${GREEN}${BOLD}Squadem is running${NC} (${LICENSE_TIER} plan, ${MODE} mode)" else echo " ${YELLOW}${BOLD}Squadem is installed but did not come up${NC} (${LICENSE_TIER} plan, ${MODE} mode)" fi echo "" echo " ${BOLD}Dashboard${NC} http://localhost:8200" echo " ${BOLD}AI Proxy${NC} http://localhost:8080" echo " ${BOLD}Config${NC} ${ENV_FILE}" echo " ${BOLD}Data${NC} ${DATA_DIR}" echo " ${BOLD}Service${NC} ${SVC_HINT}" echo "" echo " Open the dashboard to finish setup (create your admin account)." else if [ "$READY" = "true" ]; then echo " ${GREEN}${BOLD}Squadem GPU agent is running${NC}" else echo " ${YELLOW}${BOLD}Squadem GPU agent is installed but did not come up${NC}" fi echo "" echo " ${BOLD}Local API${NC} http://localhost:${SQUADEM_AGENT_PORT}" echo " ${BOLD}Joining${NC} ${SQUADEM_CENTRAL_URL}" echo " ${BOLD}Config${NC} ${AGENT_ENV_FILE}" echo " ${BOLD}Service${NC} ${SVC_HINT}" echo "" echo " In the core dashboard the new node should appear under" echo " ${BOLD}Settings → GPU${NC} within a few seconds." fi echo "" echo "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # An install whose service never answered its health check is a failed # install. Reporting success anyway is what makes provisioning runs go # green while leaving nothing listening. [ "$READY" = "true" ] || exit 1