Compare commits

...

12 Commits

10 changed files with 547 additions and 243 deletions

View File

@ -1,5 +1,48 @@
# Changelog
## 2025-03-26 - 2.6.9 - fix(cli)
Improve console output formatting for status banners and logging messages
- Standardize banner messages in daemon status updates
- Refine version information banner in nupst logging
- Update UPS connection and status banners in systemd
## 2025-03-26 - 2.6.8 - fix(cli)
Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager
- Standardize whitespace and formatting in ts/cli.ts for consistency
- Refine argument filtering for debug mode and prompt messages
- Remove unused 'dgram' import from ts/snmp/manager.ts
## 2025-03-26 - 2.6.7 - fix(setup.sh)
Clarify net-snmp dependency installation message in setup.sh
- Updated echo statement to indicate installation of net-snmp along with 2 subdependencies
- Improves clarity on dependency installation during setup
## 2025-03-26 - 2.6.6 - fix(setup.sh)
Improve setup script to detect and execute npm-cli.js directly using the Node.js binary
- Replace use of the npm binary with direct execution of npm-cli.js
- Add fallback logic to locate npm-cli.js when not found at the expected path
- Simplify cleanup by removing unnecessary PATH modifications
## 2025-03-26 - 2.6.5 - fix(daemon, setup)
Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths
- Use execFileAsync to execute shutdown commands reliably
- Add multiple fallback alternatives for shutdown and emergency shutdown handling
- Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH
## 2025-03-26 - 2.6.4 - fix(setup)
Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation.
- Remove existing package-lock.json along with node_modules to prevent stale artifacts.
- Back up the original package.json before modifying it.
- Create a minimal package.json with only the net-snmp dependency based on the backed-up version.
- Use a clean install to guarantee that only net-snmp is installed.
- Restore the original package.json if the installation fails.
## 2025-03-26 - 2.6.3 - fix(setup)
Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control.

View File

@ -1,6 +1,6 @@
{
"name": "@serve.zone/nupst",
"version": "2.6.3",
"version": "2.6.9",
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
"main": "dist/index.js",
"bin": {

2
pnpm-lock.yaml generated
View File

@ -9,7 +9,7 @@ importers:
.:
dependencies:
net-snmp:
specifier: ^3.20.0
specifier: 3.20.0
version: 3.20.0
devDependencies:
'@git.zone/tsbuild':

104
setup.sh
View File

@ -239,57 +239,89 @@ echo "dist_ts directory successfully downloaded from npm registry."
# Make launcher script executable
chmod +x "$SCRIPT_DIR/bin/nupst"
# Add our Node.js bin directory to the PATH temporarily
# Set up Node.js binary path
NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin"
OLD_PATH="$PATH"
export PATH="$NODE_BIN_DIR:$PATH"
NODE_BIN="$NODE_BIN_DIR/node"
NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js"
# Remove existing node_modules directory
echo "Removing existing node_modules directory..."
# Ensure we have executable permissions
chmod +x "$NODE_BIN"
# Make sure the npm-cli.js exists
if [ ! -f "$NPM_CLI_JS" ]; then
# Try to find npm-cli.js
NPM_CLI_JS=$(find "$NODE_BIN_DIR/.." -name "npm-cli.js" | head -1)
if [ -z "$NPM_CLI_JS" ]; then
echo "Warning: Could not find npm-cli.js, npm commands may fail"
# Set to a fallback value so code can continue
NPM_CLI_JS="$NODE_BIN_DIR/npm"
else
echo "Found npm-cli.js at: $NPM_CLI_JS"
fi
fi
# Display which binaries we're using
echo "Using Node binary: $NODE_BIN"
echo "Using NPM CLI JS: $NPM_CLI_JS"
# Remove existing node_modules directory and package files
echo "Cleaning up existing installation..."
rm -rf "$SCRIPT_DIR/node_modules"
rm -f "$SCRIPT_DIR/package-lock.json"
# Install ONLY the net-snmp production dependency
echo "Installing ONLY net-snmp production dependency..."
echo "Using Node.js binary from: $NODE_BIN_DIR"
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
# Back up existing package.json if it exists
if [ -f "$SCRIPT_DIR/package.json" ]; then
echo "Backing up existing package.json..."
cp "$SCRIPT_DIR/package.json" "$SCRIPT_DIR/package.json.bak"
fi
# Install just net-snmp directly, don't rely on package.json
npm --prefix "$SCRIPT_DIR" install --no-save net-snmp@3.20.0 --no-audit --no-fund
# Create a clean minimal package.json with ONLY net-snmp dependency
echo "Creating minimal package.json with only net-snmp dependency..."
VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json.bak" | head -1 | cut -d'"' -f4 || echo "2.6.3")
echo '{
"name": "@serve.zone/nupst",
"version": "'$VERSION'",
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
"main": "dist_ts/index.js",
"type": "module",
"bin": {
"nupst": "bin/nupst"
},
"dependencies": {
"net-snmp": "3.20.0"
},
"engines": {
"node": ">=16.0.0"
},
"private": true
}' > "$SCRIPT_DIR/package.json"
# Verify only net-snmp is installed
echo "Verifying only net-snmp is installed in node_modules..."
find "$SCRIPT_DIR/node_modules" -maxdepth 1 -type d | sort
# Install ONLY net-snmp
echo "Installing ONLY net-snmp dependency (+ 2 subdependencies)..."
echo "Node version: $("$NODE_BIN" --version)"
echo "Executing NPM directly with Node.js"
# Execute npm-cli.js directly with our Node.js binary
"$NODE_BIN" "$NPM_CLI_JS" --prefix "$SCRIPT_DIR" install --no-audit --no-fund
INSTALL_STATUS=$?
if [ $INSTALL_STATUS -ne 0 ]; then
echo "Error: Failed to install net-snmp dependency. NUPST may not function correctly."
echo "You can try to install dependencies manually by running:"
echo "cd $SCRIPT_DIR && npm install net-snmp@3.20.0"
echo "Restoring original package.json..."
mv "$SCRIPT_DIR/package.json.bak" "$SCRIPT_DIR/package.json"
exit 1
else
echo "net-snmp dependency installed successfully."
# Show what's actually installed
echo "Installed modules:"
find "$SCRIPT_DIR/node_modules" -maxdepth 1 -type d | grep -v "^$SCRIPT_DIR/node_modules$" | sort
# Create minimal package-lock.json if it doesn't exist
if [ ! -f "$SCRIPT_DIR/package-lock.json" ]; then
echo "Creating minimal package-lock.json..."
echo '{
"name": "@serve.zone/nupst",
"version": "2.6.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"net-snmp": "3.20.0"
}
}
}
}' > "$SCRIPT_DIR/package-lock.json"
fi
# Remove backup if successful
rm -f "$SCRIPT_DIR/package.json.bak"
fi
# Restore the original PATH
export PATH="$OLD_PATH"
# No temporary files to clean up
echo "NUPST setup completed successfully."
echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst"

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/nupst',
version: '2.6.3',
version: '2.6.9',
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
}

438
ts/cli.ts
View File

@ -30,7 +30,7 @@ export class NupstCli {
// 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';
@ -46,8 +46,8 @@ export class NupstCli {
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');
const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d');
return { debugMode, cleanedArgs };
}
@ -85,23 +85,23 @@ export class NupstCli {
case 'disable':
await this.disable();
break;
case 'setup':
await this.setup();
break;
case 'test':
await this.test(debugMode);
break;
case 'update':
await this.update();
break;
case 'uninstall':
await this.uninstall();
break;
case 'config':
await this.showConfig();
break;
@ -149,17 +149,17 @@ export class NupstCli {
// 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']
stdio: ['ignore', 'inherit', 'inherit'],
});
// Forward signals to child process
process.on('SIGINT', () => {
journalctl.kill('SIGINT');
process.exit(0);
});
// Wait for process to exit
await new Promise<void>((resolve) => {
journalctl.on('exit', () => resolve());
@ -229,21 +229,21 @@ export class NupstCli {
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("│ 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) {
@ -262,26 +262,26 @@ export class NupstCli {
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:');
@ -304,20 +304,20 @@ export class NupstCli {
console.log('\nTesting connection to UPS...');
try {
// Create a test config with a short timeout
const testConfig = {
const testConfig = {
...config.snmp,
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing
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);
@ -326,7 +326,7 @@ export class NupstCli {
console.error('┌─ Connection Failed! ───────────────────────┐');
console.error(`│ Error: ${error.message}`);
console.error('└──────────────────────────────────────────┘');
console.log('\nPlease check your settings and run \'nupst setup\' to reconfigure.');
console.log("\nPlease check your settings and run 'nupst setup' to reconfigure.");
}
}
@ -337,25 +337,33 @@ export class NupstCli {
*/
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(
`│ 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}%`);
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(
`│ 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(
`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`
);
}
console.log('└──────────────────────────────────────────┘');
}
@ -386,60 +394,72 @@ Options:
(Example: nupst test --debug)
`);
}
/**
* Update NUPST from repository and refresh systemd service
*/
private async update(): Promise<void> {
try {
// Check if running as root
this.checkRootAccess('This command must be run as root to update NUPST and refresh the systemd service.');
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' });
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 with force flag to update Node.js and dependencies
console.log('│ Running setup.sh to update Node.js and dependencies...');
execSync(`cd ${installDir} && bash ./setup.sh --force`, { 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');
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';
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...');
@ -449,7 +469,7 @@ Options:
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) {
@ -463,7 +483,7 @@ Options:
process.exit(1);
}
}
/**
* Interactive setup for configuring SNMP settings
*/
@ -471,12 +491,12 @@ Options:
try {
// Import readline module (ESM style)
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
@ -485,7 +505,7 @@ Options:
});
});
};
try {
await this.runSetupProcess(prompt);
} finally {
@ -504,7 +524,7 @@ Options:
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 {
@ -518,24 +538,24 @@ Options:
// 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);
// Check if service is running and restart it if needed
await this.restartServiceIfRunning();
console.log('\nSetup complete!');
await this.optionallyEnableService(prompt);
}
@ -546,18 +566,21 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherSnmpSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherSnmpSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
// SNMP IP Address
const defaultHost = config.snmp.host;
const host = await prompt(`UPS IP Address [${defaultHost}]: `);
config.snmp.host = host.trim() || defaultHost;
// SNMP Port
const defaultPort = config.snmp.port;
const portInput = await prompt(`SNMP Port [${defaultPort}]: `);
const port = parseInt(portInput, 10);
config.snmp.port = (portInput.trim() && !isNaN(port)) ? port : defaultPort;
config.snmp.port = portInput.trim() && !isNaN(port) ? port : defaultPort;
// SNMP Version
const defaultVersion = config.snmp.version;
console.log('\nSNMP Version:');
@ -566,8 +589,11 @@ Options:
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;
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';
@ -577,7 +603,7 @@ Options:
// SNMP v3 settings
config = await this.gatherSnmpV3Settings(config, prompt);
}
return config;
}
@ -587,20 +613,27 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherSnmpV3Settings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherSnmpV3Settings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
console.log('\nSNMPv3 Security Settings:');
// Security Level
console.log('\nSecurity Level:');
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
console.log(' 2) authNoPriv (Authentication, No Privacy)');
console.log(' 3) authPriv (Authentication and Privacy)');
const defaultSecLevel = config.snmp.securityLevel ?
(config.snmp.securityLevel === 'noAuthNoPriv' ? 1 :
config.snmp.securityLevel === 'authNoPriv' ? 2 : 3) : 3;
const 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
@ -622,31 +655,33 @@ Options:
// 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.');
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;
}
@ -656,21 +691,26 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherAuthenticationSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherAuthenticationSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
// Authentication protocol
console.log('\nAuthentication Protocol:');
console.log(' 1) MD5');
console.log(' 2) SHA');
const defaultAuthProtocol = config.snmp.authProtocol === 'SHA' ? 2 : 1;
const authProtocolInput = await prompt(`Select Authentication Protocol [${defaultAuthProtocol}]: `);
const 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;
}
@ -680,7 +720,10 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherPrivacySettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherPrivacySettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
// Privacy protocol
console.log('\nPrivacy Protocol:');
console.log(' 1) DES');
@ -689,12 +732,12 @@ Options:
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;
}
@ -704,33 +747,43 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherThresholdSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherThresholdSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
console.log('\nShutdown Thresholds:');
// Battery threshold
const defaultBatteryThreshold = config.thresholds.battery;
const batteryThresholdInput = await prompt(`Battery percentage threshold [${defaultBatteryThreshold}%]: `);
const batteryThresholdInput = await prompt(
`Battery percentage threshold [${defaultBatteryThreshold}%]: `
);
const batteryThreshold = parseInt(batteryThresholdInput, 10);
config.thresholds.battery = (batteryThresholdInput.trim() && !isNaN(batteryThreshold))
? batteryThreshold
: defaultBatteryThreshold;
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 runtimeThresholdInput = await prompt(
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `
);
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
config.thresholds.runtime = (runtimeThresholdInput.trim() && !isNaN(runtimeThreshold))
? runtimeThreshold
: defaultRuntimeThreshold;
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;
config.checkInterval =
intervalInput.trim() && !isNaN(interval)
? interval * 1000 // Convert to ms
: defaultInterval * 1000;
return config;
}
@ -740,7 +793,10 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherUpsModelSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherUpsModelSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
console.log('\nUPS Model Selection:');
console.log(' 1) CyberPower');
console.log(' 2) APC');
@ -748,17 +804,25 @@ Options:
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 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) {
@ -773,20 +837,20 @@ Options:
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()
BATTERY_RUNTIME: batteryRuntimeOID.trim(),
};
}
return config;
}
@ -799,8 +863,10 @@ Options:
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(
`│ Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`
);
console.log(`│ Check Interval: ${config.checkInterval / 1000} seconds`);
console.log('└──────────────────────────────────────────┘\n');
}
@ -809,17 +875,22 @@ Options:
* @param config Current configuration
* @param prompt Function to prompt for user input
*/
private async optionallyTestConnection(config: any, prompt: (question: string) => Promise<string>): Promise<void> {
const testConnection = await prompt('Would you like to test the connection to your UPS? (y/N): ');
private async optionallyTestConnection(
config: any,
prompt: (question: string) => Promise<string>
): Promise<void> {
const testConnection = await prompt(
'Would you like to test the connection to your UPS? (y/N): '
);
if (testConnection.toLowerCase() === 'y') {
console.log('\nTesting connection to UPS...');
try {
// Create a test config with a short timeout
const testConfig = {
const testConfig = {
...config.snmp,
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing
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:');
@ -843,14 +914,15 @@ Options:
private async restartServiceIfRunning(): Promise<void> {
try {
// Check if the service is active
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isActive) {
// Service is running, restart it
console.log('┌─ Service Update ─────────────────────────┐');
console.log('│ Configuration has changed.');
console.log('│ Restarting NUPST service to apply changes...');
try {
if (process.getuid && process.getuid() === 0) {
// We have root access, restart directly
@ -866,7 +938,7 @@ Options:
console.log('│ You may need to restart the service manually:');
console.log('│ sudo systemctl restart nupst.service');
}
console.log('└──────────────────────────────────────────┘');
}
} catch (error) {
@ -878,18 +950,24 @@ Options:
* Optionally enable and start systemd service
* @param prompt Function to prompt for user input
*/
private async optionallyEnableService(prompt: (question: string) => Promise<string>): Promise<void> {
private async optionallyEnableService(
prompt: (question: string) => Promise<string>
): Promise<void> {
if (process.getuid && process.getuid() !== 0) {
console.log('\nNote: Run "sudo nupst enable" to set up NUPST as a system service.');
} else {
const setupService = await prompt('Would you like to enable NUPST as a system service? (y/N): ');
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): ');
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.');
@ -902,7 +980,7 @@ Options:
}
}
}
/**
* Display the current configuration
*/
@ -914,68 +992,75 @@ Options:
} catch (error) {
console.error('┌─ Configuration Error ─────────────────────┐');
console.error('│ No configuration found.');
console.error('│ Please run \'nupst setup\' first to create a configuration.');
console.error("│ Please run 'nupst setup' first to create a configuration.");
console.error('└──────────────────────────────────────────┘');
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
console.log('┌─ NUPST Configuration ──────────────────────┐');
// SNMP Settings
console.log('│ SNMP Settings:');
console.log(`│ Host: ${config.snmp.host}`);
console.log(`│ Port: ${config.snmp.port}`);
console.log(`│ Version: ${config.snmp.version}`);
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
if (config.snmp.version === 1 || config.snmp.version === 2) {
console.log(`│ Community: ${config.snmp.community}`);
} else if (config.snmp.version === 3) {
console.log(`│ Security Level: ${config.snmp.securityLevel}`);
console.log(`│ Username: ${config.snmp.username}`);
// Show auth and privacy details based on security level
if (config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv') {
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 Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`
);
console.log(`│ Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
}
// Thresholds
console.log('│ Thresholds:');
console.log(`│ Battery: ${config.thresholds.battery}%`);
console.log(`│ Runtime: ${config.thresholds.runtime} minutes`);
console.log(`│ Check Interval: ${config.checkInterval / 1000} seconds`);
// Configuration file location
console.log('│');
console.log('│ Configuration File Location:');
console.log('│ /etc/nupst/config.json');
console.log('└──────────────────────────────────────────┘');
// Show service status
try {
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isEnabled = execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isEnabled =
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
console.log('┌─ Service Status ─────────────────────────┐');
console.log(`│ Service Active: ${isActive ? 'Yes' : 'No'}`);
console.log(`│ Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
@ -983,7 +1068,6 @@ Options:
} catch (error) {
// Ignore errors checking service status
}
} catch (error) {
console.error(`Failed to display configuration: ${error.message}`);
}
@ -999,12 +1083,12 @@ Options:
try {
// Import readline module for user input
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
@ -1019,11 +1103,13 @@ Options:
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): ');
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
@ -1031,16 +1117,13 @@ Options:
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')
];
const commonPaths = ['/opt/nupst/uninstall.sh', join(process.cwd(), 'uninstall.sh')];
for (const path of commonPaths) {
try {
await fs.access(path);
@ -1050,37 +1133,36 @@ Options:
// 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
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}`, {
execSync(`sudo bash ${uninstallScriptPath}`, {
env,
stdio: 'inherit' // Show output in the terminal
stdio: 'inherit', // Show output in the terminal
});
} catch (error) {
console.error(`Uninstall failed: ${error.message}`);
process.exit(1);
}
}
}
}

View File

@ -1,11 +1,12 @@
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { exec, execFile } from 'child_process';
import { promisify } from 'util';
import { NupstSnmp } from './snmp/manager.js';
import type { ISnmpConfig } from './snmp/types.js';
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
/**
* Configuration interface for the daemon
@ -225,8 +226,8 @@ export class NupstDaemon {
// Log status changes
if (status.powerStatus !== lastStatus) {
console.log('┌──────────────────────────────────────────┐');
console.log(`Power status changed: ${lastStatus}${status.powerStatus}`);
console.log('┌─ Power Status Change ─────────────────────┐');
console.log(`Status changed: ${lastStatus}${status.powerStatus}`);
console.log('└──────────────────────────────────────────┘');
lastStatus = status.powerStatus;
lastLogTime = currentTime; // Reset log timer when status changes
@ -234,8 +235,8 @@ export class NupstDaemon {
// Log status periodically (at least every 5 minutes)
else if (shouldLogStatus) {
const timestamp = new Date().toISOString();
console.log('┌──────────────────────────────────────────┐');
console.log(`[${timestamp}] Periodic Status Update`);
console.log('┌─ Periodic Status Update ──────────────────┐');
console.log(`Timestamp: ${timestamp}`);
console.log(`│ Power Status: ${status.powerStatus}`);
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
console.log('└──────────────────────────────────────────┘');
@ -266,8 +267,8 @@ export class NupstDaemon {
batteryCapacity: number,
batteryRuntime: number
}): Promise<void> {
console.log('┌─ UPS Status ───────────────────────────────┐');
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
console.log('┌─ UPS Status ────────────────────────────┐');
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
console.log('└──────────────────────────────────────────┘');
// Check battery threshold
@ -298,23 +299,101 @@ export class NupstDaemon {
const shutdownDelayMinutes = 5;
try {
// Execute shutdown command with delay to allow for VM graceful shutdown
const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`);
console.log('Shutdown initiated:', stdout);
console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
// Find shutdown command in common system paths
const shutdownPaths = [
'/sbin/shutdown',
'/usr/sbin/shutdown',
'/bin/shutdown',
'/usr/bin/shutdown'
];
let shutdownCmd = '';
for (const path of shutdownPaths) {
try {
if (fs.existsSync(path)) {
shutdownCmd = path;
console.log(`Found shutdown command at: ${shutdownCmd}`);
break;
}
} catch (e) {
// Continue checking other paths
}
}
if (shutdownCmd) {
// Execute shutdown command with delay to allow for VM graceful shutdown
console.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
const { stdout } = await execFileAsync(shutdownCmd, [
'-h',
`+${shutdownDelayMinutes}`,
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`
]);
console.log('Shutdown initiated:', stdout);
console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
} else {
// Try using the PATH to find shutdown
try {
console.log('Shutdown command not found in common paths, trying via PATH...');
const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, {
env: process.env // Pass the current environment
});
console.log('Shutdown initiated:', stdout);
} catch (e) {
throw new Error(`Shutdown command not found: ${e.message}`);
}
}
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
console.log('Monitoring UPS during shutdown process...');
await this.monitorDuringShutdown();
} catch (error) {
console.error('Failed to initiate shutdown:', error);
// Try a different method if first one fails
try {
console.log('Trying alternative shutdown method...');
await execAsync('poweroff --force');
} catch (innerError) {
console.error('All shutdown methods failed:', innerError);
// Try alternative shutdown methods
const alternatives = [
{ cmd: 'poweroff', args: ['--force'] },
{ cmd: 'halt', args: ['-p'] },
{ cmd: 'systemctl', args: ['poweroff'] },
{ cmd: 'reboot', args: ['-p'] } // Some systems allow reboot -p for power off
];
for (const alt of alternatives) {
try {
// First check if command exists in common system paths
const paths = [
`/sbin/${alt.cmd}`,
`/usr/sbin/${alt.cmd}`,
`/bin/${alt.cmd}`,
`/usr/bin/${alt.cmd}`
];
let cmdPath = '';
for (const path of paths) {
if (fs.existsSync(path)) {
cmdPath = path;
break;
}
}
if (cmdPath) {
console.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
await execFileAsync(cmdPath, alt.args);
return; // Exit if successful
} else {
// Try using PATH environment
console.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
env: process.env // Pass the current environment
});
return; // Exit if successful
}
} catch (altError) {
console.error(`Alternative method ${alt.cmd} failed:`, altError);
// Continue to next method
}
}
console.error('All shutdown methods failed');
}
}
@ -346,10 +425,79 @@ export class NupstDaemon {
console.log('└──────────────────────────────────────────┘');
try {
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"');
// Find shutdown command in common system paths
const shutdownPaths = [
'/sbin/shutdown',
'/usr/sbin/shutdown',
'/bin/shutdown',
'/usr/bin/shutdown'
];
let shutdownCmd = '';
for (const path of shutdownPaths) {
if (fs.existsSync(path)) {
shutdownCmd = path;
console.log(`Found shutdown command at: ${shutdownCmd}`);
break;
}
}
if (shutdownCmd) {
console.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
} else {
// Try using the PATH to find shutdown
console.log('Shutdown command not found in common paths, trying via PATH...');
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
env: process.env // Pass the current environment
});
}
} catch (error) {
console.error('Emergency shutdown failed, trying alternative method...');
await execAsync('poweroff --force');
console.error('Emergency shutdown failed, trying alternative methods...');
// Try alternative shutdown methods in sequence
const alternatives = [
{ cmd: 'poweroff', args: ['--force'] },
{ cmd: 'halt', args: ['-p'] },
{ cmd: 'systemctl', args: ['poweroff'] }
];
for (const alt of alternatives) {
try {
// Check common paths
const paths = [
`/sbin/${alt.cmd}`,
`/usr/sbin/${alt.cmd}`,
`/bin/${alt.cmd}`,
`/usr/bin/${alt.cmd}`
];
let cmdPath = '';
for (const path of paths) {
if (fs.existsSync(path)) {
cmdPath = path;
break;
}
}
if (cmdPath) {
console.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
await execFileAsync(cmdPath, alt.args);
return; // Exit if successful
} else {
// Try using PATH
console.log(`Emergency: trying ${alt.cmd} via PATH`);
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
env: process.env
});
return; // Exit if successful
}
} catch (altError) {
// Continue to next method
}
}
console.error('All emergency shutdown methods failed');
}
// Stop monitoring after initiating emergency shutdown

View File

@ -162,7 +162,7 @@ export class Nupst {
*/
public logVersionInfo(checkForUpdates: boolean = true): void {
const version = this.getVersion();
console.log('┌─ NUPST Version ────────────────────────┐');
console.log('┌─ NUPST Version ────────────────────────────┐');
console.log(`│ Current Version: ${version}`);
if (this.updateAvailable && this.latestVersion) {

View File

@ -1,4 +1,3 @@
import * as dgram from 'dgram';
import * as snmp from 'net-snmp';
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
import { UpsOidSets } from './oid-sets.js';

View File

@ -190,20 +190,20 @@ WantedBy=multi-user.target
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check
};
console.log('┌─ Connecting to UPS... ────────────────────┐');
console.log('┌─ Connecting to UPS... ────────────────────┐');
console.log(`│ Host: ${config.snmp.host}:${config.snmp.port}`);
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
console.log('└──────────────────────────────────────────┘');
console.log('└────────────────────────────────────────────┘');
const status = await snmp.getUpsStatus(snmpConfig);
console.log('┌─ UPS Status ───────────────────────────────┐');
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('┌─ UPS Status ───────────────────────────────┐');
console.error('┌─ UPS Status ─────────────────────────────┐');
console.error(`│ Failed to retrieve UPS status: ${error.message}`);
console.error('└──────────────────────────────────────────┘');
}