Ctrl K
ComunidadGuías…

install-linux.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash

#═══════════════════════════════════════════════
#   Oneirodex Linux Auto-Installer v1.0
#   Automated installation script for Linux systems
#   Compatible with bash, zsh, and other POSIX shells
#═══════════════════════════════════════════════


# Use bash for advanced features but detect shell for compatibility
if [ -n "${ZSH_VERSION:-}" ]; then
    # Running in zsh
    setopt SH_WORD_SPLIT  # Make zsh behave more like bash for word splitting
    setopt BASH_REMATCH   # Enable bash-style regex matching
elif [ -n "${BASH_VERSION:-}" ]; then
    # Running in bash
    set -euo pipefail  # Exit on error, undefined vars, pipe failures
else
    # Fallback for other POSIX shells
    set -eu
fi

# Colors and formatting
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m' # No Color

# Global variables
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="$SCRIPT_DIR/install.log"
DISTRO=""
PACKAGE_MANAGER=""
DB_PASSWORD=""
SECRET_KEY=""
GAMES_DIR=""
LIBRARY_ROOTS=""
FORCE_INSTALL=false
DEV_MODE=false
SKIP_DB=false
CUSTOM_PORT="5006"
VERBOSE_MODE=false
INSTALL_STAGE=0

# Cleanup function
cleanup() {
    local exit_code=$?
    if [ $exit_code -ne 0 ]; then
        echo -e "\n${RED}FIELD ALERT // installation did not complete.${NC}"
        echo -e "${YELLOW}Field log: $LOG_FILE${NC}"
    fi
}
trap cleanup EXIT

# Logging function
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S'): $1" >> "$LOG_FILE"
}

# Print functions
print_header() {
    clear
    echo -e "${GREEN}╭──────────────────────────────────────────────────────────────────────╮${NC}"
    echo -e "${GREEN}│  Oneirodex  //  EXPEDITION DECK                          LINUX      │${NC}"
    echo -e "${YELLOW}│  QUEST LOG · INSTALLATION                                        ◉  │${NC}"
    echo -e "${GREEN}╰──────────────────────────────────────────────────────────────────────╯${NC}"
    echo -e "${GREEN}"
    cat <<'ONEI'
                         .-""""-.
                        /  .--.  \
                       |  (o  o)  |
                       |    ∇     |     ONEI
                        \  ____  /      map scout · keeper of the lantern
                       .-|      |-.
                      /  |  []  |  \    field kit: d20 · radio · spare key
                     /___|______|___\
                          /    \
                         /______\
ONEI
    echo -e "${NC}"
    echo -e "  A little old-world magic. A well-stocked field terminal."
    echo
}

print_step() {
    INSTALL_STAGE=$((INSTALL_STAGE + 1))
    printf "\n${YELLOW}  QUEST %02d  //  FIELD LOG${NC}\n" "$INSTALL_STAGE"
    echo -e "${CYAN}  ONEI > $1${NC}"
    log "STEP: $1"
}

print_success() {
    echo -e "${GREEN}  [✓]${NC} $1"
    log "SUCCESS: $1"
}

print_error() {
    echo -e "${RED}  [!]${NC} $1"
    log "ERROR: $1"
}

print_warning() {
    echo -e "${YELLOW}  [!]${NC} $1"
    log "WARNING: $1"
}

print_info() {
    echo -e "${CYAN}  [·]${NC} $1"
    log "INFO: $1"
}

# Verbose-only info messages (suppressed in quiet mode)
print_verbose() {
    if [ "$VERBOSE_MODE" = true ]; then
        echo -e "${CYAN}  [·]${NC} $1"
    fi
    log "VERBOSE: $1"
}

# Execute command with appropriate output handling
execute_quiet() {
    local description="$1"
    shift

    if [ "$VERBOSE_MODE" = true ]; then
        # Show full output in verbose mode
        "$@"
    else
        # Suppress stdout but keep stderr in quiet mode
        "$@" >/dev/null
    fi
}

# Execute command with full output suppression in quiet mode
execute_silent() {
    local description="$1"
    shift

    if [ "$VERBOSE_MODE" = true ]; then
        # Show full output in verbose mode
        "$@"
    else
        # Suppress both stdout and stderr in quiet mode
        "$@" >/dev/null 2>&1
    fi
}

# Check if running as root (bad) or with sudo access (good)
check_permissions() {
    # Check if running as root - use $USER and id as EUID may not be available in all shells
    if [ "$(id -u)" -eq 0 ]; then
        print_error "This script should not be run as root!"
        print_info "Please run as a regular user with sudo access:"
        print_info "  ./install-linux.sh"
        exit 1
    fi

    # Check if sudo is available on the system
    if ! command -v sudo >/dev/null 2>&1; then
        print_error "sudo command not found"
        print_info "Please install sudo or run as root (not recommended)"
        exit 1
    fi

    # First try passwordless sudo check
    if sudo -n true 2>/dev/null; then
        print_success "Running with appropriate permissions"
        return 0
    fi

    # If passwordless sudo failed, authenticate the user
    print_info "This installer requires sudo access to install system packages and configure PostgreSQL"
    print_info "You will be prompted for your sudo password..."

    if ! sudo -v; then
        print_error "sudo authentication failed"
        print_info "Please ensure your user account has sudo privileges"
        print_info "Contact your system administrator if needed"
        exit 1
    fi

    # Verify sudo is now working
    if ! sudo -n true 2>/dev/null; then
        print_error "sudo access verification failed after authentication"
        exit 1
    fi

    print_success "Running with appropriate permissions"
}

# Generate secure random password
generate_secure_password() {
    if command -v openssl >/dev/null 2>&1; then
        openssl rand -base64 32 | tr -d "=+/" | cut -c1-25
    elif command -v python3 >/dev/null 2>&1; then
        python3 -c "import secrets; print(secrets.token_urlsafe(25))"
    else
        # Fallback to /dev/urandom
        cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 25 | head -n 1
    fi
}

# Generate secure secret key
generate_secret_key() {
    if command -v python3 >/dev/null 2>&1; then
        python3 -c "import secrets; print(secrets.token_urlsafe(32))"
    else
        # Fallback method
        openssl rand -base64 32 | tr -d "=+/"
    fi
}

# Parse command line arguments (POSIX compatible)
parse_arguments() {
    while [ $# -gt 0 ]; do
        case $1 in
            --force)
                FORCE_INSTALL=true
                shift
                ;;
            --dev)
                DEV_MODE=true
                shift
                ;;
            --no-db)
                SKIP_DB=true
                shift
                ;;
            --verbose|-v)
                VERBOSE_MODE=true
                shift
                ;;
            --quiet|-q)
                VERBOSE_MODE=false
                shift
                ;;
            --games-dir)
                if [ $# -lt 2 ]; then
                    print_error "--games-dir requires an argument"
                    exit 1
                fi
                GAMES_DIR="$2"
                shift 2
                ;;
            --library-roots)
                if [ $# -lt 2 ]; then
                    print_error "--library-roots requires an argument"
                    exit 1
                fi
                LIBRARY_ROOTS="$2"
                shift 2
                ;;
            --port)
                if [ $# -lt 2 ]; then
                    print_error "--port requires an argument"
                    exit 1
                fi
                CUSTOM_PORT="$2"
                shift 2
                ;;
            --help|-h)
                show_help
                exit 0
                ;;
            *)
                print_error "Unknown option: $1"
                show_help
                exit 1
                ;;
        esac
    done
}

# Show help information
show_help() {
    echo "Oneirodex Linux Auto-Installer"
    echo
    echo "USAGE:"
    echo "  ./install-linux.sh [OPTIONS]"
    echo
    echo "OPTIONS:"
    echo "  --force           Override existing installation"
    echo "  --dev            Install development dependencies"
    echo "  --no-db          Skip database setup (use existing)"
    echo "  --verbose, -v    Show detailed installation output"
    echo "  --quiet, -q      Suppress detailed output (default)"
    echo "  --games-dir PATH Specify games directory"
    echo "  --library-roots  Extra scan locations, \"|\"-separated, optional Label="
    echo "  --port PORT      Custom port (default: 5006)"
    echo "  --help, -h       Show this help message"
    echo
    echo "EXAMPLES:"
    echo "  ./install-linux.sh"
    echo "  ./install-linux.sh --verbose"
    echo "  ./install-linux.sh --games-dir /home/user/games"
    echo "  ./install-linux.sh --games-dir /srv/games --library-roots 'NAS ROMs=/mnt/nas/roms|Archive=/mnt/archive'"
    echo "  ./install-linux.sh --force --dev --verbose"
    echo
    echo "SCAN LOCATIONS:"
    echo "  Oneirodex scans any path this machine can open, so a NAS share counts"
    echo "  once the host has mounted it. Mount it first (fstab / systemd automount),"
    echo "  then pass the mount point to --library-roots. Details:"
    echo "  docs/runbooks/remote-scan-locations.md"
}

# Backup existing configuration files
backup_existing_config() {
    if [ -f "$SCRIPT_DIR/.env" ]; then
        print_step "Backing up existing configuration..."
        cp "$SCRIPT_DIR/.env" "$SCRIPT_DIR/.env.backup.$(date +%Y%m%d-%H%M%S)"
        print_success "Configuration backed up"
    fi
}

# Detect Linux distribution
detect_distribution() {
    print_step "Detecting Linux distribution..."

    if [ -f /etc/os-release ]; then
        source /etc/os-release
        DISTRO="$ID"

        case "$DISTRO" in
            ubuntu|debian)
                PACKAGE_MANAGER="apt"
                print_success "Detected: $PRETTY_NAME (using apt)"
                ;;
            fedora|rhel|centos|rocky|almalinux)
                if command -v dnf >/dev/null 2>&1; then
                    PACKAGE_MANAGER="dnf"
                else
                    PACKAGE_MANAGER="yum"
                fi
                print_success "Detected: $PRETTY_NAME (using $PACKAGE_MANAGER)"
                ;;
            arch|manjaro)
                PACKAGE_MANAGER="pacman"
                print_success "Detected: $PRETTY_NAME (using pacman)"
                ;;
            opensuse*|sles)
                PACKAGE_MANAGER="zypper"
                print_success "Detected: $PRETTY_NAME (using zypper)"
                ;;
            *)
                print_warning "Unsupported distribution: $PRETTY_NAME"
                print_info "This script supports Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, and openSUSE"
                print_info "You may need to install prerequisites manually"
                return 1
                ;;
        esac
    else
        print_error "Cannot detect Linux distribution"
        print_info "Please install prerequisites manually and run the application setup"
        return 1
    fi
}

# Install packages based on distribution
install_package() {
    local packages="$1"

    case "$PACKAGE_MANAGER" in
        apt)
            execute_quiet "apt update" sudo apt update -qq
            execute_quiet "apt install $packages" sudo apt install -y $packages
            ;;
        dnf)
            execute_quiet "dnf install $packages" sudo dnf install -y $packages
            ;;
        yum)
            execute_quiet "yum install $packages" sudo yum install -y $packages
            ;;
        pacman)
            execute_quiet "pacman install $packages" sudo pacman -Sy --noconfirm $packages
            ;;
        zypper)
            execute_quiet "zypper install $packages" sudo zypper install -y $packages
            ;;
        *)
            print_error "Unsupported package manager: $PACKAGE_MANAGER"
            return 1
            ;;
    esac
}

# Check and install prerequisites
install_prerequisites() {
    print_step "Installing prerequisites..."

    # Update package lists
    case "$PACKAGE_MANAGER" in
        apt)
            print_verbose "Updating package lists..."
            execute_quiet "apt update" sudo apt update -qq
            ;;
    esac

    # Define packages per distribution
    local python_packages=""
    local git_package="git"
    local postgresql_packages=""
    local build_packages=""

    case "$PACKAGE_MANAGER" in
        apt)
            python_packages="python3 python3-pip python3-venv python3-dev"
            postgresql_packages="postgresql postgresql-contrib"
            build_packages="build-essential libpq-dev"
            ;;
        dnf|yum)
            python_packages="python3 python3-pip python3-devel"
            postgresql_packages="postgresql postgresql-server postgresql-contrib"
            build_packages="gcc gcc-c++ make postgresql-devel"
            ;;
        pacman)
            python_packages="python python-pip python-virtualenv"
            postgresql_packages="postgresql"
            build_packages="base-devel postgresql-libs"
            ;;
        zypper)
            python_packages="python3 python3-pip python3-devel"
            postgresql_packages="postgresql postgresql-server postgresql-contrib"
            build_packages="gcc gcc-c++ make postgresql-devel"
            ;;
    esac

    # Install Python and pip
    print_verbose "Installing Python and pip..."
    install_package "$python_packages"

    # Install git
    print_verbose "Installing Git..."
    install_package "$git_package"

    # Install build dependencies
    print_verbose "Installing build dependencies..."
    install_package "$build_packages"

    # Install PostgreSQL
    if [ "$SKIP_DB" != true ]; then
        print_verbose "Installing PostgreSQL..."
        install_package "$postgresql_packages"
    fi

    # Verify installations
    print_step "Verifying installations..."

    if ! command -v python3 >/dev/null 2>&1; then
        print_error "Python3 installation failed"
        return 1
    fi
    print_success "Python3 installed: $(python3 --version)"

    if ! python3 -m pip --version >/dev/null 2>&1; then
        print_error "pip installation failed"
        return 1
    fi
    print_success "pip installed: $(python3 -m pip --version | cut -d' ' -f2)"

    if ! command -v git >/dev/null 2>&1; then
        print_error "Git installation failed"
        return 1
    fi
    print_success "Git installed: $(git --version | cut -d' ' -f3)"

    if [ "$SKIP_DB" != true ]; then
        if ! command -v psql >/dev/null 2>&1; then
            print_error "PostgreSQL installation failed"
            return 1
        fi
        print_success "PostgreSQL installed"
    fi
}

# Get PostgreSQL configuration path based on distribution
get_pg_config_path() {
    case "$PACKAGE_MANAGER" in
        apt)
            # Find the PostgreSQL version directory
            if [ -d "/etc/postgresql" ]; then
                PG_VERSION=$(ls /etc/postgresql/ 2>/dev/null | head -1)
                if [ -n "$PG_VERSION" ]; then
                    echo "/etc/postgresql/$PG_VERSION/main"
                    return 0
                fi
            fi
            echo "/etc/postgresql"
            ;;
        dnf|yum)
            echo "/var/lib/pgsql/data"
            ;;
        pacman)
            echo "/var/lib/postgres/data"
            ;;
        zypper)
            echo "/var/lib/pgsql/data"
            ;;
        *)
            # Try common locations
            for path in "/etc/postgresql" "/var/lib/pgsql/data" "/var/lib/postgres/data"; do
                if [ -d "$path" ]; then
                    echo "$path"
                    return 0
                fi
            done
            echo "/etc/postgresql"
            ;;
    esac
}

# Configure PostgreSQL authentication for password-based access
configure_postgresql_auth() {
    print_step "Configuring PostgreSQL authentication..."

    # Get PostgreSQL config path
    PG_CONFIG_PATH=$(get_pg_config_path)
    PG_HBA_CONF="$PG_CONFIG_PATH/pg_hba.conf"

    # Check if pg_hba.conf exists
    if [ ! -f "$PG_HBA_CONF" ]; then
        print_warning "PostgreSQL pg_hba.conf not found at $PG_HBA_CONF"
        print_info "Skipping authentication configuration"
        return 0
    fi

    # Backup original configuration
    sudo cp "$PG_HBA_CONF" "$PG_HBA_CONF.backup.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || {
        print_warning "Could not backup pg_hba.conf (permissions issue)"
        print_info "Continuing without modifying authentication..."
        return 0
    }

    # Add password authentication for the oneirodex database (app role only).
    # No rule is added for the `postgres` superuser: a password rule ahead of the
    # default `local all postgres peer` line would take over its access to this
    # database, and Oneirodex never needs a superuser login.
    print_verbose "Adding password authentication for oneirodex database..."

    if ! sudo grep -qF '# Added by Oneirodex installer' "$PG_HBA_CONF"; then
        # Add our rules at the top (before default rules), once only.
        {
            echo "# Added by Oneirodex installer - $(date)"
            echo "local   oneirodex   oneirodexuser   md5"
            echo "host    oneirodex   oneirodexuser   127.0.0.1/32   md5"
            echo "host    oneirodex   oneirodexuser   ::1/128        md5"
            echo ""
        } | sudo tee "$PG_HBA_CONF.new" >/dev/null
        sudo cat "$PG_HBA_CONF" | sudo tee -a "$PG_HBA_CONF.new" >/dev/null
        sudo mv "$PG_HBA_CONF.new" "$PG_HBA_CONF"
    else
        print_info "Oneirodex PostgreSQL authentication rules already exist"
    fi

    # Reload PostgreSQL configuration
    if sudo systemctl reload postgresql 2>/dev/null; then
        print_success "PostgreSQL authentication configured and reloaded"
    elif sudo service postgresql reload 2>/dev/null; then
        print_success "PostgreSQL authentication configured and reloaded"
    else
        print_warning "Could not reload PostgreSQL - changes will take effect on next restart"
    fi
}

# Test database connection with multiple methods
test_database_connection() {
    local max_attempts=3
    local attempt=1

    print_info "Testing database connection..."

    while [ $attempt -le $max_attempts ]; do
        print_info "Connection attempt $attempt/$max_attempts"

        # Method 1: TCP/IP with localhost
        if PGPASSWORD="$DB_PASSWORD" psql -h localhost -U oneirodexuser -d oneirodex -c "SELECT 1;" >/dev/null 2>&1; then
            print_success "Database connection test successful (localhost TCP/IP)"
            return 0
        fi

        # Method 2: TCP/IP with 127.0.0.1
        if PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -U oneirodexuser -d oneirodex -c "SELECT 1;" >/dev/null 2>&1; then
            print_success "Database connection test successful (127.0.0.1 TCP/IP)"
            return 0
        fi

        # Method 3: Create .pgpass file for authentication
        if [ "$attempt" -eq $max_attempts ]; then
            print_info "Trying .pgpass authentication method..."
            echo "localhost:5432:oneirodex:oneirodexuser:$DB_PASSWORD" > ~/.pgpass
            chmod 600 ~/.pgpass

            if psql -h localhost -U oneirodexuser -d oneirodex -c "SELECT 1;" >/dev/null 2>&1; then
                rm ~/.pgpass 2>/dev/null
                print_success "Database connection test successful (.pgpass method)"
                return 0
            fi
            rm ~/.pgpass 2>/dev/null
        fi

        attempt=$((attempt + 1))
        if [ $attempt -le $max_attempts ]; then
            print_info "Waiting 2 seconds before retry..."
            sleep 2
        fi
    done

    # If all methods failed, provide detailed error information
    print_error "All database connection methods failed"
    print_info "This may be due to PostgreSQL authentication configuration"
    print_info "The database and user were created successfully, but connection testing failed"
    print_info "You may need to manually configure PostgreSQL authentication"

    return 1
}

# Start and configure PostgreSQL
setup_postgresql() {
    if [ "$SKIP_DB" = true ]; then
        print_info "Skipping PostgreSQL setup (--no-db flag)"
        return 0
    fi

    print_step "Setting up PostgreSQL database..."

    # Start PostgreSQL service
    case "$PACKAGE_MANAGER" in
        apt)
            sudo systemctl start postgresql
            sudo systemctl enable postgresql
            ;;
        dnf|yum)
            # Initialize database if needed
            if [ ! -d "/var/lib/pgsql/data/base" ]; then
                print_verbose "Initializing PostgreSQL database..."
                execute_silent "postgresql setup initdb" sudo postgresql-setup --initdb || true
            fi
            sudo systemctl start postgresql
            sudo systemctl enable postgresql
            ;;
        pacman)
            # Initialize database if needed
            if [ ! -d "/var/lib/postgres/data/base" ]; then
                print_verbose "Initializing PostgreSQL database..."
                execute_silent "postgres initdb" sudo -u postgres initdb -D /var/lib/postgres/data
            fi
            sudo systemctl start postgresql
            sudo systemctl enable postgresql
            ;;
        zypper)
            sudo systemctl start postgresql
            sudo systemctl enable postgresql
            ;;
    esac

    # Wait for PostgreSQL to start
    print_verbose "Waiting for PostgreSQL to start..."
    for i in {1..30}; do
        if sudo -u postgres psql -c "SELECT 1;" >/dev/null 2>&1; then
            break
        fi
        sleep 1
    done

    if ! sudo -u postgres psql -c "SELECT 1;" >/dev/null 2>&1; then
        print_error "PostgreSQL failed to start properly"
        return 1
    fi

    print_success "PostgreSQL is running"

    # Generate secure password for database user
    DB_PASSWORD=$(generate_secure_password)

    print_verbose "Creating database and user..."

    # Create database user and database
    # Only the dedicated app role is created here. This script never sets or
    # changes the password of the `postgres` superuser: it keeps whatever
    # authentication the host already has (peer over the local socket on most
    # distros), and Oneirodex never connects as a superuser.
    sudo -u postgres psql << EOF
-- Create user
DO \$\$
BEGIN
    IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'oneirodexuser') THEN
        CREATE USER oneirodexuser WITH ENCRYPTED PASSWORD '$DB_PASSWORD';
    ELSE
        ALTER USER oneirodexuser WITH ENCRYPTED PASSWORD '$DB_PASSWORD';
    END IF;
END
\$\$;

-- Create database
SELECT 'CREATE DATABASE oneirodex OWNER oneirodexuser'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'oneirodex')\gexec

-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE oneirodex TO oneirodexuser;
GRANT CREATE ON SCHEMA public TO oneirodexuser;

-- Exit without testing connection here (avoids peer auth issues)
\q
EOF

    if [ $? -eq 0 ]; then
        print_success "Database 'oneirodex' created with user 'oneirodexuser'"
    else
        print_error "Failed to create database or user"
        return 1
    fi

    # Configure PostgreSQL authentication for password access
    configure_postgresql_auth

    # Test the database connection with multiple methods
    if ! test_database_connection; then
        print_warning "Database connection test failed"
        print_info "The database and user were created, but connection testing failed"
        print_info "This may not prevent Oneirodex from working if the application can connect"
        print_info "You can continue with the installation"

        # Ask user if they want to continue
        printf "Continue with installation? [Y/n]: "
        read -r continue_install
        if [ "${continue_install:-Y}" != "Y" ] && [ "${continue_install:-Y}" != "y" ]; then
            print_error "Installation aborted by user"
            return 1
        fi

        print_info "Continuing with installation..."
    fi
}

# Set up Python virtual environment and install dependencies
setup_python_environment() {
    print_step "Setting up Python virtual environment..."

    # Create virtual environment if it doesn't exist
    if [ ! -d "$SCRIPT_DIR/venv" ]; then
        python3 -m venv "$SCRIPT_DIR/venv"
        print_success "Virtual environment created"
    else
        print_info "Using existing virtual environment"
    fi

    # Activate virtual environment and install dependencies
    source "$SCRIPT_DIR/venv/bin/activate"

    print_verbose "Installing Python dependencies..."
    if execute_silent "pip install requirements" python3 -m pip install -r "$SCRIPT_DIR/requirements.txt"; then
        print_success "Python dependencies installed"
    else
        print_error "Failed to install Python dependencies"
        return 1
    fi

    if [ "$DEV_MODE" = true ]; then
        print_verbose "Installing development dependencies..."
        if [ -f "$SCRIPT_DIR/requirements-dev.txt" ]; then
            execute_silent "pip install dev requirements" python3 -m pip install -r "$SCRIPT_DIR/requirements-dev.txt"
        fi
    fi
}

# Configure application settings
configure_application() {
    print_step "Configuring Oneirodex application..."

    # Keep the tracked config.py as the source of truth and preserve the
    # signing key across reinstalls.
    SECRET_KEY=""
    if [ -f "$SCRIPT_DIR/.env" ]; then
        SECRET_KEY=$(sed -n 's/^SECRET_KEY=//p' "$SCRIPT_DIR/.env" | head -n 1)
    fi
    if [ -z "$SECRET_KEY" ]; then
        SECRET_KEY=$(generate_secret_key)
    fi

    # Prompt for games directory if not specified
    if [ -z "$GAMES_DIR" ]; then
        echo
        print_info "Please specify the directory containing your game files:"
        read -p "Games directory path [/home/$USER/games]: " input_dir
        GAMES_DIR="${input_dir:-/home/$USER/games}"
    fi

    # Validate games directory
    if [ ! -d "$GAMES_DIR" ]; then
        print_warning "Games directory does not exist: $GAMES_DIR"
        read -p "Create this directory? [Y/n]: " create_dir
        case "${create_dir:-Y}" in
            [Yy]|[Yy][Ee][Ss]) create_it=true ;;
            *) create_it=false ;;
        esac
        if [ "$create_it" = true ]; then
            mkdir -p "$GAMES_DIR"
            print_success "Games directory created: $GAMES_DIR"
        else
            print_warning "You'll need to update the games directory later in .env"
        fi
    else
        print_success "Games directory exists: $GAMES_DIR"
    fi

    # Extra scan locations. Anything already mounted on this machine qualifies —
    # a NAS share under /mnt, a second disk, an external drive. Mounting is the
    # operator's job; this only records where Oneirodex should look.
    if [ -z "$LIBRARY_ROOTS" ]; then
        echo
        print_info "Extra scan locations beyond $GAMES_DIR (optional)."
        print_info "Separate with '|', label with 'Name=': 'NAS ROMs=/mnt/nas/roms|/mnt/archive'"
        read -p "Extra scan locations [none]: " input_roots
        LIBRARY_ROOTS="${input_roots:-}"
    fi

    if [ -n "$LIBRARY_ROOTS" ]; then
        # Warn, do not fail: an automount can be legitimately absent at install
        # time and appear on first access.
        OLD_IFS="$IFS"
        IFS='|'
        for entry in $LIBRARY_ROOTS; do
            root_path="${entry#*=}"
            root_path="$(echo "$root_path" | sed 's/^ *//; s/ *$//')"
            if [ -n "$root_path" ] && [ ! -d "$root_path" ]; then
                print_warning "Scan location not mounted yet: $root_path"
            elif [ -n "$root_path" ]; then
                print_success "Scan location found: $root_path"
            fi
        done
        IFS="$OLD_IFS"
    fi

    # Create .env file
    print_verbose "Creating environment configuration..."

    cat > "$SCRIPT_DIR/.env" << EOF
# Oneirodex Configuration - Generated by auto-installer $(date)

# Database connection
DATABASE_URL=postgresql://oneirodexuser:$DB_PASSWORD@localhost:5432/oneirodex

# Test database (only needed if running unit tests)
TEST_DATABASE_URL=postgresql://oneirodexuser:$DB_PASSWORD@localhost:5432/oneirodextest

# Game files directory
DATA_FOLDER_GAMES=$GAMES_DIR

# Extra scan locations: NAS shares, second disks, anything else this machine
# has mounted. "|"-separated, optional "Label=" prefix. The share must already
# be mounted here — see docs/runbooks/remote-scan-locations.md
ONEIRODEX_LIBRARY_ROOTS=$LIBRARY_ROOTS

# Base folders for path resolution
BASE_FOLDER_WINDOWS=C:\\
BASE_FOLDER_POSIX=/

# Flask secret key (keep this secure!)
SECRET_KEY=$SECRET_KEY

# Upload directory for cover images and zips
UPLOAD_FOLDER=$SCRIPT_DIR/oneirodex/static/library

# Development mode
DEV_MODE=${DEV_MODE:-false}
EOF

    # Set secure permissions on .env file
    chmod 600 "$SCRIPT_DIR/.env"
    print_success "Environment configuration created"

    # Make all shell scripts executable (required after git clone)
    print_verbose "Making all shell scripts executable..."
    chmod +x "$SCRIPT_DIR"/*.sh
    print_success "Shell script permissions set"
}

# Validate the installation
validate_installation() {
    print_step "Validating installation..."

    # Check if virtual environment exists and works
    if [ -d "$SCRIPT_DIR/venv" ]; then
        source "$SCRIPT_DIR/venv/bin/activate"

        # Test Flask app creation
        if python3 -c "from oneirodex import create_app; app = create_app(); print('Flask app creation: OK')" >/dev/null 2>&1; then
            print_success "Flask application setup validated"
        else
            print_error "Flask application validation failed"
            return 1
        fi
    else
        print_error "Virtual environment not found"
        return 1
    fi

    # Check database connection
    if [ "$SKIP_DB" != true ]; then
        if test_database_connection >/dev/null 2>&1; then
            print_success "Database connection validated"
        else
            print_warning "Database connection validation failed"
            print_info "This may not prevent Oneirodex from functioning"
        fi
    fi

    # Check required files
    for file in ".env" "config.py" "startweb.sh" "requirements.txt"; do
        if [ -f "$SCRIPT_DIR/$file" ]; then
            print_success "Required file exists: $file"
        else
            print_error "Missing required file: $file"
            return 1
        fi
    done
}

# Show installation summary
show_summary() {
    echo
    echo -e "${GREEN}╭──────────────────────────────────────────────────────────────────────╮${NC}"
    echo -e "${GREEN}│  QUEST COMPLETE  //  THE DECK IS READY                              │${NC}"
    echo -e "${GREEN}╰──────────────────────────────────────────────────────────────────────╯${NC}"
    echo -e "${CYAN}  ONEI > Your worlds are waiting at the gate.${NC}"
    echo
    echo -e "${CYAN}  Launch gate:${NC} http://localhost:$CUSTOM_PORT"
    echo -e "${CYAN}  Game cache:${NC} $GAMES_DIR"
    if [ -n "$LIBRARY_ROOTS" ]; then
        echo -e "${CYAN}  Trail markers:${NC} $LIBRARY_ROOTS"
    fi
    if [ "$SKIP_DB" != true ]; then
        echo -e "${CYAN}  Camp ledger:${NC} oneirodex (credentials stored in .env)"
    fi
    echo -e "${CYAN}  Start command:${NC} ./startweb.sh"
    echo -e "${CYAN}  Stop:${NC} Press Ctrl+C"
    echo -e "${CYAN}  Reset database:${NC} ./startweb.sh --force-setup"
    echo -e "${CYAN}  Field log:${NC} $LOG_FILE"
    echo

    # Ask if user wants to start the application
    read -p "Start Oneirodex now? [Y/n]: " start_now
    case "${start_now:-Y}" in
        [Yy]|[Yy][Ee][Ss])
            start_it=true
            ;;
        *)
            start_it=false
            ;;
    esac
    if [ "$start_it" = true ]; then
        echo
        print_info "Opening the launch gate..."
        print_info "Open your browser to http://localhost:$CUSTOM_PORT when ready"
        print_info "Press Ctrl+C to stop the application"
        echo

        # Export PORT for startweb.sh
        export PORT="$CUSTOM_PORT"
        exec ./startweb.sh
    else
        echo
        print_info "To start Oneirodex later, run: ./startweb.sh"
        print_info "Then open your browser to: http://localhost:$CUSTOM_PORT"
    fi
}

# Main installation function
main() {
    # Initialize log file
    echo "Oneirodex Linux Auto-Installer - $(date)" > "$LOG_FILE"

    print_header

    # Parse command line arguments
    parse_arguments "$@"

    # Show what the installer will do
    print_info "This installer will:"
    print_info "  • Install system packages (Python, PostgreSQL, build tools)"
    print_info "  • Configure PostgreSQL database"
    print_info "  • Set up Python virtual environment"
    print_info "  • Configure Oneirodex application"
    echo
    print_info "Administrative privileges are required for system package installation."

    # Show verbosity mode
    if [ "$VERBOSE_MODE" = true ]; then
        print_info "Running in verbose mode - showing all installation details"
    else
        print_info "Running in quiet mode - use --verbose to see detailed output"
    fi

    echo
    print_info "Press any key to continue or Ctrl+C to quit..."
    read -n 1 -s -r

    # Check permissions
    check_permissions

    # Detect distribution
    if ! detect_distribution; then
        exit 1
    fi

    # Check if already in Oneirodex directory
    if [ ! -f "$SCRIPT_DIR/startweb.sh" ] || [ ! -f "$SCRIPT_DIR/requirements.txt" ]; then
        print_error "This script must be run from the Oneirodex directory"
        print_info "Please clone the repository first:"
        print_info "  git clone https://github.com/cephyrixzyth/oneirodex.git"
        print_info "  cd oneirodex"
        print_info "  ./install-linux.sh"
        exit 1
    fi

    # Backup existing configuration
    backup_existing_config

    # Install prerequisites
    install_prerequisites

    # Setup PostgreSQL
    if ! setup_postgresql; then
        exit 1
    fi

    # Setup Python environment
    if ! setup_python_environment; then
        exit 1
    fi

    # Configure application
    if ! configure_application; then
        exit 1
    fi

    # Validate installation
    if ! validate_installation; then
        exit 1
    fi

    # Show summary and optionally start
    show_summary
}

# Run main function if script is executed directly
if [ "${0##*/}" = "install-linux.sh" ] || [ "${0}" = "./install-linux.sh" ]; then
    main "$@"
fi