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