@nodedaemon/core
Version:
Production-ready Node.js process manager with zero external dependencies
459 lines • 18.5 kB
JavaScript
;
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