import { execSync } from 'child_process'; import { Nupst } from './nupst.js'; /** * Class for handling CLI commands * Provides interface between user commands and the application */ export class NupstCli { private readonly nupst: Nupst; /** * Create a new CLI handler */ constructor() { this.nupst = new Nupst(); } /** * Parse command line arguments and execute the appropriate command * @param args Command line arguments (process.argv) */ public async parseAndExecute(args: string[]): Promise { // Extract debug flag from any position const debugOptions = this.extractDebugOptions(args); if (debugOptions.debugMode) { console.log('Debug mode enabled'); // Enable debug mode in the SNMP client this.nupst.getSnmp().enableDebug(); } // Get the command (default to help if none provided) const command = args[2] || 'help'; // Route to the appropriate command handler await this.executeCommand(command, debugOptions.debugMode); } /** * Extract and remove debug options from args * @param args Command line arguments * @returns Object with debug flags and cleaned args */ private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } { const debugMode = args.includes('--debug') || args.includes('-d'); // Remove debug flags from args const cleanedArgs = args.filter(arg => arg !== '--debug' && arg !== '-d'); return { debugMode, cleanedArgs }; } /** * Execute the command with the given arguments * @param command Command to execute * @param debugMode Whether debug mode is enabled */ private async executeCommand(command: string, debugMode: boolean): Promise { switch (command) { case 'enable': await this.enable(); break; case 'daemon-start': await this.daemonStart(debugMode); break; case 'logs': await this.logs(); break; case 'stop': await this.stop(); break; case 'start': await this.start(); break; case 'status': await this.status(); break; case 'disable': await this.disable(); break; case 'setup': await this.setup(); break; case 'test': await this.test(debugMode); break; case 'help': default: this.showHelp(); break; } } /** * Enable the service (requires root) */ private async enable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.nupst.getSystemd().install(); console.log('NUPST service has been installed. Use "nupst start" to start the service.'); } /** * Start the daemon directly * @param debugMode Whether to enable debug mode */ private async daemonStart(debugMode: boolean = false): Promise { console.log('Starting NUPST daemon...'); try { // Enable debug mode for SNMP if requested if (debugMode) { this.nupst.getSnmp().enableDebug(); console.log('SNMP debug mode enabled'); } await this.nupst.getDaemon().start(); } catch (error) { // Error is already logged and process.exit is called in daemon.start() // No need to handle it here } } /** * Show logs of the systemd service */ private async logs(): Promise { try { const logs = execSync('journalctl -u nupst.service -n 50 -f').toString(); console.log(logs); } catch (error) { console.error('Failed to retrieve logs:', error); process.exit(1); } } /** * Stop the systemd service */ private async stop(): Promise { await this.nupst.getSystemd().stop(); } /** * Start the systemd service */ private async start(): Promise { try { await this.nupst.getSystemd().start(); } catch (error) { // Error will be displayed by systemd.start() process.exit(1); } } /** * Show status of the systemd service and UPS */ private async status(): Promise { await this.nupst.getSystemd().getStatus(); } /** * Disable the service (requires root) */ private async disable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.nupst.getSystemd().disable(); } /** * Check if the user has root access * @param errorMessage Error message to display if not root */ private checkRootAccess(errorMessage: string): void { if (process.getuid && process.getuid() !== 0) { console.error(errorMessage); process.exit(1); } } /** * Test the current configuration by connecting to the UPS * @param debugMode Whether to enable debug mode */ private async test(debugMode: boolean = false): Promise { try { // Debug mode is now handled in parseAndExecute if (debugMode) { console.log('┌─ Debug Mode ─────────────────────────────┐'); console.log('│ SNMP debugging enabled - detailed logs will be shown'); console.log('└──────────────────────────────────────────┘'); } // Try to load the configuration try { await this.nupst.getDaemon().loadConfig(); } catch (error) { console.error('┌─ Configuration Error ─────────────────────┐'); console.error('│ No configuration found.'); console.error('│ Please run \'nupst setup\' first to create a configuration.'); console.error('└──────────────────────────────────────────┘'); return; } // Get current configuration const config = this.nupst.getDaemon().getConfig(); this.displayTestConfig(config); await this.testConnection(config); } catch (error) { console.error(`Test failed: ${error.message}`); } } /** * Display the configuration for testing * @param config Current configuration */ private displayTestConfig(config: any): void { console.log('┌─ Testing Configuration ─────────────────────┐'); console.log('│ SNMP Settings:'); console.log(`│ Host: ${config.snmp.host}`); console.log(`│ Port: ${config.snmp.port}`); console.log(`│ Version: ${config.snmp.version}`); console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`); if (config.snmp.version === 1 || config.snmp.version === 2) { console.log(`│ Community: ${config.snmp.community}`); } else if (config.snmp.version === 3) { console.log(`│ Security Level: ${config.snmp.securityLevel}`); console.log(`│ Username: ${config.snmp.username}`); // Show auth and privacy details based on security level if (config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv') { console.log(`│ Auth Protocol: ${config.snmp.authProtocol || 'None'}`); } if (config.snmp.securityLevel === 'authPriv') { console.log(`│ Privacy Protocol: ${config.snmp.privProtocol || 'None'}`); } // Show timeout value console.log(`│ Timeout: ${config.snmp.timeout / 1000} seconds`); } // Show OIDs if custom model is selected if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) { console.log('│ Custom OIDs:'); console.log(`│ Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`); console.log(`│ Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`); console.log(`│ Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`); } console.log('│ Thresholds:'); console.log(`│ Battery: ${config.thresholds.battery}%`); console.log(`│ Runtime: ${config.thresholds.runtime} minutes`); console.log(`│ Check Interval: ${config.checkInterval / 1000} seconds`); console.log('└──────────────────────────────────────────┘'); } /** * Test connection to the UPS * @param config Current configuration */ private async testConnection(config: any): Promise { console.log('\nTesting connection to UPS...'); try { // Create a test config with a short timeout const testConfig = { ...config.snmp, timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing }; const status = await this.nupst.getSnmp().getUpsStatus(testConfig); console.log('┌─ Connection Successful! ─────────────────┐'); console.log('│ UPS Status:'); console.log(`│ Power Status: ${status.powerStatus}`); console.log(`│ Battery Capacity: ${status.batteryCapacity}%`); console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`); console.log('└──────────────────────────────────────────┘'); // Check status against thresholds if on battery if (status.powerStatus === 'onBattery') { this.analyzeThresholds(status, config); } } catch (error) { console.error('┌─ Connection Failed! ───────────────────────┐'); console.error(`│ Error: ${error.message}`); console.error('└──────────────────────────────────────────┘'); console.log('\nPlease check your settings and run \'nupst setup\' to reconfigure.'); } } /** * Analyze UPS status against thresholds * @param status UPS status * @param config Current configuration */ private analyzeThresholds(status: any, config: any): void { console.log('┌─ Threshold Analysis ───────────────────────┐'); if (status.batteryCapacity < config.thresholds.battery) { console.log('│ ⚠️ WARNING: Battery capacity below threshold'); console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`); console.log('│ System would initiate shutdown'); } else { console.log('│ ✓ Battery capacity above threshold'); console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`); } if (status.batteryRuntime < config.thresholds.runtime) { console.log('│ ⚠️ WARNING: Runtime below threshold'); console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`); console.log('│ System would initiate shutdown'); } else { console.log('│ ✓ Runtime above threshold'); console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`); } console.log('└──────────────────────────────────────────┘'); } /** * Display help message */ private showHelp(): void { console.log(` NUPST - Node.js UPS Shutdown Tool Usage: nupst enable - Install and enable the systemd service (requires root) nupst disable - Stop and uninstall the systemd service (requires root) nupst daemon-start - Start the daemon process directly nupst logs - Show logs of the systemd service nupst stop - Stop the systemd service nupst start - Start the systemd service nupst status - Show status of the systemd service and UPS status nupst setup - Run the interactive setup to configure SNMP settings nupst test - Test the current configuration by connecting to the UPS nupst help - Show this help message Options: --debug, -d - Enable debug mode for detailed SNMP logging (Example: nupst test --debug) `); } /** * Interactive setup for configuring SNMP settings */ private async setup(): Promise { try { // Import readline module (ESM style) const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Helper function to prompt for input const prompt = (question: string): Promise => { return new Promise((resolve) => { rl.question(question, (answer: string) => { resolve(answer); }); }); }; try { await this.runSetupProcess(prompt); } finally { rl.close(); } } catch (error) { console.error('Setup error:', error.message); } } /** * Run the interactive setup process * @param prompt Function to prompt for user input */ private async runSetupProcess(prompt: (question: string) => Promise): Promise { console.log('\nNUPST Interactive Setup'); console.log('======================\n'); console.log('This will guide you through configuring your UPS SNMP settings.\n'); // Try to load existing config if available let config; try { await this.nupst.getDaemon().loadConfig(); config = this.nupst.getDaemon().getConfig(); } catch (error) { // If config doesn't exist, use default config config = this.nupst.getDaemon().getConfig(); console.log('No existing configuration found. Creating a new configuration.'); } // Gather SNMP settings config = await this.gatherSnmpSettings(config, prompt); // Gather threshold settings config = await this.gatherThresholdSettings(config, prompt); // Gather UPS model settings config = await this.gatherUpsModelSettings(config, prompt); // Save the configuration await this.nupst.getDaemon().saveConfig(config); this.displayConfigSummary(config); // Test the connection if requested await this.optionallyTestConnection(config, prompt); console.log('\nSetup complete!'); await this.optionallyEnableService(prompt); } /** * Gather SNMP settings * @param config Current configuration * @param prompt Function to prompt for user input * @returns Updated configuration */ private async gatherSnmpSettings(config: any, prompt: (question: string) => Promise): Promise { // SNMP IP Address const defaultHost = config.snmp.host; const host = await prompt(`UPS IP Address [${defaultHost}]: `); config.snmp.host = host.trim() || defaultHost; // SNMP Port const defaultPort = config.snmp.port; const portInput = await prompt(`SNMP Port [${defaultPort}]: `); const port = parseInt(portInput, 10); config.snmp.port = (portInput.trim() && !isNaN(port)) ? port : defaultPort; // SNMP Version const defaultVersion = config.snmp.version; console.log('\nSNMP Version:'); console.log(' 1) SNMPv1'); console.log(' 2) SNMPv2c'); console.log(' 3) SNMPv3 (with security features)'); const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `); const version = parseInt(versionInput, 10); config.snmp.version = (versionInput.trim() && (version === 1 || version === 2 || version === 3)) ? version : defaultVersion; if (config.snmp.version === 1 || config.snmp.version === 2) { // SNMP Community String (for v1/v2c) const defaultCommunity = config.snmp.community || 'public'; const community = await prompt(`SNMP Community String [${defaultCommunity}]: `); config.snmp.community = community.trim() || defaultCommunity; } else if (config.snmp.version === 3) { // SNMP v3 settings config = await this.gatherSnmpV3Settings(config, prompt); } return config; } /** * Gather SNMPv3 specific settings * @param config Current configuration * @param prompt Function to prompt for user input * @returns Updated configuration */ private async gatherSnmpV3Settings(config: any, prompt: (question: string) => Promise): Promise { console.log('\nSNMPv3 Security Settings:'); // Security Level console.log('\nSecurity Level:'); console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)'); console.log(' 2) authNoPriv (Authentication, No Privacy)'); console.log(' 3) authPriv (Authentication and Privacy)'); const defaultSecLevel = config.snmp.securityLevel ? (config.snmp.securityLevel === 'noAuthNoPriv' ? 1 : config.snmp.securityLevel === 'authNoPriv' ? 2 : 3) : 3; const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `); const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel; if (secLevel === 1) { config.snmp.securityLevel = 'noAuthNoPriv'; // No auth, no priv - clear out authentication and privacy settings config.snmp.authProtocol = ''; config.snmp.authKey = ''; config.snmp.privProtocol = ''; config.snmp.privKey = ''; // Set appropriate timeout for security level config.snmp.timeout = 5000; // 5 seconds for basic security } else if (secLevel === 2) { config.snmp.securityLevel = 'authNoPriv'; // Auth, no priv - clear out privacy settings config.snmp.privProtocol = ''; config.snmp.privKey = ''; // Set appropriate timeout for security level config.snmp.timeout = 10000; // 10 seconds for authentication } else { config.snmp.securityLevel = 'authPriv'; // Set appropriate timeout for security level config.snmp.timeout = 15000; // 15 seconds for full encryption } // Username const defaultUsername = config.snmp.username || ''; const username = await prompt(`SNMPv3 Username [${defaultUsername}]: `); config.snmp.username = username.trim() || defaultUsername; if (secLevel >= 2) { // Authentication settings config = await this.gatherAuthenticationSettings(config, prompt); if (secLevel === 3) { // Privacy settings config = await this.gatherPrivacySettings(config, prompt); } // Allow customizing the timeout value const defaultTimeout = config.snmp.timeout / 1000; // Convert from ms to seconds for display console.log('\nSNMPv3 operations with authentication and privacy may require longer timeouts.'); const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `); const timeout = parseInt(timeoutInput, 10); if (timeoutInput.trim() && !isNaN(timeout)) { config.snmp.timeout = timeout * 1000; // Convert to ms } } return config; } /** * Gather authentication settings for SNMPv3 * @param config Current configuration * @param prompt Function to prompt for user input * @returns Updated configuration */ private async gatherAuthenticationSettings(config: any, prompt: (question: string) => Promise): Promise { // Authentication protocol console.log('\nAuthentication Protocol:'); console.log(' 1) MD5'); console.log(' 2) SHA'); const defaultAuthProtocol = config.snmp.authProtocol === 'SHA' ? 2 : 1; const authProtocolInput = await prompt(`Select Authentication Protocol [${defaultAuthProtocol}]: `); const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol; config.snmp.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5'; // Authentication Key/Password const defaultAuthKey = config.snmp.authKey || ''; const authKey = await prompt(`Authentication Password ${defaultAuthKey ? '[*****]' : ''}: `); config.snmp.authKey = authKey.trim() || defaultAuthKey; return config; } /** * Gather privacy settings for SNMPv3 * @param config Current configuration * @param prompt Function to prompt for user input * @returns Updated configuration */ private async gatherPrivacySettings(config: any, prompt: (question: string) => Promise): Promise { // Privacy protocol console.log('\nPrivacy Protocol:'); console.log(' 1) DES'); console.log(' 2) AES'); const defaultPrivProtocol = config.snmp.privProtocol === 'AES' ? 2 : 1; const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `); const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol; config.snmp.privProtocol = privProtocol === 2 ? 'AES' : 'DES'; // Privacy Key/Password const defaultPrivKey = config.snmp.privKey || ''; const privKey = await prompt(`Privacy Password ${defaultPrivKey ? '[*****]' : ''}: `); config.snmp.privKey = privKey.trim() || defaultPrivKey; return config; } /** * Gather threshold settings * @param config Current configuration * @param prompt Function to prompt for user input * @returns Updated configuration */ private async gatherThresholdSettings(config: any, prompt: (question: string) => Promise): Promise { console.log('\nShutdown Thresholds:'); // Battery threshold const defaultBatteryThreshold = config.thresholds.battery; const batteryThresholdInput = await prompt(`Battery percentage threshold [${defaultBatteryThreshold}%]: `); const batteryThreshold = parseInt(batteryThresholdInput, 10); config.thresholds.battery = (batteryThresholdInput.trim() && !isNaN(batteryThreshold)) ? batteryThreshold : defaultBatteryThreshold; // Runtime threshold const defaultRuntimeThreshold = config.thresholds.runtime; const runtimeThresholdInput = await prompt(`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `); const runtimeThreshold = parseInt(runtimeThresholdInput, 10); config.thresholds.runtime = (runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)) ? runtimeThreshold : defaultRuntimeThreshold; // Check interval const defaultInterval = config.checkInterval / 1000; // Convert from ms to seconds for display const intervalInput = await prompt(`Check interval in seconds [${defaultInterval}]: `); const interval = parseInt(intervalInput, 10); config.checkInterval = (intervalInput.trim() && !isNaN(interval)) ? interval * 1000 // Convert to ms : defaultInterval * 1000; return config; } /** * Gather UPS model settings * @param config Current configuration * @param prompt Function to prompt for user input * @returns Updated configuration */ private async gatherUpsModelSettings(config: any, prompt: (question: string) => Promise): Promise { console.log('\nUPS Model Selection:'); console.log(' 1) CyberPower'); console.log(' 2) APC'); console.log(' 3) Eaton'); console.log(' 4) TrippLite'); console.log(' 5) Liebert/Vertiv'); console.log(' 6) Custom (Advanced)'); const defaultModelValue = config.snmp.upsModel === 'cyberpower' ? 1 : config.snmp.upsModel === 'apc' ? 2 : config.snmp.upsModel === 'eaton' ? 3 : config.snmp.upsModel === 'tripplite' ? 4 : config.snmp.upsModel === 'liebert' ? 5 : config.snmp.upsModel === 'custom' ? 6 : 1; const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `); const modelValue = parseInt(modelInput, 10) || defaultModelValue; if (modelValue === 1) { config.snmp.upsModel = 'cyberpower'; } else if (modelValue === 2) { config.snmp.upsModel = 'apc'; } else if (modelValue === 3) { config.snmp.upsModel = 'eaton'; } else if (modelValue === 4) { config.snmp.upsModel = 'tripplite'; } else if (modelValue === 5) { config.snmp.upsModel = 'liebert'; } else if (modelValue === 6) { config.snmp.upsModel = 'custom'; console.log('\nEnter custom OIDs for your UPS:'); console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)'); // Custom OIDs const powerStatusOID = await prompt('Power Status OID: '); const batteryCapacityOID = await prompt('Battery Capacity OID: '); const batteryRuntimeOID = await prompt('Battery Runtime OID: '); // Create custom OIDs object config.snmp.customOIDs = { POWER_STATUS: powerStatusOID.trim(), BATTERY_CAPACITY: batteryCapacityOID.trim(), BATTERY_RUNTIME: batteryRuntimeOID.trim() }; } return config; } /** * Display configuration summary * @param config Current configuration */ private displayConfigSummary(config: any): void { console.log('\n┌─ Configuration Summary ─────────────────┐'); console.log(`│ SNMP Host: ${config.snmp.host}:${config.snmp.port}`); console.log(`│ SNMP Version: ${config.snmp.version}`); console.log(`│ UPS Model: ${config.snmp.upsModel}`); console.log(`│ Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`); console.log(`│ Check Interval: ${config.checkInterval/1000} seconds`); console.log('└──────────────────────────────────────────┘\n'); } /** * Optionally test connection to UPS * @param config Current configuration * @param prompt Function to prompt for user input */ private async optionallyTestConnection(config: any, prompt: (question: string) => Promise): Promise { const testConnection = await prompt('Would you like to test the connection to your UPS? (y/N): '); if (testConnection.toLowerCase() === 'y') { console.log('\nTesting connection to UPS...'); try { // Create a test config with a short timeout const testConfig = { ...config.snmp, timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing }; const status = await this.nupst.getSnmp().getUpsStatus(testConfig); console.log('\n┌─ Connection Successful! ─────────────────┐'); console.log('│ UPS Status:'); console.log(`│ ✓ Power Status: ${status.powerStatus}`); console.log(`│ ✓ Battery Capacity: ${status.batteryCapacity}%`); console.log(`│ ✓ Runtime Remaining: ${status.batteryRuntime} minutes`); console.log('└──────────────────────────────────────────┘'); } catch (error) { console.error('\n┌─ Connection Failed! ───────────────────────┐'); console.error('│ Error: ' + error.message); console.error('└──────────────────────────────────────────┘'); console.log('\nPlease check your settings and try again.'); } } } /** * Optionally enable systemd service * @param prompt Function to prompt for user input */ private async optionallyEnableService(prompt: (question: string) => Promise): Promise { if (process.getuid && process.getuid() !== 0) { console.log('\nNote: Run "sudo nupst enable" to set up NUPST as a system service.'); } else { const setupService = await prompt('Would you like to enable NUPST as a system service? (y/N): '); if (setupService.toLowerCase() === 'y') { await this.nupst.getSystemd().install(); console.log('Service installed. Use "nupst start" to start the service.'); } } } }