#!/bin/bash ############################################################################################# # # # Pelican Installer — The best way to install Pelican Panel & Wings # # # # Copyright (c) 2026, JarvisCLI # # https://github.com/everestmcarthur/pelican-installer # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License v3.0 # # # # Features: # # • Full CLI flags for automated/unattended installs # # • Supports Nginx, Apache, and Caddy (auto-SSL) web servers # # • Supports SQLite, MySQL, MariaDB, and PostgreSQL databases # # • Automatic SSL via Let's Encrypt or Caddy # # • Full Wings daemon installation with systemd + Docker # # • Writes all config to .env — panel works immediately after install # # • Runs database migrations automatically # # • Health checks & post-install verification # # • Colored terminal UI with step-by-step progress # # • Comprehensive logging to /var/log/pelican-installer.log # # • Uninstall & Update commands # # • Single script — no external dependencies # # # ############################################################################################# # ═══════════════════════════════════════════════════════════════════════════════ # Version & Defaults # ═══════════════════════════════════════════════════════════════════════════════ INSTALLER_VERSION="2.0.0" PELICAN_PANEL_URL="https://github.com/pelican-dev/panel/releases/latest/download/panel.tar.gz" PELICAN_WINGS_BASE="https://github.com/pelican-dev/wings/releases/latest/download" # ── Configurable variables (override via flags or environment) ── PELICAN_DIR="${PELICAN_DIR:-/var/www/pelican}" WINGS_DIR="${WINGS_DIR:-/etc/pelican}" WINGS_BIN="${WINGS_BIN:-/usr/local/bin/wings}" LOG_FILE="${LOG_FILE:-/var/log/pelican-installer.log}" FQDN="${FQDN:-}" WEBSERVER="${WEBSERVER:-}" # nginx | apache | caddy DB_CHOICE="${DB_CHOICE:-}" # sqlite | mysql | mariadb | postgres DB_HOST="${DB_HOST:-127.0.0.1}" DB_PORT="${DB_PORT:-}" # auto-set based on DB_CHOICE if empty DB_NAME="${DB_NAME:-pelican}" DB_USER="${DB_USER:-pelican}" DB_PASSWORD="${DB_PASSWORD:-}" # auto-generated if empty CONFIGURE_SSL="${CONFIGURE_SSL:-}" # true | false EMAIL="${EMAIL:-}" TIMEZONE="${TIMEZONE:-UTC}" APP_URL="${APP_URL:-}" # auto-set from FQDN if empty INSTALL_PANEL="${INSTALL_PANEL:-}" INSTALL_WINGS_FLAG="${INSTALL_WINGS_FLAG:-}" ASSUME_YES="${ASSUME_YES:-false}" ACTION="${ACTION:-}" # install | update | uninstall # ═══════════════════════════════════════════════════════════════════════════════ # Colors & Formatting # ═══════════════════════════════════════════════════════════════════════════════ if [[ -t 1 ]]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; MAGENTA='\033[0;35m'; CYAN='\033[0;36m' WHITE='\033[1;37m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' WHITE='' BOLD='' DIM='' RESET='' fi CURRENT_STEP=0 TOTAL_STEPS=0 # ═══════════════════════════════════════════════════════════════════════════════ # Logging & Output # ═══════════════════════════════════════════════════════════════════════════════ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; } print_header() { clear 2>/dev/null || true echo "" echo -e "${CYAN}${BOLD}" echo " ╔═══════════════════════════════════════════════════════════╗" echo " ║ ║" echo " ║ 🐦 Pelican Panel & Wings Installer 🐦 ║" echo " ║ ║" echo " ║ v${INSTALLER_VERSION} ║" echo " ╚═══════════════════════════════════════════════════════════╝" echo -e "${RESET}" echo "" } info() { echo -e " ${BLUE}ℹ${RESET} $*"; log "INFO: $*"; } success() { echo -e " ${GREEN}✓${RESET} $*"; log "OK: $*"; } warn() { echo -e " ${YELLOW}⚠${RESET} $*"; log "WARN: $*"; } error() { echo -e " ${RED}✗${RESET} $*"; log "ERROR: $*"; } fatal() { echo "" echo -e " ${RED}${BOLD}FATAL:${RESET} ${RED}$*${RESET}" log "FATAL: $*" echo -e " ${DIM}Check ${LOG_FILE} for details${RESET}" echo "" exit 1 } step() { CURRENT_STEP=$((CURRENT_STEP + 1)) echo "" if [[ $TOTAL_STEPS -gt 0 ]]; then echo -e " ${MAGENTA}${BOLD}[${CURRENT_STEP}/${TOTAL_STEPS}] $*${RESET}" else echo -e " ${MAGENTA}${BOLD}▸ $*${RESET}" fi echo -e " ${DIM}$(printf '%.0s─' {1..55})${RESET}" log "STEP ${CURRENT_STEP}: $*" } ask() { echo -e -n " ${CYAN}?${RESET} $* "; } confirm() { local prompt="$1" local default="${2:-Y}" if [[ "$ASSUME_YES" == true ]]; then return 0 fi ask "${prompt}" read -r answer answer="${answer:-$default}" [[ "$answer" =~ ^[Yy]$ ]] } spinner() { local pid=$1 local msg=$2 local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local i=0 while kill -0 "$pid" 2>/dev/null; do printf "\r ${CYAN}%s${RESET} %s" "${spin:i++%${#spin}:1}" "$msg" sleep 0.1 done printf "\r ${CYAN} ${RESET} %s\n" "$msg" } run_cmd() { local msg="$1" shift "$@" >> "$LOG_FILE" 2>&1 & local pid=$! spinner $pid "$msg" wait $pid local exit_code=$? if [[ $exit_code -ne 0 ]]; then error "$msg — FAILED (exit code $exit_code)" error "Check ${LOG_FILE} for details" return $exit_code fi return 0 } # ═══════════════════════════════════════════════════════════════════════════════ # CLI Argument Parsing # ═══════════════════════════════════════════════════════════════════════════════ show_help() { echo "" echo " Pelican Installer v${INSTALLER_VERSION}" echo "" echo " Usage: $0 [ACTION] [OPTIONS]" echo "" echo " Actions:" echo " install Install Panel and/or Wings (default)" echo " update Update Panel and/or Wings" echo " uninstall Uninstall Panel and/or Wings" echo "" echo " Components:" echo " --panel Install/update Panel" echo " --wings Install/update Wings" echo " --both Install/update Panel + Wings" echo "" echo " Panel options:" echo " --domain FQDN Panel domain name (e.g., panel.example.com)" echo " --webserver WS Web server: nginx, apache, caddy" echo " --database DB Database: sqlite, mysql, mariadb, postgres" echo " --db-host HOST Database host (default: 127.0.0.1)" echo " --db-port PORT Database port (auto-detected from db type)" echo " --db-name NAME Database name (default: pelican)" echo " --db-user USER Database user (default: pelican)" echo " --db-password PASS Database password (auto-generated if empty)" echo " --ssl Enable SSL via Let's Encrypt" echo " --no-ssl Disable SSL" echo " --email EMAIL Email for Let's Encrypt" echo " --timezone TZ Timezone (default: UTC)" echo "" echo " General:" echo " --yes, -y Skip all confirmation prompts" echo " --help, -h Show this help" echo "" echo " Examples:" echo " # Interactive install" echo " sudo bash install.sh" echo "" echo " # Fully automated panel install" echo " sudo bash install.sh install --panel --domain panel.example.com \\" echo " --webserver nginx --database postgres --ssl --email you@example.com -y" echo "" echo " # Wings only" echo " sudo bash install.sh install --wings -y" echo "" echo " # Update everything" echo " sudo bash install.sh update --both -y" echo "" exit 0 } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in install|update|uninstall) ACTION="$1" ;; --panel) INSTALL_PANEL=true ;; --wings) INSTALL_WINGS_FLAG=true ;; --both) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=true ;; --domain|--fqdn) FQDN="$2"; shift ;; --webserver|--ws) WEBSERVER="$2"; shift ;; --database|--db) DB_CHOICE="$2"; shift ;; --db-host) DB_HOST="$2"; shift ;; --db-port) DB_PORT="$2"; shift ;; --db-name) DB_NAME="$2"; shift ;; --db-user) DB_USER="$2"; shift ;; --db-password|--db-pass) DB_PASSWORD="$2"; shift ;; --ssl) CONFIGURE_SSL=true ;; --no-ssl) CONFIGURE_SSL=false ;; --email) EMAIL="$2"; shift ;; --timezone|--tz) TIMEZONE="$2"; shift ;; --yes|-y) ASSUME_YES=true ;; --help|-h) show_help ;; *) warn "Unknown option: $1" ;; esac shift done } # ═══════════════════════════════════════════════════════════════════════════════ # OS Detection # ═══════════════════════════════════════════════════════════════════════════════ detect_os() { step "Detecting system environment" if [[ ! -f /etc/os-release ]]; then fatal "Cannot detect operating system — /etc/os-release not found." fi . /etc/os-release OS_ID="$ID" OS_VERSION="$VERSION_ID" OS_NAME="$PRETTY_NAME" case "$OS_ID" in ubuntu|debian) OS_FAMILY="debian"; PKG_MANAGER="apt" ;; almalinux|rocky|centos|rhel) OS_FAMILY="rhel"; PKG_MANAGER="dnf" ;; *) if command -v apt &>/dev/null; then OS_FAMILY="debian"; PKG_MANAGER="apt" warn "Unknown OS '$OS_ID' — detected apt, proceeding as Debian-like" elif command -v dnf &>/dev/null; then OS_FAMILY="rhel"; PKG_MANAGER="dnf" warn "Unknown OS '$OS_ID' — detected dnf, proceeding as RHEL-like" else fatal "Unsupported OS: $OS_ID (no apt or dnf found)" fi ;; esac ARCH=$(uname -m) case "$ARCH" in x86_64) WINGS_ARCH="amd64" ;; aarch64|arm64) WINGS_ARCH="arm64" ;; *) fatal "Unsupported architecture: $ARCH" ;; esac success "OS: ${BOLD}$OS_NAME${RESET} ($OS_FAMILY/$PKG_MANAGER)" success "Architecture: ${BOLD}$ARCH${RESET} → Wings binary: $WINGS_ARCH" # Detect web server user if [[ "$OS_FAMILY" == "rhel" ]]; then case "${WEBSERVER:-nginx}" in nginx) WEBSERVER_USER="nginx" ;; apache) WEBSERVER_USER="apache" ;; caddy) WEBSERVER_USER="caddy" ;; *) WEBSERVER_USER="nginx" ;; esac else WEBSERVER_USER="www-data" fi } # ═══════════════════════════════════════════════════════════════════════════════ # Preflight Checks # ═══════════════════════════════════════════════════════════════════════════════ preflight_checks() { step "Running preflight checks" # Root check if [[ $EUID -ne 0 ]]; then fatal "This script must be run as root. Use: sudo bash $0" fi success "Running as root" # Essential tools for cmd in curl tar openssl; do if ! command -v "$cmd" &>/dev/null; then info "Installing $cmd..." $PKG_MANAGER install -y "$cmd" >> "$LOG_FILE" 2>&1 || fatal "Failed to install $cmd" fi success "$cmd available" done # Virtualization check (for Wings) if command -v systemd-detect-virt &>/dev/null; then local virt virt=$(systemd-detect-virt 2>/dev/null || echo "none") if [[ "$virt" == "openvz" || "$virt" == "lxc" ]]; then warn "Detected ${BOLD}$virt${RESET} — Docker/Wings may not work without nested virt" else success "Virtualization: $virt" fi fi # Memory local total_mem total_mem=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo "0") if [[ $total_mem -lt 1024 ]]; then warn "Low memory: ${total_mem}MB (1GB+ recommended)" else success "Memory: ${total_mem}MB" fi # Disk local free_disk free_disk=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G') if [[ $free_disk -lt 5 ]]; then warn "Low disk: ${free_disk}GB free (10GB+ recommended)" else success "Disk: ${free_disk}GB free" fi } # ═══════════════════════════════════════════════════════════════════════════════ # PHP Installation # ═══════════════════════════════════════════════════════════════════════════════ detect_php_version() { for ver in 8.4 8.3 8.2; do if [[ "$OS_FAMILY" == "debian" ]]; then if apt-cache show "php${ver}-fpm" &>/dev/null 2>&1; then PHP_VERSION="$ver"; return fi else if dnf list "php" 2>/dev/null | grep -q "$ver"; then PHP_VERSION="$ver"; return fi fi done PHP_VERSION="8.3" } install_php() { step "Installing PHP $PHP_VERSION & extensions" if [[ "$OS_FAMILY" == "debian" ]]; then # Add PHP repo if ! command -v add-apt-repository &>/dev/null; then apt install -y software-properties-common >> "$LOG_FILE" 2>&1 fi if [[ "$OS_ID" == "ubuntu" ]]; then add-apt-repository -y ppa:ondrej/php >> "$LOG_FILE" 2>&1 || true elif [[ "$OS_ID" == "debian" ]]; then apt install -y lsb-release apt-transport-https ca-certificates >> "$LOG_FILE" 2>&1 curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb >> "$LOG_FILE" 2>&1 dpkg -i /tmp/debsuryorg-archive-keyring.deb >> "$LOG_FILE" 2>&1 echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" \ > /etc/apt/sources.list.d/sury-php.list fi run_cmd "Updating package lists" apt update local pkgs=( "php${PHP_VERSION}" "php${PHP_VERSION}-fpm" "php${PHP_VERSION}-common" "php${PHP_VERSION}-cli" "php${PHP_VERSION}-gd" "php${PHP_VERSION}-mysql" "php${PHP_VERSION}-pgsql" "php${PHP_VERSION}-mbstring" "php${PHP_VERSION}-bcmath" "php${PHP_VERSION}-xml" "php${PHP_VERSION}-curl" "php${PHP_VERSION}-zip" "php${PHP_VERSION}-intl" "php${PHP_VERSION}-sqlite3" "php${PHP_VERSION}-tokenizer" "php${PHP_VERSION}-fileinfo" "php${PHP_VERSION}-dom" ) run_cmd "Installing PHP ${PHP_VERSION} and extensions" apt install -y "${pkgs[@]}" elif [[ "$OS_FAMILY" == "rhel" ]]; then dnf install -y epel-release >> "$LOG_FILE" 2>&1 dnf install -y "https://rpms.remirepo.net/enterprise/remi-release-$(rpm -E %rhel).rpm" >> "$LOG_FILE" 2>&1 || true dnf module reset php -y >> "$LOG_FILE" 2>&1 || true dnf module enable "php:remi-${PHP_VERSION}" -y >> "$LOG_FILE" 2>&1 || true run_cmd "Installing PHP ${PHP_VERSION} and extensions" dnf install -y \ php php-fpm php-common php-cli php-gd php-mysqlnd php-pgsql \ php-mbstring php-bcmath php-xml php-curl php-zip php-intl php-pdo php-tokenizer fi # Configure PHP-FPM pool local fpm_conf="" if [[ "$OS_FAMILY" == "debian" ]]; then fpm_conf="/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf" else fpm_conf="/etc/php-fpm.d/www.conf" fi if [[ -f "$fpm_conf" ]]; then sed -i "s/^user = .*/user = ${WEBSERVER_USER}/" "$fpm_conf" sed -i "s/^group = .*/group = ${WEBSERVER_USER}/" "$fpm_conf" sed -i "s/^listen.owner = .*/listen.owner = ${WEBSERVER_USER}/" "$fpm_conf" sed -i "s/^listen.group = .*/listen.group = ${WEBSERVER_USER}/" "$fpm_conf" success "PHP-FPM pool configured for ${WEBSERVER_USER}" fi systemctl enable --now "php${PHP_VERSION}-fpm" >> "$LOG_FILE" 2>&1 || \ systemctl enable --now php-fpm >> "$LOG_FILE" 2>&1 || true success "PHP-FPM started and enabled" } # ═══════════════════════════════════════════════════════════════════════════════ # Database Installation # ═══════════════════════════════════════════════════════════════════════════════ generate_db_password() { if [[ -z "$DB_PASSWORD" && "$DB_CHOICE" != "sqlite" ]]; then DB_PASSWORD=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32) log "Generated DB password: $DB_PASSWORD" fi } auto_set_db_port() { if [[ -z "$DB_PORT" ]]; then case "$DB_CHOICE" in mysql|mariadb) DB_PORT="3306" ;; postgres) DB_PORT="5432" ;; sqlite) DB_PORT="" ;; esac fi } install_database() { if [[ "$DB_CHOICE" == "sqlite" ]]; then info "Using SQLite — no database server needed" return fi step "Installing ${DB_CHOICE^} database server" case "$DB_CHOICE" in mysql) if [[ "$OS_FAMILY" == "debian" ]]; then run_cmd "Installing MySQL" apt install -y mysql-server mysql-client else run_cmd "Installing MySQL" dnf install -y mysql-server fi systemctl enable --now mysql >> "$LOG_FILE" 2>&1 || systemctl enable --now mysqld >> "$LOG_FILE" 2>&1 success "MySQL installed and running" ;; mariadb) if [[ "$OS_FAMILY" == "debian" ]]; then run_cmd "Installing MariaDB" apt install -y mariadb-server mariadb-client else run_cmd "Installing MariaDB" dnf install -y mariadb-server fi systemctl enable --now mariadb >> "$LOG_FILE" 2>&1 success "MariaDB installed and running" ;; postgres) if [[ "$OS_FAMILY" == "debian" ]]; then run_cmd "Installing PostgreSQL" apt install -y postgresql postgresql-client else run_cmd "Installing PostgreSQL" dnf install -y postgresql-server postgresql postgresql-setup --initdb >> "$LOG_FILE" 2>&1 || true fi systemctl enable --now postgresql >> "$LOG_FILE" 2>&1 success "PostgreSQL installed and running" ;; esac setup_database } setup_database() { step "Creating database & user" generate_db_password auto_set_db_port case "$DB_CHOICE" in mysql|mariadb) mysql -u root <> "$LOG_FILE" 2>&1 || { # User might already exist, try ALTER sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" >> "$LOG_FILE" 2>&1 } sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" >> "$LOG_FILE" 2>&1 || { sudo -u postgres psql -c "ALTER DATABASE ${DB_NAME} OWNER TO ${DB_USER};" >> "$LOG_FILE" 2>&1 } sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" >> "$LOG_FILE" 2>&1 # CRITICAL: Configure pg_hba.conf for password auth over TCP local pg_hba pg_hba=$(sudo -u postgres psql -t -c "SHOW hba_file;" 2>/dev/null | xargs) if [[ -n "$pg_hba" && -f "$pg_hba" ]]; then # Add md5 auth for our user before any existing host rules if ! grep -q "pelican" "$pg_hba" 2>/dev/null; then # Insert password auth rule at the top of the host section sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} 127.0.0.1/32 md5" "$pg_hba" sed -i "/^# IPv6 local connections:/a host ${DB_NAME} ${DB_USER} ::1/128 md5" "$pg_hba" success "pg_hba.conf configured for password authentication" systemctl reload postgresql >> "$LOG_FILE" 2>&1 fi else # Fallback: try common paths for hba_path in /etc/postgresql/*/main/pg_hba.conf /var/lib/pgsql/data/pg_hba.conf; do if [[ -f "$hba_path" ]]; then if ! grep -q "${DB_USER}" "$hba_path"; then sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} 127.0.0.1/32 md5" "$hba_path" sed -i "/^# IPv6 local connections:/a host ${DB_NAME} ${DB_USER} ::1/128 md5" "$hba_path" fi break fi done systemctl reload postgresql >> "$LOG_FILE" 2>&1 success "pg_hba.conf configured for password authentication" fi success "PostgreSQL database '${DB_NAME}' created with user '${DB_USER}'" ;; esac # Show credentials in a visible box echo "" echo -e " ${GREEN}${BOLD}┌─────────────────────────────────────────────┐${RESET}" echo -e " ${GREEN}${BOLD}│ DATABASE CREDENTIALS — SAVE THESE! │${RESET}" echo -e " ${GREEN}${BOLD}├─────────────────────────────────────────────┤${RESET}" echo -e " ${GREEN}${BOLD}│${RESET} Host: ${WHITE}${DB_HOST}${RESET}" echo -e " ${GREEN}${BOLD}│${RESET} Port: ${WHITE}${DB_PORT}${RESET}" echo -e " ${GREEN}${BOLD}│${RESET} Database: ${WHITE}${DB_NAME}${RESET}" echo -e " ${GREEN}${BOLD}│${RESET} User: ${WHITE}${DB_USER}${RESET}" echo -e " ${GREEN}${BOLD}│${RESET} Password: ${WHITE}${DB_PASSWORD}${RESET}" echo -e " ${GREEN}${BOLD}└─────────────────────────────────────────────┘${RESET}" echo "" } # ═══════════════════════════════════════════════════════════════════════════════ # Web Server Installation # ═══════════════════════════════════════════════════════════════════════════════ get_fpm_socket() { if [[ "$OS_FAMILY" == "rhel" ]]; then echo "/run/php-fpm/www.sock" else echo "/run/php/php${PHP_VERSION}-fpm.sock" fi } install_webserver() { step "Installing ${WEBSERVER^} web server" case "$WEBSERVER" in nginx) install_nginx ;; apache) install_apache ;; caddy) install_caddy ;; esac } install_nginx() { if [[ "$OS_FAMILY" == "debian" ]]; then run_cmd "Installing Nginx" apt install -y nginx else run_cmd "Installing Nginx" dnf install -y nginx fi rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled 2>/dev/null || true # Ensure sites-enabled is included on RHEL if [[ "$OS_FAMILY" == "rhel" ]] && ! grep -q "sites-enabled" /etc/nginx/nginx.conf 2>/dev/null; then sed -i '/http {/a \ include /etc/nginx/sites-enabled/*.conf;' /etc/nginx/nginx.conf fi local fpm_sock fpm_sock=$(get_fpm_socket) if [[ "$CONFIGURE_SSL" == true ]]; then write_nginx_ssl_config "$fpm_sock" else write_nginx_http_config "$fpm_sock" fi ln -sf /etc/nginx/sites-available/pelican.conf /etc/nginx/sites-enabled/pelican.conf nginx -t >> "$LOG_FILE" 2>&1 || warn "Nginx config test failed — check ${LOG_FILE}" systemctl enable --now nginx >> "$LOG_FILE" 2>&1 systemctl restart nginx >> "$LOG_FILE" 2>&1 success "Nginx configured and running" } write_nginx_ssl_config() { local fpm_sock="$1" cat > /etc/nginx/sites-available/pelican.conf < /etc/nginx/sites-available/pelican.conf <> "$LOG_FILE" 2>&1 || true a2dissite 000-default >> "$LOG_FILE" 2>&1 || true else run_cmd "Installing Apache" dnf install -y httpd mod_ssl fi local fpm_sock fpm_sock=$(get_fpm_socket) local conf_dir="/etc/apache2/sites-available" [[ "$OS_FAMILY" == "rhel" ]] && conf_dir="/etc/httpd/conf.d" if [[ "$CONFIGURE_SSL" == true ]]; then cat > "${conf_dir}/pelican.conf" < ServerName ${FQDN} Redirect permanent / https://${FQDN}/ ServerName ${FQDN} DocumentRoot ${PELICAN_DIR}/public AllowEncodedSlashes On php_value upload_max_filesize 100M php_value post_max_size 100M Require all granted AllowOverride all SSLEngine on SSLCertificateFile /etc/letsencrypt/live/${FQDN}/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/${FQDN}/privkey.pem APACHECFG else cat > "${conf_dir}/pelican.conf" < ServerName ${FQDN} DocumentRoot ${PELICAN_DIR}/public AllowEncodedSlashes On php_value upload_max_filesize 100M php_value post_max_size 100M Require all granted AllowOverride all APACHECFG fi if [[ "$OS_FAMILY" == "debian" ]]; then a2ensite pelican >> "$LOG_FILE" 2>&1 systemctl enable --now apache2 >> "$LOG_FILE" 2>&1 systemctl restart apache2 >> "$LOG_FILE" 2>&1 else systemctl enable --now httpd >> "$LOG_FILE" 2>&1 systemctl restart httpd >> "$LOG_FILE" 2>&1 fi success "Apache configured and running" } install_caddy() { if [[ "$OS_FAMILY" == "debian" ]]; then apt install -y debian-keyring debian-archive-keyring apt-transport-https >> "$LOG_FILE" 2>&1 curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \ gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 2>/dev/null curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \ tee /etc/apt/sources.list.d/caddy-stable.list >> "$LOG_FILE" 2>&1 apt update >> "$LOG_FILE" 2>&1 run_cmd "Installing Caddy" apt install -y caddy else dnf install -y 'dnf-command(copr)' >> "$LOG_FILE" 2>&1 dnf copr enable @caddy/caddy -y >> "$LOG_FILE" 2>&1 run_cmd "Installing Caddy" dnf install -y caddy fi local fpm_sock fpm_sock=$(get_fpm_socket) cat > /etc/caddy/Caddyfile <> "$LOG_FILE" 2>&1 systemctl restart caddy >> "$LOG_FILE" 2>&1 success "Caddy configured and running (auto-SSL enabled)" } # ═══════════════════════════════════════════════════════════════════════════════ # SSL (Let's Encrypt) # ═══════════════════════════════════════════════════════════════════════════════ install_ssl() { if [[ "$CONFIGURE_SSL" != true ]]; then return fi if [[ "$WEBSERVER" == "caddy" ]]; then info "Caddy handles SSL automatically — skipping certbot" return fi step "Obtaining SSL certificate via Let's Encrypt" if [[ "$OS_FAMILY" == "debian" ]]; then apt install -y certbot >> "$LOG_FILE" 2>&1 [[ "$WEBSERVER" == "nginx" ]] && apt install -y python3-certbot-nginx >> "$LOG_FILE" 2>&1 [[ "$WEBSERVER" == "apache" ]] && apt install -y python3-certbot-apache >> "$LOG_FILE" 2>&1 else dnf install -y certbot >> "$LOG_FILE" 2>&1 [[ "$WEBSERVER" == "nginx" ]] && dnf install -y python3-certbot-nginx >> "$LOG_FILE" 2>&1 [[ "$WEBSERVER" == "apache" ]] && dnf install -y python3-certbot-apache >> "$LOG_FILE" 2>&1 fi local plugin="--standalone" [[ "$WEBSERVER" == "nginx" ]] && plugin="--nginx" [[ "$WEBSERVER" == "apache" ]] && plugin="--apache" certbot certonly $plugin -d "$FQDN" --non-interactive --agree-tos --email "$EMAIL" >> "$LOG_FILE" 2>&1 || { warn "Plugin-based cert failed, trying standalone..." systemctl stop "${WEBSERVER}" 2>/dev/null || systemctl stop apache2 2>/dev/null || systemctl stop httpd 2>/dev/null || true certbot certonly --standalone -d "$FQDN" --non-interactive --agree-tos --email "$EMAIL" >> "$LOG_FILE" 2>&1 || \ fatal "Failed to obtain SSL certificate. Is the domain pointed to this server?" systemctl start "${WEBSERVER}" 2>/dev/null || systemctl start apache2 2>/dev/null || systemctl start httpd 2>/dev/null || true } # Auto-renewal cron (crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --deploy-hook \"systemctl restart ${WEBSERVER}\"") | sort -u | crontab - success "SSL certificate obtained — auto-renewal enabled" } # ═══════════════════════════════════════════════════════════════════════════════ # Pelican Panel Installation # ═══════════════════════════════════════════════════════════════════════════════ install_panel() { step "Downloading Pelican Panel" mkdir -p "$PELICAN_DIR" cd "$PELICAN_DIR" run_cmd "Downloading and extracting Pelican Panel" bash -c "curl -sSL '$PELICAN_PANEL_URL' | tar -xzv >> '$LOG_FILE' 2>&1" success "Panel files downloaded to ${PELICAN_DIR}" step "Installing Composer & PHP dependencies" if ! command -v composer &>/dev/null; then curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer >> "$LOG_FILE" 2>&1 success "Composer installed" fi run_cmd "Installing PHP dependencies (this takes ~1 minute)" \ env COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader --working-dir="$PELICAN_DIR" success "Composer dependencies installed" step "Configuring Pelican Panel" configure_env_file run_database_setup set_permissions setup_queue_worker setup_cron_job # Show APP_KEY warning local app_key app_key=$(grep "^APP_KEY=" "${PELICAN_DIR}/.env" 2>/dev/null | cut -d= -f2-) if [[ -n "$app_key" ]]; then echo "" echo -e " ${RED}${BOLD}┌─────────────────────────────────────────────────────────┐${RESET}" echo -e " ${RED}${BOLD}│ ⚠ ENCRYPTION KEY — BACK THIS UP! LOSING IT = DATA LOSS│${RESET}" echo -e " ${RED}${BOLD}├─────────────────────────────────────────────────────────┤${RESET}" echo -e " ${RED}${BOLD}│${RESET} APP_KEY=${WHITE}${app_key}${RESET}" echo -e " ${RED}${BOLD}└─────────────────────────────────────────────────────────┘${RESET}" echo "" fi } configure_env_file() { info "Writing .env configuration..." # Determine APP_URL if [[ -z "$APP_URL" ]]; then if [[ "$CONFIGURE_SSL" == true || "$WEBSERVER" == "caddy" ]]; then APP_URL="https://${FQDN}" else APP_URL="http://${FQDN}" fi fi # Determine DB connection string for Laravel local laravel_db_connection case "$DB_CHOICE" in mysql|mariadb) laravel_db_connection="mysql" ;; postgres) laravel_db_connection="pgsql" ;; sqlite) laravel_db_connection="sqlite" ;; esac # Copy example .env if needed if [[ ! -f "${PELICAN_DIR}/.env" && -f "${PELICAN_DIR}/.env.example" ]]; then cp "${PELICAN_DIR}/.env.example" "${PELICAN_DIR}/.env" fi # Generate APP_KEY php artisan key:generate --force --no-interaction >> "$LOG_FILE" 2>&1 success "Application key generated" # Helper to set a value in .env set_env() { local key="$1" local value="$2" if grep -q "^${key}=" "${PELICAN_DIR}/.env" 2>/dev/null; then sed -i "s|^${key}=.*|${key}=${value}|" "${PELICAN_DIR}/.env" else echo "${key}=${value}" >> "${PELICAN_DIR}/.env" fi } # Core settings set_env "APP_URL" "$APP_URL" set_env "APP_TIMEZONE" "$TIMEZONE" set_env "APP_ENV" "production" set_env "APP_DEBUG" "false" # Database settings set_env "DB_CONNECTION" "$laravel_db_connection" if [[ "$DB_CHOICE" == "sqlite" ]]; then set_env "DB_DATABASE" "${PELICAN_DIR}/database/database.sqlite" # Create SQLite file touch "${PELICAN_DIR}/database/database.sqlite" else set_env "DB_HOST" "$DB_HOST" set_env "DB_PORT" "$DB_PORT" set_env "DB_DATABASE" "$DB_NAME" set_env "DB_USERNAME" "$DB_USER" set_env "DB_PASSWORD" "$DB_PASSWORD" fi # Cache & session (file driver — reliable defaults) set_env "CACHE_STORE" "file" set_env "SESSION_DRIVER" "file" set_env "QUEUE_CONNECTION" "database" success ".env configured with all database and app settings" log "ENV configured: APP_URL=$APP_URL DB_CONNECTION=$laravel_db_connection DB_HOST=$DB_HOST DB_NAME=$DB_NAME" } run_database_setup() { info "Running database migrations..." cd "$PELICAN_DIR" # Run migrations php artisan migrate --seed --force >> "$LOG_FILE" 2>&1 if [[ $? -ne 0 ]]; then error "Database migration failed — check ${LOG_FILE}" error "You can retry manually: cd ${PELICAN_DIR} && php artisan migrate --seed --force" else success "Database migrations and seeders completed" fi } set_permissions() { info "Setting file permissions..." chown -R "${WEBSERVER_USER}:${WEBSERVER_USER}" "$PELICAN_DIR" chmod -R 775 "${PELICAN_DIR}/storage" "${PELICAN_DIR}/bootstrap/cache" # Ensure storage subdirectories exist mkdir -p "${PELICAN_DIR}/storage/framework/"{cache/data,sessions,views} mkdir -p "${PELICAN_DIR}/storage/logs" chown -R "${WEBSERVER_USER}:${WEBSERVER_USER}" "${PELICAN_DIR}/storage" success "Permissions set (owner: ${WEBSERVER_USER}, storage: 775)" } setup_queue_worker() { info "Creating queue worker service..." cat > /etc/systemd/system/pelican-queue.service <> "$LOG_FILE" 2>&1 success "Queue worker service created and running" } setup_cron_job() { info "Setting up scheduled task cron..." local cron_line="* * * * * php ${PELICAN_DIR}/artisan schedule:run >> /dev/null 2>&1" (crontab -u "${WEBSERVER_USER}" -l 2>/dev/null | grep -v "schedule:run"; echo "$cron_line") | crontab -u "${WEBSERVER_USER}" - success "Cron job configured (runs every minute)" } # ═══════════════════════════════════════════════════════════════════════════════ # Wings Installation # ═══════════════════════════════════════════════════════════════════════════════ install_wings() { step "Installing Docker" if command -v docker &>/dev/null; then success "Docker already installed" else run_cmd "Installing Docker CE (this may take a minute)" bash -c "curl -sSL https://get.docker.com/ | CHANNEL=stable sh >> '$LOG_FILE' 2>&1" systemctl enable --now docker >> "$LOG_FILE" 2>&1 success "Docker CE installed and running" fi step "Installing Wings daemon" mkdir -p "$WINGS_DIR" /var/run/wings local wings_url="${PELICAN_WINGS_BASE}/wings_linux_${WINGS_ARCH}" run_cmd "Downloading Wings binary (${WINGS_ARCH})" curl -sSL -o "$WINGS_BIN" "$wings_url" chmod u+x "$WINGS_BIN" success "Wings binary installed to ${WINGS_BIN}" step "Setting up Wings systemd service" cat > /etc/systemd/system/wings.service <> "$LOG_FILE" 2>&1 success "Wings systemd service created and enabled" echo "" echo -e " ${YELLOW}${BOLD}Wings requires manual configuration:${RESET}" echo -e " ${DIM}1. Go to Panel admin → Nodes → Create New${RESET}" echo -e " ${DIM}2. Copy the configuration YAML from the Configuration tab${RESET}" echo -e " ${DIM}3. Paste it into: ${WHITE}${WINGS_DIR}/config.yml${RESET}" echo -e " ${DIM}4. Then start Wings: ${WHITE}sudo systemctl start wings${RESET}" echo "" } # ═══════════════════════════════════════════════════════════════════════════════ # Uninstall # ═══════════════════════════════════════════════════════════════════════════════ uninstall() { echo "" echo -e " ${RED}${BOLD}⚠ Uninstall Pelican${RESET}" echo "" echo -e " ${CYAN}[1]${RESET} Uninstall Panel" echo -e " ${CYAN}[2]${RESET} Uninstall Wings" echo -e " ${CYAN}[3]${RESET} Uninstall Both" echo -e " ${CYAN}[4]${RESET} Cancel" echo "" ask "Select option [1-4]:" read -r choice case "$choice" in 1) uninstall_panel ;; 2) uninstall_wings ;; 3) uninstall_panel; uninstall_wings ;; 4|*) info "Cancelled."; exit 0 ;; esac } uninstall_panel() { if ! confirm "Remove all panel files in ${PELICAN_DIR}? (y/N):" "N"; then info "Aborted."; return fi step "Uninstalling Pelican Panel" systemctl stop pelican-queue 2>/dev/null || true systemctl disable pelican-queue 2>/dev/null || true rm -f /etc/systemd/system/pelican-queue.service rm -f /etc/nginx/sites-enabled/pelican.conf /etc/nginx/sites-available/pelican.conf 2>/dev/null || true rm -f /etc/apache2/sites-enabled/pelican.conf /etc/apache2/sites-available/pelican.conf 2>/dev/null || true rm -f /etc/httpd/conf.d/pelican.conf 2>/dev/null || true rm -rf "$PELICAN_DIR" systemctl daemon-reload systemctl restart nginx 2>/dev/null || systemctl restart apache2 2>/dev/null || \ systemctl restart httpd 2>/dev/null || systemctl restart caddy 2>/dev/null || true success "Pelican Panel uninstalled" } uninstall_wings() { if ! confirm "Stop and remove Wings? (y/N):" "N"; then info "Aborted."; return fi step "Uninstalling Wings" systemctl stop wings 2>/dev/null || true systemctl disable wings 2>/dev/null || true rm -f /etc/systemd/system/wings.service rm -f "$WINGS_BIN" systemctl daemon-reload if confirm "Also remove Wings config (${WINGS_DIR})? (y/N):" "N"; then rm -rf "$WINGS_DIR" fi success "Wings uninstalled" } # ═══════════════════════════════════════════════════════════════════════════════ # Update # ═══════════════════════════════════════════════════════════════════════════════ update_panel() { step "Updating Pelican Panel" [[ ! -d "$PELICAN_DIR" ]] && fatal "Panel not found at ${PELICAN_DIR}" cd "$PELICAN_DIR" php artisan down >> "$LOG_FILE" 2>&1 || true info "Panel in maintenance mode" cp .env ".env.backup.$(date +%Y%m%d%H%M%S)" success "Backed up .env" run_cmd "Downloading latest Pelican Panel" bash -c "curl -sSL '$PELICAN_PANEL_URL' | tar -xzv >> '$LOG_FILE' 2>&1" run_cmd "Updating PHP dependencies" \ env COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader php artisan migrate --seed --force >> "$LOG_FILE" 2>&1 success "Database migrations applied" php artisan optimize:clear >> "$LOG_FILE" 2>&1 success "Caches cleared" # Re-detect webserver user if [[ "$OS_FAMILY" == "rhel" ]]; then if systemctl is-active --quiet nginx; then WEBSERVER_USER="nginx" elif systemctl is-active --quiet httpd; then WEBSERVER_USER="apache" elif systemctl is-active --quiet caddy; then WEBSERVER_USER="caddy" else WEBSERVER_USER="nginx"; fi else WEBSERVER_USER="www-data" fi chown -R "${WEBSERVER_USER}:${WEBSERVER_USER}" "$PELICAN_DIR" chmod -R 775 "${PELICAN_DIR}/storage" "${PELICAN_DIR}/bootstrap/cache" systemctl restart pelican-queue >> "$LOG_FILE" 2>&1 || true php artisan queue:restart >> "$LOG_FILE" 2>&1 || true php artisan up >> "$LOG_FILE" 2>&1 success "Panel updated and back online!" } update_wings() { step "Updating Wings" systemctl stop wings >> "$LOG_FILE" 2>&1 || true run_cmd "Downloading latest Wings binary" curl -sSL -o "$WINGS_BIN" "${PELICAN_WINGS_BASE}/wings_linux_${WINGS_ARCH}" chmod u+x "$WINGS_BIN" systemctl start wings >> "$LOG_FILE" 2>&1 success "Wings updated and restarted!" } # ═══════════════════════════════════════════════════════════════════════════════ # Health Checks # ═══════════════════════════════════════════════════════════════════════════════ health_check() { step "Running post-install health checks" local passed=0 local failed=0 check_pass() { success "$1"; passed=$((passed + 1)); } check_fail() { error "$1"; failed=$((failed + 1)); } # PHP if php -v &>/dev/null; then check_pass "PHP $(php -v | head -1 | awk '{print $2}') is working" else check_fail "PHP is not working" fi # Web server if [[ "$INSTALL_PANEL" == true ]]; then local ws_active=false for svc in nginx apache2 httpd caddy; do if systemctl is-active --quiet "$svc" 2>/dev/null; then check_pass "${svc^} is running" ws_active=true break fi done [[ "$ws_active" == false ]] && check_fail "No web server is running" # Panel files if [[ -f "${PELICAN_DIR}/artisan" ]]; then check_pass "Panel files present at ${PELICAN_DIR}" else check_fail "Panel files missing" fi # .env has APP_KEY if grep -q "^APP_KEY=base64:" "${PELICAN_DIR}/.env" 2>/dev/null; then check_pass ".env has APP_KEY" else check_fail ".env missing or no APP_KEY" fi # .env has DB_CONNECTION if grep -q "^DB_CONNECTION=" "${PELICAN_DIR}/.env" 2>/dev/null; then check_pass ".env has database configuration" else check_fail ".env missing database configuration" fi # Queue worker if systemctl is-active --quiet pelican-queue; then check_pass "Queue worker is running" else check_fail "Queue worker is not running" fi # Can artisan run without error if cd "$PELICAN_DIR" && php artisan --version >> "$LOG_FILE" 2>&1; then check_pass "Artisan CLI works" else check_fail "Artisan CLI errors (check ${LOG_FILE})" fi # HTTP check if [[ -n "$FQDN" ]]; then local proto="http" [[ "$CONFIGURE_SSL" == true || "${WEBSERVER:-}" == "caddy" ]] && proto="https" local http_code http_code=$(curl -sSo /dev/null -w '%{http_code}' --max-time 10 "${proto}://${FQDN}" 2>/dev/null || echo "000") if [[ "$http_code" =~ ^(200|302|301)$ ]]; then check_pass "Panel responds at ${proto}://${FQDN} (HTTP ${http_code})" else check_fail "Panel not responding at ${proto}://${FQDN} (HTTP ${http_code})" fi fi fi # Database if [[ "$INSTALL_PANEL" == true && "$DB_CHOICE" != "sqlite" ]]; then case "$DB_CHOICE" in mysql) systemctl is-active --quiet mysql 2>/dev/null || systemctl is-active --quiet mysqld 2>/dev/null && check_pass "MySQL is running" || check_fail "MySQL not running" ;; mariadb) systemctl is-active --quiet mariadb && check_pass "MariaDB is running" || check_fail "MariaDB not running" ;; postgres) systemctl is-active --quiet postgresql && check_pass "PostgreSQL is running" || check_fail "PostgreSQL not running" ;; esac fi # Wings if [[ "$INSTALL_WINGS_FLAG" == true ]]; then [[ -x "$WINGS_BIN" ]] && check_pass "Wings binary installed" || check_fail "Wings binary missing" systemctl is-active --quiet docker && check_pass "Docker is running" || check_fail "Docker not running" fi echo "" if [[ $failed -eq 0 ]]; then echo -e " ${GREEN}${BOLD}✓ All ${passed} checks passed!${RESET}" else echo -e " ${YELLOW}${BOLD}⚠ ${passed} passed, ${failed} failed — review issues above${RESET}" fi } # ═══════════════════════════════════════════════════════════════════════════════ # Interactive Menus # ═══════════════════════════════════════════════════════════════════════════════ select_components() { echo "" echo -e " ${BOLD}What would you like to install?${RESET}" echo "" echo -e " ${CYAN}[1]${RESET} Panel only" echo -e " ${CYAN}[2]${RESET} Wings only" echo -e " ${CYAN}[3]${RESET} Panel + Wings ${DIM}(same machine)${RESET}" echo "" ask "Select [1-3]:" read -r comp case "${comp:-1}" in 1) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;; 2) INSTALL_PANEL=false; INSTALL_WINGS_FLAG=true ;; 3) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=true ;; *) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;; esac } collect_panel_options() { echo "" echo -e " ${BOLD}${WHITE}Panel Configuration${RESET}" echo -e " ${DIM}$(printf '%.0s─' {1..40})${RESET}" # Domain if [[ -z "$FQDN" ]]; then echo "" ask "Panel domain (e.g., panel.example.com):" read -r FQDN [[ -z "$FQDN" ]] && fatal "Domain is required." fi # Validate FQDN (basic check) if [[ ! "$FQDN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]]; then warn "Domain '${FQDN}' looks unusual — double-check it's correct" fi # Web server if [[ -z "$WEBSERVER" ]]; then echo "" echo -e " ${BOLD}Web server:${RESET}" echo -e " ${CYAN}[1]${RESET} Nginx ${DIM}(recommended)${RESET}" echo -e " ${CYAN}[2]${RESET} Apache" echo -e " ${CYAN}[3]${RESET} Caddy ${DIM}(automatic SSL, easiest setup)${RESET}" echo "" ask "Select [1-3] (default: 1):" read -r ws case "${ws:-1}" in 1) WEBSERVER="nginx" ;; 2) WEBSERVER="apache" ;; 3) WEBSERVER="caddy" ;; *) WEBSERVER="nginx" ;; esac fi # Update WEBSERVER_USER based on selection if [[ "$OS_FAMILY" == "rhel" ]]; then case "$WEBSERVER" in nginx) WEBSERVER_USER="nginx" ;; apache) WEBSERVER_USER="apache" ;; caddy) WEBSERVER_USER="caddy" ;; esac fi # Database if [[ -z "$DB_CHOICE" ]]; then echo "" echo -e " ${BOLD}Database:${RESET}" echo -e " ${CYAN}[1]${RESET} SQLite ${DIM}(simplest, no server needed)${RESET}" echo -e " ${CYAN}[2]${RESET} MySQL" echo -e " ${CYAN}[3]${RESET} MariaDB" echo -e " ${CYAN}[4]${RESET} PostgreSQL ${DIM}(recommended for production)${RESET}" echo "" ask "Select [1-4] (default: 1):" read -r db case "${db:-1}" in 1) DB_CHOICE="sqlite" ;; 2) DB_CHOICE="mysql" ;; 3) DB_CHOICE="mariadb" ;; 4) DB_CHOICE="postgres" ;; *) DB_CHOICE="sqlite" ;; esac fi auto_set_db_port generate_db_password # Custom DB credentials (skip for sqlite) if [[ "$DB_CHOICE" != "sqlite" && "$ASSUME_YES" != true ]]; then echo "" ask "Database name [${DB_NAME}]:" read -r input_db_name DB_NAME="${input_db_name:-$DB_NAME}" ask "Database user [${DB_USER}]:" read -r input_db_user DB_USER="${input_db_user:-$DB_USER}" ask "Database password [auto-generated]:" read -r -s input_db_pass echo "" DB_PASSWORD="${input_db_pass:-$DB_PASSWORD}" fi # SSL if [[ -z "$CONFIGURE_SSL" ]]; then if [[ "$WEBSERVER" == "caddy" ]]; then CONFIGURE_SSL=false # Caddy handles it info "Caddy handles SSL automatically" else echo "" if confirm "Enable SSL with Let's Encrypt? (Y/n):" "Y"; then CONFIGURE_SSL=true if [[ -z "$EMAIL" ]]; then ask "Email for Let's Encrypt certificates:" read -r EMAIL [[ -z "$EMAIL" ]] && fatal "Email is required for SSL" fi else CONFIGURE_SSL=false fi fi fi # Timezone if [[ "$ASSUME_YES" != true ]]; then echo "" ask "Timezone [${TIMEZONE}]:" read -r input_tz TIMEZONE="${input_tz:-$TIMEZONE}" fi } show_summary() { echo "" echo -e " ${CYAN}${BOLD}╔═══════════════════════════════════════════╗${RESET}" echo -e " ${CYAN}${BOLD}║ Installation Summary ║${RESET}" echo -e " ${CYAN}${BOLD}╚═══════════════════════════════════════════╝${RESET}" echo "" if [[ "$INSTALL_PANEL" == true ]]; then echo -e " ${BOLD}Panel:${RESET}" echo -e " Domain: ${GREEN}${FQDN}${RESET}" echo -e " Web server: ${GREEN}${WEBSERVER}${RESET}" echo -e " Database: ${GREEN}${DB_CHOICE}${RESET}" if [[ "$DB_CHOICE" != "sqlite" ]]; then echo -e " DB name: ${GREEN}${DB_NAME}${RESET}" echo -e " DB user: ${GREEN}${DB_USER}${RESET}" fi echo -e " SSL: ${GREEN}$([[ "$CONFIGURE_SSL" == true || "$WEBSERVER" == "caddy" ]] && echo "Yes" || echo "No")${RESET}" echo -e " Timezone: ${GREEN}${TIMEZONE}${RESET}" echo -e " PHP: ${GREEN}${PHP_VERSION}${RESET}" echo "" fi if [[ "$INSTALL_WINGS_FLAG" == true ]]; then echo -e " ${BOLD}Wings:${RESET}" echo -e " Architecture: ${GREEN}${WINGS_ARCH}${RESET}" echo -e " Docker: ${GREEN}Will be installed${RESET}" echo "" fi if ! confirm "Proceed with installation? (Y/n):" "Y"; then info "Installation cancelled." exit 0 fi } # ═══════════════════════════════════════════════════════════════════════════════ # Completion Messages # ═══════════════════════════════════════════════════════════════════════════════ print_complete() { local proto="http" [[ "$CONFIGURE_SSL" == true || "$WEBSERVER" == "caddy" ]] && proto="https" echo "" echo -e " ${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════╗${RESET}" echo -e " ${GREEN}${BOLD}║ ║${RESET}" echo -e " ${GREEN}${BOLD}║ 🎉 Installation completed successfully! 🎉 ║${RESET}" echo -e " ${GREEN}${BOLD}║ ║${RESET}" echo -e " ${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════╝${RESET}" echo "" if [[ "$INSTALL_PANEL" == true ]]; then echo -e " ${BOLD}Panel — next steps:${RESET}" echo -e " ${WHITE}1.${RESET} Open ${CYAN}${proto}://${FQDN}/installer${RESET} in your browser" echo -e " ${WHITE}2.${RESET} Complete the web-based setup wizard" echo -e " ${WHITE}3.${RESET} Create your admin account" echo "" fi if [[ "$INSTALL_WINGS_FLAG" == true ]]; then echo -e " ${BOLD}Wings — next steps:${RESET}" echo -e " ${WHITE}1.${RESET} Create a node in Panel admin → Nodes → Create New" echo -e " ${WHITE}2.${RESET} Copy the config YAML into ${CYAN}${WINGS_DIR}/config.yml${RESET}" echo -e " ${WHITE}3.${RESET} Test: ${CYAN}sudo wings --debug${RESET}" echo -e " ${WHITE}4.${RESET} Start: ${CYAN}sudo systemctl start wings${RESET}" echo "" fi echo -e " ${BOLD}Useful commands:${RESET}" echo -e " Update: ${CYAN}sudo bash install.sh update --both${RESET}" echo -e " Uninstall: ${CYAN}sudo bash install.sh uninstall${RESET}" echo -e " Logs: ${CYAN}tail -f ${LOG_FILE}${RESET}" echo "" } # ═══════════════════════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════════════════════ main() { parse_args "$@" # Initialize log mkdir -p "$(dirname "$LOG_FILE")" echo "=== Pelican Installer v${INSTALLER_VERSION} — $(date) ===" >> "$LOG_FILE" print_header detect_os preflight_checks # Determine action if [[ -z "$ACTION" ]]; then echo "" echo -e " ${BOLD}What would you like to do?${RESET}" echo "" echo -e " ${CYAN}[1]${RESET} Install Panel / Wings" echo -e " ${CYAN}[2]${RESET} Update Panel / Wings" echo -e " ${CYAN}[3]${RESET} Uninstall" echo "" ask "Select [1-3] (default: 1):" read -r action_choice case "${action_choice:-1}" in 1) ACTION="install" ;; 2) ACTION="update" ;; 3) ACTION="uninstall" ;; *) ACTION="install" ;; esac fi case "$ACTION" in install) # If components not set via flags, ask if [[ -z "$INSTALL_PANEL" && -z "$INSTALL_WINGS_FLAG" ]]; then select_components fi INSTALL_PANEL="${INSTALL_PANEL:-false}" INSTALL_WINGS_FLAG="${INSTALL_WINGS_FLAG:-false}" # Calculate total steps for progress indicator TOTAL_STEPS=2 # detect + preflight (already done but we counted them) CURRENT_STEP=2 [[ "$INSTALL_PANEL" == true ]] && TOTAL_STEPS=$((TOTAL_STEPS + 6)) # php, db, webserver, ssl, panel-download, panel-config [[ "$INSTALL_WINGS_FLAG" == true ]] && TOTAL_STEPS=$((TOTAL_STEPS + 3)) # docker, wings-download, wings-service TOTAL_STEPS=$((TOTAL_STEPS + 1)) # health check if [[ "$INSTALL_PANEL" == true ]]; then detect_php_version collect_panel_options show_summary install_php install_database install_webserver install_ssl install_panel fi if [[ "$INSTALL_WINGS_FLAG" == true ]]; then install_wings fi health_check print_complete ;; update) if [[ -z "$INSTALL_PANEL" && -z "$INSTALL_WINGS_FLAG" ]]; then echo "" echo -e " ${BOLD}What would you like to update?${RESET}" echo "" echo -e " ${CYAN}[1]${RESET} Panel" echo -e " ${CYAN}[2]${RESET} Wings" echo -e " ${CYAN}[3]${RESET} Both" echo "" ask "Select [1-3]:" read -r upd case "${upd:-1}" in 1) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;; 2) INSTALL_PANEL=false; INSTALL_WINGS_FLAG=true ;; 3) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=true ;; *) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;; esac fi detect_php_version [[ "$INSTALL_PANEL" == true ]] && update_panel [[ "$INSTALL_WINGS_FLAG" == true ]] && update_wings echo "" echo -e " ${GREEN}${BOLD}✓ Update complete!${RESET}" echo "" ;; uninstall) uninstall ;; *) fatal "Unknown action: ${ACTION}" ;; esac log "Installer finished at $(date)" } # ─── Run ─────────────────────────────────────────────────────────────────────── main "$@"