#!/usr/bin/env php make(Illuminate\Contracts\Console\Kernel::class); $kernel->bootstrap(); // Parse arguments $args = $_SERVER['argv']; array_shift($args); // Remove script name $command = $args[0] ?? ''; $subcommand = $args[1] ?? ''; $options = parseOptions(array_slice($args, 2)); // Route commands switch ($command) { case '': case 'list': case '--help': case '-h': showHelp(); break; case '--help-full': case 'help-full': showHelpFull(); break; case 'user': handleUser($subcommand, $options); break; case 'domain': handleDomain($subcommand, $options); break; case 'service': handleService($subcommand, $options); break; case 'wp': case 'wordpress': handleWordPress($subcommand, $options); break; case 'db': case 'database': handleDatabase($subcommand, $options); break; case 'mail': case 'email': handleEmail($subcommand, $options); break; case 'backup': handleBackup($subcommand, $options); break; case 'cpanel': handleCpanel($subcommand, $options); break; case 'system': handleSystem($subcommand, $options); break; case 'agent': handleAgent($subcommand, $options); break; case 'php': handlePhp($subcommand, $options); break; case 'firewall': case 'fw': handleFirewall($subcommand, $options); break; case 'ssl': handleSsl($subcommand, $options); break; case '--version': case '-v': echo "Jabali CLI v" . VERSION . "\n"; break; default: error("Unknown command: $command"); echo "Run 'jabali --help' for usage information.\n"; exit(1); } exit(0); // ============ HELPER FUNCTIONS ============ function parseOptions(array $args): array { $options = ['_args' => []]; foreach ($args as $arg) { if (strpos($arg, '--') === 0) { $arg = substr($arg, 2); if (strpos($arg, '=') !== false) { [$key, $value] = explode('=', $arg, 2); $options[$key] = $value; } else { $options[$arg] = true; } } elseif (strpos($arg, '-') === 0) { $options[substr($arg, 1)] = true; } else { $options['_args'][] = $arg; } } return $options; } function showHelp(): void { echo "\n" . C_YELLOW . "░░░░░██╗░█████╗░██████╗░░█████╗░██╗░░░░░██╗" . C_RESET . "\n"; echo C_YELLOW . "░░░░░██║██╔══██╗██╔══██╗██╔══██╗██║░░░░░██║" . C_RESET . "\n"; echo C_YELLOW . "░░░░░██║███████║██████╦╝███████║██║░░░░░██║" . C_RESET . "\n"; echo C_YELLOW . "██╗░░██║██╔══██║██╔══██╗██╔══██║██║░░░░░██║" . C_RESET . "\n"; echo C_YELLOW . "╚█████╔╝██║░░██║██████╦╝██║░░██║███████╗██║" . C_RESET . "\n"; echo C_YELLOW . "░╚════╝░╚═╝░░╚═╝╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝" . C_RESET . "\n\n"; echo " " . C_GREEN . C_BOLD . "Jabali CLI" . C_RESET . " v" . VERSION . " - " . C_CYAN . "Modern Web Hosting Control Panel" . C_RESET . "\n\n"; echo C_YELLOW . "Usage:" . C_RESET . " jabali [options]\n\n"; echo C_YELLOW . C_BOLD . "User Management:" . C_RESET . "\n"; echo " " . C_GREEN . "user list" . C_RESET . " List all users\n"; echo " " . C_GREEN . "user create " . C_RESET . " Create a new user\n"; echo " " . C_GREEN . "user show " . C_RESET . " Show user details\n"; echo " " . C_GREEN . "user delete " . C_RESET . " Delete a user\n"; echo " " . C_GREEN . "user password " . C_RESET . " Change user password\n"; echo " " . C_GREEN . "user suspend " . C_RESET . " Suspend a user\n"; echo " " . C_GREEN . "user unsuspend " . C_RESET . " Unsuspend a user\n\n"; echo C_YELLOW . C_BOLD . "Domain Management:" . C_RESET . "\n"; echo " " . C_GREEN . "domain list [--user=]" . C_RESET . " List domains\n"; echo " " . C_GREEN . "domain create " . C_RESET . " Create a domain\n"; echo " " . C_GREEN . "domain show " . C_RESET . " Show domain details\n"; echo " " . C_GREEN . "domain delete " . C_RESET . " Delete a domain\n"; echo " " . C_GREEN . "domain enable " . C_RESET . " Enable a domain\n"; echo " " . C_GREEN . "domain disable " . C_RESET . " Disable a domain\n\n"; echo C_DIM . "More commands: service, wp, db, mail, backup, cpanel, system, agent, php, firewall, ssl\n" . C_RESET; echo C_DIM . "Run " . C_RESET . C_CYAN . "jabali --help-full" . C_RESET . C_DIM . " for the full command list.\n\n" . C_RESET; echo C_YELLOW . C_BOLD . "Options:" . C_RESET . "\n"; echo " " . C_GREEN . "-h, --help" . C_RESET . " Show this help\n"; echo " " . C_GREEN . "--help-full" . C_RESET . " Show all commands\n"; echo " " . C_GREEN . "-v, --version" . C_RESET . " Show version\n"; echo " " . C_GREEN . "-y, --yes" . C_RESET . " Auto-confirm prompts\n"; echo " " . C_GREEN . "-q, --quiet" . C_RESET . " Quiet mode\n\n"; } function showHelpFull(): void { echo "\n" . C_YELLOW . "░░░░░██╗░█████╗░██████╗░░█████╗░██╗░░░░░██╗" . C_RESET . "\n"; echo C_YELLOW . "░░░░░██║██╔══██╗██╔══██╗██╔══██╗██║░░░░░██║" . C_RESET . "\n"; echo C_YELLOW . "░░░░░██║███████║██████╦╝███████║██║░░░░░██║" . C_RESET . "\n"; echo C_YELLOW . "██╗░░██║██╔══██║██╔══██╗██╔══██║██║░░░░░██║" . C_RESET . "\n"; echo C_YELLOW . "╚█████╔╝██║░░██║██████╦╝██║░░██║███████╗██║" . C_RESET . "\n"; echo C_YELLOW . "░╚════╝░╚═╝░░╚═╝╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝" . C_RESET . "\n\n"; echo " " . C_GREEN . C_BOLD . "Jabali CLI" . C_RESET . " v" . VERSION . " - " . C_CYAN . "Full Command Reference" . C_RESET . "\n\n"; echo C_YELLOW . "Usage:" . C_RESET . " jabali [options]\n\n"; // Two-column layout helper $col1Width = 40; $printRow = function($left, $right) use ($col1Width) { $leftClean = preg_replace('/\e\[[0-9;]*m/', '', $left); $padding = max(1, $col1Width - strlen($leftClean)); echo " " . $left . str_repeat(' ', $padding) . $right . "\n"; }; echo C_YELLOW . C_BOLD . "User Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Domain Management" . C_RESET . "\n"; $printRow(C_GREEN . "user list" . C_RESET . " - List users", C_GREEN . "domain list" . C_RESET . " - List domains"); $printRow(C_GREEN . "user create " . C_RESET . " - Create user", C_GREEN . "domain create " . C_RESET . " - Create domain"); $printRow(C_GREEN . "user show " . C_RESET . " - Show details", C_GREEN . "domain show " . C_RESET . " - Show details"); $printRow(C_GREEN . "user delete " . C_RESET . " - Delete user", C_GREEN . "domain delete " . C_RESET . " - Delete domain"); $printRow(C_GREEN . "user password " . C_RESET . " - Set password", C_GREEN . "domain enable " . C_RESET . " - Enable domain"); $printRow(C_GREEN . "user suspend " . C_RESET . " - Suspend", C_GREEN . "domain disable " . C_RESET . " - Disable domain"); $printRow(C_GREEN . "user unsuspend " . C_RESET . " - Unsuspend", ""); echo "\n"; echo C_YELLOW . C_BOLD . "Database Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Email Management" . C_RESET . "\n"; $printRow(C_GREEN . "db list" . C_RESET . " - List databases", C_GREEN . "mail list" . C_RESET . " - List mailboxes"); $printRow(C_GREEN . "db create " . C_RESET . " - Create database", C_GREEN . "mail create " . C_RESET . " - Create mailbox"); $printRow(C_GREEN . "db delete " . C_RESET . " - Delete database", C_GREEN . "mail delete " . C_RESET . " - Delete mailbox"); $printRow(C_GREEN . "db users" . C_RESET . " - List db users", C_GREEN . "mail password " . C_RESET . " - Set password"); $printRow(C_GREEN . "db user-create " . C_RESET . " - Create db user", C_GREEN . "mail quota " . C_RESET . " - Set quota"); $printRow(C_GREEN . "db user-delete " . C_RESET . " - Delete db user", C_GREEN . "mail domains" . C_RESET . " - List mail domains"); echo "\n"; echo C_YELLOW . C_BOLD . "Service Management" . C_RESET . " " . C_YELLOW . C_BOLD . "WordPress Management" . C_RESET . "\n"; $printRow(C_GREEN . "service list" . C_RESET . " - List services", C_GREEN . "wp list " . C_RESET . " - List WP sites"); $printRow(C_GREEN . "service status " . C_RESET . " - Show status", C_GREEN . "wp install " . C_RESET . " - Install WP"); $printRow(C_GREEN . "service start " . C_RESET . " - Start service", C_GREEN . "wp scan " . C_RESET . " - Scan for WP sites"); $printRow(C_GREEN . "service stop " . C_RESET . " - Stop service", C_GREEN . "wp import " . C_RESET . " - Import WP"); $printRow(C_GREEN . "service restart " . C_RESET . " - Restart", C_GREEN . "wp delete " . C_RESET . " - Delete WP site"); $printRow(C_GREEN . "service enable " . C_RESET . " - Enable on boot", C_GREEN . "wp update " . C_RESET . " - Update WP"); $printRow(C_GREEN . "service disable " . C_RESET . " - Disable on boot", ""); echo "\n"; echo C_YELLOW . C_BOLD . "System Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Agent Management" . C_RESET . "\n"; $printRow(C_GREEN . "system info" . C_RESET . " - System information", C_GREEN . "agent status" . C_RESET . " - Agent status"); $printRow(C_GREEN . "system status" . C_RESET . " - Services status", C_GREEN . "agent start" . C_RESET . " - Start agent"); $printRow(C_GREEN . "system hostname [name]" . C_RESET . " - Get/set hostname", C_GREEN . "agent stop" . C_RESET . " - Stop agent"); $printRow(C_GREEN . "system disk" . C_RESET . " - Disk usage", C_GREEN . "agent restart" . C_RESET . " - Restart agent"); $printRow(C_GREEN . "system memory" . C_RESET . " - Memory usage", C_GREEN . "agent ping" . C_RESET . " - Ping agent"); $printRow("", C_GREEN . "agent log [--lines=N]" . C_RESET . " - View logs"); echo "\n"; echo C_YELLOW . C_BOLD . "PHP Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Firewall Management" . C_RESET . "\n"; $printRow(C_GREEN . "php list" . C_RESET . " - List PHP versions", C_GREEN . "firewall status" . C_RESET . " - Show status"); $printRow(C_GREEN . "php install " . C_RESET . " - Install PHP", C_GREEN . "firewall enable" . C_RESET . " - Enable firewall"); $printRow(C_GREEN . "php uninstall " . C_RESET . " - Uninstall PHP", C_GREEN . "firewall disable" . C_RESET . " - Disable firewall"); $printRow(C_GREEN . "php default [ver]" . C_RESET . " - Get/set default", C_GREEN . "firewall rules" . C_RESET . " - List rules"); $printRow(C_GREEN . "php status" . C_RESET . " - PHP-FPM status", C_GREEN . "firewall allow " . C_RESET . " - Allow port"); $printRow("", C_GREEN . "firewall deny " . C_RESET . " - Deny port"); $printRow("", C_GREEN . "firewall delete " . C_RESET . " - Delete rule"); echo "\n"; echo C_YELLOW . C_BOLD . "Backup Management" . C_RESET . " " . C_YELLOW . C_BOLD . "SSL Management" . C_RESET . "\n"; $printRow(C_GREEN . "backup list [--user=]" . C_RESET . " - List backups", C_GREEN . "ssl check" . C_RESET . " - Check/issue/renew certs"); $printRow(C_GREEN . "backup create " . C_RESET . " - Create user backup", C_GREEN . "ssl issue " . C_RESET . " - Issue certificate"); $printRow(C_GREEN . "backup restore " . C_RESET . " - Restore backup", C_GREEN . "ssl renew " . C_RESET . " - Renew certificate"); $printRow(C_GREEN . "backup info " . C_RESET . " - Show backup info", C_GREEN . "ssl status " . C_RESET . " - Show cert status"); $printRow(C_GREEN . "backup verify " . C_RESET . " - Verify backup", C_GREEN . "ssl list" . C_RESET . " - List all certificates"); $printRow(C_GREEN . "backup server" . C_RESET . " - Create server backup", ""); $printRow(C_GREEN . "backup history" . C_RESET . " - Show backup history", ""); $printRow(C_GREEN . "backup schedules" . C_RESET . " - List schedules", ""); $printRow(C_GREEN . "backup destinations" . C_RESET . " - List destinations", ""); $printRow(C_GREEN . "backup help" . C_RESET . " - Show all backup cmds", ""); echo "\n"; echo C_YELLOW . C_BOLD . "Migration & Restore" . C_RESET . "\n"; $printRow(C_GREEN . "cpanel analyze " . C_RESET . " - Analyze backup", C_GREEN . "cpanel restore " . C_RESET . " - Restore backup"); $printRow(C_GREEN . "cpanel fix-permissions " . C_RESET . " - Fix backup perms", ""); echo "\n"; echo C_YELLOW . C_BOLD . "Options" . C_RESET . "\n"; echo " " . C_GREEN . "-h, --help" . C_RESET . " Show basic help " . C_GREEN . "-v, --version" . C_RESET . " Show version\n"; echo " " . C_GREEN . "--help-full" . C_RESET . " Show full help " . C_GREEN . "-y, --yes" . C_RESET . " Auto-confirm\n"; echo " " . C_GREEN . "-q, --quiet" . C_RESET . " Quiet mode\n"; echo "\n"; } function success(string $message): void { echo C_GREEN . "✓ " . C_RESET . $message . "\n"; } function error(string $message): void { echo C_RED . "✗ " . C_RESET . $message . "\n"; } function info(string $message): void { echo C_CYAN . "ℹ " . C_RESET . $message . "\n"; } function warning(string $message): void { echo C_YELLOW . "⚠ " . C_RESET . $message . "\n"; } function confirm(string $message, array $options): bool { if (isset($options['y']) || isset($options['yes'])) { return true; } echo $message . " [y/N]: "; $handle = fopen("php://stdin", "r"); $line = fgets($handle); fclose($handle); return strtolower(trim($line)) === 'y'; } function prompt(string $message, bool $hidden = false): string { echo $message . ": "; if ($hidden) { system('stty -echo'); } $handle = fopen("php://stdin", "r"); $line = trim(fgets($handle)); fclose($handle); if ($hidden) { system('stty echo'); echo "\n"; } return $line; } function generateSecurePassword(int $length = 16): string { $lower = 'abcdefghijklmnopqrstuvwxyz'; $upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $numbers = '0123456789'; $special = '!@#$%^&*'; $password = $lower[random_int(0, 25)] . $upper[random_int(0, 25)] . $numbers[random_int(0, 9)] . $special[random_int(0, 7)]; $all = $lower . $upper . $numbers . $special; for ($i = 4; $i < $length; $i++) $password .= $all[random_int(0, strlen($all) - 1)]; return str_shuffle($password); } function validatePassword(string $password): ?string { if (strlen($password) < 8) return 'Password must be at least 8 characters'; if (!preg_match('/[a-z]/', $password)) return 'Password must contain a lowercase letter'; if (!preg_match('/[A-Z]/', $password)) return 'Password must contain an uppercase letter'; if (!preg_match('/[0-9]/', $password)) return 'Password must contain a number'; return null; } function promptPassword(string $message = "Password", bool $allowAutoGenerate = false): string { $hint = $allowAutoGenerate ? " (enter to auto-generate)" : ""; while (true) { $password = prompt($message . $hint, true); if ($allowAutoGenerate && $password === '') { $password = generateSecurePassword(); echo C_GREEN . "Generated password: " . C_RESET . $password . "\n"; return $password; } if ($error = validatePassword($password)) { error($error); continue; } return $password; } } function agentSend(string $action, array $params = []): array { return agentSendWithTimeout($action, $params, 60); } function agentSendWithTimeout(string $action, array $params = [], int $timeoutSeconds = 60): array { if (!file_exists(AGENT_SOCKET)) { return ['success' => false, 'error' => 'Agent socket not found. Is the agent running?']; } $socket = @socket_create(AF_UNIX, SOCK_STREAM, 0); if (!$socket) { return ['success' => false, 'error' => 'Failed to create socket']; } socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $timeoutSeconds, 'usec' => 0]); socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $timeoutSeconds, 'usec' => 0]); if (!@socket_connect($socket, AGENT_SOCKET)) { socket_close($socket); return ['success' => false, 'error' => 'Failed to connect to agent']; } $request = json_encode(['action' => $action, 'params' => $params]); socket_write($socket, $request, strlen($request)); $response = ''; while ($buf = socket_read($socket, 8192)) { $response .= $buf; } socket_close($socket); return json_decode($response, true) ?: ['success' => false, 'error' => 'Invalid response from agent']; } function table(array $headers, array $rows): void { if (empty($rows)) { echo C_DIM . "No data to display." . C_RESET . "\n"; return; } // Calculate column widths $widths = []; foreach ($headers as $i => $header) { $widths[$i] = strlen($header); } foreach ($rows as $row) { foreach ($row as $i => $cell) { $widths[$i] = max($widths[$i] ?? 0, strlen((string)$cell)); } } // Print header echo C_BOLD; foreach ($headers as $i => $header) { echo str_pad($header, $widths[$i] + 2); } echo C_RESET . "\n"; // Print separator foreach ($widths as $width) { echo str_repeat('─', $width + 2); } echo "\n"; // Print rows foreach ($rows as $row) { foreach ($row as $i => $cell) { echo str_pad((string)$cell, $widths[$i] + 2); } echo "\n"; } } // ============ USER COMMANDS ============ function handleUser(string $subcommand, array $options): void { switch ($subcommand) { case 'list': $users = App\Models\User::all(); $rows = []; foreach ($users as $user) { $rows[] = [ $user->id, $user->username ?? $user->name, $user->email, $user->is_admin ? 'Admin' : 'User', $user->suspended ? C_RED . 'Suspended' . C_RESET : C_GREEN . 'Active' . C_RESET, $user->created_at->format('Y-m-d'), ]; } table(['ID', 'Username', 'Email', 'Role', 'Status', 'Created'], $rows); break; case 'create': $username = $options['_args'][0] ?? prompt("Username"); if (!$username) { error("Username is required"); exit(1); } $email = $options['email'] ?? prompt("Email"); if (isset($options['password'])) { $password = $options['password']; if ($err = validatePassword($password)) { error($err); exit(1); } } else { $password = promptPassword("Password"); } // Create system user via agent $result = agentSend('user.create', ['username' => $username, 'password' => $password]); if (!($result['success'] ?? false)) { error("Failed to create system user: " . ($result['error'] ?? 'Unknown error')); exit(1); } // Create database user $user = App\Models\User::create([ 'name' => $username, 'username' => $username, 'email' => $email, 'password' => bcrypt($password), ]); success("User '$username' created successfully (ID: {$user->id})"); break; case 'show': $username = $options['_args'][0] ?? null; if (!$username) { error("Username is required"); exit(1); } $user = App\Models\User::where('username', $username)->orWhere('name', $username)->first(); if (!$user) { error("User not found: $username"); exit(1); } echo "\n" . C_BOLD . "User Details" . C_RESET . "\n"; echo str_repeat('─', 40) . "\n"; echo C_CYAN . "ID:" . C_RESET . " {$user->id}\n"; echo C_CYAN . "Username:" . C_RESET . " " . ($user->username ?? $user->name) . "\n"; echo C_CYAN . "Email:" . C_RESET . " {$user->email}\n"; echo C_CYAN . "Role:" . C_RESET . " " . ($user->is_admin ? 'Admin' : 'User') . "\n"; echo C_CYAN . "Status:" . C_RESET . " " . ($user->suspended ? 'Suspended' : 'Active') . "\n"; echo C_CYAN . "Created:" . C_RESET . " {$user->created_at}\n"; $domainCount = App\Models\Domain::where('user_id', $user->id)->count(); echo C_CYAN . "Domains:" . C_RESET . " $domainCount\n"; break; case 'delete': $username = $options['_args'][0] ?? null; if (!$username) { error("Username is required"); exit(1); } $user = App\Models\User::where('username', $username)->orWhere('name', $username)->first(); if (!$user) { error("User not found: $username"); exit(1); } if (!confirm("Are you sure you want to delete user '$username'? This cannot be undone.", $options)) { info("Operation cancelled"); exit(0); } // Delete system user $result = agentSend('user.delete', ['username' => $username]); if (!($result['success'] ?? false)) { warning("Could not delete system user: " . ($result['error'] ?? 'Unknown error')); } $user->delete(); success("User '$username' deleted"); break; case 'password': $username = $options['_args'][0] ?? null; if (!$username) { error("Username is required"); exit(1); } $user = App\Models\User::where('username', $username)->orWhere('name', $username)->first(); if (!$user) { error("User not found: $username"); exit(1); } if (isset($options['password'])) { $password = $options['password']; if ($err = validatePassword($password)) { error($err); exit(1); } } else { $password = promptPassword("New password", true); } $user->password = bcrypt($password); $user->save(); // Update system user password agentSend('user.password', ['username' => $username, 'password' => $password]); success("Password updated for '$username'"); break; case 'suspend': $username = $options['_args'][0] ?? null; if (!$username) { error("Username is required"); exit(1); } $user = App\Models\User::where('username', $username)->orWhere('name', $username)->first(); if (!$user) { error("User not found: $username"); exit(1); } $user->suspended = true; $user->save(); success("User '$username' suspended"); break; case 'unsuspend': $username = $options['_args'][0] ?? null; if (!$username) { error("Username is required"); exit(1); } $user = App\Models\User::where('username', $username)->orWhere('name', $username)->first(); if (!$user) { error("User not found: $username"); exit(1); } $user->suspended = false; $user->save(); success("User '$username' unsuspended"); break; default: error("Unknown user command: $subcommand"); echo "Run 'jabali user --help' for available commands.\n"; exit(1); } } // ============ DOMAIN COMMANDS ============ function handleDomain(string $subcommand, array $options): void { switch ($subcommand) { case 'list': $query = App\Models\Domain::with('user'); if (isset($options['user'])) { $user = App\Models\User::where('username', $options['user'])->orWhere('name', $options['user'])->first(); if ($user) { $query->where('user_id', $user->id); } } $domains = $query->get(); $rows = []; foreach ($domains as $domain) { $rows[] = [ $domain->id, $domain->domain, $domain->user->username ?? $domain->user->name ?? 'N/A', $domain->is_active ? C_GREEN . 'Active' . C_RESET : C_RED . 'Inactive' . C_RESET, $domain->ssl_enabled ? C_GREEN . 'SSL' . C_RESET : C_DIM . 'No SSL' . C_RESET, $domain->created_at->format('Y-m-d'), ]; } table(['ID', 'Domain', 'User', 'Status', 'SSL', 'Created'], $rows); break; case 'create': $domain = $options['_args'][0] ?? prompt("Domain name"); if (!$domain) { error("Domain name is required"); exit(1); } $username = $options['user'] ?? prompt("Username"); $user = App\Models\User::where('username', $username)->orWhere('name', $username)->first(); if (!$user) { error("User not found: $username"); exit(1); } $result = agentSend('domain.create', [ 'username' => $username, 'domain' => $domain, ]); if ($result['success'] ?? false) { $domainModel = App\Models\Domain::create([ 'user_id' => $user->id, 'domain' => $domain, 'is_active' => true, ]); success("Domain '$domain' created (ID: {$domainModel->id})"); } else { error("Failed to create domain: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'show': $domain = $options['_args'][0] ?? null; if (!$domain) { error("Domain name is required"); exit(1); } $domainModel = App\Models\Domain::where('domain', $domain)->with('user')->first(); if (!$domainModel) { error("Domain not found: $domain"); exit(1); } echo "\n" . C_BOLD . "Domain Details" . C_RESET . "\n"; echo str_repeat('─', 40) . "\n"; echo C_CYAN . "ID:" . C_RESET . " {$domainModel->id}\n"; echo C_CYAN . "Domain:" . C_RESET . " {$domainModel->domain}\n"; echo C_CYAN . "User:" . C_RESET . " " . ($domainModel->user->username ?? $domainModel->user->name) . "\n"; echo C_CYAN . "Status:" . C_RESET . " " . ($domainModel->is_active ? 'Active' : 'Inactive') . "\n"; echo C_CYAN . "SSL:" . C_RESET . " " . ($domainModel->ssl_enabled ? 'Enabled' : 'Disabled') . "\n"; echo C_CYAN . "Created:" . C_RESET . " {$domainModel->created_at}\n"; break; case 'delete': $domain = $options['_args'][0] ?? null; if (!$domain) { error("Domain name is required"); exit(1); } $domainModel = App\Models\Domain::where('domain', $domain)->with('user')->first(); if (!$domainModel) { error("Domain not found: $domain"); exit(1); } if (!confirm("Are you sure you want to delete domain '$domain'?", $options)) { info("Operation cancelled"); exit(0); } $username = $domainModel->user->username ?? $domainModel->user->name; $result = agentSend('domain.delete', ['username' => $username, 'domain' => $domain]); $domainModel->delete(); success("Domain '$domain' deleted"); break; case 'enable': $domain = $options['_args'][0] ?? null; if (!$domain) { error("Domain name is required"); exit(1); } $domainModel = App\Models\Domain::where('domain', $domain)->first(); if (!$domainModel) { error("Domain not found: $domain"); exit(1); } $domainModel->is_active = true; $domainModel->save(); success("Domain '$domain' enabled"); break; case 'disable': $domain = $options['_args'][0] ?? null; if (!$domain) { error("Domain name is required"); exit(1); } $domainModel = App\Models\Domain::where('domain', $domain)->first(); if (!$domainModel) { error("Domain not found: $domain"); exit(1); } $domainModel->is_active = false; $domainModel->save(); success("Domain '$domain' disabled"); break; default: error("Unknown domain command: $subcommand"); exit(1); } } // ============ SERVICE COMMANDS ============ function handleService(string $subcommand, array $options): void { $services = [ 'nginx', 'mariadb', 'redis-server', 'postfix', 'dovecot', 'rspamd', 'clamav-daemon', 'named', 'opendkim', 'fail2ban', 'ssh', 'cron' ]; // Add PHP-FPM versions exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $phpServices); foreach ($phpServices as $svc) { if (preg_match('/php[\d.]+-fpm/', basename($svc, '.service'), $m)) { $services[] = $m[0]; } } switch ($subcommand) { case 'list': $result = agentSend('service.list', ['services' => $services]); if (!($result['success'] ?? false)) { error("Failed to get service list: " . ($result['error'] ?? 'Unknown error')); exit(1); } $rows = []; foreach ($result['services'] ?? [] as $name => $status) { $rows[] = [ $name, $status['is_active'] ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET, $status['is_enabled'] ? C_GREEN . 'Enabled' . C_RESET : C_DIM . 'Disabled' . C_RESET, ]; } table(['Service', 'Status', 'Boot'], $rows); break; case 'status': $service = $options['_args'][0] ?? null; if (!$service) { error("Service name is required"); exit(1); } $result = agentSend('service.list', ['services' => [$service]]); if ($result['success'] ?? false) { $status = $result['services'][$service] ?? null; if ($status) { echo C_BOLD . $service . C_RESET . ": "; echo ($status['is_active'] ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET); echo " (Boot: " . ($status['is_enabled'] ? 'Enabled' : 'Disabled') . ")\n"; } else { error("Service not found: $service"); } } break; case 'start': $service = $options['_args'][0] ?? null; if (!$service) { error("Service name is required"); exit(1); } $result = agentSend('service.start', ['service' => $service]); if ($result['success'] ?? false) { success("Service '$service' started"); } else { error("Failed to start '$service': " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'stop': $service = $options['_args'][0] ?? null; if (!$service) { error("Service name is required"); exit(1); } if (!confirm("Are you sure you want to stop '$service'?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('service.stop', ['service' => $service]); if ($result['success'] ?? false) { success("Service '$service' stopped"); } else { error("Failed to stop '$service': " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'restart': $service = $options['_args'][0] ?? null; if (!$service) { error("Service name is required"); exit(1); } $result = agentSend('service.restart', ['service' => $service]); if ($result['success'] ?? false) { success("Service '$service' restarted"); } else { error("Failed to restart '$service': " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'enable': $service = $options['_args'][0] ?? null; if (!$service) { error("Service name is required"); exit(1); } $result = agentSend('service.enable', ['service' => $service]); if ($result['success'] ?? false) { success("Service '$service' enabled on boot"); } else { error("Failed to enable '$service': " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'disable': $service = $options['_args'][0] ?? null; if (!$service) { error("Service name is required"); exit(1); } $result = agentSend('service.disable', ['service' => $service]); if ($result['success'] ?? false) { success("Service '$service' disabled on boot"); } else { error("Failed to disable '$service': " . ($result['error'] ?? 'Unknown error')); exit(1); } break; default: error("Unknown service command: $subcommand"); exit(1); } } // ============ WORDPRESS COMMANDS ============ function handleWordPress(string $subcommand, array $options): void { switch ($subcommand) { case 'list': $username = $options['_args'][0] ?? null; if (!$username) { error("Username is required"); exit(1); } $result = agentSend('wp.list', ['username' => $username]); if (!($result['success'] ?? false)) { error("Failed to list WordPress sites: " . ($result['error'] ?? 'Unknown error')); exit(1); } $rows = []; foreach ($result['sites'] ?? [] as $site) { $rows[] = [ $site['id'], $site['domain'] ?? 'N/A', $site['version'] ?? 'N/A', $site['url'] ?? 'N/A', ]; } table(['ID', 'Domain', 'Version', 'URL'], $rows); break; case 'install': $username = $options['_args'][0] ?? null; $domain = $options['_args'][1] ?? null; if (!$username || !$domain) { error("Usage: jabali wp install "); exit(1); } $title = $options['title'] ?? 'My WordPress Site'; $adminUser = $options['admin'] ?? 'admin'; $adminEmail = $options['email'] ?? prompt("Admin email"); $adminPass = $options['password'] ?? null; info("Installing WordPress for $username on $domain..."); $result = agentSend('wp.install', [ 'username' => $username, 'domain' => $domain, 'title' => $title, 'admin_user' => $adminUser, 'admin_email' => $adminEmail, 'admin_password' => $adminPass, ]); if ($result['success'] ?? false) { success("WordPress installed successfully!"); echo C_CYAN . "URL:" . C_RESET . " https://$domain\n"; echo C_CYAN . "Admin:" . C_RESET . " https://$domain/wp-admin/\n"; echo C_CYAN . "Username:" . C_RESET . " $adminUser\n"; if (isset($result['admin_password'])) { echo C_CYAN . "Password:" . C_RESET . " {$result['admin_password']}\n"; } } else { error("Failed to install WordPress: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'scan': $username = $options['_args'][0] ?? null; if (!$username) { error("Username is required"); exit(1); } info("Scanning for WordPress installations..."); $result = agentSend('wp.scan', ['username' => $username]); if (!($result['success'] ?? false)) { error("Failed to scan: " . ($result['error'] ?? 'Unknown error')); exit(1); } $found = $result['found'] ?? []; if (empty($found)) { info("No untracked WordPress installations found."); } else { success("Found " . count($found) . " WordPress installation(s):"); $rows = []; foreach ($found as $site) { $rows[] = [ $site['path'], $site['version'] ?? 'N/A', $site['site_url'] ?? 'N/A', ]; } table(['Path', 'Version', 'URL'], $rows); } break; case 'import': $username = $options['_args'][0] ?? null; $path = $options['_args'][1] ?? null; if (!$username || !$path) { error("Usage: jabali wp import "); exit(1); } $result = agentSend('wp.import', ['username' => $username, 'path' => $path]); if ($result['success'] ?? false) { success("WordPress site imported: " . ($result['site_id'] ?? '')); } else { error("Failed to import: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'delete': $username = $options['_args'][0] ?? null; $siteId = $options['_args'][1] ?? null; if (!$username || !$siteId) { error("Usage: jabali wp delete "); exit(1); } if (!confirm("Are you sure you want to delete this WordPress site?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('wp.delete', [ 'username' => $username, 'site_id' => $siteId, 'delete_files' => isset($options['files']), 'delete_database' => isset($options['database']), ]); if ($result['success'] ?? false) { success("WordPress site deleted"); } else { error("Failed to delete: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'update': $username = $options['_args'][0] ?? null; $siteId = $options['_args'][1] ?? null; if (!$username || !$siteId) { error("Usage: jabali wp update "); exit(1); } info("Updating WordPress..."); $result = agentSend('wp.update', ['username' => $username, 'site_id' => $siteId]); if ($result['success'] ?? false) { success("WordPress updated to " . ($result['new_version'] ?? 'latest')); } else { error("Failed to update: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; default: error("Unknown WordPress command: $subcommand"); exit(1); } } // ============ DATABASE COMMANDS ============ function handleDatabase(string $subcommand, array $options): void { switch ($subcommand) { case 'list': $username = $options['user'] ?? null; $result = agentSend('mysql.list_databases', ['username' => $username ?? 'admin']); if (!($result['success'] ?? false)) { error("Failed to list databases: " . ($result['error'] ?? 'Unknown error')); exit(1); } $rows = []; foreach ($result['databases'] ?? [] as $db) { $rows[] = [ is_array($db) ? ($db['name'] ?? $db) : $db, ]; } table(['Database'], $rows); break; case 'create': $dbName = $options['_args'][0] ?? prompt("Database name"); if (!$dbName) { error("Database name is required"); exit(1); } $username = $options['user'] ?? 'admin'; $result = agentSend('mysql.create_database', [ 'username' => $username, 'database' => $dbName, ]); if ($result['success'] ?? false) { success("Database '$dbName' created"); } else { error("Failed to create database: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'delete': $dbName = $options['_args'][0] ?? null; if (!$dbName) { error("Database name is required"); exit(1); } if (!confirm("Are you sure you want to delete database '$dbName'?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('mysql.delete_database', ['database' => $dbName]); if ($result['success'] ?? false) { success("Database '$dbName' deleted"); } else { error("Failed to delete database: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'users': $username = $options['user'] ?? 'admin'; $result = agentSend('mysql.list_users', ['username' => $username]); if (!($result['success'] ?? false)) { error("Failed to list users: " . ($result['error'] ?? 'Unknown error')); exit(1); } $rows = []; foreach ($result['users'] ?? [] as $user) { $rows[] = [ is_array($user) ? ($user['user'] ?? $user['name'] ?? $user) : $user, is_array($user) ? ($user['host'] ?? 'localhost') : 'localhost', ]; } table(['User', 'Host'], $rows); break; case 'user-create': $dbUser = $options['_args'][0] ?? prompt("Username"); if (isset($options['password'])) { $password = $options['password']; if ($err = validatePassword($password)) { error($err); exit(1); } } else { $password = promptPassword("Password"); } $host = $options['host'] ?? 'localhost'; $result = agentSend('mysql.create_user', [ 'username' => $dbUser, 'password' => $password, 'host' => $host, ]); if ($result['success'] ?? false) { success("Database user '$dbUser' created"); } else { error("Failed to create user: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'user-delete': $dbUser = $options['_args'][0] ?? null; if (!$dbUser) { error("Username is required"); exit(1); } $host = $options['host'] ?? 'localhost'; if (!confirm("Are you sure you want to delete database user '$dbUser'?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('mysql.delete_user', ['username' => $dbUser, 'host' => $host]); if ($result['success'] ?? false) { success("Database user '$dbUser' deleted"); } else { error("Failed to delete user: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; default: error("Unknown database command: $subcommand"); exit(1); } } // ============ EMAIL COMMANDS ============ function handleEmail(string $subcommand, array $options): void { switch ($subcommand) { case 'list': $query = App\Models\Mailbox::query(); if (isset($options['domain'])) { $query->where('domain', $options['domain']); } $mailboxes = $query->get(); $rows = []; foreach ($mailboxes as $mb) { $rows[] = [ $mb->id, $mb->local_part . '@' . $mb->domain, $mb->quota_mb . ' MB', $mb->is_active ? C_GREEN . 'Active' . C_RESET : C_RED . 'Inactive' . C_RESET, ]; } table(['ID', 'Email', 'Quota', 'Status'], $rows); break; case 'create': $email = $options['_args'][0] ?? prompt("Email address"); if (!$email || !strpos($email, '@')) { error("Valid email address is required"); exit(1); } [$localPart, $domain] = explode('@', $email); if (isset($options['password'])) { $password = $options['password']; if ($err = validatePassword($password)) { error($err); exit(1); } } else { $password = promptPassword("Password"); } $quota = $options['quota'] ?? 1024; $domainModel = App\Models\Domain::where('domain', $domain)->first(); if (!$domainModel) { error("Domain not found: $domain"); exit(1); } $result = agentSend('email.mailbox_create', [ 'username' => $domainModel->user->username ?? $domainModel->user->name, 'domain' => $domain, 'local_part' => $localPart, 'password' => $password, 'quota_mb' => (int)$quota, ]); if ($result['success'] ?? false) { App\Models\Mailbox::create([ 'domain_id' => $domainModel->id, 'local_part' => $localPart, 'domain' => $domain, 'password_hash' => $result['password_hash'] ?? '', 'quota_mb' => (int)$quota, 'is_active' => true, ]); success("Mailbox '$email' created"); } else { error("Failed to create mailbox: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'delete': $email = $options['_args'][0] ?? null; if (!$email) { error("Email address is required"); exit(1); } [$localPart, $domain] = explode('@', $email); $mailbox = App\Models\Mailbox::where('local_part', $localPart)->where('domain', $domain)->first(); if (!$mailbox) { error("Mailbox not found: $email"); exit(1); } if (!confirm("Are you sure you want to delete mailbox '$email'?", $options)) { info("Operation cancelled"); exit(0); } $domainModel = App\Models\Domain::where('domain', $domain)->first(); $username = $domainModel ? ($domainModel->user->username ?? $domainModel->user->name) : 'admin'; agentSend('email.mailbox_delete', [ 'username' => $username, 'domain' => $domain, 'local_part' => $localPart, ]); $mailbox->delete(); success("Mailbox '$email' deleted"); break; case 'password': $email = $options['_args'][0] ?? null; if (!$email) { error("Email address is required"); exit(1); } [$localPart, $domain] = explode('@', $email); $mailbox = App\Models\Mailbox::where('local_part', $localPart)->where('domain', $domain)->first(); if (!$mailbox) { error("Mailbox not found: $email"); exit(1); } if (isset($options['password'])) { $password = $options['password']; if ($err = validatePassword($password)) { error($err); exit(1); } } else { $password = promptPassword("New password", true); } $domainModel = App\Models\Domain::where('domain', $domain)->first(); $username = $domainModel ? ($domainModel->user->username ?? $domainModel->user->name) : 'admin'; $result = agentSend('email.mailbox_change_password', [ 'username' => $username, 'domain' => $domain, 'local_part' => $localPart, 'password' => $password, ]); if ($result['success'] ?? false) { if (isset($result['password_hash'])) { $mailbox->password_hash = $result['password_hash']; $mailbox->save(); } success("Password updated for '$email'"); } else { error("Failed to update password: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'quota': $email = $options['_args'][0] ?? null; $quota = $options['_args'][1] ?? null; if (!$email || !$quota) { error("Usage: jabali mail quota "); exit(1); } [$localPart, $domain] = explode('@', $email); $mailbox = App\Models\Mailbox::where('local_part', $localPart)->where('domain', $domain)->first(); if (!$mailbox) { error("Mailbox not found: $email"); exit(1); } $domainModel = App\Models\Domain::where('domain', $domain)->first(); $username = $domainModel ? ($domainModel->user->username ?? $domainModel->user->name) : 'admin'; $result = agentSend('email.mailbox_set_quota', [ 'username' => $username, 'domain' => $domain, 'local_part' => $localPart, 'quota_mb' => (int)$quota, ]); if ($result['success'] ?? false) { $mailbox->quota_mb = (int)$quota; $mailbox->save(); success("Quota set to {$quota}MB for '$email'"); } else { error("Failed to set quota: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'domains': $domains = App\Models\Domain::where('mail_enabled', true)->get(); $rows = []; foreach ($domains as $d) { $mailboxCount = App\Models\Mailbox::where('domain', $d->domain)->count(); $rows[] = [ $d->domain, $mailboxCount, $d->dkim_enabled ? C_GREEN . 'Yes' . C_RESET : C_DIM . 'No' . C_RESET, ]; } table(['Domain', 'Mailboxes', 'DKIM'], $rows); break; default: error("Unknown email command: $subcommand"); exit(1); } } // ============ BACKUP COMMANDS ============ function handleBackup(string $subcommand, array $options): void { $backupDir = '/var/backups/jabali'; switch ($subcommand) { case 'list': case 'user-list': $username = $options['user'] ?? $options['username'] ?? $options['_args'][0] ?? null; if ($username !== null && !is_string($username)) { error("Username is required"); exit(1); } if (is_string($username)) { $username = trim($username); if ($username === '') { $username = null; } } if ($subcommand === 'user-list' && $username === null) { error("Username is required"); exit(1); } if ($subcommand === 'user-list' || $username !== null) { if (!$username) { error("Username is required"); exit(1); } $path = $options['path'] ?? ''; $result = agentSend('backup.list', [ 'username' => $username, 'path' => $path, ]); if (!($result['success'] ?? false)) { error("Failed to list backups: " . ($result['error'] ?? 'Unknown error')); exit(1); } $backups = $result['backups'] ?? []; if (empty($backups)) { info("No backups found for $username."); exit(0); } $rows = []; foreach ($backups as $backup) { $manifest = $backup['manifest'] ?? []; $createdAt = $backup['created_at'] ?? null; $rows[] = [ $backup['name'] ?? '-', $backup['type'] ?? '-', formatBytes((int) ($backup['size'] ?? 0)), is_array($manifest['domains'] ?? null) ? count($manifest['domains']) : '-', is_array($manifest['databases'] ?? null) ? count($manifest['databases']) : '-', is_array($manifest['mailboxes'] ?? null) ? count($manifest['mailboxes']) : '-', $createdAt ? date('Y-m-d H:i', strtotime($createdAt)) : '-', ]; } table(['Name', 'Type', 'Size', 'Domains', 'DBs', 'Mailboxes', 'Created'], $rows); break; } if (!is_dir($backupDir)) { info("No local backups found."); exit(0); } $files = array_merge( glob("$backupDir/*.tar.gz") ?: [], glob("$backupDir/*", GLOB_ONLYDIR) ?: [] ); $files = array_filter($files, fn($f) => basename($f) !== '.' && basename($f) !== '..'); if (empty($files)) { info("No local backups found."); exit(0); } $rows = []; foreach ($files as $file) { if (is_dir($file)) { $size = trim(shell_exec("du -sh " . escapeshellarg($file) . " 2>/dev/null | cut -f1") ?: '0'); $rows[] = [ basename($file) . '/', $size, date('Y-m-d H:i', filemtime($file)), 'server', ]; } else { $rows[] = [ basename($file), formatBytes((int) filesize($file)), date('Y-m-d H:i', filemtime($file)), 'user', ]; } } table(['Filename', 'Size', 'Created', 'Type'], $rows); break; case 'create': $username = $options['_args'][0] ?? $options['user'] ?? $options['username'] ?? null; if (!is_string($username)) { error("Username is required"); exit(1); } $username = trim($username); if ($username === '') { error("Username is required"); exit(1); } $exists = agentSend('user.exists', ['username' => $username]); if (!($exists['success'] ?? false) || !($exists['exists'] ?? false)) { error("User not found: $username"); exit(1); } $backupType = $options['type'] ?? 'full'; if (!in_array($backupType, ['full', 'incremental'], true)) { error("Invalid backup type: $backupType (use full or incremental)"); exit(1); } $timestamp = date('Y-m-d_His'); $outputPath = $options['output'] ?? $options['path'] ?? null; if ($outputPath !== null && !is_string($outputPath)) { error("Output path is required"); exit(1); } if (is_string($outputPath)) { $outputPath = trim($outputPath); if ($outputPath === '') { $outputPath = null; } } if ($outputPath === null) { $outputPath = $backupType === 'incremental' ? "/home/$username/backups/{$username}_{$timestamp}" : "/home/$username/backups/{$username}_{$timestamp}.tar.gz"; } $params = [ 'username' => $username, 'output_path' => $outputPath, 'backup_type' => $backupType, 'include_files' => !isset($options['no-files']), 'include_databases' => !isset($options['no-databases']), 'include_mailboxes' => !isset($options['no-mailboxes']), 'include_dns' => !isset($options['no-dns']), 'include_ssl' => !isset($options['no-ssl']), 'domains' => parseListOption($options['domains'] ?? null), 'databases' => parseListOption($options['databases'] ?? null), 'mailboxes' => parseListOption($options['mailboxes'] ?? null), ]; if (!empty($options['incremental-base'])) { $params['incremental_base'] = $options['incremental-base']; } info("Creating backup for $username..."); $timeout = (int) ($options['timeout'] ?? 7200); $result = agentSendWithTimeout('backup.create', $params, $timeout); if ($result['success'] ?? false) { $size = formatBytes((int) ($result['size'] ?? 0)); success("Backup created: {$result['path']} ($size)"); if (!empty($result['checksum'])) { info("Checksum: {$result['checksum']}"); } if (isset($result['domains'])) { info("Domains: " . count($result['domains'])); } if (isset($result['databases'])) { info("Databases: " . count($result['databases'])); } if (isset($result['mailboxes'])) { info("Mailboxes: " . count($result['mailboxes'])); } } else { error("Backup failed: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'restore': $backupPath = $options['_args'][0] ?? $options['file'] ?? null; $username = $options['_args'][1] ?? $options['user'] ?? $options['username'] ?? null; if (!is_string($backupPath)) { error("Backup path is required"); exit(1); } $backupPath = trim($backupPath); if ($backupPath === '') { error("Backup path is required"); exit(1); } if ($username !== null && !is_string($username)) { $username = null; } if (is_string($username)) { $username = trim($username); if ($username === '') { $username = null; } } $backupPath = resolveBackupPath($backupPath, $backupDir); if (!file_exists($backupPath) && $username !== null) { $backupPath = resolveBackupPath($backupPath, "/home/$username/backups"); } if (!file_exists($backupPath)) { error("Backup not found: $backupPath"); exit(1); } if ($username === null) { $infoResult = agentSend('backup.get_info', ['backup_path' => $backupPath]); if ($infoResult['success'] ?? false) { $manifest = $infoResult['manifest'] ?? []; $username = $manifest['username'] ?? null; } } if ($username === null) { error("Username is required for restore. Use --user=."); exit(1); } if (!confirm("Restore backup '$backupPath' for user '$username'?", $options)) { info("Operation cancelled"); exit(0); } $params = [ 'username' => $username, 'backup_path' => $backupPath, 'restore_files' => !isset($options['no-files']), 'restore_databases' => !isset($options['no-databases']), 'restore_mailboxes' => !isset($options['no-mailboxes']), 'restore_dns' => !isset($options['no-dns']), 'restore_ssl' => !isset($options['no-ssl']), 'selected_domains' => parseListOption($options['domains'] ?? null), 'selected_databases' => parseListOption($options['databases'] ?? null), 'selected_mailboxes' => parseListOption($options['mailboxes'] ?? null), ]; info("Restoring backup..."); $timeout = (int) ($options['timeout'] ?? 7200); $result = agentSendWithTimeout('backup.restore', $params, $timeout); if ($result['success'] ?? false) { success("Backup restored successfully"); } else { error("Restore failed: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'info': $backupPath = $options['_args'][0] ?? $options['file'] ?? null; $username = $options['user'] ?? $options['username'] ?? null; if (!is_string($backupPath)) { error("Backup path is required"); exit(1); } $backupPath = trim($backupPath); if ($backupPath === '') { error("Backup path is required"); exit(1); } if ($username !== null && !is_string($username)) { $username = null; } if (is_string($username)) { $username = trim($username); if ($username === '') { $username = null; } } $backupPath = resolveBackupPath($backupPath, $backupDir); if (!file_exists($backupPath) && $username !== null) { $backupPath = resolveBackupPath($backupPath, "/home/$username/backups"); } if (!file_exists($backupPath)) { error("Backup not found: $backupPath"); exit(1); } $result = agentSend('backup.get_info', ['backup_path' => $backupPath]); if (!($result['success'] ?? false)) { error("Failed to read backup info: " . ($result['error'] ?? 'Unknown error')); exit(1); } $manifest = $result['manifest'] ?? null; echo "\n" . C_BOLD . "Backup Info" . C_RESET . "\n"; echo str_repeat('─', 60) . "\n"; echo C_CYAN . "Path:" . C_RESET . " $backupPath\n"; echo C_CYAN . "Type:" . C_RESET . " " . ($result['type'] ?? '-') . "\n"; echo C_CYAN . "Size:" . C_RESET . " " . formatBytes((int) ($result['size'] ?? 0)) . "\n"; echo C_CYAN . "Modified:" . C_RESET . " " . ($result['modified_at'] ?? '-') . "\n"; if (is_array($manifest)) { if (!empty($manifest['username'])) { echo C_CYAN . "User:" . C_RESET . " {$manifest['username']}\n"; } if (!empty($manifest['backup_type'])) { echo C_CYAN . "Backup Type:" . C_RESET . " {$manifest['backup_type']}\n"; } if (!empty($manifest['includes']) && is_array($manifest['includes'])) { $includes = array_keys(array_filter($manifest['includes'], fn($value) => $value)); echo C_CYAN . "Includes:" . C_RESET . " " . (empty($includes) ? '-' : implode(', ', $includes)) . "\n"; } if (isset($manifest['domains'])) { echo C_CYAN . "Domains:" . C_RESET . " " . count($manifest['domains']) . "\n"; } if (isset($manifest['databases'])) { echo C_CYAN . "Databases:" . C_RESET . " " . count($manifest['databases']) . "\n"; } if (isset($manifest['mailboxes'])) { echo C_CYAN . "Mailboxes:" . C_RESET . " " . count($manifest['mailboxes']) . "\n"; } } echo "\n"; break; case 'verify': $backupPath = $options['_args'][0] ?? $options['file'] ?? null; $username = $options['user'] ?? $options['username'] ?? null; if (!is_string($backupPath)) { error("Backup path is required"); exit(1); } $backupPath = trim($backupPath); if ($backupPath === '') { error("Backup path is required"); exit(1); } if ($username !== null && !is_string($username)) { $username = null; } if (is_string($username)) { $username = trim($username); if ($username === '') { $username = null; } } $backupPath = resolveBackupPath($backupPath, $backupDir); if (!file_exists($backupPath) && $username !== null) { $backupPath = resolveBackupPath($backupPath, "/home/$username/backups"); } if (!file_exists($backupPath)) { error("Backup not found: $backupPath"); exit(1); } $result = agentSend('backup.verify', ['backup_path' => $backupPath]); if ($result['success'] ?? false) { success("Backup verified successfully"); if (!empty($result['checksum'])) { info("Checksum: {$result['checksum']}"); } } else { error("Backup verification failed"); $issues = $result['issues'] ?? []; foreach ($issues as $issue) { echo " - $issue\n"; } exit(1); } break; case 'delete': $file = $options['_args'][0] ?? null; $username = $options['user'] ?? $options['username'] ?? null; if ($file === null) { error("Backup file or ID is required"); exit(1); } if (is_string($file)) { $file = trim($file); } if ($file === '') { error("Backup file or ID is required"); exit(1); } if (!is_string($file) && !is_numeric($file)) { error("Backup file or ID is required"); exit(1); } if ($username !== null && !is_string($username)) { error("Username is required"); exit(1); } if (is_string($username)) { $username = trim($username); if ($username === '') { $username = null; } } if ($username !== null) { $backupPath = resolveBackupPath($file, "/home/$username/backups"); if (!file_exists($backupPath)) { error("Backup file not found: $backupPath"); exit(1); } if (!confirm("Are you sure you want to delete '$backupPath'?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('backup.delete', [ 'username' => $username, 'backup_path' => $backupPath, ]); if ($result['success'] ?? false) { success("Backup deleted: $backupPath"); } else { error("Delete failed: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; } if (is_numeric($file)) { $backup = App\Models\Backup::find($file); if (!$backup) { error("Backup ID not found: $file"); exit(1); } if (!confirm("Delete backup '{$backup->name}'?", $options)) { info("Operation cancelled"); exit(0); } if ($backup->local_path && file_exists($backup->local_path)) { if (is_dir($backup->local_path)) { exec("rm -rf " . escapeshellarg($backup->local_path)); } else { unlink($backup->local_path); } } if ($backup->remote_path && $backup->destination) { info("Deleting remote backup..."); try { $config = array_merge($backup->destination->config ?? [], ['type' => $backup->destination->type]); agentSend('backup.delete_remote', [ 'remote_path' => $backup->remote_path, 'destination' => $config, ]); } catch (Exception $e) { warning("Failed to delete remote: " . $e->getMessage()); } } $backup->delete(); success("Backup deleted: {$backup->name}"); } else { $file = resolveBackupPath($file, $backupDir); if (!file_exists($file)) { error("Backup file not found: $file"); exit(1); } if (!confirm("Are you sure you want to delete '$file'?", $options)) { info("Operation cancelled"); exit(0); } if (is_dir($file)) { exec("rm -rf " . escapeshellarg($file)); } else { unlink($file); } success("Backup deleted: $file"); } break; // ========== SERVER BACKUPS ========== case 'server': case 'server-backup': $backupType = $options['type'] ?? 'full'; $users = parseListOption($options['users'] ?? null); $destId = $options['destination'] ?? $options['dest'] ?? null; $includeFiles = !isset($options['no-files']); $includeDatabases = !isset($options['no-databases']); $includeMailboxes = !isset($options['no-mailboxes']); $includeDns = !isset($options['no-dns']); info("Creating server backup ($backupType)..."); $timestamp = date('Y-m-d_His'); $outputPath = "$backupDir/$timestamp"; // Get destination if specified $destination = null; if ($destId) { $destination = App\Models\BackupDestination::find($destId); if (!$destination) { error("Destination not found: $destId"); exit(1); } } // Create backup record $backup = App\Models\Backup::create([ 'name' => "CLI Server Backup - " . now()->format('M j, Y H:i'), 'filename' => $timestamp, 'type' => 'server', 'status' => 'running', 'local_path' => $outputPath, 'destination_id' => $destination?->id, 'include_files' => $includeFiles, 'include_databases' => $includeDatabases, 'include_mailboxes' => $includeMailboxes, 'include_dns' => $includeDns, 'users' => $users, 'started_at' => now(), 'metadata' => ['backup_type' => $backupType], ]); // Dispatch the job App\Jobs\RunServerBackup::dispatch($backup->id); success("Server backup queued (ID: {$backup->id})"); info("Monitor progress: jabali backup history"); break; case 'server-list': // List server backups from database $backups = App\Models\Backup::where('type', 'server') ->orderByDesc('created_at') ->limit(20) ->get(); if ($backups->isEmpty()) { info("No server backups found."); exit(0); } $rows = []; foreach ($backups as $backup) { $rows[] = [ $backup->id, $backup->name, formatBytes((int) ($backup->size_bytes ?? 0)), $backup->status, $backup->created_at->format('Y-m-d H:i'), ]; } table(['ID', 'Name', 'Size', 'Status', 'Created'], $rows); break; // ========== BACKUP HISTORY (DATABASE) ========== case 'history': $limit = $options['limit'] ?? 20; $status = $options['status'] ?? null; $type = $options['type'] ?? null; $query = App\Models\Backup::orderByDesc('created_at')->limit($limit); if ($status) { $query->where('status', $status); } if ($type) { $query->where('type', $type); } $backups = $query->get(); if ($backups->isEmpty()) { info("No backups found."); exit(0); } $rows = []; foreach ($backups as $backup) { $location = []; if ($backup->local_path) { $location[] = 'local'; } if ($backup->remote_path) { $location[] = 'remote'; } $rows[] = [ $backup->id, strlen($backup->name) > 30 ? substr($backup->name, 0, 27) . '...' : $backup->name, $backup->type, formatBytes((int) ($backup->size_bytes ?? 0)), $backup->status, implode('+', $location) ?: '-', $backup->created_at->format('Y-m-d H:i'), ]; } table(['ID', 'Name', 'Type', 'Size', 'Status', 'Location', 'Created'], $rows); break; case 'show': $id = $options['_args'][0] ?? null; if (!$id) { error("Backup ID is required"); exit(1); } $backup = App\Models\Backup::with(['destination', 'schedule'])->find($id); if (!$backup) { error("Backup not found: $id"); exit(1); } echo "\n" . C_BOLD . "Backup Details" . C_RESET . "\n"; echo str_repeat('─', 50) . "\n"; echo C_CYAN . "ID:" . C_RESET . " $backup->id\n"; echo C_CYAN . "Name:" . C_RESET . " $backup->name\n"; echo C_CYAN . "Type:" . C_RESET . " $backup->type\n"; echo C_CYAN . "Status:" . C_RESET . " " . statusColor((string) ($backup->status ?? '')) . "\n"; echo C_CYAN . "Size:" . C_RESET . " " . formatBytes((int) ($backup->size_bytes ?? 0)) . "\n"; if ($backup->local_path) { echo C_CYAN . "Local Path:" . C_RESET . " $backup->local_path\n"; } if ($backup->remote_path) { echo C_CYAN . "Remote Path:" . C_RESET . " $backup->remote_path\n"; } if ($backup->destination) { echo C_CYAN . "Destination:" . C_RESET . " {$backup->destination->name} ({$backup->destination->type})\n"; } if ($backup->schedule) { echo C_CYAN . "Schedule:" . C_RESET . " {$backup->schedule->name}\n"; } echo C_CYAN . "Created:" . C_RESET . " " . $backup->created_at->format('Y-m-d H:i:s') . "\n"; if ($backup->started_at) { echo C_CYAN . "Started:" . C_RESET . " " . $backup->started_at->format('Y-m-d H:i:s') . "\n"; } if ($backup->completed_at) { echo C_CYAN . "Completed:" . C_RESET . " " . $backup->completed_at->format('Y-m-d H:i:s') . "\n"; } if ($backup->error_message) { echo C_CYAN . "Error:" . C_RESET . " " . C_RED . $backup->error_message . C_RESET . "\n"; } echo "\n"; break; // ========== SCHEDULES ========== case 'schedules': case 'schedule-list': $schedules = App\Models\BackupSchedule::with('destination') ->orderBy('name') ->get(); if ($schedules->isEmpty()) { info("No backup schedules found."); exit(0); } $rows = []; foreach ($schedules as $schedule) { $rows[] = [ $schedule->id, $schedule->name, $schedule->frequency, $schedule->is_active ? C_GREEN . 'active' . C_RESET : C_DIM . 'inactive' . C_RESET, $schedule->retention_count, $schedule->destination?->name ?? 'local', $schedule->next_run_at?->format('Y-m-d H:i') ?? '-', ]; } table(['ID', 'Name', 'Frequency', 'Status', 'Retention', 'Destination', 'Next Run'], $rows); break; case 'schedule-create': $name = $options['name'] ?? $options['_args'][0] ?? null; if (!$name) { error("Schedule name is required (--name=)"); exit(1); } $schedule = App\Models\BackupSchedule::create([ 'name' => $name, 'frequency' => $options['frequency'] ?? 'daily', 'time' => $options['time'] ?? '02:00', 'day_of_week' => $options['day'] ?? null, 'day_of_month' => $options['date'] ?? null, 'is_active' => true, 'is_server_backup' => ($options['type'] ?? 'server') === 'server', 'retention_count' => (int)($options['retention'] ?? 7), 'destination_id' => $options['destination'] ?? $options['dest'] ?? null, 'include_files' => !isset($options['no-files']), 'include_databases' => !isset($options['no-databases']), 'include_mailboxes' => !isset($options['no-mailboxes']), 'include_dns' => !isset($options['no-dns']), 'metadata' => ['backup_type' => $options['backup-type'] ?? 'full'], ]); $schedule->calculateNextRun(); $schedule->save(); success("Schedule created: {$schedule->name} (ID: {$schedule->id})"); info("Next run: " . $schedule->next_run_at?->format('Y-m-d H:i')); break; case 'schedule-run': $id = $options['_args'][0] ?? null; if (!$id) { error("Schedule ID is required"); exit(1); } $schedule = App\Models\BackupSchedule::find($id); if (!$schedule) { error("Schedule not found: $id"); exit(1); } info("Running schedule: {$schedule->name}..."); // Create backup record and dispatch job $timestamp = now()->format('Y-m-d_His'); $backupType = $schedule->metadata['backup_type'] ?? 'full'; $backup = App\Models\Backup::create([ 'user_id' => $schedule->user_id, 'destination_id' => $schedule->destination_id, 'schedule_id' => $schedule->id, 'name' => "{$schedule->name} - " . now()->format('M j, Y H:i'), 'filename' => $timestamp, 'type' => $schedule->is_server_backup ? 'server' : 'partial', 'include_files' => $schedule->include_files, 'include_databases' => $schedule->include_databases, 'include_mailboxes' => $schedule->include_mailboxes, 'include_dns' => $schedule->include_dns, 'users' => $schedule->users, 'status' => 'pending', 'local_path' => "/var/backups/jabali/$timestamp", 'metadata' => ['backup_type' => $backupType, 'schedule_id' => $schedule->id], ]); App\Jobs\RunServerBackup::dispatch($backup->id); success("Backup queued (ID: {$backup->id})"); break; case 'schedule-enable': $id = $options['_args'][0] ?? null; if (!$id) { error("Schedule ID is required"); exit(1); } $schedule = App\Models\BackupSchedule::find($id); if (!$schedule) { error("Schedule not found: $id"); exit(1); } $schedule->update(['is_active' => true]); $schedule->calculateNextRun(); $schedule->save(); success("Schedule enabled: {$schedule->name}"); break; case 'schedule-disable': $id = $options['_args'][0] ?? null; if (!$id) { error("Schedule ID is required"); exit(1); } $schedule = App\Models\BackupSchedule::find($id); if (!$schedule) { error("Schedule not found: $id"); exit(1); } $schedule->update(['is_active' => false, 'next_run_at' => null]); success("Schedule disabled: {$schedule->name}"); break; case 'schedule-delete': $id = $options['_args'][0] ?? null; if (!$id) { error("Schedule ID is required"); exit(1); } $schedule = App\Models\BackupSchedule::find($id); if (!$schedule) { error("Schedule not found: $id"); exit(1); } if (!confirm("Delete schedule '{$schedule->name}'?", $options)) { info("Operation cancelled"); exit(0); } $schedule->delete(); success("Schedule deleted"); break; // ========== DESTINATIONS ========== case 'destinations': case 'dest-list': $destinations = App\Models\BackupDestination::orderBy('name')->get(); if ($destinations->isEmpty()) { info("No backup destinations configured."); info("Add one with: jabali backup dest-add --type=sftp --name=..."); exit(0); } $rows = []; foreach ($destinations as $dest) { $rows[] = [ $dest->id, $dest->name, $dest->type, $dest->config['host'] ?? $dest->config['path'] ?? '-', $dest->is_active ? C_GREEN . 'active' . C_RESET : C_DIM . 'inactive' . C_RESET, ]; } table(['ID', 'Name', 'Type', 'Host/Path', 'Status'], $rows); break; case 'dest-add': $type = $options['type'] ?? null; $name = $options['name'] ?? null; if (!$type || !$name) { error("Required: --type= --name="); exit(1); } $config = []; switch ($type) { case 'sftp': $config = [ 'host' => $options['host'] ?? null, 'port' => (int)($options['port'] ?? 22), 'username' => $options['user'] ?? $options['username'] ?? null, 'password' => $options['password'] ?? null, 'path' => $options['path'] ?? '/backups', ]; if (!$config['host'] || !$config['username']) { error("SFTP requires: --host= --user= [--password=] [--port=22] [--path=/backups]"); exit(1); } break; case 'nfs': $config = [ 'host' => $options['host'] ?? null, 'path' => $options['path'] ?? null, 'mount_point' => $options['mount'] ?? '/mnt/backup', ]; if (!$config['host'] || !$config['path']) { error("NFS requires: --host= --path= [--mount=/mnt/backup]"); exit(1); } break; case 's3': $config = [ 'bucket' => $options['bucket'] ?? null, 'region' => $options['region'] ?? 'us-east-1', 'access_key' => $options['key'] ?? $options['access-key'] ?? null, 'secret_key' => $options['secret'] ?? $options['secret-key'] ?? null, 'path' => $options['path'] ?? '', ]; if (!$config['bucket'] || !$config['access_key'] || !$config['secret_key']) { error("S3 requires: --bucket= --key= --secret= [--region=us-east-1] [--path=]"); exit(1); } break; default: error("Unknown destination type: $type (use: sftp, nfs, s3)"); exit(1); } $destination = App\Models\BackupDestination::create([ 'name' => $name, 'type' => $type, 'config' => $config, 'is_active' => true, ]); success("Destination created: {$destination->name} (ID: {$destination->id})"); info("Test connection: jabali backup dest-test {$destination->id}"); break; case 'dest-test': $id = $options['_args'][0] ?? null; if (!$id) { error("Destination ID is required"); exit(1); } $destination = App\Models\BackupDestination::find($id); if (!$destination) { error("Destination not found: $id"); exit(1); } info("Testing connection to {$destination->name}..."); $config = array_merge($destination->config ?? [], ['type' => $destination->type]); $result = agentSend('backup.test_destination', ['destination' => $config]); if ($result['success'] ?? false) { success("Connection successful!"); if (!empty($result['message'])) { info($result['message']); } } else { error("Connection failed: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'dest-delete': $id = $options['_args'][0] ?? null; if (!$id) { error("Destination ID is required"); exit(1); } $destination = App\Models\BackupDestination::find($id); if (!$destination) { error("Destination not found: $id"); exit(1); } // Check if any schedules use this destination $scheduleCount = App\Models\BackupSchedule::where('destination_id', $id)->count(); if ($scheduleCount > 0) { error("Cannot delete: $scheduleCount schedule(s) use this destination"); exit(1); } if (!confirm("Delete destination '{$destination->name}'?", $options)) { info("Operation cancelled"); exit(0); } $destination->delete(); success("Destination deleted"); break; // ========== HELP ========== case 'help': case '': echo "\n" . C_BOLD . "Backup Commands" . C_RESET . "\n"; echo str_repeat('─', 60) . "\n\n"; echo C_YELLOW . "Local Backups:" . C_RESET . "\n"; echo " " . C_GREEN . "backup list [--user=]" . C_RESET . " List backups\n"; echo " " . C_GREEN . "backup user-list " . C_RESET . " List user backups\n"; echo " " . C_GREEN . "backup create " . C_RESET . " Create user backup\n"; echo " --type=full|incremental Backup type (default: full)\n"; echo " --output= Output file/dir\n"; echo " --incremental-base= Base backup for incremental\n"; echo " --domains=a,b Include domains\n"; echo " --databases=a,b Include databases\n"; echo " --mailboxes=a,b Include mailboxes\n"; echo " --no-files --no-databases --no-mailboxes --no-dns --no-ssl\n"; echo " " . C_GREEN . "backup restore []" . C_RESET . " Restore backup\n"; echo " --user= User for server backups\n"; echo " --domains=a,b Restore domains\n"; echo " --databases=a,b Restore databases\n"; echo " --mailboxes=a,b Restore mailboxes\n"; echo " --no-files --no-databases --no-mailboxes --no-dns --no-ssl\n"; echo " " . C_GREEN . "backup info " . C_RESET . " Show backup info\n"; echo " " . C_GREEN . "backup verify " . C_RESET . " Verify backup\n"; echo " " . C_GREEN . "backup delete " . C_RESET . " Delete backup\n"; echo " --user= Delete user backup\n\n"; echo C_YELLOW . "Server Backups:" . C_RESET . "\n"; echo " " . C_GREEN . "backup server" . C_RESET . " Create server backup\n"; echo " --type=full|incremental Backup type (default: full)\n"; echo " --users=user1,user2 Specific users only\n"; echo " --dest= Upload to destination\n"; echo " " . C_GREEN . "backup server-list" . C_RESET . " List server backups\n\n"; echo C_YELLOW . "Backup History:" . C_RESET . "\n"; echo " " . C_GREEN . "backup history" . C_RESET . " Show all backups from database\n"; echo " --limit=20 Number of records\n"; echo " --status=completed|failed Filter by status\n"; echo " --type=server|user Filter by type\n"; echo " " . C_GREEN . "backup show " . C_RESET . " Show backup details\n\n"; echo C_YELLOW . "Schedules:" . C_RESET . "\n"; echo " " . C_GREEN . "backup schedules" . C_RESET . " List backup schedules\n"; echo " " . C_GREEN . "backup schedule-create" . C_RESET . " Create a schedule\n"; echo " --name= Schedule name (required)\n"; echo " --frequency=daily|weekly Run frequency\n"; echo " --time=02:00 Run time (24h)\n"; echo " --retention=7 Keep N backups\n"; echo " --dest= Destination ID\n"; echo " --backup-type=full|incremental Backup type\n"; echo " " . C_GREEN . "backup schedule-run " . C_RESET . " Run schedule now\n"; echo " " . C_GREEN . "backup schedule-enable " . C_RESET . " Enable schedule\n"; echo " " . C_GREEN . "backup schedule-disable " . C_RESET . " Disable schedule\n"; echo " " . C_GREEN . "backup schedule-delete " . C_RESET . " Delete schedule\n\n"; echo C_YELLOW . "Destinations:" . C_RESET . "\n"; echo " " . C_GREEN . "backup destinations" . C_RESET . " List destinations\n"; echo " " . C_GREEN . "backup dest-add" . C_RESET . " Add destination\n"; echo " --type=sftp --host= --user=\n"; echo " --type=nfs --host= --path=\n"; echo " --type=s3 --bucket= --key= --secret=\n"; echo " " . C_GREEN . "backup dest-test " . C_RESET . " Test connection\n"; echo " " . C_GREEN . "backup dest-delete " . C_RESET . " Delete destination\n\n"; break; default: error("Unknown backup command: $subcommand"); echo "Run 'jabali backup help' for usage.\n"; exit(1); } } // ============ CPANEL COMMANDS ============ function handleCpanel(string $subcommand, array $options): void { $defaultDir = '/var/backups/jabali/cpanel-migrations'; switch ($subcommand) { case 'analyze': $backupPath = $options['_args'][0] ?? $options['file'] ?? null; if (!is_string($backupPath)) { error("Backup file is required"); exit(1); } $backupPath = trim($backupPath); if ($backupPath === '') { error("Backup file is required"); exit(1); } $backupPath = resolveBackupPath($backupPath, $defaultDir); if (!file_exists($backupPath)) { error("Backup file not found: $backupPath"); exit(1); } info("Analyzing cPanel backup..."); $timeout = (int) ($options['timeout'] ?? 600); $result = agentSendWithTimeout('cpanel.analyze_backup', ['backup_path' => $backupPath], $timeout); if (!($result['success'] ?? false)) { error("Analysis failed: " . ($result['error'] ?? 'Unknown error')); exit(1); } $data = $result['data'] ?? []; printCpanelAnalysis($backupPath, $data); break; case 'restore': $backupPath = $options['_args'][0] ?? $options['file'] ?? null; $username = $options['_args'][1] ?? $options['user'] ?? $options['username'] ?? null; if (!is_string($backupPath)) { error("Backup file is required"); exit(1); } $backupPath = trim($backupPath); if ($backupPath === '') { error("Backup file is required"); exit(1); } if (!is_string($username)) { error("Username is required"); exit(1); } $username = trim($username); if ($username === '') { error("Username is required"); exit(1); } $backupPath = resolveBackupPath($backupPath, $defaultDir); if (!file_exists($backupPath)) { error("Backup file not found: $backupPath"); exit(1); } $panelUser = App\Models\User::where('username', $username)->orWhere('name', $username)->first(); $systemCheck = agentSend('user.exists', ['username' => $username]); $systemExists = (bool) ($systemCheck['exists'] ?? false); if (!$panelUser || !$systemExists) { $missing = []; if (!$systemExists) { $missing[] = 'system user'; } if (!$panelUser) { $missing[] = 'panel user'; } error("Missing " . implode(' and ', $missing) . " for '$username'. Create with: jabali user create $username"); exit(1); } $restoreOptions = [ 'backup_path' => $backupPath, 'username' => $username, 'restore_files' => !isset($options['no-files']), 'restore_databases' => !isset($options['no-databases']), 'restore_emails' => !isset($options['no-emails']), 'restore_ssl' => !isset($options['no-ssl']), ]; if (!empty($options['log'])) { $restoreOptions['log_path'] = $options['log']; } if (isset($options['analyze'])) { info("Analyzing backup..."); $analysisTimeout = (int) ($options['analysis-timeout'] ?? $options['timeout'] ?? 600); $analysisResult = agentSendWithTimeout('cpanel.analyze_backup', ['backup_path' => $backupPath], $analysisTimeout); if (!($analysisResult['success'] ?? false)) { error("Analysis failed: " . ($analysisResult['error'] ?? 'Unknown error')); exit(1); } $analysisData = $analysisResult['data'] ?? []; printCpanelAnalysis($backupPath, $analysisData); $restoreOptions['discovered_data'] = $analysisData; } if (!confirm("Restore backup '$backupPath' into '$username'?", $options)) { info("Operation cancelled"); exit(0); } info("Restoring cPanel backup..."); $timeout = (int) ($options['timeout'] ?? 7200); $result = agentSendWithTimeout('cpanel.restore_backup', $restoreOptions, $timeout); if (!($result['success'] ?? false)) { error("Restore failed: " . ($result['error'] ?? 'Unknown error')); $logEntries = $result['log'] ?? []; if (is_array($logEntries) && !empty($logEntries)) { printCpanelLog($logEntries); } exit(1); } $logEntries = $result['log'] ?? []; if (is_array($logEntries) && !empty($logEntries)) { printCpanelLog($logEntries); } success("cPanel restore completed"); break; case 'fix-permissions': $backupPath = $options['_args'][0] ?? $options['file'] ?? null; if (!is_string($backupPath)) { error("Backup file is required"); exit(1); } $backupPath = trim($backupPath); if ($backupPath === '') { error("Backup file is required"); exit(1); } $backupPath = resolveBackupPath($backupPath, $defaultDir); $result = agentSend('cpanel.fix_backup_permissions', ['backup_path' => $backupPath]); if (!($result['success'] ?? false)) { error("Fix permissions failed: " . ($result['error'] ?? 'Unknown error')); exit(1); } success("Permissions updated for $backupPath"); break; case 'help': case '': echo "\n" . C_BOLD . "cPanel Migration Commands" . C_RESET . "\n"; echo str_repeat('─', 60) . "\n\n"; echo C_YELLOW . "Analyze:" . C_RESET . "\n"; echo " " . C_GREEN . "cpanel analyze " . C_RESET . " Analyze backup contents\n"; echo " --timeout=600 Analysis timeout\n\n"; echo C_YELLOW . "Restore:" . C_RESET . "\n"; echo " " . C_GREEN . "cpanel restore " . C_RESET . " Restore backup\n"; echo " --no-files Skip website files\n"; echo " --no-databases Skip databases\n"; echo " --no-emails Skip mailboxes/forwarders\n"; echo " --no-ssl Skip SSL certificates\n"; echo " --log=/path/to/log.jsonl Append log entries to file\n"; echo " --analyze Run analysis and reuse results\n"; echo " --timeout=7200 Restore timeout\n\n"; echo C_YELLOW . "Maintenance:" . C_RESET . "\n"; echo " " . C_GREEN . "cpanel fix-permissions " . C_RESET . " Fix backup file permissions\n\n"; break; default: error("Unknown cPanel command: $subcommand"); echo "Run 'jabali cpanel help' for usage.\n"; exit(1); } } function formatCpanelStatus(string $status): string { return match ($status) { 'success' => C_GREEN . '✓' . C_RESET, 'error' => C_RED . '✗' . C_RESET, 'warning' => C_YELLOW . '⚠' . C_RESET, 'pending' => C_CYAN . '○' . C_RESET, 'info' => C_CYAN . 'ℹ' . C_RESET, default => $status !== '' ? $status : '-', }; } function printCpanelLog(array $entries): void { foreach ($entries as $entry) { $status = formatCpanelStatus((string) ($entry['status'] ?? '')); $time = $entry['time'] ?? ''; $message = $entry['message'] ?? ''; $timeLabel = $time !== '' ? "[$time] " : ''; echo $status . " " . $timeLabel . $message . "\n"; } } function printCpanelAnalysis(string $backupPath, array $data): void { $domains = $data['domains'] ?? []; $databases = $data['databases'] ?? []; $mailboxes = $data['mailboxes'] ?? []; $forwarders = $data['forwarders'] ?? []; $ssl = $data['ssl_certificates'] ?? []; echo "\n" . C_BOLD . "cPanel Backup Analysis" . C_RESET . "\n"; echo str_repeat('─', 60) . "\n"; echo C_CYAN . "File:" . C_RESET . " $backupPath\n"; if (!empty($data['total_size'])) { echo C_CYAN . "Size:" . C_RESET . " " . formatBytes((int) $data['total_size']) . "\n"; } if (!empty($data['cpanel_username'])) { echo C_CYAN . "cPanel User:" . C_RESET . " {$data['cpanel_username']}\n"; } echo C_CYAN . "Domains:" . C_RESET . " " . count($domains) . "\n"; echo C_CYAN . "Databases:" . C_RESET . " " . count($databases) . "\n"; echo C_CYAN . "Mailboxes:" . C_RESET . " " . count($mailboxes) . "\n"; echo C_CYAN . "Forwarders:" . C_RESET . " " . count($forwarders) . "\n"; echo C_CYAN . "SSL Certificates:" . C_RESET . " " . count($ssl) . "\n\n"; if (!empty($domains)) { $rows = []; foreach ($domains as $domain) { $rows[] = [ $domain['name'] ?? (string) $domain, $domain['type'] ?? '-', ]; } table(['Domain', 'Type'], $rows); echo "\n"; } if (!empty($databases)) { $rows = []; foreach ($databases as $database) { $rows[] = [ $database['name'] ?? (string) $database, ]; } table(['Database'], $rows); echo "\n"; } if (!empty($mailboxes)) { $rows = []; foreach ($mailboxes as $mailbox) { $rows[] = [ $mailbox['email'] ?? (string) $mailbox, ]; } table(['Mailbox'], $rows); echo "\n"; } if (!empty($forwarders)) { $rows = []; foreach ($forwarders as $forwarder) { $rows[] = [ $forwarder['email'] ?? '-', $forwarder['destinations'] ?? ($forwarder['format'] ?? '-'), ]; } table(['Forwarder', 'Destinations'], $rows); echo "\n"; } if (!empty($ssl)) { $rows = []; foreach ($ssl as $cert) { $rows[] = [ $cert['domain'] ?? '-', $cert['keyid'] ?? '-', ]; } table(['Domain', 'Key ID'], $rows); echo "\n"; } } function formatBytes(int $bytes, int $precision = 2): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision) . ' ' . $units[$pow]; } function parseListOption(string|bool|null $value): ?array { if (!is_string($value) || $value === '') { return null; } $items = array_map('trim', explode(',', $value)); $items = array_values(array_filter($items, fn($item) => $item !== '')); return empty($items) ? null : $items; } function resolveBackupPath(string $path, string $defaultDir): string { if (file_exists($path)) { return $path; } $candidate = rtrim($defaultDir, '/') . '/' . ltrim($path, '/'); if (file_exists($candidate)) { return $candidate; } return $path; } function statusColor(string $status): string { return match ($status) { 'completed' => C_GREEN . $status . C_RESET, 'running', 'uploading', 'pending' => C_YELLOW . $status . C_RESET, 'failed' => C_RED . $status . C_RESET, default => $status, }; } // ============ SYSTEM COMMANDS ============ function handleSystem(string $subcommand, array $options): void { switch ($subcommand) { case 'info': $result = agentSend('server.info', []); echo "\n" . C_BOLD . "System Information" . C_RESET . "\n"; echo str_repeat('─', 50) . "\n"; // Hostname echo C_CYAN . "Hostname:" . C_RESET . " " . trim(shell_exec('hostname')) . "\n"; // OS if (file_exists('/etc/os-release')) { $osRelease = parse_ini_file('/etc/os-release'); echo C_CYAN . "OS:" . C_RESET . " " . ($osRelease['PRETTY_NAME'] ?? 'Unknown') . "\n"; } // Kernel echo C_CYAN . "Kernel:" . C_RESET . " " . trim(shell_exec('uname -r')) . "\n"; // Uptime $uptime = trim(shell_exec('uptime -p')); echo C_CYAN . "Uptime:" . C_RESET . " $uptime\n"; // CPU $cpuInfo = shell_exec("grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2"); $cpuCores = trim(shell_exec("nproc")); echo C_CYAN . "CPU:" . C_RESET . " " . trim($cpuInfo) . " ($cpuCores cores)\n"; // Load $load = sys_getloadavg(); echo C_CYAN . "Load:" . C_RESET . " " . implode(', ', array_map(fn($l) => number_format($l, 2), $load)) . "\n"; // Memory $memInfo = shell_exec('free -m | grep Mem'); if (preg_match('/Mem:\s+(\d+)\s+(\d+)/', $memInfo, $m)) { $total = $m[1]; $used = $m[2]; $pct = round($used / $total * 100); echo C_CYAN . "Memory:" . C_RESET . " {$used}MB / {$total}MB ({$pct}%)\n"; } // Disk $diskTotal = disk_total_space('/'); $diskFree = disk_free_space('/'); $diskUsed = $diskTotal - $diskFree; $diskPct = round($diskUsed / $diskTotal * 100); echo C_CYAN . "Disk:" . C_RESET . " " . round($diskUsed / 1024 / 1024 / 1024, 1) . "GB / " . round($diskTotal / 1024 / 1024 / 1024, 1) . "GB ({$diskPct}%)\n"; // PHP echo C_CYAN . "PHP:" . C_RESET . " " . PHP_VERSION . "\n"; // Laravel echo C_CYAN . "Laravel:" . C_RESET . " " . app()->version() . "\n"; break; case 'status': handleService('list', $options); break; case 'hostname': $newHostname = $options['_args'][0] ?? null; if ($newHostname) { $result = agentSend('server.set_hostname', ['hostname' => $newHostname]); if ($result['success'] ?? false) { success("Hostname set to '$newHostname'"); } else { error("Failed to set hostname: " . ($result['error'] ?? 'Unknown error')); exit(1); } } else { echo trim(shell_exec('hostname')) . "\n"; } break; case 'disk': echo shell_exec('df -h'); break; case 'memory': echo shell_exec('free -h'); break; default: error("Unknown system command: $subcommand"); exit(1); } } // ============ AGENT COMMANDS ============ function handleAgent(string $subcommand, array $options): void { $agentScript = JABALI_ROOT . '/bin/jabali-agent'; $pidFile = '/var/run/jabali/agent.pid'; switch ($subcommand) { case 'status': if (file_exists(AGENT_SOCKET)) { $result = agentSend('ping', []); if ($result['success'] ?? false) { success("Agent is running (version: " . ($result['version'] ?? 'unknown') . ")"); } else { warning("Agent socket exists but not responding"); } } else { error("Agent is not running"); } break; case 'start': if (file_exists(AGENT_SOCKET)) { $result = agentSend('ping', []); if ($result['success'] ?? false) { info("Agent is already running"); exit(0); } } info("Starting agent..."); exec("nohup /usr/bin/php $agentScript > /dev/null 2>&1 &"); sleep(2); if (file_exists(AGENT_SOCKET)) { success("Agent started"); } else { error("Failed to start agent"); exit(1); } break; case 'stop': if (file_exists($pidFile)) { $pid = trim(file_get_contents($pidFile)); if ($pid && posix_kill((int)$pid, 15)) { sleep(1); success("Agent stopped"); } else { exec("pkill -f jabali-agent"); success("Agent stopped"); } } else { exec("pkill -f jabali-agent"); info("Agent stopped"); } break; case 'restart': handleAgent('stop', $options); sleep(1); handleAgent('start', $options); break; case 'ping': $result = agentSend('ping', []); if ($result['success'] ?? false) { success("Pong! Agent version: " . ($result['version'] ?? 'unknown')); } else { error("Agent not responding: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'log': $lines = $options['lines'] ?? 50; $logFile = '/var/log/jabali/agent.log'; if (file_exists($logFile)) { passthru("tail -n $lines " . escapeshellarg($logFile)); } else { error("Log file not found: $logFile"); } break; default: error("Unknown agent command: $subcommand"); exit(1); } } // ============ PHP COMMANDS ============ function handlePhp(string $subcommand, array $options): void { switch ($subcommand) { case 'list': $result = agentSend('php.list_versions', []); if (!($result['success'] ?? false)) { // Fallback to local detection exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $services); $rows = []; foreach ($services as $svc) { if (preg_match('/php([\d.]+)-fpm/', $svc, $m)) { $version = $m[1]; exec("systemctl is-active php{$version}-fpm 2>/dev/null", $active); $isActive = (trim($active[0] ?? '') === 'active'); $rows[] = [ $version, $isActive ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET, ]; } } table(['Version', 'Status'], $rows); } else { $rows = []; foreach ($result['versions'] ?? [] as $v) { $rows[] = [ is_array($v) ? $v['version'] : $v, is_array($v) ? ($v['active'] ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET) : '', ]; } table(['Version', 'Status'], $rows); } break; case 'install': $version = $options['_args'][0] ?? null; if (!$version) { error("PHP version is required (e.g., 8.2)"); exit(1); } info("Installing PHP $version..."); $result = agentSend('php.install', ['version' => $version]); if ($result['success'] ?? false) { success("PHP $version installed"); } else { error("Failed to install PHP $version: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'uninstall': $version = $options['_args'][0] ?? null; if (!$version) { error("PHP version is required"); exit(1); } if (!confirm("Are you sure you want to uninstall PHP $version?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('php.uninstall', ['version' => $version]); if ($result['success'] ?? false) { success("PHP $version uninstalled"); } else { error("Failed to uninstall: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'default': $version = $options['_args'][0] ?? null; if ($version) { $result = agentSend('php.set_default', ['version' => $version]); if ($result['success'] ?? false) { success("Default PHP version set to $version"); } else { error("Failed to set default: " . ($result['error'] ?? 'Unknown error')); exit(1); } } else { $current = trim(shell_exec('php -v | head -1 | cut -d" " -f2 | cut -d"." -f1,2')); echo "Current default: PHP $current\n"; } break; case 'status': exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $services); foreach ($services as $svc) { if (preg_match('/php([\d.]+)-fpm/', $svc, $m)) { $service = "php{$m[1]}-fpm"; passthru("systemctl status $service --no-pager -l | head -15"); echo "\n"; } } break; default: error("Unknown PHP command: $subcommand"); exit(1); } } // ============ FIREWALL COMMANDS ============ function handleFirewall(string $subcommand, array $options): void { switch ($subcommand) { case 'status': $result = agentSend('ufw.status', []); if ($result['success'] ?? false) { echo C_BOLD . "Firewall Status: " . C_RESET; echo ($result['enabled'] ?? false) ? C_GREEN . "Active" . C_RESET : C_RED . "Inactive" . C_RESET; echo "\n"; } else { passthru('ufw status'); } break; case 'enable': $result = agentSend('ufw.enable', []); if ($result['success'] ?? false) { success("Firewall enabled"); } else { error("Failed to enable firewall: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'disable': if (!confirm("Are you sure you want to disable the firewall?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('ufw.disable', []); if ($result['success'] ?? false) { success("Firewall disabled"); } else { error("Failed to disable firewall: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'rules': $result = agentSend('ufw.list_rules', []); if ($result['success'] ?? false) { $rows = []; foreach ($result['rules'] ?? [] as $rule) { $rows[] = [ $rule['number'] ?? '', $rule['to'] ?? '', $rule['action'] ?? '', $rule['from'] ?? '', ]; } table(['#', 'To', 'Action', 'From'], $rows); } else { passthru('ufw status numbered'); } break; case 'allow': $port = $options['_args'][0] ?? null; if (!$port) { error("Port is required"); exit(1); } $result = agentSend('ufw.allow_port', ['port' => $port]); if ($result['success'] ?? false) { success("Allowed port $port"); } else { error("Failed to allow port: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'deny': $port = $options['_args'][0] ?? null; if (!$port) { error("Port is required"); exit(1); } $result = agentSend('ufw.deny_port', ['port' => $port]); if ($result['success'] ?? false) { success("Denied port $port"); } else { error("Failed to deny port: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'delete': $rule = $options['_args'][0] ?? null; if (!$rule) { error("Rule number is required"); exit(1); } if (!confirm("Are you sure you want to delete rule #$rule?", $options)) { info("Operation cancelled"); exit(0); } $result = agentSend('ufw.delete_rule', ['rule' => $rule]); if ($result['success'] ?? false) { success("Rule deleted"); } else { error("Failed to delete rule: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; default: error("Unknown firewall command: $subcommand"); exit(1); } } function handleSsl(string $subcommand, array $options): void { switch ($subcommand) { case 'check': $domain = $options['_args'][0] ?? null; info("Checking SSL certificates..."); $cmd = 'php /var/www/jabali/artisan jabali:ssl-check'; if ($domain) { $cmd .= ' --domain=' . escapeshellarg($domain); } if ($options['issue-only'] ?? false) { $cmd .= ' --issue-only'; } if ($options['renew-only'] ?? false) { $cmd .= ' --renew-only'; } passthru($cmd, $exitCode); exit($exitCode); case 'issue': $domain = $options['_args'][0] ?? null; if (!$domain) { error("Domain is required"); echo "Usage: jabali ssl issue \n"; exit(1); } info("Issuing SSL certificate for $domain..."); $result = agentSend('ssl.issue', [ 'domain' => $domain, 'force' => $options['force'] ?? false, ]); if ($result['success'] ?? false) { success("SSL certificate issued for $domain"); if (!empty($result['valid_to'])) { echo " Expires: " . $result['valid_to'] . "\n"; } } else { error("Failed to issue SSL: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'renew': $domain = $options['_args'][0] ?? null; if (!$domain) { error("Domain is required"); echo "Usage: jabali ssl renew \n"; exit(1); } info("Renewing SSL certificate for $domain..."); $result = agentSend('ssl.renew', ['domain' => $domain]); if ($result['success'] ?? false) { success("SSL certificate renewed for $domain"); if (!empty($result['valid_to'])) { echo " New expiry: " . $result['valid_to'] . "\n"; } } else { error("Failed to renew SSL: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'status': $domain = $options['_args'][0] ?? null; if (!$domain) { error("Domain is required"); echo "Usage: jabali ssl status \n"; exit(1); } $result = agentSend('ssl.info', ['domain' => $domain]); if ($result['success'] ?? false) { echo C_BOLD . "SSL Certificate Status for $domain" . C_RESET . "\n\n"; echo " Status: " . (($result['valid'] ?? false) ? C_GREEN . "Valid" : C_RED . "Invalid") . C_RESET . "\n"; echo " Issuer: " . ($result['issuer'] ?? 'Unknown') . "\n"; echo " From: " . ($result['valid_from'] ?? 'Unknown') . "\n"; echo " To: " . ($result['valid_to'] ?? 'Unknown') . "\n"; if (!empty($result['days_remaining'])) { $days = $result['days_remaining']; $color = $days > 30 ? C_GREEN : ($days > 7 ? C_YELLOW : C_RED); echo " Days: " . $color . $days . " days remaining" . C_RESET . "\n"; } } else { error("Failed to get SSL status: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case 'list': info("Listing SSL certificates..."); $result = agentSend('ssl.list', []); if ($result['success'] ?? false) { $rows = []; foreach ($result['certificates'] ?? [] as $cert) { $status = ($cert['valid'] ?? false) ? C_GREEN . 'Valid' . C_RESET : C_RED . 'Invalid' . C_RESET; $days = $cert['days_remaining'] ?? 0; $daysColor = $days > 30 ? C_GREEN : ($days > 7 ? C_YELLOW : C_RED); $rows[] = [ $cert['domain'] ?? '', $status, $cert['issuer'] ?? '', $cert['valid_to'] ?? '', $daysColor . $days . C_RESET, ]; } table(['Domain', 'Status', 'Issuer', 'Expires', 'Days'], $rows); } else { error("Failed to list SSL certificates: " . ($result['error'] ?? 'Unknown error')); exit(1); } break; case '': case 'help': echo C_BOLD . "SSL Certificate Management" . C_RESET . "\n\n"; echo "Usage: jabali ssl [options]\n\n"; echo C_YELLOW . "Commands:" . C_RESET . "\n"; echo " " . C_GREEN . "check" . C_RESET . " Check all domains and issue/renew as needed\n"; echo " " . C_GREEN . "check " . C_RESET . " Check specific domain\n"; echo " " . C_GREEN . "issue " . C_RESET . " Issue SSL certificate for domain\n"; echo " " . C_GREEN . "renew " . C_RESET . " Renew SSL certificate for domain\n"; echo " " . C_GREEN . "status " . C_RESET . " Show SSL status for domain\n"; echo " " . C_GREEN . "list" . C_RESET . " List all SSL certificates\n\n"; echo C_YELLOW . "Options:" . C_RESET . "\n"; echo " " . C_GREEN . "--issue-only" . C_RESET . " Only issue new certificates (with check)\n"; echo " " . C_GREEN . "--renew-only" . C_RESET . " Only renew expiring certificates (with check)\n"; echo " " . C_GREEN . "--force" . C_RESET . " Force issue even if certificate exists\n"; break; default: error("Unknown ssl command: $subcommand"); echo "Run 'jabali ssl help' for usage information.\n"; exit(1); } }