Ctrl K
ComunidadGuías…

install-macos.sh

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

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

#═══════════════════════════════════════════════
#   Oneirodex macOS Installer v1.0
#   Homebrew-based install for Apple Silicon and Intel Macs
#═══════════════════════════════════════════════
#
# Deliberately does NOT install Homebrew for you. The official installer is a
# script fetched over the network and run with your privileges; deciding to do
# that is the operator's call, not this script's. If brew is missing you get
# the one-liner and an exit.

set -euo pipefail

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'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="$SCRIPT_DIR/install.log"
PG_FORMULA="postgresql@17"
PYTHON_FORMULA="python@3.12"
DB_NAME="oneirodex"
TEST_DB_NAME="oneirodextest"
DB_USER="$(id -un)"
GAMES_DIR=""
LIBRARY_ROOTS=""
CUSTOM_PORT="5006"
FORCE_INSTALL=false
DEV_MODE=false
SKIP_DB=false
VERBOSE_MODE=false
INSTALL_STAGE=0

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

log() { echo "$(date '+%Y-%m-%d %H:%M:%S'): $1" >> "$LOG_FILE"; }
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"; }
print_verbose() { if [ "$VERBOSE_MODE" = true ]; then echo -e "${CYAN}  [·]${NC} $1"; fi; log "VERBOSE: $1"; }

run_quiet() {
    if [ "$VERBOSE_MODE" = true ]; then
        "$@"
    else
        "$@" >/dev/null
    fi
}

print_header() {
    clear
    echo -e "${GREEN}╭──────────────────────────────────────────────────────────────────────╮${NC}"
    echo -e "${GREEN}│  Oneirodex  //  EXPEDITION DECK                          macOS      │${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
}

show_help() {
    echo "Oneirodex macOS Installer"
    echo
    echo "USAGE:"
    echo "  ./install-macos.sh [OPTIONS]"
    echo
    echo "OPTIONS:"
    echo "  --force           Overwrite an existing .env / config.py"
    echo "  --dev             Install development dependencies too"
    echo "  --no-db           Skip PostgreSQL setup (use an existing database)"
    echo "  --verbose, -v     Show detailed output"
    echo "  --quiet, -q       Suppress detailed output (default)"
    echo "  --games-dir PATH  Games directory"
    echo "  --library-roots S Extra scan locations, pipe-separated, optional Label="
    echo "  --port PORT       Custom port (default: 5006)"
    echo "  --help, -h        Show this help message"
    echo
    echo "EXAMPLES:"
    echo "  ./install-macos.sh"
    echo "  ./install-macos.sh --games-dir ~/Games"
    echo "  ./install-macos.sh --library-roots 'NAS ROMs=/Volumes/roms|Archive=/Volumes/archive'"
    echo
    echo "SCAN LOCATIONS:"
    echo "  An SMB/AFP share mounted in Finder lands under /Volumes and is scannable"
    echo "  like any local folder. Finder mounts are per-login-session, so a share"
    echo "  you want available to a background Oneirodex needs autofs instead —"
    echo "  docs/runbooks/remote-scan-locations.md walks through both."
}

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
}

check_platform() {
    print_step "Checking platform..."
    if [ "$(uname -s)" != "Darwin" ]; then
        print_error "This installer is for macOS. On Linux use ./install-linux.sh"
        exit 1
    fi
    print_success "macOS $(sw_vers -productVersion) on $(uname -m)"
}

check_homebrew() {
    print_step "Checking Homebrew..."
    if ! command -v brew >/dev/null 2>&1; then
        # Apple Silicon puts brew outside the default PATH of a fresh shell.
        for candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do
            if [ -x "$candidate" ]; then
                eval "$("$candidate" shellenv)"
                break
            fi
        done
    fi

    if ! command -v brew >/dev/null 2>&1; then
        print_error "Homebrew is required and was not found."
        echo
        print_info "Install it yourself (review the script first — it runs as you):"
        print_info '  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
        print_info "Then re-run ./install-macos.sh"
        exit 1
    fi
    print_success "Homebrew found: $(brew --prefix)"
}

install_prerequisites() {
    print_step "Installing prerequisites (Python, PostgreSQL)..."

    if ! brew list --formula "$PYTHON_FORMULA" >/dev/null 2>&1; then
        print_verbose "Installing $PYTHON_FORMULA..."
        run_quiet brew install "$PYTHON_FORMULA"
    fi
    print_success "Python available"

    if [ "$SKIP_DB" = true ]; then
        print_info "Skipping PostgreSQL install (--no-db)"
        return 0
    fi

    if ! brew list --formula "$PG_FORMULA" >/dev/null 2>&1; then
        print_verbose "Installing $PG_FORMULA..."
        run_quiet brew install "$PG_FORMULA"
    fi
    # Homebrew keeps versioned formulae off PATH; psql/createdb live in its bin.
    PG_BIN="$(brew --prefix "$PG_FORMULA")/bin"
    export PATH="$PG_BIN:$PATH"
    print_success "PostgreSQL 17 available at $PG_BIN"
}

setup_postgresql() {
    if [ "$SKIP_DB" = true ]; then
        print_info "Skipping database setup (--no-db)"
        return 0
    fi

    print_step "Starting PostgreSQL..."
    run_quiet brew services start "$PG_FORMULA" || true

    # brew services returns before the socket is accepting connections.
    local attempts=0
    until pg_isready -q >/dev/null 2>&1; do
        attempts=$((attempts + 1))
        if [ "$attempts" -ge 30 ]; then
            print_error "PostgreSQL did not become ready. Check: brew services list"
            return 1
        fi
        sleep 1
    done
    print_success "PostgreSQL is accepting connections"

    print_step "Creating databases..."
    # Homebrew's PostgreSQL trusts the local account, so Oneirodex connects as
    # you over the local socket rather than as a password-protected role.
    for database in "$DB_NAME" "$TEST_DB_NAME"; do
        if psql -lqt 2>/dev/null | cut -d '|' -f 1 | grep -qw "$database"; then
            print_info "Database already exists: $database"
        else
            createdb "$database"
            print_success "Database created: $database"
        fi
    done
}

setup_python_environment() {
    print_step "Setting up Python virtual environment..."
    local python_bin
    python_bin="$(brew --prefix "$PYTHON_FORMULA")/bin/python3"
    [ -x "$python_bin" ] || python_bin="python3"

    if [ ! -d "$SCRIPT_DIR/venv" ]; then
        "$python_bin" -m venv "$SCRIPT_DIR/venv"
        print_success "Virtual environment created"
    else
        print_info "Using existing virtual environment"
    fi

    # shellcheck disable=SC1091
    source "$SCRIPT_DIR/venv/bin/activate"
    run_quiet python3 -m pip install --upgrade pip
    print_verbose "Installing Python dependencies..."
    if run_quiet 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 ] && [ -f "$SCRIPT_DIR/requirements-dev.txt" ]; then
        run_quiet python3 -m pip install -r "$SCRIPT_DIR/requirements-dev.txt"
    fi
}

check_scan_locations() {
    # Warn, never fail: an autofs mount can legitimately be absent until first
    # access, and a share that is down today should not block the install.
    [ -n "$LIBRARY_ROOTS" ] || return 0
    local old_ifs="$IFS"
    IFS='|'
    for entry in $LIBRARY_ROOTS; do
        local root_path="${entry#*=}"
        root_path="$(echo "$root_path" | sed 's/^ *//; s/ *$//')"
        if [ -z "$root_path" ]; then
            continue
        elif [ -d "$root_path" ]; then
            print_success "Scan location found: $root_path"
        else
            print_warning "Scan location not mounted yet: $root_path"
        fi
    done
    IFS="$old_ifs"
}

configure_application() {
    print_step "Configuring Oneirodex..."

    if [ -f "$SCRIPT_DIR/.env" ]; then
        cp "$SCRIPT_DIR/.env" "$SCRIPT_DIR/.env.backup.$(date +%Y%m%d-%H%M%S)"
        print_success "Existing .env backed up"
    fi

    local secret_key
    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="$(python3 -c 'import secrets; print(secrets.token_urlsafe(64))')"
    fi

    if [ -z "$GAMES_DIR" ]; then
        echo
        print_info "Which folder holds your games?"
        read -r -p "Games directory path [$HOME/Games]: " input_dir
        GAMES_DIR="${input_dir:-$HOME/Games}"
    fi

    if [ ! -d "$GAMES_DIR" ]; then
        print_warning "Games directory does not exist: $GAMES_DIR"
        read -r -p "Create this directory? [Y/n]: " create_dir
        case "${create_dir:-Y}" in
            [Yy]|[Yy][Ee][Ss]) mkdir -p "$GAMES_DIR"; print_success "Created $GAMES_DIR" ;;
            *) print_warning "Update DATA_FOLDER_GAMES in .env before your first scan" ;;
        esac
    else
        print_success "Games directory exists: $GAMES_DIR"
    fi

    # Extra scan locations. On macOS a mounted share shows up under /Volumes,
    # so pointing Oneirodex at a NAS is a matter of naming the mount point.
    if [ -z "$LIBRARY_ROOTS" ]; then
        echo
        print_info "Extra scan locations beyond $GAMES_DIR (optional)."
        print_info "Mounted shares live under /Volumes — e.g. 'NAS ROMs=/Volumes/roms'"
        print_info "Separate several with a pipe character."
        read -r -p "Extra scan locations [none]: " input_roots
        LIBRARY_ROOTS="${input_roots:-}"
    fi
    check_scan_locations

    local db_url="postgresql://$DB_USER@localhost:5432/$DB_NAME"
    local test_db_url="postgresql://$DB_USER@localhost:5432/$TEST_DB_NAME"

    cat > "$SCRIPT_DIR/.env" <<EOF
# Oneirodex Configuration - Generated by install-macos.sh $(date)

# Database connection (Homebrew PostgreSQL trusts the local account)
DATABASE_URL=$db_url
TEST_DATABASE_URL=$test_db_url

# Game files directory
DATA_FOLDER_GAMES=$GAMES_DIR

# Extra scan locations: mounted shares (/Volumes/...), second disks, anything
# else this Mac can open. Pipe-separated, optional "Label=" prefix.
# See docs/runbooks/remote-scan-locations.md
ONEIRODEX_LIBRARY_ROOTS=$LIBRARY_ROOTS

# Base folders for path resolution
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

PORT=$CUSTOM_PORT
DEV_MODE=$DEV_MODE

# Local HTTP — set both true once you put Oneirodex behind HTTPS
SESSION_COOKIE_SECURE=false
REMEMBER_COOKIE_SECURE=false
EOF

    chmod 600 "$SCRIPT_DIR/.env"
    print_success "Environment configuration created"

    chmod +x "$SCRIPT_DIR"/*.sh
    print_success "Shell script permissions set"
}

validate_installation() {
    print_step "Validating installation..."
    # shellcheck disable=SC1091
    source "$SCRIPT_DIR/venv/bin/activate"

    set -a
    # shellcheck disable=SC1091
    source "$SCRIPT_DIR/.env"
    set +a

    if python3 -c "from oneirodex import create_app; create_app()" >/dev/null 2>&1; then
        print_success "Flask application setup validated"
    else
        print_error "Flask application validation failed — see $LOG_FILE"
        return 1
    fi

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

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} $DB_NAME (local socket, user $DB_USER)"
    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}  Run at login:${NC} docs/runbooks/install-native.md (macOS launchd)"
    echo -e "${CYAN}  Field log:${NC} $LOG_FILE"
    echo

    read -r -p "Start Oneirodex now? [Y/n]: " start_now
    case "${start_now:-Y}" in
        [Yy]|[Yy][Ee][Ss])
            echo
            print_info "Opening the launch gate — http://localhost:$CUSTOM_PORT"
            print_info "Press Ctrl+C to stop"
            echo
            export PORT="$CUSTOM_PORT"
            exec ./startweb.sh
            ;;
        *)
            echo
            print_info "To start Oneirodex later, run: ./startweb.sh"
            print_info "Then open: http://localhost:$CUSTOM_PORT"
            ;;
    esac
}

main() {
    echo "Oneirodex macOS Installer - $(date)" > "$LOG_FILE"
    print_header
    parse_arguments "$@"

    print_info "This installer will:"
    print_info "  • Install Python and PostgreSQL 17 via Homebrew"
    print_info "  • Create the oneirodex databases"
    print_info "  • Set up a Python virtual environment"
    print_info "  • Write .env (games folder, scan locations, secret key)"
    echo
    print_info "Press any key to continue or Ctrl+C to quit..."
    read -n 1 -s -r
    echo

    check_platform
    check_homebrew
    install_prerequisites
    setup_postgresql
    setup_python_environment
    configure_application
    validate_installation
    show_summary
}

main "$@"