Files
onebox/ts/cli.ts
Juergen Kunz 8ebd677478 feat: Implement platform service providers for MinIO and MongoDB
- Added base interface and abstract class for platform service providers.
- Created MinIOProvider class for S3-compatible storage with deployment, provisioning, and deprovisioning functionalities.
- Implemented MongoDBProvider class for MongoDB service with similar capabilities.
- Introduced error handling utilities for better error management.
- Developed TokensComponent for managing registry tokens in the UI, including creation, deletion, and display of tokens.
2025-11-25 04:20:19 +00:00

466 lines
12 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';
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 {
// 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;
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 daemon stop');
logger.info('Or run without --ephemeral to use the existing daemon');
Deno.exit(1);
}
}
logger.info('Starting Onebox server...');
// Start HTTP server
await onebox.httpServer.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.httpServer.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));
}
// 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
daemon install
daemon start
daemon stop
daemon logs
daemon status
config show
config set <key> <value>
status
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
`);
}