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,51 +169,26 @@ 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 systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
SERVICE_WAS_RUNNING=1
if [ -d "$INSTALL_DIR" ]; then if systemctl is-active --quiet nupst 2>/dev/null; then
# Check if this is an old Node.js-based installation echo "Stopping NUPST service..."
if [ -f "$INSTALL_DIR/package.json" ] || [ -d "$INSTALL_DIR/node_modules" ]; then systemctl stop nupst
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 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
SERVICE_WAS_RUNNING=1
if systemctl is-active --quiet nupst 2>/dev/null; then
echo "Stopping NUPST service..."
systemctl stop nupst
else
echo "Service is installed but not currently running (will be updated)..."
fi
fi
# Clean up old Node.js installation files
if [ $OLD_NODE_INSTALL -eq 1 ]; then
echo "Cleaning up old Node.js installation files..."
rm -rf "$INSTALL_DIR/node_modules" 2>/dev/null || true
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
else
echo "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
fi fi
# Clean installation directory - ensure only binary exists
if [ -d "$INSTALL_DIR" ]; then
echo "Cleaning installation directory: $INSTALL_DIR"
rm -rf "$INSTALL_DIR"
fi
# Create fresh installation directory
echo "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# Download binary # Download binary
echo "Downloading NUPST binary..." echo "Downloading NUPST binary..."
TEMP_FILE="$INSTALL_DIR/nupst.download" TEMP_FILE="$INSTALL_DIR/nupst.download"
@@ -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"

1021
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';

103
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 {
this.displayTargetActions(group!, 'Group');
}
} else { } else {
// List actions for all UPS devices // 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