562 lines
15 KiB
TypeScript
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
|
|
`);
|
|
}
|