#!/bin/sh
FERRON_INSTALLER_EXTRACT_DIR=$(mktemp -d)
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/assets"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner.txt" << 'EOF'
X [38:5:208m ▟██████▙[m
X[38:5:208m ▟█▘ ▟  ▝█▙  [m  ▟▀
X[38:5:208m▐█▌ ▟██▛ ▐█▌ [m ▀█▀▗▛▜▖▜▞▘▜▞▘▟▀▙▝▙▀▙
X[38:5:208m ▜█▖  ▛ ▗█▛  [m  █ ▝█▙▖▐▌ ▐▌ ▜▄▛ █ █
X [38:5:208m ▜██████▛[m
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/assets"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner-mono.txt" << 'EOF'
X  ▟██████▙
X ▟█▘ ▟  ▝█▙    ▟▀
X▐█▌ ▟██▛ ▐█▌  ▀█▀▗▛▜▖▜▞▘▜▞▘▟▀▙▝▙▀▙
X ▜█▖  ▛ ▗█▛    █ ▝█▙▖▐▌ ▐▌ ▜▄▛ █ █
X  ▜██████▛
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/assets"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner-ascii.txt" << 'EOF'
X   _____
X  |  ___|__ _ __ _ __ ___  _ __
X  | |_ / _ \ '__| '__/ _ \| '_ \
X  |  _|  __/ |  | | | (_) | | | |
X  |_|  \___|_|  |_|  \___/|_| |_|
EOF
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/main.sh" << 'EOF'
X#!/bin/sh
X#
X# main.sh — entry point for the self-extracting Ferron installer.
X#
X# install.sh (produced by the Makefile) extracts every staging file into
X# $FERRON_INSTALLER_EXTRACT_DIR and then sources this script. Our job is to:
X#
X#   1. Load the UI libraries.
X#   2. Render the welcome banner.
X#   3. Walk the numbered steps/ directory in order, letting run_step manage
X#      the spinner / OK / FAIL transitions for each one.
X#   4. Render the success screen (or let ui_failure in step.sh handle the
X#      unhappy path).
X#
X# The extraction directory is cleaned up on successful exit only; a failed
X# install leaves the log and the extracted scripts behind for inspection.
X
Xset -eu
X
X: "${FERRON_INSTALLER_EXTRACT_DIR:?main.sh: FERRON_INSTALLER_EXTRACT_DIR is unset}"
X
XD=$FERRON_INSTALLER_EXTRACT_DIR
X
X. "$D/lib/tty.sh"
X. "$D/lib/log.sh"
X. "$D/lib/ui.sh"
X. "$D/lib/prompt.sh"
X. "$D/lib/step.sh"
X
Xlog_init
Xui_init
X
X# Clean up the extraction directory only on clean exit. Failed installs fall
X# through ui_failure → exit in run_step, which skips this trap's success
X# path; we preserve the log and the extracted scripts in that case.
X_cleanup_success() {
X    rm -rf "$FERRON_INSTALLER_EXTRACT_DIR"
X}
X
X# If anything in main.sh itself (outside of run_step) kills the shell with a
X# signal, make sure we restore the cursor that ui_step_begin hid.
X_cleanup_signal() {
X    ui_spinner_pause || true
X    printf '\033[?25h' >/dev/tty 2>/dev/null || true
X    exit 130
X}
Xtrap _cleanup_signal INT TERM HUP
X
Xui_banner
X
X# Walk the numbered step files in lexical order. Each one calls run_step
X# internally, so the loop body just sources them.
Xfor _step in "$D"/steps/[0-9]*.sh; do
X    [ -r "$_step" ] || continue
X    # shellcheck disable=SC1090  # dynamic sourcing by design.
X    . "$_step"
Xdone
X
Xui_success
X_cleanup_success
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/50_config.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 50_config.sh — install ferron.conf and the default wwwroot.
X#
X# For archive installs this step installs the configuration file and the
X# default web root from the extracted archive. It preserves existing files
X# to match the `%config(noreplace)` semantics of RPM and the `conffiles`
X# mechanism of Debian packages.
X#
X# For package installs the step skips itself because the package manager
X# handles configuration file placement.
X#
X# Configuration file:
X#   /etc/ferron/ferron.conf — only installed if it does not already exist
X#
X# Web root:
X#   /var/www/ferron — only populated if the directory is empty or missing
X#
X# Ownership:
X#   /etc/ferron/ferron.conf — root:root 0644
X#   /var/www/ferron/*       — ferron:ferron 0644 (files), 0755 (dirs)
X
Xstep_install_config() {
X    # Package managers handle configuration.
X    if [ "$FERRON_INSTALL_METHOD" != "archive" ]; then
X        step_skip "package manager handles configuration"
X        return 0
X    fi
X
X    # ------------------------------------------------------------------
X    # Configuration file
X    # ------------------------------------------------------------------
X    # Only install the configuration file if it does not already exist.
X    # This preserves the user's existing configuration across updates,
X    # matching the behavior of package managers.
X    if [ -f /etc/ferron/ferron.conf ]; then
X        log_write "skipping config install: /etc/ferron/ferron.conf already exists"
X    else
X        # The bundled config comes from the installer bundle (placed by the
X        # Makefile's `prepare` step into $FERRON_INSTALLER_EXTRACT_DIR/ferron.conf),
X        # not from the downloaded release archive.
X        _conf_src="$FERRON_INSTALLER_EXTRACT_DIR/ferron.conf"
X        if [ -f "$_conf_src" ]; then
X            log_write "installing /etc/ferron/ferron.conf from installer bundle"
X            cp "$_conf_src" /etc/ferron/ferron.conf
X            chown root:root /etc/ferron/ferron.conf
X            chmod 0644 /etc/ferron/ferron.conf
X            log_write "installed /etc/ferron/ferron.conf (mode 0644)"
X        else
X            log_write "warning: bundled ferron.conf not found in installer bundle at $_conf_src"
X        fi
X    fi
X
X    # ------------------------------------------------------------------
X    # Web root
X    # ------------------------------------------------------------------
X    # Only populate /var/www/ferron if it does not exist or is empty.
X    # This preserves the user's existing website files across updates.
X    if [ ! -d /var/www/ferron ]; then
X        log_write "creating /var/www/ferron"
X        mkdir -p /var/www/ferron
X    fi
X
X    _wwwroot_src="$FERRON_EXTRACT_DIR/wwwroot"
X    if [ -d "$_wwwroot_src" ]; then
X        # Check if the directory already has content.
X        if [ -z "$(ls -A /var/www/ferron 2>/dev/null)" ]; then
X            log_write "populating /var/www/ferron with default files"
X            cp -r "$_wwwroot_src"/* /var/www/ferron/
X            chown -R ferron:ferron /var/www/ferron
X            chmod -R 0755 /var/www/ferron
X            log_write "populated /var/www/ferron"
X        else
X            log_write "skipping wwwroot install: /var/www/ferron already has content"
X        fi
X    else
X        log_write "warning: wwwroot directory not found in archive"
X    fi
X
X    # Clean up extraction directory.
X    rm -rf "$FERRON_EXTRACT_DIR"
X    log_write "cleaned up extraction directory"
X
X    # Clean up backup directory if update succeeded.
X    if [ "$FERRON_INSTALL_MODE" = "update" ]; then
X        rm -rf "$FERRON_BACKUP_DIR"
X        log_write "cleaned up backup directory"
X    fi
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Installing configuration" step_install_config
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/00_preflight.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 00_preflight.sh — host detection, install method selection, and optional
X# package-manager installation.
X#
X# This is the most complex step because it sets up the environment for every
X# subsequent step. It runs first and exports variables that control the rest
X# of the installer.
X#
X# Detection performed:
X#   - Root / elevated privileges
X#   - Distro name and version (from /etc/os-release, /etc/redhat-release, etc.)
X#   - CPU architecture normalized to a target-triple component
X#   - C library (glibc vs musl) for Linux
X#   - Active init system (systemd vs SysV)
X#   - Existing Ferron installation (via installer or package manager)
X#
X# User interaction:
X#   - Asks for install method: archive, Debian/Ubuntu APT, RHEL/Fedora DNF, etc.
X#   - If an existing installer-managed install is found, offers update / uninstall
X#   - For package methods on fresh installs, sets up the repository
X#
X# Key variables exported:
X#   FERRON_DISTRO          — distro name (debian, rhel, alpine, freebsd, unknown)
X#   FERRON_DISTRO_VERSION  — distro version or codename
X#   FERRON_ARCH            — normalized architecture (x86_64, aarch64, armv7, …)
X#   FERRON_LIBC            — libc variant for Linux (gnu, musl, or empty)
X#   FERRON_TARGET_TRIPLE   — full triple used in download URLs
X#   FERRON_HAS_SYSTEMD     — 1 if systemd is active, 0 otherwise
X#   FERRON_HAS_OPENRC      — 1 if OpenRC is active, 0 otherwise
X#   FERRON_INSTALL_METHOD  — archive, debian, rhel, alpine, freebsd
X#   FERRON_INSTALL_MODE    — install, update, uninstall
X#   FERRON_INSTALL_LTS     — 1 if LTS channel is requested
X#   FERRON_ARCHIVE_PATH    — path to downloaded or local archive (archive mode)
X
Xstep_preflight() {
X    # ------------------------------------------------------------------
X    # 1. Root check
X    # ------------------------------------------------------------------
X    if [ "$(id -u)" != "0" ]; then
X        log_write "error: this installer must be run as root"
X        printf '%sError: the installer must be run as root (or with sudo).%s\n' \
X            "$(printf '\033[1m')" "$(printf '\033[0m')" >&2
X        return 1
X    fi
X    log_write "root check passed"
X
X    # ------------------------------------------------------------------
X    # 2. Distro detection
X    # ------------------------------------------------------------------
X    FERRON_DISTRO="unknown"
X    FERRON_DISTRO_VERSION=""
X
X    # Try /etc/os-release first (systemd-based distros, modern distros).
X    if [ -r /etc/os-release ]; then
X        # shellcheck disable=SC1090
X        . /etc/os-release
X        case "${ID:-}" in
X            debian|ubuntu|devuan|mx|pop|elementary|linuxmint)
X                FERRON_DISTRO="debian" ;;
X            rhel|fedora|centos|rocky|almalinux|amzn|oracle|scientific)
X                FERRON_DISTRO="rhel" ;;
X            alpine)
X                FERRON_DISTRO="alpine" ;;
X            arch)
X                FERRON_DISTRO="arch" ;;
X            freebsd)
X                FERRON_DISTRO="freebsd" ;;
X        esac
X        FERRON_DISTRO_VERSION="${VERSION_ID:-${VERSION_CODENAME:-}}"
X    fi
X
X    # Fallback: check /etc/redhat-release.
X    if [ "$FERRON_DISTRO" = "unknown" ] && [ -r /etc/redhat-release ]; then
X        if grep -qi 'red hat\|rhel\|enterprise' /etc/redhat-release 2>/dev/null; then
X            FERRON_DISTRO="rhel"
X        elif grep -qi 'fedora' /etc/redhat-release 2>/dev/null; then
X            FERRON_DISTRO="fedora"
X        elif grep -qi 'centos' /etc/redhat-release 2>/dev/null; then
X            FERRON_DISTRO="centos"
X        fi
X        FERRON_DISTRO_VERSION=$(grep -oE '[0-9]+(\.[0-9]+)*' /etc/redhat-release 2>/dev/null | head -1)
X    fi
X
X    # Fallback: check lsb_release.
X    if [ "$FERRON_DISTRO" = "unknown" ] && command -v lsb_release >/dev/null 2>&1; then
X        _lsb_id=$(lsb_release -si 2>/dev/null | tr '[:upper:]' '[:lower:]')
X        case "$_lsb_id" in
X            debian|ubuntu|devuan) FERRON_DISTRO="debian" ;;
X            redhat|fedora|centos) FERRON_DISTRO="rhel" ;;
X            alpine)               FERRON_DISTRO="alpine" ;;
X            arch)                 FERRON_DISTRO="arch" ;;
X            freebsd)              FERRON_DISTRO="freebsd" ;;
X        esac
X        _lsb_rel=$(lsb_release -sr 2>/dev/null)
X        [ -n "$_lsb_rel" ] && FERRON_DISTRO_VERSION="$_lsb_rel"
X    fi
X
X    # Fallback check if arch_release exists.
X    if [ "$FERRON_DISTRO" = "unknown" ] && [ -f /etc/arch-release ]; then
X        FERRON_DISTRO="arch"
X    fi
X
X    # Final fallback: just use the hostname's first word.
X    if [ "$FERRON_DISTRO" = "unknown" ]; then
X        log_write "warning: could not detect distro, defaulting to unknown"
X    else
X        log_write "detected distro: $FERRON_DISTRO $FERRON_DISTRO_VERSION"
X    fi
X
X    # ------------------------------------------------------------------
X    # 3. Architecture detection
X    # ------------------------------------------------------------------
X    _uname_arch=$(uname -m 2>/dev/null || echo "unknown")
X    case "$_uname_arch" in
X        x86_64|amd64)    FERRON_ARCH="x86_64" ;;
X        aarch64|arm64)    FERRON_ARCH="aarch64" ;;
X        armv7l|armv7)     FERRON_ARCH="armv7" ;;
X        armv6l)           FERRON_ARCH="armv6" ;;
X        riscv64)          FERRON_ARCH="riscv64" ;;
X        s390x)            FERRON_ARCH="s390x" ;;
X        ppc64le)          FERRON_ARCH="powerpc64le" ;;
X        ppc64)            FERRON_ARCH="powerpc64" ;;
X        i686|i586|i486|i386) FERRON_ARCH="i686" ;;
X        *)                FERRON_ARCH="$_uname_arch" ;;
X    esac
X    log_write "detected architecture: $FERRON_ARCH (uname: $_uname_arch)"
X
X    # ------------------------------------------------------------------
X    # 4. C library detection (Linux only)
X    # ------------------------------------------------------------------
X    FERRON_LIBC=""
X    FERRON_OS=$(uname -s 2>/dev/null || echo "unknown")
X    case "$FERRON_OS" in
X        Linux)
X            # Detect glibc version via ldd.
X            _glibc_version=""
X            if command -v ldd >/dev/null 2>&1; then
X                _glibc_version=$(ldd --version 2>&1 | awk '/ldd/{print $NF}' | head -1)
X            fi
X
X            # If glibc >= 2.31, use "gnu"; otherwise musl.
X            if [ -n "$_glibc_version" ]; then
X                if printf '%s\n' "2.31" "$_glibc_version" | sort -V | head -1 | grep -q '^2\.31$'; then
X                    FERRON_LIBC="gnu"
X                else
X                    FERRON_LIBC="musl"
X                fi
X            else
X                # Try to detect musl explicitly.
X                if command -v musl-gcc >/dev/null 2>&1 || \
X                   ldd --version 2>&1 | grep -qi musl; then
X                    FERRON_LIBC="musl"
X                else
X                    # Default to glibc if we can't determine.
X                    FERRON_LIBC="gnu"
X                fi
X            fi
X            log_write "detected libc: $FERRON_LIBC (ldd version: $_glibc_version)"
X            ;;
X        FreeBSD)
X            # FreeBSD binaries are built without a libc suffix in the triple.
X            FERRON_LIBC=""
X            ;;
X        Darwin)
X            # macOS binaries are built without a libc suffix.
X            FERRON_LIBC=""
X            ;;
X    esac
X
X    # ------------------------------------------------------------------
X    # 5. Target triple construction
X    # ------------------------------------------------------------------
X    # The triple format is: ARCH-unknown-OS-LIBC (libc omitted for non-Linux).
X    # Examples:
X    #   x86_64-unknown-linux-gnu
X    #   x86_64-unknown-linux-musl
X    #   aarch64-unknown-freebsd
X    #   aarch64-apple-darwin
X    if [ -n "$FERRON_LIBC" ]; then
X        FERRON_TARGET_TRIPLE="${FERRON_ARCH}-unknown-$(echo $FERRON_OS | tr '[:upper:]' '[:lower:]')-${FERRON_LIBC}"
X    else
X        FERRON_TARGET_TRIPLE="${FERRON_ARCH}-unknown-$(echo $FERRON_OS | tr '[:upper:]' '[:lower:]')"
X    fi
X
X    # For FreeBSD, normalize the OS name in the triple.
X    case "$FERRON_OS" in
X        FreeBSD) FERRON_TARGET_TRIPLE="${FERRON_ARCH}-unknown-freebsd" ;;
X        Darwin)  FERRON_TARGET_TRIPLE="${FERRON_ARCH}-apple-darwin" ;;
X    esac
X
X    log_write "target triple: $FERRON_TARGET_TRIPLE"
X
X    # ------------------------------------------------------------------
X    # 6. Init system detection
X    # ------------------------------------------------------------------
X    # systemd is "active" if /run/systemd/system exists. This is the same
X    # check used by systemd itself to determine whether it is PID 1.
X    if [ -d /run/systemd/system ] 2>/dev/null; then
X        FERRON_HAS_SYSTEMD=1
X        FERRON_HAS_OPENRC=0
X        log_write "active init system: systemd"
X    elif [ -d /run/openrc ] 2>/dev/null; then
X        FERRON_HAS_SYSTEMD=0
X        FERRON_HAS_OPENRC=1
X        log_write "active init system: OpenRC"
X    else
X        FERRON_HAS_SYSTEMD=0
X        FERRON_HAS_OPENRC=0
X        log_write "active init system: SysV (systemd and OpenRC not active)"
X    fi
X
X    # ------------------------------------------------------------------
X    # 7. Existing Ferron installation detection
X    # ------------------------------------------------------------------
X    FERRON_EXISTING_INSTALL=0
X    FERRON_EXISTING_METHOD=""
X
X    # Check if installed via this installer.
X    if [ -f /etc/.ferron-installer.version ]; then
X        FERRON_EXISTING_INSTALL=1
X        FERRON_EXISTING_METHOD="installer"
X        FERRON_PREVIOUS_VERSION=$(cat /etc/.ferron-installer.version 2>/dev/null || echo "unknown")
X        log_write "existing install detected: via installer (version $FERRON_PREVIOUS_VERSION)"
X    fi
X
X    # Check if installed via package manager.
X    if [ "$FERRON_EXISTING_INSTALL" = 0 ]; then
X        if command -v dpkg >/dev/null 2>&1 && dpkg -l ferron3 >/dev/null 2>&1; then
X            FERRON_EXISTING_INSTALL=1
X            FERRON_EXISTING_METHOD="debian"
X            FERRON_PREVIOUS_VERSION=$(dpkg -s ferron3 2>/dev/null | awk '/^Version:/{print $2}')
X            log_write "existing install detected: via Debian package (version $FERRON_PREVIOUS_VERSION)"
X        elif command -v rpm >/dev/null 2>&1 && rpm -q ferron3 >/dev/null 2>&1; then
X            FERRON_EXISTING_INSTALL=1
X            FERRON_EXISTING_METHOD="rhel"
X            FERRON_PREVIOUS_VERSION=$(rpm -q --queryformat '%{VERSION}' ferron3 2>/dev/null)
X            log_write "existing install detected: via RPM package (version $FERRON_PREVIOUS_VERSION)"
X        fi
X    fi
X
X    # Check if the binary exists but wasn't detected via package manager.
X    if [ "$FERRON_EXISTING_INSTALL" = 0 ] && [ -x /usr/sbin/ferron ]; then
X        FERRON_EXISTING_INSTALL=1
X        FERRON_EXISTING_METHOD="binary"
X        FERRON_PREVIOUS_VERSION=$(/usr/sbin/ferron --version 2>/dev/null | head -1 || echo "unknown")
X        log_write "existing binary detected: /usr/sbin/ferron"
X    fi
X
X    # ------------------------------------------------------------------
X    # 8. Install method selection
X    # ------------------------------------------------------------------
X    if [ "$FERRON_EXISTING_INSTALL" = 1 ]; then
X        # Existing install: offer update or uninstall.
X        ui_spinner_pause
X        if [ "$FERRON_EXISTING_METHOD" = "installer" ]; then
X            if ask_choice FERRON_INSTALL_MODE \
X                "Ferron is already installed via the installer. What would you like to do?" \
X                "update" "uninstall"; then
X                log_write "user chose: $FERRON_INSTALL_MODE"
X            fi
X        elif [ "$FERRON_EXISTING_METHOD" = "debian" ] || [ "$FERRON_EXISTING_METHOD" = "rhel" ]; then
X            if ask_choice FERRON_INSTALL_MODE \
X                "Ferron is already installed via the package manager. Manage via package manager?" \
X                "update" "uninstall" "skip"; then
X                log_write "user chose: $FERRON_INSTALL_MODE"
X                # If they chose to manage via package manager, set the method.
X                if [ "${FERRON_INSTALL_MODE:-}" = "update" ] || \
X                   [ "${FERRON_INSTALL_MODE:-}" = "uninstall" ]; then
X                    FERRON_INSTALL_METHOD="$FERRON_EXISTING_METHOD"
X                    log_write "using existing package method: $FERRON_INSTALL_METHOD"
X                fi
X            fi
X        else
X            # Binary install — offer to replace or skip.
X            if ask_choice FERRON_INSTALL_MODE \
X                "A Ferron binary was detected. What would you like to do?" \
X                "update" "skip"; then
X                log_write "user chose: $FERRON_INSTALL_MODE"
X            fi
X        fi
X        ui_spinner_resume
X
X        # If the user chose to skip, we still need a method for the remaining
X        # steps (e.g., service management). Default to the existing method.
X        if [ "${FERRON_INSTALL_MODE:-skip}" = "skip" ]; then
X            FERRON_INSTALL_METHOD="skip"
X            log_write "user chose to skip; FERRON_INSTALL_METHOD=skip"
X            return 0
X        fi
X
X        # If they chose update/uninstall for a package-based install, we can
X        # short-circuit: set the method and skip the rest of preflight.
X        if [ "$FERRON_EXISTING_METHOD" = "debian" ] || \
X           [ "$FERRON_EXISTING_METHOD" = "rhel" ]; then
X            FERRON_INSTALL_METHOD="$FERRON_EXISTING_METHOD"
X            log_write "using existing package method: $FERRON_INSTALL_METHOD"
X            return 0
X        fi
X    fi
X
X    # Fresh install: only ask for method if we haven't already set it.
X    if [ -z "${FERRON_INSTALL_METHOD:-}" ]; then
X        AVAILABLE_METHODS="archive"
X        if [ "$FERRON_DISTRO" = "debian" ] || \
X           [ "$FERRON_DISTRO" = "rhel" ]; then
X            AVAILABLE_METHODS="$AVAILABLE_METHODS $FERRON_DISTRO"
X        fi
X        ui_spinner_pause
X        if ask_choice FERRON_INSTALL_METHOD \
X            "Choose your install method" \
X            $AVAILABLE_METHODS; then
X            log_write "user chose install method: $FERRON_INSTALL_METHOD"
X        fi
X        ui_spinner_resume
X
X        FERRON_INSTALL_MODE="install"
X    else
X        # Existing installer-managed install (update/uninstall chosen).
X        if [ "${FERRON_INSTALL_MODE:-}" = "uninstall" ]; then
X            # For uninstall, set method so archive-specific steps skip.
X            FERRON_INSTALL_METHOD="uninstall"
X            log_write "uninstall mode: skipping rest of preflight"
X            return 0
X        else
X            # Update: default to archive method so the archive-specific steps run.
X            FERRON_INSTALL_METHOD="archive"
X            log_write "update mode: defaulting to archive method for existing $FERRON_EXISTING_METHOD install"
X        fi
X    fi
X
X    # ------------------------------------------------------------------
X    # 9. Channel selection (for archive downloads)
X    # ------------------------------------------------------------------
X    if [ "$FERRON_INSTALL_METHOD" = "archive" ]; then
X        if [ -z "${FERRON_VERSION:-}" ]; then
X            # These lines would be uncommented, once the LTS channel is available.
X
X            #ui_spinner_pause
X            #if ask_choice FERRON_INSTALL_CHANNEL \
X            #    "Which release channel?" \
X            #    "stable" "lts"; then
X            #    log_write "user chose channel: $FERRON_INSTALL_CHANNEL"
X            #fi
X            #ui_spinner_resume
X
X            #case "${FERRON_INSTALL_CHANNEL:-stable}" in
X            #    lts) FERRON_INSTALL_LTS=1 ;;
X            #    *)   FERRON_INSTALL_LTS=0 ;;
X            #esac
X
X            FERRON_INSTALL_LTS=0
X        else
X            log_write "using user-specified version: $FERRON_VERSION"
X            FERRON_INSTALL_LTS=0
X        fi
X    fi
X
X    # ------------------------------------------------------------------
X    # 10. Package manager setup (fresh install only)
X    # ------------------------------------------------------------------
X    if [ "$FERRON_INSTALL_METHOD" = "debian" ]; then
X        log_write "setting up Debian/Ubuntu repository"
X
X        # Install prerequisites if needed.
X        _need_prereqs=0
X        for _cmd in curl gnupg2 ca-certificates lsb-release; do
X            if ! command -v "$_cmd" >/dev/null 2>&1; then
X                _need_prereqs=1
X                break
X            fi
X        done
X        # debian-archive-keyring might already be present.
X        if [ -z "$(dpkg -l debian-archive-keyring 2>/dev/null | grep '^ii')" ]; then
X            _need_prereqs=1
X        fi
X
X        if [ "$_need_prereqs" = 1 ]; then
X            log_write "installing prerequisites: curl gnupg2 ca-certificates lsb-release debian-archive-keyring"
X            if ! DEBIAN_FRONTEND=noninteractive apt install -y \
X                    curl gnupg2 ca-certificates lsb-release debian-archive-keyring; then
X                log_write "warning: failed to install prerequisites"
X            fi
X        fi
X
X        # Install the signing key.
X        _keyring="/usr/share/keyrings/ferron-keyring.gpg"
X        if [ ! -f "$_keyring" ]; then
X            log_write "installing Ferron GPG key"
X            if curl -fsSL https://deb.ferron.sh/signing.pgp | \
X                   gpg --dearmor -o "$_keyring" 2>/dev/null; then
X                chmod 0644 "$_keyring"
X                log_write "installed GPG key to $_keyring"
X            else
X                log_write "warning: failed to install GPG key"
X            fi
X        fi
X
X        # Add the repository if not already present.
X        _sources_list="/etc/apt/sources.list.d/ferron.list"
X        _codename="${FERRON_DISTRO_VERSION:-}"
X        if [ -z "$_codename" ] && command -v lsb_release >/dev/null 2>&1; then
X            _codename=$(lsb_release -cs 2>/dev/null || echo "")
X        fi
X        if [ -z "$_codename" ]; then
X            _codename="unknown"
X            log_write "warning: could not detect distro codename, using 'unknown'"
X        fi
X
X        if [ ! -f "$_sources_list" ] || ! grep -q "deb.ferron.sh" "$_sources_list" 2>/dev/null; then
X            log_write "adding repository for codename $_codename"
X            printf 'deb [signed-by=%s] https://deb.ferron.sh %s main\n' \
X                "$_keyring" "$_codename" > "$_sources_list"
X            log_write "added repository to $_sources_list"
X        else
X            log_write "repository already configured"
X        fi
X
X        # Update package lists.
X        log_write "running apt update"
X        if ! DEBIAN_FRONTEND=noninteractive apt update; then
X            log_write "warning: apt update failed"
X        fi
X
X        # Install Ferron.
X        log_write "installing ferron3 package"
X        if ! DEBIAN_FRONTEND=noninteractive apt install -y ferron3; then
X            log_write "error: failed to install ferron3 package"
X            return 1
X        fi
X        log_write "installed ferron3 via APT"
X
X        # After a package install, we're done with preflight.
X        return 0
X
X    elif [ "$FERRON_INSTALL_METHOD" = "rhel" ]; then
X        log_write "setting up RHEL/Fedora repository"
X
X        # Install yum-utils if needed.
X        if ! command -v yum-config-manager >/dev/null 2>&1 && \
X           ! command -v dnf-config-manager >/dev/null 2>&1; then
X            log_write "installing yum-utils"
X            if command -v dnf >/dev/null 2>&1; then
X                dnf install -y yum-utils 2>/dev/null || true
X            else
X                yum install -y yum-utils 2>/dev/null || true
X            fi
X        fi
X
X        # Add the repository.
X        _repo_file="/etc/yum.repos.d/ferron.repo"
X        if [ ! -f "$_repo_file" ]; then
X            log_write "adding repository from https://rpm.ferron.sh/ferron.repo"
X            if command -v yum-config-manager >/dev/null 2>&1; then
X                yum-config-manager --add-repo https://rpm.ferron.sh/ferron.repo 2>/dev/null || true
X            elif command -v dnf-config-manager >/dev/null 2>&1; then
X                dnf-config-manager --add-repo https://rpm.ferron.sh/ferron.repo 2>/dev/null || true
X            else
X                # Fallback: create the repo file manually.
X                cat > "$_repo_file" <<'REPOEOF'
X[ferron]
Xname=Ferron Repository
Xbaseurl=https://rpm.ferron.sh/ferron.repo
Xenabled=1
Xgpgcheck=0
XREPOEOF
X                log_write "created repo file $_repo_file (manual)"
X            fi
X            log_write "added repository to $_repo_file"
X        else
X            log_write "repository already configured"
X        fi
X
X        # Install Ferron.
X        log_write "installing ferron3 package"
X        if command -v dnf >/dev/null 2>&1; then
X            dnf install -y ferron3 2>/dev/null || yum install -y ferron3 2>/dev/null || (log_write "error: failed to install ferron3 via YUM/DNF" && return 1)
X        else
X            yum install -y ferron3 2>/dev/null || (log_write "error: failed to install ferron3 via YUM/DNF" && return 1)
X        fi
X        log_write "installed ferron3 via YUM/DNF"
X
X        # After a package install, we're done with preflight.
X        return 0
X    fi
X
X    # For archive installs, the download step will handle fetching the archive.
X    log_write "preflight complete: method=$FERRON_INSTALL_METHOD"
X}
X
Xrun_step "Running preflight checks" step_preflight
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/10_download.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 10_download.sh — fetch the Ferron release tarball for the detected arch.
X#
X# For archive installs this step either:
X#   1. Downloads the latest release from https://dl.ferron.sh/ (or a
X#      user-specified version via the FERRON_VERSION environment variable), or
X#   2. Validates a locally downloaded archive that the user provides.
X#
X# For package installs the step skips itself because the package manager
X# handles binary distribution via its own repositories.
X#
X# Downloaded tarball:
X#   https://dl.ferron.sh/{version}/ferron-{version}-{triple}.tar.gz
X#
X# Checksum verification:
X#   https://dl.ferron.sh/{version}/SHA256SUMS  (signed with GPG)
X#
X# Key variables exported:
X#   FERRON_VERSION      — detected or user-specified release version
X#   FERRON_ARCHIVE_PATH — path to the downloaded or local archive file
X
Xstep_download() {
X    # Package managers handle binary distribution.
X    if [ "$FERRON_INSTALL_METHOD" != "archive" ]; then
X        step_skip "package manager handles binary distribution"
X        return 0
X    fi
X
X    # ------------------------------------------------------------------
X    # Detect the download source mode.
X    # ------------------------------------------------------------------
X    # Honor FERRON_ARCHIVE_PATH as an explicit override for local installs.
X    # If it's set and points to an existing file, we use local mode.
X    # Otherwise we download from the internet.
X    if [ -n "${FERRON_ARCHIVE_PATH:-}" ] && [ -f "$FERRON_ARCHIVE_PATH" ]; then
X        FERRON_INSTALL_SOURCE="archive"
X        log_write "using local archive: $FERRON_ARCHIVE_PATH"
X    else
X        FERRON_INSTALL_SOURCE="download"
X        log_write "install source: download from internet"
X    fi
X
X    # ------------------------------------------------------------------
X    # Local archive mode
X    # ------------------------------------------------------------------
X    if [ "$FERRON_INSTALL_SOURCE" = "archive" ]; then
X        # The user may have set FERRON_ARCHIVE_PATH via environment, or we
X        # ask them for the path.
X        if [ ! -f "$FERRON_ARCHIVE_PATH" ]; then
X            ui_spinner_pause
X            ask_input FERRON_ARCHIVE_PATH \
X                "Path to the downloaded Ferron archive"
X            ui_spinner_resume
X        fi
X
X        # Validate the file exists and is a valid gzip-compressed tar archive.
X        if [ ! -f "$FERRON_ARCHIVE_PATH" ]; then
X            log_write "error: archive file not found: $FERRON_ARCHIVE_PATH"
X            return 1
X        fi
X
X        log_write "validating archive $FERRON_ARCHIVE_PATH"
X        if ! tar -tzf "$FERRON_ARCHIVE_PATH" >/dev/null 2>&1; then
X            log_write "error: $FERRON_ARCHIVE_PATH is not a valid tar.gz archive"
X            return 1
X        fi
X        log_write "archive validation passed"
X
X        # Try to detect the version from the archive filename (e.g. ferron-3.0.0-x86_64-unknown-linux-gnu.tar.gz).
X        _archive_basename=$(basename "$FERRON_ARCHIVE_PATH")
X        # Extract version: look for pattern -{digits}.{digits}.{digits}-
X        if printf '%s' "$_archive_basename" | grep -qE -- '-[0-9]+\.[0-9]+\.[0-9]+-'; then
X            FERRON_VERSION=$(printf '%s' "$_archive_basename" | sed -n 's/.*-\([0-9]\+\.[0-9]\+\.[0-9]\+\)-.*/\1/p')
X            log_write "detected version from filename: $FERRON_VERSION"
X        else
X            log_write "warning: could not detect version from archive filename"
X            FERRON_VERSION="unknown"
X        fi
X
X        return 0
X    fi
X
X    # ------------------------------------------------------------------
X    # Download mode
X    # ------------------------------------------------------------------
X
X    # ------------------------------------------------------------------
X    # Choose the download tool.
X    # ------------------------------------------------------------------
X    _download_cmd=""
X    _download_args=""
X
X    if command -v curl >/dev/null 2>&1; then
X        _download_cmd="curl"
X        _download_args="-fsSL"
X    elif command -v wget >/dev/null 2>&1; then
X        _download_cmd="wget"
X        _download_args="-qO-"
X    else
X        log_write "error: neither curl nor wget is available"
X        log_write "install one of them and retry, or use FERRON_ARCHIVE_PATH for local install"
X        return 1
X    fi
X
X    # ------------------------------------------------------------------
X    # Determine the version to download.
X    # ------------------------------------------------------------------
X    # Honor FERRON_VERSION env override first. Then check if the user
X    # wants the LTS channel. Finally, fetch the latest version from the
X    # server.
X    if [ -n "${FERRON_VERSION:-}" ]; then
X        log_write "using user-specified version: $FERRON_VERSION"
X    elif [ "$FERRON_INSTALL_LTS" = "1" ]; then
X        log_write "fetching LTS version from dl.ferron.sh"
X        FERRON_VERSION=$($_download_cmd $_download_args https://dl.ferron.sh/lts3.ferron 2>/dev/null)
X        if [ -z "$FERRON_VERSION" ]; then
X            log_write "error: failed to fetch LTS version from dl.ferron.sh"
X            return 1
X        fi
X        log_write "detected LTS version: $FERRON_VERSION"
X    else
X        log_write "fetching latest version from dl.ferron.sh"
X        FERRON_VERSION=$($_download_cmd $_download_args https://dl.ferron.sh/latest3.ferron 2>/dev/null)
X        if [ -z "$FERRON_VERSION" ]; then
X            log_write "error: failed to fetch latest version from dl.ferron.sh"
X            return 1
X        fi
X        log_write "detected latest version: $FERRON_VERSION"
X    fi
X
X    # ------------------------------------------------------------------
X    # Construct the download URL.
X    # ------------------------------------------------------------------
X    # The archive naming convention is:
X    #   ferron-{VERSION}-{ARCH}-unknown-{OS}-{LIBC}.tar.gz
X    # Examples:
X    #   ferron-3.0.0-x86_64-unknown-linux-gnu.tar.gz
X    #   ferron-3.0.0-aarch64-unknown-linux-musl.tar.gz
X    #   ferron-3.0.0-x86_64-unknown-freebsd.tar.gz
X    #   ferron-3.0.0-aarch64-apple-darwin.tar.gz
X
X    _download_url="https://dl.ferron.sh/$FERRON_VERSION/ferron-$FERRON_VERSION-$FERRON_TARGET_TRIPLE.tar.gz"
X    log_write "download URL: $_download_url"
X
X    # ------------------------------------------------------------------
X    # Create a temporary file for the download.
X    # ------------------------------------------------------------------
X    FERRON_ARCHIVE_PATH=$(mktemp /tmp/ferron-download.XXXXXX)
X    log_write "downloading to $FERRON_ARCHIVE_PATH"
X
X    # Download the archive.
X    if ! $_download_cmd $_download_args "$_download_url" > "$FERRON_ARCHIVE_PATH"; then
X        log_write "error: failed to download ferron archive"
X        rm -f "$FERRON_ARCHIVE_PATH"
X        return 1
X    fi
X
X    if [ ! -s "$FERRON_ARCHIVE_PATH" ]; then
X        log_write "error: downloaded archive is empty"
X        rm -f "$FERRON_ARCHIVE_PATH"
X        return 1
X    fi
X
X    log_write "download complete ($(du -h "$FERRON_ARCHIVE_PATH" | cut -f1))"
X
X    # ------------------------------------------------------------------
X    # Verify the checksum.
X    # ------------------------------------------------------------------
X    log_write "verifying checksum"
X    _expected_checksum=$($_download_cmd $_download_args "https://dl.ferron.sh/$FERRON_VERSION/SHA256SUMS" 2>/dev/null | grep "ferron-$FERRON_VERSION-$FERRON_TARGET_TRIPLE.tar.gz" | awk '{print $1}')
X
X    if [ -n "$_expected_checksum" ]; then
X        _actual_checksum=$(sha256sum "$FERRON_ARCHIVE_PATH" | awk '{print $1}')
X        if [ "$_expected_checksum" != "$_actual_checksum" ]; then
X            log_write "error: checksum mismatch"
X            log_write "  expected: $_expected_checksum"
X            log_write "  actual:   $_actual_checksum"
X            rm -f "$FERRON_ARCHIVE_PATH"
X            return 1
X        fi
X        log_write "checksum verification passed"
X    else
X        log_write "warning: could not fetch checksum from server, skipping verification"
X    fi
X
X    log_write "downloaded ferron $FERRON_VERSION for $FERRON_TARGET_TRIPLE"
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Downloading Ferron release" step_download
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/40_binaries.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 40_binaries.sh — install the ferron binaries into /usr/sbin.
X#
X# For archive installs this step extracts the downloaded tarball and copies
X# the binaries (ferron, ferron-kdl2ferron, ferron-passwd, ferron-precompress,
X# ferron-serve) into /usr/sbin with mode 0755.
X#
X# For package installs the step skips itself because the package manager
X# handles binary installation via its payload.
X#
X# When FERRON_INSTALL_MODE=update the step also creates a backup of the
X# existing binaries so they can be restored if the installation fails.
X#
X# Additionally, this step copies the service configuration files (ferron.service
X# for systemd, ferron.init for SysV init) from the installer bundle.
X
Xstep_install_binaries() {
X    # Package managers handle binary installation.
X    if [ "$FERRON_INSTALL_METHOD" != "archive" ]; then
X        step_skip "package manager handles binary installation"
X        return 0
X    fi
X
X    # Create a temporary extraction directory.
X    FERRON_EXTRACT_DIR=$(mktemp -d /tmp/ferron-install.XXXXXX)
X    log_write "extraction directory: $FERRON_EXTRACT_DIR"
X
X    # Extract the archive.
X    log_write "extracting archive $FERRON_ARCHIVE_PATH"
X    if ! tar -xzf "$FERRON_ARCHIVE_PATH" -C "$FERRON_EXTRACT_DIR"; then
X        log_write "error: failed to extract archive"
X        rm -rf "$FERRON_EXTRACT_DIR"
X        return 1
X    fi
X
X    # Verify that the expected binaries exist in the archive.
X    _expected_binaries="ferron ferron-kdl2ferron ferron-passwd ferron-precompress ferron-serve"
X    for _bin in $_expected_binaries; do
X        if [ ! -f "$FERRON_EXTRACT_DIR/$_bin" ]; then
X            log_write "warning: binary $_bin not found in archive, skipping"
X        fi
X    done
X
X    # If updating, back up existing binaries so we can restore on failure.
X    if [ "$FERRON_INSTALL_MODE" = "update" ]; then
X        FERRON_BACKUP_DIR=$(mktemp -d /tmp/ferron-backup.XXXXXX)
X        log_write "backup directory: $FERRON_BACKUP_DIR"
X        for _bin in $_expected_binaries; do
X            if [ -f "/usr/sbin/$_bin" ]; then
X                cp "/usr/sbin/$_bin" "$FERRON_BACKUP_DIR/"
X                log_write "backed up /usr/sbin/$_bin"
X            fi
X        done
X    fi
X
X    # Install binaries to /usr/sbin.
X    for _bin in $_expected_binaries; do
X        _src="$FERRON_EXTRACT_DIR/$_bin"
X        [ -f "$_src" ] || continue
X
X        log_write "installing /usr/sbin/$_bin"
X        if ! cp "$_src" "/usr/sbin/$_bin"; then
X            log_write "error: failed to install /usr/sbin/$_bin"
X            # Restore backup if update mode.
X            if [ "$FERRON_INSTALL_MODE" = "update" ] && [ -f "$FERRON_BACKUP_DIR/$_bin" ]; then
X                log_write "restoring backup /usr/sbin/$_bin"
X                cp "$FERRON_BACKUP_DIR/$_bin" "/usr/sbin/$_bin"
X            fi
X            rm -rf "$FERRON_EXTRACT_DIR" "$FERRON_BACKUP_DIR"
X            return 1
X        fi
X        chmod 0755 "/usr/sbin/$_bin"
X        log_write "installed /usr/sbin/$_bin (mode 0755)"
X    done
X
X    # Note: service unit files (ferron.service / ferron.init) are generated
X    # inline by step 60_service.sh, so we do not copy them from the bundle
X    # here.
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Installing binaries" step_install_binaries
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/20_user.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 20_user.sh — create the `ferron` system user and group.
X#
X# For archive installs this step creates the ferron system user and group
X# idempotently. It mirrors the behavior of the Debian postinst and the RPM
X# %pre scriptlet:
X#
X#     useradd -r -g ferron -d /var/lib/ferron -m -s /usr/sbin/nologin ferron
X#
X# For package installs the step skips itself because the package postinst
X# creates the user via its `useradd` or `adduser` commands.
X#
X# Alpine Linux uses `adduser` / `addgroup` instead of `useradd` / `groupadd`,
X# so we detect the distro and pick the right commands.
X
Xstep_create_user() {
X    # Package managers handle user creation.
X    if [ "$FERRON_INSTALL_METHOD" != "archive" ]; then
X        step_skip "package manager handles user creation"
X        return 0
X    fi
X
X    # Detect distro for command selection.
X    _has_groupadd=0
X    _has_useradd=0
X    _has_addgroup=0
X    _has_adduser=0
X
X    command -v groupadd >/dev/null 2>&1 && _has_groupadd=1
X    command -v useradd >/dev/null 2>&1 && _has_useradd=1
X    command -v addgroup >/dev/null 2>&1 && _has_addgroup=1
X    command -v adduser >/dev/null 2>&1 && _has_adduser=1
X
X    # Create the group if it doesn't exist.
X    if ! getent group ferron >/dev/null 2>&1; then
X        log_write "creating group ferron"
X        if [ "$_has_groupadd" = 1 ]; then
X            groupadd -r ferron || {
X                log_write "groupadd failed, trying addgroup"
X                if [ "$_has_addgroup" = 1 ]; then
X                    addgroup -S ferron
X                else
X                    log_write "error: neither groupadd nor addgroup available"
X                    return 1
X                fi
X            }
X        elif [ "$_has_addgroup" = 1 ]; then
X            addgroup -S ferron || {
X                log_write "error: addgroup -S ferron failed"
X                return 1
X            }
X        else
X            log_write "error: neither groupadd nor addgroup available"
X            return 1
X        fi
X    else
X        log_write "group ferron already exists"
X    fi
X
X    # Create the user if it doesn't exist.
X    if ! id -u ferron >/dev/null 2>&1; then
X        log_write "creating user ferron"
X        if [ "$_has_useradd" = 1 ]; then
X            useradd -r -g ferron -d /var/lib/ferron -m \
X                -s /usr/sbin/nologin ferron || {
X                log_write "useradd failed, trying adduser"
X                if [ "$_has_adduser" = 1 ]; then
X                    adduser -S -G ferron -D -H \
X                        -h /var/lib/ferron -s /sbin/nologin ferron
X                else
X                    log_write "error: neither useradd nor adduser available"
X                    return 1
X                fi
X            }
X        elif [ "$_has_adduser" = 1 ]; then
X            adduser -S -G ferron -D -H \
X                -h /var/lib/ferron -s /sbin/nologin ferron || {
X                log_write "error: adduser failed"
X                return 1
X            }
X        else
X            log_write "error: neither useradd nor adduser available"
X            return 1
X        fi
X        log_write "created user ferron (uid=$(id -u ferron))"
X    else
X        log_write "user ferron already exists (uid=$(id -u ferron))"
X
X        # Ensure the user's primary group is ferron.
X        _user_gid=$(id -g ferron)
X        _group_gid=$(getent group ferron | cut -d: -f3)
X        if [ "$_user_gid" != "$_group_gid" ]; then
X            log_write "updating ferron user's primary group to ferron"
X            if [ "$_has_useradd" = 1 ]; then
X                usermod -g ferron ferron
X            elif [ "$_has_adduser" = 1 ]; then
X                log_write "warning: cannot update group with adduser, skipping"
X            else
X                log_write "warning: cannot update group, no usermod available"
X            fi
X        fi
X    fi
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Creating ferron system user" step_create_user
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/90_verify.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 90_verify.sh — post-install smoke tests.
X#
X# This step verifies that the Ferron installation is working correctly.
X# It performs multiple checks depending on the install method:
X#
X# For archive installs:
X#   - Binary check: ferron --version
X#   - Config validation: ferron validate -c /etc/ferron/ferron.conf
X#   - Service status: systemctl or init script status
X#   - HTTP check: curl http://localhost/
X#
X# For package installs:
X#   - Package presence: dpkg -l ferron3 or rpm -q ferron3
X#   - Service status: same as above
X#
X# The step does NOT abort on failures — it logs them and lets the
X# installer complete. The user can then manually investigate.
X
Xstep_verify() {
X    # ------------------------------------------------------------------
X    # Binary check
X    # ------------------------------------------------------------------
X    log_write "verifying ferron binary"
X    if [ -x /usr/sbin/ferron ]; then
X        log_write "ferron binary exists and is executable"
X    else
X        log_write "warning: /usr/sbin/ferron does not exist or is not executable"
X    fi
X
X    # Try running --version to ensure it works.
X    if /usr/sbin/ferron --version >/dev/null 2>&1; then
X        _version=$(/usr/sbin/ferron --version 2>/dev/null | head -1)
X        log_write "ferron version: $_version"
X    else
X        log_write "warning: ferron --version failed"
X    fi
X
X    # ------------------------------------------------------------------
X    # Configuration validation (archive installs only)
X    # ------------------------------------------------------------------
X    if [ "$FERRON_INSTALL_METHOD" = "archive" ]; then
X        if [ -f /etc/ferron/ferron.conf ]; then
X            log_write "configuration file exists: /etc/ferron/ferron.conf"
X            if /usr/sbin/ferron validate -c /etc/ferron/ferron.conf >/dev/null 2>&1; then
X                log_write "configuration validation passed"
X            else
X                log_write "warning: configuration validation failed"
X            fi
X        else
X            log_write "warning: configuration file not found"
X        fi
X    fi
X
X    # ------------------------------------------------------------------
X    # Service status check
X    # ------------------------------------------------------------------
X    log_write "checking service status"
X
X    if [ "$FERRON_HAS_SYSTEMD" = 1 ]; then
X        if systemctl is-active --quiet ferron 2>/dev/null; then
X            log_write "systemd service is active"
X        else
X            log_write "systemd service is NOT active (may be started manually)"
X        fi
X    elif [ -f /etc/init.d/ferron ]; then
X        if /etc/init.d/ferron status >/dev/null 2>&1; then
X            log_write "init script reports service is running"
X        else
X            log_write "init script reports service is NOT running"
X        fi
X    else
X        log_write "warning: no service manager detected"
X    fi
X
X    # ------------------------------------------------------------------
X    # Port check
X    # ------------------------------------------------------------------
X    log_write "checking port 80"
X    if command -v ss >/dev/null 2>&1; then
X        _port_check=$(ss -tlnp 2>/dev/null | grep -c ':80 ' || echo "0")
X        if [ "$_port_check" -gt 0 ]; then
X            log_write "port 80 is listening"
X        else
X            log_write "warning: port 80 is NOT listening"
X        fi
X    elif command -v netstat >/dev/null 2>&1; then
X        _port_check=$(netstat -tlnp 2>/dev/null | grep -c ':80 ' || echo "0")
X        if [ "$_port_check" -gt 0 ]; then
X            log_write "port 80 is listening"
X        else
X            log_write "warning: port 80 is NOT listening"
X        fi
X    else
X        log_write "warning: neither ss nor netstat available for port check"
X    fi
X
X    # ------------------------------------------------------------------
X    # HTTP check (non-blocking)
X    # ------------------------------------------------------------------
X    log_write "checking HTTP response"
X    if command -v curl >/dev/null 2>&1; then
X        if curl -s --max-time 5 http://localhost/ >/dev/null 2>&1; then
X            log_write "HTTP check passed (curl)"
X        else
X            log_write "warning: HTTP check failed (curl)"
X        fi
X    elif command -v wget >/dev/null 2>&1; then
X        if wget -q --timeout=5 -O /dev/null http://localhost/ 2>/dev/null; then
X            log_write "HTTP check passed (wget)"
X        else
X            log_write "warning: HTTP check failed (wget)"
X        fi
X    else
X        log_write "warning: neither curl nor wget available for HTTP check"
X    fi
X
X    # ------------------------------------------------------------------
X    # Package check (package installs only)
X    # ------------------------------------------------------------------
X    if [ "$FERRON_INSTALL_METHOD" = "debian" ] || [ "$FERRON_INSTALL_METHOD" = "rhel" ]; then
X        if [ "$FERRON_INSTALL_METHOD" = "debian" ]; then
X            if dpkg -l ferron3 >/dev/null 2>&1; then
X                _pkg_version=$(dpkg -s ferron3 2>/dev/null | awk '/^Version:/{print $2}')
X                log_write "Debian package ferron3 is installed (version: $_pkg_version)"
X            else
X                log_write "warning: Debian package ferron3 is NOT installed"
X            fi
X        elif [ "$FERRON_INSTALL_METHOD" = "rhel" ]; then
X            if rpm -q ferron3 >/dev/null 2>&1; then
X                _pkg_version=$(rpm -q --queryformat '%{VERSION}' ferron3 2>/dev/null)
X                log_write "RPM package ferron3 is installed (version: $_pkg_version)"
X            else
X                log_write "warning: RPM package ferron3 is NOT installed"
X            fi
X        fi
X    fi
X
X    # ------------------------------------------------------------------
X    # Summary
X    # ------------------------------------------------------------------
X    log_write "=== verification complete ==="
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Verifying installation" step_verify
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/70_selinux.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 70_selinux.sh — configure SELinux contexts and booleans for Ferron.
X#
X# This step runs only for archive installs on RHEL/Fedora systems where
X# SELinux is enabled. It mirrors the logic from the RPM %post scriptlet:
X#
X#   - Sets httpd_can_network_connect boolean (for ACME/TLS and reverse proxy)
X#   - Adds file contexts for binaries, config, and data directories
X#   - Restores contexts with restorecon
X#   - Optionally adds QUIC UDP ports (80, 443) if semanage is available
X#
X# For package installs (Debian/RHEL RPM) this step skips because the
X# package manager's %post scriptlet handles it.
X#
X# For non-SELinux systems this step skips.
X#
X# For non-RHEL/Fedora systems this step skips.
X
Xstep_configure_selinux() {
X    # Only run for archive installs.
X    if [ "$FERRON_INSTALL_METHOD" != "archive" ]; then
X        step_skip "package manager handles SELinux configuration"
X        return 0
X    fi
X
X    # Skip if SELinux is not enabled.
X    if ! type selinuxenabled >/dev/null 2>&1; then
X        log_write "SELinux tools not found, skipping SELinux configuration"
X        return 0
X    fi
X
X    if ! selinuxenabled; then
X        log_write "SELinux is not enabled, skipping SELinux configuration"
X        return 0
X    fi
X
X    log_write "SELinux is enabled, configuring contexts and booleans"
X
X    # ------------------------------------------------------------------
X    # Set SELinux boolean for network connectivity (ACME, reverse proxy)
X    # ------------------------------------------------------------------
X    if type setsebool >/dev/null 2>&1; then
X        log_write "setting httpd_can_network_connect boolean"
X        if ! setsebool -P httpd_can_network_connect on; then
X            log_write "warning: failed to set httpd_can_network_connect boolean"
X        fi
X    else
X        log_write "warning: setsebool not available, skipping boolean configuration"
X    fi
X
X    # ------------------------------------------------------------------
X    # Set file contexts using semanage
X    # ------------------------------------------------------------------
X    if type semanage >/dev/null 2>&1; then
X        log_write "configuring SELinux file contexts"
X
X        # Ferron binary executable
X        if ! semanage fcontext -a -t httpd_exec_t "/usr/sbin/ferron" 2>/dev/null; then
X            semanage fcontext -m -t httpd_exec_t "/usr/sbin/ferron" 2>/dev/null || true
X        fi
X
X        # Configuration file
X        if ! semanage fcontext -a -t httpd_config_t "/etc/ferron/ferron.conf" 2>/dev/null; then
X            semanage fcontext -m -t httpd_config_t "/etc/ferron/ferron.conf" 2>/dev/null || true
X        fi
X
X        # Web root (content)
X        if ! semanage fcontext -a -t httpd_sys_content_t "/var/www/ferron(/.*)?" 2>/dev/null; then
X            semanage fcontext -m -t httpd_sys_content_t "/var/www/ferron(/.*)?" 2>/dev/null || true
X        fi
X
X        # Log directory
X        if ! semanage fcontext -a -t httpd_log_t "/var/log/ferron(/.*)?" 2>/dev/null; then
X            semanage fcontext -m -t httpd_log_t "/var/log/ferron(/.*)?" 2>/dev/null || true
X        fi
X
X        # Runtime data directory
X        if ! semanage fcontext -a -t httpd_var_lib_t "/var/lib/ferron(/.*)?" 2>/dev/null; then
X            semanage fcontext -m -t httpd_var_lib_t "/var/lib/ferron(/.*)?" 2>/dev/null || true
X        fi
X
X        # PID/runtime directory
X        if ! semanage fcontext -a -t httpd_var_run_t "/run/ferron(/.*)?" 2>/dev/null; then
X            semanage fcontext -m -t httpd_var_run_t "/run/ferron(/.*)?" 2>/dev/null || true
X        fi
X
X        log_write "file contexts configured"
X    else
X        log_write "warning: semanage not available, skipping file context configuration"
X    fi
X
X    # ------------------------------------------------------------------
X    # Restore contexts with restorecon
X    # ------------------------------------------------------------------
X    if type restorecon >/dev/null 2>&1; then
X        log_write "restoring SELinux contexts"
X        if ! restorecon -r /usr/sbin/ferron \
X            /usr/sbin/ferron-kdl2ferron \
X            /usr/sbin/ferron-passwd \
X            /usr/sbin/ferron-precompress \
X            /usr/sbin/ferron-serve \
X            /etc/ferron/ferron.conf \
X            /var/www/ferron \
X            /var/log/ferron \
X            /var/lib/ferron \
X            /run/ferron 2>/dev/null; then
X            log_write "warning: restorecon failed"
X        fi
X    else
X        log_write "warning: restorecon not available, skipping context restoration"
X    fi
X
X    # ------------------------------------------------------------------
X    # Optionally add QUIC UDP ports (80, 443)
X    # ------------------------------------------------------------------
X    if type semanage >/dev/null 2>&1; then
X        log_write "checking QUIC port configuration"
X
X        if ! semanage port -l | grep -q "http_port_t.*udp.*80"; then
X            if ! semanage port -a -t http_port_t -p udp 80 2>/dev/null; then
X                semanage port -a -t http_port_t -p udp 80 2>/dev/null || true
X            fi
X        fi
X
X        if ! semanage port -l | grep -q "http_port_t.*udp.*443"; then
X            if ! semanage port -a -t http_port_t -p udp 443 2>/dev/null; then
X                semanage port -a -t http_port_t -p udp 443 2>/dev/null || true
X            fi
X        fi
X    fi
X
X    log_write "SELinux configuration complete"
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Configuring SELinux" step_configure_selinux
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/30_dirs.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 30_dirs.sh — create the directory layout Ferron expects.
X#
X# For archive installs this step creates and chowns all directories that the
X# package manager would normally handle via its %dir or Files sections.
X#
X# For package installs the step skips itself because the package postinst
X# creates everything.
X#
X# Directories created:
X#   /etc/ferron          — configuration
X#   /var/log/ferron      — logs (owned by ferron:ferron, mode 0750)
X#   /var/lib/ferron      — runtime data (owned by ferron:ferron, mode 0750)
X#   /var/www/ferron      — web root
X#   /run/ferron          — PID files, sockets
X
Xstep_create_dirs() {
X    # Package managers handle directory layout.
X    if [ "$FERRON_INSTALL_METHOD" != "archive" ]; then
X        step_skip "package manager handles directory layout"
X        return 0
X    fi
X
X    # Create directories (idempotent — mkdir -p is safe to call repeatedly).
X    for _dir in /etc/ferron /var/log/ferron /var/lib/ferron /var/www/ferron /run/ferron; do
X        if [ ! -d "$_dir" ]; then
X            mkdir -p "$_dir"
X            log_write "created directory $_dir"
X        else
X            log_write "directory $_dir already exists"
X        fi
X    done
X
X    # Apply ownership.
X    # Log and data dirs are owned by the ferron user so the server can write
X    # logs and PID files without root.
X    if id -u ferron >/dev/null 2>&1; then
X        chown ferron:ferron /var/log/ferron /var/lib/ferron /run/ferron
X        chmod 0750 /var/log/ferron /var/lib/ferron /run/ferron
X        log_write "set ownership on /var/log/ferron, /var/lib/ferron and /run/ferron"
X    else
X        log_write "warning: ferron user not found, skipping chown"
X    fi
X
X    # Web root is owned by root so only root (or the installer) can modify it.
X    # The ferron user reads from it but never writes.
X    chown root:root /var/www/ferron
X    chmod 0755 /var/www/ferron
X    log_write "set ownership on /var/www/ferron"
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Creating directory layout" step_create_dirs
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/60_service.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 60_service.sh — install and enable the Ferron service.
X#
X# For archive installs this step:
X#   1. Detects whether systemd or SysV init is the active init system.
X#   2. Generates the appropriate unit file or init script inline (as a
X#      heredoc) — no external assets are needed.
X#   3. Installs it to the correct location.
X#   4. Asks the user whether to enable and start the service now.
X#   5. Enables and starts the service.
X#
X# For package installs the step skips itself because the package manager
X# handles service setup via its %systemd_unit, %pre, or postinst scripts.
X
Xstep_install_service() {
X    # Package managers handle service setup.
X    if [ "$FERRON_INSTALL_METHOD" != "archive" ]; then
X        step_skip "package manager handles service setup"
X        return 0
X    fi
X
X    # ------------------------------------------------------------------
X    # Ask the user whether to enable and start the service.
X    # ------------------------------------------------------------------
X    # In non-interactive mode we default to enabling and starting.
X    _enable_default="yes"
X    if [ "$FERRON_UI_STDIN" != 1 ]; then
X        FERRON_ENABLE_SERVICE="$_enable_default"
X        log_write "non-interactive mode: defaulting to enable=$FERRON_ENABLE_SERVICE"
X    else
X        ui_spinner_pause
X        if ask_choice FERRON_ENABLE_SERVICE \
X            "Enable and start the Ferron service now?" \
X            "yes" "no"; then
X            log_write "user chose to enable service: $FERRON_ENABLE_SERVICE"
X        else
X            log_write "user declined to enable service"
X        fi
X        ui_spinner_resume
X    fi
X
X    # ------------------------------------------------------------------
X    # Generate and install the service unit / init script.
X    # ------------------------------------------------------------------
X    if [ "$FERRON_HAS_SYSTEMD" = 1 ]; then
X        # ------------------------------------------------------------------
X        # Generate systemd unit file inline.
X        # ------------------------------------------------------------------
X        _unit_content=$(cat <<'UNIT_EOF'
X[Unit]
XDescription=Ferron web server
XAfter=network.target
X
X[Service]
XType=forking
XUser=ferron
XExecStart=/usr/sbin/ferron daemon -c /etc/ferron/ferron.conf --pid-file /run/ferron/ferron.pid
XExecReload=/bin/kill -HUP $MAINPID
XPIDFile=/run/ferron/ferron.pid
XRuntimeDirectory=ferron
XRestart=on-failure
XAmbientCapabilities=CAP_NET_BIND_SERVICE
X
X[Install]
XWantedBy=multi-user.target
XUNIT_EOF
X)
X
X        _unit_dst="/usr/lib/systemd/system/ferron.service"
X        log_write "writing systemd unit file to $_unit_dst"
X        printf '%s\n' "$_unit_content" > "$_unit_dst"
X        chmod 0644 "$_unit_dst"
X        log_write "installed systemd unit (mode 0644)"
X
X        # Reload systemd to pick up the new unit.
X        log_write "running systemctl daemon-reload"
X        if ! systemctl daemon-reload; then
X            log_write "warning: systemctl daemon-reload failed"
X        fi
X
X        # Enable and optionally start the service.
X        if [ "${FERRON_ENABLE_SERVICE:-no}" = "yes" ]; then
X            log_write "enabling and starting ferron service"
X            if ! systemctl enable --now ferron; then
X                log_write "warning: systemctl enable --now ferron failed"
X                # Try enable and start separately for better diagnostics.
X                if ! systemctl enable ferron; then
X                    log_write "warning: systemctl enable ferron failed"
X                fi
X                if ! systemctl start ferron; then
X                    log_write "warning: systemctl start ferron failed"
X                fi
X            fi
X        else
X            log_write "skipping service enable/start per user choice"
X        fi
X
X    # ------------------------------------------------------------------
X    # Generate OpenRC init script inline.
X    # ------------------------------------------------------------------
X    elif [ "$FERRON_HAS_OPENRC" -eq 1 ]; then
X        _init_content=$(cat <<'INIT_EOF'
X#!/sbin/openrc-run
X
Xname=$RC_SVCNAME
Xcommand="/usr/sbin/ferron"
Xcommand_args="daemon -c /etc/ferron/ferron.conf --pid-file /run/$RC_SVCNAME/$RC_SVCNAME.pid"
Xcommand_user="ferron"
Xstart_stop_daemon_args="--capabilities ^cap_net_bind_service"
Xextra_started_commands="reload"
X
Xdepend() {
X    need net
X}
X
Xstart_pre() {
X    checkpath --directory --owner $command_user:$command_user --mode 0775 \
X        /run/$RC_SVCNAME /var/log/$RC_SVCNAME
X}
X
Xreload() {
X    ebegin "Reloading ${RC_SVCNAME}"
X    start-stop-daemon --signal HUP --pidfile "${pidfile}"
X    eend $?
X}
X
XINIT_EOF
X)
X
X        _init_dst="/etc/init.d/ferron"
X        log_write "writing OpenRC init script to $_init_dst"
X        printf '%s\n' "$_init_content" > "$_init_dst"
X        chmod 0755 "$_init_dst"
X        log_write "installed init script (mode 0755)"
X
X        log_write "enabling init script via rc-update"
X        if ! rc-update add ferron default; then
X            log_write "warning: rc-update add ferron default failed"
X        fi
X
X        # Start the service if the user opted in.
X        if [ "${FERRON_ENABLE_SERVICE:-no}" = "yes" ]; then
X            log_write "starting ferron via init script"
X            if ! rc-service ferron start; then
X                log_write "warning: rc-service ferron start failed"
X                log_write "you can start the service manually with: /etc/init.d/ferron start"
X            fi
X        else
X            log_write "skipping service start per user choice"
X        fi
X
X    # ------------------------------------------------------------------
X    # Generate SysV init script inline.
X    # ------------------------------------------------------------------
X    else
X        _init_content=$(cat <<'INIT_EOF'
X#!/bin/sh
X### BEGIN INIT INFO
X# Provides:          ferron
X# Required-Start:    $network $syslog
X# Required-Stop:     $network $syslog
X# Default-Start:     2 3 4 5
X# Default-Stop:      0 1 6
X# Description:       Ferron web server
X### END INIT INFO
X
XNAME=ferron
XDAEMON=/usr/sbin/ferron
XPIDFILE=/run/ferron/${NAME}.pid
XRUNTIME_DIR=/run/ferron
XCONF=/etc/ferron/${NAME}.conf
XUSER=ferron
X
Xcase "$1" in
X    start)
X        mkdir -p $RUNTIME_DIR || true
X        chown -R $USER $RUNTIME_DIR || true
X        setcap 'cap_net_bind_service=+ep' $DAEMON >/dev/null 2>&1 || true
X        start-stop-daemon --start --user $USER --exec $DAEMON \
X            -- daemon -c $CONF --pid-file $PIDFILE
X        ;;
X    stop)
X        start-stop-daemon --stop --pidfile $PIDFILE --retry 10
X        rm -f $PIDFILE
X        ;;
X    reload)
X        if [ -f $PIDFILE ]; then
X            kill -HUP $(cat $PIDFILE)
X        fi
X        ;;
X    restart)
X        $0 stop
X        $0 start
X        ;;
X    status)
X        if [ -f $PIDFILE ] && kill -0 $(cat $PIDFILE) 2>/dev/null; then
X            echo "$NAME is running"
X            exit 0
X        fi
X        echo "$NAME is not running"
X        exit 1
X        ;;
X    *)
X        echo "Usage: $0 {start|stop|restart|status}"
X        exit 1
X        ;;
Xesac
Xexit 0
XINIT_EOF
X)
X
X        _init_dst="/etc/init.d/ferron"
X        log_write "writing SysV init script to $_init_dst"
X        printf '%s\n' "$_init_content" > "$_init_dst"
X        chmod 0755 "$_init_dst"
X        log_write "installed init script (mode 0755)"
X
X        # Enable the init script per distro.
X        case "$FERRON_DISTRO" in
X            debian|ubuntu|devuan|mx|pop|elementary|linuxmint)\
X                if ! type setcap >/dev/null 2>&1; then
X                    log_write "setcap not found, installing via apt"
X                    if ! apt-get install -y libcap2-bin; then
X                        log_write "warning: apt-get install libcap2-bin failed"
X                    fi
X                fi
X                log_write "enabling init script via update-rc.d"
X                if ! update-rc.d ferron defaults; then
X                    log_write "warning: update-rc.d ferron defaults failed"
X                fi
X                ;;
X            rhel|fedora|centos|rocky|almalinux|amzn|oracle|scientific|sles|opensuse)
X                if ! type setcap >/dev/null 2>&1; then
X                    log_write "setcap not found, installing via yum"
X                    if ! yum install -y libcap; then
X                        log_write "warning: yum install libcap failed"
X                    fi
X                fi
X                log_write "enabling init script via chkconfig"
X                if ! chkconfig --add ferron; then
X                    log_write "warning: chkconfig --add ferron failed, trying chkconfig --level"
X                    chkconfig --level 35 ferron on 2>/dev/null || true
X                fi
X                ;;
X            alpine)
X                if ! type setcap >/dev/null 2>&1; then
X                    log_write "setcap not found, installing via apk"
X                    if ! apk add --no-cache libcap; then
X                        log_write "warning: apk add libcap failed"
X                    fi
X                fi
X                log_write "enabling init script via rc-update"
X                if ! rc-update add ferron default; then
X                    log_write "warning: rc-update add ferron default failed"
X                fi
X                ;;
X            arch)
X                if ! type setcap >/dev/null 2>&1; then
X                    log_write "setcap not found, installing via pacman"
X                    if ! pacman -S --noconfirm libcap; then
X                        log_write "warning: pacman -S libcap failed"
X                    fi
X                fi
X                log_write "enabling init script via rc-update"
X                if ! rc-update add ferron default; then
X                    log_write "warning: rc-update add ferron default failed"
X                fi
X                ;;
X            freebsd)
X                # Setcap is Linux-specific, so we don't need it on FreeBSD.
X                log_write "enabling init script via rc.d (FreeBSD)"
X                if [ ! -d /etc/rc.d ]; then
X                    mkdir -p /etc/rc.d
X                fi
X                if [ ! -f /etc/rc.d/ferron ]; then
X                    cp "$_init_dst" /etc/rc.d/ferron
X                fi
X                chmod 0755 /etc/rc.d/ferron
X                log_write "enabled via /etc/rc.d/ferron"
X                ;;
X            *)
X                log_write "warning: unknown distro $FERRON_DISTRO, cannot enable init script automatically"
X                log_write "please run the appropriate command to enable the service manually"
X                ;;
X        esac
X
X        # Start the service if the user opted in.
X        if [ "${FERRON_ENABLE_SERVICE:-no}" = "yes" ]; then
X            log_write "starting ferron via init script"
X            if ! /etc/init.d/ferron start; then
X                log_write "warning: /etc/init.d/ferron start failed"
X                log_write "you can start the service manually with: /etc/init.d/ferron start"
X            fi
X        else
X            log_write "skipping service start per user choice"
X        fi
X    fi
X}
X
Xif [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X    run_step "Installing service configuration" step_install_service
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/steps"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/steps/80_uninstall.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# 80_uninstall.sh — uninstall Ferron completely.
X#
X# This step runs only when FERRON_INSTALL_MODE=uninstall. It detects how
X# Ferron was installed (installer-managed or package manager), stops and
X# disables the service, removes binaries and configuration files, and
X# optionally removes the ferron system user.
X#
X# For installer-managed installs:
X#   - Removes binaries from /usr/sbin/
X#   - Removes /etc/ferron, /var/log/ferron, /var/www/ferron, /run/ferron
X#   - Removes /etc/.ferron-installer.* metadata
X#   - Optionally removes the ferron user/group
X#
X# For package-based installs:
X#   - Uses the package manager to purge the package
X#   - Removes service configuration
X#   - Optionally removes the ferron user/group
X
Xstep_uninstall() {
X    # This step only runs when FERRON_INSTALL_MODE=uninstall.
X    if [ "$FERRON_INSTALL_MODE" != "uninstall" ]; then
X        step_skip "not an uninstall operation"
X        return 0
X    fi
X
X    # Detect how Ferron was installed.
X    FERRON_WAS_INSTALLED_BY_INSTALLER=0
X    FERRON_WAS_INSTALLED_BY_PACKAGE=0
X
X    if [ -f /etc/.ferron-installer.version ]; then
X        FERRON_WAS_INSTALLED_BY_INSTALLER=1
X        log_write "uninstall: detected installer-managed installation"
X    elif dpkg -l ferron3 >/dev/null 2>&1; then
X        FERRON_WAS_INSTALLED_BY_PACKAGE=1
X        FERRON_INSTALL_METHOD="debian"
X        log_write "uninstall: detected Debian package installation"
X    elif rpm -q ferron3 >/dev/null 2>&1; then
X        FERRON_WAS_INSTALLED_BY_PACKAGE=1
X        FERRON_INSTALL_METHOD="rhel"
X        log_write "uninstall: detected RPM package installation"
X    elif [ -x /usr/sbin/ferron ]; then
X        # Binary-only install detected — treat as installer-managed for removal.
X        FERRON_WAS_INSTALLED_BY_INSTALLER=1
X        log_write "uninstall: detected binary-only installation"
X    fi
X
X    # ------------------------------------------------------------------
X    # Stop and disable the service
X    # ------------------------------------------------------------------
X    log_write "stopping Ferron service"
X
X    if [ "$FERRON_HAS_SYSTEMD" = 1 ]; then
X        log_write "stopping via systemctl"
X        systemctl stop ferron 2>/dev/null || true
X        systemctl disable ferron 2>/dev/null || true
X        rm -f /usr/lib/systemd/system/ferron.service
X        log_write "removed systemd unit file"
X        systemctl daemon-reload 2>/dev/null || true
X
X    elif [ "$FERRON_HAS_OPENRC" = 1 ]; then
X        log_write "stopping via OpenRC"
X        rc-service ferron stop 2>/dev/null || true
X        rc-update del ferron default 2>/dev/null || true
X        rm -f /etc/init.d/ferron
X        log_write "removed OpenRC init script"
X
X    elif [ -f /etc/init.d/ferron ]; then
X        log_write "stopping via init script"
X        /etc/init.d/ferron stop 2>/dev/null || true
X        case "$FERRON_DISTRO" in
X            debian|ubuntu|devuan|mx|pop|elementary|linuxmint)
X                log_write "disabling via update-rc.d"
X                update-rc.d -f ferron remove 2>/dev/null || true
X                ;;
X            rhel|fedora|centos|rocky|almalinux|amzn|oracle|scientific|sles|opensuse)
X                log_write "disabling via chkconfig"
X                chkconfig --del ferron 2>/dev/null || true
X                ;;
X            alpine)
X                log_write "disabling via rc-update"
X                rc-update del ferron default 2>/dev/null || true
X                ;;
X            arch)
X                log_write "disabling via rc-update"
X                rc-update del ferron default 2>/dev/null || true
X                ;;
X            freebsd)
X                log_write "disabling via /etc/rc.d/"
X                rm -f /etc/rc.d/ferron 2>/dev/null || true
X                ;;
X        esac
X        rm -f /etc/init.d/ferron
X        log_write "removed init script"
X    fi
X
X    # ------------------------------------------------------------------
X    # Remove binaries
X    # ------------------------------------------------------------------
X    if [ "$FERRON_WAS_INSTALLED_BY_INSTALLER" = 1 ]; then
X        log_write "removing binaries from /usr/sbin/"
X        for _bin in ferron ferron-kdl2ferron ferron-passwd ferron-precompress ferron-serve; do
X            rm -f "/usr/sbin/$_bin" 2>/dev/null || true
X            log_write "removed /usr/sbin/$_bin"
X        done
X    elif [ "$FERRON_WAS_INSTALLED_BY_PACKAGE" = 1 ]; then
X        log_write "package manager will handle binary removal"
X        case "$FERRON_INSTALL_METHOD" in
X            debian)
X                log_write "purging ferron3 via apt"
X                DEBIAN_FRONTEND=noninteractive apt purge -y ferron3 2>/dev/null || true
X                ;;
X            rhel)
X                log_write "purging ferron3 via yum/dnf"
X                if command -v dnf >/dev/null 2>&1; then
X                    dnf remove -y ferron3 2>/dev/null || true
X                else
X                    yum remove -y ferron3 2>/dev/null || true
X                fi
X                ;;
X        esac
X    fi
X
X    # ------------------------------------------------------------------
X    # Remove files (with confirmation)
X    # ------------------------------------------------------------------
X    if [ "$FERRON_WAS_INSTALLED_BY_INSTALLER" = 1 ]; then
X        log_write "removing configuration and data directories"
X
X        # Remove main directories
X        rm -rf /etc/ferron
X        log_write "removed /etc/ferron"
X
X        rm -rf /var/log/ferron
X        log_write "removed /var/log/ferron"
X
X        rm -rf /var/lib/ferron
X        log_write "removed /var/lib/ferron"
X
X        rm -rf /var/www/ferron
X        log_write "removed /var/www/ferron"
X
X        rm -rf /run/ferron
X        log_write "removed /run/ferron"
X
X        # Remove installer metadata
X        rm -f /etc/.ferron-installer.version
X        rm -f /etc/.ferron-installer.prop
X        rm -f /etc/.ferron-installer-channel
X        log_write "removed installer metadata"
X    fi
X
X    # ------------------------------------------------------------------
X    # Optionally remove the ferron system user
X    # ------------------------------------------------------------------
X    if [ "$FERRON_WAS_INSTALLED_BY_INSTALLER" = 1 ]; then
X        ui_spinner_pause
X        if ask_choice FERRON_REMOVE_USER \
X            "Remove the ferron system user and group?" \
X            "yes" "no"; then
X            log_write "user chose: $FERRON_REMOVE_USER"
X        else
X            log_write "user declined to remove ferron user/group"
X        fi
X        ui_spinner_resume
X
X        if [ "${FERRON_REMOVE_USER:-no}" = "yes" ]; then
X            log_write "removing ferron user and group"
X            userdel -r ferron 2>/dev/null || true
X            groupdel ferron 2>/dev/null || true
X            log_write "removed ferron user and group"
X        fi
X    fi
X
X    log_write "uninstall complete"
X}
X
Xif [ "$FERRON_INSTALL_MODE" = "uninstall" ]; then
X    run_step "Uninstalling Ferron" step_uninstall
Xfi
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/lib"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/lib/log.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# log.sh — installer log file management.
X#
X# Every step's stdout and stderr are redirected into $FERRON_INSTALLER_LOG so
X# that the on-screen UI stays clean and a full transcript is available when
X# something goes wrong. The log file lives inside the extraction directory so
X# it survives a failed install (the trap in main.sh only cleans up on
X# success).
X
Xlog_init() {
X    : "${FERRON_INSTALLER_EXTRACT_DIR:?log_init: FERRON_INSTALLER_EXTRACT_DIR is unset}"
X    FERRON_INSTALLER_LOG="$FERRON_INSTALLER_EXTRACT_DIR/install.log"
X    : >"$FERRON_INSTALLER_LOG"
X    export FERRON_INSTALLER_LOG
X}
X
X# log_write MESSAGE…
X#
X# Append a timestamped line to the log file only. Useful for breadcrumbs that
X# shouldn't appear on screen (e.g. detected distro, chosen options).
Xlog_write() {
X    printf '[%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$*" \
X        >>"$FERRON_INSTALLER_LOG"
X}
X
X# log_tail [N]
X#
X# Print the last N lines (default 20) of the log to stdout, each prefixed with
X# two spaces so the failure report visually nests under its header.
Xlog_tail() {
X    n=${1:-20}
X    if [ -s "$FERRON_INSTALLER_LOG" ]; then
X        tail -n "$n" "$FERRON_INSTALLER_LOG" | sed 's/^/  /'
X    else
X        printf '  (log file is empty)\n'
X    fi
X}
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/lib"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/lib/step.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# step.sh — step state machine.
X#
X# Wraps the "announce step → run command → render final status" lifecycle so
X# each individual step file stays tiny. Steps that fail abort the installer
X# via ui_failure; steps that succeed fall through to the next one.
X#
X# Usage:
X#
X#     my_step() {
X#         do_thing_one
X#         do_thing_two
X#     }
X#     run_step "Short human-readable label" my_step
X#
X# The command and its arguments are executed in the current shell (no
X# subshell) so environment changes made by a step — for example, exporting a
X# detected distro name — are visible to subsequent steps. Stdout and stderr
X# are redirected to the installer log for the duration of the step.
X#
X# A step that wants to be skipped (e.g. systemd setup on a non-systemd host)
X# should call `step_skip "reason"` and return 0. The WAIT/OK/SKIP/FAIL state
X# transitions are handled here, not in the step body.
X
X# step_skip REASON
X#
X# Mark the current step as skipped. Prints REASON into the log for
X# traceability and sets a flag that run_step reads after the step returns.
Xstep_skip() {
X    FERRON_STEP_SKIP=1
X    log_write "step skipped: $*"
X}
X
X# run_step LABEL FUNCTION [ARGS…]
X#
X# Runs FUNCTION with ARGS, showing LABEL on screen. If FUNCTION exits
X# non-zero, renders the failure UI and exits the installer with that code.
Xrun_step() {
X    _label=$1
X    shift
X
X    FERRON_STEP_SKIP=0
X    ui_step_begin "$_label"
X    log_write "=== step begin: $_label ==="
X
X    # Run the step with stdout+stderr going to the log, but keep the step
X    # executing in the current shell so it can export variables. We capture
X    # the exit status by wrapping in an if-guard; `set -e` inside the step
X    # body still works because functions inherit errexit from the caller.
X    _rc=0
X    "$@" >>"$FERRON_INSTALLER_LOG" 2>&1 || _rc=$?
X
X    if [ "$_rc" -ne 0 ]; then
X        ui_step_end FAIL
X        log_write "=== step failed: $_label (rc=$_rc) ==="
X        ui_failure "$_label" "$_rc"
X        exit "$_rc"
X    fi
X
X    if [ "$FERRON_STEP_SKIP" = 1 ]; then
X        ui_step_end SKIP
X        log_write "=== step skipped: $_label ==="
X    else
X        ui_step_end OK
X        log_write "=== step ok: $_label ==="
X    fi
X}
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/lib"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/lib/ui.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# ui.sh — rendering primitives for the installer UI.
X#
X# The UI is line-oriented, not screen-oriented. We render by appending lines
X# and — in ANSI mode — rewriting only the status cell of the current step
X# line. This keeps the implementation small and robust against stray output
X# from misbehaving steps (such output just scrolls past; it doesn't corrupt a
X# screen buffer we'd otherwise have to maintain).
X#
X# Status line anatomy:
X#
X#     [ OK ] Installing binaries
X#     ^^^^^^ ^^^^^^^^^^^^^^^^^^^
X#       |      label
X#       status cell, exactly 6 characters wide (`[`, space, 2- or 4-char
X#       state text, space, `]`). Two-letter states like `OK` get an extra
X#       leading space so the right bracket stays at the same column as it
X#       does for 4-letter states like `FAIL`.
X#
X# The status cell is rewritten in place by moving the cursor back to column 1,
X# erasing the line, and reprinting `[ STATE ] label`.
X#
X# Globals set by ui_init (in addition to those from tty_init):
X#
X#   FERRON_UI_C_RESET      SGR reset sequence (or empty).
X#   FERRON_UI_C_DIM        Dim/gray SGR.
X#   FERRON_UI_C_GREEN      Green SGR.
X#   FERRON_UI_C_RED        Red SGR.
X#   FERRON_UI_C_YELLOW     Yellow SGR.
X#   FERRON_UI_C_BOLD       Bold SGR.
X#
X#   FERRON_UI_SPINNER_PID  PID of the currently running spinner subshell, or
X#                          empty if no spinner is active.
X#   FERRON_UI_STEP_LABEL   Label of the currently in-progress step (used to
X#                          redraw the line when ending the step).
X
Xui_init() {
X    tty_init
X
X    if [ "$FERRON_UI_COLOR" = 1 ]; then
X        # Using printf with \033 rather than tput so we don't depend on the
X        # full ncurses terminfo database being installed.
X        FERRON_UI_C_RESET=$(printf '\033[0m')
X        FERRON_UI_C_DIM=$(printf '\033[2m')
X        FERRON_UI_C_GREEN=$(printf '\033[32m')
X        FERRON_UI_C_RED=$(printf '\033[31m')
X        FERRON_UI_C_YELLOW=$(printf '\033[33m')
X        FERRON_UI_C_BOLD=$(printf '\033[1m')
X    else
X        FERRON_UI_C_RESET=''
X        FERRON_UI_C_DIM=''
X        FERRON_UI_C_GREEN=''
X        FERRON_UI_C_RED=''
X        FERRON_UI_C_YELLOW=''
X        FERRON_UI_C_BOLD=''
X    fi
X
X    FERRON_UI_SPINNER_PID=''
X    FERRON_UI_STEP_LABEL=''
X}
X
X# ui_banner
X#
X# Print the ASCII-art banner followed by the welcome line. Uses the Unicode
X# banner when the locale looks UTF-8 capable, otherwise falls back to the
X# plain-ASCII variant.
Xui_banner() {
X    if [ "$FERRON_UI_COLOR" = 1 ] && [ "$FERRON_UI_ANSI" = 1 ] && \
X       [ -r "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner.txt" ]; then
X        cat "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner.txt"
X    elif [ "$FERRON_UI_UTF8" = 1 ] && \
X       [ -r "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner-mono.txt" ]; then
X        cat "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner-mono.txt"
X    elif [ -r "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner-ascii.txt" ]; then
X        cat "$FERRON_INSTALLER_EXTRACT_DIR/assets/banner-ascii.txt"
X    fi
X    printf '\n'
X    printf '%sWelcome to the Ferron 3 installer for Linux!%s\n' \
X        "$FERRON_UI_C_BOLD" "$FERRON_UI_C_RESET"
X    printf '\n'
X}
X
X# ui_info MESSAGE…
X#
X# Print an informational line between steps (no status cell).
Xui_info() {
X    printf '%s\n' "$*"
X}
X
X# _ui_render_status STATE
X#
X# Internal: print a 6-character-wide status cell `[ STATE ]`. Two-letter
X# states like `OK` are rendered as `[ OK ]` (matching the mockup); four-
X# letter states like `FAIL`, `SKIP`, and `WAIT` are rendered as `[FAIL]`
X# (without interior spaces) so the outer brackets stay aligned across every
X# row. Does NOT emit a trailing newline.
X_ui_render_status() {
X    case "$1" in
X        OK)   color=$FERRON_UI_C_GREEN;  text=' OK ' ;;
X        FAIL) color=$FERRON_UI_C_RED;    text='FAIL' ;;
X        SKIP) color=$FERRON_UI_C_YELLOW; text='SKIP' ;;
X        WAIT) color=$FERRON_UI_C_DIM;    text='....' ;;
X        *)    color='';                  text=$(printf '%-4.4s' "$1") ;;
X    esac
X    printf '[%s%s%s]' "$color" "$text" "$FERRON_UI_C_RESET"
X}
X
X# _ui_spinner_frames
X#
X# Internal: print the spinner frame set as a single space-separated line.
X# Braille dots look great on modern terminals; ASCII fallback stays readable
X# on serial consoles and minimal busybox shells.
X_ui_spinner_frames() {
X    if [ "$FERRON_UI_UTF8" = 1 ]; then
X        printf '⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏'
X    else
X        printf '| / - \\'
X    fi
X}
X
X# _ui_spinner_loop LABEL
X#
X# Internal: runs in a background subshell. Every ~100ms, rewrites the current
X# line with `[ <frame> ] LABEL`. Exits silently when killed by ui_step_end.
X_ui_spinner_loop() {
X    label=$1
X    # shellcheck disable=SC2086  # intentional word-splitting on the frames.
X    set -- $(_ui_spinner_frames)
X    # Ignore TERM so we can clean up deterministically; the parent kills us.
X    trap 'exit 0' TERM INT
X    while :; do
X        for frame in "$@"; do
X            # \r moves to column 1, \033[2K erases the whole line. Together
X            # they give us a stable, flicker-free rewrite even when the
X            # terminal is narrower than the label. The spinner frame takes
X            # the place of the 4-char state text so the cell stays 6 chars
X            # wide (`[`, space, frame, 2 spaces, `]`).
X            printf '\r\033[2K[ %s%s%s  ] %s' \
X                "$FERRON_UI_C_DIM" "$frame" "$FERRON_UI_C_RESET" "$label"
X            sleep 0.1 2>/dev/null || sleep 1
X        done
X    done
X}
X
X# ui_step_begin LABEL
X#
X# Announce the start of a step. In ANSI mode, spawns a spinner that updates
X# the status cell until ui_step_end is called. In degraded mode, just prints
X# `[ .... ] LABEL` on its own line.
Xui_step_begin() {
X    FERRON_UI_STEP_LABEL=$1
X
X    if [ "$FERRON_UI_ANSI" = 1 ]; then
X        # Hide the cursor so the spinner doesn't make it jitter.
X        printf '\033[?25l'
X        _ui_spinner_loop "$FERRON_UI_STEP_LABEL" &
X        FERRON_UI_SPINNER_PID=$!
X    else
X        _ui_render_status WAIT
X        printf ' %s\n' "$FERRON_UI_STEP_LABEL"
X    fi
X}
X
X# ui_step_end STATUS
X#
X# Finalize the currently in-progress step. STATUS is one of OK, FAIL, SKIP.
X# In ANSI mode, rewrites the spinner line with the final status and emits a
X# newline so subsequent output appears below. In degraded mode, prints a
X# fresh line (the earlier `[ .... ]` line stays in the scrollback, which is
X# fine for log capture).
Xui_step_end() {
X    status=$1
X
X    if [ "$FERRON_UI_ANSI" = 1 ]; then
X        if [ -n "$FERRON_UI_SPINNER_PID" ]; then
X            kill "$FERRON_UI_SPINNER_PID" 2>/dev/null || true
X            wait "$FERRON_UI_SPINNER_PID" 2>/dev/null || true
X            FERRON_UI_SPINNER_PID=''
X        fi
X        printf '\r\033[2K'
X        _ui_render_status "$status"
X        printf ' %s\n' "$FERRON_UI_STEP_LABEL"
X        # Restore the cursor.
X        printf '\033[?25h'
X    else
X        _ui_render_status "$status"
X        printf ' %s\n' "$FERRON_UI_STEP_LABEL"
X    fi
X
X    FERRON_UI_STEP_LABEL=''
X}
X
X# ui_spinner_pause
X#
X# Temporarily kill the spinner and erase its line. Use this before reading
X# interactive input so the prompt isn't overwritten by the next spinner
X# frame. Pair with ui_spinner_resume.
Xui_spinner_pause() {
X    if [ "$FERRON_UI_ANSI" = 1 ] && [ -n "$FERRON_UI_SPINNER_PID" ]; then
X        kill "$FERRON_UI_SPINNER_PID" 2>/dev/null || true
X        wait "$FERRON_UI_SPINNER_PID" 2>/dev/null || true
X        FERRON_UI_SPINNER_PID=''
X        printf '\r\033[2K'
X        printf '\033[?25h'
X    fi
X}
X
X# ui_spinner_resume
X#
X# Resume the spinner for the currently-tracked step label. No-op if no step
X# is in progress.
Xui_spinner_resume() {
X    if [ "$FERRON_UI_ANSI" = 1 ] && [ -n "$FERRON_UI_STEP_LABEL" ] && \
X       [ -z "$FERRON_UI_SPINNER_PID" ]; then
X        printf '\033[?25l'
X        _ui_spinner_loop "$FERRON_UI_STEP_LABEL" >&3 &
X        FERRON_UI_SPINNER_PID=$!
X    fi
X}
X
X# ui_success
X#
X# Final screen for a successful installation. Matches the mockup exactly,
X# modulo the emoji which degrades to ":)" in non-UTF-8 locales.
Xui_success() {
X    if [ "$FERRON_INSTALL_MODE" = "uninstall" ]; then
X        printf '\n'
X        printf '%sUninstall completed successfully.%s\n' \
X            "$FERRON_UI_C_BOLD" "$FERRON_UI_C_RESET"
X        return
X    fi
X    if [ "$FERRON_UI_UTF8" = 1 ]; then
X        celebrate='🥳'
X    else
X        celebrate=':)'
X    fi
X    printf '\n'
X    printf '%sFerron is installed successfully!%s %s\n' \
X        "$FERRON_UI_C_BOLD" "$FERRON_UI_C_RESET" "$celebrate"
X    printf '\n'
X    printf 'To access the website, open the web browser and navigate to your server'\''s address.\n'
X    printf 'For more information, the documentation is in https://ferron.sh/docs/v3\n'
X}
X
X# ui_failure LABEL EXIT_CODE
X#
X# Final screen for a failed installation. Prints a red header, a tail of the
X# log, and a pointer to the full log file. Does NOT exit — the caller decides
X# what exit code to propagate.
Xui_failure() {
X    label=$1
X    rc=$2
X    printf '\n'
X    printf '%sInstallation failed at step:%s %s (exit code %s)\n' \
X        "$FERRON_UI_C_RED$FERRON_UI_C_BOLD" "$FERRON_UI_C_RESET" \
X        "$label" "$rc"
X    printf '\n'
X    printf 'Last lines of the installer log:\n'
X    log_tail 20
X    printf '\n'
X    printf 'Full log: %s\n' "$FERRON_INSTALLER_LOG"
X}
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/lib"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/lib/prompt.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# prompt.sh — interactive input helpers.
X#
X# Two public entry points:
X#
X#   ask_input VARNAME "Question" [default]
X#   ask_choice VARNAME "Question" opt1 opt2 …
X#
X# Both assign the user's answer to the shell variable named by VARNAME. In
X# non-interactive mode (stdin is not a TTY, typical of `curl … | sh`) they
X# honor an environment-variable override of the same name, then fall back to
X# the default (for ask_input) or the first option (for ask_choice). If
X# neither is available, they die with a clear message pointing at the env
X# variable to set.
X#
X# Prompts automatically pause and resume the spinner if one is running.
X
X# _prompt_validate_varname NAME
X#
X# Internal: ensure NAME is a syntactically valid shell variable name before
X# using it in `eval`. Aborts the installer on violation; a bad varname here
X# is always a programmer bug, not user input.
X_prompt_validate_varname() {
X    case "$1" in
X        ''|*[!A-Za-z0-9_]*|[0-9]*)
X            printf 'prompt: invalid variable name: %s\n' "$1" >&2
X            exit 2
X            ;;
X    esac
X}
X
X# _prompt_assign VARNAME VALUE
X#
X# Internal: safely set a shell variable by name. VALUE is single-quoted with
X# embedded single quotes escaped so arbitrary user input can't break out.
X_prompt_assign() {
X    _name=$1
X    _value=$2
X    # Escape every ' as '\'' so the value survives single-quote wrapping.
X    _escaped=$(printf '%s' "$_value" | sed "s/'/'\\\\''/g")
X    eval "$_name='$_escaped'"
X}
X
X# ask_input VARNAME "Question" [default]
Xask_input() {
X    _prompt_validate_varname "$1"
X    _varname=$1
X    _question=$2
X    _default=${3:-}
X
X    # Honor env override first — works in both interactive and pipe modes.
X    eval "_envval=\${$_varname:-}"
X    if [ -n "$_envval" ]; then
X        log_write "ask_input $_varname: using env override"
X        return 0
X    fi
X
X    if [ "$FERRON_UI_STDIN" != 1 ]; then
X        if [ -n "$_default" ]; then
X            log_write "ask_input $_varname: non-interactive, using default: $_default"
X            _prompt_assign "$_varname" "$_default"
X            return 0
X        fi
X        printf 'This installer needs a value for %s but stdin is not a terminal.\n' \
X            "$_varname" >&2
X        printf 'Set the %s environment variable before running the installer.\n' \
X            "$_varname" >&2
X        exit 2
X    fi
X
X    ui_spinner_pause
X    printf '\033[2K\r' >&3
X    if [ -n "$_default" ]; then
X        printf '%s? %s%s [%s]: ' \
X            "$FERRON_UI_C_BOLD" "$_question" "$FERRON_UI_C_RESET" "$_default" >&3
X    else
X        printf '%s? %s%s: ' \
X            "$FERRON_UI_C_BOLD" "$_question" "$FERRON_UI_C_RESET" >&3
X    fi
X    IFS= read -r _answer || _answer=''
X    if [ -z "$_answer" ]; then
X        _answer=$_default
X    fi
X    _prompt_assign "$_varname" "$_answer"
X    log_write "ask_input $_varname: '$_answer'"
X    ui_spinner_resume
X}
X
X# ask_choice VARNAME "Question" opt1 opt2 …
X#
X# Renders a numbered menu. Accepts either the option number or the exact
X# option text as input. In non-interactive mode, the first option is used as
X# the default unless an env override is set.
Xask_choice() {
X    _prompt_validate_varname "$1"
X    _varname=$1
X    _question=$2
X    shift 2
X
X    if [ $# -lt 1 ]; then
X        printf 'ask_choice: at least one option is required\n' >&2
X        exit 2
X    fi
X
X    # Env override: must match one of the options literally.
X    eval "_envval=\${$_varname:-}"
X    if [ -n "$_envval" ]; then
X        for _opt in "$@"; do
X            if [ "$_opt" = "$_envval" ]; then
X                log_write "ask_choice $_varname: env override '$_envval'"
X                return 0
X            fi
X        done
X        printf 'Environment variable %s=%s is not a valid choice.\n' \
X            "$_varname" "$_envval" >&2
X        printf 'Valid choices:' >&2
X        for _opt in "$@"; do
X            printf ' %s' "$_opt" >&2
X        done
X        printf '\n' >&2
X        exit 2
X    fi
X
X    if [ "$FERRON_UI_STDIN" != 1 ]; then
X        log_write "ask_choice $_varname: non-interactive, using default: $1"
X        _prompt_assign "$_varname" "$1"
X        return 0
X    fi
X
X    ui_spinner_pause
X    if [ "$FERRON_UI_ANSI" = 1 ]; then
X        printf '\033[2K\r' >&3
X    else
X        printf '\n' >&3
X    fi
X    printf '%s? %s%s\n' \
X        "$FERRON_UI_C_BOLD" "$_question" "$FERRON_UI_C_RESET" >&3
X    _i=1
X    for _opt in "$@"; do
X        printf '  %s) %s\n' "$_i" "$_opt" >&3
X        _i=$((_i + 1))
X    done
X
X    while :; do
X        printf '  choice [1]: ' >&3
X        IFS= read -r _answer || _answer=''
X        [ -z "$_answer" ] && _answer=1
X
X        # Numeric selection.
X        case "$_answer" in
X            ''|*[!0-9]*) ;;
X            *)
X                if [ "$_answer" -ge 1 ] && [ "$_answer" -le $# ]; then
X                    _i=1
X                    for _opt in "$@"; do
X                        if [ "$_i" = "$_answer" ]; then
X                            _prompt_assign "$_varname" "$_opt"
X                            log_write "ask_choice $_varname: '$_opt'"
X                            ui_spinner_resume
X                            return 0
X                        fi
X                        _i=$((_i + 1))
X                    done
X                fi
X                ;;
X        esac
X
X        # Literal match on the option text.
X        for _opt in "$@"; do
X            if [ "$_opt" = "$_answer" ]; then
X                _prompt_assign "$_varname" "$_opt"
X                log_write "ask_choice $_varname: '$_opt'"
X                ui_spinner_resume
X                return 0
X            fi
X        done
X
X        printf '  invalid choice, please enter a number between 1 and %s\n' "$#" >&3
X    done
X}
EOF
mkdir -p "$FERRON_INSTALLER_EXTRACT_DIR/lib"
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/lib/tty.sh" << 'EOF'
X# shellcheck shell=sh
X#
X# tty.sh — terminal capability detection.
X#
X# Sets the following globals after `tty_init` has been called:
X#
X#   FERRON_UI_TTY      1 if stdout is connected to a terminal, 0 otherwise.
X#   FERRON_UI_STDIN    1 if stdin is connected to a terminal, 0 otherwise.
X#   FERRON_UI_ANSI     1 if we should emit ANSI escape sequences (cursor moves,
X#                      line erases, spinner redraws), 0 otherwise.
X#   FERRON_UI_COLOR    1 if we should emit SGR color codes, 0 otherwise.
X#                      Honors the NO_COLOR convention (https://no-color.org).
X#   FERRON_UI_UTF8     1 if the locale looks UTF-8 capable, 0 otherwise.
X#                      Used to pick between Unicode and ASCII glyphs.
X#   FERRON_UI_COLS     Detected terminal width, or 80 as a safe default.
X#
X# The detection is intentionally conservative: when in doubt we pick the
X# degraded (plain-text, no-color) variant so logs piped through `tee` or
X# captured by CI systems stay readable.
X
Xtty_init() {
X    if [ -t 1 ]; then
X        FERRON_UI_TTY=1
X    else
X        FERRON_UI_TTY=0
X    fi
X
X    if [ -t 0 ]; then
X        FERRON_UI_STDIN=1
X    else
X        FERRON_UI_STDIN=0
X    fi
X
X    # ANSI sequences only make sense on a real terminal, and only when TERM
X    # isn't explicitly "dumb".
X    if [ "$FERRON_UI_TTY" = 1 ] && [ "${TERM:-dumb}" != "dumb" ]; then
X        FERRON_UI_ANSI=1
X    else
X        FERRON_UI_ANSI=0
X    fi
X
X    # Color follows ANSI capability, with NO_COLOR as an explicit opt-out.
X    if [ "$FERRON_UI_ANSI" = 1 ] && [ -z "${NO_COLOR:-}" ]; then
X        FERRON_UI_COLOR=1
X    else
X        FERRON_UI_COLOR=0
X    fi
X
X    case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
X        *UTF-8*|*utf-8*|*UTF8*|*utf8*) FERRON_UI_UTF8=1 ;;
X        *)                              FERRON_UI_UTF8=0 ;;
X    esac
X
X    # Terminal width. `tput cols` is the most portable, but it isn't always
X    # installed; fall back to $COLUMNS, then 80.
X    if [ "$FERRON_UI_TTY" = 1 ] && command -v tput >/dev/null 2>&1; then
X        FERRON_UI_COLS=$(tput cols 2>/dev/null || echo "${COLUMNS:-80}")
X    else
X        FERRON_UI_COLS=${COLUMNS:-80}
X    fi
X
X    # Save stdout to a temporary file descriptor for use in ask_input.
X    exec 3>&1
X
X    export FERRON_UI_TTY FERRON_UI_STDIN FERRON_UI_ANSI \
X           FERRON_UI_COLOR FERRON_UI_UTF8 FERRON_UI_COLS
X}
EOF
sed s/^X//g > "$FERRON_INSTALLER_EXTRACT_DIR/ferron.conf" << 'EOF'
X# See https://ferron.sh/docs/v3/configuration/syntax for the configuration reference
X
X# Global configuration.
X#
X# Here you can put configuration that applies to all hosts,
X# and even to the web server itself.
X{
X    # Log requests and errors into log files
X    log /var/log/ferron/access.log
X    error_log /var/log/ferron/error.log
X}
X
X# Host configuration.
X#
X# Here you can put configuration that applies to a specific host,
X# by default a catch-all "*:80" host that applies to all hostnames and port 80 (HTTP).
X#
X# Replace "*:80" with your domain name (pointing to your server) to use HTTPS.
X# If you don't specify the paths to the TLS certificate and private key manually,
X# Ferron will obtain a TLS certificate automatically (via Let's Encrypt by default).
X*:80 {
X    # Serve static files
X    root /var/www/ferron
X
X    # Reverse proxy to a backend server
X    #proxy http://localhost:3000/
X
X    # Serve a PHP site with PHP-FPM (you would need to specify the webroot also used for serving static files)
X    # Replace "unix:///run/php/php-fpm.sock" with your Unix socket URL
X    #fcgi_php unix:///run/php/php-fpm.sock
X
X    # If using Unix socket with PHP-FPM,
X    # set the listener owner and group in the PHP pool configuration to the web server user (`ferron`, if you used installer for GNU/Linux)
X    # For example:
X    #   listen.owner = ferron
X    #   listen.group = ferron
X}
EOF
. $FERRON_INSTALLER_EXTRACT_DIR/main.sh
