Compare commits

...

7 Commits

Author SHA1 Message Date
8516056f84 chore(release): bump version to 4.1.5
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 44s
CI / Build All Platforms (push) Successful in 48s
2025-10-20 01:31:43 +00:00
07ec9d7595 feat(cli): modernize all CLI output to use logger tables
- Modernize ups list command with logger.logTable()
- Modernize group list command with logger.logTable()
- Completely rewrite config show with tables and proper box styling
- Add professional column definitions with themed colors
- Replace all manual table formatting (padEnd, pipe separators)
- Improve visual hierarchy with appropriate box styles (info, warning, success)
- Ensure consistent theming across all CLI commands
2025-10-20 01:30:57 +00:00
d14ba1dd65 feat(status): display version and update status in nupst status command
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 46s
CI / Build All Platforms (push) Successful in 49s
- Add version display at the top of status output
- Check for available updates and notify user
- Show "Up to date" or "Update available" with version
- Display before service and UPS status information
- Improves user awareness of software version and updates

Bumps version to 4.1.4
2025-10-20 01:01:06 +00:00
7d595fa175 chore(release): bump version to 4.1.3
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 45s
CI / Build All Platforms (push) Successful in 51s
2025-10-20 00:40:56 +00:00
df417432b0 chore(branding): update description to 'Network UPS Shutdown Tool' 2025-10-20 00:40:52 +00:00
e5f1ebf343 chore(release): bump version to 4.1.2
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 46s
CI / Build All Platforms (push) Successful in 50s
2025-10-20 00:34:03 +00:00
3ff0dd7ac8 fix(cli): resolve process hang and improve output consistency
- Add process.stdin.destroy() after rl.close() in all interactive commands
  to properly release stdin and allow process to exit cleanly
- Replace raw console.log with logger methods throughout CLI handlers
- Convert manual box drawing to logger.logBox() in daemon.ts
- Standardize menu formatting with logger.info() and logger.dim()
- Improve migration output to only show when migrations actually run

Fixes issue where process would not exit after "Setup complete!" message
due to stdin keeping the event loop alive.
2025-10-20 00:32:06 +00:00
10 changed files with 368 additions and 290 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/nupst",
"version": "4.1.1",
"version": "4.1.5",
"exports": "./mod.ts",
"tasks": {
"dev": "deno run --allow-all mod.ts",

View File

@@ -6,5 +6,5 @@ import denoConfig from '../deno.json' with { type: 'json' };
export const commitinfo = {
name: denoConfig.name,
version: denoConfig.version,
description: 'Deno-powered UPS monitoring tool for SNMP-enabled UPS devices',
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)',
};

252
ts/cli.ts
View File

@@ -1,6 +1,6 @@
import { execSync } from 'node:child_process';
import { Nupst } from './nupst.ts';
import { logger } from './logger.ts';
import { logger, type ITableColumn } from './logger.ts';
import { theme, symbols } from './colors.ts';
/**
@@ -303,154 +303,164 @@ export class NupstCli {
try {
await this.nupst.getDaemon().loadConfig();
} catch (_error) {
const errorBoxWidth = 45;
logger.logBoxTitle('Configuration Error', errorBoxWidth);
logger.logBoxLine('No configuration found.');
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
logger.logBoxEnd();
logger.logBox('Configuration Error', [
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
], 50, 'error');
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
const boxWidth = 50;
logger.logBoxTitle('NUPST Configuration', boxWidth);
// Check if multi-UPS config
if (config.upsDevices && Array.isArray(config.upsDevices)) {
// Multi-UPS configuration
logger.logBoxLine(`UPS Devices: ${config.upsDevices.length}`);
logger.logBoxLine(`Groups: ${config.groups ? config.groups.length : 0}`);
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
logger.logBoxLine('');
logger.logBoxLine('Configuration File Location:');
logger.logBoxLine(' /etc/nupst/config.json');
logger.logBoxEnd();
// === Multi-UPS Configuration ===
// Show UPS devices
// Overview Box
logger.log('');
logger.logBox('NUPST Configuration', [
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
], 60, 'info');
// UPS Devices Table
if (config.upsDevices.length > 0) {
logger.logBoxTitle('UPS Devices', boxWidth);
for (const ups of config.upsDevices) {
logger.logBoxLine(`${ups.name} (${ups.id}):`);
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`);
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`);
logger.logBoxLine(
` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
);
logger.logBoxLine(
` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`,
);
logger.logBoxLine('');
}
logger.logBoxEnd();
const upsRows = config.upsDevices.map((ups) => ({
name: ups.name,
id: theme.dim(ups.id),
host: `${ups.snmp.host}:${ups.snmp.port}`,
model: ups.snmp.upsModel || 'cyberpower',
thresholds: `${ups.thresholds.battery}% / ${ups.thresholds.runtime}min`,
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
}));
const upsColumns: ITableColumn[] = [
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left' },
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
{ header: 'Model', key: 'model', align: 'left' },
{ header: 'Battery/Runtime', key: 'thresholds', align: 'left' },
{ header: 'Groups', key: 'groups', align: 'left' },
];
logger.log('');
logger.info(`UPS Devices (${config.upsDevices.length}):`);
logger.log('');
logger.logTable(upsColumns, upsRows);
}
// Show groups
// Groups Table
if (config.groups && config.groups.length > 0) {
logger.logBoxTitle('UPS Groups', boxWidth);
for (const group of config.groups) {
logger.logBoxLine(`${group.name} (${group.id}):`);
logger.logBoxLine(` Mode: ${group.mode}`);
if (group.description) {
logger.logBoxLine(` Description: ${group.description}`);
}
// List UPS devices in this group
const groupRows = config.groups.map((group) => {
const upsInGroup = config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(group.id)
);
logger.logBoxLine(
` UPS Devices: ${
upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : 'None'
}`,
);
logger.logBoxLine('');
}
logger.logBoxEnd();
return {
name: group.name,
id: theme.dim(group.id),
mode: group.mode,
upsCount: String(upsInGroup.length),
ups: upsInGroup.length > 0
? upsInGroup.map((ups) => ups.name).join(', ')
: theme.dim('None'),
description: group.description || theme.dim('—'),
};
});
const groupColumns: ITableColumn[] = [
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left' },
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
{ header: 'UPS', key: 'upsCount', align: 'right' },
{ header: 'UPS Devices', key: 'ups', align: 'left' },
{ header: 'Description', key: 'description', align: 'left' },
];
logger.log('');
logger.info(`UPS Groups (${config.groups.length}):`);
logger.log('');
logger.logTable(groupColumns, groupRows);
}
} else {
// Legacy single UPS configuration
if (!config.snmp) {
logger.logBoxLine('Error: Legacy configuration missing SNMP settings');
} else {
// SNMP Settings
logger.logBoxLine('SNMP Settings:');
logger.logBoxLine(` Host: ${config.snmp.host}`);
logger.logBoxLine(` Port: ${config.snmp.port}`);
logger.logBoxLine(` Version: ${config.snmp.version}`);
logger.logBoxLine(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
// === Legacy Single UPS Configuration ===
if (config.snmp.version === 1 || config.snmp.version === 2) {
logger.logBoxLine(` Community: ${config.snmp.community}`);
} else if (config.snmp.version === 3) {
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
logger.logBoxLine(` Username: ${config.snmp.username}`);
// Show auth and privacy details based on security level
if (
config.snmp.securityLevel === 'authNoPriv' ||
config.snmp.securityLevel === 'authPriv'
) {
logger.logBoxLine(` Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
}
if (config.snmp.securityLevel === 'authPriv') {
logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
}
// Show timeout value
logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
}
// Show OIDs if custom model is selected
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
logger.logBoxLine('Custom OIDs:');
logger.logBoxLine(
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
);
logger.logBoxLine(
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
);
logger.logBoxLine(
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
);
}
if (!config.snmp || !config.thresholds) {
logger.logBox('Configuration Error', [
'Error: Legacy configuration missing SNMP or threshold settings',
], 60, 'error');
return;
}
// Thresholds
if (!config.thresholds) {
logger.logBoxLine('Error: Legacy configuration missing threshold settings');
} else {
logger.logBoxLine('Thresholds:');
logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
}
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
// Configuration file location
logger.logBoxLine('');
logger.logBoxLine('Configuration File Location:');
logger.logBoxLine(' /etc/nupst/config.json');
logger.logBoxLine('');
logger.logBoxLine('Note: Using legacy single-UPS configuration format.');
logger.logBoxLine('Consider using "nupst add" to migrate to multi-UPS format.');
logger.logBoxEnd();
logger.log('');
logger.logBox('NUPST Configuration (Legacy)', [
theme.warning('Legacy single-UPS configuration format'),
'',
theme.dim('SNMP Settings:'),
` Host: ${theme.info(config.snmp.host)}`,
` Port: ${theme.info(String(config.snmp.port))}`,
` Version: ${config.snmp.version}`,
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
...(config.snmp.version === 1 || config.snmp.version === 2
? [` Community: ${config.snmp.community}`]
: []
),
...(config.snmp.version === 3
? [
` Security Level: ${config.snmp.securityLevel}`,
` Username: ${config.snmp.username}`,
...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv'
? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`]
: []
),
...(config.snmp.securityLevel === 'authPriv'
? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`]
: []
),
` Timeout: ${config.snmp.timeout / 1000} seconds`,
]
: []
),
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
? [
theme.dim('Custom OIDs:'),
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
]
: []
),
'',
theme.dim('Thresholds:'),
` Battery: ${theme.highlight(String(config.thresholds.battery))}%`,
` Runtime: ${theme.highlight(String(config.thresholds.runtime))} minutes`,
` Check Interval: ${config.checkInterval / 1000} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
'',
theme.warning('Note: Using legacy single-UPS configuration format.'),
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
], 70, 'warning');
}
// Show service status
// Service Status
try {
const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isEnabled =
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
const statusBoxWidth = 45;
logger.logBoxTitle('Service Status', statusBoxWidth);
logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`);
logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
logger.logBoxEnd();
logger.log('');
logger.logBox('Service Status', [
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
], 50, isActive ? 'success' : 'default');
logger.log('');
} catch (_error) {
// Ignore errors checking service status
}
@@ -469,7 +479,7 @@ export class NupstCli {
private showVersion(): void {
const version = this.nupst.getVersion();
logger.log(`NUPST version ${version}`);
logger.log('Deno-powered UPS monitoring tool');
logger.log('Network UPS Shutdown Tool (https://nupst.serve.zone)');
}
/**

View File

@@ -1,6 +1,7 @@
import process from 'node:process';
import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts';
import { type IGroupConfig } from '../daemon.ts';
@@ -28,11 +29,10 @@ export class GroupHandler {
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
const errorBoxWidth = 45;
logger.logBoxTitle('Configuration Error', errorBoxWidth);
logger.logBoxLine('No configuration found.');
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
logger.logBoxEnd();
logger.logBox('Configuration Error', [
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
], 50, 'error');
return;
}
@@ -41,43 +41,53 @@ export class GroupHandler {
// Check if multi-UPS config
if (!config.groups || !Array.isArray(config.groups)) {
// Legacy or missing groups configuration
const boxWidth = 45;
logger.logBoxTitle('UPS Groups', boxWidth);
logger.logBoxLine('No groups configured.');
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
logger.logBoxEnd();
logger.logBox('UPS Groups', [
'No groups configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
], 50, 'info');
return;
}
// Display group list
const boxWidth = 60;
logger.logBoxTitle('UPS Groups', boxWidth);
// Display group list with modern table
if (config.groups.length === 0) {
logger.logBoxLine('No UPS groups configured.');
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
} else {
logger.logBoxLine(`Found ${config.groups.length} group(s)`);
logger.logBoxLine('');
logger.logBoxLine('ID | Name | Mode | UPS Devices');
logger.logBoxLine('-----------+----------------------+--------------+----------------');
for (const group of config.groups) {
const id = group.id.padEnd(10, ' ').substring(0, 10);
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
// Count UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
const upsCount = upsInGroup.length;
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
}
logger.logBox('UPS Groups', [
'No UPS groups configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
], 60, 'info');
return;
}
logger.logBoxEnd();
// Prepare table data
const rows = config.groups.map((group) => {
// Count UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
const upsCount = upsInGroup.length;
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
return {
id: group.id,
name: group.name || '',
mode: group.mode || 'unknown',
count: String(upsCount),
devices: upsCount > 0 ? upsNames : theme.dim('None'),
};
});
const columns: ITableColumn[] = [
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
{ header: 'Name', key: 'name', align: 'left' },
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
{ header: 'UPS Count', key: 'count', align: 'right' },
{ header: 'UPS Devices', key: 'devices', align: 'left' },
];
logger.log('');
logger.info(`UPS Groups (${config.groups.length}):`);
logger.log('');
logger.logTable(columns, rows);
logger.log('');
} catch (error) {
logger.error(
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
@@ -192,6 +202,7 @@ export class GroupHandler {
logger.log('\nGroup setup complete!');
} finally {
rl.close();
process.stdin.destroy();
}
} catch (error) {
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
@@ -309,6 +320,7 @@ export class GroupHandler {
logger.log('\nGroup edit complete!');
} finally {
rl.close();
process.stdin.destroy();
}
} catch (error) {
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
@@ -366,6 +378,7 @@ export class GroupHandler {
});
rl.close();
process.stdin.destroy();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');

View File

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

View File

@@ -1,7 +1,8 @@
import process from 'node:process';
import { execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts';
import type { TUpsModel } from '../snmp/types.ts';
import type { INupstConfig } from '../daemon.ts';
@@ -47,6 +48,7 @@ export class UpsHandler {
await this.runAddProcess(prompt);
} finally {
rl.close();
process.stdin.destroy();
}
} catch (error) {
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
@@ -178,6 +180,7 @@ export class UpsHandler {
await this.runEditProcess(upsId, prompt);
} finally {
rl.close();
process.stdin.destroy();
}
} catch (error) {
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
@@ -344,6 +347,7 @@ export class UpsHandler {
});
rl.close();
process.stdin.destroy();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');
@@ -376,11 +380,10 @@ export class UpsHandler {
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
const errorBoxWidth = 45;
logger.logBoxTitle('Configuration Error', errorBoxWidth);
logger.logBoxLine('No configuration found.');
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
logger.logBoxEnd();
logger.logBox('Configuration Error', [
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
], 50, 'error');
return;
}
@@ -390,58 +393,57 @@ export class UpsHandler {
// Check if multi-UPS config
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
// Legacy single UPS configuration
const boxWidth = 45;
logger.logBoxTitle('UPS Devices', boxWidth);
logger.logBoxLine('Legacy single-UPS configuration detected.');
if (!config.snmp || !config.thresholds) {
logger.logBoxLine('');
logger.logBoxLine('Error: Configuration missing SNMP or threshold settings');
logger.logBoxEnd();
return;
}
logger.logBoxLine('');
logger.logBoxLine('Default UPS:');
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
logger.logBoxLine(
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
);
logger.logBoxLine('');
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
logger.logBoxLine('to the multi-UPS configuration format.');
logger.logBoxEnd();
logger.logBox('UPS Devices', [
'Legacy single-UPS configuration detected.',
'',
...((!config.snmp || !config.thresholds)
? ['Error: Configuration missing SNMP or threshold settings']
: [
'Default UPS:',
` Host: ${config.snmp.host}:${config.snmp.port}`,
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
'',
'Use "nupst ups add" to add more UPS devices and migrate',
'to the multi-UPS configuration format.',
]
),
], 60, 'warning');
return;
}
// Display UPS list
const boxWidth = 60;
logger.logBoxTitle('UPS Devices', boxWidth);
// Display UPS list with modern table
if (config.upsDevices.length === 0) {
logger.logBoxLine('No UPS devices configured.');
logger.logBoxLine('Use "nupst add" to add a UPS device.');
} else {
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
logger.logBoxLine('');
logger.logBoxLine(
'ID | Name | Host | Mode | Groups',
);
logger.logBoxLine(
'-----------+----------------------+-----------------+--------------+----------------',
);
for (const ups of config.upsDevices) {
const id = ups.id.padEnd(10, ' ').substring(0, 10);
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15);
const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12);
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
}
logger.logBox('UPS Devices', [
'No UPS devices configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
], 60, 'info');
return;
}
logger.logBoxEnd();
// Prepare table data
const rows = config.upsDevices.map((ups) => ({
id: ups.id,
name: ups.name || '',
host: `${ups.snmp.host}:${ups.snmp.port}`,
model: ups.snmp.upsModel || 'cyberpower',
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
}));
const columns: ITableColumn[] = [
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
{ header: 'Name', key: 'name', align: 'left' },
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
{ header: 'Model', key: 'model', align: 'left' },
{ header: 'Groups', key: 'groups', align: 'left' },
];
logger.log('');
logger.info(`UPS Devices (${config.upsDevices.length}):`);
logger.log('');
logger.logTable(columns, rows);
logger.log('');
} catch (error) {
logger.error(
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
@@ -667,10 +669,11 @@ export class UpsHandler {
// SNMP Version
const defaultVersion = snmpConfig.version || 1;
console.log('\nSNMP Version:');
console.log(' 1) SNMPv1');
console.log(' 2) SNMPv2c');
console.log(' 3) SNMPv3 (with security features)');
logger.log('');
logger.info('SNMP Version:');
logger.dim(' 1) SNMPv1');
logger.dim(' 2) SNMPv2c');
logger.dim(' 3) SNMPv3 (with security features)');
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
const version = parseInt(versionInput, 10);
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
@@ -697,13 +700,15 @@ export class UpsHandler {
snmpConfig: any,
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nSNMPv3 Security Settings:');
logger.log('');
logger.info('SNMPv3 Security Settings:');
// Security Level
console.log('\nSecurity Level:');
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
console.log(' 2) authNoPriv (Authentication, No Privacy)');
console.log(' 3) authPriv (Authentication and Privacy)');
logger.log('');
logger.info('Security Level:');
logger.dim(' 1) noAuthNoPriv (No Authentication, No Privacy)');
logger.dim(' 2) authNoPriv (Authentication, No Privacy)');
logger.dim(' 3) authPriv (Authentication and Privacy)');
const defaultSecLevel = snmpConfig.securityLevel
? snmpConfig.securityLevel === 'noAuthNoPriv'
? 1
@@ -752,8 +757,9 @@ export class UpsHandler {
// Allow customizing the timeout value
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
console.log(
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.',
logger.log('');
logger.info(
'SNMPv3 operations with authentication and privacy may require longer timeouts.',
);
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
const timeout = parseInt(timeoutInput, 10);
@@ -773,9 +779,10 @@ export class UpsHandler {
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Authentication protocol
console.log('\nAuthentication Protocol:');
console.log(' 1) MD5');
console.log(' 2) SHA');
logger.log('');
logger.info('Authentication Protocol:');
logger.dim(' 1) MD5');
logger.dim(' 2) SHA');
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
const authProtocolInput = await prompt(
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
@@ -799,9 +806,10 @@ export class UpsHandler {
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Privacy protocol
console.log('\nPrivacy Protocol:');
console.log(' 1) DES');
console.log(' 2) AES');
logger.log('');
logger.info('Privacy Protocol:');
logger.dim(' 1) DES');
logger.dim(' 2) AES');
const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
@@ -822,7 +830,8 @@ export class UpsHandler {
thresholds: any,
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nShutdown Thresholds:');
logger.log('');
logger.info('Shutdown Thresholds:');
// Battery threshold
const defaultBatteryThreshold = thresholds.battery || 60;
@@ -854,13 +863,14 @@ export class UpsHandler {
snmpConfig: any,
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nUPS Model Selection:');
console.log(' 1) CyberPower');
console.log(' 2) APC');
console.log(' 3) Eaton');
console.log(' 4) TrippLite');
console.log(' 5) Liebert/Vertiv');
console.log(' 6) Custom (Advanced)');
logger.log('');
logger.info('UPS Model Selection:');
logger.dim(' 1) CyberPower');
logger.dim(' 2) APC');
logger.dim(' 3) Eaton');
logger.dim(' 4) TrippLite');
logger.dim(' 5) Liebert/Vertiv');
logger.dim(' 6) Custom (Advanced)');
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
? 1
@@ -891,8 +901,9 @@ export class UpsHandler {
snmpConfig.upsModel = 'liebert';
} else if (modelValue === 6) {
snmpConfig.upsModel = 'custom';
console.log('\nEnter custom OIDs for your UPS:');
console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)');
logger.log('');
logger.info('Enter custom OIDs for your UPS:');
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
// Custom OIDs
const powerStatusOID = await prompt('Power Status OID: ');

View File

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

View File

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

View File

@@ -50,11 +50,11 @@ WantedBy=multi-user.target
try {
await fs.access(configPath);
} catch (error) {
console.log('');
console.log(`${symbols.error} ${theme.error('No configuration found')}`);
console.log(` ${theme.dim('Config file:')} ${configPath}`);
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
console.log('');
logger.log('');
logger.error('No configuration found');
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
logger.log('');
throw new Error('Configuration not found');
}
}
@@ -134,6 +134,45 @@ WantedBy=multi-user.target
* Get status of the systemd service and UPS
* @param debugMode Whether to enable debug mode for SNMP
*/
/**
* Display version information and update status
* @private
*/
private async displayVersionInfo(): Promise<void> {
try {
const nupst = this.daemon.getNupstSnmp().getNupst();
const version = nupst.getVersion();
// Check for updates
const updateAvailable = await nupst.checkForUpdates();
// Display version info
if (updateAvailable) {
const updateStatus = nupst.getUpdateStatus();
logger.log('');
logger.log(
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`,
);
logger.log(` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`);
} else {
logger.log('');
logger.log(
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`,
);
}
} catch (error) {
// If version check fails, show at least the current version
try {
const nupst = this.daemon.getNupstSnmp().getNupst();
const version = nupst.getVersion();
logger.log('');
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
} catch (_innerError) {
// Silently fail if we can't even get the version
}
}
}
public async getStatus(debugMode: boolean = false): Promise<void> {
try {
// Enable debug mode if requested
@@ -144,7 +183,10 @@ WantedBy=multi-user.target
this.daemon.getNupstSnmp().enableDebug();
}
// Check if config exists first
// Display version and update status first
await this.displayVersionInfo();
// Check if config exists
try {
await this.checkConfigExists();
} catch (error) {
@@ -192,11 +234,11 @@ WantedBy=multi-user.target
}
// Display beautiful status
console.log('');
logger.log('');
if (isActive) {
console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
} else {
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
}
if (pid || memory || cpu) {
@@ -204,14 +246,14 @@ WantedBy=multi-user.target
if (pid) details.push(`PID: ${theme.dim(pid)}`);
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
console.log(` ${details.join(' ')}`);
logger.log(` ${details.join(' ')}`);
}
console.log('');
logger.log('');
} catch (error) {
console.log('');
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
console.log('');
logger.log('');
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
logger.log('');
}
}
@@ -228,7 +270,7 @@ WantedBy=multi-user.target
// Check if we have the new multi-UPS config format
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`));
logger.info(`UPS Devices (${config.upsDevices.length}):`);
// Show status for each UPS
for (const ups of config.upsDevices) {
@@ -236,7 +278,7 @@ WantedBy=multi-user.target
}
} else if (config.snmp) {
// Legacy single UPS configuration
console.log(theme.info('UPS Devices (1):'));
logger.info('UPS Devices (1):');
const legacyUps = {
id: 'default',
name: 'Default UPS',
@@ -247,16 +289,16 @@ WantedBy=multi-user.target
await this.displaySingleUpsStatus(legacyUps, snmp);
} else {
console.log('');
console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`);
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
console.log('');
logger.log('');
logger.warn('No UPS devices configured');
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
logger.log('');
}
} catch (error) {
console.log('');
console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`);
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log('');
logger.log('');
logger.error('Failed to retrieve UPS status');
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
logger.log('');
}
}
@@ -284,15 +326,15 @@ WantedBy=multi-user.target
}
// Display UPS name and power status
console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
// Display battery with color coding
const batteryColor = getBatteryColor(status.batteryCapacity);
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
// Display host info
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
// Display groups if any
if (ups.groups && ups.groups.length > 0) {
@@ -301,17 +343,17 @@ WantedBy=multi-user.target
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
return group ? group.name : groupId;
});
console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
}
console.log('');
logger.log('');
} catch (error) {
// Display error for this UPS
console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
console.log('');
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
logger.log('');
}
}