Files
onebox/ts/cli.ts

562 lines
15 KiB
TypeScript

/**
* 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<void> {
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<string, string> = {};
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<void> {
// 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 <command> [options]
Commands:
server [--ephemeral] [--port <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 <name> --image <image> [--domain <domain>] [--port <port>] [--env KEY=VALUE]
service remove <name>
service start <name>
service stop <name>
service restart <name>
service list
service logs <name>
registry add --url <url> --username <user> --password <pass>
registry remove --url <url>
registry list
dns add <domain>
dns remove <domain>
dns list
dns sync
ssl renew [domain]
ssl list
ssl force-renew <domain>
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 <key> <value>
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
`);
}