From 5f21a91ff957168a651570228f80ea8bad208020 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Sun, 5 Apr 2026 08:13:28 +0000 Subject: [PATCH] Add universal Linux installer script Bash installer that works on any Linux distro: - Auto-detects OS and architecture - Installs Docker via get.docker.com if missing - Installs Docker Compose plugin if missing - Auto-discovers local Frigate and go2rtc instances - Interactive dialogs with 10s timeouts - Generates docker-compose.yml and .env in /opt/strix/ - Supports install and update modes - CLI flags for non-interactive use (--yes, --version, --no-logo) - Returns machine-readable status for parent scripts (Proxmox LXC) --- install.sh | 863 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 863 insertions(+) create mode 100755 install.sh diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..43c153d --- /dev/null +++ b/install.sh @@ -0,0 +1,863 @@ +#!/usr/bin/env bash +# ============================================================================= +# Strix Installer +# Universal installer for any Linux distribution. +# Installs Docker (via get.docker.com), Docker Compose, detects Frigate/go2rtc, +# and deploys Strix via docker compose. +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/eduard256/Strix/main/install.sh | sudo bash +# sudo bash install.sh [OPTIONS] +# +# Options: +# --no-logo Suppress ASCII logo (useful when called from another script) +# --no-color Disable colored output +# --yes, -y Non-interactive mode, accept all defaults +# --version TAG Set image tag without prompt (latest/dev/1.0.9) +# --verbose, -v Show detailed output from docker commands +# +# Exit codes: +# 0 = success +# 1 = docker installation failed +# 2 = docker compose not available +# 3 = image pull failed +# 4 = container failed to start +# 5 = healthcheck failed +# ============================================================================= + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Globals +# --------------------------------------------------------------------------- +STRIX_DIR="/opt/strix" +STRIX_PORT="4567" +IMAGE="eduard256/strix" +LOG_FILE="${STRIX_DIR}/install.log" +DIALOG_TIMEOUT=10 + +# Flags (overridden by CLI args) +SHOW_LOGO=true +USE_COLOR=true +INTERACTIVE=true +VERBOSE=false +TAG="" + +# Result variables (printed at the end for parent scripts) +INSTALL_MODE="" # install | update +FRIGATE_STATUS="none" # found | set | none +GO2RTC_STATUS="none" # found | set | none +FINAL_VERSION="" + +# --------------------------------------------------------------------------- +# Parse CLI arguments +# --------------------------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --no-logo) SHOW_LOGO=false; shift ;; + --no-color) USE_COLOR=false; shift ;; + --yes|-y) INTERACTIVE=false; shift ;; + --verbose|-v) VERBOSE=true; shift ;; + --version) TAG="$2"; shift 2 ;; + *) shift ;; + esac +done + +# --------------------------------------------------------------------------- +# Color setup +# --------------------------------------------------------------------------- +setup_colors() { + if [[ "$USE_COLOR" == false ]] || [[ "${NO_COLOR:-}" != "" ]] || [[ ! -t 1 ]]; then + USE_COLOR=false + C_RESET="" C_BOLD="" C_DIM="" + C_RED="" C_GREEN="" C_YELLOW="" C_CYAN="" C_MAGENTA="" C_WHITE="" + return + fi + + local colors + colors=$(tput colors 2>/dev/null || echo 0) + if [[ "$colors" -lt 8 ]]; then + USE_COLOR=false + C_RESET="" C_BOLD="" C_DIM="" + C_RED="" C_GREEN="" C_YELLOW="" C_CYAN="" C_MAGENTA="" C_WHITE="" + return + fi + + C_RESET="\033[0m" + C_BOLD="\033[1m" + C_DIM="\033[2m" + C_RED="\033[31m" + C_GREEN="\033[32m" + C_YELLOW="\033[33m" + C_CYAN="\033[36m" + C_MAGENTA="\033[35m" + C_WHITE="\033[97m" +} + +# --------------------------------------------------------------------------- +# Terminal width helpers +# --------------------------------------------------------------------------- +term_width() { + local w + w=$(tput cols 2>/dev/null || echo 80) + [[ "$w" -lt 40 ]] && w=40 + echo "$w" +} + +# Print text centered in terminal +center() { + local text="$1" + local w + w=$(term_width) + # Strip ANSI codes for length calculation + local stripped + stripped=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g') + local len=${#stripped} + local pad=$(( (w - len) / 2 )) + [[ "$pad" -lt 0 ]] && pad=0 + printf "%*s%b\n" "$pad" "" "$text" +} + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- +mkdir -p "$STRIX_DIR" 2>/dev/null || true + +log_raw() { + local ts + ts=$(date '+%H:%M:%S') + echo -e "$ts $1" >> "$LOG_FILE" 2>/dev/null || true +} + +log() { + log_raw "$1" + if [[ "$VERBOSE" == true ]]; then + local ts + ts=$(date '+%H:%M:%S') + echo -e " ${C_DIM}${ts} $1${C_RESET}" + fi +} + +# Status line helpers +status_ok() { echo -e " ${C_GREEN}${C_BOLD}[OK]${C_RESET} $1"; log "[OK] $1"; } +status_warn() { echo -e " ${C_YELLOW}${C_BOLD}[!!]${C_RESET} $1"; log "[!!] $1"; } +status_fail() { echo -e " ${C_RED}${C_BOLD}[XX]${C_RESET} $1"; log "[XX] $1"; } +status_info() { echo -e " ${C_CYAN}${C_BOLD}[..]${C_RESET} $1"; log "[..] $1"; } +status_skip() { echo -e " ${C_DIM}[--]${C_RESET} $1"; log "[--] $1"; } + +# --------------------------------------------------------------------------- +# ASCII art +# --------------------------------------------------------------------------- +show_logo() { + [[ "$SHOW_LOGO" == false ]] && return + + echo "" + + # Block STRIX title + center "${C_MAGENTA}${C_BOLD}███████╗████████╗██████╗ ██╗██╗ ██╗${C_RESET}" + center "${C_MAGENTA}${C_BOLD}██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝${C_RESET}" + center "${C_MAGENTA}${C_BOLD}███████╗ ██║ ██████╔╝██║ ╚███╔╝${C_RESET}" + center "${C_MAGENTA}${C_BOLD}╚════██║ ██║ ██╔══██╗██║ ██╔██╗${C_RESET}" + center "${C_MAGENTA}${C_BOLD}███████║ ██║ ██║ ██║██║██╔╝ ██╗${C_RESET}" + center "${C_MAGENTA}${C_BOLD}╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝${C_RESET}" + + echo "" + + # Owl + center "${C_CYAN} __________-------____ ____-------__________${C_RESET}" + center "${C_CYAN} \\------____-------___--__---------__--___-------____------/${C_RESET}" + center "${C_CYAN} \\//////// / / / / / \\ _-------_ / \\ \\ \\ \\ \\ \\\\\\\\\\\\\\\\/${C_RESET}" + center "${C_CYAN} \\////-/-/------/_/_| /___ ___\\ |_\\_\\------\\-\\-\\\\\\\\/${C_RESET}" + center "${C_CYAN} --//// / / / //|| ${C_YELLOW}(O)${C_CYAN}\\ /${C_YELLOW}(O)${C_CYAN} ||\\\\ \\ \\ \\ \\\\\\\\--${C_RESET}" + center "${C_CYAN} ---__/ // /| \\_ /V\\ _/ |\\ \\\\ \\__---${C_RESET}" + center "${C_CYAN} -// / /\\_ ------- _/\\ \\ \\\\-${C_RESET}" + center "${C_CYAN} \\_/_/ /\\---------/\\ \\_\\_/${C_RESET}" + center "${C_CYAN} ----\\ | /----${C_RESET}" + center "${C_CYAN} | -|- |${C_RESET}" + center "${C_CYAN} / | \\${C_RESET}" + center "${C_CYAN} ---- ----${C_RESET}" + + echo "" + center "${C_WHITE}${C_BOLD}Smart IP Camera Stream Finder${C_RESET}" + center "${C_DIM}────────────────────────────────────────${C_RESET}" + echo "" +} + +show_owl_small() { + echo -e " ${C_CYAN} ,___,${C_RESET}" + echo -e " ${C_CYAN} /(6 6)\\_${C_RESET}" + echo -e " ${C_CYAN} /\\\` ' \`'\\\\_${C_RESET}" + echo -e " ${C_CYAN} \\\\\\\\_''''|\\\\\\\\${C_RESET}" + echo -e " ${C_CYAN} )\\\\\\\\\\\\''//||/${C_RESET}" + echo -e " ${C_CYAN} ._,--/////\"\"------${C_RESET}" +} + +# --------------------------------------------------------------------------- +# Dialog boxes +# --------------------------------------------------------------------------- + +# Draw a box around content. +# Usage: draw_box "Title" "line1" "line2" ... +draw_box() { + local title="$1"; shift + local w + w=$(term_width) + local box_w=$(( w - 4 )) + [[ "$box_w" -gt 60 ]] && box_w=60 + + local top_line="" + local bot_line="" + local i + + # Build horizontal lines + for (( i = 0; i < box_w - 2; i++ )); do + top_line="${top_line}─" + bot_line="${bot_line}─" + done + + # Center the box + local pad=$(( (w - box_w) / 2 )) + [[ "$pad" -lt 0 ]] && pad=0 + local sp + sp=$(printf "%*s" "$pad" "") + + echo "" + echo -e "${sp}${C_CYAN}┌─ ${C_WHITE}${C_BOLD}${title}${C_RESET}${C_CYAN} ${top_line:$(( ${#title} + 3 ))}┐${C_RESET}" + echo -e "${sp}${C_CYAN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}" + + for line in "$@"; do + local stripped + stripped=$(echo -e "$line" | sed 's/\x1b\[[0-9;]*m//g') + local line_len=${#stripped} + local right_pad=$(( box_w - 2 - line_len )) + [[ "$right_pad" -lt 0 ]] && right_pad=0 + echo -e "${sp}${C_CYAN}│${C_RESET} ${line}$(printf "%*s" "$right_pad" "")${C_CYAN}│${C_RESET}" + done + + echo -e "${sp}${C_CYAN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}" + echo -e "${sp}${C_CYAN}└${bot_line}┘${C_RESET}" + echo "" +} + +# Prompt with timeout. Returns user input or default. +# Usage: timed_prompt "prompt text" "default" timeout_seconds +timed_prompt() { + local prompt_text="$1" + local default="$2" + local timeout="$3" + local result="" + + if [[ "$INTERACTIVE" == false ]]; then + echo "$default" + return + fi + + # Show countdown hint + echo -ne " ${C_YELLOW}${prompt_text}${C_RESET} ${C_DIM}(${timeout}s -> ${default})${C_RESET}: " + + if read -r -t "$timeout" result 2>/dev/null; then + if [[ -z "$result" ]]; then + echo "$default" + else + echo "$result" + fi + else + echo "" # newline after timeout + echo "$default" + fi +} + +# --------------------------------------------------------------------------- +# System detection +# --------------------------------------------------------------------------- +detect_system() { + log "Detecting OS from /etc/os-release" + + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + . /etc/os-release + OS_ID="${ID:-unknown}" + OS_VERSION="${VERSION_ID:-unknown}" + OS_NAME="${PRETTY_NAME:-${ID} ${VERSION_ID}}" + else + OS_ID="unknown" + OS_VERSION="unknown" + OS_NAME="Unknown Linux" + fi + + ARCH=$(uname -m) + case "$ARCH" in + x86_64) ARCH_LABEL="amd64" ;; + aarch64) ARCH_LABEL="arm64" ;; + armv7l) ARCH_LABEL="armv7" ;; + *) ARCH_LABEL="$ARCH" ;; + esac + + log "Detected: ${OS_ID} ${OS_VERSION} (${ARCH_LABEL})" + status_ok "System: ${C_WHITE}${C_BOLD}${OS_NAME}${C_RESET} (${ARCH_LABEL})" +} + +# --------------------------------------------------------------------------- +# Ensure root +# --------------------------------------------------------------------------- +ensure_root() { + if [[ "$(id -u)" -ne 0 ]]; then + status_info "Root privileges required, re-running with sudo..." + exec sudo "$0" "$@" + fi +} + +# --------------------------------------------------------------------------- +# Install curl if missing +# --------------------------------------------------------------------------- +ensure_curl() { + if command -v curl &>/dev/null; then + log "curl found" + return + fi + + status_info "Installing curl..." + log "curl not found, installing" + + case "$OS_ID" in + ubuntu|debian|raspbian|linuxmint|pop) + apt-get update -qq && apt-get install -y -qq curl ;; + centos|rhel|rocky|almalinux|ol) + yum install -y -q curl ;; + fedora) + dnf install -y -q curl ;; + alpine) + apk add --no-cache curl ;; + arch|manjaro) + pacman -Sy --noconfirm curl ;; + opensuse*|sles) + zypper install -y curl ;; + *) + status_fail "Cannot install curl: unknown package manager for ${OS_ID}" + status_fail "Please install curl manually and re-run the script" + exit 1 ;; + esac + + if command -v curl &>/dev/null; then + status_ok "curl installed" + else + status_fail "Failed to install curl" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# Docker installation +# --------------------------------------------------------------------------- +check_docker() { + if command -v docker &>/dev/null; then + local ver + ver=$(docker --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown") + status_ok "Docker ${C_WHITE}${C_BOLD}${ver}${C_RESET}" + log "Docker found: ${ver}" + return 0 + fi + return 1 +} + +install_docker() { + status_info "Installing Docker via ${C_WHITE}get.docker.com${C_RESET}..." + log "Downloading and running get.docker.com" + + if [[ "$VERBOSE" == true ]]; then + curl -fsSL https://get.docker.com | sh + else + curl -fsSL https://get.docker.com | sh &>/dev/null + fi + + # Enable and start docker + if command -v systemctl &>/dev/null; then + systemctl enable docker &>/dev/null || true + systemctl start docker &>/dev/null || true + fi + + if check_docker; then + return 0 + else + status_fail "Docker installation failed" + log "Docker installation failed" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# Docker Compose check +# --------------------------------------------------------------------------- +check_compose() { + # Check plugin first (docker compose v2) + if docker compose version &>/dev/null; then + local ver + ver=$(docker compose version --short 2>/dev/null || echo "unknown") + status_ok "Docker Compose ${C_WHITE}${C_BOLD}${ver}${C_RESET}" + log "Docker Compose found: ${ver}" + COMPOSE_CMD="docker compose" + return 0 + fi + + # Check standalone docker-compose + if command -v docker-compose &>/dev/null; then + local ver + ver=$(docker-compose --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown") + status_ok "Docker Compose ${C_WHITE}${C_BOLD}${ver}${C_RESET} (standalone)" + log "Docker Compose standalone found: ${ver}" + COMPOSE_CMD="docker-compose" + return 0 + fi + + return 1 +} + +install_compose() { + status_info "Installing Docker Compose plugin..." + log "Installing Docker Compose plugin" + + # Docker Compose V2 is typically bundled with Docker now. + # Try installing the plugin package. + case "$OS_ID" in + ubuntu|debian|raspbian|linuxmint|pop) + apt-get update -qq && apt-get install -y -qq docker-compose-plugin 2>/dev/null ;; + centos|rhel|rocky|almalinux|ol|fedora) + yum install -y -q docker-compose-plugin 2>/dev/null || dnf install -y -q docker-compose-plugin 2>/dev/null ;; + *) + # Fallback: download binary + local compose_ver="v2.29.1" + local compose_arch + case "$ARCH_LABEL" in + amd64) compose_arch="x86_64" ;; + arm64) compose_arch="aarch64" ;; + *) compose_arch="$ARCH" ;; + esac + mkdir -p /usr/local/lib/docker/cli-plugins + curl -fsSL "https://github.com/docker/compose/releases/download/${compose_ver}/docker-compose-linux-${compose_arch}" \ + -o /usr/local/lib/docker/cli-plugins/docker-compose + chmod +x /usr/local/lib/docker/cli-plugins/docker-compose + ;; + esac + + if check_compose; then + return 0 + else + status_fail "Docker Compose installation failed" + log "Docker Compose installation failed" + exit 2 + fi +} + +# --------------------------------------------------------------------------- +# Check if Strix is already installed +# --------------------------------------------------------------------------- +check_existing() { + if [[ -f "${STRIX_DIR}/docker-compose.yml" ]]; then + if $COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" ps --format '{{.State}}' 2>/dev/null | grep -qi "running"; then + INSTALL_MODE="update" + status_info "Strix is already running -- ${C_WHITE}${C_BOLD}update mode${C_RESET}" + log "Existing Strix installation found, switching to update mode" + return 0 + fi + # compose file exists but not running + INSTALL_MODE="update" + status_info "Strix config found but not running -- ${C_WHITE}${C_BOLD}update mode${C_RESET}" + log "Existing Strix config found (not running), switching to update mode" + return 0 + fi + + INSTALL_MODE="install" + log "No existing Strix installation found" + return 1 +} + +# --------------------------------------------------------------------------- +# Version selection dialog +# --------------------------------------------------------------------------- +select_version() { + # If already set via --version flag + if [[ -n "$TAG" ]]; then + FINAL_VERSION="$TAG" + status_ok "Version: ${C_WHITE}${C_BOLD}${TAG}${C_RESET} (from --version flag)" + log "Version set via flag: ${TAG}" + return + fi + + if [[ "$INTERACTIVE" == false ]]; then + FINAL_VERSION="latest" + status_ok "Version: ${C_WHITE}${C_BOLD}latest${C_RESET} (non-interactive default)" + log "Version defaulted to latest (non-interactive)" + return + fi + + draw_box "Select Version" \ + " ${C_GREEN}${C_BOLD}[1]${C_RESET} latest ${C_DIM}(recommended)${C_RESET}" \ + " ${C_YELLOW}${C_BOLD}[2]${C_RESET} dev ${C_DIM}(development)${C_RESET}" \ + " ${C_CYAN}${C_BOLD}[3]${C_RESET} custom tag ${C_DIM}(e.g. 1.0.9)${C_RESET}" + + local choice + choice=$(timed_prompt "Choice [1/2/3]" "1" "$DIALOG_TIMEOUT") + + case "$choice" in + 1|"") FINAL_VERSION="latest" ;; + 2) FINAL_VERSION="dev" ;; + 3) + echo -ne " ${C_YELLOW}Enter tag: ${C_RESET}" + local custom_tag + if read -r -t "$DIALOG_TIMEOUT" custom_tag 2>/dev/null && [[ -n "$custom_tag" ]]; then + FINAL_VERSION="$custom_tag" + else + FINAL_VERSION="latest" + echo "" + fi + ;; + *) FINAL_VERSION="latest" ;; + esac + + status_ok "Version: ${C_WHITE}${C_BOLD}${FINAL_VERSION}${C_RESET}" + log "Version selected: ${FINAL_VERSION}" +} + +# --------------------------------------------------------------------------- +# Service detection (Frigate / go2rtc) +# --------------------------------------------------------------------------- +probe_frigate() { + log "Probing Frigate at localhost:5000" + + if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:5000/api/config" &>/dev/null; then + FRIGATE_STATUS="found" + FRIGATE_URL="http://localhost:5000" + status_ok "Frigate: ${C_WHITE}${C_BOLD}localhost:5000${C_RESET}" + log "Frigate found at localhost:5000" + return + fi + + log "Frigate not found locally" + + if [[ "$INTERACTIVE" == false ]]; then + FRIGATE_STATUS="none" + FRIGATE_URL="" + status_skip "Frigate: not found (skipped)" + return + fi + + echo "" + show_owl_small + draw_box "Frigate Not Found" \ + " Frigate was not detected on this machine." \ + " Enter Frigate URL or leave empty to skip." \ + "" \ + " ${C_DIM}Example: http://192.168.1.100:5000${C_RESET}" + + local input + input=$(timed_prompt "Frigate URL" "" "$DIALOG_TIMEOUT") + + if [[ -n "$input" ]]; then + FRIGATE_STATUS="set" + FRIGATE_URL="$input" + status_ok "Frigate: ${C_WHITE}${C_BOLD}${input}${C_RESET} (manual)" + log "Frigate URL set manually: ${input}" + else + FRIGATE_STATUS="none" + FRIGATE_URL="" + status_skip "Frigate: not configured" + log "Frigate skipped" + fi +} + +probe_go2rtc() { + log "Probing go2rtc at localhost:1984 and localhost:11984" + + if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:1984/api" &>/dev/null; then + GO2RTC_STATUS="found" + GO2RTC_URL="http://localhost:1984" + status_ok "go2rtc: ${C_WHITE}${C_BOLD}localhost:1984${C_RESET}" + log "go2rtc found at localhost:1984" + return + fi + + if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:11984/api" &>/dev/null; then + GO2RTC_STATUS="found" + GO2RTC_URL="http://localhost:11984" + status_ok "go2rtc: ${C_WHITE}${C_BOLD}localhost:11984${C_RESET}" + log "go2rtc found at localhost:11984" + return + fi + + log "go2rtc not found locally" + + if [[ "$INTERACTIVE" == false ]]; then + GO2RTC_STATUS="none" + GO2RTC_URL="" + status_skip "go2rtc: not found (skipped)" + return + fi + + echo "" + show_owl_small + draw_box "go2rtc Not Found" \ + " go2rtc was not detected on this machine." \ + " Enter go2rtc URL or leave empty to skip." \ + "" \ + " ${C_DIM}Example: http://192.168.1.100:1984${C_RESET}" + + local input + input=$(timed_prompt "go2rtc URL" "" "$DIALOG_TIMEOUT") + + if [[ -n "$input" ]]; then + GO2RTC_STATUS="set" + GO2RTC_URL="$input" + status_ok "go2rtc: ${C_WHITE}${C_BOLD}${input}${C_RESET} (manual)" + log "go2rtc URL set manually: ${input}" + else + GO2RTC_STATUS="none" + GO2RTC_URL="" + status_skip "go2rtc: not configured" + log "go2rtc skipped" + fi +} + +# --------------------------------------------------------------------------- +# Generate config files +# --------------------------------------------------------------------------- +generate_env() { + log "Generating ${STRIX_DIR}/.env" + + cat > "${STRIX_DIR}/.env" <> "${STRIX_DIR}/.env" + fi + + if [[ -n "${GO2RTC_URL:-}" ]]; then + echo "STRIX_GO2RTC_URL=${GO2RTC_URL}" >> "${STRIX_DIR}/.env" + fi + + log "Generated .env: TAG=${FINAL_VERSION}, FRIGATE=${FRIGATE_URL:-}, GO2RTC=${GO2RTC_URL:-}" +} + +generate_compose() { + log "Generating ${STRIX_DIR}/docker-compose.yml" + + local env_section="" + if [[ -n "${FRIGATE_URL:-}" ]] || [[ -n "${GO2RTC_URL:-}" ]]; then + env_section=" environment:" + [[ -n "${FRIGATE_URL:-}" ]] && env_section="${env_section} + - STRIX_FRIGATE_URL=\${STRIX_FRIGATE_URL}" + [[ -n "${GO2RTC_URL:-}" ]] && env_section="${env_section} + - STRIX_GO2RTC_URL=\${STRIX_GO2RTC_URL}" + fi + + cat > "${STRIX_DIR}/docker-compose.yml" <&1 | tail -1 + fi + + if [[ $? -eq 0 ]]; then + status_ok "Image pulled: ${C_WHITE}${C_BOLD}${IMAGE}:${FINAL_VERSION}${C_RESET}" + log "Image pulled successfully" + else + status_fail "Failed to pull image ${IMAGE}:${FINAL_VERSION}" + log "Image pull failed" + exit 3 + fi +} + +start_container() { + log "Starting container" + + if [[ "$INSTALL_MODE" == "update" ]]; then + status_info "Recreating container..." + $COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" up -d --force-recreate 2>&1 | \ + if [[ "$VERBOSE" == true ]]; then cat; else tail -1; fi + else + status_info "Starting container..." + $COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" up -d 2>&1 | \ + if [[ "$VERBOSE" == true ]]; then cat; else tail -1; fi + fi + + if [[ $? -ne 0 ]]; then + status_fail "Container failed to start" + log "Container failed to start" + exit 4 + fi + + log "Container started" +} + +healthcheck() { + status_info "Running healthcheck..." + log "Waiting for healthcheck on localhost:${STRIX_PORT}" + + local retries=10 + local i + for (( i = 1; i <= retries; i++ )); do + if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:${STRIX_PORT}/api/health" &>/dev/null; then + local version + version=$(curl -sf --max-time 3 "http://localhost:${STRIX_PORT}/api" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown") + status_ok "Strix is running ${C_WHITE}${C_BOLD}v${version}${C_RESET} on port ${C_WHITE}${C_BOLD}${STRIX_PORT}${C_RESET}" + log "Healthcheck passed, version: ${version}" + return 0 + fi + sleep 1 + done + + status_fail "Healthcheck failed after ${retries} attempts" + log "Healthcheck failed" + exit 5 +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +show_summary() { + echo "" + local w + w=$(term_width) + local line="" + local box_w=$(( w - 4 )) + [[ "$box_w" -gt 60 ]] && box_w=60 + for (( i = 0; i < box_w - 2; i++ )); do line="${line}─"; done + local pad=$(( (w - box_w) / 2 )) + [[ "$pad" -lt 0 ]] && pad=0 + local sp + sp=$(printf "%*s" "$pad" "") + + echo -e "${sp}${C_GREEN}┌─ ${C_WHITE}${C_BOLD}Complete${C_RESET} ${C_GREEN}${line:11}┐${C_RESET}" + echo -e "${sp}${C_GREEN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} Mode: ${C_WHITE}${C_BOLD}${INSTALL_MODE}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#INSTALL_MODE} )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} Version: ${C_WHITE}${C_BOLD}${FINAL_VERSION}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#FINAL_VERSION} )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} Port: ${C_WHITE}${C_BOLD}${STRIX_PORT}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#STRIX_PORT} )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} Frigate: ${C_WHITE}${FRIGATE_STATUS}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#FRIGATE_STATUS} )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} go2rtc: ${C_WHITE}${GO2RTC_STATUS}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#GO2RTC_STATUS} )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} Config: ${C_DIM}${STRIX_DIR}/${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#STRIX_DIR} - 1 )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} Log: ${C_DIM}${LOG_FILE}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#LOG_FILE} )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}" + echo -e "${sp}${C_GREEN}│${C_RESET} ${C_CYAN}Open: ${C_WHITE}${C_BOLD}http://localhost:${STRIX_PORT}${C_RESET}$(printf "%*s" $(( box_w - 30 - ${#STRIX_PORT} )) "")${C_GREEN}│${C_RESET}" + echo -e "${sp}${C_GREEN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}" + echo -e "${sp}${C_GREEN}└${line}┘${C_RESET}" + echo "" + + # Machine-readable output for parent scripts (always last line) + echo "STRIX_RESULT=OK MODE=${INSTALL_MODE} VERSION=${FINAL_VERSION} FRIGATE=${FRIGATE_STATUS} GO2RTC=${GO2RTC_STATUS}" +} + +# --------------------------------------------------------------------------- +# Verbose log tail (shown on error) +# --------------------------------------------------------------------------- +show_error_log() { + if [[ -f "$LOG_FILE" ]]; then + echo "" + echo -e " ${C_RED}${C_BOLD}── Last log entries ──${C_RESET}" + tail -20 "$LOG_FILE" | while IFS= read -r line; do + echo -e " ${C_RED}${line}${C_RESET}" + done + echo -e " ${C_RED}${C_BOLD}──────────────────────${C_RESET}" + echo "" + echo -e " Full log: ${C_WHITE}${LOG_FILE}${C_RESET}" + fi +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + # Trap errors to show log + trap 'show_error_log' ERR + + # Initialize + setup_colors + ensure_root "$@" + + # Show logo + show_logo + + # Detect system + detect_system + + # Ensure curl is available + ensure_curl + + # Docker + if ! check_docker; then + install_docker + fi + + # Docker Compose + if ! check_compose; then + install_compose + fi + + # Version selection + select_version + + # Check existing installation + check_existing || true + + # Detect services (only for fresh install) + if [[ "$INSTALL_MODE" == "install" ]]; then + probe_frigate + probe_go2rtc + else + # For updates, load existing env + if [[ -f "${STRIX_DIR}/.env" ]]; then + # shellcheck disable=SC1091 + source "${STRIX_DIR}/.env" 2>/dev/null || true + FRIGATE_URL="${STRIX_FRIGATE_URL:-}" + GO2RTC_URL="${STRIX_GO2RTC_URL:-}" + [[ -n "$FRIGATE_URL" ]] && FRIGATE_STATUS="set" + [[ -n "$GO2RTC_URL" ]] && GO2RTC_STATUS="set" + fi + fi + + echo "" + echo -e " ${C_DIM}────────────────────────────────────────${C_RESET}" + echo "" + + # Generate configs + generate_env + generate_compose + + # Deploy + pull_image + start_container + healthcheck + + # Done + show_summary +} + +main "$@"