#!/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}
/>