#!/usr/bin/env bash # Pulse Installer Script # Supports: Ubuntu 20.04+, Debian 11+, Proxmox VE 7+ set -euo pipefail # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration INSTALL_DIR="/opt/pulse" CONFIG_DIR="/etc/pulse" # All config and data goes here for manual installs SERVICE_NAME="pulse" GITHUB_REPO="rcourtman/Pulse" # Detect existing service name (pulse or pulse-backend) detect_service_name() { if systemctl list-unit-files --no-legend | grep -q "^pulse-backend.service"; then echo "pulse-backend" elif systemctl list-unit-files --no-legend | grep -q "^pulse.service"; then echo "pulse" else echo "pulse" # Default for new installations fi } # Functions print_header() { echo -e "${BLUE}=================================================${NC}" echo -e "${BLUE} Pulse Installation Script${NC}" echo -e "${BLUE}=================================================${NC}" echo } # Safe read function that works with or without TTY safe_read() { local prompt="$1" local var_name="$2" # When script is piped (curl | bash), stdin is the pipe, not the terminal # We need to read from /dev/tty for user input if [[ -t 0 ]]; then # stdin is a terminal, read normally echo -n "$prompt" IFS= read -r "$var_name" else # stdin is not a terminal (piped), try /dev/tty if available if { exec 3< /dev/tty; } 2>/dev/null; then # /dev/tty is available and usable echo -n "$prompt" IFS= read -r "$var_name" <&3 exec 3<&- else # No TTY available, read from stdin (which may be piped) echo -n "$prompt" IFS= read -r "$var_name" fi fi } print_error() { echo -e "${RED}[ERROR] $1${NC}" >&2 } print_success() { echo -e "${GREEN}[SUCCESS] $1${NC}" } print_info() { echo -e "${YELLOW}[INFO] $1${NC}" } check_root() { if [[ $EUID -ne 0 ]]; then print_error "This script must be run as root" exit 1 fi } # V3 is deprecated - no longer checking for it detect_os() { if [[ -f /etc/os-release ]]; then . /etc/os-release OS=$ID VER=$VERSION_ID else print_error "Cannot detect OS" exit 1 fi } check_proxmox_host() { # Check if this is a Proxmox VE host if command -v pvesh &> /dev/null && [[ -d /etc/pve ]]; then return 0 fi return 1 } check_docker_environment() { # Detect if we're running inside Docker if [[ -f /.dockerenv ]] || grep -q docker /proc/1/cgroup 2>/dev/null; then print_error "Docker environment detected" echo "Please use the Docker image directly: docker run -d -p 7655:7655 rcourtman/pulse:latest" echo "See: https://github.com/rcourtman/Pulse/blob/main/docs/DOCKER.md" exit 1 fi } create_lxc_container() { # Set up trap to cleanup on interrupt (CTID will be set later) trap 'echo ""; print_error "Installation cancelled"; exit 1' INT print_header echo "Proxmox VE detected. Installing Pulse in a container." echo # Check if we can interact with the user # Try to read from /dev/tty to test if we have terminal access if test -e /dev/tty && (echo -n "" > /dev/tty) 2>/dev/null; then # We have terminal access, show menu echo "Installation mode:" echo " 1) Quick (recommended)" echo " 2) Advanced" echo " 3) Cancel" safe_read "Select [1-3]: " mode -n 1 -r echo else # No terminal access - truly non-interactive echo "Non-interactive mode detected. Using Quick installation." mode="1" fi case $mode in 3) print_info "Installation cancelled" exit 0 ;; 2) ADVANCED_MODE=true ;; *) ADVANCED_MODE=false ;; esac # Get next available container ID from Proxmox local CTID=$(pvesh get /cluster/nextid 2>/dev/null || echo "100") # If pvesh failed, fallback to manual search if [[ "$CTID" == "100" ]]; then while pct status $CTID &>/dev/null 2>&1 || qm status $CTID &>/dev/null 2>&1; do ((CTID++)) done fi if [[ "$ADVANCED_MODE" == "true" ]]; then echo # Ask for port configuration safe_read "Frontend port [7655]: " frontend_port frontend_port=${frontend_port:-7655} if [[ ! "$frontend_port" =~ ^[0-9]+$ ]] || [[ "$frontend_port" -lt 1 ]] || [[ "$frontend_port" -gt 65535 ]]; then print_error "Invalid port number. Using default port 7655." frontend_port=7655 fi echo # Try to get cluster-wide IDs, fall back to local local USED_IDS="" if command -v pvesh &>/dev/null; then # Parse JSON output using grep and sed (works without jq) USED_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | \ grep -o '"vmid":[0-9]*' | \ sed 's/"vmid"://' | \ sort -n | \ paste -sd',' -) fi if [[ -z "$USED_IDS" ]]; then # Fallback: get local containers and VMs local LOCAL_CTS=$(pct list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n) local LOCAL_VMS=$(qm list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n) USED_IDS=$(echo -e "$LOCAL_CTS\n$LOCAL_VMS" | grep -v '^$' | sort -n | paste -sd',' -) fi echo "Container/VM IDs in use: ${USED_IDS:-none}" safe_read "Container ID [$CTID]: " custom_ctid if [[ -n "$custom_ctid" ]] && [[ "$custom_ctid" =~ ^[0-9]+$ ]]; then # Check if ID is in use if pct status $custom_ctid &>/dev/null 2>&1 || qm status $custom_ctid &>/dev/null 2>&1; then print_error "Container/VM ID $custom_ctid is already in use" exit 1 fi # Also check cluster if possible if command -v pvesh &>/dev/null; then if pvesh get /cluster/resources --type vm 2>/dev/null | jq -e ".[] | select(.vmid == $custom_ctid)" &>/dev/null; then print_error "Container/VM ID $custom_ctid is already in use in the cluster" exit 1 fi fi CTID=$custom_ctid fi fi print_info "Using container ID: $CTID" if [[ "$ADVANCED_MODE" == "true" ]]; then echo echo -e "${BLUE}Advanced Mode - Customize all settings${NC}" echo -e "${YELLOW}Defaults shown are suitable for monitoring 10-20 nodes${NC}" echo # Container settings safe_read "Container hostname [pulse]: " hostname hostname=${hostname:-pulse} safe_read "Memory (MB) [1024]: " memory memory=${memory:-1024} safe_read "Disk size (GB) [4]: " disk disk=${disk:-4} safe_read "CPU cores [2]: " cores cores=${cores:-2} safe_read "CPU limit (0=unlimited) [2]: " cpulimit cpulimit=${cpulimit:-2} safe_read "Swap (MB) [256]: " swap swap=${swap:-256} safe_read "Start on boot? [Y/n]: " onboot -n 1 -r echo if [[ "$onboot" =~ ^[Nn]$ ]]; then onboot=0 else onboot=1 fi safe_read "Enable firewall? [Y/n]: " firewall -n 1 -r echo if [[ "$firewall" =~ ^[Nn]$ ]]; then firewall=0 else firewall=1 fi safe_read "Unprivileged container? [Y/n]: " unprivileged -n 1 -r echo if [[ "$unprivileged" =~ ^[Nn]$ ]]; then unprivileged=0 else unprivileged=1 fi else # Quick mode - just use defaults silently # Use optimized defaults hostname="pulse" memory=1024 disk=4 cores=2 cpulimit=2 swap=256 onboot=1 firewall=1 unprivileged=1 frontend_port=7655 fi # Get available network bridges echo print_info "Detecting available resources..." # Get available bridges local BRIDGES=$(ip link show type bridge | grep -E '^[0-9]+:' | cut -d: -f2 | tr -d ' ' | paste -sd',' -) # First try to find the default network interface (could be bridge or regular interface) local DEFAULT_INTERFACE=$(ip route | grep default | head -1 | grep -oP 'dev \K\S+') # Check if the default interface is a bridge local DEFAULT_BRIDGE="" if [[ -n "$DEFAULT_INTERFACE" ]]; then if ip link show "$DEFAULT_INTERFACE" type bridge &>/dev/null; then # Default interface is a bridge, use it DEFAULT_BRIDGE="$DEFAULT_INTERFACE" fi fi # If no default bridge found, try to use the first available bridge if [[ -z "$DEFAULT_BRIDGE" && -n "$BRIDGES" ]]; then DEFAULT_BRIDGE=$(echo "$BRIDGES" | cut -d',' -f1) fi # If still no bridge found, we'll need to ask the user if [[ -z "$DEFAULT_BRIDGE" ]]; then if [[ -n "$DEFAULT_INTERFACE" ]]; then # We have a default interface but it's not a bridge print_info "Default network interface is $DEFAULT_INTERFACE (not a bridge)" fi DEFAULT_BRIDGE="vmbr0" # Fallback suggestion only fi # Get available storage with usage info local STORAGE_INFO=$(pvesm status -content rootdir 2>/dev/null | tail -n +2) local DEFAULT_STORAGE=$(echo "$STORAGE_INFO" | awk '{print $1}' | head -1) DEFAULT_STORAGE=${DEFAULT_STORAGE:-local-lvm} if [[ "$ADVANCED_MODE" == "true" ]]; then # Show available bridges echo if [[ -n "$BRIDGES" ]]; then echo "Available network bridges: $BRIDGES" else echo "No network bridges detected" echo "You may need to create a bridge first (e.g., vmbr0) or use the host's network interface" fi # If no valid default bridge, warn the user if [[ "$DEFAULT_BRIDGE" == "vmbr0" && ! -z "$BRIDGES" && ! "$BRIDGES" =~ vmbr0 ]]; then print_info "Warning: vmbr0 does not exist on this system" print_info "Please enter one of the available bridges: $BRIDGES" safe_read "Network bridge: " bridge else safe_read "Network bridge [$DEFAULT_BRIDGE]: " bridge bridge=${bridge:-$DEFAULT_BRIDGE} fi # Show available storage with usage details echo echo "Available storage pools:" echo "$STORAGE_INFO" | awk '{ # Convert KB to GB for readability avail_gb = $6 / 1048576 total_gb = $4 / 1048576 printf " %-15s %-8s %6.1f GB free of %6.1f GB (%s used)\n", $1, $2, avail_gb, total_gb, $7 }' || echo " No storage pools found" safe_read "Storage [$DEFAULT_STORAGE]: " storage storage=${storage:-$DEFAULT_STORAGE} safe_read "Static IP (leave empty for DHCP): " static_ip safe_read "DNS servers (comma-separated, empty for host settings): " nameserver safe_read "Startup order [99]: " startup startup=${startup:-99} else # Quick mode - but still need to verify critical settings # Network bridge selection echo if [[ -n "$BRIDGES" ]]; then echo "Available network bridges: $BRIDGES" else echo "No network bridges detected" fi # Always ask if default doesn't exist or no bridges found if [[ "$DEFAULT_BRIDGE" == "vmbr0" && -n "$BRIDGES" && ! "$BRIDGES" =~ vmbr0 ]]; then print_info "Default bridge vmbr0 not found" safe_read "Select network bridge: " bridge elif [[ -z "$BRIDGES" ]]; then print_error "No network bridges detected on this system" print_info "You may need to create a bridge first (e.g., vmbr0)" safe_read "Enter network bridge name to use: " bridge else safe_read "Network bridge [$DEFAULT_BRIDGE]: " bridge bridge=${bridge:-$DEFAULT_BRIDGE} fi # Storage selection echo if [[ -n "$STORAGE_INFO" ]]; then echo "Available storage pools:" echo "$STORAGE_INFO" | awk '{ # Convert KB to GB for readability avail_gb = $6 / 1048576 total_gb = $4 / 1048576 printf " %-15s %-8s %6.1f GB free of %6.1f GB (%s used)\n", $1, $2, avail_gb, total_gb, $7 }' else echo "No storage pools detected" fi # Always ask if default doesn't exist local STORAGE_LIST=$(echo "$STORAGE_INFO" | awk '{print $1}' | paste -sd',' -) if [[ "$DEFAULT_STORAGE" == "local-lvm" && -n "$STORAGE_LIST" && ! "$STORAGE_LIST" =~ local-lvm ]]; then print_info "Default storage local-lvm not found" safe_read "Select storage pool: " storage elif [[ -z "$STORAGE_INFO" ]]; then print_error "No storage pools detected" safe_read "Enter storage pool name to use: " storage else safe_read "Storage [$DEFAULT_STORAGE]: " storage storage=${storage:-$DEFAULT_STORAGE} fi static_ip="" nameserver="" startup=99 fi # Handle OS template selection echo if [[ "$ADVANCED_MODE" == "true" ]]; then echo "Available OS templates:" # Use pveam to list templates properly local TEMPLATES=$(pveam list local 2>/dev/null | tail -n +2 | awk '{print $1}' | sed 's/local:vztmpl\///' | nl -w2 -s') ') if [[ -n "$TEMPLATES" ]]; then echo "$TEMPLATES" echo echo "Or download a new template:" echo " d) Download Debian 12 (recommended)" echo " u) Download Ubuntu 22.04 LTS" echo " a) Download Alpine Linux (minimal)" echo safe_read "Select template number or option [Enter for Debian 12]: " template_choice if [[ -n "$template_choice" ]]; then case "$template_choice" in d|D) print_info "Downloading Debian 12..." pveam download local debian-12-standard_12.7-1_amd64.tar.zst TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst" ;; u|U) print_info "Downloading Ubuntu 22.04..." pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst TEMPLATE="/var/lib/vz/template/cache/ubuntu-22.04-standard_22.04-1_amd64.tar.zst" ;; a|A) print_info "Downloading Alpine Linux..." pveam download local alpine-3.18-default_20230607_amd64.tar.xz TEMPLATE="/var/lib/vz/template/cache/alpine-3.18-default_20230607_amd64.tar.xz" ;; [0-9]*) TEMPLATE_NAME=$(pveam list local 2>/dev/null | tail -n +2 | awk '{print $1}' | sed 's/local:vztmpl\///' | sed -n "${template_choice}p") if [[ -n "$TEMPLATE_NAME" ]]; then TEMPLATE="/var/lib/vz/template/cache/$TEMPLATE_NAME" print_info "Using template: $TEMPLATE_NAME" else TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst" print_info "Invalid selection, using Debian 12" fi ;; *) TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst" ;; esac else TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst" fi else TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst" fi else # Quick mode - use Debian 12 TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst" fi # Download template if it doesn't exist if [[ ! -f "$TEMPLATE" ]]; then print_info "Template not found, downloading Debian 12..." if ! pveam download local debian-12-standard_12.7-1_amd64.tar.zst; then print_error "Failed to download template. Please check your internet connection and try again." print_info "You can manually download with: pveam download local debian-12-standard_12.7-1_amd64.tar.zst" exit 1 fi TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst" if [[ ! -f "$TEMPLATE" ]]; then print_error "Template download succeeded but file not found at expected location" exit 1 fi fi print_info "Creating container..." # Build network configuration if [[ -n "$static_ip" ]]; then NET_CONFIG="name=eth0,bridge=${bridge},ip=${static_ip},firewall=${firewall}" else NET_CONFIG="name=eth0,bridge=${bridge},ip=dhcp,firewall=${firewall}" fi # Build container create command local CREATE_CMD="pct create $CTID $TEMPLATE" CREATE_CMD="$CREATE_CMD --hostname $hostname" CREATE_CMD="$CREATE_CMD --memory $memory" CREATE_CMD="$CREATE_CMD --cores $cores" if [[ "$cpulimit" != "0" ]]; then CREATE_CMD="$CREATE_CMD --cpulimit $cpulimit" fi CREATE_CMD="$CREATE_CMD --rootfs ${storage}:${disk}" CREATE_CMD="$CREATE_CMD --net0 $NET_CONFIG" CREATE_CMD="$CREATE_CMD --unprivileged $unprivileged" CREATE_CMD="$CREATE_CMD --features nesting=1" CREATE_CMD="$CREATE_CMD --onboot $onboot" CREATE_CMD="$CREATE_CMD --startup order=$startup" CREATE_CMD="$CREATE_CMD --protection 0" CREATE_CMD="$CREATE_CMD --swap $swap" if [[ -n "$nameserver" ]]; then CREATE_CMD="$CREATE_CMD --nameserver $nameserver" fi # Execute container creation (suppress verbose output) if ! eval $CREATE_CMD >/dev/null 2>&1; then print_error "Failed to create container" exit 1 fi # From this point on, cleanup container if we fail cleanup_on_error() { print_error "Installation failed, cleaning up container $CTID..." pct stop $CTID 2>/dev/null || true sleep 2 pct destroy $CTID 2>/dev/null || true exit 1 } # Start container print_info "Starting container..." if ! pct start $CTID >/dev/null 2>&1; then print_error "Failed to start container" cleanup_on_error fi sleep 3 # Wait for network to be ready print_info "Waiting for network..." local network_ready=false for i in {1..60}; do if pct exec $CTID -- ping -c 1 8.8.8.8 &>/dev/null 2>&1; then network_ready=true break fi sleep 1 done if [[ "$network_ready" != "true" ]]; then print_error "Container network failed to come up after 60 seconds" cleanup_on_error fi # Install dependencies and optimize container print_info "Installing dependencies..." if ! pct exec $CTID -- bash -c " apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq curl wget ca-certificates >/dev/null 2>&1 # Set timezone to UTC for consistent logging ln -sf /usr/share/zoneinfo/UTC /etc/localtime 2>/dev/null # Optimize sysctl for monitoring workload echo 'net.core.somaxconn=1024' >> /etc/sysctl.conf echo 'net.ipv4.tcp_keepalive_time=60' >> /etc/sysctl.conf sysctl -p >/dev/null 2>&1 # Create convenient update script cat > /usr/local/bin/update << 'EOF' #!/bin/bash echo 'Updating Pulse...' curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash EOF chmod +x /usr/local/bin/update "; then print_error "Failed to install dependencies in container" cleanup_on_error fi # Install Pulse inside container print_info "Installing Pulse..." # When piped through curl, $0 is "bash" not the script. Download fresh copy. local script_source="/tmp/pulse_install_$$.sh" if [[ "$0" == "bash" ]] || [[ ! -f "$0" ]]; then # We're being piped, download the script if ! curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh > "$script_source" 2>/dev/null; then print_error "Failed to download install script" cleanup_on_error fi else # We have a local script file script_source="$0" fi # Copy this script to container and run it if ! pct push $CTID "$script_source" /tmp/install.sh >/dev/null 2>&1; then print_error "Failed to copy install script to container" cleanup_on_error fi # Clean up temp file if we created one if [[ "$script_source" == "/tmp/pulse_install_"* ]]; then rm -f "$script_source" fi # Run installation quietly (suppress verbose output) with port configuration local install_cmd="bash /tmp/install.sh --in-container" if [[ "$frontend_port" != "7655" ]]; then install_cmd="FRONTEND_PORT=$frontend_port $install_cmd" fi local install_output=$(pct exec $CTID -- bash -c "$install_cmd" 2>&1) local install_status=$? if [[ $install_status -ne 0 ]]; then print_error "Failed to install Pulse inside container" cleanup_on_error fi # Get container IP local IP=$(pct exec $CTID -- hostname -I | awk '{print $1}') # Clean final output echo print_success "Pulse installation complete!" echo echo " Web UI: http://${IP}:${frontend_port}" echo " Container: $CTID" echo echo " Commands:" echo " pct enter $CTID # Enter container" echo " pct exec $CTID -- update # Update Pulse" echo exit 0 } # Compare two version strings # Returns: 0 if equal, 1 if first > second, 2 if first < second compare_versions() { local v1="${1#v}" # Remove 'v' prefix local v2="${2#v}" # Strip any pre-release suffix (e.g., -rc.1, -beta, etc.) local base_v1="${v1%%-*}" local base_v2="${v2%%-*}" local suffix_v1="${v1#*-}" local suffix_v2="${v2#*-}" # If no suffix, suffix equals the full version [[ "$suffix_v1" == "$v1" ]] && suffix_v1="" [[ "$suffix_v2" == "$v2" ]] && suffix_v2="" # Split base versions into parts IFS='.' read -ra V1_PARTS <<< "$base_v1" IFS='.' read -ra V2_PARTS <<< "$base_v2" # Compare major.minor.patch for i in 0 1 2; do local p1="${V1_PARTS[$i]:-0}" local p2="${V2_PARTS[$i]:-0}" if [[ "$p1" -gt "$p2" ]]; then return 1 elif [[ "$p1" -lt "$p2" ]]; then return 2 fi done # Base versions are equal, now compare suffixes # No suffix (stable) > rc suffix if [[ -z "$suffix_v1" ]] && [[ -n "$suffix_v2" ]]; then return 1 # v1 (stable) > v2 (rc) elif [[ -n "$suffix_v1" ]] && [[ -z "$suffix_v2" ]]; then return 2 # v1 (rc) < v2 (stable) elif [[ -n "$suffix_v1" ]] && [[ -n "$suffix_v2" ]]; then # Both have suffixes, compare them lexicographically if [[ "$suffix_v1" > "$suffix_v2" ]]; then return 1 elif [[ "$suffix_v1" < "$suffix_v2" ]]; then return 2 fi fi return 0 # versions are equal } check_existing_installation() { CURRENT_VERSION="" # Make it global so we can use it later local BINARY_PATH="" # Check for the binary in expected locations if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then BINARY_PATH="$INSTALL_DIR/bin/pulse" elif [[ -f "$INSTALL_DIR/pulse" ]]; then BINARY_PATH="$INSTALL_DIR/pulse" fi # Try to get version if binary exists if [[ -n "$BINARY_PATH" ]]; then CURRENT_VERSION=$($BINARY_PATH --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?' | head -1 || echo "unknown") fi if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then print_info "Pulse $CURRENT_VERSION is currently running" else print_info "Pulse is currently running" fi return 0 elif [[ -n "$BINARY_PATH" ]]; then if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then print_info "Pulse $CURRENT_VERSION is installed but not running" else print_info "Pulse is installed but not running" fi return 0 else return 1 fi } install_dependencies() { print_info "Installing dependencies..." apt-get update -qq >/dev/null 2>&1 apt-get install -y -qq curl wget >/dev/null 2>&1 } create_user() { if ! id -u pulse &>/dev/null; then print_info "Creating pulse user..." useradd --system --home-dir $INSTALL_DIR --shell /bin/false pulse fi } backup_existing() { if [[ -d "$CONFIG_DIR" ]]; then print_info "Backing up existing configuration..." cp -a "$CONFIG_DIR" "${CONFIG_DIR}.backup.$(date +%Y%m%d-%H%M%S)" fi } download_pulse() { print_info "Downloading Pulse..." # Check for forced version first if [[ -n "${FORCE_VERSION}" ]]; then LATEST_RELEASE="${FORCE_VERSION}" print_info "Installing specific version: $LATEST_RELEASE" # Verify the version exists if ! curl -fsS "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$LATEST_RELEASE" > /dev/null 2>&1; then print_error "Version $LATEST_RELEASE not found" exit 1 fi else # UPDATE_CHANNEL should already be set by main(), but set default if not if [[ -z "${UPDATE_CHANNEL:-}" ]]; then UPDATE_CHANNEL="stable" # Allow override via command line if [[ -n "${FORCE_CHANNEL}" ]]; then UPDATE_CHANNEL="${FORCE_CHANNEL}" print_info "Using $UPDATE_CHANNEL channel from command line" elif [[ -f "$CONFIG_DIR/system.json" ]]; then CONFIGURED_CHANNEL=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/') if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then UPDATE_CHANNEL="rc" print_info "RC channel detected in configuration" fi fi fi # Get appropriate release based on channel if [[ "$UPDATE_CHANNEL" == "rc" ]]; then # Get all releases and find the latest (including pre-releases) LATEST_RELEASE=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/') else # Get latest stable release only LATEST_RELEASE=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') fi if [[ -z "$LATEST_RELEASE" ]]; then print_error "Could not determine latest release" exit 1 fi print_info "Latest version: $LATEST_RELEASE" fi # Detect architecture ARCH=$(uname -m) case $ARCH in x86_64) PULSE_ARCH="amd64" ;; aarch64) PULSE_ARCH="arm64" ;; armv7l) PULSE_ARCH="armv7" ;; *) print_error "Unsupported architecture: $ARCH" exit 1 ;; esac print_info "Detected architecture: $ARCH ($PULSE_ARCH)" # Download architecture-specific release DOWNLOAD_URL="https://github.com/$GITHUB_REPO/releases/download/$LATEST_RELEASE/pulse-${LATEST_RELEASE}-linux-${PULSE_ARCH}.tar.gz" print_info "Downloading from: $DOWNLOAD_URL" # Detect and stop existing service BEFORE downloading (to free the binary) EXISTING_SERVICE=$(detect_service_name) if systemctl is-active --quiet $EXISTING_SERVICE; then print_info "Stopping existing Pulse service ($EXISTING_SERVICE)..." systemctl stop $EXISTING_SERVICE sleep 2 # Give the process time to fully stop and release the binary fi cd /tmp if ! wget -q -O pulse.tar.gz "$DOWNLOAD_URL"; then print_error "Failed to download Pulse release" exit 1 fi # Extract to temporary directory first TEMP_EXTRACT="/tmp/pulse-extract-$$" mkdir -p "$TEMP_EXTRACT" tar -xzf pulse.tar.gz -C "$TEMP_EXTRACT" # Ensure install directory and bin subdirectory exist mkdir -p "$INSTALL_DIR/bin" # Copy Pulse binary to the correct location (/opt/pulse/bin/pulse) if [[ -f "$TEMP_EXTRACT/bin/pulse" ]]; then cp "$TEMP_EXTRACT/bin/pulse" "$INSTALL_DIR/bin/pulse" elif [[ -f "$TEMP_EXTRACT/pulse" ]]; then # Fallback for old archives (pre-v4.3.1) cp "$TEMP_EXTRACT/pulse" "$INSTALL_DIR/bin/pulse" else print_error "Pulse binary not found in archive" exit 1 fi chmod +x "$INSTALL_DIR/bin/pulse" chown -R pulse:pulse "$INSTALL_DIR" # Create symlink in /usr/local/bin for PATH convenience ln -sf "$INSTALL_DIR/bin/pulse" /usr/local/bin/pulse print_success "Pulse binary installed to $INSTALL_DIR/bin/pulse" print_success "Symlink created at /usr/local/bin/pulse" # Copy VERSION file if present if [[ -f "$TEMP_EXTRACT/VERSION" ]]; then cp "$TEMP_EXTRACT/VERSION" "$INSTALL_DIR/VERSION" chown pulse:pulse "$INSTALL_DIR/VERSION" fi # Cleanup rm -rf "$TEMP_EXTRACT" pulse.tar.gz } setup_directories() { print_info "Setting up directories..." # Create directories mkdir -p "$CONFIG_DIR" mkdir -p "$INSTALL_DIR" # Set permissions chown -R pulse:pulse "$CONFIG_DIR" "$INSTALL_DIR" chmod 700 "$CONFIG_DIR" } setup_auto_updates() { print_info "Setting up automatic updates..." # Copy auto-update script if it exists in the release if [[ -f "$INSTALL_DIR/scripts/pulse-auto-update.sh" ]]; then cp "$INSTALL_DIR/scripts/pulse-auto-update.sh" /usr/local/bin/pulse-auto-update.sh chmod +x /usr/local/bin/pulse-auto-update.sh else # Download from GitHub if not in release print_info "Downloading auto-update script..." if ! curl -fsSL "https://raw.githubusercontent.com/$GITHUB_REPO/main/scripts/pulse-auto-update.sh" -o /usr/local/bin/pulse-auto-update.sh; then print_error "Failed to download auto-update script" return 1 fi chmod +x /usr/local/bin/pulse-auto-update.sh fi # Install systemd timer and service cat > /etc/systemd/system/pulse-update.service << 'EOF' [Unit] Description=Automatic Pulse update check and install Documentation=https://github.com/rcourtman/Pulse After=network-online.target Wants=network-online.target [Service] Type=oneshot User=root Group=root ExecStart=/usr/local/bin/pulse-auto-update.sh Restart=no TimeoutStartSec=600 StandardOutput=journal StandardError=journal SyslogIdentifier=pulse-update PrivateTmp=yes ProtectHome=yes ProtectSystem=strict ReadWritePaths=/opt/pulse /etc/pulse /tmp PrivateNetwork=no Nice=10 [Install] WantedBy=multi-user.target EOF cat > /etc/systemd/system/pulse-update.timer << 'EOF' [Unit] Description=Daily check for Pulse updates Documentation=https://github.com/rcourtman/Pulse After=network-online.target Wants=network-online.target [Timer] OnCalendar=daily OnCalendar=02:00 RandomizedDelaySec=4h Persistent=true AccuracySec=1h [Install] WantedBy=timers.target EOF systemctl daemon-reload >/dev/null 2>&1 # Enable timer but don't start it yet systemctl enable pulse-update.timer >/dev/null 2>&1 # Update system.json to enable auto-updates if [[ -f "$CONFIG_DIR/system.json" ]]; then # Update existing file local temp_file="/tmp/system_$$.json" if command -v jq &> /dev/null; then jq '.autoUpdateEnabled = true' "$CONFIG_DIR/system.json" > "$temp_file" && mv "$temp_file" "$CONFIG_DIR/system.json" else # Fallback to sed if jq not available sed -i 's/"pollingInterval"/"autoUpdateEnabled":true,"pollingInterval"/' "$CONFIG_DIR/system.json" 2>/dev/null || \ sed -i 's/^{/{"autoUpdateEnabled":true,/' "$CONFIG_DIR/system.json" 2>/dev/null || true fi else # Create new file with auto-updates enabled echo '{"autoUpdateEnabled":true,"pollingInterval":5}' > "$CONFIG_DIR/system.json" fi chown pulse:pulse "$CONFIG_DIR/system.json" 2>/dev/null || true # Start the timer systemctl start pulse-update.timer >/dev/null 2>&1 print_success "Automatic updates enabled (daily check with 2-6 hour random delay)" } install_systemd_service() { print_info "Installing systemd service..." # Use existing service name if found, otherwise use default EXISTING_SERVICE=$(detect_service_name) if [[ "$EXISTING_SERVICE" == "pulse-backend" ]] && [[ -f "/etc/systemd/system/pulse-backend.service" ]]; then # Keep using pulse-backend for compatibility (ProxmoxVE) SERVICE_NAME="pulse-backend" print_info "Using existing service name: pulse-backend" fi cat > /etc/systemd/system/$SERVICE_NAME.service << EOF [Unit] Description=Pulse Monitoring Server After=network.target [Service] Type=simple User=pulse Group=pulse WorkingDirectory=$INSTALL_DIR ExecStart=$INSTALL_DIR/bin/pulse Restart=always RestartSec=3 StandardOutput=journal StandardError=journal Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" Environment="PULSE_DATA_DIR=$CONFIG_DIR" EOF # Add port configuration if not default if [[ "${FRONTEND_PORT:-7655}" != "7655" ]]; then cat >> /etc/systemd/system/$SERVICE_NAME.service << EOF Environment="FRONTEND_PORT=$FRONTEND_PORT" EOF fi cat >> /etc/systemd/system/$SERVICE_NAME.service << EOF # Security hardening NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=$INSTALL_DIR $CONFIG_DIR [Install] WantedBy=multi-user.target EOF systemctl daemon-reload >/dev/null 2>&1 } start_pulse() { print_info "Starting Pulse..." systemctl enable $SERVICE_NAME >/dev/null 2>&1 systemctl start $SERVICE_NAME # Wait for service to start sleep 3 if systemctl is-active --quiet $SERVICE_NAME; then print_success "Pulse started successfully" else print_error "Failed to start Pulse" journalctl -u $SERVICE_NAME -n 20 exit 1 fi } print_completion() { local IP=$(hostname -I | awk '{print $1}') # Get the port from the service file or use default local PORT="${FRONTEND_PORT:-7655}" if [[ -z "${FRONTEND_PORT:-}" ]] && [[ -f "/etc/systemd/system/$SERVICE_NAME.service" ]]; then # Try to extract port from service file PORT=$(grep -oP 'FRONTEND_PORT=\K\d+' "/etc/systemd/system/$SERVICE_NAME.service" 2>/dev/null || echo "7655") fi echo print_header print_success "Pulse installation completed!" echo echo -e "${GREEN}Access Pulse at:${NC} http://${IP}:${PORT}" echo echo -e "${YELLOW}Quick commands:${NC}" echo " systemctl status $SERVICE_NAME - Check status" echo " systemctl restart $SERVICE_NAME - Restart" echo " journalctl -u $SERVICE_NAME -f - View logs" echo echo -e "${YELLOW}Management:${NC}" echo " Update: curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash" echo " Reset: curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash -s -- --reset" echo " Uninstall: curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash -s -- --uninstall" # Show auto-update status if timer exists if systemctl list-unit-files --no-legend | grep -q "^pulse-update.timer"; then echo echo -e "${YELLOW}Auto-updates:${NC}" if systemctl is-enabled --quiet pulse-update.timer 2>/dev/null; then echo " Status: ${GREEN}Enabled${NC} (daily check between 2-6 AM)" echo " Disable: systemctl disable --now pulse-update.timer" else echo " Status: Disabled" echo " Enable: systemctl enable --now pulse-update.timer" fi fi echo } # Main installation flow main() { # Skip Proxmox host check if we're already inside a container if [[ "$IN_CONTAINER" != "true" ]] && check_proxmox_host; then create_lxc_container exit 0 fi print_header check_root detect_os check_docker_environment # Check for existing installation FIRST before asking for configuration if check_existing_installation; then # If a specific version was requested, just update to it if [[ -n "${FORCE_VERSION}" ]]; then # Determine if this is an upgrade, downgrade, or reinstall local action_word="Installing" if [[ -n "$CURRENT_VERSION" ]] && [[ "$CURRENT_VERSION" != "unknown" ]]; then local compare_result compare_versions "$FORCE_VERSION" "$CURRENT_VERSION" && compare_result=$? || compare_result=$? case $compare_result in 0) action_word="Reinstalling" ;; 1) action_word="Updating to" ;; 2) action_word="Downgrading to" ;; esac fi print_info "${action_word} version ${FORCE_VERSION}..." LATEST_RELEASE="${FORCE_VERSION}" # Detect the actual service name before trying to stop it SERVICE_NAME=$(detect_service_name) backup_existing systemctl stop $SERVICE_NAME || true create_user download_pulse start_pulse print_completion return 0 fi # Get both stable and RC versions # Try GitHub API first, but have a fallback local STABLE_VERSION=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' 2>/dev/null || true) # If rate limited or failed, try direct GitHub latest URL if [[ -z "$STABLE_VERSION" ]] || [[ "$STABLE_VERSION" == *"rate limit"* ]]; then # Use the GitHub latest release redirect to get version STABLE_VERSION=$(curl -sI https://github.com/$GITHUB_REPO/releases/latest | grep -i '^location:' | sed -E 's|.*tag/([^[:space:]]+).*|\1|' | tr -d '\r' || true) fi # For RC, we need the API, so if it fails just use empty local RC_VERSION="" RC_VERSION=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases 2>/dev/null | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/' || true) # Determine default update channel UPDATE_CHANNEL="stable" # Allow override via command line if [[ -n "${FORCE_CHANNEL}" ]]; then UPDATE_CHANNEL="${FORCE_CHANNEL}" elif [[ -f "$CONFIG_DIR/system.json" ]]; then CONFIGURED_CHANNEL=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/' || true) if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then UPDATE_CHANNEL="rc" fi fi echo echo "What would you like to do?" # Show update options based on available versions local menu_option=1 if [[ -n "$STABLE_VERSION" ]] && [[ "$STABLE_VERSION" != "$CURRENT_VERSION" ]]; then echo "${menu_option}) Update to $STABLE_VERSION (stable)" ((menu_option++)) fi if [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]] && [[ "$RC_VERSION" != "$CURRENT_VERSION" ]]; then echo "${menu_option}) Update to $RC_VERSION (release candidate)" ((menu_option++)) fi echo "${menu_option}) Reinstall current version" ((menu_option++)) echo "${menu_option}) Remove Pulse" ((menu_option++)) echo "${menu_option}) Cancel" local max_option=$menu_option # In non-interactive container mode, auto-select update if [[ "$IN_CONTAINER" == "true" ]] && ! test -t 0; then print_info "Non-interactive mode detected. Auto-selecting update option." # Select stable update by default, or RC if configured if [[ "$UPDATE_CHANNEL" == "rc" ]] && [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]]; then choice=2 # RC version else choice=1 # Stable version fi else safe_read "Select option [1-${max_option}]: " choice fi # Debug: Check if choice was read correctly if [[ -z "$choice" ]]; then print_error "No option selected. Exiting." exit 1 fi # Debug output to see what's happening # print_info "DEBUG: You selected option $choice" # Determine what action to take based on the dynamic menu local action="" local target_version="" local current_choice=1 # Check if user selected stable update if [[ -n "$STABLE_VERSION" ]] && [[ "$STABLE_VERSION" != "$CURRENT_VERSION" ]]; then if [[ "$choice" == "$current_choice" ]]; then action="update" target_version="$STABLE_VERSION" UPDATE_CHANNEL="stable" fi ((current_choice++)) fi # Check if user selected RC update if [[ -n "$RC_VERSION" ]] && [[ "$RC_VERSION" != "$STABLE_VERSION" ]] && [[ "$RC_VERSION" != "$CURRENT_VERSION" ]]; then if [[ "$choice" == "$current_choice" ]]; then action="update" target_version="$RC_VERSION" UPDATE_CHANNEL="rc" fi ((current_choice++)) fi # Check if user selected reinstall if [[ "$choice" == "$current_choice" ]]; then action="reinstall" fi ((current_choice++)) # Check if user selected remove if [[ "$choice" == "$current_choice" ]]; then action="remove" fi ((current_choice++)) # Check if user selected cancel if [[ "$choice" == "$current_choice" ]]; then action="cancel" fi # Debug: Show what action was determined # print_info "DEBUG: Action determined: ${action:-'none'}" case $action in update) # Determine if this is an upgrade or downgrade local action_word="Installing" if [[ -n "$CURRENT_VERSION" ]] && [[ "$CURRENT_VERSION" != "unknown" ]]; then local cmp_result=0 compare_versions "$target_version" "$CURRENT_VERSION" || cmp_result=$? case $cmp_result in 0) action_word="Reinstalling" ;; 1) action_word="Updating to" ;; 2) action_word="Downgrading to" ;; esac fi print_info "${action_word} $target_version..." LATEST_RELEASE="$target_version" # Check if auto-updates are not installed yet (upgrading from older version) # and prompt the user to enable them (unless already forced by flag) if [[ "$ENABLE_AUTO_UPDATES" != "true" ]] && [[ "$IN_CONTAINER" != "true" ]]; then if ! systemctl list-unit-files --no-legend 2>/dev/null | grep -q "^pulse-update.timer"; then echo echo -e "${YELLOW}New feature: Automatic updates!${NC}" echo "Pulse can now automatically install stable updates daily (between 2-6 AM)" echo "This keeps your installation secure and up-to-date." safe_read "Enable auto-updates? [Y/n]: " enable_updates # Default to yes for this prompt since they're already updating if [[ ! "$enable_updates" =~ ^[Nn]$ ]]; then ENABLE_AUTO_UPDATES=true fi fi fi backup_existing systemctl stop $SERVICE_NAME || true create_user download_pulse # Setup auto-updates if requested during update if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then setup_auto_updates fi start_pulse print_completion exit 0 ;; reinstall) # Check if auto-updates are not installed yet # and prompt the user to enable them (unless already forced by flag) if [[ "$ENABLE_AUTO_UPDATES" != "true" ]] && [[ "$IN_CONTAINER" != "true" ]]; then if ! systemctl list-unit-files --no-legend 2>/dev/null | grep -q "^pulse-update.timer"; then echo echo -e "${YELLOW}New feature: Automatic updates!${NC}" echo "Pulse can now automatically install stable updates daily (between 2-6 AM)" echo "This keeps your installation secure and up-to-date." safe_read "Enable auto-updates? [Y/n]: " enable_updates # Default to yes for this prompt if [[ ! "$enable_updates" =~ ^[Nn]$ ]]; then ENABLE_AUTO_UPDATES=true fi fi fi backup_existing systemctl stop $SERVICE_NAME || true create_user download_pulse setup_directories install_systemd_service # Setup auto-updates if requested during reinstall if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then setup_auto_updates fi start_pulse print_completion exit 0 ;; remove) # Stop and disable service systemctl stop $SERVICE_NAME 2>/dev/null || true systemctl disable $SERVICE_NAME 2>/dev/null || true # Remove service files rm -f /etc/systemd/system/$SERVICE_NAME.service rm -f /etc/systemd/system/pulse.service rm -f /etc/systemd/system/pulse-backend.service systemctl daemon-reload >/dev/null 2>&1 # Remove installation directory rm -rf "$INSTALL_DIR" # Remove symlink rm -f /usr/local/bin/pulse # Ask about config/data removal echo print_info "Config and data files exist in $CONFIG_DIR" safe_read "Remove all configuration and data? (y/N): " remove_config if [[ "$remove_config" =~ ^[Yy]$ ]]; then rm -rf "$CONFIG_DIR" print_success "Configuration and data removed" else print_info "Configuration preserved in $CONFIG_DIR" fi # Ask about user removal if id "pulse" &>/dev/null; then safe_read "Remove pulse user account? (y/N): " remove_user if [[ "$remove_user" =~ ^[Yy]$ ]]; then userdel pulse 2>/dev/null || true print_success "User account removed" else print_info "User account preserved" fi fi # Remove any log files rm -f /var/log/pulse*.log 2>/dev/null || true rm -f /opt/pulse.log 2>/dev/null || true print_success "Pulse removed successfully" ;; cancel) print_info "Installation cancelled" exit 0 ;; *) print_error "Invalid option" exit 1 ;; esac else # Check if this is truly a fresh installation or an update # If binary exists OR config exists OR --version was specified, it's likely an update if [[ -f "$INSTALL_DIR/bin/pulse" ]] || [[ -f "$INSTALL_DIR/pulse" ]] || [[ -d "$CONFIG_DIR" ]] || [[ -n "${FORCE_VERSION}" ]]; then # This is an update/reinstall, don't prompt for port FRONTEND_PORT=${FRONTEND_PORT:-7655} else # Fresh installation - ask for port configuration and auto-updates FRONTEND_PORT=${FRONTEND_PORT:-} if [[ -z "$FRONTEND_PORT" ]]; then if [[ "$IN_CONTAINER" == "true" ]]; then # In container mode, use default port without prompting FRONTEND_PORT=7655 else echo safe_read "Frontend port [7655]: " FRONTEND_PORT FRONTEND_PORT=${FRONTEND_PORT:-7655} if [[ ! "$FRONTEND_PORT" =~ ^[0-9]+$ ]] || [[ "$FRONTEND_PORT" -lt 1 ]] || [[ "$FRONTEND_PORT" -gt 65535 ]]; then print_error "Invalid port number. Using default port 7655." FRONTEND_PORT=7655 fi fi fi # Ask about auto-updates for fresh installation (unless forced by flag) if [[ "$ENABLE_AUTO_UPDATES" != "true" ]] && [[ "$IN_CONTAINER" != "true" ]]; then echo echo "Enable automatic updates?" echo "Pulse can automatically install stable updates daily (between 2-6 AM)" safe_read "Enable auto-updates? [y/N]: " enable_updates if [[ "$enable_updates" =~ ^[Yy]$ ]]; then ENABLE_AUTO_UPDATES=true fi fi fi install_dependencies create_user setup_directories download_pulse install_systemd_service # Setup auto-updates if requested if [[ "$ENABLE_AUTO_UPDATES" == "true" ]]; then setup_auto_updates fi start_pulse print_completion fi } # Uninstall function uninstall_pulse() { check_root print_header echo -e "\033[0;33mUninstalling Pulse...\033[0m" echo # Detect service name local SERVICE_NAME=$(detect_service_name) # Stop and disable service if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then echo "Stopping $SERVICE_NAME..." systemctl stop $SERVICE_NAME fi if systemctl is-enabled --quiet $SERVICE_NAME 2>/dev/null; then echo "Disabling $SERVICE_NAME..." systemctl disable $SERVICE_NAME fi # Stop and disable auto-update timer if it exists if systemctl is-enabled --quiet pulse-update.timer 2>/dev/null; then echo "Disabling auto-update timer..." systemctl disable --now pulse-update.timer fi # Remove files echo "Removing Pulse files..." rm -rf /opt/pulse rm -rf /etc/pulse rm -f /etc/systemd/system/pulse.service rm -f /etc/systemd/system/pulse-backend.service rm -f /etc/systemd/system/pulse-update.service rm -f /etc/systemd/system/pulse-update.timer rm -f /usr/local/bin/pulse rm -f /usr/local/bin/pulse-auto-update.sh # Remove user (if it exists and isn't being used by other services) if id "pulse" &>/dev/null; then echo "Removing pulse user..." userdel pulse 2>/dev/null || true fi # Reload systemd systemctl daemon-reload echo echo -e "\033[0;32m✓ Pulse has been completely uninstalled\033[0m" exit 0 } # Reset function reset_pulse() { check_root print_header echo -e "\033[0;33mResetting Pulse configuration...\033[0m" echo # Detect service name local SERVICE_NAME=$(detect_service_name) # Stop service if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then echo "Stopping $SERVICE_NAME..." systemctl stop $SERVICE_NAME fi # Remove config but keep binary echo "Removing configuration and data..." rm -rf /etc/pulse/* # Restart service echo "Starting $SERVICE_NAME with fresh configuration..." systemctl start $SERVICE_NAME echo echo -e "\033[0;32m✓ Pulse has been reset to fresh configuration\033[0m" echo "Access Pulse at: http://$(hostname -I | awk '{print $1}'):7655" exit 0 } # Parse command line arguments FORCE_VERSION="" FORCE_CHANNEL="" IN_CONTAINER=false ENABLE_AUTO_UPDATES=false while [[ $# -gt 0 ]]; do case $1 in --uninstall) uninstall_pulse ;; --reset) reset_pulse ;; --rc|--pre|--prerelease) FORCE_CHANNEL="rc" shift ;; --stable) FORCE_CHANNEL="stable" shift ;; --version) FORCE_VERSION="$2" shift 2 ;; --in-container) IN_CONTAINER=true shift ;; --enable-auto-updates) ENABLE_AUTO_UPDATES=true shift ;; -h|--help) echo "Usage: $0 [OPTIONS]" echo "" echo "Installation options:" echo " --rc, --pre Install latest RC/pre-release version" echo " --stable Install latest stable version (default)" echo " --version VERSION Install specific version (e.g., v4.4.0-rc.1)" echo " --enable-auto-updates Enable automatic stable updates (via systemd timer)" echo "" echo "Management options:" echo " --reset Reset Pulse to fresh configuration" echo " --uninstall Completely remove Pulse from system" echo "" echo " -h, --help Show this help message" exit 0 ;; *) print_error "Unknown option: $1" echo "Use --help for usage information" exit 1 ;; esac done # Export for use in download_pulse function export FORCE_VERSION FORCE_CHANNEL # Run main function main