/** * CLI Router for Onebox */ import { logger } from './logging.ts'; import { projectInfo } from './info.ts'; import { getErrorMessage } from './utils/error.ts'; import { Onebox } from './classes/onebox.ts'; import { OneboxDaemon } from './classes/daemon.ts'; import { OneboxSystemd } from './classes/systemd.ts'; export async function runCli(): Promise { const args = Deno.args; if (args.length === 0 || args.includes('--help') || args.includes('-h')) { printHelp(); return; } if (args.includes('--version') || args.includes('-v')) { console.log(`${projectInfo.name} v${projectInfo.version}`); return; } const command = args[0]; const subcommand = args[1]; try { // === LIGHTWEIGHT COMMANDS (no init()) === if (command === 'systemd') { await handleSystemdCommand(subcommand, args.slice(2)); return; } if (command === 'upgrade') { await handleUpgradeCommand(); return; } // === HEAVY COMMANDS (require full init()) === // Server command has special handling (doesn't shut down) if (command === 'server') { const onebox = new Onebox(); await onebox.init(); await handleServerCommand(onebox, args.slice(1)); // Server command runs forever (or until Ctrl+C), so this never returns return; } // Initialize Onebox const onebox = new Onebox(); await onebox.init(); // Route commands switch (command) { case 'service': await handleServiceCommand(onebox, subcommand, args.slice(2)); break; case 'registry': await handleRegistryCommand(onebox, subcommand, args.slice(2)); break; case 'dns': await handleDnsCommand(onebox, subcommand, args.slice(2)); break; case 'ssl': await handleSslCommand(onebox, subcommand, args.slice(2)); break; case 'nginx': await handleNginxCommand(onebox, subcommand, args.slice(2)); break; case 'config': await handleConfigCommand(onebox, subcommand, args.slice(2)); break; case 'status': await handleStatusCommand(onebox); break; default: logger.error(`Unknown command: ${command}`); printHelp(); Deno.exit(1); } // Cleanup await onebox.shutdown(); } catch (error) { logger.error(getErrorMessage(error)); Deno.exit(1); } } // Service commands async function handleServiceCommand(onebox: Onebox, subcommand: string, args: string[]) { switch (subcommand) { case 'add': { const name = args[0]; const image = getArg(args, '--image'); const domain = getArg(args, '--domain'); const port = parseInt(getArg(args, '--port') || '80', 10); const envArgs = args.filter((a) => a.startsWith('--env=')).map((a) => a.slice(6)); const envVars: Record = {}; for (const env of envArgs) { const [key, value] = env.split('='); envVars[key] = value; } await onebox.services.deployService({ name, image, port, domain, envVars }); break; } case 'remove': await onebox.services.removeService(args[0]); break; case 'start': await onebox.services.startService(args[0]); break; case 'stop': await onebox.services.stopService(args[0]); break; case 'restart': await onebox.services.restartService(args[0]); break; case 'list': { const services = onebox.services.listServices(); logger.table( ['Name', 'Image', 'Status', 'Domain', 'Port'], services.map((s) => [s.name, s.image, s.status, s.domain || '-', s.port.toString()]) ); break; } case 'logs': { const logs = await onebox.services.getServiceLogs(args[0]); console.log(logs); break; } default: logger.error(`Unknown service subcommand: ${subcommand}`); } } // Registry commands async function handleRegistryCommand(onebox: Onebox, subcommand: string, args: string[]) { switch (subcommand) { case 'add': { const url = getArg(args, '--url'); const username = getArg(args, '--username'); const password = getArg(args, '--password'); await onebox.registries.addRegistry(url, username, password); break; } case 'remove': await onebox.registries.removeRegistry(getArg(args, '--url')); break; case 'list': { const registries = onebox.registries.listRegistries(); logger.table( ['URL', 'Username'], registries.map((r) => [r.url, r.username]) ); break; } default: logger.error(`Unknown registry subcommand: ${subcommand}`); } } // DNS commands async function handleDnsCommand(onebox: Onebox, subcommand: string, args: string[]) { switch (subcommand) { case 'add': await onebox.dns.addDNSRecord(args[0]); break; case 'remove': await onebox.dns.removeDNSRecord(args[0]); break; case 'list': { const records = onebox.dns.listDNSRecords(); logger.table( ['Domain', 'Type', 'Value'], records.map((r) => [r.domain, r.type, r.value]) ); break; } case 'sync': await onebox.dns.syncFromCloudflare(); break; default: logger.error(`Unknown dns subcommand: ${subcommand}`); } } // SSL commands async function handleSslCommand(onebox: Onebox, subcommand: string, args: string[]) { switch (subcommand) { case 'renew': if (args[0]) { await onebox.ssl.renewCertificate(args[0]); } else { await onebox.ssl.renewExpiring(); } break; case 'list': { const certs = onebox.ssl.listCertificates(); logger.table( ['Domain', 'Expiry', 'Issuer'], certs.map((c) => [c.domain, new Date(c.expiryDate).toISOString(), c.issuer]) ); break; } case 'force-renew': await onebox.ssl.renewCertificate(args[0]); break; default: logger.error(`Unknown ssl subcommand: ${subcommand}`); } } // Reverse proxy commands (formerly nginx commands) async function handleNginxCommand(onebox: Onebox, subcommand: string, _args: string[]) { switch (subcommand) { case 'reload': // Reload routes and certificates await onebox.reverseProxy.reloadRoutes(); await onebox.reverseProxy.reloadCertificates(); logger.success('Reverse proxy configuration reloaded'); break; case 'test': // Verify reverse proxy is running const proxyStatus = onebox.reverseProxy.getStatus(); if (proxyStatus.http.running || proxyStatus.https.running) { logger.success('Reverse proxy is running'); logger.info(`HTTP: ${proxyStatus.http.running ? 'active' : 'inactive'} (port ${proxyStatus.http.port})`); logger.info(`HTTPS: ${proxyStatus.https.running ? 'active' : 'inactive'} (port ${proxyStatus.https.port})`); logger.info(`Routes: ${proxyStatus.routes}, Certificates: ${proxyStatus.https.certificates}`); } else { logger.error('Reverse proxy is not running'); } break; case 'status': { const status = onebox.reverseProxy.getStatus(); logger.info(`Reverse proxy status:`); logger.info(` HTTP: ${status.http.running ? 'running' : 'stopped'} (port ${status.http.port})`); logger.info(` HTTPS: ${status.https.running ? 'running' : 'stopped'} (port ${status.https.port})`); logger.info(` Routes: ${status.routes}`); logger.info(` Certificates: ${status.https.certificates}`); break; } default: logger.error(`Unknown nginx subcommand: ${subcommand}`); } } // Server command async function handleServerCommand(onebox: Onebox, args: string[]) { const ephemeral = args.includes('--ephemeral'); const port = parseInt(getArg(args, '--port') || '3000', 10); const monitor = args.includes('--monitor') || ephemeral; // ephemeral includes monitoring if (ephemeral) { // Ensure no daemon is running try { await OneboxDaemon.ensureNoDaemon(); } catch (error) { logger.error('Cannot start in ephemeral mode: Daemon is already running'); logger.info('Stop the daemon first: onebox systemd stop'); logger.info('Or run without --ephemeral to use the existing daemon'); Deno.exit(1); } } logger.info('Starting Onebox server...'); // Start OpsServer (serves new UI + TypedRequest API) await onebox.opsServer.start(port); // Start monitoring if requested if (monitor) { logger.info('Starting monitoring loop...'); onebox.daemon.startMonitoring(); } logger.success(`Onebox server running on http://localhost:${port}`); if (ephemeral) { logger.info('Running in ephemeral mode - Press Ctrl+C to stop'); } else { logger.info('Press Ctrl+C to stop'); } // Setup signal handlers const shutdown = async () => { logger.info('Shutting down...'); if (monitor) { onebox.daemon.stopMonitoring(); } await onebox.opsServer.stop(); await onebox.shutdown(); Deno.exit(0); }; Deno.addSignalListener('SIGINT', shutdown); Deno.addSignalListener('SIGTERM', shutdown); // Keep alive while (true) { await new Promise(resolve => setTimeout(resolve, 1000)); } } // Systemd service commands (lightweight — no Onebox init) async function handleSystemdCommand(subcommand: string, _args: string[]) { const systemd = new OneboxSystemd(); switch (subcommand) { case 'enable': await systemd.enable(); break; case 'disable': await systemd.disable(); break; case 'start': await systemd.start(); break; case 'stop': await systemd.stop(); break; case 'status': { const status = await systemd.getStatus(); logger.info(`Service status: ${status}`); break; } case 'logs': await systemd.showLogs(); break; case 'start-daemon': { // This is what systemd's ExecStart calls — full init + daemon loop const onebox = new Onebox(); await onebox.init(); await onebox.daemon.start(); // start() blocks (keepAlive loop) until SIGTERM/SIGINT break; } default: logger.error(`Unknown systemd subcommand: ${subcommand}`); logger.info('Available: enable, disable, start, stop, status, logs'); } } // Config commands async function handleConfigCommand(onebox: Onebox, subcommand: string, args: string[]) { switch (subcommand) { case 'show': { const settings = onebox.database.getAllSettings(); logger.table( ['Key', 'Value'], Object.entries(settings).map(([k, v]) => [k, v]) ); break; } case 'set': onebox.database.setSetting(args[0], args[1]); logger.success(`Setting ${args[0]} updated`); break; default: logger.error(`Unknown config subcommand: ${subcommand}`); } } // Status command async function handleStatusCommand(onebox: Onebox) { const status = await onebox.getSystemStatus(); console.log(JSON.stringify(status, null, 2)); } // Upgrade command - self-update onebox to latest version async function handleUpgradeCommand(): Promise { // Check if running as root if (Deno.uid() !== 0) { logger.error('This command must be run as root to upgrade Onebox.'); logger.info('Try: sudo onebox upgrade'); Deno.exit(1); } logger.info('Checking for updates...'); try { // Get current version const currentVersion = projectInfo.version; // Fetch latest version from Gitea API const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/onebox/releases/latest'; const curlCmd = new Deno.Command('curl', { args: ['-sSL', apiUrl], stdout: 'piped', stderr: 'piped', }); const curlResult = await curlCmd.output(); const response = new TextDecoder().decode(curlResult.stdout); const release = JSON.parse(response); const latestVersion = release.tag_name as string; // e.g., "v1.11.0" // Normalize versions for comparison (ensure both have "v" prefix) const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`; const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`; console.log(` Current version: ${normalizedCurrent}`); console.log(` Latest version: ${normalizedLatest}`); console.log(''); // Compare normalized versions if (normalizedCurrent === normalizedLatest) { logger.success('Already up to date!'); return; } logger.info(`New version available: ${latestVersion}`); logger.info('Downloading and installing...'); console.log(''); // Download and run the install script const installUrl = 'https://code.foss.global/serve.zone/onebox/raw/branch/main/install.sh'; const installCmd = new Deno.Command('bash', { args: ['-c', `curl -sSL ${installUrl} | bash`], stdin: 'inherit', stdout: 'inherit', stderr: 'inherit', }); const installResult = await installCmd.output(); if (!installResult.success) { logger.error('Upgrade failed'); Deno.exit(1); } console.log(''); logger.success(`Upgraded to ${latestVersion}`); } catch (error) { logger.error(`Upgrade failed: ${getErrorMessage(error)}`); Deno.exit(1); } } // Helpers function getArg(args: string[], flag: string): string { const arg = args.find((a) => a.startsWith(`${flag}=`)); return arg ? arg.split('=')[1] : ''; } function printHelp(): void { console.log(` Onebox v${projectInfo.version} - Self-hosted container platform Usage: onebox [options] Commands: server [--ephemeral] [--port ] [--monitor] Start HTTP server --ephemeral: Run in foreground (development mode, checks no daemon running) --port: HTTP server port (default: 3000) --monitor: Enable monitoring loop (included with --ephemeral) service add --image [--domain ] [--port ] [--env KEY=VALUE] service remove service start service stop service restart service list service logs registry add --url --username --password registry remove --url registry list dns add dns remove dns list dns sync ssl renew [domain] ssl list ssl force-renew nginx reload nginx test nginx status systemd enable Install and enable systemd service systemd disable Stop, disable, and remove systemd service systemd start Start onebox via systemctl systemd stop Stop onebox via systemctl systemd status Show systemd service status systemd logs Follow service logs (journalctl) config show config set status upgrade Upgrade Onebox to the latest version (requires root) Options: --help, -h Show this help message --version, -v Show version --debug Enable debug logging Development Workflow: deno task dev # Start ephemeral server with monitoring onebox service add ... # In another terminal Production Workflow: onebox systemd enable # Install and enable systemd service onebox systemd start # Start via systemctl onebox service add ... # CLI manages services Examples: onebox server --ephemeral # Start dev server onebox service add myapp --image nginx:latest --domain app.example.com --port 80 onebox registry add --url registry.example.com --username user --password pass onebox systemd enable onebox systemd start `); }