fix(cli): resolve process hang and improve output consistency

- Add process.stdin.destroy() after rl.close() in all interactive commands
  to properly release stdin and allow process to exit cleanly
- Replace raw console.log with logger methods throughout CLI handlers
- Convert manual box drawing to logger.logBox() in daemon.ts
- Standardize menu formatting with logger.info() and logger.dim()
- Improve migration output to only show when migrations actually run

Fixes issue where process would not exit after "Setup complete!" message
due to stdin keeping the event loop alive.
This commit is contained in:
2025-10-20 00:32:06 +00:00
parent bb87316dd3
commit 3ff0dd7ac8
6 changed files with 93 additions and 76 deletions

View File

@@ -192,6 +192,7 @@ export class GroupHandler {
logger.log('\nGroup setup complete!'); logger.log('\nGroup setup complete!');
} finally { } finally {
rl.close(); rl.close();
process.stdin.destroy();
} }
} catch (error) { } catch (error) {
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
@@ -309,6 +310,7 @@ export class GroupHandler {
logger.log('\nGroup edit complete!'); logger.log('\nGroup edit complete!');
} finally { } finally {
rl.close(); rl.close();
process.stdin.destroy();
} }
} catch (error) { } catch (error) {
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
@@ -366,6 +368,7 @@ export class GroupHandler {
}); });
rl.close(); rl.close();
process.stdin.destroy();
if (confirm !== 'y' && confirm !== 'yes') { if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.'); logger.log('Deletion cancelled.');

View File

@@ -213,9 +213,11 @@ export class ServiceHandler {
}); });
}; };
console.log('\nNUPST Uninstaller'); logger.log('');
console.log('==============='); logger.highlight('NUPST Uninstaller');
console.log('This will completely remove NUPST from your system.\n'); logger.dim('===============');
logger.log('This will completely remove NUPST from your system.');
logger.log('');
// Ask about removing configuration // Ask about removing configuration
const removeConfig = await prompt( const removeConfig = await prompt(
@@ -251,17 +253,20 @@ export class ServiceHandler {
} }
if (!uninstallScriptPath) { if (!uninstallScriptPath) {
console.error('Could not locate uninstall.sh script. Aborting uninstall.'); logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
rl.close(); rl.close();
process.stdin.destroy();
process.exit(1); process.exit(1);
} }
} }
// Close readline before executing script // Close readline before executing script
rl.close(); rl.close();
process.stdin.destroy();
// Execute uninstall.sh with the appropriate option // Execute uninstall.sh with the appropriate option
console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`); logger.log('');
logger.log(`Running uninstaller from ${uninstallScriptPath}...`);
// Pass the configuration removal option as an environment variable // Pass the configuration removal option as an environment variable
const env = { const env = {
@@ -277,7 +282,7 @@ export class ServiceHandler {
stdio: 'inherit', // Show output in the terminal stdio: 'inherit', // Show output in the terminal
}); });
} catch (error) { } catch (error) {
console.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1); process.exit(1);
} }
} }

View File

@@ -47,6 +47,7 @@ export class UpsHandler {
await this.runAddProcess(prompt); await this.runAddProcess(prompt);
} finally { } finally {
rl.close(); rl.close();
process.stdin.destroy();
} }
} catch (error) { } catch (error) {
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
@@ -178,6 +179,7 @@ export class UpsHandler {
await this.runEditProcess(upsId, prompt); await this.runEditProcess(upsId, prompt);
} finally { } finally {
rl.close(); rl.close();
process.stdin.destroy();
} }
} catch (error) { } catch (error) {
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
@@ -344,6 +346,7 @@ export class UpsHandler {
}); });
rl.close(); rl.close();
process.stdin.destroy();
if (confirm !== 'y' && confirm !== 'yes') { if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.'); logger.log('Deletion cancelled.');
@@ -667,10 +670,11 @@ export class UpsHandler {
// SNMP Version // SNMP Version
const defaultVersion = snmpConfig.version || 1; const defaultVersion = snmpConfig.version || 1;
console.log('\nSNMP Version:'); logger.log('');
console.log(' 1) SNMPv1'); logger.info('SNMP Version:');
console.log(' 2) SNMPv2c'); logger.dim(' 1) SNMPv1');
console.log(' 3) SNMPv3 (with security features)'); logger.dim(' 2) SNMPv2c');
logger.dim(' 3) SNMPv3 (with security features)');
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `); const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
const version = parseInt(versionInput, 10); const version = parseInt(versionInput, 10);
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3) snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
@@ -697,13 +701,15 @@ export class UpsHandler {
snmpConfig: any, snmpConfig: any,
prompt: (question: string) => Promise<string>, prompt: (question: string) => Promise<string>,
): Promise<void> { ): Promise<void> {
console.log('\nSNMPv3 Security Settings:'); logger.log('');
logger.info('SNMPv3 Security Settings:');
// Security Level // Security Level
console.log('\nSecurity Level:'); logger.log('');
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)'); logger.info('Security Level:');
console.log(' 2) authNoPriv (Authentication, No Privacy)'); logger.dim(' 1) noAuthNoPriv (No Authentication, No Privacy)');
console.log(' 3) authPriv (Authentication and Privacy)'); logger.dim(' 2) authNoPriv (Authentication, No Privacy)');
logger.dim(' 3) authPriv (Authentication and Privacy)');
const defaultSecLevel = snmpConfig.securityLevel const defaultSecLevel = snmpConfig.securityLevel
? snmpConfig.securityLevel === 'noAuthNoPriv' ? snmpConfig.securityLevel === 'noAuthNoPriv'
? 1 ? 1
@@ -752,8 +758,9 @@ export class UpsHandler {
// Allow customizing the timeout value // Allow customizing the timeout value
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
console.log( logger.log('');
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.', logger.info(
'SNMPv3 operations with authentication and privacy may require longer timeouts.',
); );
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `); const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
const timeout = parseInt(timeoutInput, 10); const timeout = parseInt(timeoutInput, 10);
@@ -773,9 +780,10 @@ export class UpsHandler {
prompt: (question: string) => Promise<string>, prompt: (question: string) => Promise<string>,
): Promise<void> { ): Promise<void> {
// Authentication protocol // Authentication protocol
console.log('\nAuthentication Protocol:'); logger.log('');
console.log(' 1) MD5'); logger.info('Authentication Protocol:');
console.log(' 2) SHA'); logger.dim(' 1) MD5');
logger.dim(' 2) SHA');
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1; const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
const authProtocolInput = await prompt( const authProtocolInput = await prompt(
`Select Authentication Protocol [${defaultAuthProtocol}]: `, `Select Authentication Protocol [${defaultAuthProtocol}]: `,
@@ -799,9 +807,10 @@ export class UpsHandler {
prompt: (question: string) => Promise<string>, prompt: (question: string) => Promise<string>,
): Promise<void> { ): Promise<void> {
// Privacy protocol // Privacy protocol
console.log('\nPrivacy Protocol:'); logger.log('');
console.log(' 1) DES'); logger.info('Privacy Protocol:');
console.log(' 2) AES'); logger.dim(' 1) DES');
logger.dim(' 2) AES');
const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1; const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `); const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol; const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
@@ -822,7 +831,8 @@ export class UpsHandler {
thresholds: any, thresholds: any,
prompt: (question: string) => Promise<string>, prompt: (question: string) => Promise<string>,
): Promise<void> { ): Promise<void> {
console.log('\nShutdown Thresholds:'); logger.log('');
logger.info('Shutdown Thresholds:');
// Battery threshold // Battery threshold
const defaultBatteryThreshold = thresholds.battery || 60; const defaultBatteryThreshold = thresholds.battery || 60;
@@ -854,13 +864,14 @@ export class UpsHandler {
snmpConfig: any, snmpConfig: any,
prompt: (question: string) => Promise<string>, prompt: (question: string) => Promise<string>,
): Promise<void> { ): Promise<void> {
console.log('\nUPS Model Selection:'); logger.log('');
console.log(' 1) CyberPower'); logger.info('UPS Model Selection:');
console.log(' 2) APC'); logger.dim(' 1) CyberPower');
console.log(' 3) Eaton'); logger.dim(' 2) APC');
console.log(' 4) TrippLite'); logger.dim(' 3) Eaton');
console.log(' 5) Liebert/Vertiv'); logger.dim(' 4) TrippLite');
console.log(' 6) Custom (Advanced)'); logger.dim(' 5) Liebert/Vertiv');
logger.dim(' 6) Custom (Advanced)');
const defaultModelValue = snmpConfig.upsModel === 'cyberpower' const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
? 1 ? 1
@@ -891,8 +902,9 @@ export class UpsHandler {
snmpConfig.upsModel = 'liebert'; snmpConfig.upsModel = 'liebert';
} else if (modelValue === 6) { } else if (modelValue === 6) {
snmpConfig.upsModel = 'custom'; snmpConfig.upsModel = 'custom';
console.log('\nEnter custom OIDs for your UPS:'); logger.log('');
console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)'); logger.info('Enter custom OIDs for your UPS:');
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
// Custom OIDs // Custom OIDs
const powerStatusOID = await prompt('Power Status OID: '); const powerStatusOID = await prompt('Power Status OID: ');

View File

@@ -207,11 +207,9 @@ export class NupstDaemon {
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2)); fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
this.config = configToSave; this.config = configToSave;
console.log('┌─ Configuration Saved ─────────────────────┐'); logger.logBox('Configuration Saved', [`Location: ${this.CONFIG_PATH}`], 45, 'success');
console.log(`│ Location: ${this.CONFIG_PATH}`);
console.log('└──────────────────────────────────────────┘');
} catch (error) { } catch (error) {
console.error('Error saving configuration:', error); logger.error(`Error saving configuration: ${error}`);
} }
} }
@@ -219,10 +217,7 @@ export class NupstDaemon {
* Helper method to log configuration errors consistently * Helper method to log configuration errors consistently
*/ */
private logConfigError(message: string): void { private logConfigError(message: string): void {
console.error('┌─ Configuration Error ─────────────────────┐'); logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
console.error(`${message}`);
console.error("│ Please run 'nupst setup' first to create a configuration.");
console.error('└───────────────────────────────────────────┘');
} }
/** /**

View File

@@ -34,12 +34,14 @@ export class MigrationRunner {
let currentConfig = config; let currentConfig = config;
let anyMigrationsRan = false; let anyMigrationsRan = false;
logger.dim('Checking for required config migrations...');
for (const migration of this.migrations) { for (const migration of this.migrations) {
const shouldRun = await migration.shouldRun(currentConfig); const shouldRun = await migration.shouldRun(currentConfig);
if (shouldRun) { if (shouldRun) {
// Only show "checking" message when we actually need to migrate
if (!anyMigrationsRan) {
logger.dim('Checking for required config migrations...');
}
logger.info(`Running ${migration.getName()}...`); logger.info(`Running ${migration.getName()}...`);
currentConfig = await migration.migrate(currentConfig); currentConfig = await migration.migrate(currentConfig);
anyMigrationsRan = true; anyMigrationsRan = true;
@@ -49,7 +51,7 @@ export class MigrationRunner {
if (anyMigrationsRan) { if (anyMigrationsRan) {
logger.success('Configuration migrations complete'); logger.success('Configuration migrations complete');
} else { } else {
logger.dim('No migrations needed'); logger.success('config format ok');
} }
return { return {

View File

@@ -50,11 +50,11 @@ WantedBy=multi-user.target
try { try {
await fs.access(configPath); await fs.access(configPath);
} catch (error) { } catch (error) {
console.log(''); logger.log('');
console.log(`${symbols.error} ${theme.error('No configuration found')}`); logger.error('No configuration found');
console.log(` ${theme.dim('Config file:')} ${configPath}`); logger.log(` ${theme.dim('Config file:')} ${configPath}`);
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`); logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
console.log(''); logger.log('');
throw new Error('Configuration not found'); throw new Error('Configuration not found');
} }
} }
@@ -192,11 +192,11 @@ WantedBy=multi-user.target
} }
// Display beautiful status // Display beautiful status
console.log(''); logger.log('');
if (isActive) { if (isActive) {
console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`); logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
} else { } else {
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`); logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
} }
if (pid || memory || cpu) { if (pid || memory || cpu) {
@@ -204,14 +204,14 @@ WantedBy=multi-user.target
if (pid) details.push(`PID: ${theme.dim(pid)}`); if (pid) details.push(`PID: ${theme.dim(pid)}`);
if (memory) details.push(`Memory: ${theme.dim(memory)}`); if (memory) details.push(`Memory: ${theme.dim(memory)}`);
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`); if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
console.log(` ${details.join(' ')}`); logger.log(` ${details.join(' ')}`);
} }
console.log(''); logger.log('');
} catch (error) { } catch (error) {
console.log(''); logger.log('');
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`); logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
console.log(''); logger.log('');
} }
} }
@@ -228,7 +228,7 @@ WantedBy=multi-user.target
// Check if we have the new multi-UPS config format // Check if we have the new multi-UPS config format
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) { if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`)); logger.info(`UPS Devices (${config.upsDevices.length}):`);
// Show status for each UPS // Show status for each UPS
for (const ups of config.upsDevices) { for (const ups of config.upsDevices) {
@@ -236,7 +236,7 @@ WantedBy=multi-user.target
} }
} else if (config.snmp) { } else if (config.snmp) {
// Legacy single UPS configuration // Legacy single UPS configuration
console.log(theme.info('UPS Devices (1):')); logger.info('UPS Devices (1):');
const legacyUps = { const legacyUps = {
id: 'default', id: 'default',
name: 'Default UPS', name: 'Default UPS',
@@ -247,16 +247,16 @@ WantedBy=multi-user.target
await this.displaySingleUpsStatus(legacyUps, snmp); await this.displaySingleUpsStatus(legacyUps, snmp);
} else { } else {
console.log(''); logger.log('');
console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`); logger.warn('No UPS devices configured');
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`); logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
console.log(''); logger.log('');
} }
} catch (error) { } catch (error) {
console.log(''); logger.log('');
console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`); logger.error('Failed to retrieve UPS status');
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`); logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log(''); logger.log('');
} }
} }
@@ -284,15 +284,15 @@ WantedBy=multi-user.target
} }
// Display UPS name and power status // Display UPS name and power status
console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`); logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
// Display battery with color coding // Display battery with color coding
const batteryColor = getBatteryColor(status.batteryCapacity); const batteryColor = getBatteryColor(status.batteryCapacity);
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning; const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`); logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
// Display host info // Display host info
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`); logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
// Display groups if any // Display groups if any
if (ups.groups && ups.groups.length > 0) { if (ups.groups && ups.groups.length > 0) {
@@ -301,17 +301,17 @@ WantedBy=multi-user.target
const group = config.groups?.find((g: { id: string }) => g.id === groupId); const group = config.groups?.find((g: { id: string }) => g.id === groupId);
return group ? group.name : groupId; return group ? group.name : groupId;
}); });
console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`); logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
} }
console.log(''); logger.log('');
} catch (error) { } catch (error) {
// Display error for this UPS // Display error for this UPS
console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`); logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`); logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`); logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
console.log(''); logger.log('');
} }
} }