Compare commits

..

35 Commits

Author SHA1 Message Date
bd3042de25 2.6.17 2025-03-26 22:43:19 +00:00
456351ca34 fix(logger): Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set width. 2025-03-26 22:43:18 +00:00
00afa317ef 2.6.16 2025-03-26 22:38:24 +00:00
45ee8208b5 fix(cli): Improve CLI logging consistency by replacing direct console output with unified logger calls. 2025-03-26 22:38:24 +00:00
39bf3e2239 2.6.15 2025-03-26 22:28:38 +00:00
f3de3f0618 fix(logger): Replace direct console logging with unified logger interface for consistent formatting 2025-03-26 22:28:38 +00:00
03056d279d update 2025-03-26 22:19:24 +00:00
f860f39e59 2.6.14 2025-03-26 18:15:17 +00:00
fa4516de3b fix(systemd): Shorten closing log divider in systemd service installation output for consistent formatting. 2025-03-26 18:15:17 +00:00
539547beb8 2.6.13 2025-03-26 18:13:12 +00:00
6eb92959ec fix(cli): Fix CLI update output box formatting 2025-03-26 18:13:12 +00:00
4af9af0845 2.6.12 2025-03-26 18:10:49 +00:00
f7e12cdcbb fix(systemd): Adjust logging border in systemd service installation output 2025-03-26 18:10:49 +00:00
002498b91b 2.6.11 2025-03-26 18:08:43 +00:00
459911fe5f fix(cli, systemd): Adjust log formatting for consistent output in CLI and systemd commands 2025-03-26 18:08:43 +00:00
9859a02ea2 2.6.10 2025-03-26 18:04:12 +00:00
65444b6d25 fix(daemon): Adjust console log box formatting for consistent output in daemon status messages 2025-03-26 18:04:12 +00:00
d049e8741f 2.6.9 2025-03-26 18:00:55 +00:00
1123a99aea fix(cli): Improve console output formatting for status banners and logging messages 2025-03-26 18:00:54 +00:00
d01e878310 2.6.8 2025-03-26 17:49:50 +00:00
588aeabf4b fix(cli): Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager 2025-03-26 17:49:50 +00:00
87005e72f1 2.6.7 2025-03-26 15:56:31 +00:00
f799c2ee66 fix(setup.sh): Clarify net-snmp dependency installation message in setup.sh 2025-03-26 15:56:31 +00:00
1a029ba493 2.6.6 2025-03-26 15:53:38 +00:00
5b756dd223 fix(setup.sh): Improve setup script to detect and execute npm-cli.js directly using the Node.js binary 2025-03-26 15:53:38 +00:00
4cac599a58 2.6.5 2025-03-26 15:49:54 +00:00
be6a7314c3 fix(daemon, setup): Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths 2025-03-26 15:49:54 +00:00
83ba9c2611 2.6.4 2025-03-26 14:09:01 +00:00
22ab472e58 fix(setup): Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation. 2025-03-26 14:09:01 +00:00
9a77030377 2.6.3 2025-03-26 14:05:44 +00:00
ceff285ff5 fix(setup): Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control. 2025-03-26 14:05:44 +00:00
d8bfbf0be3 2.6.2 2025-03-26 13:54:49 +00:00
3e6b883b38 fix(setup/readme): Improve force update instructions and dependency installation process in setup.sh and readme.md 2025-03-26 13:54:49 +00:00
47ef918128 2.6.1 2025-03-26 13:51:45 +00:00
5951638967 fix(setup): Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards. 2025-03-26 13:51:45 +00:00
14 changed files with 1188 additions and 442 deletions

View File

@ -1,5 +1,118 @@
# Changelog # Changelog
## 2025-03-26 - 2.6.17 - fix(logger)
Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set width.
- Removed the reset of currentBoxWidth in logBoxEnd to allow persistent width across logbox calls.
- Ensures that logBoxLine uses the previously set width when no new width is provided.
## 2025-03-26 - 2.6.16 - fix(cli)
Improve CLI logging consistency by replacing direct console output with unified logger calls.
- Replaced console.log and console.error with logger.log and logger.error in CLI commands
- Standardized debug, error, and status messages using logger's logbox utilities
- Enhanced consistency of log output throughout the ts/cli.ts file
## 2025-03-26 - 2.6.15 - fix(logger)
Replace direct console logging with unified logger interface for consistent formatting
- Substitute console.log, console.error, and related calls with logger methods in cli, daemon, systemd, nupst, and index modules
- Integrate logBox formatting for structured output and consistent log presentation
- Update test expectations in test.logger.ts to check for standardized error messages
- Refactor logging calls throughout the codebase for improved clarity and maintainability
## 2025-03-26 - 2.6.14 - fix(systemd)
Shorten closing log divider in systemd service installation output for consistent formatting.
- Replaced the overly long footer with a shorter one in ts/systemd.ts.
- This change improves log readability without affecting functionality.
## 2025-03-26 - 2.6.13 - fix(cli)
Fix CLI update output box formatting
- Adjusted the closing box line in the update process log messages for consistent visual formatting
## 2025-03-26 - 2.6.12 - fix(systemd)
Adjust logging border in systemd service installation output
- Updated the closing border line for consistent output formatting in ts/systemd.ts
## 2025-03-26 - 2.6.11 - fix(cli, systemd)
Adjust log formatting for consistent output in CLI and systemd commands
- Fixed spacing issues in service installation and status log messages in the systemd module.
- Revised output formatting in the CLI to improve message clarity.
## 2025-03-26 - 2.6.10 - fix(daemon)
Adjust console log box formatting for consistent output in daemon status messages
- Updated closing box borders to align properly in configuration error, periodic updates, and UPS status logs
- Improved visual consistency in log messages
## 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.
- Removed full production dependency install in favor of installing only net-snmp@3.20.0
- Added verification step to confirm net-snmp installation
- Generate a minimal package-lock.json if one does not exist
## 2025-03-26 - 2.6.2 - fix(setup/readme)
Improve force update instructions and dependency installation process in setup.sh and readme.md
- Clarify force update commands with explicit paths in readme.md
- Remove existing node_modules before installing dependencies in setup.sh
- Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions
## 2025-03-26 - 2.6.1 - fix(setup)
Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards.
- Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution.
- Log Node.js and npm versions for debugging purposes.
- Restore the original PATH after installing dependencies.
## 2025-03-26 - 2.6.0 - feat(setup) ## 2025-03-26 - 2.6.0 - feat(setup)
Add --force update flag to setup script and update installation instructions Add --force update flag to setup script and update installation instructions

View File

@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/nupst", "name": "@serve.zone/nupst",
"version": "2.6.0", "version": "2.6.17",
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices", "description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
@ -56,5 +56,6 @@
"mongodb-memory-server", "mongodb-memory-server",
"puppeteer" "puppeteer"
] ]
} },
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
} }

2
pnpm-lock.yaml generated
View File

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

View File

@ -234,7 +234,11 @@ This will:
You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code: You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code:
```bash ```bash
bash setup.sh --force # If you're in the nupst directory:
bash ./setup.sh --force
# If you're in another directory, specify the full path:
bash /opt/nupst/setup.sh --force
``` ```
## Security ## Security

View File

@ -239,25 +239,90 @@ echo "dist_ts directory successfully downloaded from npm registry."
# Make launcher script executable # Make launcher script executable
chmod +x "$SCRIPT_DIR/bin/nupst" chmod +x "$SCRIPT_DIR/bin/nupst"
# Install production dependencies # Set up Node.js binary path
echo "Installing production dependencies..." NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin"
"$SCRIPT_DIR/vendor/$NODE_DIR/bin/npm" --prefix "$SCRIPT_DIR" ci --only=production --no-audit --no-fund NODE_BIN="$NODE_BIN_DIR/node"
NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js"
if [ $? -ne 0 ]; then # Ensure we have executable permissions
echo "Warning: Failed to install dependencies with 'npm ci'. Trying 'npm install'..." chmod +x "$NODE_BIN"
"$SCRIPT_DIR/vendor/$NODE_DIR/bin/npm" --prefix "$SCRIPT_DIR" install --only=production --no-audit --no-fund
if [ $? -ne 0 ]; then # Make sure the npm-cli.js exists
echo "Error: Failed to install dependencies. NUPST may not function correctly." if [ ! -f "$NPM_CLI_JS" ]; then
echo "You can try to install dependencies manually by running:" # Try to find npm-cli.js
echo "cd $SCRIPT_DIR && npm install --only=production" 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 else
echo "Dependencies installed successfully with 'npm install'." echo "Found npm-cli.js at: $NPM_CLI_JS"
fi 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"
# 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
# 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"
# 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 "Restoring original package.json..."
mv "$SCRIPT_DIR/package.json.bak" "$SCRIPT_DIR/package.json"
exit 1
else else
echo "Dependencies installed successfully with 'npm ci'." 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
# Remove backup if successful
rm -f "$SCRIPT_DIR/package.json.bak"
fi fi
# No temporary files to clean up
echo "NUPST setup completed successfully." echo "NUPST setup completed successfully."
echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst" echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst"
echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst" echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst"

147
test/test.logger.ts Normal file
View File

@ -0,0 +1,147 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { Logger } from '../ts/logger.js';
// Create a Logger instance for testing
const logger = new Logger();
tap.test('should create a logger instance', async () => {
expect(logger instanceof Logger).toBeTruthy();
});
tap.test('should log messages with different log levels', async () => {
// We're not testing console output directly, just ensuring no errors
logger.log('Regular log message');
logger.error('Error message');
logger.warn('Warning message');
logger.success('Success message');
// Just assert that the test runs without errors
expect(true).toBeTruthy();
});
tap.test('should create a logbox with title, content, and end', async () => {
// Just ensuring no errors occur
logger.logBoxTitle('Test Box', 40);
logger.logBoxLine('This is a test line');
logger.logBoxEnd();
// Just assert that the test runs without errors
expect(true).toBeTruthy();
});
tap.test('should handle width persistence between logbox calls', async () => {
logger.logBoxTitle('Width Test', 45);
// These should use the width from the title
logger.logBoxLine('Line 1');
logger.logBoxLine('Line 2');
logger.logBoxEnd();
let errorThrown = false;
try {
// This should work fine after the reset in logBoxEnd
logger.logBoxTitle('New Box', 30);
logger.logBoxLine('New line');
logger.logBoxEnd();
} catch (error) {
errorThrown = true;
}
expect(errorThrown).toBeFalsy();
});
tap.test('should throw error when using logBoxLine without width', async () => {
let errorThrown = false;
let errorMessage = '';
try {
// Should throw because no width is set
logger.logBoxLine('This should fail');
} catch (error) {
errorThrown = true;
errorMessage = (error as Error).message;
}
expect(errorThrown).toBeTruthy();
expect(errorMessage).toBeTruthy();
expect(errorMessage.includes('No box width')).toBeTruthy();
});
tap.test('should create a complete logbox in one call', async () => {
// Just ensuring no errors occur
logger.logBox('Complete Box', [
'Line 1',
'Line 2',
'Line 3'
], 40);
// Just assert that the test runs without errors
expect(true).toBeTruthy();
});
tap.test('should handle content that exceeds box width', async () => {
// Just ensuring no errors occur when content is too long
logger.logBox('Truncation Test', [
'This line is way too long and should be truncated because it exceeds the available space'
], 30);
// Just assert that the test runs without errors
expect(true).toBeTruthy();
});
tap.test('should create dividers with custom characters', async () => {
// Just ensuring no errors occur
logger.logDivider(30);
logger.logDivider(20, '*');
// Just assert that the test runs without errors
expect(true).toBeTruthy();
});
tap.test('Logger Demo', async () => {
console.log('\n=== LOGGER DEMO ===\n');
// Basic logging
logger.log('Regular log message');
logger.error('Error message');
logger.warn('Warning message');
logger.success('Success message');
// Logbox with title, content lines, and end
logger.logBoxTitle('Configuration Loaded', 50);
logger.logBoxLine('SNMP Settings:');
logger.logBoxLine(' Host: 127.0.0.1');
logger.logBoxLine(' Port: 161');
logger.logBoxLine(' Version: 1');
logger.logBoxEnd();
// Complete logbox in one call
logger.logBox('UPS Status', [
'Power Status: onBattery',
'Battery Capacity: 75%',
'Runtime Remaining: 30 minutes'
], 45);
// Logbox with content that's too long for the width
logger.logBox('Truncation Example', [
'This line is short enough to fit within the box width',
'This line is way too long and will be truncated because it exceeds the available space for content within the logbox'
], 40);
// Demonstrating logbox width being remembered
logger.logBoxTitle('Width Persistence Example', 60);
logger.logBoxLine('These lines use the width from the title');
logger.logBoxLine('No need to specify the width again');
logger.logBoxEnd();
// Divider example
logger.log('\nDivider example:');
logger.logDivider(30);
logger.logDivider(30, '*');
expect(true).toBeTruthy();
});
// Export the default tap object
export default tap.start();

View File

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

497
ts/cli.ts
View File

@ -3,6 +3,7 @@ import { promises as fs } from 'fs';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { Nupst } from './nupst.js'; import { Nupst } from './nupst.js';
import { logger } from './logger.js';
/** /**
* Class for handling CLI commands * Class for handling CLI commands
@ -26,7 +27,7 @@ export class NupstCli {
// Extract debug flag from any position // Extract debug flag from any position
const debugOptions = this.extractDebugOptions(args); const debugOptions = this.extractDebugOptions(args);
if (debugOptions.debugMode) { if (debugOptions.debugMode) {
console.log('Debug mode enabled'); logger.log('Debug mode enabled');
// Enable debug mode in the SNMP client // Enable debug mode in the SNMP client
this.nupst.getSnmp().enableDebug(); this.nupst.getSnmp().enableDebug();
} }
@ -46,7 +47,7 @@ export class NupstCli {
private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } { private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } {
const debugMode = args.includes('--debug') || args.includes('-d'); const debugMode = args.includes('--debug') || args.includes('-d');
// Remove debug flags from args // 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 }; return { debugMode, cleanedArgs };
} }
@ -119,7 +120,7 @@ export class NupstCli {
private async enable(): Promise<void> { private async enable(): Promise<void> {
this.checkRootAccess('This command must be run as root.'); this.checkRootAccess('This command must be run as root.');
await this.nupst.getSystemd().install(); await this.nupst.getSystemd().install();
console.log('NUPST service has been installed. Use "nupst start" to start the service.'); logger.log('NUPST service has been installed. Use "nupst start" to start the service.');
} }
/** /**
@ -127,12 +128,12 @@ export class NupstCli {
* @param debugMode Whether to enable debug mode * @param debugMode Whether to enable debug mode
*/ */
private async daemonStart(debugMode: boolean = false): Promise<void> { private async daemonStart(debugMode: boolean = false): Promise<void> {
console.log('Starting NUPST daemon...'); logger.log('Starting NUPST daemon...');
try { try {
// Enable debug mode for SNMP if requested // Enable debug mode for SNMP if requested
if (debugMode) { if (debugMode) {
this.nupst.getSnmp().enableDebug(); this.nupst.getSnmp().enableDebug();
console.log('SNMP debug mode enabled'); logger.log('SNMP debug mode enabled');
} }
await this.nupst.getDaemon().start(); await this.nupst.getDaemon().start();
} catch (error) { } catch (error) {
@ -148,10 +149,10 @@ export class NupstCli {
try { try {
// Use exec with spawn to properly follow logs in real-time // Use exec with spawn to properly follow logs in real-time
const { spawn } = await import('child_process'); const { spawn } = await import('child_process');
console.log('Tailing nupst service logs (Ctrl+C to exit)...\n'); logger.log('Tailing nupst service logs (Ctrl+C to exit)...\n');
const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], { const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], {
stdio: ['ignore', 'inherit', 'inherit'] stdio: ['ignore', 'inherit', 'inherit'],
}); });
// Forward signals to child process // Forward signals to child process
@ -165,7 +166,7 @@ export class NupstCli {
journalctl.on('exit', () => resolve()); journalctl.on('exit', () => resolve());
}); });
} catch (error) { } catch (error) {
console.error('Failed to retrieve logs:', error); logger.error(`Failed to retrieve logs: ${error}`);
process.exit(1); process.exit(1);
} }
} }
@ -212,7 +213,7 @@ export class NupstCli {
*/ */
private checkRootAccess(errorMessage: string): void { private checkRootAccess(errorMessage: string): void {
if (process.getuid && process.getuid() !== 0) { if (process.getuid && process.getuid() !== 0) {
console.error(errorMessage); logger.error(errorMessage);
process.exit(1); process.exit(1);
} }
} }
@ -225,19 +226,21 @@ export class NupstCli {
try { try {
// Debug mode is now handled in parseAndExecute // Debug mode is now handled in parseAndExecute
if (debugMode) { if (debugMode) {
console.log('┌─ Debug Mode ─────────────────────────────┐'); const boxWidth = 45;
console.log('│ SNMP debugging enabled - detailed logs will be shown'); logger.logBoxTitle('Debug Mode', boxWidth);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
logger.logBoxEnd();
} }
// Try to load the configuration // Try to load the configuration
try { try {
await this.nupst.getDaemon().loadConfig(); await this.nupst.getDaemon().loadConfig();
} catch (error) { } catch (error) {
console.error('┌─ Configuration Error ─────────────────────┐'); const errorBoxWidth = 45;
console.error('│ No configuration found.'); logger.logBoxTitle('Configuration Error', errorBoxWidth);
console.error('│ Please run \'nupst setup\' first to create a configuration.'); logger.logBoxLine('No configuration found.');
console.error('└──────────────────────────────────────────┘'); logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
logger.logBoxEnd();
return; return;
} }
@ -247,7 +250,7 @@ export class NupstCli {
this.displayTestConfig(config); this.displayTestConfig(config);
await this.testConnection(config); await this.testConnection(config);
} catch (error) { } catch (error) {
console.error(`Test failed: ${error.message}`); logger.error(`Test failed: ${error.message}`);
} }
} }
@ -256,44 +259,45 @@ export class NupstCli {
* @param config Current configuration * @param config Current configuration
*/ */
private displayTestConfig(config: any): void { private displayTestConfig(config: any): void {
console.log('┌─ Testing Configuration ─────────────────────┐'); const boxWidth = 45;
console.log('│ SNMP Settings:'); logger.logBoxTitle('Testing Configuration', boxWidth);
console.log(`│ Host: ${config.snmp.host}`); logger.logBoxLine('SNMP Settings:');
console.log(` Port: ${config.snmp.port}`); logger.logBoxLine(` Host: ${config.snmp.host}`);
console.log(`│ Version: ${config.snmp.version}`); logger.logBoxLine(` Port: ${config.snmp.port}`);
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`); logger.logBoxLine(` Version: ${config.snmp.version}`);
logger.logBoxLine(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
if (config.snmp.version === 1 || config.snmp.version === 2) { if (config.snmp.version === 1 || config.snmp.version === 2) {
console.log(` Community: ${config.snmp.community}`); logger.logBoxLine(` Community: ${config.snmp.community}`);
} else if (config.snmp.version === 3) { } else if (config.snmp.version === 3) {
console.log(` Security Level: ${config.snmp.securityLevel}`); logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
console.log(` Username: ${config.snmp.username}`); logger.logBoxLine(` Username: ${config.snmp.username}`);
// Show auth and privacy details based on security level // 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'}`); logger.logBoxLine(` Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
} }
if (config.snmp.securityLevel === 'authPriv') { if (config.snmp.securityLevel === 'authPriv') {
console.log(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`); logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
} }
// Show timeout value // Show timeout value
console.log(` Timeout: ${config.snmp.timeout / 1000} seconds`); logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
} }
// Show OIDs if custom model is selected // Show OIDs if custom model is selected
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) { if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
console.log('Custom OIDs:'); logger.logBoxLine('Custom OIDs:');
console.log(` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`); logger.logBoxLine(` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`);
console.log(` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`); logger.logBoxLine(` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`);
console.log(` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`); logger.logBoxLine(` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
} }
console.log('Thresholds:'); logger.logBoxLine('Thresholds:');
console.log(` Battery: ${config.thresholds.battery}%`); logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
console.log(` Runtime: ${config.thresholds.runtime} minutes`); logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
console.log(`Check Interval: ${config.checkInterval / 1000} seconds`); logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
} }
/** /**
@ -301,32 +305,34 @@ export class NupstCli {
* @param config Current configuration * @param config Current configuration
*/ */
private async testConnection(config: any): Promise<void> { private async testConnection(config: any): Promise<void> {
console.log('\nTesting connection to UPS...'); logger.log('\nTesting connection to UPS...');
try { try {
// Create a test config with a short timeout // Create a test config with a short timeout
const testConfig = { const testConfig = {
...config.snmp, ...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); const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
console.log('┌─ Connection Successful! ─────────────────┐'); const boxWidth = 45;
console.log('│ UPS Status:'); logger.logBoxTitle('Connection Successful!', boxWidth);
console.log(`│ Power Status: ${status.powerStatus}`); logger.logBoxLine('UPS Status:');
console.log(` Battery Capacity: ${status.batteryCapacity}%`); logger.logBoxLine(` Power Status: ${status.powerStatus}`);
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`); logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`);
logger.logBoxEnd();
// Check status against thresholds if on battery // Check status against thresholds if on battery
if (status.powerStatus === 'onBattery') { if (status.powerStatus === 'onBattery') {
this.analyzeThresholds(status, config); this.analyzeThresholds(status, config);
} }
} catch (error) { } catch (error) {
console.error('┌─ Connection Failed! ───────────────────────┐'); const errorBoxWidth = 45;
console.error(`│ Error: ${error.message}`); logger.logBoxTitle('Connection Failed!', errorBoxWidth);
console.error('└──────────────────────────────────────────┘'); logger.logBoxLine(`Error: ${error.message}`);
console.log('\nPlease check your settings and run \'nupst setup\' to reconfigure.'); logger.logBoxEnd();
logger.log("\nPlease check your settings and run 'nupst setup' to reconfigure.");
} }
} }
@ -336,34 +342,43 @@ export class NupstCli {
* @param config Current configuration * @param config Current configuration
*/ */
private analyzeThresholds(status: any, config: any): void { private analyzeThresholds(status: any, config: any): void {
console.log('┌─ Threshold Analysis ───────────────────────┐'); const boxWidth = 45;
logger.logBoxTitle('Threshold Analysis', boxWidth);
if (status.batteryCapacity < config.thresholds.battery) { if (status.batteryCapacity < config.thresholds.battery) {
console.log('⚠️ WARNING: Battery capacity below threshold'); logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`); logger.logBoxLine(
console.log('│ System would initiate shutdown'); ` Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`
);
logger.logBoxLine(' System would initiate shutdown');
} else { } else {
console.log('✓ Battery capacity above threshold'); logger.logBoxLine('✓ Battery capacity above threshold');
console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`); logger.logBoxLine(
` Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`
);
} }
if (status.batteryRuntime < config.thresholds.runtime) { if (status.batteryRuntime < config.thresholds.runtime) {
console.log('⚠️ WARNING: Runtime below threshold'); logger.logBoxLine('⚠️ WARNING: Runtime below threshold');
console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`); logger.logBoxLine(
console.log('│ System would initiate shutdown'); ` Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`
);
logger.logBoxLine(' System would initiate shutdown');
} else { } else {
console.log('✓ Runtime above threshold'); logger.logBoxLine('✓ Runtime above threshold');
console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`); logger.logBoxLine(
` Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`
);
} }
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
} }
/** /**
* Display help message * Display help message
*/ */
private showHelp(): void { private showHelp(): void {
console.log(` logger.log(`
NUPST - Node.js UPS Shutdown Tool NUPST - Node.js UPS Shutdown Tool
Usage: Usage:
@ -393,10 +408,13 @@ Options:
private async update(): Promise<void> { private async update(): Promise<void> {
try { try {
// Check if running as root // 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 ──────────────────┐'); const boxWidth = 45;
console.log(' Updating NUPST from repository...'); logger.logBoxTitle('NUPST Update Process', boxWidth);
logger.logBoxLine('Updating NUPST from repository...');
// Determine the installation directory (assuming it's either /opt/nupst or the current directory) // Determine the installation directory (assuming it's either /opt/nupst or the current directory)
const { existsSync } = await import('fs'); const { existsSync } = await import('fs');
@ -406,60 +424,70 @@ Options:
// If not installed in /opt/nupst, use the current directory // If not installed in /opt/nupst, use the current directory
const { dirname } = await import('path'); const { dirname } = await import('path');
installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
console.log(`Using local installation directory: ${installDir}`); logger.logBoxLine(`Using local installation directory: ${installDir}`);
} }
try { try {
// 1. Update the repository // 1. Update the repository
console.log('Pulling latest changes from git repository...'); logger.logBoxLine('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 // 2. Run the install.sh script
console.log('Running install.sh to update NUPST...'); logger.logBoxLine('Running install.sh to update NUPST...');
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
// 3. Run the setup.sh script with force flag to update Node.js and dependencies // 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...'); logger.logBoxLine('Running setup.sh to update Node.js and dependencies...');
execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' }); execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' });
// 4. Refresh the systemd service // 4. Refresh the systemd service
console.log('Refreshing systemd service...'); logger.logBoxLine('Refreshing systemd service...');
// First check if service exists // 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) { if (serviceExists) {
// Stop the service if it's running // 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) { if (isRunning) {
console.log('Stopping nupst service...'); logger.logBoxLine('Stopping nupst service...');
execSync('systemctl stop nupst.service'); execSync('systemctl stop nupst.service');
} }
// Reinstall the service // Reinstall the service
console.log('Reinstalling systemd service...'); logger.logBoxLine('Reinstalling systemd service...');
await this.nupst.getSystemd().install(); await this.nupst.getSystemd().install();
// Restart the service if it was running // Restart the service if it was running
if (isRunning) { if (isRunning) {
console.log('Restarting nupst service...'); logger.logBoxLine('Restarting nupst service...');
execSync('systemctl start nupst.service'); execSync('systemctl start nupst.service');
} }
} else { } else {
console.log('Systemd service not installed, skipping service refresh.'); logger.logBoxLine('Systemd service not installed, skipping service refresh.');
console.log('Run "nupst enable" to install the service.'); logger.logBoxLine('Run "nupst enable" to install the service.');
} }
console.log('Update completed successfully!'); logger.logBoxLine('Update completed successfully!');
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
} catch (error) { } catch (error) {
console.error('Error during update process:'); logger.logBoxLine('Error during update process:');
console.error(`${error.message}`); logger.logBoxLine(`${error.message}`);
console.error('└──────────────────────────────────────────┘'); logger.logBoxEnd();
process.exit(1); process.exit(1);
} }
} catch (error) { } catch (error) {
console.error(`Update failed: ${error.message}`); logger.error(`Update failed: ${error.message}`);
process.exit(1); process.exit(1);
} }
} }
@ -474,7 +502,7 @@ Options:
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout,
}); });
// Helper function to prompt for input // Helper function to prompt for input
@ -492,7 +520,7 @@ Options:
rl.close(); rl.close();
} }
} catch (error) { } catch (error) {
console.error('Setup error:', error.message); logger.error(`Setup error: ${error.message}`);
} }
} }
@ -501,9 +529,9 @@ Options:
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
*/ */
private async runSetupProcess(prompt: (question: string) => Promise<string>): Promise<void> { private async runSetupProcess(prompt: (question: string) => Promise<string>): Promise<void> {
console.log('\nNUPST Interactive Setup'); logger.log('\nNUPST Interactive Setup');
console.log('======================\n'); logger.log('======================\n');
console.log('This will guide you through configuring your UPS SNMP settings.\n'); logger.log('This will guide you through configuring your UPS SNMP settings.\n');
// Try to load existing config if available // Try to load existing config if available
let config; let config;
@ -513,7 +541,7 @@ Options:
} catch (error) { } catch (error) {
// If config doesn't exist, use default config // If config doesn't exist, use default config
config = this.nupst.getDaemon().getConfig(); config = this.nupst.getDaemon().getConfig();
console.log('No existing configuration found. Creating a new configuration.'); logger.log('No existing configuration found. Creating a new configuration.');
} }
// Gather SNMP settings // Gather SNMP settings
@ -536,7 +564,7 @@ Options:
// Check if service is running and restart it if needed // Check if service is running and restart it if needed
await this.restartServiceIfRunning(); await this.restartServiceIfRunning();
console.log('\nSetup complete!'); logger.log('\nSetup complete!');
await this.optionallyEnableService(prompt); await this.optionallyEnableService(prompt);
} }
@ -546,7 +574,10 @@ Options:
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
* @returns Updated configuration * @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 // SNMP IP Address
const defaultHost = config.snmp.host; const defaultHost = config.snmp.host;
const host = await prompt(`UPS IP Address [${defaultHost}]: `); const host = await prompt(`UPS IP Address [${defaultHost}]: `);
@ -556,7 +587,7 @@ Options:
const defaultPort = config.snmp.port; const defaultPort = config.snmp.port;
const portInput = await prompt(`SNMP Port [${defaultPort}]: `); const portInput = await prompt(`SNMP Port [${defaultPort}]: `);
const port = parseInt(portInput, 10); 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 // SNMP Version
const defaultVersion = config.snmp.version; const defaultVersion = config.snmp.version;
@ -566,7 +597,10 @@ Options:
console.log(' 3) SNMPv3 (with security features)'); console.log(' 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);
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) { if (config.snmp.version === 1 || config.snmp.version === 2) {
// SNMP Community String (for v1/v2c) // SNMP Community String (for v1/v2c)
@ -587,7 +621,10 @@ Options:
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
* @returns Updated configuration * @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:'); console.log('\nSNMPv3 Security Settings:');
// Security Level // Security Level
@ -595,9 +632,13 @@ Options:
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)'); console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
console.log(' 2) authNoPriv (Authentication, No Privacy)'); console.log(' 2) authNoPriv (Authentication, No Privacy)');
console.log(' 3) authPriv (Authentication and Privacy)'); console.log(' 3) authPriv (Authentication and Privacy)');
const defaultSecLevel = config.snmp.securityLevel ? const defaultSecLevel = config.snmp.securityLevel
(config.snmp.securityLevel === 'noAuthNoPriv' ? 1 : ? config.snmp.securityLevel === 'noAuthNoPriv'
config.snmp.securityLevel === 'authNoPriv' ? 2 : 3) : 3; ? 1
: config.snmp.securityLevel === 'authNoPriv'
? 2
: 3
: 3;
const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `); const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `);
const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel; const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel;
@ -639,7 +680,9 @@ Options:
// Allow customizing the timeout value // Allow customizing the timeout value
const defaultTimeout = config.snmp.timeout / 1000; // Convert from ms to seconds for display 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 timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
const timeout = parseInt(timeoutInput, 10); const timeout = parseInt(timeoutInput, 10);
if (timeoutInput.trim() && !isNaN(timeout)) { if (timeoutInput.trim() && !isNaN(timeout)) {
@ -656,13 +699,18 @@ Options:
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
* @returns Updated configuration * @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 // Authentication protocol
console.log('\nAuthentication Protocol:'); console.log('\nAuthentication Protocol:');
console.log(' 1) MD5'); console.log(' 1) MD5');
console.log(' 2) SHA'); console.log(' 2) SHA');
const defaultAuthProtocol = config.snmp.authProtocol === 'SHA' ? 2 : 1; 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; const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
config.snmp.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5'; config.snmp.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
@ -680,7 +728,10 @@ Options:
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
* @returns Updated configuration * @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 // Privacy protocol
console.log('\nPrivacy Protocol:'); console.log('\nPrivacy Protocol:');
console.log(' 1) DES'); console.log(' 1) DES');
@ -704,22 +755,31 @@ Options:
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
* @returns Updated configuration * @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:'); console.log('\nShutdown Thresholds:');
// Battery threshold // Battery threshold
const defaultBatteryThreshold = config.thresholds.battery; 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); const batteryThreshold = parseInt(batteryThresholdInput, 10);
config.thresholds.battery = (batteryThresholdInput.trim() && !isNaN(batteryThreshold)) config.thresholds.battery =
batteryThresholdInput.trim() && !isNaN(batteryThreshold)
? batteryThreshold ? batteryThreshold
: defaultBatteryThreshold; : defaultBatteryThreshold;
// Runtime threshold // Runtime threshold
const defaultRuntimeThreshold = config.thresholds.runtime; 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); const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
config.thresholds.runtime = (runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)) config.thresholds.runtime =
runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
? runtimeThreshold ? runtimeThreshold
: defaultRuntimeThreshold; : defaultRuntimeThreshold;
@ -727,7 +787,8 @@ Options:
const defaultInterval = config.checkInterval / 1000; // Convert from ms to seconds for display const defaultInterval = config.checkInterval / 1000; // Convert from ms to seconds for display
const intervalInput = await prompt(`Check interval in seconds [${defaultInterval}]: `); const intervalInput = await prompt(`Check interval in seconds [${defaultInterval}]: `);
const interval = parseInt(intervalInput, 10); const interval = parseInt(intervalInput, 10);
config.checkInterval = (intervalInput.trim() && !isNaN(interval)) config.checkInterval =
intervalInput.trim() && !isNaN(interval)
? interval * 1000 // Convert to ms ? interval * 1000 // Convert to ms
: defaultInterval * 1000; : defaultInterval * 1000;
@ -740,7 +801,10 @@ Options:
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
* @returns Updated configuration * @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('\nUPS Model Selection:');
console.log(' 1) CyberPower'); console.log(' 1) CyberPower');
console.log(' 2) APC'); console.log(' 2) APC');
@ -749,12 +813,20 @@ Options:
console.log(' 5) Liebert/Vertiv'); console.log(' 5) Liebert/Vertiv');
console.log(' 6) Custom (Advanced)'); console.log(' 6) Custom (Advanced)');
const defaultModelValue = config.snmp.upsModel === 'cyberpower' ? 1 : const defaultModelValue =
config.snmp.upsModel === 'apc' ? 2 : config.snmp.upsModel === 'cyberpower'
config.snmp.upsModel === 'eaton' ? 3 : ? 1
config.snmp.upsModel === 'tripplite' ? 4 : : config.snmp.upsModel === 'apc'
config.snmp.upsModel === 'liebert' ? 5 : ? 2
config.snmp.upsModel === 'custom' ? 6 : 1; : 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 modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
const modelValue = parseInt(modelInput, 10) || defaultModelValue; const modelValue = parseInt(modelInput, 10) || defaultModelValue;
@ -783,7 +855,7 @@ Options:
config.snmp.customOIDs = { config.snmp.customOIDs = {
POWER_STATUS: powerStatusOID.trim(), POWER_STATUS: powerStatusOID.trim(),
BATTERY_CAPACITY: batteryCapacityOID.trim(), BATTERY_CAPACITY: batteryCapacityOID.trim(),
BATTERY_RUNTIME: batteryRuntimeOID.trim() BATTERY_RUNTIME: batteryRuntimeOID.trim(),
}; };
} }
@ -795,13 +867,18 @@ Options:
* @param config Current configuration * @param config Current configuration
*/ */
private displayConfigSummary(config: any): void { private displayConfigSummary(config: any): void {
console.log('\n┌─ Configuration Summary ─────────────────┐'); const boxWidth = 45;
console.log(`│ SNMP Host: ${config.snmp.host}:${config.snmp.port}`); logger.log('');
console.log(`│ SNMP Version: ${config.snmp.version}`); logger.logBoxTitle('Configuration Summary', boxWidth);
console.log(`│ UPS Model: ${config.snmp.upsModel}`); logger.logBoxLine(`SNMP Host: ${config.snmp.host}:${config.snmp.port}`);
console.log(`│ Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`); logger.logBoxLine(`SNMP Version: ${config.snmp.version}`);
console.log(`│ Check Interval: ${config.checkInterval/1000} seconds`); logger.logBoxLine(`UPS Model: ${config.snmp.upsModel}`);
console.log('└──────────────────────────────────────────┘\n'); logger.logBoxLine(
`Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`
);
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
logger.logBoxEnd();
logger.log('');
} }
/** /**
@ -809,29 +886,38 @@ Options:
* @param config Current configuration * @param config Current configuration
* @param prompt Function to prompt for user input * @param prompt Function to prompt for user input
*/ */
private async optionallyTestConnection(config: any, prompt: (question: string) => Promise<string>): Promise<void> { private async optionallyTestConnection(
const testConnection = await prompt('Would you like to test the connection to your UPS? (y/N): '); 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') { if (testConnection.toLowerCase() === 'y') {
console.log('\nTesting connection to UPS...'); logger.log('\nTesting connection to UPS...');
try { try {
// Create a test config with a short timeout // Create a test config with a short timeout
const testConfig = { const testConfig = {
...config.snmp, ...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); const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
console.log('\n┌─ Connection Successful! ─────────────────┐'); const boxWidth = 45;
console.log('│ UPS Status:'); logger.log('');
console.log(`│ ✓ Power Status: ${status.powerStatus}`); logger.logBoxTitle('Connection Successful!', boxWidth);
console.log(`│ ✓ Battery Capacity: ${status.batteryCapacity}%`); logger.logBoxLine('UPS Status:');
console.log(`│ ✓ Runtime Remaining: ${status.batteryRuntime} minutes`); logger.logBoxLine(`✓ Power Status: ${status.powerStatus}`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine(`✓ Battery Capacity: ${status.batteryCapacity}%`);
logger.logBoxLine(`✓ Runtime Remaining: ${status.batteryRuntime} minutes`);
logger.logBoxEnd();
} catch (error) { } catch (error) {
console.error('\n┌─ Connection Failed! ───────────────────────┐'); const errorBoxWidth = 45;
console.error('│ Error: ' + error.message); logger.log('');
console.error('└──────────────────────────────────────────┘'); logger.logBoxTitle('Connection Failed!', errorBoxWidth);
console.log('\nPlease check your settings and try again.'); logger.logBoxLine(`Error: ${error.message}`);
logger.logBoxEnd();
logger.log('\nPlease check your settings and try again.');
} }
} }
} }
@ -843,31 +929,33 @@ Options:
private async restartServiceIfRunning(): Promise<void> { private async restartServiceIfRunning(): Promise<void> {
try { try {
// Check if the service is active // 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) { if (isActive) {
// Service is running, restart it // Service is running, restart it
console.log('┌─ Service Update ─────────────────────────┐'); const boxWidth = 45;
console.log('│ Configuration has changed.'); logger.logBoxTitle('Service Update', boxWidth);
console.log('│ Restarting NUPST service to apply changes...'); logger.logBoxLine('Configuration has changed.');
logger.logBoxLine('Restarting NUPST service to apply changes...');
try { try {
if (process.getuid && process.getuid() === 0) { if (process.getuid && process.getuid() === 0) {
// We have root access, restart directly // We have root access, restart directly
execSync('systemctl restart nupst.service'); execSync('systemctl restart nupst.service');
console.log('Service restarted successfully.'); logger.logBoxLine('Service restarted successfully.');
} else { } else {
// No root access, show instructions // No root access, show instructions
console.log('Please restart the service with:'); logger.logBoxLine('Please restart the service with:');
console.log(' sudo systemctl restart nupst.service'); logger.logBoxLine(' sudo systemctl restart nupst.service');
} }
} catch (error) { } catch (error) {
console.log(`Error restarting service: ${error.message}`); logger.logBoxLine(`Error restarting service: ${error.message}`);
console.log('You may need to restart the service manually:'); logger.logBoxLine('You may need to restart the service manually:');
console.log(' sudo systemctl restart nupst.service'); logger.logBoxLine(' sudo systemctl restart nupst.service');
} }
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
} }
} catch (error) { } catch (error) {
// Ignore errors checking service status // Ignore errors checking service status
@ -878,18 +966,24 @@ Options:
* Optionally enable and start systemd service * Optionally enable and start systemd service
* @param prompt Function to prompt for user input * @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) { if (process.getuid && process.getuid() !== 0) {
console.log('\nNote: Run "sudo nupst enable" to set up NUPST as a system service.'); console.log('\nNote: Run "sudo nupst enable" to set up NUPST as a system service.');
} else { } 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') { if (setupService.toLowerCase() === 'y') {
try { try {
await this.nupst.getSystemd().install(); await this.nupst.getSystemd().install();
console.log('Service installed and enabled to start on boot.'); console.log('Service installed and enabled to start on boot.');
// Ask if the user wants to start the service now // 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') { if (startService.toLowerCase() !== 'n') {
await this.nupst.getSystemd().start(); await this.nupst.getSystemd().start();
console.log('NUPST service started successfully.'); console.log('NUPST service started successfully.');
@ -912,80 +1006,89 @@ Options:
try { try {
await this.nupst.getDaemon().loadConfig(); await this.nupst.getDaemon().loadConfig();
} catch (error) { } catch (error) {
console.error('┌─ Configuration Error ─────────────────────┐'); const errorBoxWidth = 45;
console.error('│ No configuration found.'); logger.logBoxTitle('Configuration Error', errorBoxWidth);
console.error('│ Please run \'nupst setup\' first to create a configuration.'); logger.logBoxLine('No configuration found.');
console.error('└──────────────────────────────────────────┘'); logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
logger.logBoxEnd();
return; return;
} }
// Get current configuration // Get current configuration
const config = this.nupst.getDaemon().getConfig(); const config = this.nupst.getDaemon().getConfig();
console.log('┌─ NUPST Configuration ──────────────────────┐'); const boxWidth = 50;
logger.logBoxTitle('NUPST Configuration', boxWidth);
// SNMP Settings // SNMP Settings
console.log('SNMP Settings:'); logger.logBoxLine('SNMP Settings:');
console.log(` Host: ${config.snmp.host}`); logger.logBoxLine(` Host: ${config.snmp.host}`);
console.log(` Port: ${config.snmp.port}`); logger.logBoxLine(` Port: ${config.snmp.port}`);
console.log(` Version: ${config.snmp.version}`); logger.logBoxLine(` Version: ${config.snmp.version}`);
console.log(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`); logger.logBoxLine(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
if (config.snmp.version === 1 || config.snmp.version === 2) { if (config.snmp.version === 1 || config.snmp.version === 2) {
console.log(` Community: ${config.snmp.community}`); logger.logBoxLine(` Community: ${config.snmp.community}`);
} else if (config.snmp.version === 3) { } else if (config.snmp.version === 3) {
console.log(` Security Level: ${config.snmp.securityLevel}`); logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
console.log(` Username: ${config.snmp.username}`); logger.logBoxLine(` Username: ${config.snmp.username}`);
// Show auth and privacy details based on security level // Show auth and privacy details based on security level
if (config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv') { if (
console.log(`│ Auth Protocol: ${config.snmp.authProtocol || 'None'}`); config.snmp.securityLevel === 'authNoPriv' ||
config.snmp.securityLevel === 'authPriv'
) {
logger.logBoxLine(` Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
} }
if (config.snmp.securityLevel === 'authPriv') { if (config.snmp.securityLevel === 'authPriv') {
console.log(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`); logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
} }
// Show timeout value // Show timeout value
console.log(` Timeout: ${config.snmp.timeout / 1000} seconds`); logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
} }
// Show OIDs if custom model is selected // Show OIDs if custom model is selected
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) { if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
console.log('Custom OIDs:'); logger.logBoxLine('Custom OIDs:');
console.log(` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`); logger.logBoxLine(` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`);
console.log(`│ Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`); logger.logBoxLine(
console.log(` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`); ` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`
);
logger.logBoxLine(` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
} }
// Thresholds // Thresholds
console.log('Thresholds:'); logger.logBoxLine('Thresholds:');
console.log(` Battery: ${config.thresholds.battery}%`); logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
console.log(` Runtime: ${config.thresholds.runtime} minutes`); logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
console.log(`Check Interval: ${config.checkInterval / 1000} seconds`); logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
// Configuration file location // Configuration file location
console.log(''); logger.logBoxLine('');
console.log('Configuration File Location:'); logger.logBoxLine('Configuration File Location:');
console.log(' /etc/nupst/config.json'); logger.logBoxLine(' /etc/nupst/config.json');
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
// Show service status // Show service status
try { try {
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; const isActive =
const isEnabled = execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled'; 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 ─────────────────────────┐'); const statusBoxWidth = 45;
console.log(`│ Service Active: ${isActive ? 'Yes' : 'No'}`); logger.logBoxTitle('Service Status', statusBoxWidth);
console.log(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`); logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
logger.logBoxEnd();
} catch (error) { } catch (error) {
// Ignore errors checking service status // Ignore errors checking service status
} }
} catch (error) { } catch (error) {
console.error(`Failed to display configuration: ${error.message}`); logger.error(`Failed to display configuration: ${error.message}`);
} }
} }
@ -1002,7 +1105,7 @@ Options:
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout,
}); });
// Helper function to prompt for input // Helper function to prompt for input
@ -1019,7 +1122,9 @@ Options:
console.log('This will completely remove NUPST from your system.\n'); console.log('This will completely remove NUPST from your system.\n');
// Ask about removing configuration // 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 // Find the uninstall.sh script location
let uninstallScriptPath: string; let uninstallScriptPath: string;
@ -1036,10 +1141,7 @@ Options:
await fs.access(uninstallScriptPath); await fs.access(uninstallScriptPath);
} catch (error) { } catch (error) {
// If we can't find it in the expected location, try common installation paths // If we can't find it in the expected location, try common installation paths
const commonPaths = [ const commonPaths = ['/opt/nupst/uninstall.sh', join(process.cwd(), 'uninstall.sh')];
'/opt/nupst/uninstall.sh',
join(process.cwd(), 'uninstall.sh')
];
for (const path of commonPaths) { for (const path of commonPaths) {
try { try {
@ -1069,15 +1171,14 @@ Options:
...process.env, ...process.env,
REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no', REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no',
REMOVE_REPO: 'yes', // Always remove repo as requested REMOVE_REPO: 'yes', // Always remove repo as requested
NUPST_CLI_CALL: 'true' // Flag to indicate this is being called from CLI NUPST_CLI_CALL: 'true', // Flag to indicate this is being called from CLI
}; };
// Run the uninstall script with sudo // Run the uninstall script with sudo
execSync(`sudo bash ${uninstallScriptPath}`, { execSync(`sudo bash ${uninstallScriptPath}`, {
env, env,
stdio: 'inherit' // Show output in the terminal stdio: 'inherit', // Show output in the terminal
}); });
} catch (error) { } catch (error) {
console.error(`Uninstall failed: ${error.message}`); console.error(`Uninstall failed: ${error.message}`);
process.exit(1); process.exit(1);

View File

@ -1,11 +1,13 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { exec } from 'child_process'; import { exec, execFile } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { NupstSnmp } from './snmp/manager.js'; import { NupstSnmp } from './snmp/manager.js';
import type { ISnmpConfig } from './snmp/types.js'; import type { ISnmpConfig } from './snmp/types.js';
import { logger } from './logger.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
/** /**
* Configuration interface for the daemon * Configuration interface for the daemon
@ -124,7 +126,7 @@ export class NupstDaemon {
console.error('┌─ Configuration Error ─────────────────────┐'); console.error('┌─ Configuration Error ─────────────────────┐');
console.error(`${message}`); console.error(`${message}`);
console.error('│ Please run \'nupst setup\' first to create a configuration.'); console.error('│ Please run \'nupst setup\' first to create a configuration.');
console.error('└──────────────────────────────────────────┘'); console.error('└──────────────────────────────────────────┘');
} }
/** /**
@ -146,11 +148,11 @@ export class NupstDaemon {
*/ */
public async start(): Promise<void> { public async start(): Promise<void> {
if (this.isRunning) { if (this.isRunning) {
console.log('Daemon is already running'); logger.log('Daemon is already running');
return; return;
} }
console.log('Starting NUPST daemon...'); logger.log('Starting NUPST daemon...');
try { try {
// Load configuration - this will throw an error if config doesn't exist // Load configuration - this will throw an error if config doesn't exist
@ -164,11 +166,12 @@ export class NupstDaemon {
this.snmp.getNupst().checkForUpdates().then(updateAvailable => { this.snmp.getNupst().checkForUpdates().then(updateAvailable => {
if (updateAvailable) { if (updateAvailable) {
const updateStatus = this.snmp.getNupst().getUpdateStatus(); const updateStatus = this.snmp.getNupst().getUpdateStatus();
console.log('┌─ Update Available ───────────────────────┐'); const boxWidth = 45;
console.log(`│ Current Version: ${updateStatus.currentVersion}`); logger.logBoxTitle('Update Available', boxWidth);
console.log(`│ Latest Version: ${updateStatus.latestVersion}`); logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`);
console.log('│ Run "sudo nupst update" to update'); logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine('Run "sudo nupst update" to update');
logger.logBoxEnd();
} }
}).catch(() => {}); // Ignore errors checking for updates }).catch(() => {}); // Ignore errors checking for updates
@ -177,7 +180,7 @@ export class NupstDaemon {
await this.monitor(); await this.monitor();
} catch (error) { } catch (error) {
this.isRunning = false; this.isRunning = false;
console.error(`Daemon failed to start: ${error.message}`); logger.error(`Daemon failed to start: ${error.message}`);
process.exit(1); // Exit with error process.exit(1); // Exit with error
} }
} }
@ -186,23 +189,24 @@ export class NupstDaemon {
* Log the loaded configuration settings * Log the loaded configuration settings
*/ */
private logConfigLoaded(): void { private logConfigLoaded(): void {
console.log('┌─ Configuration Loaded ─────────────────────┐'); const boxWidth = 50;
console.log('│ SNMP Settings:'); logger.logBoxTitle('Configuration Loaded', boxWidth);
console.log(`│ Host: ${this.config.snmp.host}`); logger.logBoxLine('SNMP Settings:');
console.log(` Port: ${this.config.snmp.port}`); logger.logBoxLine(` Host: ${this.config.snmp.host}`);
console.log(`│ Version: ${this.config.snmp.version}`); logger.logBoxLine(` Port: ${this.config.snmp.port}`);
console.log('│ Thresholds:'); logger.logBoxLine(` Version: ${this.config.snmp.version}`);
console.log(`│ Battery: ${this.config.thresholds.battery}%`); logger.logBoxLine('Thresholds:');
console.log(`│ Runtime: ${this.config.thresholds.runtime} minutes`); logger.logBoxLine(` Battery: ${this.config.thresholds.battery}%`);
console.log(`│ Check Interval: ${this.config.checkInterval / 1000} seconds`); logger.logBoxLine(` Runtime: ${this.config.thresholds.runtime} minutes`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
logger.logBoxEnd();
} }
/** /**
* Stop the monitoring daemon * Stop the monitoring daemon
*/ */
public stop(): void { public stop(): void {
console.log('Stopping NUPST daemon...'); logger.log('Stopping NUPST daemon...');
this.isRunning = false; this.isRunning = false;
} }
@ -210,7 +214,7 @@ export class NupstDaemon {
* Monitor the UPS status and trigger shutdown when necessary * Monitor the UPS status and trigger shutdown when necessary
*/ */
private async monitor(): Promise<void> { private async monitor(): Promise<void> {
console.log('Starting UPS monitoring...'); logger.log('Starting UPS monitoring...');
let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown'; let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
let lastLogTime = 0; // Track when we last logged status let lastLogTime = 0; // Track when we last logged status
@ -225,20 +229,22 @@ export class NupstDaemon {
// Log status changes // Log status changes
if (status.powerStatus !== lastStatus) { if (status.powerStatus !== lastStatus) {
console.log('┌──────────────────────────────────────────┐'); const statusBoxWidth = 45;
console.log(`Power status changed: ${lastStatus}${status.powerStatus}`); logger.logBoxTitle('Power Status Change', statusBoxWidth);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine(`Status changed: ${lastStatus}${status.powerStatus}`);
logger.logBoxEnd();
lastStatus = status.powerStatus; lastStatus = status.powerStatus;
lastLogTime = currentTime; // Reset log timer when status changes lastLogTime = currentTime; // Reset log timer when status changes
} }
// Log status periodically (at least every 5 minutes) // Log status periodically (at least every 5 minutes)
else if (shouldLogStatus) { else if (shouldLogStatus) {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
console.log('┌──────────────────────────────────────────┐'); const periodicBoxWidth = 45;
console.log(`│ [${timestamp}] Periodic Status Update`); logger.logBoxTitle('Periodic Status Update', periodicBoxWidth);
console.log(`│ Power Status: ${status.powerStatus}`); logger.logBoxLine(`Timestamp: ${timestamp}`);
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`); logger.logBoxLine(`Power Status: ${status.powerStatus}`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine(`Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
logger.logBoxEnd();
lastLogTime = currentTime; lastLogTime = currentTime;
} }
@ -266,8 +272,8 @@ export class NupstDaemon {
batteryCapacity: number, batteryCapacity: number,
batteryRuntime: number batteryRuntime: number
}): Promise<void> { }): Promise<void> {
console.log('┌─ UPS Status ───────────────────────────────┐'); console.log('┌─ UPS Status ─────────────────────────────┐');
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`); console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
console.log('└──────────────────────────────────────────┘'); console.log('└──────────────────────────────────────────┘');
// Check battery threshold // Check battery threshold
@ -292,30 +298,108 @@ export class NupstDaemon {
* @param reason Reason for shutdown * @param reason Reason for shutdown
*/ */
public async initiateShutdown(reason: string): Promise<void> { public async initiateShutdown(reason: string): Promise<void> {
console.log(`Initiating system shutdown due to: ${reason}`); logger.log(`Initiating system shutdown due to: ${reason}`);
// Set a longer delay for shutdown to allow VMs and services to close // Set a longer delay for shutdown to allow VMs and services to close
const shutdownDelayMinutes = 5; const shutdownDelayMinutes = 5;
try { try {
// 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;
logger.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 // 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"`); logger.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
console.log('Shutdown initiated:', stdout); const { stdout } = await execFileAsync(shutdownCmd, [
console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); '-h',
`+${shutdownDelayMinutes}`,
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`
]);
logger.log(`Shutdown initiated: ${stdout}`);
logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
} else {
// Try using the PATH to find shutdown
try {
logger.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
});
logger.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 // Monitor UPS during shutdown and force immediate shutdown if battery gets too low
console.log('Monitoring UPS during shutdown process...'); logger.log('Monitoring UPS during shutdown process...');
await this.monitorDuringShutdown(); await this.monitorDuringShutdown();
} catch (error) { } catch (error) {
console.error('Failed to initiate shutdown:', error); logger.error(`Failed to initiate shutdown: ${error}`);
// Try a different method if first one fails
// 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 { try {
console.log('Trying alternative shutdown method...'); // First check if command exists in common system paths
await execAsync('poweroff --force'); const paths = [
} catch (innerError) { `/sbin/${alt.cmd}`,
console.error('All shutdown methods failed:', innerError); `/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) {
logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
await execFileAsync(cmdPath, alt.args);
return; // Exit if successful
} else {
// Try using PATH environment
logger.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) {
logger.error(`Alternative method ${alt.cmd} failed: ${altError}`);
// Continue to next method
}
}
logger.error('All shutdown methods failed');
}
} }
/** /**
@ -346,10 +430,79 @@ export class NupstDaemon {
console.log('└──────────────────────────────────────────┘'); console.log('└──────────────────────────────────────────┘');
try { 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) { } catch (error) {
console.error('Emergency shutdown failed, trying alternative method...'); console.error('Emergency shutdown failed, trying alternative methods...');
await execAsync('poweroff --force');
// 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 // Stop monitoring after initiating emergency shutdown

View File

@ -1,6 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import { NupstCli } from './cli.js'; import { NupstCli } from './cli.js';
import { logger } from './logger.js';
/** /**
* Main entry point for NUPST * Main entry point for NUPST
@ -13,6 +14,6 @@ async function main() {
// Run the main function and handle any errors // Run the main function and handle any errors
main().catch(error => { main().catch(error => {
console.error('Error:', error); logger.error(`Error: ${error}`);
process.exit(1); process.exit(1);
}); });

144
ts/logger.ts Normal file
View File

@ -0,0 +1,144 @@
/**
* A simple logger class that provides consistent formatting for log messages
* including support for logboxes with title, lines, and closing
*/
export class Logger {
private currentBoxWidth: number | null = null;
private static instance: Logger;
/**
* Creates a new Logger instance
*/
constructor() {
this.currentBoxWidth = null;
}
/**
* Get the singleton logger instance
* @returns The singleton logger instance
*/
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
/**
* Log a message
* @param message Message to log
*/
public log(message: string): void {
console.log(message);
}
/**
* Log an error message
* @param message Error message to log
*/
public error(message: string): void {
console.error(message);
}
/**
* Log a warning message with a warning emoji
* @param message Warning message to log
*/
public warn(message: string): void {
console.warn(`⚠️ ${message}`);
}
/**
* Log a success message with a checkmark
* @param message Success message to log
*/
public success(message: string): void {
console.log(`${message}`);
}
/**
* Log a logbox title and set the current box width
* @param title Title of the logbox
* @param width Width of the logbox (including borders)
*/
public logBoxTitle(title: string, width: number): void {
this.currentBoxWidth = width;
// Create the title line with appropriate padding
const paddedTitle = ` ${title} `;
const remainingSpace = width - 3 - paddedTitle.length;
// Title line: ┌─ Title ───┐
const titleLine = `┌─${paddedTitle}${'─'.repeat(remainingSpace)}`;
console.log(titleLine);
}
/**
* Log a logbox line
* @param content Content of the line
* @param width Optional width override. If not provided, uses the current box width.
*/
public logBoxLine(content: string, width?: number): void {
const boxWidth = width || this.currentBoxWidth;
if (!boxWidth) {
throw new Error('No box width specified and no previous box width to use');
}
// Calculate the available space for content
const availableSpace = boxWidth - 2; // Account for left and right borders
if (content.length <= availableSpace - 1) {
// If content fits with at least one space for the right border stripe
const padding = availableSpace - content.length - 1;
console.log(`${content}${' '.repeat(padding)}`);
} else {
// Content is too long, let it flow out of boundaries.
console.log(`${content}`);
}
}
/**
* Log a logbox end
* @param width Optional width override. If not provided, uses the current box width.
*/
public logBoxEnd(width?: number): void {
const boxWidth = width || this.currentBoxWidth;
if (!boxWidth) {
throw new Error('No box width specified and no previous box width to use');
}
// Create the bottom border: └────────┘
console.log(`${'─'.repeat(boxWidth - 2)}`);
}
/**
* Log a complete logbox with title, content lines, and ending
* @param title Title of the logbox
* @param lines Array of content lines
* @param width Width of the logbox
*/
public logBox(title: string, lines: string[], width: number): void {
this.logBoxTitle(title, width);
for (const line of lines) {
this.logBoxLine(line);
}
this.logBoxEnd();
}
/**
* Log a divider line
* @param width Width of the divider
* @param character Character to use for the divider (default: ─)
*/
public logDivider(width: number, character: string = '─'): void {
console.log(character.repeat(width));
}
}
// Export a singleton instance for easy use
export const logger = Logger.getInstance();

View File

@ -4,6 +4,7 @@ import { NupstSystemd } from './systemd.js';
import { commitinfo } from './00_commitinfo_data.js'; import { commitinfo } from './00_commitinfo_data.js';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import * as https from 'https'; import * as https from 'https';
import { logger } from './logger.js';
/** /**
* Main Nupst class that coordinates all components * Main Nupst class that coordinates all components
@ -70,7 +71,7 @@ export class Nupst {
return this.updateAvailable; return this.updateAvailable;
} catch (error) { } catch (error) {
console.error(`Error checking for updates: ${error.message}`); logger.error(`Error checking for updates: ${error.message}`);
return false; return false;
} }
} }
@ -162,28 +163,33 @@ export class Nupst {
*/ */
public logVersionInfo(checkForUpdates: boolean = true): void { public logVersionInfo(checkForUpdates: boolean = true): void {
const version = this.getVersion(); const version = this.getVersion();
console.log('┌─ NUPST Version ────────────────────────┐'); const boxWidth = 45;
console.log(`│ Current Version: ${version}`);
logger.logBoxTitle('NUPST Version', boxWidth);
logger.logBoxLine(`Current Version: ${version}`);
if (this.updateAvailable && this.latestVersion) { if (this.updateAvailable && this.latestVersion) {
console.log(`Update Available: ${this.latestVersion}`); logger.logBoxLine(`Update Available: ${this.latestVersion}`);
console.log('Run "sudo nupst update" to update'); logger.logBoxLine('Run "sudo nupst update" to update');
logger.logBoxEnd();
} else if (checkForUpdates) { } else if (checkForUpdates) {
console.log('Checking for updates...'); logger.logBoxLine('Checking for updates...');
// We can't end the box yet since we're in an async operation
this.checkForUpdates().then(updateAvailable => { this.checkForUpdates().then(updateAvailable => {
if (updateAvailable) { if (updateAvailable) {
console.log(`Update Available: ${this.latestVersion}`); logger.logBoxLine(`Update Available: ${this.latestVersion}`);
console.log('Run "sudo nupst update" to update'); logger.logBoxLine('Run "sudo nupst update" to update');
} else { } else {
console.log('You are running the latest version'); logger.logBoxLine('You are running the latest version');
} }
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
}).catch(() => { }).catch(() => {
console.log('Could not check for updates'); logger.logBoxLine('Could not check for updates');
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
}); });
} else { } else {
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
} }
} }
} }

View File

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

View File

@ -1,6 +1,7 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { NupstDaemon } from './daemon.js'; import { NupstDaemon } from './daemon.js';
import { logger } from './logger.js';
/** /**
* Class for managing systemd service * Class for managing systemd service
@ -47,10 +48,11 @@ WantedBy=multi-user.target
try { try {
await fs.access(configPath); await fs.access(configPath);
} catch (error) { } catch (error) {
console.error('┌─ Configuration Error ─────────────────────┐'); const boxWidth = 50;
console.error(`│ No configuration file found at ${configPath}`); logger.logBoxTitle('Configuration Error', boxWidth);
console.error('│ Please run \'nupst setup\' first to create a configuration.'); logger.logBoxLine(`No configuration file found at ${configPath}`);
console.error('└──────────────────────────────────────────┘'); logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
logger.logBoxEnd();
throw new Error('Configuration not found'); throw new Error('Configuration not found');
} }
} }
@ -66,23 +68,24 @@ WantedBy=multi-user.target
// Write the service file // Write the service file
await fs.writeFile(this.serviceFilePath, this.serviceTemplate); await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
console.log('┌─ Service Installation ─────────────────────┐'); const boxWidth = 50;
console.log(`│ Service file created at ${this.serviceFilePath}`); logger.logBoxTitle('Service Installation', boxWidth);
logger.logBoxLine(`Service file created at ${this.serviceFilePath}`);
// Reload systemd daemon // Reload systemd daemon
execSync('systemctl daemon-reload'); execSync('systemctl daemon-reload');
console.log('Systemd daemon reloaded'); logger.logBoxLine('Systemd daemon reloaded');
// Enable the service // Enable the service
execSync('systemctl enable nupst.service'); execSync('systemctl enable nupst.service');
console.log('Service enabled to start on boot'); logger.logBoxLine('Service enabled to start on boot');
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
} catch (error) { } catch (error) {
if (error.message === 'Configuration not found') { if (error.message === 'Configuration not found') {
// Just rethrow the error as the message has already been displayed // Just rethrow the error as the message has already been displayed
throw error; throw error;
} }
console.error('Failed to install systemd service:', error); logger.error(`Failed to install systemd service: ${error}`);
throw error; throw error;
} }
} }
@ -97,15 +100,16 @@ WantedBy=multi-user.target
await this.checkConfigExists(); await this.checkConfigExists();
execSync('systemctl start nupst.service'); execSync('systemctl start nupst.service');
console.log('┌─ Service Status ─────────────────────────┐'); const boxWidth = 45;
console.log('│ NUPST service started successfully'); logger.logBoxTitle('Service Status', boxWidth);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine('NUPST service started successfully');
logger.logBoxEnd();
} catch (error) { } catch (error) {
if (error.message === 'Configuration not found') { if (error.message === 'Configuration not found') {
// Exit with error code since configuration is required // Exit with error code since configuration is required
process.exit(1); process.exit(1);
} }
console.error('Failed to start service:', error); logger.error(`Failed to start service: ${error}`);
throw error; throw error;
} }
} }
@ -117,9 +121,9 @@ WantedBy=multi-user.target
public async stop(): Promise<void> { public async stop(): Promise<void> {
try { try {
execSync('systemctl stop nupst.service'); execSync('systemctl stop nupst.service');
console.log('NUPST service stopped'); logger.success('NUPST service stopped');
} catch (error) { } catch (error) {
console.error('Failed to stop service:', error); logger.error(`Failed to stop service: ${error}`);
throw error; throw error;
} }
} }
@ -132,9 +136,10 @@ WantedBy=multi-user.target
try { try {
// Enable debug mode if requested // Enable debug mode if requested
if (debugMode) { if (debugMode) {
console.log('┌─ Debug Mode ─────────────────────────────┐'); const boxWidth = 45;
console.log('│ SNMP debugging enabled - detailed logs will be shown'); logger.logBoxTitle('Debug Mode', boxWidth);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
logger.logBoxEnd();
this.daemon.getNupstSnmp().enableDebug(); this.daemon.getNupstSnmp().enableDebug();
} }
@ -152,7 +157,7 @@ WantedBy=multi-user.target
await this.displayServiceStatus(); await this.displayServiceStatus();
await this.displayUpsStatus(); await this.displayUpsStatus();
} catch (error) { } catch (error) {
console.error(`Failed to get status: ${error.message}`); logger.error(`Failed to get status: ${error.message}`);
} }
} }
@ -163,13 +168,18 @@ WantedBy=multi-user.target
private async displayServiceStatus(): Promise<void> { private async displayServiceStatus(): Promise<void> {
try { try {
const serviceStatus = execSync('systemctl status nupst.service').toString(); const serviceStatus = execSync('systemctl status nupst.service').toString();
console.log('┌─ Service Status ─────────────────────────┐'); const boxWidth = 45;
console.log(serviceStatus.split('\n').map(line => `${line}`).join('\n')); logger.logBoxTitle('Service Status', boxWidth);
console.log('└──────────────────────────────────────────┘'); // Process each line of the status output
serviceStatus.split('\n').forEach(line => {
logger.logBoxLine(line);
});
logger.logBoxEnd();
} catch (error) { } catch (error) {
console.error('┌─ Service Status ─────────────────────────┐'); const boxWidth = 45;
console.error('Service is not running'); logger.logBoxTitle('Service Status', boxWidth);
console.error('└──────────────────────────────────────────┘'); logger.logBoxLine('Service is not running');
logger.logBoxEnd();
} }
} }
@ -190,22 +200,24 @@ WantedBy=multi-user.target
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check
}; };
console.log('┌─ Connecting to UPS... ────────────────────┐'); const boxWidth = 45;
console.log(`│ Host: ${config.snmp.host}:${config.snmp.port}`); logger.logBoxTitle('Connecting to UPS...', boxWidth);
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`); logger.logBoxLine(`Host: ${config.snmp.host}:${config.snmp.port}`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxLine(`UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
logger.logBoxEnd();
const status = await snmp.getUpsStatus(snmpConfig); const status = await snmp.getUpsStatus(snmpConfig);
console.log('┌─ UPS Status ───────────────────────────────┐'); logger.logBoxTitle('UPS Status', boxWidth);
console.log(`Power Status: ${status.powerStatus}`); logger.logBoxLine(`Power Status: ${status.powerStatus}`);
console.log(`Battery Capacity: ${status.batteryCapacity}%`); logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
console.log(`Runtime Remaining: ${status.batteryRuntime} minutes`); logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
console.log('└──────────────────────────────────────────┘'); logger.logBoxEnd();
} catch (error) { } catch (error) {
console.error('┌─ UPS Status ───────────────────────────────┐'); const boxWidth = 45;
console.error(`│ Failed to retrieve UPS status: ${error.message}`); logger.logBoxTitle('UPS Status', boxWidth);
console.error('└──────────────────────────────────────────┘'); logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
logger.logBoxEnd();
} }
} }
@ -221,10 +233,10 @@ WantedBy=multi-user.target
// Reload systemd daemon // Reload systemd daemon
execSync('systemctl daemon-reload'); execSync('systemctl daemon-reload');
console.log('Systemd daemon reloaded'); logger.log('Systemd daemon reloaded');
console.log('NUPST service has been successfully uninstalled'); logger.success('NUPST service has been successfully uninstalled');
} catch (error) { } catch (error) {
console.error('Failed to disable and uninstall service:', error); logger.error(`Failed to disable and uninstall service: ${error}`);
throw error; throw error;
} }
} }
@ -235,11 +247,11 @@ WantedBy=multi-user.target
*/ */
private async stopService(): Promise<void> { private async stopService(): Promise<void> {
try { try {
console.log('Stopping NUPST service...'); logger.log('Stopping NUPST service...');
execSync('systemctl stop nupst.service'); execSync('systemctl stop nupst.service');
} catch (error) { } catch (error) {
// Service might not be running, that's okay // Service might not be running, that's okay
console.log('Service was not running or could not be stopped'); logger.log('Service was not running or could not be stopped');
} }
} }
@ -249,10 +261,10 @@ WantedBy=multi-user.target
*/ */
private async disableService(): Promise<void> { private async disableService(): Promise<void> {
try { try {
console.log('Disabling NUPST service...'); logger.log('Disabling NUPST service...');
execSync('systemctl disable nupst.service'); execSync('systemctl disable nupst.service');
} catch (error) { } catch (error) {
console.log('Service was not enabled or could not be disabled'); logger.log('Service was not enabled or could not be disabled');
} }
} }
@ -262,11 +274,11 @@ WantedBy=multi-user.target
*/ */
private async removeServiceFile(): Promise<void> { private async removeServiceFile(): Promise<void> {
if (await fs.stat(this.serviceFilePath).catch(() => null)) { if (await fs.stat(this.serviceFilePath).catch(() => null)) {
console.log(`Removing service file ${this.serviceFilePath}...`); logger.log(`Removing service file ${this.serviceFilePath}...`);
await fs.unlink(this.serviceFilePath); await fs.unlink(this.serviceFilePath);
console.log('Service file removed'); logger.log('Service file removed');
} else { } else {
console.log('Service file did not exist'); logger.log('Service file did not exist');
} }
} }
} }