Compare commits

...

16 Commits

Author SHA1 Message Date
f03c683d02 fix(install): correct installation order for updates
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 42s
CI / Build All Platforms (push) Successful in 47s
- Stop service first
- Remove /opt/nupst
- Create fresh directory
- Download binary
- Ensures clean installation without leaving empty directories
2025-10-20 13:28:56 +00:00
f750299780 fix(install): simplify installation to only binary in /opt/nupst
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 48s
- Remove all conditional migration logic
- Always completely clean /opt/nupst before installation
- Ensures only NUPST binary exists in installation directory
- Simplified service restart logic
2025-10-20 13:24:03 +00:00
ca1039408d chore(release): bump version to 5.0.2
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 42s
CI / Build All Platforms (push) Successful in 47s
2025-10-20 13:09:20 +00:00
df3e0b9424 fix: import process from node:process in script-action
Some checks failed
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
CI / Build All Platforms (push) Has been cancelled
Fixes TS2580 error where process was undefined
2025-10-20 13:08:43 +00:00
c8e5960abd chore(release): bump version to 5.0.1
Some checks failed
CI / Type Check & Lint (push) Failing after 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 48s
2025-10-20 13:07:07 +00:00
7304a62357 chore(release): bump version to 5.0.0
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 41s
CI / Build All Platforms (push) Successful in 47s
BREAKING CHANGE: Deprecated CLI commands removed

This is a major version bump due to breaking changes:
- Removed all deprecated flat command structure
- Users must now use modern subcommand structure:
  - nupst service <subcommand>
  - nupst ups <subcommand>
  - nupst group <subcommand>
  - nupst action <subcommand>

New in v5.0:
- Enhanced status display showing actions and groups (v4.3.3)
- Action management system (v4.3.0)
- Improved type safety (v4.2.5)
- Config auto-reload (v4.3.2)

Updated readme version references to v5.0.0
2025-10-20 13:00:42 +00:00
a5a88e53ba docs: improve accuracy of dependency claims
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 5s
CI / Build All Platforms (push) Successful in 45s
- Clarify 'zero runtime dependencies' means for compiled binary
- Change 'no npm' to 'no installation required'
- Update 'Zero Dependencies' to 'Self-Contained Binary'
- Improve migration table accuracy (Runtime Dependencies vs build deps)
- More honest about supply chain (reduced vs eliminated)
2025-10-20 12:59:14 +00:00
73bc271c59 docs: remove deprecated command migration documentation
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 4s
CI / Build All Platforms (push) Successful in 45s
- Remove 'Command Migration' section showing old commands
- Users must now use modern subcommand structure
- Clean up migration guide to focus on actual changes
2025-10-20 12:57:48 +00:00
1e98181e71 feat: remove deprecated CLI commands
BREAKING CHANGE: Old flat command structure no longer supported

Removed deprecated commands:
- nupst add → use 'nupst ups add'
- nupst edit → use 'nupst ups edit'
- nupst delete → use 'nupst ups remove'
- nupst list → use 'nupst ups list'
- nupst test → use 'nupst ups test'
- nupst setup → use 'nupst ups edit'
- nupst enable → use 'nupst service enable'
- nupst disable → use 'nupst service disable'
- nupst start → use 'nupst service start'
- nupst stop → use 'nupst service stop'
- nupst status → use 'nupst service status'
- nupst logs → use 'nupst service logs'
- nupst daemon-start → use 'nupst service start-daemon'

Also removed 'delete' as alias for 'remove' (use 'rm' instead)

Modern command structure is now required:
- nupst service <subcommand>
- nupst ups <subcommand>
- nupst group <subcommand>
- nupst action <subcommand>

Kept modern aliases: rm, ls
2025-10-20 12:57:23 +00:00
eb5a8185ae docs: create comprehensive readme with v4.3 features
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Successful in 6s
CI / Build All Platforms (push) Successful in 45s
- Add action management documentation (new feature)
- Update configuration examples to v4.1+ action-based format
- Document trigger modes and action system
- Update status command output examples with groups and actions
- Remove outdated contributing section
- Add modern emojis and engaging tone
- Update all version references to v4.3.3
- Maintain Task Venture Capital GmbH legal section
2025-10-20 12:52:26 +00:00
ef3d3f3fa3 chore(release): bump version to 4.3.3
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 43s
CI / Build All Platforms (push) Successful in 48s
2025-10-20 12:48:26 +00:00
34e6e850ad feat(status): display actions and groups in status command
- Add action display to UPS status showing trigger mode, thresholds, and delays
- Create displayGroupsStatus() method to show group information
- Display group mode, member UPS devices, and group actions
- Integrate groups section into status command output
2025-10-20 12:48:14 +00:00
992a776fd2 chore(release): bump version to 4.3.2
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 42s
CI / Build All Platforms (push) Successful in 47s
2025-10-20 12:42:34 +00:00
3e15a2d52f fix(action): correct message about config reload
The daemon already has automatic config file watching and reloads changes
without requiring a restart. Updated action handler messages to correctly
reflect this behavior.

Changed:
- 'Restart service to apply changes: nupst service restart'
  → 'Changes saved and will be applied automatically'

The config file watcher (daemon.ts:986) uses Deno.watchFs() to monitor
/etc/nupst/config.json and automatically calls reloadConfig() when changes
are detected. No restart needed.
2025-10-20 12:42:31 +00:00
d1a3576d31 chore(release): bump version to 4.3.1
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 42s
CI / Build All Platforms (push) Successful in 46s
2025-10-20 12:34:52 +00:00
1ca05e879b feat(action): add group action support
Extended action management to support groups in addition to UPS devices:

Changes:
- Auto-detects whether target ID is a UPS or group
- All action commands now work with both UPS and groups:
  * nupst action add <ups-id|group-id>
  * nupst action remove <ups-id|group-id> <index>
  * nupst action list [ups-id|group-id]

- Updated ActionHandler methods to handle both target types
- Updated help text and usage examples
- List command shows both UPS and group actions when no target specified
- Clear labeling in output distinguishes UPS actions from group actions

Example usage:
  nupst action list                 # Shows all UPS and group actions
  nupst action add dc-rack-1        # Adds action to group 'dc-rack-1'
  nupst action remove default 0     # Removes action from UPS 'default'

Groups can now have their own shutdown actions, allowing fine-grained
control over group behavior during power events.
2025-10-20 12:34:47 +00:00
7 changed files with 802 additions and 655 deletions

View File

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

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# NUPST Installer Script (v4.0+) # NUPST Installer Script (v5.0+)
# Downloads and installs pre-compiled NUPST binary from Gitea releases # Downloads and installs pre-compiled NUPST binary from Gitea releases
# #
# Usage: # Usage:
@@ -8,7 +8,7 @@
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash # curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
# #
# 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 v5.0.0
# #
# Options: # Options:
# -h, --help Show this help message # -h, --help Show this help message
@@ -48,14 +48,14 @@ while [[ $# -gt 0 ]]; do
done done
if [ $SHOW_HELP -eq 1 ]; then if [ $SHOW_HELP -eq 1 ]; then
echo "NUPST Installer Script (v4.0+)" echo "NUPST Installer Script (v5.0+)"
echo "Downloads and installs pre-compiled NUPST binary" echo "Downloads and installs pre-compiled NUPST binary"
echo "" echo ""
echo "Usage: $0 [options]" echo "Usage: $0 [options]"
echo "" echo ""
echo "Options:" echo "Options:"
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., v5.0.0)"
echo " --install-dir DIR Installation directory (default: /opt/nupst)" echo " --install-dir DIR Installation directory (default: /opt/nupst)"
echo "" echo ""
echo "Examples:" echo "Examples:"
@@ -63,7 +63,7 @@ if [ $SHOW_HELP -eq 1 ]; then
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash" echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
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 v5.0.0"
exit 0 exit 0
fi fi
@@ -145,7 +145,7 @@ get_latest_version() {
# Main installation process # Main installation process
echo "================================================" echo "================================================"
echo " NUPST Installation Script (v4.0+)" echo " NUPST Installation Script (v5.0+)"
echo "================================================" echo "================================================"
echo "" echo ""
@@ -169,50 +169,25 @@ DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BIN
echo "Download URL: $DOWNLOAD_URL" echo "Download URL: $DOWNLOAD_URL"
echo "" echo ""
# Check if installation directory exists # Check if service is running and stop it
SERVICE_WAS_RUNNING=0 SERVICE_WAS_RUNNING=0
OLD_NODE_INSTALL=0
if [ -d "$INSTALL_DIR" ]; then
# Check if this is an old Node.js-based installation
if [ -f "$INSTALL_DIR/package.json" ] || [ -d "$INSTALL_DIR/node_modules" ]; then
OLD_NODE_INSTALL=1
echo "Detected old Node.js-based NUPST installation (v3.x or earlier)"
echo "This installer will migrate to the new Deno-based binary version (v4.0+)"
echo ""
fi
echo "Updating existing installation at $INSTALL_DIR..."
# Check if service exists (enabled or running) and stop it if active
if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
SERVICE_WAS_RUNNING=1 SERVICE_WAS_RUNNING=1
if systemctl is-active --quiet nupst 2>/dev/null; then if systemctl is-active --quiet nupst 2>/dev/null; then
echo "Stopping NUPST service..." echo "Stopping NUPST service..."
systemctl stop nupst systemctl stop nupst
else
echo "Service is installed but not currently running (will be updated)..."
fi fi
fi fi
# Clean up old Node.js installation files # Clean installation directory - ensure only binary exists
if [ $OLD_NODE_INSTALL -eq 1 ]; then if [ -d "$INSTALL_DIR" ]; then
echo "Cleaning up old Node.js installation files..." echo "Cleaning installation directory: $INSTALL_DIR"
rm -rf "$INSTALL_DIR/node_modules" 2>/dev/null || true rm -rf "$INSTALL_DIR"
rm -rf "$INSTALL_DIR/vendor" 2>/dev/null || true
rm -rf "$INSTALL_DIR/dist_ts" 2>/dev/null || true
rm -f "$INSTALL_DIR/package.json" 2>/dev/null || true
rm -f "$INSTALL_DIR/package-lock.json" 2>/dev/null || true
rm -f "$INSTALL_DIR/pnpm-lock.yaml" 2>/dev/null || true
rm -f "$INSTALL_DIR/tsconfig.json" 2>/dev/null || true
rm -f "$INSTALL_DIR/setup.sh" 2>/dev/null || true
rm -rf "$INSTALL_DIR/bin" 2>/dev/null || true
echo "Old installation files removed."
fi fi
else
# Create fresh installation directory
echo "Creating installation directory: $INSTALL_DIR" echo "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR" mkdir -p "$INSTALL_DIR"
fi
# Download binary # Download binary
echo "Downloading NUPST binary..." echo "Downloading NUPST binary..."
@@ -260,14 +235,6 @@ echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH"
echo "" echo ""
# Update systemd service file if migrating from v3
if [ $SERVICE_WAS_RUNNING -eq 1 ] && [ $OLD_NODE_INSTALL -eq 1 ]; then
echo "Updating systemd service file for v4..."
$BINARY_PATH service enable > /dev/null 2>&1
echo "Service file updated."
echo ""
fi
# Restart service if it was running before update # Restart service if it was running before update
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
echo "Restarting NUPST service..." echo "Restarting NUPST service..."
@@ -280,20 +247,6 @@ echo "================================================"
echo " NUPST Installation Complete!" echo " NUPST Installation Complete!"
echo "================================================" echo "================================================"
echo "" echo ""
if [ $OLD_NODE_INSTALL -eq 1 ]; then
echo "Migration from v3.x to v4.0 successful!"
echo ""
echo "What changed:"
echo " • Node.js runtime removed (now a self-contained binary)"
echo " • Faster startup and lower memory usage"
echo " • CLI commands now use subcommand structure"
echo " (old commands still work with deprecation warnings)"
echo ""
echo "See readme for migration details: https://code.foss.global/serve.zone/nupst#migration-from-v3x"
echo ""
fi
echo "Installation details:" echo "Installation details:"
echo " Binary location: $BINARY_PATH" echo " Binary location: $BINARY_PATH"
echo " Symlink location: $BIN_DIR/nupst" echo " Symlink location: $BIN_DIR/nupst"

1005
readme.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import * as path from 'node:path'; import * as path from 'node:path';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import process from 'node:process';
import { exec } from 'node:child_process'; import { exec } from 'node:child_process';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { Action, type IActionConfig, type IActionContext } from './base-action.ts'; import { Action, type IActionConfig, type IActionContext } from './base-action.ts';

101
ts/cli.ts
View File

@@ -127,8 +127,7 @@ export class NupstCli {
break; break;
} }
case 'remove': case 'remove':
case 'rm': // Alias case 'rm': {
case 'delete': { // Backward compatibility
const upsIdToRemove = subcommandArgs[0]; const upsIdToRemove = subcommandArgs[0];
if (!upsIdToRemove) { if (!upsIdToRemove) {
logger.error('UPS ID is required for remove command'); logger.error('UPS ID is required for remove command');
@@ -172,8 +171,7 @@ export class NupstCli {
break; break;
} }
case 'remove': case 'remove':
case 'rm': // Alias case 'rm': {
case 'delete': { // Backward compatibility
const groupIdToRemove = subcommandArgs[0]; const groupIdToRemove = subcommandArgs[0];
if (!groupIdToRemove) { if (!groupIdToRemove) {
logger.error('Group ID is required for remove command'); logger.error('Group ID is required for remove command');
@@ -206,8 +204,7 @@ export class NupstCli {
break; break;
} }
case 'remove': case 'remove':
case 'rm': // Alias case 'rm': {
case 'delete': { // Backward compatibility
const upsId = subcommandArgs[0]; const upsId = subcommandArgs[0];
const actionIndex = subcommandArgs[1]; const actionIndex = subcommandArgs[1];
await actionHandler.remove(upsId, actionIndex); await actionHandler.remove(upsId, actionIndex);
@@ -242,72 +239,8 @@ export class NupstCli {
return; return;
} }
// Handle top-level commands and backward compatibility // Handle top-level commands
switch (command) { switch (command) {
// Backward compatibility - old UPS commands
case 'add':
logger.log("Note: 'nupst add' is deprecated. Use 'nupst ups add' instead.");
await upsHandler.add();
break;
case 'edit':
logger.log("Note: 'nupst edit' is deprecated. Use 'nupst ups edit' instead.");
await upsHandler.edit(commandArgs[0]);
break;
case 'delete':
logger.log("Note: 'nupst delete' is deprecated. Use 'nupst ups remove' instead.");
if (!commandArgs[0]) {
logger.error('UPS ID is required for delete command');
this.showHelp();
return;
}
await upsHandler.remove(commandArgs[0]);
break;
case 'list':
logger.log("Note: 'nupst list' is deprecated. Use 'nupst ups list' instead.");
await upsHandler.list();
break;
case 'test':
logger.log("Note: 'nupst test' is deprecated. Use 'nupst ups test' instead.");
await upsHandler.test(debugMode);
break;
case 'setup':
logger.log("Note: 'nupst setup' is deprecated. Use 'nupst ups edit' instead.");
await upsHandler.edit(undefined);
break;
// Backward compatibility - old service commands
case 'enable':
logger.log("Note: 'nupst enable' is deprecated. Use 'nupst service enable' instead.");
await serviceHandler.enable();
break;
case 'disable':
logger.log("Note: 'nupst disable' is deprecated. Use 'nupst service disable' instead.");
await serviceHandler.disable();
break;
case 'start':
logger.log("Note: 'nupst start' is deprecated. Use 'nupst service start' instead.");
await serviceHandler.start();
break;
case 'stop':
logger.log("Note: 'nupst stop' is deprecated. Use 'nupst service stop' instead.");
await serviceHandler.stop();
break;
case 'status':
logger.log("Note: 'nupst status' is deprecated. Use 'nupst service status' instead.");
await serviceHandler.status();
break;
case 'logs':
logger.log("Note: 'nupst logs' is deprecated. Use 'nupst service logs' instead.");
await serviceHandler.logs();
break;
case 'daemon-start':
logger.log(
"Note: 'nupst daemon-start' is deprecated. Use 'nupst service start-daemon' instead.",
);
await serviceHandler.daemonStart(debugMode);
break;
// Top-level commands (no changes)
case 'update': case 'update':
await serviceHandler.update(); await serviceHandler.update();
break; break;
@@ -571,9 +504,9 @@ export class NupstCli {
// Action subcommands // Action subcommands
logger.log(theme.info('Action Subcommands:')); logger.log(theme.info('Action Subcommands:'));
this.printCommand('nupst action add <ups-id>', 'Add a new action to a UPS'); this.printCommand('nupst action add <target-id>', 'Add a new action to a UPS or group');
this.printCommand('nupst action remove <ups-id> <index>', 'Remove an action by index'); this.printCommand('nupst action remove <target-id> <index>', 'Remove an action by index');
this.printCommand('nupst action list [ups-id]', 'List all actions (optionally for specific UPS)'); this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)');
console.log(''); console.log('');
// Options // Options
@@ -589,11 +522,6 @@ export class NupstCli {
logger.dim(' nupst group list # Show all configured groups'); logger.dim(' nupst group list # Show all configured groups');
logger.dim(' nupst config # Display current configuration'); logger.dim(' nupst config # Display current configuration');
console.log(''); console.log('');
// Note about deprecated commands
logger.warn('Note: Old command format (e.g., \'nupst add\') still works but is deprecated.');
logger.dim(' Use the new format (e.g., \'nupst ups add\') going forward.');
console.log('');
} }
/** /**
@@ -691,18 +619,19 @@ Usage:
nupst action <subcommand> [arguments] nupst action <subcommand> [arguments]
Subcommands: Subcommands:
add <ups-id> - Add a new action to a UPS interactively add <ups-id|group-id> - Add a new action to a UPS or group interactively
remove <ups-id> <index> - Remove an action by index (alias: rm, delete) remove <ups-id|group-id> <index> - Remove an action by index (alias: rm)
list [ups-id] - List all actions (optionally for specific UPS) (alias: ls) list [ups-id|group-id] - List all actions (optionally for specific target) (alias: ls)
Options: Options:
--debug, -d - Enable debug mode for detailed logging --debug, -d - Enable debug mode for detailed logging
Examples: Examples:
nupst action list - List actions for all UPS devices nupst action list - List actions for all UPS devices and groups
nupst action list default - List actions for UPS with ID 'default' nupst action list default - List actions for UPS or group with ID 'default'
nupst action add default - Add a new action to UPS 'default' nupst action add default - Add a new action to UPS or group 'default'
nupst action remove default 0 - Remove action at index 0 from UPS 'default' nupst action remove default 0 - Remove action at index 0 from UPS or group 'default'
nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1'
`); `);
} }
} }

View File

@@ -3,7 +3,7 @@ import { Nupst } from '../nupst.ts';
import { logger, type ITableColumn } from '../logger.ts'; import { logger, type ITableColumn } from '../logger.ts';
import { theme, symbols } from '../colors.ts'; import { theme, symbols } from '../colors.ts';
import type { IActionConfig } from '../actions/base-action.ts'; import type { IActionConfig } from '../actions/base-action.ts';
import type { IUpsConfig } from '../daemon.ts'; import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
/** /**
* Class for handling action-related CLI commands * Class for handling action-related CLI commands
@@ -21,30 +21,42 @@ export class ActionHandler {
} }
/** /**
* Add a new action to a UPS * Add a new action to a UPS or group
*/ */
public async add(upsId?: string): Promise<void> { public async add(targetId?: string): Promise<void> {
try { try {
if (!upsId) { if (!targetId) {
logger.error('UPS ID is required'); logger.error('Target ID is required');
logger.log(` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id>')}`); logger.log(
` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
);
logger.log(''); logger.log('');
logger.log(` ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(` ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(` ${theme.dim('List groups:')} ${theme.command('nupst group list')}`);
logger.log(''); logger.log('');
process.exit(1); process.exit(1);
} }
const config = await this.nupst.getDaemon().loadConfig(); const config = await this.nupst.getDaemon().loadConfig();
const ups = config.upsDevices.find((u) => u.id === upsId);
if (!ups) { // Check if it's a UPS
logger.error(`UPS with ID '${upsId}' not found`); const ups = config.upsDevices.find((u) => u.id === targetId);
// Check if it's a group
const group = config.groups?.find((g) => g.id === targetId);
if (!ups && !group) {
logger.error(`UPS or Group with ID '${targetId}' not found`);
logger.log(''); logger.log('');
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
logger.log(''); logger.log('');
process.exit(1); process.exit(1);
} }
const target = ups || group;
const targetType = ups ? 'UPS' : 'Group';
const targetName = ups ? ups.name : group!.name;
const readline = await import('node:readline'); const readline = await import('node:readline');
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
@@ -61,7 +73,7 @@ export class ActionHandler {
try { try {
logger.log(''); logger.log('');
logger.info(`Add Action to ${theme.highlight(ups.name)}`); logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
logger.log(''); logger.log('');
// Action type (currently only shutdown is supported) // Action type (currently only shutdown is supported)
@@ -130,37 +142,38 @@ export class ActionHandler {
shutdownDelay, shutdownDelay,
}; };
// Add to UPS // Add to target (UPS or group)
if (!ups.actions) { if (!target!.actions) {
ups.actions = []; target!.actions = [];
} }
ups.actions.push(newAction); target!.actions.push(newAction);
await this.nupst.getDaemon().saveConfig(config); await this.nupst.getDaemon().saveConfig(config);
logger.log(''); logger.log('');
logger.success(`Action added to ${ups.name}`); logger.success(`Action added to ${targetType} ${targetName}`);
logger.log(''); logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`);
logger.log(''); logger.log('');
} finally { } finally {
rl.close(); rl.close();
} }
} catch (error) { } catch (error) {
logger.error(`Failed to add action: ${error instanceof Error ? error.message : String(error)}`); logger.error(
`Failed to add action: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1); process.exit(1);
} }
} }
/** /**
* Remove an action from a UPS * Remove an action from a UPS or group
*/ */
public async remove(upsId?: string, actionIndexStr?: string): Promise<void> { public async remove(targetId?: string, actionIndexStr?: string): Promise<void> {
try { try {
if (!upsId || !actionIndexStr) { if (!targetId || !actionIndexStr) {
logger.error('UPS ID and action index are required'); logger.error('Target ID and action index are required');
logger.log( logger.log(
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id> <action-index>')}`, ` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id|group-id> <action-index>')}`,
); );
logger.log(''); logger.log('');
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`); logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
@@ -175,47 +188,57 @@ export class ActionHandler {
} }
const config = await this.nupst.getDaemon().loadConfig(); const config = await this.nupst.getDaemon().loadConfig();
const ups = config.upsDevices.find((u) => u.id === upsId);
if (!ups) { // Check if it's a UPS
logger.error(`UPS with ID '${upsId}' not found`); const ups = config.upsDevices.find((u) => u.id === targetId);
// Check if it's a group
const group = config.groups?.find((g) => g.id === targetId);
if (!ups && !group) {
logger.error(`UPS or Group with ID '${targetId}' not found`);
logger.log(''); logger.log('');
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
logger.log(''); logger.log('');
process.exit(1); process.exit(1);
} }
if (!ups.actions || ups.actions.length === 0) { const target = ups || group;
logger.error(`No actions configured for UPS '${ups.name}'`); const targetType = ups ? 'UPS' : 'Group';
const targetName = ups ? ups.name : group!.name;
if (!target!.actions || target!.actions.length === 0) {
logger.error(`No actions configured for ${targetType} '${targetName}'`);
logger.log(''); logger.log('');
process.exit(1); process.exit(1);
} }
if (actionIndex >= ups.actions.length) { if (actionIndex >= target!.actions.length) {
logger.error( logger.error(
`Invalid action index. UPS '${ups.name}' has ${ups.actions.length} action(s) (index 0-${ups.actions.length - 1})`, `Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
); );
logger.log(''); logger.log('');
logger.log(` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${upsId}`)}`); logger.log(
` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${targetId}`)}`,
);
logger.log(''); logger.log('');
process.exit(1); process.exit(1);
} }
const removedAction = ups.actions[actionIndex]; const removedAction = target!.actions[actionIndex];
ups.actions.splice(actionIndex, 1); target!.actions.splice(actionIndex, 1);
await this.nupst.getDaemon().saveConfig(config); await this.nupst.getDaemon().saveConfig(config);
logger.log(''); logger.log('');
logger.success(`Action removed from ${ups.name}`); logger.success(`Action removed from ${targetType} ${targetName}`);
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`); logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
if (removedAction.thresholds) { if (removedAction.thresholds) {
logger.log( logger.log(
` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`, ` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
); );
} }
logger.log(''); logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`);
logger.log(''); logger.log('');
} catch (error) { } catch (error) {
logger.error( logger.error(
@@ -226,43 +249,61 @@ export class ActionHandler {
} }
/** /**
* List all actions for a specific UPS or all UPS devices * List all actions for a specific UPS/group or all devices
*/ */
public async list(upsId?: string): Promise<void> { public async list(targetId?: string): Promise<void> {
try { try {
const config = await this.nupst.getDaemon().loadConfig(); const config = await this.nupst.getDaemon().loadConfig();
if (upsId) { if (targetId) {
// List actions for specific UPS // List actions for specific UPS or group
const ups = config.upsDevices.find((u) => u.id === upsId); const ups = config.upsDevices.find((u) => u.id === targetId);
const group = config.groups?.find((g) => g.id === targetId);
if (!ups) { if (!ups && !group) {
logger.error(`UPS with ID '${upsId}' not found`); logger.error(`UPS or Group with ID '${targetId}' not found`);
logger.log(''); logger.log('');
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
logger.log(''); logger.log('');
process.exit(1); process.exit(1);
} }
this.displayUpsActions(ups); if (ups) {
this.displayTargetActions(ups, 'UPS');
} else { } else {
// List actions for all UPS devices this.displayTargetActions(group!, 'Group');
}
} else {
// List actions for all UPS devices and groups
logger.log(''); logger.log('');
logger.info('Actions for All UPS Devices'); logger.info('Actions for All UPS Devices and Groups');
logger.log(''); logger.log('');
let hasAnyActions = false; let hasAnyActions = false;
// Display UPS actions
for (const ups of config.upsDevices) { for (const ups of config.upsDevices) {
if (ups.actions && ups.actions.length > 0) { if (ups.actions && ups.actions.length > 0) {
hasAnyActions = true; hasAnyActions = true;
this.displayUpsActions(ups); this.displayTargetActions(ups, 'UPS');
}
}
// Display Group actions
for (const group of config.groups || []) {
if (group.actions && group.actions.length > 0) {
hasAnyActions = true;
this.displayTargetActions(group, 'Group');
} }
} }
if (!hasAnyActions) { if (!hasAnyActions) {
logger.log(` ${theme.dim('No actions configured')}`); logger.log(` ${theme.dim('No actions configured')}`);
logger.log(''); logger.log('');
logger.log(` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id>')}`); logger.log(
` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
);
logger.log(''); logger.log('');
} }
} }
@@ -275,13 +316,18 @@ export class ActionHandler {
} }
/** /**
* Display actions for a single UPS * Display actions for a single UPS or Group
*/ */
private displayUpsActions(ups: IUpsConfig): void { private displayTargetActions(
logger.log(`${symbols.info} ${theme.highlight(ups.name)} ${theme.dim(`(${ups.id})`)}`); target: IUpsConfig | IGroupConfig,
targetType: 'UPS' | 'Group',
): void {
logger.log(
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
);
logger.log(''); logger.log('');
if (!ups.actions || ups.actions.length === 0) { if (!target.actions || target.actions.length === 0) {
logger.log(` ${theme.dim('No actions configured')}`); logger.log(` ${theme.dim('No actions configured')}`);
logger.log(''); logger.log('');
return; return;
@@ -296,7 +342,7 @@ export class ActionHandler {
{ header: 'Delay', key: 'delay', align: 'right' }, { header: 'Delay', key: 'delay', align: 'right' },
]; ];
const rows = ups.actions.map((action, index) => ({ const rows = target.actions.map((action, index) => ({
index: theme.dim(index.toString()), index: theme.dim(index.toString()),
type: theme.highlight(action.type), type: theme.highlight(action.type),
battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'), battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'),

View File

@@ -277,6 +277,9 @@ WantedBy=multi-user.target
for (const ups of config.upsDevices) { for (const ups of config.upsDevices) {
await this.displaySingleUpsStatus(ups, snmp); await this.displaySingleUpsStatus(ups, snmp);
} }
// Display groups after UPS devices
this.displayGroupsStatus();
} else if (config.snmp) { } else if (config.snmp) {
// Legacy single UPS configuration (v1/v2 format) // Legacy single UPS configuration (v1/v2 format)
logger.info('UPS Devices (1):'); logger.info('UPS Devices (1):');
@@ -365,6 +368,27 @@ WantedBy=multi-user.target
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`); logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
} }
// Display actions if any
if (ups.actions && ups.actions.length > 0) {
for (const action of ups.actions) {
let actionDesc = `${action.type}`;
if (action.thresholds) {
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
} else {
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
}
logger.log(` ${theme.dim('Action:')} ${theme.info(actionDesc)}`);
}
}
logger.log(''); logger.log('');
} catch (error) { } catch (error) {
@@ -376,6 +400,69 @@ WantedBy=multi-user.target
} }
} }
/**
* Display status of all groups
* @private
*/
private displayGroupsStatus(): void {
const config = this.daemon.getConfig();
if (!config.groups || config.groups.length === 0) {
return; // No groups to display
}
logger.log('');
logger.info(`Groups (${config.groups.length}):`);
for (const group of config.groups) {
// Display group name and mode
const modeColor = group.mode === 'redundant' ? theme.success : theme.warning;
logger.log(
` ${symbols.info} ${theme.highlight(group.name)} ${theme.dim(`(${modeColor(group.mode)})`)}`,
);
// Display description if present
if (group.description) {
logger.log(` ${theme.dim(group.description)}`);
}
// Display UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(group.id)
);
if (upsInGroup.length > 0) {
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
logger.log(` ${theme.dim(`UPS Devices (${upsInGroup.length}):`)} ${upsNames}`);
} else {
logger.log(` ${theme.dim('UPS Devices: None')}`);
}
// Display actions if any
if (group.actions && group.actions.length > 0) {
for (const action of group.actions) {
let actionDesc = `${action.type}`;
if (action.thresholds) {
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
} else {
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
actionDesc += ')';
}
logger.log(` ${theme.dim('Action:')} ${theme.info(actionDesc)}`);
}
}
logger.log('');
}
}
/** /**
* Disable and uninstall the systemd service * Disable and uninstall the systemd service
* @throws Error if disabling fails * @throws Error if disabling fails