1032 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1032 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { execSync } from 'child_process';
 | |
| import { promises as fs } from 'fs';
 | |
| import { dirname, join } from 'path';
 | |
| import { fileURLToPath } from 'url';
 | |
| 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<void> {
 | |
|     // 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<void> {
 | |
|     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 'update':
 | |
|         await this.update();
 | |
|         break;
 | |
|         
 | |
|       case 'uninstall':
 | |
|         await this.uninstall();
 | |
|         break;
 | |
|         
 | |
|       case 'config':
 | |
|         await this.showConfig();
 | |
|         break;
 | |
| 
 | |
|       case 'help':
 | |
|       default:
 | |
|         this.showHelp();
 | |
|         break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Enable the service (requires root)
 | |
|    */
 | |
|   private async enable(): Promise<void> {
 | |
|     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<void> {
 | |
|     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<void> {
 | |
|     try {
 | |
|       // Use exec with spawn to properly follow logs in real-time
 | |
|       const { spawn } = await import('child_process');
 | |
|       console.log('Tailing nupst service logs (Ctrl+C to exit)...\n');
 | |
|       
 | |
|       const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], {
 | |
|         stdio: ['ignore', 'inherit', 'inherit']
 | |
|       });
 | |
|       
 | |
|       // Forward signals to child process
 | |
|       process.on('SIGINT', () => {
 | |
|         journalctl.kill('SIGINT');
 | |
|         process.exit(0);
 | |
|       });
 | |
|       
 | |
|       // Wait for process to exit
 | |
|       await new Promise<void>((resolve) => {
 | |
|         journalctl.on('exit', () => resolve());
 | |
|       });
 | |
|     } catch (error) {
 | |
|       console.error('Failed to retrieve logs:', error);
 | |
|       process.exit(1);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Stop the systemd service
 | |
|    */
 | |
|   private async stop(): Promise<void> {
 | |
|     await this.nupst.getSystemd().stop();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Start the systemd service
 | |
|    */
 | |
|   private async start(): Promise<void> {
 | |
|     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<void> {
 | |
|     // Extract debug options from args array
 | |
|     const debugOptions = this.extractDebugOptions(process.argv);
 | |
|     await this.nupst.getSystemd().getStatus(debugOptions.debugMode);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Disable the service (requires root)
 | |
|    */
 | |
|   private async disable(): Promise<void> {
 | |
|     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<void> {
 | |
|     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<void> {
 | |
|     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 config         - Display the current configuration
 | |
|   nupst update         - Update NUPST from repository and refresh systemd service (requires root)
 | |
|   nupst uninstall      - Completely uninstall NUPST from the system (requires root)
 | |
|   nupst help           - Show this help message
 | |
| 
 | |
| Options:
 | |
|   --debug, -d         - Enable debug mode for detailed SNMP logging
 | |
|                         (Example: nupst test --debug)
 | |
| `);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Update NUPST from repository and refresh systemd service
 | |
|    */
 | |
|   private async update(): Promise<void> {
 | |
|     try {
 | |
|       // Check if running as root
 | |
|       this.checkRootAccess('This command must be run as root to update NUPST and refresh the systemd service.');
 | |
|       
 | |
|       console.log('┌─ NUPST Update Process ──────────────────┐');
 | |
|       console.log('│ Updating NUPST from repository...');
 | |
|       
 | |
|       // Determine the installation directory (assuming it's either /opt/nupst or the current directory)
 | |
|       const { existsSync } = await import('fs');
 | |
|       let installDir = '/opt/nupst';
 | |
|       
 | |
|       if (!existsSync(installDir)) {
 | |
|         // If not installed in /opt/nupst, use the current directory
 | |
|         const { dirname } = await import('path');
 | |
|         installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
 | |
|         console.log(`│ Using local installation directory: ${installDir}`);
 | |
|       }
 | |
|       
 | |
|       try {
 | |
|         // 1. Update the repository
 | |
|         console.log('│ Pulling latest changes from git repository...');
 | |
|         execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, { stdio: 'pipe' });
 | |
|         
 | |
|         // 2. Run the install.sh script
 | |
|         console.log('│ Running install.sh to update NUPST...');
 | |
|         execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
 | |
|         
 | |
|         // 3. Run the setup.sh script 
 | |
|         console.log('│ Running setup.sh to update dependencies...');
 | |
|         execSync(`cd ${installDir} && bash ./setup.sh`, { stdio: 'pipe' });
 | |
|         
 | |
|         // 4. Refresh the systemd service
 | |
|         console.log('│ Refreshing systemd service...');
 | |
|         
 | |
|         // First check if service exists
 | |
|         const serviceExists = execSync('systemctl list-unit-files | grep nupst.service').toString().includes('nupst.service');
 | |
|         
 | |
|         if (serviceExists) {
 | |
|           // Stop the service if it's running
 | |
|           const isRunning = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
 | |
|           if (isRunning) {
 | |
|             console.log('│ Stopping nupst service...');
 | |
|             execSync('systemctl stop nupst.service');
 | |
|           }
 | |
|           
 | |
|           // Reinstall the service
 | |
|           console.log('│ Reinstalling systemd service...');
 | |
|           await this.nupst.getSystemd().install();
 | |
|           
 | |
|           // Restart the service if it was running
 | |
|           if (isRunning) {
 | |
|             console.log('│ Restarting nupst service...');
 | |
|             execSync('systemctl start nupst.service');
 | |
|           }
 | |
|         } else {
 | |
|           console.log('│ Systemd service not installed, skipping service refresh.');
 | |
|           console.log('│ Run "nupst enable" to install the service.');
 | |
|         }
 | |
|         
 | |
|         console.log('│ Update completed successfully!');
 | |
|         console.log('└──────────────────────────────────────────┘');
 | |
|       } catch (error) {
 | |
|         console.error('│ Error during update process:');
 | |
|         console.error(`│ ${error.message}`);
 | |
|         console.error('└──────────────────────────────────────────┘');
 | |
|         process.exit(1);
 | |
|       }
 | |
|     } catch (error) {
 | |
|       console.error(`Update failed: ${error.message}`);
 | |
|       process.exit(1);
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Interactive setup for configuring SNMP settings
 | |
|    */
 | |
|   private async setup(): Promise<void> {
 | |
|     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<string> => {
 | |
|         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<string>): Promise<void> {
 | |
|     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<string>): Promise<any> {
 | |
|     // 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<string>): Promise<any> {
 | |
|     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<string>): Promise<any> {
 | |
|     // 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<string>): Promise<any> {
 | |
|     // 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<string>): Promise<any> {
 | |
|     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<string>): Promise<any> {
 | |
|     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<string>): Promise<void> {
 | |
|     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<string>): Promise<void> {
 | |
|     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.');
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Display the current configuration
 | |
|    */
 | |
|   private async showConfig(): Promise<void> {
 | |
|     try {
 | |
|       // Try to load 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();
 | |
|       
 | |
|       console.log('┌─ NUPST Configuration ──────────────────────┐');
 | |
|       
 | |
|       // SNMP Settings
 | |
|       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'}`);
 | |
|       }
 | |
|       
 | |
|       // Thresholds
 | |
|       console.log('│ Thresholds:');
 | |
|       console.log(`│   Battery: ${config.thresholds.battery}%`);
 | |
|       console.log(`│   Runtime: ${config.thresholds.runtime} minutes`);
 | |
|       console.log(`│ Check Interval: ${config.checkInterval / 1000} seconds`);
 | |
|       
 | |
|       // Configuration file location
 | |
|       console.log('│');
 | |
|       console.log('│ Configuration File Location:');
 | |
|       console.log('│   /etc/nupst/config.json');
 | |
|       
 | |
|       console.log('└──────────────────────────────────────────┘');
 | |
|       
 | |
|       // Show service status
 | |
|       try {
 | |
|         const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
 | |
|         const isEnabled = execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
 | |
|         
 | |
|         console.log('┌─ Service Status ─────────────────────────┐');
 | |
|         console.log(`│ Service Active: ${isActive ? 'Yes' : 'No'}`);
 | |
|         console.log(`│ Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
 | |
|         console.log('└──────────────────────────────────────────┘');
 | |
|       } catch (error) {
 | |
|         // Ignore errors checking service status
 | |
|       }
 | |
|       
 | |
|     } catch (error) {
 | |
|       console.error(`Failed to display configuration: ${error.message}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Completely uninstall NUPST from the system
 | |
|    */
 | |
|   private async uninstall(): Promise<void> {
 | |
|     // Check if running as root
 | |
|     this.checkRootAccess('This command must be run as root.');
 | |
| 
 | |
|     try {
 | |
|       // Import readline module for user input
 | |
|       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<string> => {
 | |
|         return new Promise((resolve) => {
 | |
|           rl.question(question, (answer: string) => {
 | |
|             resolve(answer);
 | |
|           });
 | |
|         });
 | |
|       };
 | |
| 
 | |
|       console.log('\nNUPST Uninstaller');
 | |
|       console.log('===============');
 | |
|       console.log('This will completely remove NUPST from your system.\n');
 | |
| 
 | |
|       // Ask about removing configuration
 | |
|       const removeConfig = await prompt('Do you want to remove the NUPST configuration files? (y/N): ');
 | |
|       
 | |
|       // Find the uninstall.sh script location
 | |
|       let uninstallScriptPath: string;
 | |
|       
 | |
|       // Try to determine script location based on executable path
 | |
|       try {
 | |
|         // For ESM, we can use import.meta.url, but since we might be in CJS
 | |
|         // we'll use a more reliable approach based on process.argv[1]
 | |
|         const binPath = process.argv[1];
 | |
|         const modulePath = dirname(dirname(binPath));
 | |
|         uninstallScriptPath = join(modulePath, 'uninstall.sh');
 | |
|         
 | |
|         // Check if the script exists
 | |
|         await fs.access(uninstallScriptPath);
 | |
|       } catch (error) {
 | |
|         // If we can't find it in the expected location, try common installation paths
 | |
|         const commonPaths = [
 | |
|           '/opt/nupst/uninstall.sh',
 | |
|           join(process.cwd(), 'uninstall.sh')
 | |
|         ];
 | |
|         
 | |
|         for (const path of commonPaths) {
 | |
|           try {
 | |
|             await fs.access(path);
 | |
|             uninstallScriptPath = path;
 | |
|             break;
 | |
|           } catch {
 | |
|             // Continue to next path
 | |
|           }
 | |
|         }
 | |
|         
 | |
|         if (!uninstallScriptPath) {
 | |
|           console.error('Could not locate uninstall.sh script. Aborting uninstall.');
 | |
|           rl.close();
 | |
|           process.exit(1);
 | |
|         }
 | |
|       }
 | |
|       
 | |
|       // Close readline before executing script
 | |
|       rl.close();
 | |
|       
 | |
|       // Execute uninstall.sh with the appropriate option
 | |
|       console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`);
 | |
|       
 | |
|       // Pass the configuration removal option as an environment variable
 | |
|       const env = {
 | |
|         ...process.env,
 | |
|         REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no',
 | |
|         REMOVE_REPO: 'yes',  // Always remove repo as requested
 | |
|         NUPST_CLI_CALL: 'true'  // Flag to indicate this is being called from CLI
 | |
|       };
 | |
|       
 | |
|       // Run the uninstall script with sudo
 | |
|       execSync(`sudo bash ${uninstallScriptPath}`, { 
 | |
|         env,
 | |
|         stdio: 'inherit'  // Show output in the terminal
 | |
|       });
 | |
|       
 | |
|     } catch (error) {
 | |
|       console.error(`Uninstall failed: ${error.message}`);
 | |
|       process.exit(1);
 | |
|     }
 | |
|   }
 | |
| } |