#!/usr/bin/env bash # ============================================================================= # Pterodactyl Advanced Admin Addons — Installer / Updater # Author : Ball Studios # Repo : https://git.balls.studio/BallStudios/pterodactyl_addon # License: MIT # # Fresh install: # bash <(curl -fsSL https://git.balls.studio/BallStudios/pterodactyl_addon/raw/branch/main/install.sh) # # Update existing install: # bash <(curl -fsSL .../install.sh) --update # # Custom panel path: # bash <(curl -fsSL .../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}"; } check() { echo -e " ${GREEN}✔${NC} $*"; } fail() { echo -e " ${RED}✘${NC} $*"; } # ── Args ────────────────────────────────────────────────────────────────────── PANEL_DIR="/var/www/pterodactyl" IS_UPDATE=false for arg in "$@"; do case "$arg" in --update|-u) IS_UPDATE=true ;; /*) PANEL_DIR="$arg" ;; esac done # ── Config ──────────────────────────────────────────────────────────────────── 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)" ADDON_MARKER="${PANEL_DIR}/.addon_installed" 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} " if [[ -f "$ADDON_MARKER" ]] && [[ "$IS_UPDATE" == "false" ]]; then INSTALLED_VER="$(cat "$ADDON_MARKER" 2>/dev/null || echo unknown)" echo -e "${YELLOW}[!] Addon v${INSTALLED_VER} is already installed.${NC}" echo -e " Run with ${CYAN}--update${NC} to update, or re-run without to reinstall." echo "" read -rp "Reinstall/update anyway? [y/N] " CONFIRM [[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; exit 0; } IS_UPDATE=true fi # ═════════════════════════════════════════════════════════════════════════════ # STEP 1: DEPENDENCY CHECKS # ═════════════════════════════════════════════════════════════════════════════ step "Checking dependencies..." DEPS_OK=true # Root check if [[ $EUID -ne 0 ]]; then fail "Must run as root (or with sudo)." DEPS_OK=false else check "Running as root" fi # Panel directory if [[ -f "${PANEL_DIR}/artisan" ]]; then check "Pterodactyl panel found at ${PANEL_DIR}" else fail "Panel not found at ${PANEL_DIR}" DEPS_OK=false fi # Blueprint check (warn if installed — may conflict) if [[ -f "${PANEL_DIR}/BlueprintFramework" ]] || \ [[ -d "${PANEL_DIR}/blueprint" ]] || \ command -v blueprint >/dev/null 2>&1; then warn "Blueprint Framework detected. This addon patches core files directly" warn "and may conflict with Blueprint extensions. Proceed with caution." fi # PHP if command -v php >/dev/null 2>&1; then PHP_BIN="$(command -v php)" PHP_VER="$($PHP_BIN -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;' 2>/dev/null || echo 0)" PHP_MAJOR="${PHP_VER%%.*}" if [[ "$PHP_MAJOR" -ge 8 ]]; then check "PHP ${PHP_VER}" else fail "PHP ${PHP_VER} is too old (need >= 8.0)" DEPS_OK=false fi else fail "PHP not found" DEPS_OK=false fi # Required PHP extensions for EXT in pdo_mysql mbstring openssl tokenizer xml ctype json bcmath; do if $PHP_BIN -m 2>/dev/null | grep -qi "^${EXT}$"; then check "PHP ext: ${EXT}" else # Try to install automatically PKG="php${PHP_VER}-${EXT//_/-}" if command -v apt-get >/dev/null 2>&1; then info "Installing missing PHP extension: ${EXT}..." DEBIAN_FRONTEND=noninteractive apt-get install -y "$PKG" -q >/dev/null 2>&1 \ && check "PHP ext: ${EXT} (auto-installed)" \ || warn "PHP ext: ${EXT} missing — install manually: apt install ${PKG}" else warn "PHP ext: ${EXT} missing — install it manually" fi fi done # Composer if command -v composer >/dev/null 2>&1; then COMPOSER_BIN="$(command -v composer)" check "Composer found at ${COMPOSER_BIN}" elif [[ -f /usr/local/bin/composer ]]; then COMPOSER_BIN="/usr/local/bin/composer" check "Composer found at ${COMPOSER_BIN}" else warn "Composer not found — attempting auto-install..." curl -fsSL https://getcomposer.org/installer | $PHP_BIN -- --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 \ && COMPOSER_BIN="/usr/local/bin/composer" \ && check "Composer installed" \ || { fail "Composer not found — install from https://getcomposer.org"; DEPS_OK=false; } fi # Git if command -v git >/dev/null 2>&1; then check "Git: $(git --version | head -1)" else fail "git not installed — installing..." apt-get install -y git -q >/dev/null 2>&1 \ && check "git installed" \ || { fail "Could not install git"; DEPS_OK=false; } fi # curl command -v curl >/dev/null 2>&1 && check "curl available" || { fail "curl not found"; DEPS_OK=false; } # Node.js if command -v node >/dev/null 2>&1; then NODE_VER="$(node --version 2>/dev/null)" NODE_MAJ="${NODE_VER//v/}"; NODE_MAJ="${NODE_MAJ%%.*}" if [[ "$NODE_MAJ" -ge 22 ]]; then check "Node.js ${NODE_VER}" else warn "Node ${NODE_VER} is too old (panel requires >= 22). Will auto-upgrade." # Upgrade via n npm install -g n >/dev/null 2>&1 && n 22 >/dev/null 2>&1 && hash -r 2>/dev/null || true NEW_VER="$(node --version 2>/dev/null)" info "Node upgraded to ${NEW_VER}" check "Node.js ${NEW_VER} (upgraded)" fi else fail "Node.js not installed — installing via NodeSource..." curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null 2>&1 apt-get install -y nodejs >/dev/null 2>&1 check "Node.js $(node --version)" fi # yarn if command -v yarn >/dev/null 2>&1; then check "yarn: $(yarn --version)" else warn "yarn not found — installing..." npm install -g yarn >/dev/null 2>&1 \ && check "yarn installed" \ || { fail "Could not install yarn"; DEPS_OK=false; } fi # python3 (needed for patching) if command -v python3 >/dev/null 2>&1; then check "python3: $(python3 --version)" else apt-get install -y python3 -q >/dev/null 2>&1 \ && check "python3 installed" \ || { fail "python3 not available — needed for file patching"; DEPS_OK=false; } fi [[ "$DEPS_OK" == "true" ]] || error "Dependency checks failed. Fix the issues above and re-run." success "All dependencies satisfied." # ═════════════════════════════════════════════════════════════════════════════ # STEP 2: CONFIRM # ═════════════════════════════════════════════════════════════════════════════ PHP_BIN="$(command -v php)" PANEL_VER="$(cd "$PANEL_DIR" && $PHP_BIN artisan --version 2>/dev/null | grep -oP '[\d\.]+' | head -1 || echo unknown)" info "Panel version: ${PANEL_VER}" [[ "$PANEL_VER" != "1.12"* ]] && [[ "$PANEL_VER" != "unknown" ]] && \ warn "Addon targets v1.12.x. Detected: v${PANEL_VER}. May still work." echo "" if [[ "$IS_UPDATE" == "true" ]]; then echo -e "${YELLOW}Update mode — will:${NC}" echo " 1. Pull latest addon files from ${REPO_URL}" echo " 2. Re-apply all patches (idempotent — skips already-applied ones)" echo " 3. Run any new migrations" echo " 4. Ensure recharts is added to package.json" echo " 5. Rebuild frontend" echo " 6. Clear caches" else echo -e "${YELLOW}Fresh install — will:${NC}" echo " 1. Backup core panel files → ${BACKUP_DIR}" echo " 2. Clone addon → ${TMP_DIR}" echo " 3. Copy addon files into panel" echo " 4. Apply patches (AppServiceProvider, routes.ts, FileEditContainer, Console)" echo " 5. Run database migrations + seed default roles" echo " 6. Add recharts dependency + rebuild frontend" echo " 7. Clear caches" fi echo "" read -rp "Continue? [y/N] " CONFIRM [[ "${CONFIRM,,}" == "y" ]] || { info "Aborted."; exit 0; } # ═════════════════════════════════════════════════════════════════════════════ # STEP 3: BACKUP # ═════════════════════════════════════════════════════════════════════════════ step "Backing up patched files..." mkdir -p "$BACKUP_DIR" for F in \ "app/Providers/AppServiceProvider.php" \ "resources/scripts/routers/routes.ts" \ "resources/scripts/routers/ServerRouter.tsx" \ "resources/scripts/components/server/files/FileEditContainer.tsx" \ "resources/scripts/components/server/console/Console.tsx"; do [[ -f "${PANEL_DIR}/${F}" ]] && cp "${PANEL_DIR}/${F}" "$BACKUP_DIR/" 2>/dev/null || true done success "Backup → ${BACKUP_DIR}" # ═════════════════════════════════════════════════════════════════════════════ # STEP 4: CLONE / PULL ADDON # ═════════════════════════════════════════════════════════════════════════════ step "Fetching addon from repository..." git clone --depth=1 "$REPO_URL" "$TMP_DIR/addon" \ || error "Failed to clone ${REPO_URL}. Check network and repo visibility." success "Addon fetched." # ═════════════════════════════════════════════════════════════════════════════ # STEP 5: COPY FILES # ═════════════════════════════════════════════════════════════════════════════ step "Copying addon files..." cd "$TMP_DIR/addon" cp -r app/Addons "${PANEL_DIR}/app/" cp config/advanced-admin.php "${PANEL_DIR}/config/" cp -r config/addon-commands "${PANEL_DIR}/config/" mkdir -p "${PANEL_DIR}/database/migrations/addon" cp -r database/migrations/addon/. "${PANEL_DIR}/database/migrations/addon/" cp routes/addon-client.php "${PANEL_DIR}/routes/" cp routes/addon-application.php "${PANEL_DIR}/routes/" mkdir -p "${PANEL_DIR}/resources/scripts/addons" cp -r resources/scripts/addons/advanced-admin \ "${PANEL_DIR}/resources/scripts/addons/" cp -r database/seeders/. "${PANEL_DIR}/database/seeders/" [[ -d tests/Addons ]] && cp -r tests/Addons "${PANEL_DIR}/tests/" || true success "Files copied." # ═════════════════════════════════════════════════════════════════════════════ # STEP 6: APPLY ALL PATCHES (idempotent Python script) # ═════════════════════════════════════════════════════════════════════════════ step "Applying patches..." PATCH_SCRIPT="$(mktemp /tmp/addon_patch_XXXXXX.py)" cat > "$PATCH_SCRIPT" << 'PYEOF' import sys, re, os PANEL = sys.argv[1] def read(p): return open(p).read() def write(p, c): open(p, 'w').write(c) def patch(path, label, check, anchor, inject): c = read(path) if check in c: print(f" [skip] {label} — already applied") return if anchor not in c: print(f" [warn] {label} — anchor not found, skipping") return write(path, c.replace(anchor, anchor + '\n' + inject, 1)) print(f" [ ok ] {label}") # 1. AppServiceProvider patch( f'{PANEL}/app/Providers/AppServiceProvider.php', 'Register AddonServiceProvider', 'AddonServiceProvider::class', ' public function register(): void\n {', " $this->app->register(\\Pterodactyl\\Addons\\AdvancedAdmin\\AddonServiceProvider::class);" ) # 2. routes.ts — imports rts = f'{PANEL}/resources/scripts/routers/routes.ts' c = read(rts) lines = c.splitlines(keepends=True) last_import = max((i for i, l in enumerate(lines) if l.strip().startswith('import ')), default=0) added = [] for check, imp in [ ('NetworkTab', "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';"), ('RolesPage', "import RolesPage from '@/addons/advanced-admin/roles/RolesPage';"), ]: if check not in c: lines.insert(last_import + 1, imp + '\n') last_import += 1 added.append(check) print(f" [ ok ] routes.ts import: {check}") else: print(f" [skip] routes.ts import: {check}") c = ''.join(lines) # 2b. routes.ts — route entries for rpath, block in [ ('/addon-network', """, { path: '/addon-network', permission: null, name: 'Live Network', component: NetworkTab, }"""), ('/addon-roles', """, { path: '/addon-roles', permission: null, name: 'Roles', component: RolesPage, }"""), ]: if rpath in c: print(f" [skip] routes.ts route: {rpath}") else: target = " ],\n} as Routes;" if target in c: c = c.replace(target, f"{block},\n ],\n}} as Routes;") print(f" [ ok ] routes.ts route: {rpath}") else: print(f" [warn] routes.ts: closing not found for {rpath}") write(rts, c) # 3. FileEditContainer — import fec = f'{PANEL}/resources/scripts/components/server/files/FileEditContainer.tsx' patch(fec, 'FileHistoryButton import', 'FileHistoryButton', "import { encodePathSegments, hashToPath } from '@/helpers';", "import FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';" ) # 3b. FileEditContainer — JSX button patch(fec, 'FileHistoryButton JSX', '", " {action === 'edit' && }" ) # 4. Console.tsx — CommandInput con = f'{PANEL}/resources/scripts/components/server/console/Console.tsx' patch(con, 'CommandInput import', 'CommandInput', "import { ServerContext } from '@/state/server';", "import CommandInput from '@/addons/advanced-admin/console/CommandInput';" ) # 4b. Inject CommandInput (replace native input) c = read(con) if ' { setHistory((prev: string[]) => [cmd, ...(prev || [])].slice(0, 32)); instance && instance.send('send command', cmd); }} disabled={!connected} /> middleware\('throttle:addon-[^']+'\)", "", c) cleaned = re.sub(r",?\s*'addon\.ratelimit'", "", cleaned) if cleaned != c: write(path, cleaned) print(f" [ ok ] Cleaned middleware: {rf}") else: print(f" [skip] Middleware clean: {rf}") print("\nAll patches done.") PYEOF python3 "$PATCH_SCRIPT" "$PANEL_DIR" rm -f "$PATCH_SCRIPT" success "Patches applied." # ═════════════════════════════════════════════════════════════════════════════ # STEP 7: DATABASE MIGRATIONS # ═════════════════════════════════════════════════════════════════════════════ step "Running database migrations..." cd "$PANEL_DIR" $PHP_BIN artisan migrate --path=database/migrations/addon --force \ || warn "Migration issues — check logs. May be already run." success "Migrations complete." # ═════════════════════════════════════════════════════════════════════════════ # STEP 8: SEED DEFAULT ROLES # ═════════════════════════════════════════════════════════════════════════════ step "Seeding default roles..." cd "$PANEL_DIR" SEED_SCRIPT="$(mktemp /tmp/addon_seed_XXXXXX.php)" PANEL_ESC="${PANEL_DIR//\'/\'}" cat > "$SEED_SCRIPT" << PHPEOF make(Illuminate\Contracts\Console\Kernel::class); \$kernel->bootstrap(); require_once '${PANEL_ESC}/database/seeders/DefaultRolesSeeder.php'; \$seeder = new Database\Seeders\DefaultRolesSeeder(); \$seeder->setContainer(\$app); \$seeder->run(); echo "Roles seeded.\n"; PHPEOF $PHP_BIN "$SEED_SCRIPT" 2>&1 || warn "Seeder failed or already run — skipping." rm -f "$SEED_SCRIPT" success "Seeding complete." # ═════════════════════════════════════════════════════════════════════════════ # STEP 9: ADD ENV VARS (if not present) # ═════════════════════════════════════════════════════════════════════════════ step "Checking .env addon vars..." ENV_FILE="${PANEL_DIR}/.env" if [[ -f "$ENV_FILE" ]] && ! grep -q "ADDON_NETWORK_ENABLED" "$ENV_FILE"; then cat >> "$ENV_FILE" << 'ENVEOF' # ── Pterodactyl Advanced Admin Addons ───────────────────────────────────────── ADDON_NETWORK_ENABLED=true ADDON_NETWORK_INTERVAL=10 ADDON_NETWORK_IPTABLES=false ADDON_NETWORK_RETENTION_RAW=1 ADDON_NETWORK_RETENTION_5M=7 ADDON_NETWORK_RETENTION_1H=30 ADDON_NETWORK_RETENTION_1D=365 ADDON_ROLES_ENABLED=true ADDON_REVISIONS_ENABLED=true ADDON_REVISIONS_MAX_FILE_SIZE=10485760 ADDON_REVISIONS_MAX_PER_FILE=50 ADDON_REVISIONS_RETENTION_DAYS=90 ADDON_CONSOLE_ENABLED=true ADDON_CONSOLE_RATE_LIMIT=60 ADDON_CONSOLE_PLAYER_TTL=30 ENVEOF success ".env vars added." else info ".env addon vars already present — skipping." fi # ═════════════════════════════════════════════════════════════════════════════ # STEP 10: BUILD FRONTEND # ═════════════════════════════════════════════════════════════════════════════ step "Building frontend assets..." cd "$PANEL_DIR" # Ensure recharts is in package.json (survives yarn install --frozen-lockfile) if ! grep -q '"recharts"' package.json 2>/dev/null; then info "Adding recharts@2.12.7 to package.json..." yarn add recharts@2.12.7 --no-lockfile >/dev/null 2>&1 success "recharts added." else info "recharts already in package.json." fi yarn install 2>&1 | tail -3 yarn build:production 2>&1 | tail -15 success "Frontend built." # ═════════════════════════════════════════════════════════════════════════════ # STEP 11: CACHES + PERMISSIONS # ═════════════════════════════════════════════════════════════════════════════ step "Clearing caches and fixing permissions..." cd "$PANEL_DIR" $PHP_BIN artisan optimize:clear $PHP_BIN artisan optimize $PHP_BIN artisan queue:restart 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 "Caches cleared and permissions set." # ── Mark installation ───────────────────────────────────────────────────────── echo "$ADDON_VERSION" > "$ADDON_MARKER" # ── Done ────────────────────────────────────────────────────────────────────── echo "" echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════════╗ ║ ✅ $([ "$IS_UPDATE" == "true" ] && echo "Update" || echo "Installation") complete! v${ADDON_VERSION} ║ ╚══════════════════════════════════════════════════════════╝${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 " ${BOLD}Active modules:${NC}" echo " 🌐 Live Network — server nav tab → /addon-network" echo " 🛡 Roles — server nav tab → /addon-roles" echo " 🕐 File History — button in file editor toolbar" echo " 🤖 Autocomplete — enhanced console command input" echo "" echo -e " ${YELLOW}To update in the future, run:${NC}" echo " bash <(curl -fsSL ${RAW_URL}/install.sh) --update" echo ""