Compare commits

..

15 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
bb87316dd3 fix(snmp): correct power status interpretation using OID set mappings
All checks were successful
CI / Type Check & Lint (push) Successful in 7s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 44s
CI / Build All Platforms (push) Successful in 49s
Move power status value interpretation from hardcoded logic to OID set configuration.
Each UPS model now defines its own value mappings (e.g., CyberPower: 2=online, 3=onBattery).

Fixes incorrect status display where UPS showed "On Battery" when actually online.

Changes:
- Add POWER_STATUS_VALUES to IOidSet interface
- Define value mappings for all UPS models (cyberpower, apc, eaton, tripplite, liebert)
- Refactor determinePowerStatus() to use OID set mappings instead of hardcoded values
- CyberPower now correctly interprets value 2 as online (was incorrectly onBattery)
2025-10-19 23:48:13 +00:00
d6e0a1a274 feat(cli): remove ALL ugly boxes from status output - now fully beautiful
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 44s
CI / Build All Platforms (push) Successful in 49s
Removed the last remaining ugly ASCII boxes:
- Version info box (┌─┐│└┘) that appeared at top
- Async version check box that ended randomly in middle
- Configuration error box

Now status output is 100% clean and beautiful with just colored text:

● Service: active (running)
  PID: 9120  Memory: 45.7M  CPU: 190ms

UPS Devices (2):
  ⚠ Test UPS (SNMP v1) - On Battery
    Battery: 100% ✓  Runtime: 48 min
    Host: 192.168.187.140:161

  ◯ Test UPS (SNMP v3) - Unknown
    Battery: 0% ⚠  Runtime: 0 min
    Host: 192.168.187.140:161

No boxes, just beautiful colored output with symbols!
Bumped to v4.1.0 to mark completion of beautiful CLI feature.
2025-10-19 23:01:25 +00:00
95fa4f8b0b fix(update): normalize version strings for correct comparison
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
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
The version check was comparing "4.0.8" (no prefix) with "v4.0.8"
(with prefix), causing it to always think an update was available.

Now both versions are normalized to have the "v" prefix before
comparison, so "Already up to date!" works correctly.
2025-10-19 22:56:35 +00:00
c2f2f1e2ee feat(update): add version check to skip update when already latest
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
Release / build-and-release (push) Successful in 48s
CI / Build All Platforms (push) Successful in 53s
Now `nupst update` checks current version against latest release before
downloading anything.

Behavior:
- Fetches latest version from Gitea API
- Compares with current version
- Shows "Already up to date!" if versions match
- Only downloads/installs if newer version available

Example output when up to date:
  Checking for updates...
  Current version: v4.0.8
  Latest version:  v4.0.8

  ✓ Already up to date!
2025-10-19 22:50:03 +00:00
936f86c346 fix(update): rewrite nupst update for v4 (download install script instead of git pull)
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
Release / build-and-release (push) Successful in 45s
CI / Build All Platforms (push) Successful in 50s
The update command was still using v3 logic (git pull, setup.sh) which
doesn't work for v4 binary distribution.

Now it simply:
1. Downloads install.sh from main branch
2. Runs it (handles download, stop, replace, restart automatically)

This is much simpler and matches how v4 is distributed. No more git,
no more setup.sh, just download the latest binary.
2025-10-19 21:54:05 +00:00
7ff1a7da36 feat(cli): replace ugly ASCII boxes with beautiful colored status output
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 48s
CI / Build All Platforms (push) Successful in 52s
Replaced all ASCII box characters (┌─┐│└┘) with modern, clean colored
output using the existing color theme and symbols.

Changes in ts/systemd.ts:
- displayServiceStatus(): Parse systemctl output and show key info
  with colored symbols (● for running, ○ for stopped)
- displaySingleUpsStatus(): Clean output with battery/runtime colors
  - Green >60%, yellow 30-60%, red <30% for battery
  - Power status with colored symbols and text
  - Clean indented layout without boxes

Example new output:
  ● Service: active (running)
    PID: 7606  Memory: 41.5M  CPU: 279ms

  UPS Devices (2):
    ● Test UPS (SNMP v1) - Online
      Battery: 100% ✓  Runtime: 48 min
      Host: 192.168.187.140:161

Much cleaner and more readable than ASCII boxes!
2025-10-19 21:50:31 +00:00
a87710144c fix(migration): detect flat structure in upsDevices for proper v3→v4 migration
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
CI / Build All Platforms (push) Successful in 52s
Release / build-and-release (push) Successful in 50s
The previous migration only checked for upsList field, but saveConfig()
strips upsList when saving, creating a race condition. If the daemon
restarted with a partially-migrated config (upsDevices with flat structure),
the migration wouldn't run because it only looked for upsList.

Now shouldRun() also detects:
- upsDevices with flat structure (host at top level, no snmp object)

And migrate() handles both:
- config.upsList (pure v3)
- config.upsDevices with flat structure (partially migrated)

This fixes the "Cannot read properties of undefined (reading 'host')"
error that occurred when configs had upsDevices but flat structure.
2025-10-19 21:41:50 +00:00
23fd5cc5cd refactor(install): remove interactive mode, simplify installation
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 5s
CI / Build All Platforms (push) Successful in 50s
Interactive mode was causing issues with automated testing and the
nupst update command (failed with /dev/tty errors). Since users
running curl|bash have already decided to install, prompts add no
value and only create friction.

Changes:
- Removed -y/--yes flag (no longer needed)
- Removed all interactive confirmation prompts
- Removed terminal detection logic (/dev/tty handling)
- Updated README to remove all -y flag references
- Simplified installation examples

Benefits:
- Works in all environments (piped, non-interactive, containers)
- Fixes nupst update command
- Cleaner user experience
- Matches standard install script patterns (homebrew, rustup, etc.)
2025-10-19 21:37:41 +00:00
16 changed files with 550 additions and 538 deletions

View File

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

View File

@@ -10,15 +10,7 @@
# With version specification: # With version specification:
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0 # curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0
# #
# Non-interactive mode (auto-confirm):
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y
#
# Downloaded script:
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh
# sudo bash nupst-install.sh
#
# Options: # Options:
# -y, --yes Automatically answer yes to all prompts
# -h, --help Show this help message # -h, --help Show this help message
# --version VERSION Install specific version (e.g., v4.0.0) # --version VERSION Install specific version (e.g., v4.0.0)
# --install-dir DIR Installation directory (default: /opt/nupst) # --install-dir DIR Installation directory (default: /opt/nupst)
@@ -26,7 +18,6 @@
set -e set -e
# Default values # Default values
AUTO_YES=0
SHOW_HELP=0 SHOW_HELP=0
SPECIFIED_VERSION="" SPECIFIED_VERSION=""
INSTALL_DIR="/opt/nupst" INSTALL_DIR="/opt/nupst"
@@ -36,10 +27,6 @@ GITEA_REPO="serve.zone/nupst"
# Parse command line arguments # Parse command line arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
-y|--yes)
AUTO_YES=1
shift
;;
-h|--help) -h|--help)
SHOW_HELP=1 SHOW_HELP=1
shift shift
@@ -67,7 +54,6 @@ if [ $SHOW_HELP -eq 1 ]; then
echo "Usage: $0 [options]" echo "Usage: $0 [options]"
echo "" echo ""
echo "Options:" echo "Options:"
echo " -y, --yes Automatically answer yes to all prompts"
echo " -h, --help Show this help message" echo " -h, --help Show this help message"
echo " --version VERSION Install specific version (e.g., v4.0.0)" echo " --version VERSION Install specific version (e.g., v4.0.0)"
echo " --install-dir DIR Installation directory (default: /opt/nupst)" echo " --install-dir DIR Installation directory (default: /opt/nupst)"
@@ -78,9 +64,6 @@ if [ $SHOW_HELP -eq 1 ]; then
echo "" echo ""
echo " # Install specific version" echo " # Install specific version"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0" echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0"
echo ""
echo " # Non-interactive installation"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
exit 0 exit 0
fi fi
@@ -90,36 +73,6 @@ if [ "$EUID" -ne 0 ]; then
exit 1 exit 1
fi fi
# Detect if script is being piped or run directly
INTERACTIVE=1
if [ ! -t 0 ] || [ ! -t 1 ]; then
# Either stdin or stdout is not a terminal
if [ $AUTO_YES -ne 1 ]; then
echo "Script detected it's running in a non-interactive environment without -y flag."
echo "Attempting to find a controlling terminal for interactive prompts..."
# Try to use a controlling terminal for user input
if exec < /dev/tty 2>/dev/null && [ -t 0 ]; then
INTERACTIVE=1
else
INTERACTIVE=0
fi
if [ $INTERACTIVE -eq 0 ]; then
echo "ERROR: No controlling terminal available for interactive prompts."
echo ""
echo "For interactive installation (RECOMMENDED):"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh"
echo " sudo bash nupst-install.sh"
echo ""
echo "For non-interactive installation with auto-confirm:"
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
exit 1
else
echo "Interactive terminal found, continuing with prompts..."
fi
fi
fi
# Helper function to detect OS and architecture # Helper function to detect OS and architecture
detect_platform() { detect_platform() {
local os=$(uname -s) local os=$(uname -s)
@@ -229,22 +182,6 @@ if [ -d "$INSTALL_DIR" ]; then
echo "" echo ""
fi fi
if [ $AUTO_YES -eq 0 ] && [ $INTERACTIVE -eq 1 ]; then
if [ $OLD_NODE_INSTALL -eq 1 ]; then
echo "This will replace your Node.js installation with a pre-compiled binary."
echo "Your configuration in /etc/nupst/config.json will be preserved."
echo ""
fi
echo "Installation directory already exists: $INSTALL_DIR"
echo "Do you want to update/reinstall? (Y/n): "
read -r update_confirm
if [[ "$update_confirm" =~ ^[Nn]$ ]]; then
echo "Installation cancelled."
exit 0
fi
fi
echo "Updating existing installation at $INSTALL_DIR..." echo "Updating existing installation at $INSTALL_DIR..."
# Check if service exists (enabled or running) and stop it if active # Check if service exists (enabled or running) and stop it if active
@@ -273,17 +210,6 @@ if [ -d "$INSTALL_DIR" ]; then
echo "Old installation files removed." echo "Old installation files removed."
fi fi
else else
if [ $AUTO_YES -eq 0 ] && [ $INTERACTIVE -eq 1 ]; then
echo "NUPST will be installed to: $INSTALL_DIR"
echo "Continue? (Y/n): "
read -r install_confirm
if [[ "$install_confirm" =~ ^[Nn]$ ]]; then
echo "Installation cancelled."
exit 0
fi
fi
echo "Creating installation directory: $INSTALL_DIR" echo "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR" mkdir -p "$INSTALL_DIR"
fi fi
@@ -329,22 +255,8 @@ else
fi fi
# Create symlink for global access # Create symlink for global access
if [ $AUTO_YES -eq 0 ] && [ $INTERACTIVE -eq 1 ]; then
echo "Create symlink in $BIN_DIR for global access? (Y/n): "
read -r symlink_confirm
if [[ ! "$symlink_confirm" =~ ^[Nn]$ ]]; then
ln -sf "$BINARY_PATH" "$BIN_DIR/nupst" ln -sf "$BINARY_PATH" "$BIN_DIR/nupst"
echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH" echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH"
else
echo "Symlink creation skipped."
echo "To use NUPST, run: $BINARY_PATH"
echo "Or manually create symlink: sudo ln -sf $BINARY_PATH $BIN_DIR/nupst"
fi
else
ln -sf "$BINARY_PATH" "$BIN_DIR/nupst"
echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH"
fi
echo "" echo ""

View File

@@ -29,15 +29,8 @@ dependencies.
The easiest way to install NUPST is using the automated installer: The easiest way to install NUPST is using the automated installer:
```bash ```bash
# Download and run installer (most reliable) # One-line installation
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
sudo bash nupst-install.sh
rm nupst-install.sh
```
```bash
# One-line installation (non-interactive with auto-confirm)
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y
``` ```
The installer will: The installer will:
@@ -76,7 +69,6 @@ sudo mv nupst /usr/local/bin/nupst
The installer script (`install.sh`) supports the following options: The installer script (`install.sh`) supports the following options:
``` ```
-y, --yes Automatically answer yes to all prompts
-h, --help Show help message -h, --help Show help message
--version VERSION Install specific version (e.g., --version v4.0.0) --version VERSION Install specific version (e.g., --version v4.0.0)
--install-dir DIR Custom installation directory (default: /opt/nupst) --install-dir DIR Custom installation directory (default: /opt/nupst)
@@ -373,7 +365,7 @@ sudo nupst service disable
Re-run the installer to update to the latest version: Re-run the installer to update to the latest version:
```bash ```bash
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
``` ```
The installer will: The installer will:
@@ -461,7 +453,7 @@ The installer script automatically handles the entire migration while preserving
```bash ```bash
# Run the installer (handles stop/update/restart automatically) # Run the installer (handles stop/update/restart automatically)
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
# Verify # Verify
nupst service status nupst service status

View File

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

246
ts/cli.ts
View File

@@ -1,6 +1,6 @@
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { Nupst } from './nupst.ts'; import { Nupst } from './nupst.ts';
import { logger } from './logger.ts'; import { logger, type ITableColumn } from './logger.ts';
import { theme, symbols } from './colors.ts'; import { theme, symbols } from './colors.ts';
/** /**
@@ -303,154 +303,164 @@ export class NupstCli {
try { try {
await this.nupst.getDaemon().loadConfig(); await this.nupst.getDaemon().loadConfig();
} catch (_error) { } catch (_error) {
const errorBoxWidth = 45; logger.logBox('Configuration Error', [
logger.logBoxTitle('Configuration Error', errorBoxWidth); 'No configuration found.',
logger.logBoxLine('No configuration found.'); "Please run 'nupst ups add' first to create a configuration.",
logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); ], 50, 'error');
logger.logBoxEnd();
return; return;
} }
// Get current configuration // Get current configuration
const config = this.nupst.getDaemon().getConfig(); const config = this.nupst.getDaemon().getConfig();
const boxWidth = 50;
logger.logBoxTitle('NUPST Configuration', boxWidth);
// Check if multi-UPS config // Check if multi-UPS config
if (config.upsDevices && Array.isArray(config.upsDevices)) { if (config.upsDevices && Array.isArray(config.upsDevices)) {
// Multi-UPS configuration // === 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();
// 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) { if (config.upsDevices.length > 0) {
logger.logBoxTitle('UPS Devices', boxWidth); const upsRows = config.upsDevices.map((ups) => ({
for (const ups of config.upsDevices) { name: ups.name,
logger.logBoxLine(`${ups.name} (${ups.id}):`); id: theme.dim(ups.id),
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`); host: `${ups.snmp.host}:${ups.snmp.port}`,
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`); model: ups.snmp.upsModel || 'cyberpower',
logger.logBoxLine( thresholds: `${ups.thresholds.battery}% / ${ups.thresholds.runtime}min`,
` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`, groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
); }));
logger.logBoxLine(
` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`, const upsColumns: ITableColumn[] = [
); { header: 'Name', key: 'name', align: 'left', color: theme.highlight },
logger.logBoxLine(''); { header: 'ID', key: 'id', align: 'left' },
} { header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
logger.logBoxEnd(); { 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) { if (config.groups && config.groups.length > 0) {
logger.logBoxTitle('UPS Groups', boxWidth); const groupRows = config.groups.map((group) => {
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 upsInGroup = config.upsDevices.filter((ups) => const upsInGroup = config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(group.id) ups.groups && ups.groups.includes(group.id)
); );
logger.logBoxLine( return {
` UPS Devices: ${ name: group.name,
upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : 'None' id: theme.dim(group.id),
}`, mode: group.mode,
); upsCount: String(upsInGroup.length),
logger.logBoxLine(''); ups: upsInGroup.length > 0
} ? upsInGroup.map((ups) => ups.name).join(', ')
logger.logBoxEnd(); : 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 { } else {
// Legacy single UPS configuration // === 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'}`);
if (config.snmp.version === 1 || config.snmp.version === 2) { if (!config.snmp || !config.thresholds) {
logger.logBoxLine(` Community: ${config.snmp.community}`); logger.logBox('Configuration Error', [
} else if (config.snmp.version === 3) { 'Error: Legacy configuration missing SNMP or threshold settings',
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`); ], 60, 'error');
logger.logBoxLine(` Username: ${config.snmp.username}`); return;
// 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.log('');
logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`); logger.logBox('NUPST Configuration (Legacy)', [
} theme.warning('Legacy single-UPS configuration format'),
'',
// Show timeout value theme.dim('SNMP Settings:'),
logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`); ` Host: ${theme.info(config.snmp.host)}`,
} ` Port: ${theme.info(String(config.snmp.port))}`,
` Version: ${config.snmp.version}`,
// Show OIDs if custom model is selected ` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) { ...(config.snmp.version === 1 || config.snmp.version === 2
logger.logBoxLine('Custom OIDs:'); ? [` Community: ${config.snmp.community}`]
logger.logBoxLine( : []
),
...(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'}`, ` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
);
logger.logBoxLine(
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`, ` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
);
logger.logBoxLine(
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || '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');
} }
// Thresholds // Service Status
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();
}
// Show service status
try { try {
const isActive = const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isEnabled = const isEnabled =
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled'; execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
const statusBoxWidth = 45; logger.log('');
logger.logBoxTitle('Service Status', statusBoxWidth); logger.logBox('Service Status', [
logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`); `Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`); `Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
logger.logBoxEnd(); ], 50, isActive ? 'success' : 'default');
logger.log('');
} catch (_error) { } catch (_error) {
// Ignore errors checking service status // Ignore errors checking service status
} }
@@ -469,7 +479,7 @@ export class NupstCli {
private showVersion(): void { private showVersion(): void {
const version = this.nupst.getVersion(); const version = this.nupst.getVersion();
logger.log(`NUPST version ${version}`); 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 process from 'node:process';
import { Nupst } from '../nupst.ts'; 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 * as helpers from '../helpers/index.ts';
import { type IGroupConfig } from '../daemon.ts'; import { type IGroupConfig } from '../daemon.ts';
@@ -28,11 +29,10 @@ export class GroupHandler {
try { try {
await this.nupst.getDaemon().loadConfig(); await this.nupst.getDaemon().loadConfig();
} catch (error) { } catch (error) {
const errorBoxWidth = 45; logger.logBox('Configuration Error', [
logger.logBoxTitle('Configuration Error', errorBoxWidth); 'No configuration found.',
logger.logBoxLine('No configuration found.'); "Please run 'nupst ups add' first to create a configuration.",
logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); ], 50, 'error');
logger.logBoxEnd();
return; return;
} }
@@ -41,43 +41,53 @@ export class GroupHandler {
// Check if multi-UPS config // Check if multi-UPS config
if (!config.groups || !Array.isArray(config.groups)) { if (!config.groups || !Array.isArray(config.groups)) {
// Legacy or missing groups configuration logger.logBox('UPS Groups', [
const boxWidth = 45; 'No groups configured.',
logger.logBoxTitle('UPS Groups', boxWidth); '',
logger.logBoxLine('No groups configured.'); `${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
logger.logBoxLine('Use "nupst group add" to add a UPS group.'); ], 50, 'info');
logger.logBoxEnd();
return; return;
} }
// Display group list // Display group list with modern table
const boxWidth = 60;
logger.logBoxTitle('UPS Groups', boxWidth);
if (config.groups.length === 0) { if (config.groups.length === 0) {
logger.logBoxLine('No UPS groups configured.'); logger.logBox('UPS Groups', [
logger.logBoxLine('Use "nupst group add" to add a UPS group.'); 'No UPS groups configured.',
} else { '',
logger.logBoxLine(`Found ${config.groups.length} group(s)`); `${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
logger.logBoxLine(''); ], 60, 'info');
logger.logBoxLine('ID | Name | Mode | UPS Devices'); return;
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);
// Prepare table data
const rows = config.groups.map((group) => {
// Count UPS devices in this group // Count UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id)); const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
const upsCount = upsInGroup.length; const upsCount = upsInGroup.length;
const upsNames = upsInGroup.map((ups) => ups.name).join(', '); const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`); return {
} id: group.id,
} name: group.name || '',
mode: group.mode || 'unknown',
count: String(upsCount),
devices: upsCount > 0 ? upsNames : theme.dim('None'),
};
});
logger.logBoxEnd(); 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) { } catch (error) {
logger.error( logger.error(
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(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!'); logger.log('\nGroup setup complete!');
} finally { } finally {
rl.close(); rl.close();
process.stdin.destroy();
} }
} catch (error) { } catch (error) {
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
@@ -309,6 +320,7 @@ export class GroupHandler {
logger.log('\nGroup edit complete!'); logger.log('\nGroup edit complete!');
} finally { } finally {
rl.close(); rl.close();
process.stdin.destroy();
} }
} catch (error) { } catch (error) {
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
@@ -366,6 +378,7 @@ export class GroupHandler {
}); });
rl.close(); rl.close();
process.stdin.destroy();
if (confirm !== 'y' && confirm !== 'yes') { if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.'); logger.log('Deletion cancelled.');

View File

@@ -129,81 +129,57 @@ export class ServiceHandler {
try { try {
// Check if running as root // Check if running as root
this.checkRootAccess( this.checkRootAccess(
'This command must be run as root to update NUPST and refresh the systemd service.', 'This command must be run as root to update NUPST.',
); );
const boxWidth = 45; console.log('');
logger.logBoxTitle('NUPST Update Process', boxWidth); logger.info('Checking for updates...');
logger.logBoxLine('Updating NUPST from repository...');
// Determine the installation directory (assuming it's either /opt/nupst or the current directory)
const { existsSync } = await import('fs');
let installDir = '/opt/nupst';
if (!existsSync(installDir)) {
// If not installed in /opt/nupst, use the current directory
const { dirname } = await import('path');
installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
logger.logBoxLine(`Using local installation directory: ${installDir}`);
}
try { try {
// 1. Update the repository // Get current version
logger.logBoxLine('Pulling latest changes from git repository...'); const currentVersion = this.nupst.getVersion();
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
stdio: 'pipe', // Fetch latest version from Gitea API
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
const response = execSync(`curl -sSL ${apiUrl}`).toString();
const release = JSON.parse(response);
const latestVersion = release.tag_name; // e.g., "v4.0.7"
// Normalize versions for comparison (ensure both have "v" prefix)
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
logger.dim(`Current version: ${normalizedCurrent}`);
logger.dim(`Latest version: ${normalizedLatest}`);
console.log('');
// Compare normalized versions
if (normalizedCurrent === normalizedLatest) {
logger.success('Already up to date!');
console.log('');
return;
}
logger.info(`New version available: ${latestVersion}`);
logger.dim('Downloading and installing...');
console.log('');
// Download and run the install script
// This handles everything: download binary, stop service, replace, restart
const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh';
execSync(`curl -sSL ${installUrl} | bash`, {
stdio: 'inherit', // Show install script output to user
}); });
// 2. Run the install.sh script console.log('');
logger.logBoxLine('Running install.sh to update NUPST...'); logger.success(`Updated to ${latestVersion}`);
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); console.log('');
// 3. Run the setup.sh script with force flag 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' });
// 4. Refresh the systemd service
logger.logBoxLine('Refreshing systemd service...');
// First check if service exists
let serviceExists = false;
try {
const output = execSync('systemctl list-unit-files | grep nupst.service').toString();
serviceExists = output.includes('nupst.service');
} catch (error) { } catch (error) {
// If grep fails (service not found), serviceExists remains false console.log('');
serviceExists = false; logger.error('Update failed');
} logger.dim(`${error instanceof Error ? error.message : String(error)}`);
console.log('');
if (serviceExists) {
// Stop the service if it's running
const isRunning =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isRunning) {
logger.logBoxLine('Stopping nupst service...');
execSync('systemctl stop nupst.service');
}
// Reinstall the service
logger.logBoxLine('Reinstalling systemd service...');
await this.nupst.getSystemd().install();
// Restart the service if it was running
if (isRunning) {
logger.logBoxLine('Restarting nupst service...');
execSync('systemctl start nupst.service');
}
} else {
logger.logBoxLine('Systemd service not installed, skipping service refresh.');
logger.logBoxLine('Run "nupst enable" to install the service.');
}
logger.logBoxLine('Update completed successfully!');
logger.logBoxEnd();
} catch (error) {
logger.logBoxLine('Error during update process:');
logger.logBoxLine(`${error instanceof Error ? error.message : String(error)}`);
logger.logBoxEnd();
process.exit(1); process.exit(1);
} }
} catch (error) { } catch (error) {
@@ -237,9 +213,11 @@ export class ServiceHandler {
}); });
}; };
console.log('\nNUPST Uninstaller'); logger.log('');
console.log('==============='); logger.highlight('NUPST Uninstaller');
console.log('This will completely remove NUPST from your system.\n'); logger.dim('===============');
logger.log('This will completely remove NUPST from your system.');
logger.log('');
// Ask about removing configuration // Ask about removing configuration
const removeConfig = await prompt( const removeConfig = await prompt(
@@ -275,17 +253,20 @@ export class ServiceHandler {
} }
if (!uninstallScriptPath) { if (!uninstallScriptPath) {
console.error('Could not locate uninstall.sh script. Aborting uninstall.'); logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
rl.close(); rl.close();
process.stdin.destroy();
process.exit(1); process.exit(1);
} }
} }
// Close readline before executing script // Close readline before executing script
rl.close(); rl.close();
process.stdin.destroy();
// Execute uninstall.sh with the appropriate option // Execute uninstall.sh with the appropriate option
console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`); logger.log('');
logger.log(`Running uninstaller from ${uninstallScriptPath}...`);
// Pass the configuration removal option as an environment variable // Pass the configuration removal option as an environment variable
const env = { const env = {
@@ -301,7 +282,7 @@ export class ServiceHandler {
stdio: 'inherit', // Show output in the terminal stdio: 'inherit', // Show output in the terminal
}); });
} catch (error) { } catch (error) {
console.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`); logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1); process.exit(1);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -44,17 +44,30 @@ export class MigrationV3ToV4 extends BaseMigration {
readonly toVersion = '4.0'; readonly toVersion = '4.0';
async shouldRun(config: any): Promise<boolean> { async shouldRun(config: any): Promise<boolean> {
// V3 format has upsList instead of upsDevices // V3 format has upsList OR has upsDevices with flat structure (host at top level)
return !!config.upsList && !config.upsDevices; if (config.upsList && !config.upsDevices) {
return true; // Classic v3 with upsList
}
// Check if upsDevices exists but has flat structure (v3 format)
if (config.upsDevices && config.upsDevices.length > 0) {
const firstDevice = config.upsDevices[0];
// V3 has host at top level, v4 has it nested in snmp object
return !!firstDevice.host && !firstDevice.snmp;
}
return false;
} }
async migrate(config: any): Promise<any> { async migrate(config: any): Promise<any> {
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`); logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
logger.dim(` - Renaming upsList → upsDevices`);
logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`); logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`);
// Get devices from either upsList or upsDevices (for partially migrated configs)
const sourceDevices = config.upsList || config.upsDevices;
// Transform each UPS device from v3 flat structure to v4 nested structure // Transform each UPS device from v3 flat structure to v4 nested structure
const transformedDevices = config.upsList.map((device: any) => { const transformedDevices = sourceDevices.map((device: any) => {
// Build SNMP config object // Build SNMP config object
const snmpConfig: any = { const snmpConfig: any = {
host: device.host, host: device.host,

View File

@@ -525,6 +525,7 @@ export class NupstSnmp {
/** /**
* Determine power status based on UPS model and raw value * Determine power status based on UPS model and raw value
* Uses the value mappings defined in the OID sets
* @param upsModel UPS model * @param upsModel UPS model
* @param powerStatusValue Raw power status value * @param powerStatusValue Raw power status value
* @returns Standardized power status * @returns Standardized power status
@@ -533,38 +534,27 @@ export class NupstSnmp {
upsModel: TUpsModel | undefined, upsModel: TUpsModel | undefined,
powerStatusValue: number, powerStatusValue: number,
): 'online' | 'onBattery' | 'unknown' { ): 'online' | 'onBattery' | 'unknown' {
if (upsModel === 'cyberpower') { // Get the OID set for this UPS model
// CyberPower RMCARD205: upsBaseOutputStatus values if (upsModel && upsModel !== 'custom') {
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. const oidSet = UpsOidSets.getOidSet(upsModel);
if (powerStatusValue === 2) {
// Use the value mappings if available
if (oidSet.POWER_STATUS_VALUES) {
if (powerStatusValue === oidSet.POWER_STATUS_VALUES.online) {
return 'online'; return 'online';
} else if (powerStatusValue === 3) { } else if (powerStatusValue === oidSet.POWER_STATUS_VALUES.onBattery) {
return 'onBattery'; return 'onBattery';
} }
} else if (upsModel === 'eaton') { }
// Eaton UPS: xupsOutputSource values }
// 3=normal/mains, 5=battery, etc.
// Fallback for custom or undefined models (RFC 1628 standard)
// upsOutputSource: 3=normal (mains), 5=battery
if (powerStatusValue === 3) { if (powerStatusValue === 3) {
return 'online'; return 'online';
} else if (powerStatusValue === 5) { } else if (powerStatusValue === 5) {
return 'onBattery'; return 'onBattery';
} }
} else if (upsModel === 'apc') {
// APC UPS: upsBasicOutputStatus values
// 2=online, 3=onBattery, etc.
if (powerStatusValue === 2) {
return 'online';
} else if (powerStatusValue === 3) {
return 'onBattery';
}
} else {
// Default interpretation for other UPS models
if (powerStatusValue === 1) {
return 'online';
} else if (powerStatusValue === 2) {
return 'onBattery';
}
}
return 'unknown'; return 'unknown';
} }

View File

@@ -11,37 +11,57 @@ export class UpsOidSets {
private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = { private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = {
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11) // Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
cyberpower: { cyberpower: {
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery) POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage) BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks) BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
POWER_STATUS_VALUES: {
online: 2, // upsBaseOutputStatus: 2=onLine
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
},
}, },
// APC OIDs // APC OIDs
apc: { apc: {
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery) POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
POWER_STATUS_VALUES: {
online: 2, // upsBasicOutputStatus: 2=onLine
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
},
}, },
// Eaton OIDs // Eaton OIDs
eaton: { eaton: {
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery) POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage) BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds) BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
POWER_STATUS_VALUES: {
online: 3, // xupsOutputSource: 3=normal (mains power)
onBattery: 5, // xupsOutputSource: 5=battery
},
}, },
// TrippLite OIDs // TrippLite OIDs
tripplite: { tripplite: {
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
POWER_STATUS_VALUES: {
online: 2, // tlUpsOutputSource: 2=normal (mains power)
onBattery: 3, // tlUpsOutputSource: 3=onBattery
},
}, },
// Liebert/Vertiv OIDs // Liebert/Vertiv OIDs
liebert: { liebert: {
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
POWER_STATUS_VALUES: {
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
},
}, },
// Custom OIDs (to be provided by the user) // Custom OIDs (to be provided by the user)

View File

@@ -28,6 +28,13 @@ export interface IOidSet {
BATTERY_CAPACITY: string; BATTERY_CAPACITY: string;
/** OID for battery runtime */ /** OID for battery runtime */
BATTERY_RUNTIME: string; BATTERY_RUNTIME: string;
/** Power status value mappings */
POWER_STATUS_VALUES?: {
/** SNMP value that indicates UPS is online (on AC power) */
online: number;
/** SNMP value that indicates UPS is on battery */
onBattery: number;
};
} }
/** /**

View File

@@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { NupstDaemon } from './daemon.ts'; import { NupstDaemon } from './daemon.ts';
import { logger } from './logger.ts'; import { logger } from './logger.ts';
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
/** /**
* Class for managing systemd service * Class for managing systemd service
@@ -49,11 +50,11 @@ WantedBy=multi-user.target
try { try {
await fs.access(configPath); await fs.access(configPath);
} catch (error) { } catch (error) {
const boxWidth = 50; logger.log('');
logger.logBoxTitle('Configuration Error', boxWidth); logger.error('No configuration found');
logger.logBoxLine(`No configuration file found at ${configPath}`); logger.log(` ${theme.dim('Config file:')} ${configPath}`);
logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration."); logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
logger.logBoxEnd(); logger.log('');
throw new Error('Configuration not found'); throw new Error('Configuration not found');
} }
} }
@@ -133,21 +134,59 @@ WantedBy=multi-user.target
* Get status of the systemd service and UPS * Get status of the systemd service and UPS
* @param debugMode Whether to enable debug mode for SNMP * @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> { public async getStatus(debugMode: boolean = false): Promise<void> {
try { try {
// Enable debug mode if requested // Enable debug mode if requested
if (debugMode) { if (debugMode) {
const boxWidth = 45; console.log('');
logger.logBoxTitle('Debug Mode', boxWidth); logger.info('Debug Mode: SNMP debugging enabled');
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown'); console.log('');
logger.logBoxEnd();
this.daemon.getNupstSnmp().enableDebug(); this.daemon.getNupstSnmp().enableDebug();
} }
// Display version information // Display version and update status first
this.daemon.getNupstSnmp().getNupst().logVersionInfo(); await this.displayVersionInfo();
// Check if config exists first // Check if config exists
try { try {
await this.checkConfigExists(); await this.checkConfigExists();
} catch (error) { } catch (error) {
@@ -171,18 +210,50 @@ WantedBy=multi-user.target
private displayServiceStatus(): void { private displayServiceStatus(): void {
try { try {
const serviceStatus = execSync('systemctl status nupst.service').toString(); const serviceStatus = execSync('systemctl status nupst.service').toString();
const boxWidth = 45; const lines = serviceStatus.split('\n');
logger.logBoxTitle('Service Status', boxWidth);
// Process each line of the status output // Parse key information from systemctl output
serviceStatus.split('\n').forEach((line) => { let isActive = false;
logger.logBoxLine(line); let pid = '';
}); let memory = '';
logger.logBoxEnd(); let cpu = '';
for (const line of lines) {
if (line.includes('Active:')) {
isActive = line.includes('active (running)');
} else if (line.includes('Main PID:')) {
const match = line.match(/Main PID:\s+(\d+)/);
if (match) pid = match[1];
} else if (line.includes('Memory:')) {
const match = line.match(/Memory:\s+([\d.]+[A-Z])/);
if (match) memory = match[1];
} else if (line.includes('CPU:')) {
const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/);
if (match) cpu = match[1];
}
}
// Display beautiful status
logger.log('');
if (isActive) {
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
} else {
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
}
if (pid || memory || cpu) {
const details = [];
if (pid) details.push(`PID: ${theme.dim(pid)}`);
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
logger.log(` ${details.join(' ')}`);
}
logger.log('');
} catch (error) { } catch (error) {
const boxWidth = 45; logger.log('');
logger.logBoxTitle('Service Status', boxWidth); logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
logger.logBoxLine('Service is not running'); logger.log('');
logger.logBoxEnd();
} }
} }
@@ -199,7 +270,7 @@ WantedBy=multi-user.target
// Check if we have the new multi-UPS config format // Check if we have the new multi-UPS config format
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) { if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`); logger.info(`UPS Devices (${config.upsDevices.length}):`);
// Show status for each UPS // Show status for each UPS
for (const ups of config.upsDevices) { for (const ups of config.upsDevices) {
@@ -207,6 +278,7 @@ WantedBy=multi-user.target
} }
} else if (config.snmp) { } else if (config.snmp) {
// Legacy single UPS configuration // Legacy single UPS configuration
logger.info('UPS Devices (1):');
const legacyUps = { const legacyUps = {
id: 'default', id: 'default',
name: 'Default UPS', name: 'Default UPS',
@@ -217,15 +289,16 @@ WantedBy=multi-user.target
await this.displaySingleUpsStatus(legacyUps, snmp); await this.displaySingleUpsStatus(legacyUps, snmp);
} else { } else {
logger.error('No UPS devices found in configuration'); 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) { } catch (error) {
const boxWidth = 45; logger.log('');
logger.logBoxTitle('UPS Status', boxWidth); logger.error('Failed to retrieve UPS status');
logger.logBoxLine( logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`, logger.log('');
);
logger.logBoxEnd();
} }
} }
@@ -235,24 +308,6 @@ WantedBy=multi-user.target
* @param snmp SNMP manager * @param snmp SNMP manager
*/ */
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> { private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
const boxWidth = 45;
logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth);
logger.logBoxLine(`ID: ${ups.id}`);
logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`);
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`);
if (ups.groups && ups.groups.length > 0) {
// Get group names if available
const config = this.daemon.getConfig();
const groupNames = ups.groups.map((groupId: string) => {
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
return group ? group.name : groupId;
});
logger.logBoxLine(`Groups: ${groupNames.join(', ')}`);
}
logger.logBoxEnd();
try { try {
// Create a test config with a short timeout // Create a test config with a short timeout
const testConfig = { const testConfig = {
@@ -262,32 +317,43 @@ WantedBy=multi-user.target
const status = await snmp.getUpsStatus(testConfig); const status = await snmp.getUpsStatus(testConfig);
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); // Determine status symbol based on power status
logger.logBoxLine(`Power Status: ${status.powerStatus}`); let statusSymbol = symbols.unknown;
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`); if (status.powerStatus === 'online') {
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`); statusSymbol = symbols.running;
} else if (status.powerStatus === 'onBattery') {
statusSymbol = symbols.warning;
}
// Show threshold status // Display UPS name and power status
logger.logBoxLine(''); logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
logger.logBoxLine('Thresholds:');
logger.logBoxLine( // Display battery with color coding
` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${ const batteryColor = getBatteryColor(status.batteryCapacity);
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓' const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
}`, logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
);
logger.logBoxLine( // Display host info
` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${ logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
}`, // Display groups if any
); if (ups.groups && ups.groups.length > 0) {
const config = this.daemon.getConfig();
const groupNames = ups.groups.map((groupId: string) => {
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
return group ? group.name : groupId;
});
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
}
logger.log('');
logger.logBoxEnd();
} catch (error) { } catch (error) {
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); // Display error for this UPS
logger.logBoxLine( logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`, logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
); logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
logger.logBoxEnd(); logger.log('');
} }
} }