#!/usr/bin/env bash # ============================================================================= # Pterodactyl Advanced Admin Addons — Auto Installer # Author : Ball Studios # Repo : https://git.balls.studio/BallStudios/pterodactyl_addon # License: MIT # # Usage (run as root or with sudo): # bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh) # # Or with a custom panel path: # bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh) /var/www/pterodactyl # ============================================================================= set -euo pipefail # ── Colours ────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } step() { echo -e "\n${BOLD}▶ $*${NC}"; } # ── Config ─────────────────────────────────────────────────────────────────── PANEL_DIR="${1:-/var/www/pterodactyl}" REPO_URL="https://git.balls.studio/BallStudios/pterodactyl_addon" RAW_URL="${REPO_URL}/raw/branch/main" ADDON_VERSION="1.0.0" TMP_DIR="$(mktemp -d)" BACKUP_DIR="${PANEL_DIR}/.addon_backup_$(date +%Y%m%d_%H%M%S)" cleanup() { rm -rf "$TMP_DIR"; } trap cleanup EXIT # ── Banner ─────────────────────────────────────────────────────────────────── echo -e " ${BOLD}${CYAN}╔══════════════════════════════════════════════════════╗ ║ Pterodactyl Advanced Admin Addons v${ADDON_VERSION} ║ ║ by Ball Studios — git.balls.studio ║ ╚══════════════════════════════════════════════════════╝${NC} " # ── Preflight checks ───────────────────────────────────────────────────────── step "Running preflight checks..." [[ $EUID -eq 0 ]] || error "This script must be run as root (or with sudo)." command -v php >/dev/null 2>&1 || error "PHP is not installed or not in PATH." command -v curl >/dev/null 2>&1 || error "curl is not installed." command -v git >/dev/null 2>&1 || error "git is not installed." [[ -f "${PANEL_DIR}/artisan" ]] || \ error "Pterodactyl panel not found at '${PANEL_DIR}'. Pass the correct path as first argument." PHP_BIN="$(command -v php)" PHP_VER="$($PHP_BIN -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')" info "PHP version: ${PHP_VER}" PANEL_VER="$(cd "$PANEL_DIR" && $PHP_BIN artisan --version 2>/dev/null | grep -oP '[\d\.]+' | head -1 || echo unknown)" info "Panel artisan version: ${PANEL_VER}" # Warn but don't block on panel version — admin may know what they're doing if [[ "$PANEL_VER" != "1.12"* ]] && [[ "$PANEL_VER" != "unknown" ]]; then warn "This addon targets Panel v1.12.x. Detected: v${PANEL_VER}. Proceed at your own risk." fi success "Preflight checks passed." # ── Confirm ─────────────────────────────────────────────────────────────────── echo "" echo -e "${YELLOW}The following actions will be taken:${NC}" echo " 1. Backup existing panel files to ${BACKUP_DIR}" echo " 2. Clone addon into ${TMP_DIR}" echo " 3. Copy addon files into ${PANEL_DIR}" echo " 4. Apply minimal patches to 3 core files (documented in patches/PATCHES.md)" echo " 5. Run database migrations" echo " 6. Seed default roles" echo " 7. Rebuild frontend assets (yarn build:production)" echo " 8. Clear panel caches" echo "" read -rp "Continue? [y/N] " CONFIRM [[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; exit 0; } # ── Backup ──────────────────────────────────────────────────────────────────── step "Backing up panel to ${BACKUP_DIR}..." mkdir -p "$BACKUP_DIR" cp "${PANEL_DIR}/app/Providers/AppServiceProvider.php" "$BACKUP_DIR/" 2>/dev/null || true cp "${PANEL_DIR}/resources/scripts/routers/ServerRouter.tsx" "$BACKUP_DIR/" 2>/dev/null || true cp "${PANEL_DIR}/resources/scripts/components/server/files/FileEditContainer.tsx" "$BACKUP_DIR/" 2>/dev/null || true cp "${PANEL_DIR}/resources/scripts/components/server/console/ServerConsole.tsx" "$BACKUP_DIR/" 2>/dev/null || true success "Backup complete → ${BACKUP_DIR}" # ── Clone Addon ─────────────────────────────────────────────────────────────── step "Cloning addon repository..." git clone --depth=1 "$REPO_URL" "$TMP_DIR/addon" \ || error "Failed to clone ${REPO_URL}. Check network access and repository visibility." success "Clone complete." # ── Copy Addon Files ────────────────────────────────────────────────────────── step "Copying addon files into panel..." cd "$TMP_DIR/addon" # Backend cp -r app/Addons "${PANEL_DIR}/app/" # Config cp -r config/advanced-admin.php "${PANEL_DIR}/config/" cp -r config/addon-commands "${PANEL_DIR}/config/" # Migrations mkdir -p "${PANEL_DIR}/database/migrations/addon" cp -r database/migrations/addon/. "${PANEL_DIR}/database/migrations/addon/" # Routes cp routes/addon-client.php "${PANEL_DIR}/routes/" cp routes/addon-application.php "${PANEL_DIR}/routes/" # Frontend mkdir -p "${PANEL_DIR}/resources/scripts/addons" cp -r resources/scripts/addons/advanced-admin "${PANEL_DIR}/resources/scripts/addons/" # Seeders cp -r database/seeders/. "${PANEL_DIR}/database/seeders/" # Tests cp -r tests/Addons "${PANEL_DIR}/tests/" success "Files copied." # ── Apply Core Patches ──────────────────────────────────────────────────────── step "Applying core patches..." apply_patch() { local FILE="$1" local SEARCH="$2" local REPLACE="$3" local LABEL="$4" if grep -qF "$SEARCH" "$FILE"; then warn "Patch '${LABEL}' already applied — skipping." return 0 fi if ! grep -qF "$REPLACE" "$FILE"; then # Use python for reliable multiline sed (available on most Linux) python3 -c " import sys content = open('$FILE').read() if '''$SEARCH''' not in content: sys.stderr.write('WARN: anchor not found in $FILE\n') sys.exit(0) content = content.replace('''$SEARCH''', '''$SEARCH\n$REPLACE''', 1) open('$FILE','w').write(content) " success "Patch applied: ${LABEL}" fi } # Patch 1: AppServiceProvider — register addon ASP="${PANEL_DIR}/app/Providers/AppServiceProvider.php" apply_patch "$ASP" \ "public function register(): void" \ " \$this->app->register(\Pterodactyl\Addons\AdvancedAdmin\AddonServiceProvider::class);" \ "Register AddonServiceProvider" # Patch 2: ServerRouter — add Network tab route SR="${PANEL_DIR}/resources/scripts/routers/ServerRouter.tsx" if [[ -f "$SR" ]]; then if ! grep -q "addon-network" "$SR"; then python3 -c " content = open('$SR').read() import_line = \"import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';\" route_line = \" } />\" # Insert import after last existing addon import or at top of imports block if import_line not in content: content = content.replace(\"import React\", import_line + \"\nimport React\", 1) if route_line not in content: content = content.replace(\"\", route_line + \"\n \", 1) open('$SR','w').write(content) " success "Patch applied: ServerRouter NetworkTab" else warn "ServerRouter patch already applied — skipping." fi fi # Patch 3: FileEditContainer — inject history button FEC="${PANEL_DIR}/resources/scripts/components/server/files/FileEditContainer.tsx" if [[ -f "$FEC" ]]; then if ! grep -q "FileHistoryButton" "$FEC"; then python3 -c " content = open('$FEC').read() import_line = \"import FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';\" if import_line not in content: content = content.replace(\"import React\", import_line + \"\nimport React\", 1) open('$FEC','w').write(content) " success "Patch applied: FileEditContainer FileHistoryButton" else warn "FileEditContainer patch already applied — skipping." fi fi # Patch 4: ServerConsole — wrap command input SC="${PANEL_DIR}/resources/scripts/components/server/console/ServerConsole.tsx" if [[ -f "$SC" ]]; then if ! grep -q "CommandInput" "$SC"; then python3 -c " content = open('$SC').read() import_line = \"import CommandInput from '@/addons/advanced-admin/console/CommandInput';\" if import_line not in content: content = content.replace(\"import React\", import_line + \"\nimport React\", 1) open('$SC','w').write(content) " success "Patch applied: ServerConsole CommandInput" else warn "ServerConsole patch already applied — skipping." fi fi success "All patches applied." # ── Database Migrations ─────────────────────────────────────────────────────── step "Running database migrations..." cd "$PANEL_DIR" $PHP_BIN artisan migrate --path=database/migrations/addon --force \ || error "Migration failed. Check your database connection and logs." success "Migrations complete." # ── Seed Default Roles ──────────────────────────────────────────────────────── step "Seeding default roles..." # Run composer dump-autoload so Laravel can resolve the seeder class $PHP_BIN /usr/local/bin/composer dump-autoload --no-interaction -q 2>/dev/null \ || $PHP_BIN "$(which composer 2>/dev/null || echo composer)" dump-autoload -q 2>/dev/null \ || true $PHP_BIN artisan db:seed --class="Database\\Seeders\\DefaultRolesSeeder" --force \ || warn "Seeder failed or already run — skipping." success "Seeding complete." # ── Publish Config ──────────────────────────────────────────────────────────── step "Publishing config..." $PHP_BIN artisan vendor:publish --tag=advanced-admin-config --force 2>/dev/null || true success "Config published." # ── Build Frontend ──────────────────────────────────────────────────────────── step "Building frontend assets (this may take a few minutes)..." command -v yarn >/dev/null 2>&1 || error "yarn is not installed. Install with: npm install -g yarn" cd "$PANEL_DIR" # Ensure Node >=22 (panel requires it; upgrade via 'n' if needed) NODE_MAJ="$(node --version 2>/dev/null | sed 's/v//;s/\..*//' || echo 0)" if [[ "${NODE_MAJ}" -lt 22 ]]; then info "Node ${NODE_MAJ} is too old (need >=22). Upgrading via 'n'..." npm install -g n >/dev/null 2>&1 \ && n 22 >/dev/null 2>&1 \ && hash -r 2>/dev/null || true info "Node version now: $(node --version 2>/dev/null || echo unknown)" fi export NODE_OPTIONS="${NODE_OPTIONS:---openssl-legacy-provider}" yarn install --frozen-lockfile 2>&1 | tail -5 yarn build:production 2>&1 | tail -30 success "Frontend built." # ── Clear Caches ────────────────────────────────────────────────────────────── step "Clearing caches..." $PHP_BIN artisan optimize:clear $PHP_BIN artisan optimize success "Caches cleared." # ── Fix Permissions ─────────────────────────────────────────────────────────── step "Fixing file permissions..." chown -R www-data:www-data "${PANEL_DIR}/storage" "${PANEL_DIR}/bootstrap/cache" \ "${PANEL_DIR}/app/Addons" "${PANEL_DIR}/resources/scripts/addons" 2>/dev/null || true success "Permissions set." # ── Done ────────────────────────────────────────────────────────────────────── echo "" echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════╗ ║ ✅ Installation complete! ║ ╚══════════════════════════════════════════════════════╝${NC}" echo "" echo -e " Panel path : ${CYAN}${PANEL_DIR}${NC}" echo -e " Backup path : ${CYAN}${BACKUP_DIR}${NC}" echo -e " Addon repo : ${CYAN}${REPO_URL}${NC}" echo "" echo -e " ${YELLOW}Next steps:${NC}" echo " 1. Review config: ${PANEL_DIR}/config/advanced-admin.php" echo " 2. Add ADDON_* env vars to .env if needed (see docs/CONFIG.md)" echo " 3. Restart your queue worker: php artisan queue:restart" echo " 4. Visit your panel — four new modules are now active!" echo ""