Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
c2f2f1e2ee | |||
936f86c346 | |||
7ff1a7da36 | |||
a87710144c | |||
23fd5cc5cd | |||
fb4d776bdd | |||
88ad16c638 | |||
016681b77b | |||
49f7a7da8b |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "4.0.2",
|
||||
"version": "4.0.8",
|
||||
"exports": "./mod.ts",
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-all mod.ts",
|
||||
|
88
install.sh
88
install.sh
@@ -10,15 +10,7 @@
|
||||
# With version specification:
|
||||
# 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:
|
||||
# -y, --yes Automatically answer yes to all prompts
|
||||
# -h, --help Show this help message
|
||||
# --version VERSION Install specific version (e.g., v4.0.0)
|
||||
# --install-dir DIR Installation directory (default: /opt/nupst)
|
||||
@@ -26,7 +18,6 @@
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
AUTO_YES=0
|
||||
SHOW_HELP=0
|
||||
SPECIFIED_VERSION=""
|
||||
INSTALL_DIR="/opt/nupst"
|
||||
@@ -36,10 +27,6 @@ GITEA_REPO="serve.zone/nupst"
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-y|--yes)
|
||||
AUTO_YES=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
SHOW_HELP=1
|
||||
shift
|
||||
@@ -67,7 +54,6 @@ if [ $SHOW_HELP -eq 1 ]; then
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -y, --yes Automatically answer yes to all prompts"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " --version VERSION Install specific version (e.g., v4.0.0)"
|
||||
echo " --install-dir DIR Installation directory (default: /opt/nupst)"
|
||||
@@ -78,9 +64,6 @@ if [ $SHOW_HELP -eq 1 ]; then
|
||||
echo ""
|
||||
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 ""
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -90,32 +73,6 @@ if [ "$EUID" -ne 0 ]; then
|
||||
exit 1
|
||||
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
|
||||
exec < /dev/tty 2>/dev/null || INTERACTIVE=0
|
||||
|
||||
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
|
||||
detect_platform() {
|
||||
local os=$(uname -s)
|
||||
@@ -225,22 +182,6 @@ if [ -d "$INSTALL_DIR" ]; then
|
||||
echo ""
|
||||
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..."
|
||||
|
||||
# Check if service exists (enabled or running) and stop it if active
|
||||
@@ -269,17 +210,6 @@ if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Old installation files removed."
|
||||
fi
|
||||
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"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
fi
|
||||
@@ -325,22 +255,8 @@ else
|
||||
fi
|
||||
|
||||
# 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"
|
||||
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
|
||||
ln -sf "$BINARY_PATH" "$BIN_DIR/nupst"
|
||||
echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH"
|
||||
|
||||
echo ""
|
||||
|
||||
|
16
readme.md
16
readme.md
@@ -29,15 +29,8 @@ dependencies.
|
||||
The easiest way to install NUPST is using the automated installer:
|
||||
|
||||
```bash
|
||||
# Download and run installer (most reliable)
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh
|
||||
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
|
||||
# One-line installation
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||
```
|
||||
|
||||
The installer will:
|
||||
@@ -76,7 +69,6 @@ sudo mv nupst /usr/local/bin/nupst
|
||||
The installer script (`install.sh`) supports the following options:
|
||||
|
||||
```
|
||||
-y, --yes Automatically answer yes to all prompts
|
||||
-h, --help Show help message
|
||||
--version VERSION Install specific version (e.g., --version v4.0.0)
|
||||
--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:
|
||||
|
||||
```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:
|
||||
@@ -461,7 +453,7 @@ The installer script automatically handles the entire migration while preserving
|
||||
|
||||
```bash
|
||||
# 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
|
||||
nupst service status
|
||||
|
168
test/manualdocker/00-test-fresh-v4-install.sh
Executable file
168
test/manualdocker/00-test-fresh-v4-install.sh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Test fresh v4 installation from scratch
|
||||
# Tests the most common user scenario: clean install using curl | bash
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="nupst-test-fresh-v4"
|
||||
|
||||
echo "================================================"
|
||||
echo " NUPST Fresh v4 Installation Test"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Check if container already exists
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "⚠️ Container ${CONTAINER_NAME} already exists"
|
||||
read -p "Remove and recreate? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "→ Stopping and removing existing container..."
|
||||
docker stop ${CONTAINER_NAME} 2>/dev/null || true
|
||||
docker rm ${CONTAINER_NAME} 2>/dev/null || true
|
||||
else
|
||||
echo "Exiting. Remove manually with: docker rm -f ${CONTAINER_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "→ Creating Docker container with systemd..."
|
||||
docker run -d \
|
||||
--name ${CONTAINER_NAME} \
|
||||
--privileged \
|
||||
--cgroupns=host \
|
||||
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
||||
ubuntu:22.04 \
|
||||
/bin/bash -c "apt-get update && apt-get install -y systemd systemd-sysv && exec /sbin/init"
|
||||
|
||||
echo "→ Waiting for systemd to initialize..."
|
||||
sleep 10
|
||||
|
||||
echo "→ Waiting for dpkg lock to be released..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
|
||||
echo ' Waiting for dpkg lock...'
|
||||
sleep 2
|
||||
done
|
||||
echo ' dpkg lock released'
|
||||
"
|
||||
|
||||
echo "→ Installing prerequisites (curl)..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Installing NUPST v4 using curl | bash..."
|
||||
echo " Command: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y"
|
||||
echo ""
|
||||
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " Verifying Installation"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
echo "→ Checking binary location..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
if [ -f /opt/nupst/nupst ]; then
|
||||
echo ' ✓ Binary exists at /opt/nupst/nupst'
|
||||
ls -lh /opt/nupst/nupst
|
||||
else
|
||||
echo ' ✗ Binary not found at /opt/nupst/nupst'
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Checking symlink..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
if [ -L /usr/local/bin/nupst ]; then
|
||||
echo ' ✓ Symlink exists at /usr/local/bin/nupst'
|
||||
ls -lh /usr/local/bin/nupst
|
||||
elif [ -L /usr/bin/nupst ]; then
|
||||
echo ' ✓ Symlink exists at /usr/bin/nupst'
|
||||
ls -lh /usr/bin/nupst
|
||||
else
|
||||
echo ' ✗ Symlink not found in /usr/local/bin or /usr/bin'
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Checking PATH integration..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
NUPST_PATH=\$(which nupst 2>/dev/null)
|
||||
if [ -n \"\$NUPST_PATH\" ]; then
|
||||
echo ' ✓ nupst found in PATH at: '\$NUPST_PATH
|
||||
else
|
||||
echo ' ✗ nupst not found in PATH'
|
||||
echo ' PATH contents:'
|
||||
echo \$PATH
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Testing nupst command execution..."
|
||||
docker exec ${CONTAINER_NAME} nupst --version
|
||||
|
||||
echo ""
|
||||
echo "→ Creating minimal config for service test..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
mkdir -p /etc/nupst
|
||||
cat > /etc/nupst/config.json << 'EOF'
|
||||
{
|
||||
\"version\": \"4.0\",
|
||||
\"upsDevices\": [],
|
||||
\"groups\": [],
|
||||
\"checkInterval\": 30000
|
||||
}
|
||||
EOF
|
||||
echo ' ✓ Minimal config created'
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Testing service creation..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
echo ' Running: nupst service enable'
|
||||
nupst service enable
|
||||
|
||||
if [ -f /etc/systemd/system/nupst.service ]; then
|
||||
echo ' ✓ Service file created successfully'
|
||||
else
|
||||
echo ' ✗ Service file creation failed'
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Checking if service is enabled..."
|
||||
docker exec ${CONTAINER_NAME} systemctl is-enabled nupst
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " ✓ Fresh v4 Installation Test Complete"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Installation verified successfully:"
|
||||
echo " • Binary installed to /opt/nupst/nupst"
|
||||
echo " • Symlink created for global access"
|
||||
echo " • nupst command available in PATH"
|
||||
echo " • Command executes correctly"
|
||||
echo " • Systemd service file created"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " docker exec -it ${CONTAINER_NAME} bash"
|
||||
echo " docker exec ${CONTAINER_NAME} nupst --help"
|
||||
echo " docker exec ${CONTAINER_NAME} nupst service status"
|
||||
echo " docker stop ${CONTAINER_NAME}"
|
||||
echo " docker rm -f ${CONTAINER_NAME}"
|
||||
echo ""
|
@@ -53,7 +53,7 @@ docker exec ${CONTAINER_NAME} bash -c "
|
||||
echo "→ Installing prerequisites in container..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl sudo
|
||||
apt-get install -y -qq git curl sudo jq
|
||||
"
|
||||
|
||||
echo "→ Cloning NUPST v3 (commit ${V3_COMMIT})..."
|
||||
@@ -66,35 +66,59 @@ docker exec ${CONTAINER_NAME} bash -c "
|
||||
git log -1 --oneline
|
||||
"
|
||||
|
||||
echo "→ Running NUPST v3 installation script..."
|
||||
echo "→ Running NUPST v3 installation directly (bypassing install.sh auto-update)..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
cd /opt/nupst
|
||||
bash install.sh -y
|
||||
# Run setup.sh directly to avoid install.sh trying to update to v4
|
||||
bash setup.sh -y
|
||||
"
|
||||
|
||||
echo "→ Creating dummy NUPST configuration for testing..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
mkdir -p /etc/nupst
|
||||
cat > /etc/nupst/config.json << 'EOF'
|
||||
echo "→ Creating NUPST configuration using real UPS data from .nogit/env.json..."
|
||||
|
||||
# Check if .nogit/env.json exists
|
||||
if [ ! -f "../../.nogit/env.json" ]; then
|
||||
echo "❌ Error: .nogit/env.json not found"
|
||||
echo "This file contains test UPS credentials and is required for testing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read UPS data from .nogit/env.json and create v3 config
|
||||
docker exec ${CONTAINER_NAME} bash -c "mkdir -p /etc/nupst"
|
||||
|
||||
# Generate config from .nogit/env.json using jq
|
||||
cat ../../.nogit/env.json | jq -r '
|
||||
{
|
||||
\"upsList\": [
|
||||
"upsList": [
|
||||
{
|
||||
\"id\": \"test-ups\",
|
||||
\"name\": \"Test UPS\",
|
||||
\"host\": \"127.0.0.1\",
|
||||
\"port\": 161,
|
||||
\"community\": \"public\",
|
||||
\"version\": \"2c\",
|
||||
\"batteryLowOID\": \"1.3.6.1.4.1.935.1.1.1.3.3.1.0\",
|
||||
\"onBatteryOID\": \"1.3.6.1.4.1.935.1.1.1.3.3.2.0\",
|
||||
\"shutdownCommand\": \"echo 'Shutdown triggered'\"
|
||||
"id": "test-ups-v1",
|
||||
"name": "Test UPS (SNMP v1)",
|
||||
"host": .testConfigV1.snmp.host,
|
||||
"port": .testConfigV1.snmp.port,
|
||||
"community": .testConfigV1.snmp.community,
|
||||
"version": (.testConfigV1.snmp.version | tostring),
|
||||
"batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0",
|
||||
"onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0",
|
||||
"shutdownCommand": "echo \"Shutdown triggered for test-ups-v1\""
|
||||
},
|
||||
{
|
||||
"id": "test-ups-v3",
|
||||
"name": "Test UPS (SNMP v3)",
|
||||
"host": .testConfigV3.snmp.host,
|
||||
"port": .testConfigV3.snmp.port,
|
||||
"version": (.testConfigV3.snmp.version | tostring),
|
||||
"securityLevel": .testConfigV3.snmp.securityLevel,
|
||||
"username": .testConfigV3.snmp.username,
|
||||
"authProtocol": .testConfigV3.snmp.authProtocol,
|
||||
"authKey": .testConfigV3.snmp.authKey,
|
||||
"batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0",
|
||||
"onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0",
|
||||
"shutdownCommand": "echo \"Shutdown triggered for test-ups-v3\""
|
||||
}
|
||||
],
|
||||
\"groups\": []
|
||||
}
|
||||
EOF
|
||||
echo 'Dummy config created at /etc/nupst/config.json'
|
||||
"
|
||||
"groups": []
|
||||
}' | docker exec -i ${CONTAINER_NAME} tee /etc/nupst/config.json > /dev/null
|
||||
|
||||
echo " ✓ Real UPS config created at /etc/nupst/config.json (from .nogit/env.json)"
|
||||
|
||||
echo "→ Enabling NUPST systemd service..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
|
@@ -32,23 +32,10 @@ echo "→ Stopping v3 service..."
|
||||
docker exec ${CONTAINER_NAME} systemctl stop nupst
|
||||
echo ""
|
||||
|
||||
echo "→ Pulling latest v4 code from migration/deno-v4 branch..."
|
||||
echo "→ Running v4 installation from main branch (should auto-detect v3 and migrate)..."
|
||||
echo " Using: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
cd /opt/nupst
|
||||
git fetch origin
|
||||
# Reset any local changes made by install.sh
|
||||
git reset --hard HEAD
|
||||
git clean -fd
|
||||
git checkout migration/deno-v4
|
||||
git pull origin migration/deno-v4
|
||||
echo 'Now on:'
|
||||
git log -1 --oneline
|
||||
"
|
||||
|
||||
echo "→ Running install.sh (should auto-detect v3 and migrate)..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
cd /opt/nupst
|
||||
bash install.sh -y
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y
|
||||
"
|
||||
|
||||
echo "→ Checking service status after migration..."
|
||||
|
@@ -129,81 +129,53 @@ export class ServiceHandler {
|
||||
try {
|
||||
// Check if running as root
|
||||
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;
|
||||
logger.logBoxTitle('NUPST Update Process', boxWidth);
|
||||
logger.logBoxLine('Updating NUPST from repository...');
|
||||
|
||||
// Determine the installation directory (assuming it's either /opt/nupst or the current directory)
|
||||
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}`);
|
||||
}
|
||||
console.log('');
|
||||
logger.info('Checking for updates...');
|
||||
|
||||
try {
|
||||
// 1. Update the repository
|
||||
logger.logBoxLine('Pulling latest changes from git repository...');
|
||||
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
|
||||
stdio: 'pipe',
|
||||
// Get current version
|
||||
const currentVersion = this.nupst.getVersion();
|
||||
|
||||
// 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"
|
||||
|
||||
logger.dim(`Current version: ${currentVersion}`);
|
||||
logger.dim(`Latest version: ${latestVersion}`);
|
||||
console.log('');
|
||||
|
||||
// Compare versions (both are in format "v4.0.7")
|
||||
if (currentVersion === latestVersion) {
|
||||
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
|
||||
logger.logBoxLine('Running install.sh to update NUPST...');
|
||||
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
|
||||
|
||||
// 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');
|
||||
console.log('');
|
||||
logger.success(`Updated to ${latestVersion}`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
// If grep fails (service not found), serviceExists remains false
|
||||
serviceExists = false;
|
||||
}
|
||||
|
||||
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();
|
||||
console.log('');
|
||||
logger.error('Update failed');
|
||||
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -15,7 +15,6 @@ export const theme = {
|
||||
info: colors.cyan,
|
||||
dim: colors.dim,
|
||||
highlight: colors.bold,
|
||||
bright: colors.bright,
|
||||
|
||||
// Status indicators
|
||||
statusActive: (text: string) => colors.green(colors.bold(text)),
|
||||
|
52
ts/daemon.ts
52
ts/daemon.ts
@@ -6,6 +6,7 @@ import { promisify } from 'node:util';
|
||||
import { NupstSnmp } from './snmp/manager.ts';
|
||||
import type { ISnmpConfig } from './snmp/types.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { MigrationRunner } from './migrations/index.ts';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -49,6 +50,8 @@ export interface IGroupConfig {
|
||||
* Configuration interface for the daemon
|
||||
*/
|
||||
export interface INupstConfig {
|
||||
/** Configuration format version */
|
||||
version?: string;
|
||||
/** UPS devices configuration */
|
||||
upsDevices: IUpsConfig[];
|
||||
/** Groups configuration */
|
||||
@@ -56,10 +59,12 @@ export interface INupstConfig {
|
||||
/** Check interval in milliseconds */
|
||||
checkInterval: number;
|
||||
|
||||
// Legacy fields for backward compatibility
|
||||
/** SNMP configuration settings (legacy) */
|
||||
// Legacy fields for backward compatibility (will be migrated away)
|
||||
/** UPS list (v3 format - legacy) */
|
||||
upsList?: IUpsConfig[];
|
||||
/** SNMP configuration settings (v1 format - legacy) */
|
||||
snmp?: ISnmpConfig;
|
||||
/** Threshold settings (legacy) */
|
||||
/** Threshold settings (v1 format - legacy) */
|
||||
thresholds?: {
|
||||
/** Shutdown when battery below this percentage */
|
||||
battery: number;
|
||||
@@ -91,6 +96,7 @@ export class NupstDaemon {
|
||||
|
||||
/** Default configuration */
|
||||
private readonly DEFAULT_CONFIG: INupstConfig = {
|
||||
version: '4.0',
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
@@ -153,29 +159,16 @@ export class NupstDaemon {
|
||||
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
||||
const parsedConfig = JSON.parse(configData);
|
||||
|
||||
// Handle legacy configuration format
|
||||
if (!parsedConfig.upsDevices && parsedConfig.snmp) {
|
||||
// Convert legacy format to new format
|
||||
this.config = {
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: parsedConfig.snmp,
|
||||
thresholds: parsedConfig.thresholds,
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: parsedConfig.checkInterval,
|
||||
};
|
||||
// Run migrations to upgrade config format if needed
|
||||
const migrationRunner = new MigrationRunner();
|
||||
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
||||
|
||||
logger.log('Legacy configuration format detected. Converting to multi-UPS format.');
|
||||
|
||||
// Save the new format
|
||||
// Save migrated config back to disk if any migrations ran
|
||||
if (migrated) {
|
||||
this.config = migratedConfig;
|
||||
await this.saveConfig(this.config);
|
||||
} else {
|
||||
this.config = parsedConfig;
|
||||
this.config = migratedConfig;
|
||||
}
|
||||
|
||||
return this.config;
|
||||
@@ -202,8 +195,17 @@ export class NupstDaemon {
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||
this.config = config;
|
||||
|
||||
// Ensure version is always set and remove legacy fields before saving
|
||||
const configToSave: INupstConfig = {
|
||||
version: '4.0',
|
||||
upsDevices: config.upsDevices,
|
||||
groups: config.groups,
|
||||
checkInterval: config.checkInterval,
|
||||
};
|
||||
|
||||
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
|
||||
this.config = configToSave;
|
||||
|
||||
console.log('┌─ Configuration Saved ─────────────────────┐');
|
||||
console.log(`│ Location: ${this.CONFIG_PATH}`);
|
||||
|
54
ts/migrations/base-migration.ts
Normal file
54
ts/migrations/base-migration.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Abstract base class for configuration migrations
|
||||
*
|
||||
* Each migration represents an upgrade from one config version to another.
|
||||
* Migrations run in order based on the `order` field, allowing users to jump
|
||||
* multiple versions (e.g., v1 → v4 runs migrations 2, 3, and 4).
|
||||
*/
|
||||
export abstract class BaseMigration {
|
||||
/**
|
||||
* Migration order number
|
||||
* - Order 2: v1 → v2
|
||||
* - Order 3: v2 → v3
|
||||
* - Order 4: v3 → v4
|
||||
* etc.
|
||||
*/
|
||||
abstract readonly order: number;
|
||||
|
||||
/**
|
||||
* Source version this migration upgrades from
|
||||
* e.g., "1.x", "3.x"
|
||||
*/
|
||||
abstract readonly fromVersion: string;
|
||||
|
||||
/**
|
||||
* Target version this migration upgrades to
|
||||
* e.g., "2.0", "4.0"
|
||||
*/
|
||||
abstract readonly toVersion: string;
|
||||
|
||||
/**
|
||||
* Check if this migration should run on the given config
|
||||
*
|
||||
* @param config - Raw configuration object to check
|
||||
* @returns True if migration should run, false otherwise
|
||||
*/
|
||||
abstract shouldRun(config: any): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Perform the migration on the given config
|
||||
*
|
||||
* @param config - Raw configuration object to migrate
|
||||
* @returns Migrated configuration object
|
||||
*/
|
||||
abstract migrate(config: any): Promise<any>;
|
||||
|
||||
/**
|
||||
* Get human-readable name for this migration
|
||||
*
|
||||
* @returns Migration name
|
||||
*/
|
||||
getName(): string {
|
||||
return `Migration ${this.fromVersion} → ${this.toVersion}`;
|
||||
}
|
||||
}
|
10
ts/migrations/index.ts
Normal file
10
ts/migrations/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Configuration migrations module
|
||||
*
|
||||
* Exports the migration system for upgrading configs between versions.
|
||||
*/
|
||||
|
||||
export { BaseMigration } from './base-migration.ts';
|
||||
export { MigrationRunner } from './migration-runner.ts';
|
||||
export { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||
export { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
69
ts/migrations/migration-runner.ts
Normal file
69
ts/migrations/migration-runner.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||
import { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration runner
|
||||
*
|
||||
* Discovers all available migrations, sorts them by order,
|
||||
* and runs applicable migrations in sequence.
|
||||
*/
|
||||
export class MigrationRunner {
|
||||
private migrations: BaseMigration[];
|
||||
|
||||
constructor() {
|
||||
// Register all migrations here
|
||||
this.migrations = [
|
||||
new MigrationV1ToV2(),
|
||||
new MigrationV3ToV4(),
|
||||
// Add future migrations here (v4→v5, v5→v6, etc.)
|
||||
];
|
||||
|
||||
// Sort by order to ensure they run in sequence
|
||||
this.migrations.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all applicable migrations on the config
|
||||
*
|
||||
* @param config - Raw configuration object to migrate
|
||||
* @returns Migrated configuration and whether migrations ran
|
||||
*/
|
||||
async run(config: any): Promise<{ config: any; migrated: boolean }> {
|
||||
let currentConfig = config;
|
||||
let anyMigrationsRan = false;
|
||||
|
||||
logger.dim('Checking for required config migrations...');
|
||||
|
||||
for (const migration of this.migrations) {
|
||||
const shouldRun = await migration.shouldRun(currentConfig);
|
||||
|
||||
if (shouldRun) {
|
||||
logger.info(`Running ${migration.getName()}...`);
|
||||
currentConfig = await migration.migrate(currentConfig);
|
||||
anyMigrationsRan = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyMigrationsRan) {
|
||||
logger.success('Configuration migrations complete');
|
||||
} else {
|
||||
logger.dim('No migrations needed');
|
||||
}
|
||||
|
||||
return {
|
||||
config: currentConfig,
|
||||
migrated: anyMigrationsRan,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered migrations
|
||||
*
|
||||
* @returns Array of all migrations sorted by order
|
||||
*/
|
||||
getMigrations(): BaseMigration[] {
|
||||
return [...this.migrations];
|
||||
}
|
||||
}
|
56
ts/migrations/migration-v1-to-v2.ts
Normal file
56
ts/migrations/migration-v1-to-v2.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration from v1 (single SNMP config) to v2 (upsDevices array)
|
||||
*
|
||||
* Detects old format:
|
||||
* {
|
||||
* snmp: { ... },
|
||||
* thresholds: { ... },
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*
|
||||
* Converts to:
|
||||
* {
|
||||
* version: "2.0",
|
||||
* upsDevices: [{ id: "default", name: "Default UPS", snmp: ..., thresholds: ... }],
|
||||
* groups: [],
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*/
|
||||
export class MigrationV1ToV2 extends BaseMigration {
|
||||
readonly order = 2;
|
||||
readonly fromVersion = '1.x';
|
||||
readonly toVersion = '2.0';
|
||||
|
||||
async shouldRun(config: any): Promise<boolean> {
|
||||
// V1 format has snmp field directly at root, no upsDevices or upsList
|
||||
return !!config.snmp && !config.upsDevices && !config.upsList;
|
||||
}
|
||||
|
||||
async migrate(config: any): Promise<any> {
|
||||
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
|
||||
|
||||
const migrated = {
|
||||
version: this.toVersion,
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds || {
|
||||
battery: 60,
|
||||
runtime: 20,
|
||||
},
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(`${this.getName()}: Migration complete`);
|
||||
return migrated;
|
||||
}
|
||||
}
|
119
ts/migrations/migration-v3-to-v4.ts
Normal file
119
ts/migrations/migration-v3-to-v4.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration from v3 (upsList) to v4 (upsDevices)
|
||||
*
|
||||
* Transforms v3 format with flat SNMP config:
|
||||
* {
|
||||
* upsList: [
|
||||
* {
|
||||
* id: "ups-1",
|
||||
* name: "UPS 1",
|
||||
* host: "192.168.1.1",
|
||||
* port: 161,
|
||||
* community: "public",
|
||||
* version: "1" // string
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* To v4 format with nested SNMP config:
|
||||
* {
|
||||
* version: "4.0",
|
||||
* upsDevices: [
|
||||
* {
|
||||
* id: "ups-1",
|
||||
* name: "UPS 1",
|
||||
* snmp: {
|
||||
* host: "192.168.1.1",
|
||||
* port: 161,
|
||||
* community: "public",
|
||||
* version: 1, // number
|
||||
* timeout: 5000
|
||||
* },
|
||||
* thresholds: { battery: 60, runtime: 20 },
|
||||
* groups: []
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export class MigrationV3ToV4 extends BaseMigration {
|
||||
readonly order = 4;
|
||||
readonly fromVersion = '3.x';
|
||||
readonly toVersion = '4.0';
|
||||
|
||||
async shouldRun(config: any): Promise<boolean> {
|
||||
// V3 format has upsList OR has upsDevices with flat structure (host at top level)
|
||||
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> {
|
||||
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
|
||||
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
|
||||
const transformedDevices = sourceDevices.map((device: any) => {
|
||||
// Build SNMP config object
|
||||
const snmpConfig: any = {
|
||||
host: device.host,
|
||||
port: device.port || 161,
|
||||
version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version,
|
||||
timeout: device.timeout || 5000,
|
||||
};
|
||||
|
||||
// Add SNMPv1/v2c fields
|
||||
if (device.community) {
|
||||
snmpConfig.community = device.community;
|
||||
}
|
||||
|
||||
// Add SNMPv3 fields
|
||||
if (device.securityLevel) snmpConfig.securityLevel = device.securityLevel;
|
||||
if (device.username) snmpConfig.username = device.username;
|
||||
if (device.authProtocol) snmpConfig.authProtocol = device.authProtocol;
|
||||
if (device.authKey) snmpConfig.authKey = device.authKey;
|
||||
if (device.privProtocol) snmpConfig.privProtocol = device.privProtocol;
|
||||
if (device.privKey) snmpConfig.privKey = device.privKey;
|
||||
|
||||
// Add UPS model if present
|
||||
if (device.upsModel) snmpConfig.upsModel = device.upsModel;
|
||||
if (device.customOIDs) snmpConfig.customOIDs = device.customOIDs;
|
||||
|
||||
// Return v4 format with nested structure
|
||||
return {
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
snmp: snmpConfig,
|
||||
thresholds: device.thresholds || {
|
||||
battery: 60,
|
||||
runtime: 20,
|
||||
},
|
||||
groups: device.groups || [],
|
||||
};
|
||||
});
|
||||
|
||||
const migrated = {
|
||||
version: this.toVersion,
|
||||
upsDevices: transformedDevices,
|
||||
groups: config.groups || [],
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`);
|
||||
return migrated;
|
||||
}
|
||||
}
|
148
ts/systemd.ts
148
ts/systemd.ts
@@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { NupstDaemon } from './daemon.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Class for managing systemd service
|
||||
@@ -171,18 +172,50 @@ WantedBy=multi-user.target
|
||||
private displayServiceStatus(): void {
|
||||
try {
|
||||
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
// Process each line of the status output
|
||||
serviceStatus.split('\n').forEach((line) => {
|
||||
logger.logBoxLine(line);
|
||||
});
|
||||
logger.logBoxEnd();
|
||||
const lines = serviceStatus.split('\n');
|
||||
|
||||
// Parse key information from systemctl output
|
||||
let isActive = false;
|
||||
let pid = '';
|
||||
let memory = '';
|
||||
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
|
||||
console.log('');
|
||||
if (isActive) {
|
||||
console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
||||
} else {
|
||||
console.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)}`);
|
||||
console.log(` ${details.join(' ')}`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('Service is not running');
|
||||
logger.logBoxEnd();
|
||||
console.log('');
|
||||
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +232,7 @@ WantedBy=multi-user.target
|
||||
|
||||
// Check if we have the new multi-UPS config format
|
||||
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
|
||||
console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`));
|
||||
|
||||
// Show status for each UPS
|
||||
for (const ups of config.upsDevices) {
|
||||
@@ -207,6 +240,7 @@ WantedBy=multi-user.target
|
||||
}
|
||||
} else if (config.snmp) {
|
||||
// Legacy single UPS configuration
|
||||
console.log(theme.info('UPS Devices (1):'));
|
||||
const legacyUps = {
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
@@ -217,15 +251,16 @@ WantedBy=multi-user.target
|
||||
|
||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
console.log('');
|
||||
console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`);
|
||||
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
||||
console.log('');
|
||||
}
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Status', boxWidth);
|
||||
logger.logBoxLine(
|
||||
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
console.log('');
|
||||
console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`);
|
||||
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,24 +270,6 @@ WantedBy=multi-user.target
|
||||
* @param snmp SNMP manager
|
||||
*/
|
||||
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 {
|
||||
// Create a test config with a short timeout
|
||||
const testConfig = {
|
||||
@@ -262,32 +279,43 @@ WantedBy=multi-user.target
|
||||
|
||||
const status = await snmp.getUpsStatus(testConfig);
|
||||
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
|
||||
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
// Determine status symbol based on power status
|
||||
let statusSymbol = symbols.unknown;
|
||||
if (status.powerStatus === 'online') {
|
||||
statusSymbol = symbols.running;
|
||||
} else if (status.powerStatus === 'onBattery') {
|
||||
statusSymbol = symbols.warning;
|
||||
}
|
||||
|
||||
// Show threshold status
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(
|
||||
` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
|
||||
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
|
||||
}`,
|
||||
);
|
||||
logger.logBoxLine(
|
||||
` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
|
||||
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
|
||||
}`,
|
||||
);
|
||||
// Display UPS name and power status
|
||||
console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
||||
|
||||
// Display battery with color coding
|
||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
||||
console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||
|
||||
// Display host info
|
||||
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||
|
||||
// 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;
|
||||
});
|
||||
console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(
|
||||
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
// Display error for this UPS
|
||||
console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
||||
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user