/** * 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'; 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 { // 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 'daemon': await handleDaemonCommand(onebox, subcommand, args.slice(2)); break; case 'config': await handleConfigCommand(onebox, subcommand, args.slice(2)); break; case 'status': await handleStatusCommand(onebox); break; case 'upgrade': await handleUpgradeCommand(); 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 daemon 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)); } } // Daemon commands async function handleDaemonCommand(onebox: Onebox, subcommand: string, _args: string[]) { switch (subcommand) { case 'install': await onebox.daemon.installService(); break; case 'start': await onebox.startDaemon(); break; case 'stop': await onebox.stopDaemon(); break; case 'logs': { const command = new Deno.Command('journalctl', { args: ['-u', 'smartdaemon_onebox', '-f'], stdout: 'inherit', stderr: 'inherit', }); await command.output(); break; } case 'status': { const status = await onebox.daemon.getServiceStatus(); logger.info(`Daemon status: ${status}`); break; } default: logger.error(`Unknown daemon subcommand: ${subcommand}`); } } // 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 daemon install daemon start daemon stop daemon logs daemon status 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 daemon install # Install systemd service onebox daemon start # Start daemon onebox service add ... # CLI uses daemon 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 daemon install onebox daemon start `); }