#!/usr/bin/env python3 """ Patch all 4 addon modules into the panel frontend: 1. Live Network → /addon-network nav tab (NetworkTab.tsx) 2. File History → FileHistoryButton injected into FileEditContainer.tsx 3. Console Autocomplete → CommandInput replaces bare in Console.tsx 4. Roles → /addon-roles nav tab (RolesPage.tsx) """ import sys, os, shutil PANEL = '/var/www/pterodactyl' ADDON_SRC = '/var/www/pterodactyl/resources/scripts/addons/advanced-admin' def read(path): return open(path).read() def write(path, content): open(path, 'w').write(content) def patch_file(path, label, check_str, old, new): content = read(path) if check_str in content: print(f" [{label}] Already patched") return False if old not in content: print(f" [{label}] WARN: anchor not found — check manually") return False write(path, content.replace(old, new, 1)) print(f" [{label}] ✅ Patched") return True # ═══════════════════════════════════════════════════════════════════════════════ # 1. routes.ts — register all 4 tabs # ═══════════════════════════════════════════════════════════════════════════════ print("\n=== 1. routes.ts ===") rts = f'{PANEL}/resources/scripts/routers/routes.ts' content = read(rts) # Imports to inject IMPORTS = [ ('NetworkTab', "import NetworkTab from '@/addons/advanced-admin/network/NetworkTab';"), ('RolesPage', "import RolesPage from '@/addons/advanced-admin/roles/RolesPage';"), ] lines = content.splitlines(keepends=True) last_import_idx = max(i for i, l in enumerate(lines) if l.strip().startswith('import ')) inserts = [] for check, imp in IMPORTS: if check not in content: inserts.append(imp + '\n') print(f" Adding import: {check}") else: print(f" Import exists: {check}") if inserts: for imp in reversed(inserts): lines.insert(last_import_idx + 1, imp) content = ''.join(lines) # Routes to append before closing of server array ROUTES = [ ('/addon-network', """, { path: '/addon-network', permission: null, name: 'Live Network', component: NetworkTab, }"""), ('/addon-roles', """, { path: '/addon-roles', permission: null, name: 'Roles', component: RolesPage, }"""), ] for route_path, block in ROUTES: if route_path not in content: target = " ],\n} as Routes;" if target in content: content = content.replace(target, f"{block},\n ],\n}} as Routes;") print(f" Added route: {route_path}") else: print(f" WARN: insertion point not found for {route_path}") else: print(f" Route exists: {route_path}") write(rts, content) # ═══════════════════════════════════════════════════════════════════════════════ # 2. FileEditContainer.tsx — inject FileHistoryButton # ═══════════════════════════════════════════════════════════════════════════════ print("\n=== 2. FileEditContainer.tsx — History button ===") fec = f'{PANEL}/resources/scripts/components/server/files/FileEditContainer.tsx' # Step A: add import patch_file( fec, 'import', 'FileHistoryButton', "import { encodePathSegments, hashToPath } from '@/helpers';", "import { encodePathSegments, hashToPath } from '@/helpers';\nimport FileHistoryButton from '@/addons/advanced-admin/revisions/FileHistoryButton';" ) # Step B: inject button in the footer action row, before Save/Create buttons patch_file( fec, 'JSX button', '", "
\n {action === 'edit' && }" ) # ═══════════════════════════════════════════════════════════════════════════════ # 3. Console.tsx — inject CommandInput wrapper # # The console's existing handles history/keyboard; our CommandInput # is a self-contained replacement that renders its own + dropdown. # We inject it BELOW the existing console output area, not replacing the input, # so we don't break the existing keyboard shortcuts. Instead we ADD an # autocomplete overlay above it. # ═══════════════════════════════════════════════════════════════════════════════ print("\n=== 3. Console.tsx — autocomplete ===") con = f'{PANEL}/resources/scripts/components/server/console/Console.tsx' # Add import patch_file( con, 'import', 'CommandInput', "import { ServerContext } from '@/state/server';", "import { ServerContext } from '@/state/server';\nimport CommandInput from '@/addons/advanced-admin/console/CommandInput';" ) # We inject CommandInput ABOVE the existing input row as an enhancement panel. # It sits above the native input and provides suggestions; the native input # still works for history navigation (up/down arrow). content = read(con) if 'CommandInput' in content and ' { setHistory((prev: string[]) => [cmd, ...(prev || [])].slice(0, 32)); instance && instance.send('send command', cmd); }} disabled={!connected} /> {/* native input kept for history arrow-key navigation */} { instance && instance.send('send command', cmd); }} disabled={!connected} />