Files
jabali-panel/bin/jabali
2026-02-02 03:11:45 +02:00

3281 lines
128 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Jabali CLI - Command Line Interface for Jabali Panel
*/
define('JABALI_ROOT', dirname(__DIR__));
define('AGENT_SOCKET', '/var/run/jabali/agent.sock');
// Read version from VERSION file
$versionFile = JABALI_ROOT . '/VERSION';
$version = '1.0.1';
if (file_exists($versionFile)) {
$content = file_get_contents($versionFile);
if (preg_match('/VERSION=(.+)/', $content, $m)) {
$version = trim($m[1]);
}
}
define('VERSION', $version);
// Colors
define('C_RESET', "\033[0m");
define('C_RED', "\033[31m");
define('C_GREEN', "\033[32m");
define('C_YELLOW', "\033[33m");
define('C_BLUE', "\033[34m");
define('C_MAGENTA', "\033[35m");
define('C_CYAN', "\033[36m");
define('C_WHITE', "\033[37m");
define('C_BOLD', "\033[1m");
define('C_DIM', "\033[2m");
chdir(JABALI_ROOT);
// Bootstrap Laravel for database access
require 'vendor/autoload.php';
$app = require 'bootstrap/app.php';
$kernel = $app->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 <command> <subcommand> [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 <username>" . C_RESET . " Create a new user\n";
echo " " . C_GREEN . "user show <username>" . C_RESET . " Show user details\n";
echo " " . C_GREEN . "user delete <username>" . C_RESET . " Delete a user\n";
echo " " . C_GREEN . "user password <username>" . C_RESET . " Change user password\n";
echo " " . C_GREEN . "user suspend <username>" . C_RESET . " Suspend a user\n";
echo " " . C_GREEN . "user unsuspend <username>" . C_RESET . " Unsuspend a user\n\n";
echo C_YELLOW . C_BOLD . "Domain Management:" . C_RESET . "\n";
echo " " . C_GREEN . "domain list [--user=<user>]" . C_RESET . " List domains\n";
echo " " . C_GREEN . "domain create <domain>" . C_RESET . " Create a domain\n";
echo " " . C_GREEN . "domain show <domain>" . C_RESET . " Show domain details\n";
echo " " . C_GREEN . "domain delete <domain>" . C_RESET . " Delete a domain\n";
echo " " . C_GREEN . "domain enable <domain>" . C_RESET . " Enable a domain\n";
echo " " . C_GREEN . "domain disable <domain>" . 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 <command> <subcommand> [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 <user>" . C_RESET . " - Create user", C_GREEN . "domain create <d>" . C_RESET . " - Create domain");
$printRow(C_GREEN . "user show <user>" . C_RESET . " - Show details", C_GREEN . "domain show <d>" . C_RESET . " - Show details");
$printRow(C_GREEN . "user delete <user>" . C_RESET . " - Delete user", C_GREEN . "domain delete <d>" . C_RESET . " - Delete domain");
$printRow(C_GREEN . "user password <user>" . C_RESET . " - Set password", C_GREEN . "domain enable <d>" . C_RESET . " - Enable domain");
$printRow(C_GREEN . "user suspend <user>" . C_RESET . " - Suspend", C_GREEN . "domain disable <d>" . C_RESET . " - Disable domain");
$printRow(C_GREEN . "user unsuspend <user>" . 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 <name>" . C_RESET . " - Create database", C_GREEN . "mail create <email>" . C_RESET . " - Create mailbox");
$printRow(C_GREEN . "db delete <name>" . C_RESET . " - Delete database", C_GREEN . "mail delete <email>" . C_RESET . " - Delete mailbox");
$printRow(C_GREEN . "db users" . C_RESET . " - List db users", C_GREEN . "mail password <email>" . C_RESET . " - Set password");
$printRow(C_GREEN . "db user-create <name>" . C_RESET . " - Create db user", C_GREEN . "mail quota <email> <MB>" . C_RESET . " - Set quota");
$printRow(C_GREEN . "db user-delete <name>" . 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 <user>" . C_RESET . " - List WP sites");
$printRow(C_GREEN . "service status <name>" . C_RESET . " - Show status", C_GREEN . "wp install <user> <d>" . C_RESET . " - Install WP");
$printRow(C_GREEN . "service start <name>" . C_RESET . " - Start service", C_GREEN . "wp scan <user>" . C_RESET . " - Scan for WP sites");
$printRow(C_GREEN . "service stop <name>" . C_RESET . " - Stop service", C_GREEN . "wp import <user> <path>" . C_RESET . " - Import WP");
$printRow(C_GREEN . "service restart <name>" . C_RESET . " - Restart", C_GREEN . "wp delete <user> <id>" . C_RESET . " - Delete WP site");
$printRow(C_GREEN . "service enable <name>" . C_RESET . " - Enable on boot", C_GREEN . "wp update <user> <id>" . C_RESET . " - Update WP");
$printRow(C_GREEN . "service disable <name>" . 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 <ver>" . C_RESET . " - Install PHP", C_GREEN . "firewall enable" . C_RESET . " - Enable firewall");
$printRow(C_GREEN . "php uninstall <ver>" . 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 <port>" . C_RESET . " - Allow port");
$printRow("", C_GREEN . "firewall deny <port>" . C_RESET . " - Deny port");
$printRow("", C_GREEN . "firewall delete <rule>" . 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=<u>]" . C_RESET . " - List backups", C_GREEN . "ssl check" . C_RESET . " - Check/issue/renew certs");
$printRow(C_GREEN . "backup create <user>" . C_RESET . " - Create user backup", C_GREEN . "ssl issue <domain>" . C_RESET . " - Issue certificate");
$printRow(C_GREEN . "backup restore <path>" . C_RESET . " - Restore backup", C_GREEN . "ssl renew <domain>" . C_RESET . " - Renew certificate");
$printRow(C_GREEN . "backup info <path>" . C_RESET . " - Show backup info", C_GREEN . "ssl status <domain>" . C_RESET . " - Show cert status");
$printRow(C_GREEN . "backup verify <path>" . 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 <file>" . C_RESET . " - Analyze backup", C_GREEN . "cpanel restore <file> <user>" . C_RESET . " - Restore backup");
$printRow(C_GREEN . "cpanel fix-permissions <file>" . 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 <username> <domain>");
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 <username> <path>");
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 <username> <site_id>");
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 <username> <site_id>");
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 <email> <size_mb>");
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=<username>.");
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=<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=<sftp|nfs|s3> --name=<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=<host> --user=<user> [--password=<pass>] [--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=<host> --path=<remote-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=<name> --key=<access-key> --secret=<secret-key> [--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=<user>]" . C_RESET . " List backups\n";
echo " " . C_GREEN . "backup user-list <user>" . C_RESET . " List user backups\n";
echo " " . C_GREEN . "backup create <user>" . C_RESET . " Create user backup\n";
echo " --type=full|incremental Backup type (default: full)\n";
echo " --output=<path> Output file/dir\n";
echo " --incremental-base=<path> 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 <path> [<user>]" . C_RESET . " Restore backup\n";
echo " --user=<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 <path>" . C_RESET . " Show backup info\n";
echo " " . C_GREEN . "backup verify <path>" . C_RESET . " Verify backup\n";
echo " " . C_GREEN . "backup delete <file|id>" . C_RESET . " Delete backup\n";
echo " --user=<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=<id> 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 <id>" . 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=<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=<id> Destination ID\n";
echo " --backup-type=full|incremental Backup type\n";
echo " " . C_GREEN . "backup schedule-run <id>" . C_RESET . " Run schedule now\n";
echo " " . C_GREEN . "backup schedule-enable <id>" . C_RESET . " Enable schedule\n";
echo " " . C_GREEN . "backup schedule-disable <id>" . C_RESET . " Disable schedule\n";
echo " " . C_GREEN . "backup schedule-delete <id>" . 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=<host> --user=<user>\n";
echo " --type=nfs --host=<host> --path=<path>\n";
echo " --type=s3 --bucket=<name> --key=<key> --secret=<secret>\n";
echo " " . C_GREEN . "backup dest-test <id>" . C_RESET . " Test connection\n";
echo " " . C_GREEN . "backup dest-delete <id>" . 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 <file>" . 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 <file> <user>" . 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 <file>" . 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 <domain>\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 <domain>\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 <domain>\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 <command> [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 <domain>" . C_RESET . " Check specific domain\n";
echo " " . C_GREEN . "issue <domain>" . C_RESET . " Issue SSL certificate for domain\n";
echo " " . C_GREEN . "renew <domain>" . C_RESET . " Renew SSL certificate for domain\n";
echo " " . C_GREEN . "status <domain>" . 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);
}
}