UNPKG

@nodedaemon/core

Version:

Production-ready Node.js process manager with zero external dependencies

459 lines 18.5 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeDaemonCLI = void 0; const child_process_1 = require("child_process"); const path_1 = require("path"); const NodeDaemonCore_1 = require("../daemon/NodeDaemonCore"); const IPCClient_1 = require("./IPCClient"); const CommandParser_1 = require("./CommandParser"); const Formatter_1 = require("./Formatter"); class NodeDaemonCLI { client = new IPCClient_1.IPCClient(); parser = new CommandParser_1.CommandParser(); async run(argv) { try { const parsed = this.parser.parse(argv); switch (parsed.command) { case 'daemon': await this.handleDaemon(parsed.options); break; case 'start': await this.handleStart(parsed.options); break; case 'stop': await this.handleStop(parsed.options); break; case 'restart': await this.handleRestart(parsed.options); break; case 'list': await this.handleList(parsed.options); break; case 'status': await this.handleStatus(parsed.options); break; case 'logs': await this.handleLogs(parsed.options); break; case 'shutdown': await this.handleShutdown(parsed.options); break; case 'webui': await this.handleWebUI(parsed.options); break; case 'help': this.showHelp(); break; case 'version': this.showVersion(); break; default: throw new Error(`Unknown command: ${parsed.command}`); } } catch (error) { this.handleError(error); } } async handleDaemon(options) { if (options.detach) { // Start daemon in background const daemonScript = (0, path_1.resolve)(__dirname, '../daemon/index.js'); const child = (0, child_process_1.spawn)(process.execPath, [daemonScript], { detached: true, stdio: 'ignore', env: { ...process.env, ...options } }); child.unref(); // Wait a moment to verify daemon started setTimeout(async () => { try { const client = new IPCClient_1.IPCClient(); await client.connect(); await client.disconnect(); console.log(Formatter_1.Formatter.formatSuccess(`Daemon started with PID ${child.pid}`)); } catch (error) { console.log(Formatter_1.Formatter.formatError('Daemon failed to start properly')); console.log(Formatter_1.Formatter.formatInfo('Check daemon logs for details')); } }, 1000); } else { // Start daemon in foreground console.log(Formatter_1.Formatter.formatInfo('Starting NodeDaemon...')); const daemon = new NodeDaemonCore_1.NodeDaemonCore(); daemon.on('started', () => { console.log(Formatter_1.Formatter.formatSuccess('NodeDaemon started successfully')); }); daemon.on('shutdown', () => { console.log(Formatter_1.Formatter.formatInfo('NodeDaemon shutdown complete')); process.exit(0); }); await daemon.start(); // Keep process running process.on('SIGINT', () => { console.log(Formatter_1.Formatter.formatInfo('Shutting down daemon...')); daemon.gracefulShutdown('SIGINT'); }); process.on('SIGTERM', () => { console.log(Formatter_1.Formatter.formatInfo('Shutting down daemon...')); daemon.gracefulShutdown('SIGTERM'); }); } } async handleStart(options) { // Auto-start daemon if not running and not explicitly disabled if (!options.noDaemon) { await this.ensureDaemonRunning(); } try { await this.client.connect(); const result = await this.client.start(options.config); console.log(Formatter_1.Formatter.formatSuccess(`Process started successfully`)); console.log(`Process ID: ${result.processId}`); console.log(`Name: ${options.config?.name || 'unnamed'}`); console.log(`Script: ${options.config?.script}`); if (options.config?.instances && options.config.instances > 1) { console.log(`Instances: ${options.config.instances}`); } if (options.config?.watch) { console.log(`Watch: enabled`); } } finally { this.client.disconnect(); } } async handleStop(options) { try { await this.client.connect(); const stopOptions = { force: options.force }; if (options.byName) { stopOptions.name = options.target; } else { stopOptions.processId = options.target; } await this.client.stop(stopOptions); console.log(Formatter_1.Formatter.formatSuccess(`Process stopped successfully`)); } finally { this.client.disconnect(); } } async handleRestart(options) { try { await this.client.connect(); const restartOptions = {}; if (options.byName) { restartOptions.name = options.target; } else { restartOptions.processId = options.target; } if (options.graceful) { restartOptions.graceful = true; } await this.client.restart(restartOptions); console.log(Formatter_1.Formatter.formatSuccess(options.graceful ? 'Process gracefully reloaded successfully' : 'Process restarted successfully')); } finally { this.client.disconnect(); } } async handleList(options) { try { await this.client.connect(); const result = await this.client.list(); if (options.json) { console.log(JSON.stringify(result, null, 2)); return; } console.log(Formatter_1.Formatter.formatProcessList(result.processes)); if (result.stats) { console.log('\nDaemon Stats:'); console.log(` Uptime: ${Formatter_1.Formatter.formatUptime(result.stats.uptime || 0)}`); console.log(` Total Processes: ${result.stats.processCount}`); console.log(` Running: ${result.stats.runningProcesses}`); console.log(` Stopped: ${result.stats.stoppedProcesses}`); console.log(` Errored: ${result.stats.erroredProcesses}`); } if (options.watch) { console.log(Formatter_1.Formatter.formatInfo('Watching for changes... (Press Ctrl+C to exit)')); setInterval(async () => { try { const updated = await this.client.list(); console.clear(); console.log(Formatter_1.Formatter.formatProcessList(updated.processes)); } catch (error) { console.error(Formatter_1.Formatter.formatError('Failed to update list')); } }, 2000); } } finally { if (!options.watch) { this.client.disconnect(); } } } async handleStatus(options) { try { await this.client.connect(); let statusOptions; if (options.target) { statusOptions = {}; if (options.byName) { statusOptions.name = options.target; } else { statusOptions.processId = options.target; } } const result = await this.client.status(statusOptions); if (options.json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.daemon) { // Daemon status console.log(Formatter_1.Formatter.formatDaemonStatus(result)); } else { // Process status console.log(Formatter_1.Formatter.formatProcessStatus(result)); } } finally { this.client.disconnect(); } } async handleLogs(options) { try { await this.client.connect(); const logsOptions = { lines: options.lines }; if (options.target) { if (options.byName) { logsOptions.name = options.target; } else { logsOptions.processId = options.target; } } const result = await this.client.logs(logsOptions); if (options.json) { console.log(JSON.stringify(result, null, 2)); return; } if (result.logs && result.logs.length > 0) { console.log(Formatter_1.Formatter.formatLogs(result.logs)); } else { console.log(Formatter_1.Formatter.formatInfo('No logs available')); } if (options.follow) { console.log(Formatter_1.Formatter.formatInfo('Following logs... (Press Ctrl+C to exit)')); let lastTimestamp = Math.max(...result.logs.map((log) => log.timestamp), 0); setInterval(async () => { try { const updated = await this.client.logs(logsOptions); const newLogs = updated.logs.filter((log) => log.timestamp > lastTimestamp); if (newLogs.length > 0) { console.log(Formatter_1.Formatter.formatLogs(newLogs)); lastTimestamp = Math.max(...newLogs.map((log) => log.timestamp)); } } catch (error) { console.error(Formatter_1.Formatter.formatError('Failed to fetch logs')); } }, 1000); } } finally { if (!options.follow) { this.client.disconnect(); } } } async handleShutdown(options) { try { await this.client.connect(); if (options.force) { console.log(Formatter_1.Formatter.formatWarning('Force shutdown requested')); } else { console.log(Formatter_1.Formatter.formatInfo('Gracefully shutting down daemon...')); } await this.client.shutdown(); console.log(Formatter_1.Formatter.formatSuccess('Daemon shutdown initiated')); } catch (error) { if (error.message.includes('not running')) { console.log(Formatter_1.Formatter.formatInfo('Daemon is not running')); } else { throw error; } } finally { this.client.disconnect(); } } async handleWebUI(options) { try { await this.client.connect(); switch (options.action) { case 'start': const config = { enabled: true }; if (options.port) { config.port = parseInt(options.port); if (isNaN(config.port)) { throw new Error('Invalid port number'); } } if (options.host) { config.host = options.host; } if (options.username && options.password) { config.auth = { username: options.username, password: options.password }; } else if (options.username || options.password) { throw new Error('Both username and password are required for authentication'); } const startResult = await this.client.sendMessage('webui', { action: 'set', config }); if (startResult.success) { const webConfig = startResult.data; console.log(Formatter_1.Formatter.formatSuccess('Web UI started')); console.log(Formatter_1.Formatter.formatInfo(`URL: http://${webConfig.host}:${webConfig.port}`)); if (webConfig.auth) { console.log(Formatter_1.Formatter.formatWarning('Authentication enabled')); } } else { throw new Error(startResult.error || 'Failed to start Web UI'); } break; case 'stop': const stopResult = await this.client.sendMessage('webui', { action: 'set', config: { enabled: false } }); if (stopResult.success) { console.log(Formatter_1.Formatter.formatSuccess('Web UI stopped')); } else { throw new Error(stopResult.error || 'Failed to stop Web UI'); } break; case 'status': const statusResult = await this.client.sendMessage('webui', { action: 'status' }); if (statusResult.success) { const config = statusResult.data; if (config && config.enabled) { console.log(Formatter_1.Formatter.formatSuccess('Web UI is running')); console.log(Formatter_1.Formatter.formatInfo(`URL: http://${config.host}:${config.port}`)); if (config.auth) { console.log(Formatter_1.Formatter.formatInfo('Authentication: Enabled')); } else { console.log(Formatter_1.Formatter.formatInfo('Authentication: Disabled')); } } else { console.log(Formatter_1.Formatter.formatInfo('Web UI is not running')); } } else { throw new Error(statusResult.error || 'Failed to get Web UI status'); } break; default: throw new Error(`Unknown webui action: ${options.action}`); } } catch (error) { if (error.message.includes('not running')) { console.log(Formatter_1.Formatter.formatError('Daemon is not running')); console.log(Formatter_1.Formatter.formatInfo('Start the daemon first: nodedaemon daemon')); } else { throw error; } } finally { this.client.disconnect(); } } async ensureDaemonRunning() { try { await this.client.connect(); await this.client.ping(); this.client.disconnect(); } catch (error) { if (error.message.includes('not running')) { console.log(Formatter_1.Formatter.formatInfo('Starting daemon...')); // Start daemon in background const daemonScript = (0, path_1.resolve)(__dirname, '../daemon/index.js'); const child = (0, child_process_1.spawn)(process.execPath, [daemonScript], { detached: true, stdio: 'ignore' }); child.unref(); // Wait for daemon to be ready let retries = 0; const maxRetries = 30; while (retries < maxRetries) { try { await new Promise(resolve => setTimeout(resolve, 1000)); await this.client.connect(); await this.client.ping(); this.client.disconnect(); console.log(Formatter_1.Formatter.formatSuccess('Daemon started successfully')); return; } catch { retries++; } } throw new Error('Failed to start daemon - timeout'); } else { throw error; } } } showHelp() { console.log(this.parser.getHelp()); } showVersion() { console.log(`NodeDaemon v${this.parser.getVersion()}`); } handleError(error) { const message = error?.message || String(error); console.error(Formatter_1.Formatter.formatError(message)); if (process.env.NODE_ENV === 'development') { console.error(error?.stack || error); } process.exit(1); } } exports.NodeDaemonCLI = NodeDaemonCLI; // Entry point if (require.main === module) { const cli = new NodeDaemonCLI(); cli.run(process.argv).catch((error) => { const message = error?.message || String(error); console.error(Formatter_1.Formatter.formatError(`Unexpected error: ${message}`)); process.exit(1); }); } //# sourceMappingURL=index.js.map