From 9969e0f7036e0b92dfa123b6523fc11712d4452c Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Fri, 28 Mar 2025 22:12:01 +0000 Subject: [PATCH] feat(cli): Refactor CLI commands to use dedicated handlers for UPS, group, and service management --- changelog.md | 9 + ts/00_commitinfo_data.ts | 2 +- ts/cli.ts | 2041 ++----------------------------------- ts/cli/group-handler.ts | 565 ++++++++++ ts/cli/service-handler.ts | 320 ++++++ ts/cli/ups-handler.ts | 986 ++++++++++++++++++ ts/nupst.ts | 38 +- 7 files changed, 1992 insertions(+), 1969 deletions(-) create mode 100644 ts/cli/group-handler.ts create mode 100644 ts/cli/service-handler.ts create mode 100644 ts/cli/ups-handler.ts diff --git a/changelog.md b/changelog.md index a8e33a3..f8117f0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-03-28 - 3.1.0 - feat(cli) +Refactor CLI commands to use dedicated handlers for UPS, group, and service management + +- Extracted UPS-related CLI logic into a new UpsHandler +- Introduced GroupHandler to manage UPS groups commands +- Added ServiceHandler for systemd service operations +- Updated CLI routing in cli.ts to delegate commands to the new handlers +- Exposed getters for the new handlers in the Nupst class + ## 2025-03-28 - 3.0.1 - fix(cli) Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module and replacing it with the shortId helper. diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index b973ba8..a6af9b4 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/nupst', - version: '3.0.1', + version: '3.1.0', description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices' } diff --git a/ts/cli.ts b/ts/cli.ts index 7ebc572..1692985 100644 --- a/ts/cli.ts +++ b/ts/cli.ts @@ -1,12 +1,6 @@ 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'; import { logger } from './logger.js'; -import { type IGroupConfig } from './daemon.js'; - -import * as helpers from './helpers/index.js'; /** * Class for handling CLI commands @@ -63,6 +57,11 @@ export class NupstCli { * @param debugMode Whether debug mode is enabled */ private async executeCommand(command: string, commandArgs: string[], debugMode: boolean): Promise { + // Get access to the handlers + const upsHandler = this.nupst.getUpsHandler(); + const groupHandler = this.nupst.getGroupHandler(); + const serviceHandler = this.nupst.getServiceHandler(); + // Handle group subcommands if (command === 'group') { const subcommand = commandArgs[0] || 'list'; @@ -70,7 +69,7 @@ export class NupstCli { switch (subcommand) { case 'add': - await this.groupAdd(); + await groupHandler.add(); break; case 'edit': @@ -80,7 +79,7 @@ export class NupstCli { this.showGroupHelp(); return; } - await this.groupEdit(groupId); + await groupHandler.edit(groupId); break; case 'delete': @@ -90,11 +89,11 @@ export class NupstCli { this.showGroupHelp(); return; } - await this.groupDelete(groupIdToDelete); + await groupHandler.delete(groupIdToDelete); break; case 'list': - await this.groupList(); + await groupHandler.list(); break; default: @@ -107,12 +106,12 @@ export class NupstCli { // Handle main commands switch (command) { case 'add': - await this.add(); + await upsHandler.add(); break; case 'edit': const upsId = commandArgs[0]; - await this.edit(upsId); + await upsHandler.edit(upsId); break; case 'delete': @@ -122,56 +121,56 @@ export class NupstCli { this.showHelp(); return; } - await this.delete(upsIdToDelete); + await upsHandler.delete(upsIdToDelete); break; case 'list': - await this.list(); + await upsHandler.list(); break; case 'setup': // Backward compatibility: setup is now an alias for edit with no specific UPS ID - await this.edit(undefined); + await upsHandler.edit(undefined); break; case 'enable': - await this.enable(); + await serviceHandler.enable(); break; case 'daemon-start': - await this.daemonStart(debugMode); + await serviceHandler.daemonStart(debugMode); break; case 'logs': - await this.logs(); + await serviceHandler.logs(); break; case 'stop': - await this.stop(); + await serviceHandler.stop(); break; case 'start': - await this.start(); + await serviceHandler.start(); break; case 'status': - await this.status(); + await serviceHandler.status(); break; case 'disable': - await this.disable(); + await serviceHandler.disable(); break; case 'test': - await this.test(debugMode); + await upsHandler.test(debugMode); break; case 'update': - await this.update(); + await serviceHandler.update(); break; case 'uninstall': - await this.uninstall(); + await serviceHandler.uninstall(); break; case 'config': @@ -185,1329 +184,6 @@ export class NupstCli { } } - /** - * Enable the service (requires root) - */ - private async enable(): Promise { - this.checkRootAccess('This command must be run as root.'); - await this.nupst.getSystemd().install(); - logger.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 { - logger.log('Starting NUPST daemon...'); - try { - // Enable debug mode for SNMP if requested - if (debugMode) { - this.nupst.getSnmp().enableDebug(); - logger.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 { - // Use exec with spawn to properly follow logs in real-time - const { spawn } = await import('child_process'); - logger.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((resolve) => { - journalctl.on('exit', () => resolve()); - }); - } catch (error) { - logger.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 { - // 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 { - 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) { - logger.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) { - const boxWidth = 45; - logger.logBoxTitle('Debug Mode', boxWidth); - logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown'); - logger.logBoxEnd(); - } - - // Try to load the configuration - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - const errorBoxWidth = 45; - logger.logBoxTitle('Configuration Error', errorBoxWidth); - logger.logBoxLine('No configuration found.'); - logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); - logger.logBoxEnd(); - return; - } - - // Get current configuration - const config = this.nupst.getDaemon().getConfig(); - - // Handle new multi-UPS configuration format - if (config.upsDevices && config.upsDevices.length > 0) { - logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`); - - for (let i = 0; i < config.upsDevices.length; i++) { - const ups = config.upsDevices[i]; - logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`); - this.displayTestConfig(ups); - await this.testConnection(ups); - } - } else { - // Legacy configuration format - this.displayTestConfig(config); - await this.testConnection(config); - } - } catch (error) { - logger.error(`Test failed: ${error.message}`); - } - } - - /** - * Update NUPST from repository and refresh systemd service - */ - private async update(): Promise { - try { - // Check if running as root - this.checkRootAccess( - 'This command must be run as root to update NUPST and refresh the systemd service.' - ); - - const boxWidth = 45; - logger.logBoxTitle('NUPST Update Process', boxWidth); - logger.logBoxLine('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 - logger.logBoxLine(`Using local installation directory: ${installDir}`); - } - - try { - // 1. Update the repository - logger.logBoxLine('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 - logger.logBoxLine('Running install.sh to update NUPST...'); - execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); - - // 3. Run the setup.sh script with force flag to update Node.js and dependencies - logger.logBoxLine('Running setup.sh to update Node.js and dependencies...'); - execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' }); - - // 4. Refresh the systemd service - logger.logBoxLine('Refreshing systemd service...'); - - // First check if service exists - let serviceExists = false; - try { - const output = execSync('systemctl list-unit-files | grep nupst.service').toString(); - serviceExists = output.includes('nupst.service'); - } catch (error) { - // If grep fails (service not found), serviceExists remains false - serviceExists = false; - } - - if (serviceExists) { - // Stop the service if it's running - const isRunning = - execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; - if (isRunning) { - logger.logBoxLine('Stopping nupst service...'); - execSync('systemctl stop nupst.service'); - } - - // Reinstall the service - logger.logBoxLine('Reinstalling systemd service...'); - await this.nupst.getSystemd().install(); - - // Restart the service if it was running - if (isRunning) { - logger.logBoxLine('Restarting nupst service...'); - execSync('systemctl start nupst.service'); - } - } else { - logger.logBoxLine('Systemd service not installed, skipping service refresh.'); - logger.logBoxLine('Run "nupst enable" to install the service.'); - } - - logger.logBoxLine('Update completed successfully!'); - logger.logBoxEnd(); - } catch (error) { - logger.logBoxLine('Error during update process:'); - logger.logBoxLine(`${error.message}`); - logger.logBoxEnd(); - process.exit(1); - } - } catch (error) { - logger.error(`Update failed: ${error.message}`); - process.exit(1); - } - } - - /** - * Completely uninstall NUPST from the system - */ - private async uninstall(): Promise { - // 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 => { - 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); - } - } - - /** - * Add a new UPS configuration - */ - private async add(): Promise { - 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 => { - return new Promise((resolve) => { - rl.question(question, (answer: string) => { - resolve(answer); - }); - }); - }; - - try { - await this.runAddProcess(prompt); - } finally { - rl.close(); - } - } catch (error) { - logger.error(`Add UPS error: ${error.message}`); - } - } - - /** - * Run the interactive process to add a new UPS - * @param prompt Function to prompt for user input - */ - private async runAddProcess(prompt: (question: string) => Promise): Promise { - logger.log('\nNUPST Add UPS'); - logger.log('=============\n'); - logger.log('This will guide you through configuring a new UPS.\n'); - - // Try to load existing config if available - let config; - try { - await this.nupst.getDaemon().loadConfig(); - config = this.nupst.getDaemon().getConfig(); - - // Convert old format to new format if needed - if (!config.upsDevices) { - // Initialize with the current config as the first UPS - config = { - checkInterval: config.checkInterval, - upsDevices: [{ - id: 'default', - name: 'Default UPS', - snmp: config.snmp, - thresholds: config.thresholds, - groups: [] - }], - groups: [] - }; - logger.log('Converting existing configuration to multi-UPS format.'); - } - } catch (error) { - // If config doesn't exist, initialize with empty config - config = { - checkInterval: 30000, // Default check interval - upsDevices: [], - groups: [] - }; - logger.log('No existing configuration found. Creating a new configuration.'); - } - - // Get UPS ID and name - const upsId = helpers.shortId(); - const name = await prompt('UPS Name: '); - - // Create a new UPS configuration object with defaults - const newUps = { - id: upsId, - name: name || `UPS-${upsId}`, - snmp: { - host: '127.0.0.1', - port: 161, - community: 'public', - version: 1, - timeout: 5000, - upsModel: 'cyberpower' - }, - thresholds: { - battery: 60, - runtime: 20 - }, - groups: [] - }; - - // Gather SNMP settings - await this.gatherSnmpSettings(newUps.snmp, prompt); - - // Gather threshold settings - await this.gatherThresholdSettings(newUps.thresholds, prompt); - - // Gather UPS model settings - await this.gatherUpsModelSettings(newUps.snmp, prompt); - - // Assign to groups if any exist - if (config.groups && config.groups.length > 0) { - await this.assignUpsToGroups(newUps, config.groups, prompt); - } - - // Add the new UPS to the config - config.upsDevices.push(newUps); - - // Save the configuration - await this.nupst.getDaemon().saveConfig(config); - - this.displayUpsConfigSummary(newUps); - - // Test the connection if requested - await this.optionallyTestConnection(newUps.snmp, prompt); - - // Check if service is running and restart it if needed - await this.restartServiceIfRunning(); - - logger.log('\nSetup complete!'); - } - - /** - * Edit an existing UPS configuration - * @param upsId ID of the UPS to edit (undefined for default UPS) - */ - private async edit(upsId?: string): Promise { - 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 => { - return new Promise((resolve) => { - rl.question(question, (answer: string) => { - resolve(answer); - }); - }); - }; - - try { - await this.runEditProcess(upsId, prompt); - } finally { - rl.close(); - } - } catch (error) { - logger.error(`Edit UPS error: ${error.message}`); - } - } - - /** - * Run the interactive process to edit a UPS - * @param upsId ID of the UPS to edit (undefined for default UPS) - * @param prompt Function to prompt for user input - */ - private async runEditProcess(upsId: string | undefined, prompt: (question: string) => Promise): Promise { - logger.log('\nNUPST Edit UPS'); - logger.log('=============\n'); - - // Try to load existing config - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - if (!upsId) { - // For default UPS (no ID specified), run setup if no config exists - logger.log('No existing configuration found. Running setup for new UPS.'); - await this.runAddProcess(prompt); - return; - } else { - // For specific UPS ID, error if config doesn't exist - logger.error('No configuration found. Please run "nupst setup" first.'); - return; - } - } - - // Get the config - const config = this.nupst.getDaemon().getConfig(); - - // Convert old format to new format if needed - if (!config.upsDevices) { - // Initialize with the current config as the first UPS - config.upsDevices = [{ - id: 'default', - name: 'Default UPS', - snmp: config.snmp, - thresholds: config.thresholds, - groups: [] - }]; - config.groups = []; - logger.log('Converting existing configuration to multi-UPS format.'); - } - - // Find the UPS to edit - let upsToEdit; - if (upsId) { - // Find specific UPS by ID - upsToEdit = config.upsDevices.find(ups => ups.id === upsId); - if (!upsToEdit) { - logger.error(`UPS with ID "${upsId}" not found.`); - return; - } - logger.log(`Editing UPS: ${upsToEdit.name} (${upsToEdit.id})\n`); - } else { - // For backward compatibility, edit the first UPS if no ID specified - if (config.upsDevices.length === 0) { - logger.error('No UPS devices configured. Please run "nupst add" to add a UPS.'); - return; - } - upsToEdit = config.upsDevices[0]; - logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`); - } - - // Allow editing UPS name - const newName = await prompt(`UPS Name [${upsToEdit.name}]: `); - if (newName.trim()) { - upsToEdit.name = newName; - } - - // Edit SNMP settings - await this.gatherSnmpSettings(upsToEdit.snmp, prompt); - - // Edit threshold settings - await this.gatherThresholdSettings(upsToEdit.thresholds, prompt); - - // Edit UPS model settings - await this.gatherUpsModelSettings(upsToEdit.snmp, prompt); - - // Edit group assignments - if (config.groups && config.groups.length > 0) { - await this.assignUpsToGroups(upsToEdit, config.groups, prompt); - } - - // Save the configuration - await this.nupst.getDaemon().saveConfig(config); - - this.displayUpsConfigSummary(upsToEdit); - - // Test the connection if requested - await this.optionallyTestConnection(upsToEdit.snmp, prompt); - - // Check if service is running and restart it if needed - await this.restartServiceIfRunning(); - - logger.log('\nEdit complete!'); - } - - /** - * Display the configuration for testing - * @param config Current configuration or individual UPS configuration - */ - private displayTestConfig(config: any): void { - // Check if this is a UPS device or full configuration - const isUpsConfig = config.snmp && config.thresholds; - const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {}; - const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {}; - const checkInterval = config.checkInterval || 30000; - - // Get UPS name and ID if available - const upsName = config.name ? config.name : 'Default UPS'; - const upsId = config.id ? config.id : 'default'; - - const boxWidth = 45; - logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth); - logger.logBoxLine(`UPS ID: ${upsId}`); - logger.logBoxLine('SNMP Settings:'); - logger.logBoxLine(` Host: ${snmpConfig.host}`); - logger.logBoxLine(` Port: ${snmpConfig.port}`); - logger.logBoxLine(` Version: ${snmpConfig.version}`); - logger.logBoxLine(` UPS Model: ${snmpConfig.upsModel || 'cyberpower'}`); - - if (snmpConfig.version === 1 || snmpConfig.version === 2) { - logger.logBoxLine(` Community: ${snmpConfig.community}`); - } else if (snmpConfig.version === 3) { - logger.logBoxLine(` Security Level: ${snmpConfig.securityLevel}`); - logger.logBoxLine(` Username: ${snmpConfig.username}`); - - // Show auth and privacy details based on security level - if (snmpConfig.securityLevel === 'authNoPriv' || snmpConfig.securityLevel === 'authPriv') { - logger.logBoxLine(` Auth Protocol: ${snmpConfig.authProtocol || 'None'}`); - } - - if (snmpConfig.securityLevel === 'authPriv') { - logger.logBoxLine(` Privacy Protocol: ${snmpConfig.privProtocol || 'None'}`); - } - - // Show timeout value - logger.logBoxLine(` Timeout: ${snmpConfig.timeout / 1000} seconds`); - } - - // Show OIDs if custom model is selected - if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) { - logger.logBoxLine('Custom OIDs:'); - logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`); - logger.logBoxLine(` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`); - logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`); - } - logger.logBoxLine('Thresholds:'); - logger.logBoxLine(` Battery: ${thresholds.battery}%`); - logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`); - - // Show group assignments if this is a UPS config - if (config.groups && Array.isArray(config.groups)) { - logger.logBoxLine(`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`); - } - - logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`); - logger.logBoxEnd(); - } - - /** - * Test connection to the UPS - * @param config Current UPS configuration or legacy config - */ - private async testConnection(config: any): Promise { - const upsId = config.id || 'default'; - const upsName = config.name || 'Default UPS'; - logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`); - - try { - // Create a test config with a short timeout - const snmpConfig = config.snmp ? config.snmp : config.snmp; - const thresholds = config.thresholds ? config.thresholds : config.thresholds; - - const testConfig = { - ...snmpConfig, - timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing - }; - - const status = await this.nupst.getSnmp().getUpsStatus(testConfig); - - const boxWidth = 45; - logger.logBoxTitle(`Connection Successful: ${upsName}`, boxWidth); - logger.logBoxLine('UPS Status:'); - logger.logBoxLine(` Power Status: ${status.powerStatus}`); - logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`); - logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`); - logger.logBoxEnd(); - - // Check status against thresholds if on battery - if (status.powerStatus === 'onBattery') { - this.analyzeThresholds(status, thresholds); - } - } catch (error) { - const errorBoxWidth = 45; - logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth); - logger.logBoxLine(`Error: ${error.message}`); - logger.logBoxEnd(); - logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS."); - } - } - - /** - * Analyze UPS status against thresholds - * @param status UPS status - * @param thresholds Threshold configuration - */ - private analyzeThresholds(status: any, thresholds: any): void { - const boxWidth = 45; - logger.logBoxTitle('Threshold Analysis', boxWidth); - - if (status.batteryCapacity < thresholds.battery) { - logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold'); - logger.logBoxLine( - ` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%` - ); - logger.logBoxLine(' System would initiate shutdown'); - } else { - logger.logBoxLine('✓ Battery capacity above threshold'); - logger.logBoxLine( - ` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%` - ); - } - - if (status.batteryRuntime < thresholds.runtime) { - logger.logBoxLine('⚠️ WARNING: Runtime below threshold'); - logger.logBoxLine( - ` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min` - ); - logger.logBoxLine(' System would initiate shutdown'); - } else { - logger.logBoxLine('✓ Runtime above threshold'); - logger.logBoxLine( - ` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min` - ); - } - - logger.logBoxEnd(); - } - - /** - * List all configured UPS devices - */ - private async list(): Promise { - try { - // Try to load configuration - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - const errorBoxWidth = 45; - logger.logBoxTitle('Configuration Error', errorBoxWidth); - logger.logBoxLine('No configuration found.'); - logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); - logger.logBoxEnd(); - return; - } - - // Get current configuration - const config = this.nupst.getDaemon().getConfig(); - - // Check if multi-UPS config - if (!config.upsDevices || !Array.isArray(config.upsDevices)) { - // Legacy single UPS configuration - const boxWidth = 45; - logger.logBoxTitle('UPS Devices', boxWidth); - logger.logBoxLine('Legacy single-UPS configuration detected.'); - logger.logBoxLine(''); - logger.logBoxLine('Default UPS:'); - logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`); - logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`); - logger.logBoxLine(` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`); - logger.logBoxLine(''); - logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate'); - logger.logBoxLine('to the multi-UPS configuration format.'); - logger.logBoxEnd(); - return; - } - - // Display UPS list - const boxWidth = 60; - logger.logBoxTitle('UPS Devices', boxWidth); - - if (config.upsDevices.length === 0) { - logger.logBoxLine('No UPS devices configured.'); - logger.logBoxLine('Use "nupst add" to add a UPS device.'); - } else { - logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`); - logger.logBoxLine(''); - logger.logBoxLine('ID | Name | Host | Mode | Groups'); - logger.logBoxLine('-----------+----------------------+----------------+--------------+----------------'); - - for (const ups of config.upsDevices) { - const id = ups.id.padEnd(10, ' ').substring(0, 10); - const name = (ups.name || '').padEnd(20, ' ').substring(0, 20); - const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15); - const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12); - const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None'; - - logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`); - } - } - - logger.logBoxEnd(); - } catch (error) { - logger.error(`Failed to list UPS devices: ${error.message}`); - } - } - - /** - * Delete a UPS by ID - * @param upsId ID of the UPS to delete - */ - private async delete(upsId: string): Promise { - try { - // Try to load configuration - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - const errorBoxWidth = 45; - logger.logBoxTitle('Configuration Error', errorBoxWidth); - logger.logBoxLine('No configuration found.'); - logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); - logger.logBoxEnd(); - return; - } - - // Get current configuration - const config = this.nupst.getDaemon().getConfig(); - - // Check if multi-UPS config - if (!config.upsDevices || !Array.isArray(config.upsDevices)) { - logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.'); - logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.'); - return; - } - - // Find the UPS to delete - const upsIndex = config.upsDevices.findIndex(ups => ups.id === upsId); - if (upsIndex === -1) { - logger.error(`UPS with ID "${upsId}" not found.`); - return; - } - - const upsToDelete = config.upsDevices[upsIndex]; - - // Get confirmation before deleting - const readline = await import('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - const confirm = await new Promise(resolve => { - rl.question(`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `, answer => { - resolve(answer.toLowerCase()); - }); - }); - - rl.close(); - - if (confirm !== 'y' && confirm !== 'yes') { - logger.log('Deletion cancelled.'); - return; - } - - // Remove the UPS from the array - config.upsDevices.splice(upsIndex, 1); - - // Save the configuration - await this.nupst.getDaemon().saveConfig(config); - - logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`); - - // Check if service is running and restart it if needed - await this.restartServiceIfRunning(); - } catch (error) { - logger.error(`Failed to delete UPS: ${error.message}`); - } - } - - /** - * Interactive setup for configuring SNMP settings (alias for edit) - */ - 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 { - // Setup is now an alias for edit with no UPS ID (for backward compatibility) - await this.runEditProcess(undefined, prompt); - } finally { - rl.close(); - } - } catch (error) { - logger.error(`Setup error: ${error.message}`); - } - } - - /** - * List all UPS groups - */ - private async groupList(): Promise { - try { - // Try to load configuration - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - const errorBoxWidth = 45; - logger.logBoxTitle('Configuration Error', errorBoxWidth); - logger.logBoxLine('No configuration found.'); - logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); - logger.logBoxEnd(); - return; - } - - // Get current configuration - const config = this.nupst.getDaemon().getConfig(); - - // Check if multi-UPS config - if (!config.groups || !Array.isArray(config.groups)) { - // Legacy or missing groups configuration - const boxWidth = 45; - logger.logBoxTitle('UPS Groups', boxWidth); - logger.logBoxLine('No groups configured.'); - logger.logBoxLine('Use "nupst group add" to add a UPS group.'); - logger.logBoxEnd(); - return; - } - - // Display group list - const boxWidth = 60; - logger.logBoxTitle('UPS Groups', boxWidth); - - if (config.groups.length === 0) { - logger.logBoxLine('No UPS groups configured.'); - logger.logBoxLine('Use "nupst group add" to add a UPS group.'); - } else { - logger.logBoxLine(`Found ${config.groups.length} group(s)`); - logger.logBoxLine(''); - logger.logBoxLine('ID | Name | Mode | UPS Devices'); - logger.logBoxLine('-----------+----------------------+--------------+----------------'); - - for (const group of config.groups) { - const id = group.id.padEnd(10, ' ').substring(0, 10); - const name = (group.name || '').padEnd(20, ' ').substring(0, 20); - const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12); - - // Count UPS devices in this group - const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id)); - const upsCount = upsInGroup.length; - const upsNames = upsInGroup.map(ups => ups.name).join(', '); - - logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`); - } - } - - logger.logBoxEnd(); - } catch (error) { - logger.error(`Failed to list UPS groups: ${error.message}`); - } - } - - /** - * Add a new UPS group - */ - private async groupAdd(): Promise { - 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 => { - return new Promise((resolve) => { - rl.question(question, (answer: string) => { - resolve(answer); - }); - }); - }; - - try { - // Try to load configuration - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); - return; - } - - // Get current configuration - const config = this.nupst.getDaemon().getConfig(); - - // Initialize groups array if not exists - if (!config.groups) { - config.groups = []; - } - - // Check if upsDevices is initialized - if (!config.upsDevices) { - config.upsDevices = []; - } - - logger.log('\nNUPST Add Group'); - logger.log('==============\n'); - logger.log('This will guide you through creating a new UPS group.\n'); - - // Generate a new unique group ID - const groupId = helpers.shortId(); - - // Get group name - const name = await prompt('Group Name: '); - - // Get group mode - const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: '); - const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant'; - - // Get optional description - const description = await prompt('Group Description (optional): '); - - // Create the new group - const newGroup: IGroupConfig = { - id: groupId, - name: name || `Group-${groupId}`, - mode, - description: description || undefined - }; - - // Add the group to the configuration - config.groups.push(newGroup); - - // Save the configuration - await this.nupst.getDaemon().saveConfig(config); - - // Display summary - const boxWidth = 45; - logger.logBoxTitle('Group Created', boxWidth); - logger.logBoxLine(`ID: ${newGroup.id}`); - logger.logBoxLine(`Name: ${newGroup.name}`); - logger.logBoxLine(`Mode: ${newGroup.mode}`); - if (newGroup.description) { - logger.logBoxLine(`Description: ${newGroup.description}`); - } - logger.logBoxEnd(); - - // Check if there are UPS devices to assign to this group - if (config.upsDevices.length > 0) { - const assignUps = await prompt('Would you like to assign UPS devices to this group now? (y/N): '); - if (assignUps.toLowerCase() === 'y') { - await this.assignUpsToGroup(newGroup.id, config, prompt); - } - } - - // Check if service is running and restart it if needed - await this.restartServiceIfRunning(); - - logger.log('\nGroup setup complete!'); - } finally { - rl.close(); - } - } catch (error) { - logger.error(`Add group error: ${error.message}`); - } - } - - /** - * Edit an existing UPS group - * @param groupId ID of the group to edit - */ - private async groupEdit(groupId: string): Promise { - 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 => { - return new Promise((resolve) => { - rl.question(question, (answer: string) => { - resolve(answer); - }); - }); - }; - - try { - // Try to load configuration - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); - return; - } - - // Get current configuration - const config = this.nupst.getDaemon().getConfig(); - - // Check if groups are initialized - if (!config.groups || !Array.isArray(config.groups)) { - logger.error('No groups configured. Please run "nupst group add" first to create a group.'); - return; - } - - // Find the group to edit - const groupIndex = config.groups.findIndex(group => group.id === groupId); - if (groupIndex === -1) { - logger.error(`Group with ID "${groupId}" not found.`); - return; - } - - const group = config.groups[groupIndex]; - - logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`); - logger.log('==============================================\n'); - - // Edit group name - const newName = await prompt(`Group Name [${group.name}]: `); - if (newName.trim()) { - group.name = newName; - } - - // Edit group mode - const currentMode = group.mode || 'redundant'; - const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `); - if (modeInput.trim()) { - group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant'; - } - - // Edit description - const currentDesc = group.description || ''; - const newDesc = await prompt(`Group Description [${currentDesc}]: `); - if (newDesc.trim() || newDesc === '') { - group.description = newDesc.trim() || undefined; - } - - // Update the group in the configuration - config.groups[groupIndex] = group; - - // Save the configuration - await this.nupst.getDaemon().saveConfig(config); - - // Display summary - const boxWidth = 45; - logger.logBoxTitle('Group Updated', boxWidth); - logger.logBoxLine(`ID: ${group.id}`); - logger.logBoxLine(`Name: ${group.name}`); - logger.logBoxLine(`Mode: ${group.mode}`); - if (group.description) { - logger.logBoxLine(`Description: ${group.description}`); - } - logger.logBoxEnd(); - - // Edit UPS assignments if requested - const editAssignments = await prompt('Would you like to edit UPS assignments for this group? (y/N): '); - if (editAssignments.toLowerCase() === 'y') { - await this.assignUpsToGroup(group.id, config, prompt); - } - - // Check if service is running and restart it if needed - await this.restartServiceIfRunning(); - - logger.log('\nGroup edit complete!'); - } finally { - rl.close(); - } - } catch (error) { - logger.error(`Edit group error: ${error.message}`); - } - } - - /** - * Delete an existing UPS group - * @param groupId ID of the group to delete - */ - private async groupDelete(groupId: string): Promise { - try { - // Try to load configuration - try { - await this.nupst.getDaemon().loadConfig(); - } catch (error) { - logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); - return; - } - - // Get current configuration - const config = this.nupst.getDaemon().getConfig(); - - // Check if groups are initialized - if (!config.groups || !Array.isArray(config.groups)) { - logger.error('No groups configured.'); - return; - } - - // Find the group to delete - const groupIndex = config.groups.findIndex(group => group.id === groupId); - if (groupIndex === -1) { - logger.error(`Group with ID "${groupId}" not found.`); - return; - } - - const groupToDelete = config.groups[groupIndex]; - - // Get confirmation before deleting - const readline = await import('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - const confirm = await new Promise(resolve => { - rl.question(`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => { - resolve(answer.toLowerCase()); - }); - }); - - rl.close(); - - if (confirm !== 'y' && confirm !== 'yes') { - logger.log('Deletion cancelled.'); - return; - } - - // Remove this group from all UPS device group assignments - if (config.upsDevices && Array.isArray(config.upsDevices)) { - for (const ups of config.upsDevices) { - const groupIndex = ups.groups.indexOf(groupId); - if (groupIndex !== -1) { - ups.groups.splice(groupIndex, 1); - } - } - } - - // Remove the group from the array - config.groups.splice(groupIndex, 1); - - // Save the configuration - await this.nupst.getDaemon().saveConfig(config); - - logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`); - - // Check if service is running and restart it if needed - await this.restartServiceIfRunning(); - } catch (error) { - logger.error(`Failed to delete group: ${error.message}`); - } - } - - /** - * Display help message - */ - private showHelp(): void { - logger.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 - -UPS Management: - nupst add - Add a new UPS device - nupst edit [id] - Edit an existing UPS (default UPS if no ID provided) - nupst delete - Delete a UPS by ID - nupst list - List all configured UPS devices - nupst setup - Alias for 'nupst edit' (backward compatibility) - -Group Management: - nupst group list - List all UPS groups - nupst group add - Add a new UPS group - nupst group edit - Edit an existing UPS group - nupst group delete - Delete a UPS group - -System Commands: - nupst test - Test the current configuration by connecting to all UPS devices - 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) -`); - } - - /** - * Display help message for group commands - */ - private showGroupHelp(): void { - logger.log(` -NUPST - Group Management Commands - -Usage: - nupst group list - List all UPS groups - nupst group add - Add a new UPS group - nupst group edit - Edit an existing UPS group - nupst group delete - Delete a UPS group - -Options: - --debug, -d - Enable debug mode for detailed logging -`); - } - /** * Display the current configuration */ @@ -1567,7 +243,7 @@ Options: } // List UPS devices in this group - const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id)); + const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(group.id)); logger.logBoxLine(` UPS Devices: ${upsInGroup.length > 0 ? upsInGroup.map(ups => ups.name).join(', ') : 'None'}`); logger.logBoxLine(''); @@ -1651,629 +327,64 @@ Options: logger.error(`Failed to display configuration: ${error.message}`); } } + + /** + * Display help message + */ + private showHelp(): void { + logger.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 + +UPS Management: + nupst add - Add a new UPS device + nupst edit [id] - Edit an existing UPS (default UPS if no ID provided) + nupst delete - Delete a UPS by ID + nupst list - List all configured UPS devices + nupst setup - Alias for 'nupst edit' (backward compatibility) - /** - * Generate a unique group ID - * @param prompt Function to prompt for user input - * @param existingGroups Array of existing groups - * @returns Unique group ID - */ - private async promptForUniqueGroupId( - prompt: (question: string) => Promise, - existingGroups: any[] - ): Promise { - const existingIds = existingGroups.map(group => group.id); - - // First ask for a custom ID - const customId = await prompt('Group ID (leave empty for auto-generated): '); - - if (customId.trim()) { - // Check if ID is already in use - if (existingIds.includes(customId.trim())) { - logger.error(`Group ID "${customId.trim()}" is already in use.`); - // Recursively call this function to try again - return this.promptForUniqueGroupId(prompt, existingGroups); - } - return customId.trim(); - } - - // Generate a unique ID with timestamp - const timestamp = new Date().getTime().toString(36); - const randomPart = Math.floor(Math.random() * 1000).toString(36); - return `group-${timestamp}-${randomPart}`; - } +Group Management: + nupst group list - List all UPS groups + nupst group add - Add a new UPS group + nupst group edit - Edit an existing UPS group + nupst group delete - Delete a UPS group - /** - * Display UPS configuration summary - * @param ups UPS configuration - */ - private displayUpsConfigSummary(ups: any): void { - const boxWidth = 45; - logger.log(''); - logger.logBoxTitle(`UPS Configuration: ${ups.name}`, boxWidth); - logger.logBoxLine(`UPS ID: ${ups.id}`); - logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`); - logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`); - logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`); - logger.logBoxLine( - `Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime` - ); - if (ups.groups && ups.groups.length > 0) { - logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`); - } else { - logger.logBoxLine('Groups: None'); - } - logger.logBoxEnd(); - logger.log(''); - } - - /** - * Assign UPS devices to groups - * @param ups UPS configuration to update - * @param groups Available groups - * @param prompt Function to prompt for user input - */ - private async assignUpsToGroups( - ups: any, - groups: any[], - prompt: (question: string) => Promise - ): Promise { - // Show current group assignments - logger.log('\nCurrent Group Assignments:'); - if (ups.groups && ups.groups.length > 0) { - for (const groupId of ups.groups) { - const group = groups.find(g => g.id === groupId); - if (group) { - logger.log(`- ${group.name} (${group.id})`); - } else { - logger.log(`- Unknown group (${groupId})`); - } - } - } else { - logger.log('- None'); - } - - // Show available groups - logger.log('\nAvailable Groups:'); - if (groups.length === 0) { - logger.log('- No groups available. Use "nupst group add" to create groups.'); - return; - } - - for (let i = 0; i < groups.length; i++) { - const group = groups[i]; - const assigned = ups.groups && ups.groups.includes(group.id); - logger.log(`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`); - } - - // Prompt for group selection - const selection = await prompt('\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): '); - - if (selection.toLowerCase() === 'clear') { - // Clear all group assignments - ups.groups = []; - logger.log('All group assignments cleared.'); - return; - } - - if (!selection.trim()) { - // No change if empty input - return; - } - - // Process selections - const selections = selection.split(',').map(s => s.trim()); - - for (const sel of selections) { - const index = parseInt(sel, 10) - 1; - if (isNaN(index) || index < 0 || index >= groups.length) { - logger.error(`Invalid selection: ${sel}`); - continue; - } - - const group = groups[index]; - - // Toggle assignment - if (!ups.groups) { - ups.groups = []; - } - - const groupIndex = ups.groups.indexOf(group.id); - if (groupIndex === -1) { - // Add to group - ups.groups.push(group.id); - logger.log(`Added to group: ${group.name} (${group.id})`); - } else { - // Remove from group - ups.groups.splice(groupIndex, 1); - logger.log(`Removed from group: ${group.name} (${group.id})`); - } - } - } - - /** - * Assign UPS devices to a specific group - * @param groupId Group ID to assign UPS devices to - * @param config Full configuration - * @param prompt Function to prompt for user input - */ - private async assignUpsToGroup( - groupId: string, - config: any, - prompt: (question: string) => Promise - ): Promise { - if (!config.upsDevices || config.upsDevices.length === 0) { - logger.log('No UPS devices available. Use "nupst add" to add UPS devices.'); - return; - } - - const group = config.groups.find(g => g.id === groupId); - if (!group) { - logger.error(`Group with ID "${groupId}" not found.`); - return; - } - - // Show current assignments - logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`); - const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(groupId)); - if (upsInGroup.length === 0) { - logger.log('- None'); - } else { - for (const ups of upsInGroup) { - logger.log(`- ${ups.name} (${ups.id})`); - } - } - - // Show all UPS devices - logger.log('\nAvailable UPS devices:'); - for (let i = 0; i < config.upsDevices.length; i++) { - const ups = config.upsDevices[i]; - const assigned = ups.groups && ups.groups.includes(groupId); - logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`); - } - - // Prompt for UPS selection - const selection = await prompt('\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): '); - - if (selection.toLowerCase() === 'clear') { - // Clear all UPS from this group - for (const ups of config.upsDevices) { - if (ups.groups) { - const groupIndex = ups.groups.indexOf(groupId); - if (groupIndex !== -1) { - ups.groups.splice(groupIndex, 1); - } - } - } - logger.log(`All UPS devices removed from group "${group.name}".`); - return; - } - - if (!selection.trim()) { - // No change if empty input - return; - } - - // Process selections - const selections = selection.split(',').map(s => s.trim()); - - for (const sel of selections) { - const index = parseInt(sel, 10) - 1; - if (isNaN(index) || index < 0 || index >= config.upsDevices.length) { - logger.error(`Invalid selection: ${sel}`); - continue; - } - - const ups = config.upsDevices[index]; - - // Initialize groups array if needed - if (!ups.groups) { - ups.groups = []; - } - - // Toggle assignment - const groupIndex = ups.groups.indexOf(groupId); - if (groupIndex === -1) { - // Add to group - ups.groups.push(groupId); - logger.log(`Added "${ups.name}" to group "${group.name}"`); - } else { - // Remove from group - ups.groups.splice(groupIndex, 1); - logger.log(`Removed "${ups.name}" from group "${group.name}"`); - } - } - } - - /** - * Run the interactive setup process (legacy version, now replaced by edit) - * @param prompt Function to prompt for user input - */ - private async runSetupProcess(prompt: (question: string) => Promise): Promise { - // For backward compatibility, just call runEditProcess with no UPS ID - await this.runEditProcess(undefined, prompt); +System Commands: + nupst test - Test the current configuration by connecting to all UPS devices + 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) +`); } /** - * Gather SNMP settings - * @param snmpConfig SNMP configuration object to update - * @param prompt Function to prompt for user input + * Display help message for group commands */ - private async gatherSnmpSettings( - snmpConfig: any, - prompt: (question: string) => Promise - ): Promise { - // SNMP IP Address - const defaultHost = snmpConfig.host || '127.0.0.1'; - const host = await prompt(`UPS IP Address [${defaultHost}]: `); - snmpConfig.host = host.trim() || defaultHost; + private showGroupHelp(): void { + logger.log(` +NUPST - Group Management Commands - // SNMP Port - const defaultPort = snmpConfig.port || 161; - const portInput = await prompt(`SNMP Port [${defaultPort}]: `); - const port = parseInt(portInput, 10); - snmpConfig.port = portInput.trim() && !isNaN(port) ? port : defaultPort; +Usage: + nupst group list - List all UPS groups + nupst group add - Add a new UPS group + nupst group edit - Edit an existing UPS group + nupst group delete - Delete a UPS group - // SNMP Version - const defaultVersion = snmpConfig.version || 1; - 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); - snmpConfig.version = - versionInput.trim() && (version === 1 || version === 2 || version === 3) - ? version - : defaultVersion; - - if (snmpConfig.version === 1 || snmpConfig.version === 2) { - // SNMP Community String (for v1/v2c) - const defaultCommunity = snmpConfig.community || 'public'; - const community = await prompt(`SNMP Community String [${defaultCommunity}]: `); - snmpConfig.community = community.trim() || defaultCommunity; - } else if (snmpConfig.version === 3) { - // SNMP v3 settings - await this.gatherSnmpV3Settings(snmpConfig, prompt); - } - } - - /** - * Gather SNMPv3 specific settings - * @param snmpConfig SNMP configuration object to update - * @param prompt Function to prompt for user input - */ - private async gatherSnmpV3Settings( - snmpConfig: 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 = snmpConfig.securityLevel - ? snmpConfig.securityLevel === 'noAuthNoPriv' - ? 1 - : snmpConfig.securityLevel === 'authNoPriv' - ? 2 - : 3 - : 3; - const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `); - const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel; - - if (secLevel === 1) { - snmpConfig.securityLevel = 'noAuthNoPriv'; - // No auth, no priv - clear out authentication and privacy settings - snmpConfig.authProtocol = ''; - snmpConfig.authKey = ''; - snmpConfig.privProtocol = ''; - snmpConfig.privKey = ''; - // Set appropriate timeout for security level - snmpConfig.timeout = 5000; // 5 seconds for basic security - } else if (secLevel === 2) { - snmpConfig.securityLevel = 'authNoPriv'; - // Auth, no priv - clear out privacy settings - snmpConfig.privProtocol = ''; - snmpConfig.privKey = ''; - // Set appropriate timeout for security level - snmpConfig.timeout = 10000; // 10 seconds for authentication - } else { - snmpConfig.securityLevel = 'authPriv'; - // Set appropriate timeout for security level - snmpConfig.timeout = 15000; // 15 seconds for full encryption - } - - // Username - const defaultUsername = snmpConfig.username || ''; - const username = await prompt(`SNMPv3 Username [${defaultUsername}]: `); - snmpConfig.username = username.trim() || defaultUsername; - - if (secLevel >= 2) { - // Authentication settings - await this.gatherAuthenticationSettings(snmpConfig, prompt); - - if (secLevel === 3) { - // Privacy settings - await this.gatherPrivacySettings(snmpConfig, prompt); - } - - // Allow customizing the timeout value - const defaultTimeout = snmpConfig.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)) { - snmpConfig.timeout = timeout * 1000; // Convert to ms - } - } - } - - /** - * Gather authentication settings for SNMPv3 - * @param snmpConfig SNMP configuration object to update - * @param prompt Function to prompt for user input - */ - private async gatherAuthenticationSettings( - snmpConfig: any, - prompt: (question: string) => Promise - ): Promise { - // Authentication protocol - console.log('\nAuthentication Protocol:'); - console.log(' 1) MD5'); - console.log(' 2) SHA'); - const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1; - const authProtocolInput = await prompt( - `Select Authentication Protocol [${defaultAuthProtocol}]: ` - ); - const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol; - snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5'; - - // Authentication Key/Password - const defaultAuthKey = snmpConfig.authKey || ''; - const authKey = await prompt(`Authentication Password ${defaultAuthKey ? '[*****]' : ''}: `); - snmpConfig.authKey = authKey.trim() || defaultAuthKey; - } - - /** - * Gather privacy settings for SNMPv3 - * @param snmpConfig SNMP configuration object to update - * @param prompt Function to prompt for user input - */ - private async gatherPrivacySettings( - snmpConfig: any, - prompt: (question: string) => Promise - ): Promise { - // Privacy protocol - console.log('\nPrivacy Protocol:'); - console.log(' 1) DES'); - console.log(' 2) AES'); - const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1; - const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `); - const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol; - snmpConfig.privProtocol = privProtocol === 2 ? 'AES' : 'DES'; - - // Privacy Key/Password - const defaultPrivKey = snmpConfig.privKey || ''; - const privKey = await prompt(`Privacy Password ${defaultPrivKey ? '[*****]' : ''}: `); - snmpConfig.privKey = privKey.trim() || defaultPrivKey; - } - - /** - * Gather threshold settings - * @param thresholds Thresholds configuration object to update - * @param prompt Function to prompt for user input - */ - private async gatherThresholdSettings( - thresholds: any, - prompt: (question: string) => Promise - ): Promise { - console.log('\nShutdown Thresholds:'); - - // Battery threshold - const defaultBatteryThreshold = thresholds.battery || 60; - const batteryThresholdInput = await prompt( - `Battery percentage threshold [${defaultBatteryThreshold}%]: ` - ); - const batteryThreshold = parseInt(batteryThresholdInput, 10); - thresholds.battery = - batteryThresholdInput.trim() && !isNaN(batteryThreshold) - ? batteryThreshold - : defaultBatteryThreshold; - - // Runtime threshold - const defaultRuntimeThreshold = thresholds.runtime || 20; - const runtimeThresholdInput = await prompt( - `Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: ` - ); - const runtimeThreshold = parseInt(runtimeThresholdInput, 10); - thresholds.runtime = - runtimeThresholdInput.trim() && !isNaN(runtimeThreshold) - ? runtimeThreshold - : defaultRuntimeThreshold; - } - - /** - * Gather UPS model settings - * @param snmpConfig SNMP configuration object to update - * @param prompt Function to prompt for user input - */ - private async gatherUpsModelSettings( - snmpConfig: 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 = - snmpConfig.upsModel === 'cyberpower' - ? 1 - : snmpConfig.upsModel === 'apc' - ? 2 - : snmpConfig.upsModel === 'eaton' - ? 3 - : snmpConfig.upsModel === 'tripplite' - ? 4 - : snmpConfig.upsModel === 'liebert' - ? 5 - : snmpConfig.upsModel === 'custom' - ? 6 - : 1; - - const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `); - const modelValue = parseInt(modelInput, 10) || defaultModelValue; - - if (modelValue === 1) { - snmpConfig.upsModel = 'cyberpower'; - } else if (modelValue === 2) { - snmpConfig.upsModel = 'apc'; - } else if (modelValue === 3) { - snmpConfig.upsModel = 'eaton'; - } else if (modelValue === 4) { - snmpConfig.upsModel = 'tripplite'; - } else if (modelValue === 5) { - snmpConfig.upsModel = 'liebert'; - } else if (modelValue === 6) { - snmpConfig.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 - snmpConfig.customOIDs = { - POWER_STATUS: powerStatusOID.trim(), - BATTERY_CAPACITY: batteryCapacityOID.trim(), - BATTERY_RUNTIME: batteryRuntimeOID.trim(), - }; - } - } - - /** - * Optionally test connection to UPS - * @param snmpConfig SNMP configuration to test - * @param prompt Function to prompt for user input - */ - private async optionallyTestConnection( - snmpConfig: 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') { - logger.log('\nTesting connection to UPS...'); - try { - // Create a test config with a short timeout - const testConfig = { - ...snmpConfig, - timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing - }; - - const status = await this.nupst.getSnmp().getUpsStatus(testConfig); - const boxWidth = 45; - logger.log(''); - logger.logBoxTitle('Connection Successful!', boxWidth); - logger.logBoxLine('UPS Status:'); - logger.logBoxLine(`✓ Power Status: ${status.powerStatus}`); - logger.logBoxLine(`✓ Battery Capacity: ${status.batteryCapacity}%`); - logger.logBoxLine(`✓ Runtime Remaining: ${status.batteryRuntime} minutes`); - logger.logBoxEnd(); - } catch (error) { - const errorBoxWidth = 45; - logger.log(''); - logger.logBoxTitle('Connection Failed!', errorBoxWidth); - logger.logBoxLine(`Error: ${error.message}`); - logger.logBoxEnd(); - logger.log('\nPlease check your settings and try again.'); - } - } - } - - /** - * Check if the systemd service is running and restart it if it is - * This is useful after configuration changes - */ - private async restartServiceIfRunning(): Promise { - try { - // Check if the service is active - const isActive = - execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; - - if (isActive) { - // Service is running, restart it - const boxWidth = 45; - logger.logBoxTitle('Service Update', boxWidth); - logger.logBoxLine('Configuration has changed.'); - logger.logBoxLine('Restarting NUPST service to apply changes...'); - - try { - if (process.getuid && process.getuid() === 0) { - // We have root access, restart directly - execSync('systemctl restart nupst.service'); - logger.logBoxLine('Service restarted successfully.'); - } else { - // No root access, show instructions - logger.logBoxLine('Please restart the service with:'); - logger.logBoxLine(' sudo systemctl restart nupst.service'); - } - } catch (error) { - logger.logBoxLine(`Error restarting service: ${error.message}`); - logger.logBoxLine('You may need to restart the service manually:'); - logger.logBoxLine(' sudo systemctl restart nupst.service'); - } - - logger.logBoxEnd(); - } - } catch (error) { - // Ignore errors checking service status - } - } - - /** - * Optionally enable and start 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') { - try { - await this.nupst.getSystemd().install(); - console.log('Service installed and enabled to start on boot.'); - - // Ask if the user wants to start the service now - const startService = await prompt( - 'Would you like to start the NUPST service now? (Y/n): ' - ); - if (startService.toLowerCase() !== 'n') { - await this.nupst.getSystemd().start(); - console.log('NUPST service started successfully.'); - } else { - console.log('Service not started. Use "nupst start" to start the service manually.'); - } - } catch (error) { - console.error(`Failed to setup service: ${error.message}`); - } - } - } +Options: + --debug, -d - Enable debug mode for detailed logging +`); } } \ No newline at end of file diff --git a/ts/cli/group-handler.ts b/ts/cli/group-handler.ts new file mode 100644 index 0000000..e3fdb6b --- /dev/null +++ b/ts/cli/group-handler.ts @@ -0,0 +1,565 @@ +import { Nupst } from '../nupst.js'; +import { logger } from '../logger.js'; +import * as helpers from '../helpers/index.js'; +import { type IGroupConfig } from '../daemon.js'; + +/** + * Class for handling group-related CLI commands + * Provides interface for managing UPS groups + */ +export class GroupHandler { + private readonly nupst: Nupst; + + /** + * Create a new Group handler + * @param nupst Reference to the main Nupst instance + */ + constructor(nupst: Nupst) { + this.nupst = nupst; + } + + /** + * List all UPS groups + */ + public async list(): Promise { + try { + // Try to load configuration + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + const errorBoxWidth = 45; + logger.logBoxTitle('Configuration Error', errorBoxWidth); + logger.logBoxLine('No configuration found.'); + logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); + logger.logBoxEnd(); + return; + } + + // Get current configuration + const config = this.nupst.getDaemon().getConfig(); + + // Check if multi-UPS config + if (!config.groups || !Array.isArray(config.groups)) { + // Legacy or missing groups configuration + const boxWidth = 45; + logger.logBoxTitle('UPS Groups', boxWidth); + logger.logBoxLine('No groups configured.'); + logger.logBoxLine('Use "nupst group add" to add a UPS group.'); + logger.logBoxEnd(); + return; + } + + // Display group list + const boxWidth = 60; + logger.logBoxTitle('UPS Groups', boxWidth); + + if (config.groups.length === 0) { + logger.logBoxLine('No UPS groups configured.'); + logger.logBoxLine('Use "nupst group add" to add a UPS group.'); + } else { + logger.logBoxLine(`Found ${config.groups.length} group(s)`); + logger.logBoxLine(''); + logger.logBoxLine('ID | Name | Mode | UPS Devices'); + logger.logBoxLine('-----------+----------------------+--------------+----------------'); + + for (const group of config.groups) { + const id = group.id.padEnd(10, ' ').substring(0, 10); + const name = (group.name || '').padEnd(20, ' ').substring(0, 20); + const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12); + + // Count UPS devices in this group + const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id)); + const upsCount = upsInGroup.length; + const upsNames = upsInGroup.map(ups => ups.name).join(', '); + + logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`); + } + } + + logger.logBoxEnd(); + } catch (error) { + logger.error(`Failed to list UPS groups: ${error.message}`); + } + } + + /** + * Add a new UPS group + */ + public async add(): Promise { + 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 => { + return new Promise((resolve) => { + rl.question(question, (answer: string) => { + resolve(answer); + }); + }); + }; + + try { + // Try to load configuration + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); + return; + } + + // Get current configuration + const config = this.nupst.getDaemon().getConfig(); + + // Initialize groups array if not exists + if (!config.groups) { + config.groups = []; + } + + // Check if upsDevices is initialized + if (!config.upsDevices) { + config.upsDevices = []; + } + + logger.log('\nNUPST Add Group'); + logger.log('==============\n'); + logger.log('This will guide you through creating a new UPS group.\n'); + + // Generate a new unique group ID + const groupId = helpers.shortId(); + + // Get group name + const name = await prompt('Group Name: '); + + // Get group mode + const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: '); + const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant'; + + // Get optional description + const description = await prompt('Group Description (optional): '); + + // Create the new group + const newGroup: IGroupConfig = { + id: groupId, + name: name || `Group-${groupId}`, + mode, + description: description || undefined + }; + + // Add the group to the configuration + config.groups.push(newGroup); + + // Save the configuration + await this.nupst.getDaemon().saveConfig(config); + + // Display summary + const boxWidth = 45; + logger.logBoxTitle('Group Created', boxWidth); + logger.logBoxLine(`ID: ${newGroup.id}`); + logger.logBoxLine(`Name: ${newGroup.name}`); + logger.logBoxLine(`Mode: ${newGroup.mode}`); + if (newGroup.description) { + logger.logBoxLine(`Description: ${newGroup.description}`); + } + logger.logBoxEnd(); + + // Check if there are UPS devices to assign to this group + if (config.upsDevices.length > 0) { + const assignUps = await prompt('Would you like to assign UPS devices to this group now? (y/N): '); + if (assignUps.toLowerCase() === 'y') { + await this.assignUpsToGroup(newGroup.id, config, prompt); + + // Save again after assigning UPS devices + await this.nupst.getDaemon().saveConfig(config); + } + } + + // Check if service is running and restart it if needed + this.nupst.getUpsHandler().restartServiceIfRunning(); + + logger.log('\nGroup setup complete!'); + } finally { + rl.close(); + } + } catch (error) { + logger.error(`Add group error: ${error.message}`); + } + } + + /** + * Edit an existing UPS group + * @param groupId ID of the group to edit + */ + public async edit(groupId: string): Promise { + 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 => { + return new Promise((resolve) => { + rl.question(question, (answer: string) => { + resolve(answer); + }); + }); + }; + + try { + // Try to load configuration + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); + return; + } + + // Get current configuration + const config = this.nupst.getDaemon().getConfig(); + + // Check if groups are initialized + if (!config.groups || !Array.isArray(config.groups)) { + logger.error('No groups configured. Please run "nupst group add" first to create a group.'); + return; + } + + // Find the group to edit + const groupIndex = config.groups.findIndex(group => group.id === groupId); + if (groupIndex === -1) { + logger.error(`Group with ID "${groupId}" not found.`); + return; + } + + const group = config.groups[groupIndex]; + + logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`); + logger.log('==============================================\n'); + + // Edit group name + const newName = await prompt(`Group Name [${group.name}]: `); + if (newName.trim()) { + group.name = newName; + } + + // Edit group mode + const currentMode = group.mode || 'redundant'; + const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `); + if (modeInput.trim()) { + group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant'; + } + + // Edit description + const currentDesc = group.description || ''; + const newDesc = await prompt(`Group Description [${currentDesc}]: `); + if (newDesc.trim() || newDesc === '') { + group.description = newDesc.trim() || undefined; + } + + // Update the group in the configuration + config.groups[groupIndex] = group; + + // Save the configuration + await this.nupst.getDaemon().saveConfig(config); + + // Display summary + const boxWidth = 45; + logger.logBoxTitle('Group Updated', boxWidth); + logger.logBoxLine(`ID: ${group.id}`); + logger.logBoxLine(`Name: ${group.name}`); + logger.logBoxLine(`Mode: ${group.mode}`); + if (group.description) { + logger.logBoxLine(`Description: ${group.description}`); + } + logger.logBoxEnd(); + + // Edit UPS assignments if requested + const editAssignments = await prompt('Would you like to edit UPS assignments for this group? (y/N): '); + if (editAssignments.toLowerCase() === 'y') { + await this.assignUpsToGroup(group.id, config, prompt); + + // Save again after editing assignments + await this.nupst.getDaemon().saveConfig(config); + } + + // Check if service is running and restart it if needed + this.nupst.getUpsHandler().restartServiceIfRunning(); + + logger.log('\nGroup edit complete!'); + } finally { + rl.close(); + } + } catch (error) { + logger.error(`Edit group error: ${error.message}`); + } + } + + /** + * Delete an existing UPS group + * @param groupId ID of the group to delete + */ + public async delete(groupId: string): Promise { + try { + // Try to load configuration + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); + return; + } + + // Get current configuration + const config = this.nupst.getDaemon().getConfig(); + + // Check if groups are initialized + if (!config.groups || !Array.isArray(config.groups)) { + logger.error('No groups configured.'); + return; + } + + // Find the group to delete + const groupIndex = config.groups.findIndex(group => group.id === groupId); + if (groupIndex === -1) { + logger.error(`Group with ID "${groupId}" not found.`); + return; + } + + const groupToDelete = config.groups[groupIndex]; + + // Get confirmation before deleting + const readline = await import('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const confirm = await new Promise(resolve => { + rl.question(`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => { + resolve(answer.toLowerCase()); + }); + }); + + rl.close(); + + if (confirm !== 'y' && confirm !== 'yes') { + logger.log('Deletion cancelled.'); + return; + } + + // Remove this group from all UPS device group assignments + if (config.upsDevices && Array.isArray(config.upsDevices)) { + for (const ups of config.upsDevices) { + const groupIndex = ups.groups.indexOf(groupId); + if (groupIndex !== -1) { + ups.groups.splice(groupIndex, 1); + } + } + } + + // Remove the group from the array + config.groups.splice(groupIndex, 1); + + // Save the configuration + await this.nupst.getDaemon().saveConfig(config); + + logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`); + + // Check if service is running and restart it if needed + this.nupst.getUpsHandler().restartServiceIfRunning(); + } catch (error) { + logger.error(`Failed to delete group: ${error.message}`); + } + } + + /** + * Assign UPS devices to groups + * @param ups UPS configuration to update + * @param groups Available groups + * @param prompt Function to prompt for user input + */ + public async assignUpsToGroups( + ups: any, + groups: any[], + prompt: (question: string) => Promise + ): Promise { + // Initialize groups array if it doesn't exist + if (!ups.groups) { + ups.groups = []; + } + + // Show current group assignments + logger.log('\nCurrent Group Assignments:'); + if (ups.groups && ups.groups.length > 0) { + for (const groupId of ups.groups) { + const group = groups.find(g => g.id === groupId); + if (group) { + logger.log(`- ${group.name} (${group.id})`); + } else { + logger.log(`- Unknown group (${groupId})`); + } + } + } else { + logger.log('- None'); + } + + // Show available groups + logger.log('\nAvailable Groups:'); + if (groups.length === 0) { + logger.log('- No groups available. Use "nupst group add" to create groups.'); + return; + } + + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + const assigned = ups.groups && ups.groups.includes(group.id); + logger.log(`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`); + } + + // Prompt for group selection + const selection = await prompt('\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): '); + + if (selection.toLowerCase() === 'clear') { + // Clear all group assignments + ups.groups = []; + logger.log('All group assignments cleared.'); + return; + } + + if (!selection.trim()) { + // No change if empty input + return; + } + + // Process selections + const selections = selection.split(',').map(s => s.trim()); + + for (const sel of selections) { + const index = parseInt(sel, 10) - 1; + if (isNaN(index) || index < 0 || index >= groups.length) { + logger.error(`Invalid selection: ${sel}`); + continue; + } + + const group = groups[index]; + + // Initialize groups array if needed (should already be done above) + if (!ups.groups) { + ups.groups = []; + } + + // Toggle assignment + const groupIndex = ups.groups.indexOf(group.id); + if (groupIndex === -1) { + // Add to group + ups.groups.push(group.id); + logger.log(`Added to group: ${group.name} (${group.id})`); + } else { + // Remove from group + ups.groups.splice(groupIndex, 1); + logger.log(`Removed from group: ${group.name} (${group.id})`); + } + } + } + + /** + * Assign UPS devices to a specific group + * @param groupId Group ID to assign UPS devices to + * @param config Full configuration + * @param prompt Function to prompt for user input + */ + public async assignUpsToGroup( + groupId: string, + config: any, + prompt: (question: string) => Promise + ): Promise { + if (!config.upsDevices || config.upsDevices.length === 0) { + logger.log('No UPS devices available. Use "nupst add" to add UPS devices.'); + return; + } + + const group = config.groups.find(g => g.id === groupId); + if (!group) { + logger.error(`Group with ID "${groupId}" not found.`); + return; + } + + // Show current assignments + logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`); + const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(groupId)); + if (upsInGroup.length === 0) { + logger.log('- None'); + } else { + for (const ups of upsInGroup) { + logger.log(`- ${ups.name} (${ups.id})`); + } + } + + // Show all UPS devices + logger.log('\nAvailable UPS devices:'); + for (let i = 0; i < config.upsDevices.length; i++) { + const ups = config.upsDevices[i]; + const assigned = ups.groups && ups.groups.includes(groupId); + logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`); + } + + // Prompt for UPS selection + const selection = await prompt('\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): '); + + if (selection.toLowerCase() === 'clear') { + // Clear all UPS from this group + for (const ups of config.upsDevices) { + if (ups.groups) { + const groupIndex = ups.groups.indexOf(groupId); + if (groupIndex !== -1) { + ups.groups.splice(groupIndex, 1); + } + } + } + logger.log(`All UPS devices removed from group "${group.name}".`); + return; + } + + if (!selection.trim()) { + // No change if empty input + return; + } + + // Process selections + const selections = selection.split(',').map(s => s.trim()); + + for (const sel of selections) { + const index = parseInt(sel, 10) - 1; + if (isNaN(index) || index < 0 || index >= config.upsDevices.length) { + logger.error(`Invalid selection: ${sel}`); + continue; + } + + const ups = config.upsDevices[index]; + + // Initialize groups array if needed + if (!ups.groups) { + ups.groups = []; + } + + // Toggle assignment + const groupIndex = ups.groups.indexOf(groupId); + if (groupIndex === -1) { + // Add to group + ups.groups.push(groupId); + logger.log(`Added "${ups.name}" to group "${group.name}"`); + } else { + // Remove from group + ups.groups.splice(groupIndex, 1); + logger.log(`Removed "${ups.name}" from group "${group.name}"`); + } + } + } +} \ No newline at end of file diff --git a/ts/cli/service-handler.ts b/ts/cli/service-handler.ts new file mode 100644 index 0000000..32c3512 --- /dev/null +++ b/ts/cli/service-handler.ts @@ -0,0 +1,320 @@ +import { execSync } from 'child_process'; +import { Nupst } from '../nupst.js'; +import { logger } from '../logger.js'; + +/** + * Class for handling service-related CLI commands + * Provides interface for managing systemd service + */ +export class ServiceHandler { + private readonly nupst: Nupst; + + /** + * Create a new Service handler + * @param nupst Reference to the main Nupst instance + */ + constructor(nupst: Nupst) { + this.nupst = nupst; + } + + /** + * Enable the service (requires root) + */ + public async enable(): Promise { + this.checkRootAccess('This command must be run as root.'); + await this.nupst.getSystemd().install(); + logger.log('NUPST service has been installed. Use "nupst start" to start the service.'); + } + + /** + * Start the daemon directly + * @param debugMode Whether to enable debug mode + */ + public async daemonStart(debugMode: boolean = false): Promise { + logger.log('Starting NUPST daemon...'); + try { + // Enable debug mode for SNMP if requested + if (debugMode) { + this.nupst.getSnmp().enableDebug(); + logger.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 + */ + public async logs(): Promise { + try { + // Use exec with spawn to properly follow logs in real-time + const { spawn } = await import('child_process'); + logger.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((resolve) => { + journalctl.on('exit', () => resolve()); + }); + } catch (error) { + logger.error(`Failed to retrieve logs: ${error}`); + process.exit(1); + } + } + + /** + * Stop the systemd service + */ + public async stop(): Promise { + await this.nupst.getSystemd().stop(); + } + + /** + * Start the systemd service + */ + public 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 + */ + public async status(): Promise { + // Extract debug options from args array + const debugOptions = this.extractDebugOptions(process.argv); + await this.nupst.getSystemd().getStatus(debugOptions.debugMode); + } + + /** + * Disable the service (requires root) + */ + public 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) { + logger.error(errorMessage); + process.exit(1); + } + } + + /** + * Update NUPST from repository and refresh systemd service + */ + public async update(): Promise { + try { + // Check if running as root + this.checkRootAccess( + 'This command must be run as root to update NUPST and refresh the systemd service.' + ); + + const boxWidth = 45; + logger.logBoxTitle('NUPST Update Process', boxWidth); + logger.logBoxLine('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 + logger.logBoxLine(`Using local installation directory: ${installDir}`); + } + + try { + // 1. Update the repository + logger.logBoxLine('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 + logger.logBoxLine('Running install.sh to update NUPST...'); + execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); + + // 3. Run the setup.sh script with force flag to update Node.js and dependencies + logger.logBoxLine('Running setup.sh to update Node.js and dependencies...'); + execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' }); + + // 4. Refresh the systemd service + logger.logBoxLine('Refreshing systemd service...'); + + // First check if service exists + let serviceExists = false; + try { + const output = execSync('systemctl list-unit-files | grep nupst.service').toString(); + serviceExists = output.includes('nupst.service'); + } catch (error) { + // If grep fails (service not found), serviceExists remains false + serviceExists = false; + } + + if (serviceExists) { + // Stop the service if it's running + const isRunning = + execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; + if (isRunning) { + logger.logBoxLine('Stopping nupst service...'); + execSync('systemctl stop nupst.service'); + } + + // Reinstall the service + logger.logBoxLine('Reinstalling systemd service...'); + await this.nupst.getSystemd().install(); + + // Restart the service if it was running + if (isRunning) { + logger.logBoxLine('Restarting nupst service...'); + execSync('systemctl start nupst.service'); + } + } else { + logger.logBoxLine('Systemd service not installed, skipping service refresh.'); + logger.logBoxLine('Run "nupst enable" to install the service.'); + } + + logger.logBoxLine('Update completed successfully!'); + logger.logBoxEnd(); + } catch (error) { + logger.logBoxLine('Error during update process:'); + logger.logBoxLine(`${error.message}`); + logger.logBoxEnd(); + process.exit(1); + } + } catch (error) { + logger.error(`Update failed: ${error.message}`); + process.exit(1); + } + } + + /** + * Completely uninstall NUPST from the system + */ + public async uninstall(): Promise { + // 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 => { + 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 { dirname, join } = await import('path'); + const modulePath = dirname(dirname(binPath)); + uninstallScriptPath = join(modulePath, 'uninstall.sh'); + + // Check if the script exists + const { access } = await import('fs/promises'); + await access(uninstallScriptPath); + } catch (error) { + // If we can't find it in the expected location, try common installation paths + const commonPaths = ['/opt/nupst/uninstall.sh', `${process.cwd()}/uninstall.sh`]; + const { existsSync } = await import('fs'); + + uninstallScriptPath = ''; + for (const path of commonPaths) { + if (existsSync(path)) { + uninstallScriptPath = path; + break; + } + } + + 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); + } + } + + /** + * Extract and remove debug options from args array + * @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 }; + } +} \ No newline at end of file diff --git a/ts/cli/ups-handler.ts b/ts/cli/ups-handler.ts new file mode 100644 index 0000000..e58dba8 --- /dev/null +++ b/ts/cli/ups-handler.ts @@ -0,0 +1,986 @@ +import { execSync } from 'child_process'; +import { Nupst } from '../nupst.js'; +import { logger } from '../logger.js'; +import * as helpers from '../helpers/index.js'; + +/** + * Class for handling UPS-related CLI commands + * Provides interface for managing UPS devices + */ +export class UpsHandler { + private readonly nupst: Nupst; + + /** + * Create a new UPS handler + * @param nupst Reference to the main Nupst instance + */ + constructor(nupst: Nupst) { + this.nupst = nupst; + } + + /** + * Add a new UPS configuration + */ + public async add(): Promise { + 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 => { + return new Promise((resolve) => { + rl.question(question, (answer: string) => { + resolve(answer); + }); + }); + }; + + try { + await this.runAddProcess(prompt); + } finally { + rl.close(); + } + } catch (error) { + logger.error(`Add UPS error: ${error.message}`); + } + } + + /** + * Run the interactive process to add a new UPS + * @param prompt Function to prompt for user input + */ + public async runAddProcess(prompt: (question: string) => Promise): Promise { + logger.log('\nNUPST Add UPS'); + logger.log('=============\n'); + logger.log('This will guide you through configuring a new UPS.\n'); + + // Try to load existing config if available + let config; + try { + await this.nupst.getDaemon().loadConfig(); + config = this.nupst.getDaemon().getConfig(); + + // Convert old format to new format if needed + if (!config.upsDevices) { + // Initialize with the current config as the first UPS + config = { + checkInterval: config.checkInterval, + upsDevices: [{ + id: 'default', + name: 'Default UPS', + snmp: config.snmp, + thresholds: config.thresholds, + groups: [] + }], + groups: [] + }; + logger.log('Converting existing configuration to multi-UPS format.'); + } + } catch (error) { + // If config doesn't exist, initialize with empty config + config = { + checkInterval: 30000, // Default check interval + upsDevices: [], + groups: [] + }; + logger.log('No existing configuration found. Creating a new configuration.'); + } + + // Get UPS ID and name + const upsId = helpers.shortId(); + const name = await prompt('UPS Name: '); + + // Create a new UPS configuration object with defaults + const newUps = { + id: upsId, + name: name || `UPS-${upsId}`, + snmp: { + host: '127.0.0.1', + port: 161, + community: 'public', + version: 1, + timeout: 5000, + upsModel: 'cyberpower' + }, + thresholds: { + battery: 60, + runtime: 20 + }, + groups: [] + }; + + // Gather SNMP settings + await this.gatherSnmpSettings(newUps.snmp, prompt); + + // Gather threshold settings + await this.gatherThresholdSettings(newUps.thresholds, prompt); + + // Gather UPS model settings + await this.gatherUpsModelSettings(newUps.snmp, prompt); + + // Get access to GroupHandler for group assignments + const groupHandler = this.nupst.getGroupHandler(); + + // Assign to groups if any exist + if (config.groups && config.groups.length > 0) { + await groupHandler.assignUpsToGroups(newUps, config.groups, prompt); + } + + // Add the new UPS to the config + config.upsDevices.push(newUps); + + // Save the configuration + await this.nupst.getDaemon().saveConfig(config); + + this.displayUpsConfigSummary(newUps); + + // Test the connection if requested + await this.optionallyTestConnection(newUps.snmp, prompt); + + // Check if service is running and restart it if needed + await this.restartServiceIfRunning(); + + logger.log('\nSetup complete!'); + } + + /** + * Edit an existing UPS configuration + * @param upsId ID of the UPS to edit (undefined for default UPS) + */ + public async edit(upsId?: string): Promise { + 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 => { + return new Promise((resolve) => { + rl.question(question, (answer: string) => { + resolve(answer); + }); + }); + }; + + try { + await this.runEditProcess(upsId, prompt); + } finally { + rl.close(); + } + } catch (error) { + logger.error(`Edit UPS error: ${error.message}`); + } + } + + /** + * Run the interactive process to edit a UPS + * @param upsId ID of the UPS to edit (undefined for default UPS) + * @param prompt Function to prompt for user input + */ + public async runEditProcess(upsId: string | undefined, prompt: (question: string) => Promise): Promise { + logger.log('\nNUPST Edit UPS'); + logger.log('=============\n'); + + // Try to load existing config + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + if (!upsId) { + // For default UPS (no ID specified), run setup if no config exists + logger.log('No existing configuration found. Running setup for new UPS.'); + await this.runAddProcess(prompt); + return; + } else { + // For specific UPS ID, error if config doesn't exist + logger.error('No configuration found. Please run "nupst setup" first.'); + return; + } + } + + // Get the config + const config = this.nupst.getDaemon().getConfig(); + + // Convert old format to new format if needed + if (!config.upsDevices) { + // Initialize with the current config as the first UPS + config.upsDevices = [{ + id: 'default', + name: 'Default UPS', + snmp: config.snmp, + thresholds: config.thresholds, + groups: [] + }]; + config.groups = []; + logger.log('Converting existing configuration to multi-UPS format.'); + } + + // Find the UPS to edit + let upsToEdit; + if (upsId) { + // Find specific UPS by ID + upsToEdit = config.upsDevices.find(ups => ups.id === upsId); + if (!upsToEdit) { + logger.error(`UPS with ID "${upsId}" not found.`); + return; + } + logger.log(`Editing UPS: ${upsToEdit.name} (${upsToEdit.id})\n`); + } else { + // For backward compatibility, edit the first UPS if no ID specified + if (config.upsDevices.length === 0) { + logger.error('No UPS devices configured. Please run "nupst add" to add a UPS.'); + return; + } + upsToEdit = config.upsDevices[0]; + logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`); + } + + // Allow editing UPS name + const newName = await prompt(`UPS Name [${upsToEdit.name}]: `); + if (newName.trim()) { + upsToEdit.name = newName; + } + + // Edit SNMP settings + await this.gatherSnmpSettings(upsToEdit.snmp, prompt); + + // Edit threshold settings + await this.gatherThresholdSettings(upsToEdit.thresholds, prompt); + + // Edit UPS model settings + await this.gatherUpsModelSettings(upsToEdit.snmp, prompt); + + // Get access to GroupHandler for group assignments + const groupHandler = this.nupst.getGroupHandler(); + + // Edit group assignments + if (config.groups && config.groups.length > 0) { + await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt); + } + + // Save the configuration + await this.nupst.getDaemon().saveConfig(config); + + this.displayUpsConfigSummary(upsToEdit); + + // Test the connection if requested + await this.optionallyTestConnection(upsToEdit.snmp, prompt); + + // Check if service is running and restart it if needed + await this.restartServiceIfRunning(); + + logger.log('\nEdit complete!'); + } + + /** + * Delete a UPS by ID + * @param upsId ID of the UPS to delete + */ + public async delete(upsId: string): Promise { + try { + // Try to load configuration + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + const errorBoxWidth = 45; + logger.logBoxTitle('Configuration Error', errorBoxWidth); + logger.logBoxLine('No configuration found.'); + logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); + logger.logBoxEnd(); + return; + } + + // Get current configuration + const config = this.nupst.getDaemon().getConfig(); + + // Check if multi-UPS config + if (!config.upsDevices || !Array.isArray(config.upsDevices)) { + logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.'); + logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.'); + return; + } + + // Find the UPS to delete + const upsIndex = config.upsDevices.findIndex(ups => ups.id === upsId); + if (upsIndex === -1) { + logger.error(`UPS with ID "${upsId}" not found.`); + return; + } + + const upsToDelete = config.upsDevices[upsIndex]; + + // Get confirmation before deleting + const readline = await import('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const confirm = await new Promise(resolve => { + rl.question(`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `, answer => { + resolve(answer.toLowerCase()); + }); + }); + + rl.close(); + + if (confirm !== 'y' && confirm !== 'yes') { + logger.log('Deletion cancelled.'); + return; + } + + // Remove the UPS from the array + config.upsDevices.splice(upsIndex, 1); + + // Save the configuration + await this.nupst.getDaemon().saveConfig(config); + + logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`); + + // Check if service is running and restart it if needed + await this.restartServiceIfRunning(); + } catch (error) { + logger.error(`Failed to delete UPS: ${error.message}`); + } + } + + /** + * List all configured UPS devices + */ + public async list(): Promise { + try { + // Try to load configuration + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + const errorBoxWidth = 45; + logger.logBoxTitle('Configuration Error', errorBoxWidth); + logger.logBoxLine('No configuration found.'); + logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); + logger.logBoxEnd(); + return; + } + + // Get current configuration + const config = this.nupst.getDaemon().getConfig(); + + // Check if multi-UPS config + if (!config.upsDevices || !Array.isArray(config.upsDevices)) { + // Legacy single UPS configuration + const boxWidth = 45; + logger.logBoxTitle('UPS Devices', boxWidth); + logger.logBoxLine('Legacy single-UPS configuration detected.'); + logger.logBoxLine(''); + logger.logBoxLine('Default UPS:'); + logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`); + logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`); + logger.logBoxLine(` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`); + logger.logBoxLine(''); + logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate'); + logger.logBoxLine('to the multi-UPS configuration format.'); + logger.logBoxEnd(); + return; + } + + // Display UPS list + const boxWidth = 60; + logger.logBoxTitle('UPS Devices', boxWidth); + + if (config.upsDevices.length === 0) { + logger.logBoxLine('No UPS devices configured.'); + logger.logBoxLine('Use "nupst add" to add a UPS device.'); + } else { + logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`); + logger.logBoxLine(''); + logger.logBoxLine('ID | Name | Host | Mode | Groups'); + logger.logBoxLine('-----------+----------------------+----------------+--------------+----------------'); + + for (const ups of config.upsDevices) { + const id = ups.id.padEnd(10, ' ').substring(0, 10); + const name = (ups.name || '').padEnd(20, ' ').substring(0, 20); + const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15); + const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12); + const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None'; + + logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`); + } + } + + logger.logBoxEnd(); + } catch (error) { + logger.error(`Failed to list UPS devices: ${error.message}`); + } + } + + /** + * Test the current configuration by connecting to the UPS + * @param debugMode Whether to enable debug mode + */ + public async test(debugMode: boolean = false): Promise { + try { + // Debug mode is now handled in parseAndExecute + if (debugMode) { + const boxWidth = 45; + logger.logBoxTitle('Debug Mode', boxWidth); + logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown'); + logger.logBoxEnd(); + } + + // Try to load the configuration + try { + await this.nupst.getDaemon().loadConfig(); + } catch (error) { + const errorBoxWidth = 45; + logger.logBoxTitle('Configuration Error', errorBoxWidth); + logger.logBoxLine('No configuration found.'); + logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); + logger.logBoxEnd(); + return; + } + + // Get current configuration + const config = this.nupst.getDaemon().getConfig(); + + // Handle new multi-UPS configuration format + if (config.upsDevices && config.upsDevices.length > 0) { + logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`); + + for (let i = 0; i < config.upsDevices.length; i++) { + const ups = config.upsDevices[i]; + logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`); + this.displayTestConfig(ups); + await this.testConnection(ups); + } + } else { + // Legacy configuration format + this.displayTestConfig(config); + await this.testConnection(config); + } + } catch (error) { + logger.error(`Test failed: ${error.message}`); + } + } + + /** + * Display the configuration for testing + * @param config Current configuration or individual UPS configuration + */ + private displayTestConfig(config: any): void { + // Check if this is a UPS device or full configuration + const isUpsConfig = config.snmp && config.thresholds; + const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {}; + const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {}; + const checkInterval = config.checkInterval || 30000; + + // Get UPS name and ID if available + const upsName = config.name ? config.name : 'Default UPS'; + const upsId = config.id ? config.id : 'default'; + + const boxWidth = 45; + logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth); + logger.logBoxLine(`UPS ID: ${upsId}`); + logger.logBoxLine('SNMP Settings:'); + logger.logBoxLine(` Host: ${snmpConfig.host}`); + logger.logBoxLine(` Port: ${snmpConfig.port}`); + logger.logBoxLine(` Version: ${snmpConfig.version}`); + logger.logBoxLine(` UPS Model: ${snmpConfig.upsModel || 'cyberpower'}`); + + if (snmpConfig.version === 1 || snmpConfig.version === 2) { + logger.logBoxLine(` Community: ${snmpConfig.community}`); + } else if (snmpConfig.version === 3) { + logger.logBoxLine(` Security Level: ${snmpConfig.securityLevel}`); + logger.logBoxLine(` Username: ${snmpConfig.username}`); + + // Show auth and privacy details based on security level + if (snmpConfig.securityLevel === 'authNoPriv' || snmpConfig.securityLevel === 'authPriv') { + logger.logBoxLine(` Auth Protocol: ${snmpConfig.authProtocol || 'None'}`); + } + + if (snmpConfig.securityLevel === 'authPriv') { + logger.logBoxLine(` Privacy Protocol: ${snmpConfig.privProtocol || 'None'}`); + } + + // Show timeout value + logger.logBoxLine(` Timeout: ${snmpConfig.timeout / 1000} seconds`); + } + + // Show OIDs if custom model is selected + if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) { + logger.logBoxLine('Custom OIDs:'); + logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`); + logger.logBoxLine(` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`); + logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`); + } + logger.logBoxLine('Thresholds:'); + logger.logBoxLine(` Battery: ${thresholds.battery}%`); + logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`); + + // Show group assignments if this is a UPS config + if (config.groups && Array.isArray(config.groups)) { + logger.logBoxLine(`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`); + } + + logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`); + logger.logBoxEnd(); + } + + /** + * Test connection to the UPS + * @param config Current UPS configuration or legacy config + */ + private async testConnection(config: any): Promise { + const upsId = config.id || 'default'; + const upsName = config.name || 'Default UPS'; + logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`); + + try { + // Create a test config with a short timeout + const snmpConfig = config.snmp ? config.snmp : config.snmp; + const thresholds = config.thresholds ? config.thresholds : config.thresholds; + + const testConfig = { + ...snmpConfig, + timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing + }; + + const status = await this.nupst.getSnmp().getUpsStatus(testConfig); + + const boxWidth = 45; + logger.logBoxTitle(`Connection Successful: ${upsName}`, boxWidth); + logger.logBoxLine('UPS Status:'); + logger.logBoxLine(` Power Status: ${status.powerStatus}`); + logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`); + logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`); + logger.logBoxEnd(); + + // Check status against thresholds if on battery + if (status.powerStatus === 'onBattery') { + this.analyzeThresholds(status, thresholds); + } + } catch (error) { + const errorBoxWidth = 45; + logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth); + logger.logBoxLine(`Error: ${error.message}`); + logger.logBoxEnd(); + logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS."); + } + } + + /** + * Analyze UPS status against thresholds + * @param status UPS status + * @param thresholds Threshold configuration + */ + private analyzeThresholds(status: any, thresholds: any): void { + const boxWidth = 45; + logger.logBoxTitle('Threshold Analysis', boxWidth); + + if (status.batteryCapacity < thresholds.battery) { + logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold'); + logger.logBoxLine( + ` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%` + ); + logger.logBoxLine(' System would initiate shutdown'); + } else { + logger.logBoxLine('✓ Battery capacity above threshold'); + logger.logBoxLine( + ` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%` + ); + } + + if (status.batteryRuntime < thresholds.runtime) { + logger.logBoxLine('⚠️ WARNING: Runtime below threshold'); + logger.logBoxLine( + ` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min` + ); + logger.logBoxLine(' System would initiate shutdown'); + } else { + logger.logBoxLine('✓ Runtime above threshold'); + logger.logBoxLine( + ` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min` + ); + } + + logger.logBoxEnd(); + } + + /** + * Gather SNMP settings + * @param snmpConfig SNMP configuration object to update + * @param prompt Function to prompt for user input + */ + private async gatherSnmpSettings( + snmpConfig: any, + prompt: (question: string) => Promise + ): Promise { + // SNMP IP Address + const defaultHost = snmpConfig.host || '127.0.0.1'; + const host = await prompt(`UPS IP Address [${defaultHost}]: `); + snmpConfig.host = host.trim() || defaultHost; + + // SNMP Port + const defaultPort = snmpConfig.port || 161; + const portInput = await prompt(`SNMP Port [${defaultPort}]: `); + const port = parseInt(portInput, 10); + snmpConfig.port = portInput.trim() && !isNaN(port) ? port : defaultPort; + + // SNMP Version + const defaultVersion = snmpConfig.version || 1; + 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); + snmpConfig.version = + versionInput.trim() && (version === 1 || version === 2 || version === 3) + ? version + : defaultVersion; + + if (snmpConfig.version === 1 || snmpConfig.version === 2) { + // SNMP Community String (for v1/v2c) + const defaultCommunity = snmpConfig.community || 'public'; + const community = await prompt(`SNMP Community String [${defaultCommunity}]: `); + snmpConfig.community = community.trim() || defaultCommunity; + } else if (snmpConfig.version === 3) { + // SNMP v3 settings + await this.gatherSnmpV3Settings(snmpConfig, prompt); + } + } + + /** + * Gather SNMPv3 specific settings + * @param snmpConfig SNMP configuration object to update + * @param prompt Function to prompt for user input + */ + private async gatherSnmpV3Settings( + snmpConfig: 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 = snmpConfig.securityLevel + ? snmpConfig.securityLevel === 'noAuthNoPriv' + ? 1 + : snmpConfig.securityLevel === 'authNoPriv' + ? 2 + : 3 + : 3; + const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `); + const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel; + + if (secLevel === 1) { + snmpConfig.securityLevel = 'noAuthNoPriv'; + // No auth, no priv - clear out authentication and privacy settings + snmpConfig.authProtocol = ''; + snmpConfig.authKey = ''; + snmpConfig.privProtocol = ''; + snmpConfig.privKey = ''; + // Set appropriate timeout for security level + snmpConfig.timeout = 5000; // 5 seconds for basic security + } else if (secLevel === 2) { + snmpConfig.securityLevel = 'authNoPriv'; + // Auth, no priv - clear out privacy settings + snmpConfig.privProtocol = ''; + snmpConfig.privKey = ''; + // Set appropriate timeout for security level + snmpConfig.timeout = 10000; // 10 seconds for authentication + } else { + snmpConfig.securityLevel = 'authPriv'; + // Set appropriate timeout for security level + snmpConfig.timeout = 15000; // 15 seconds for full encryption + } + + // Username + const defaultUsername = snmpConfig.username || ''; + const username = await prompt(`SNMPv3 Username [${defaultUsername}]: `); + snmpConfig.username = username.trim() || defaultUsername; + + if (secLevel >= 2) { + // Authentication settings + await this.gatherAuthenticationSettings(snmpConfig, prompt); + + if (secLevel === 3) { + // Privacy settings + await this.gatherPrivacySettings(snmpConfig, prompt); + } + + // Allow customizing the timeout value + const defaultTimeout = snmpConfig.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)) { + snmpConfig.timeout = timeout * 1000; // Convert to ms + } + } + } + + /** + * Gather authentication settings for SNMPv3 + * @param snmpConfig SNMP configuration object to update + * @param prompt Function to prompt for user input + */ + private async gatherAuthenticationSettings( + snmpConfig: any, + prompt: (question: string) => Promise + ): Promise { + // Authentication protocol + console.log('\nAuthentication Protocol:'); + console.log(' 1) MD5'); + console.log(' 2) SHA'); + const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1; + const authProtocolInput = await prompt( + `Select Authentication Protocol [${defaultAuthProtocol}]: ` + ); + const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol; + snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5'; + + // Authentication Key/Password + const defaultAuthKey = snmpConfig.authKey || ''; + const authKey = await prompt(`Authentication Password ${defaultAuthKey ? '[*****]' : ''}: `); + snmpConfig.authKey = authKey.trim() || defaultAuthKey; + } + + /** + * Gather privacy settings for SNMPv3 + * @param snmpConfig SNMP configuration object to update + * @param prompt Function to prompt for user input + */ + private async gatherPrivacySettings( + snmpConfig: any, + prompt: (question: string) => Promise + ): Promise { + // Privacy protocol + console.log('\nPrivacy Protocol:'); + console.log(' 1) DES'); + console.log(' 2) AES'); + const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1; + const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `); + const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol; + snmpConfig.privProtocol = privProtocol === 2 ? 'AES' : 'DES'; + + // Privacy Key/Password + const defaultPrivKey = snmpConfig.privKey || ''; + const privKey = await prompt(`Privacy Password ${defaultPrivKey ? '[*****]' : ''}: `); + snmpConfig.privKey = privKey.trim() || defaultPrivKey; + } + + /** + * Gather threshold settings + * @param thresholds Thresholds configuration object to update + * @param prompt Function to prompt for user input + */ + private async gatherThresholdSettings( + thresholds: any, + prompt: (question: string) => Promise + ): Promise { + console.log('\nShutdown Thresholds:'); + + // Battery threshold + const defaultBatteryThreshold = thresholds.battery || 60; + const batteryThresholdInput = await prompt( + `Battery percentage threshold [${defaultBatteryThreshold}%]: ` + ); + const batteryThreshold = parseInt(batteryThresholdInput, 10); + thresholds.battery = + batteryThresholdInput.trim() && !isNaN(batteryThreshold) + ? batteryThreshold + : defaultBatteryThreshold; + + // Runtime threshold + const defaultRuntimeThreshold = thresholds.runtime || 20; + const runtimeThresholdInput = await prompt( + `Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: ` + ); + const runtimeThreshold = parseInt(runtimeThresholdInput, 10); + thresholds.runtime = + runtimeThresholdInput.trim() && !isNaN(runtimeThreshold) + ? runtimeThreshold + : defaultRuntimeThreshold; + } + + /** + * Gather UPS model settings + * @param snmpConfig SNMP configuration object to update + * @param prompt Function to prompt for user input + */ + private async gatherUpsModelSettings( + snmpConfig: 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 = + snmpConfig.upsModel === 'cyberpower' + ? 1 + : snmpConfig.upsModel === 'apc' + ? 2 + : snmpConfig.upsModel === 'eaton' + ? 3 + : snmpConfig.upsModel === 'tripplite' + ? 4 + : snmpConfig.upsModel === 'liebert' + ? 5 + : snmpConfig.upsModel === 'custom' + ? 6 + : 1; + + const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `); + const modelValue = parseInt(modelInput, 10) || defaultModelValue; + + if (modelValue === 1) { + snmpConfig.upsModel = 'cyberpower'; + } else if (modelValue === 2) { + snmpConfig.upsModel = 'apc'; + } else if (modelValue === 3) { + snmpConfig.upsModel = 'eaton'; + } else if (modelValue === 4) { + snmpConfig.upsModel = 'tripplite'; + } else if (modelValue === 5) { + snmpConfig.upsModel = 'liebert'; + } else if (modelValue === 6) { + snmpConfig.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 + snmpConfig.customOIDs = { + POWER_STATUS: powerStatusOID.trim(), + BATTERY_CAPACITY: batteryCapacityOID.trim(), + BATTERY_RUNTIME: batteryRuntimeOID.trim(), + }; + } + } + + /** + * Display UPS configuration summary + * @param ups UPS configuration + */ + private displayUpsConfigSummary(ups: any): void { + const boxWidth = 45; + logger.log(''); + logger.logBoxTitle(`UPS Configuration: ${ups.name}`, boxWidth); + logger.logBoxLine(`UPS ID: ${ups.id}`); + logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`); + logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`); + logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`); + logger.logBoxLine( + `Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime` + ); + if (ups.groups && ups.groups.length > 0) { + logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`); + } else { + logger.logBoxLine('Groups: None'); + } + logger.logBoxEnd(); + logger.log(''); + } + + /** + * Optionally test connection to UPS + * @param snmpConfig SNMP configuration to test + * @param prompt Function to prompt for user input + */ + private async optionallyTestConnection( + snmpConfig: 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') { + logger.log('\nTesting connection to UPS...'); + try { + // Create a test config with a short timeout + const testConfig = { + ...snmpConfig, + timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing + }; + + const status = await this.nupst.getSnmp().getUpsStatus(testConfig); + const boxWidth = 45; + logger.log(''); + logger.logBoxTitle('Connection Successful!', boxWidth); + logger.logBoxLine('UPS Status:'); + logger.logBoxLine(`✓ Power Status: ${status.powerStatus}`); + logger.logBoxLine(`✓ Battery Capacity: ${status.batteryCapacity}%`); + logger.logBoxLine(`✓ Runtime Remaining: ${status.batteryRuntime} minutes`); + logger.logBoxEnd(); + } catch (error) { + const errorBoxWidth = 45; + logger.log(''); + logger.logBoxTitle('Connection Failed!', errorBoxWidth); + logger.logBoxLine(`Error: ${error.message}`); + logger.logBoxEnd(); + logger.log('\nPlease check your settings and try again.'); + } + } + } + + /** + * Check if the systemd service is running and restart it if it is + * This is useful after configuration changes + */ + public async restartServiceIfRunning(): Promise { + try { + // Check if the service is active + const isActive = + execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; + + if (isActive) { + // Service is running, restart it + const boxWidth = 45; + logger.logBoxTitle('Service Update', boxWidth); + logger.logBoxLine('Configuration has changed.'); + logger.logBoxLine('Restarting NUPST service to apply changes...'); + + try { + if (process.getuid && process.getuid() === 0) { + // We have root access, restart directly + execSync('systemctl restart nupst.service'); + logger.logBoxLine('Service restarted successfully.'); + } else { + // No root access, show instructions + logger.logBoxLine('Please restart the service with:'); + logger.logBoxLine(' sudo systemctl restart nupst.service'); + } + } catch (error) { + logger.logBoxLine(`Error restarting service: ${error.message}`); + logger.logBoxLine('You may need to restart the service manually:'); + logger.logBoxLine(' sudo systemctl restart nupst.service'); + } + + logger.logBoxEnd(); + } + } catch (error) { + // Ignore errors checking service status + } + } +} \ No newline at end of file diff --git a/ts/nupst.ts b/ts/nupst.ts index 9321999..3d80cdd 100644 --- a/ts/nupst.ts +++ b/ts/nupst.ts @@ -2,9 +2,11 @@ import { NupstSnmp } from './snmp/manager.js'; import { NupstDaemon } from './daemon.js'; import { NupstSystemd } from './systemd.js'; import { commitinfo } from './00_commitinfo_data.js'; -import { spawn } from 'child_process'; -import * as https from 'https'; import { logger } from './logger.js'; +import { UpsHandler } from './cli/ups-handler.js'; +import { GroupHandler } from './cli/group-handler.js'; +import { ServiceHandler } from './cli/service-handler.js'; +import * as https from 'https'; /** * Main Nupst class that coordinates all components @@ -14,6 +16,9 @@ export class Nupst { private readonly snmp: NupstSnmp; private readonly daemon: NupstDaemon; private readonly systemd: NupstSystemd; + private readonly upsHandler: UpsHandler; + private readonly groupHandler: GroupHandler; + private readonly serviceHandler: ServiceHandler; private updateAvailable: boolean = false; private latestVersion: string = ''; @@ -21,10 +26,16 @@ export class Nupst { * Create a new Nupst instance with all necessary components */ constructor() { + // Initialize core components this.snmp = new NupstSnmp(); this.snmp.setNupst(this); // Set up bidirectional reference this.daemon = new NupstDaemon(this.snmp); this.systemd = new NupstSystemd(this.daemon); + + // Initialize handlers + this.upsHandler = new UpsHandler(this); + this.groupHandler = new GroupHandler(this); + this.serviceHandler = new ServiceHandler(this); } /** @@ -48,6 +59,27 @@ export class Nupst { return this.systemd; } + /** + * Get the UPS handler for UPS management + */ + public getUpsHandler(): UpsHandler { + return this.upsHandler; + } + + /** + * Get the Group handler for group management + */ + public getGroupHandler(): GroupHandler { + return this.groupHandler; + } + + /** + * Get the Service handler for service management + */ + public getServiceHandler(): ServiceHandler { + return this.serviceHandler; + } + /** * Get the current version of NUPST * @returns The current version string @@ -192,4 +224,4 @@ export class Nupst { logger.logBoxEnd(); } } -} +} \ No newline at end of file