Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
88ad16c638 | |||
016681b77b | |||
49f7a7da8b | |||
f8269a1cb7 | |||
b37e1aae6c | |||
7076829747 | |||
1387ca262b | |||
684f034aee |
@@ -152,6 +152,29 @@ jobs:
|
|||||||
echo "Release notes:"
|
echo "Release notes:"
|
||||||
cat /tmp/release_notes.md
|
cat /tmp/release_notes.md
|
||||||
|
|
||||||
|
- name: Delete existing release if it exists
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
echo "Checking for existing release $VERSION..."
|
||||||
|
|
||||||
|
# Try to get existing release by tag
|
||||||
|
EXISTING_RELEASE_ID=$(curl -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/tags/$VERSION" \
|
||||||
|
| jq -r '.id // empty')
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_RELEASE_ID" ]; then
|
||||||
|
echo "Found existing release (ID: $EXISTING_RELEASE_ID), deleting..."
|
||||||
|
curl -X DELETE -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/$EXISTING_RELEASE_ID"
|
||||||
|
echo "Existing release deleted"
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
echo "No existing release found, proceeding with creation"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Create Gitea Release
|
- name: Create Gitea Release
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "4.0.0",
|
"version": "4.0.3",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-all mod.ts",
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
20
install.sh
20
install.sh
@@ -243,11 +243,15 @@ if [ -d "$INSTALL_DIR" ]; then
|
|||||||
|
|
||||||
echo "Updating existing installation at $INSTALL_DIR..."
|
echo "Updating existing installation at $INSTALL_DIR..."
|
||||||
|
|
||||||
# Check if service is running and stop it
|
# Check if service exists (enabled or running) and stop it if active
|
||||||
if 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
|
||||||
echo "Stopping NUPST service..."
|
|
||||||
systemctl stop nupst
|
|
||||||
SERVICE_WAS_RUNNING=1
|
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
|
fi
|
||||||
|
|
||||||
# Clean up old Node.js installation files
|
# Clean up old Node.js installation files
|
||||||
@@ -340,6 +344,14 @@ fi
|
|||||||
|
|
||||||
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..."
|
||||||
|
@@ -53,7 +53,7 @@ docker exec ${CONTAINER_NAME} bash -c "
|
|||||||
echo "→ Installing prerequisites in container..."
|
echo "→ Installing prerequisites in container..."
|
||||||
docker exec ${CONTAINER_NAME} bash -c "
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
apt-get update -qq
|
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})..."
|
echo "→ Cloning NUPST v3 (commit ${V3_COMMIT})..."
|
||||||
@@ -66,35 +66,59 @@ docker exec ${CONTAINER_NAME} bash -c "
|
|||||||
git log -1 --oneline
|
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 "
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
cd /opt/nupst
|
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..."
|
echo "→ Creating NUPST configuration using real UPS data from .nogit/env.json..."
|
||||||
docker exec ${CONTAINER_NAME} bash -c "
|
|
||||||
mkdir -p /etc/nupst
|
# Check if .nogit/env.json exists
|
||||||
cat > /etc/nupst/config.json << 'EOF'
|
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\",
|
"id": "test-ups-v1",
|
||||||
\"name\": \"Test UPS\",
|
"name": "Test UPS (SNMP v1)",
|
||||||
\"host\": \"127.0.0.1\",
|
"host": .testConfigV1.snmp.host,
|
||||||
\"port\": 161,
|
"port": .testConfigV1.snmp.port,
|
||||||
\"community\": \"public\",
|
"community": .testConfigV1.snmp.community,
|
||||||
\"version\": \"2c\",
|
"version": (.testConfigV1.snmp.version | tostring),
|
||||||
\"batteryLowOID\": \"1.3.6.1.4.1.935.1.1.1.3.3.1.0\",
|
"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\",
|
"onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0",
|
||||||
\"shutdownCommand\": \"echo 'Shutdown triggered'\"
|
"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\": []
|
"groups": []
|
||||||
}
|
}' | docker exec -i ${CONTAINER_NAME} tee /etc/nupst/config.json > /dev/null
|
||||||
EOF
|
|
||||||
echo 'Dummy config created at /etc/nupst/config.json'
|
echo " ✓ Real UPS config created at /etc/nupst/config.json (from .nogit/env.json)"
|
||||||
"
|
|
||||||
|
|
||||||
echo "→ Enabling NUPST systemd service..."
|
echo "→ Enabling NUPST systemd service..."
|
||||||
docker exec ${CONTAINER_NAME} bash -c "
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
|
@@ -32,23 +32,10 @@ echo "→ Stopping v3 service..."
|
|||||||
docker exec ${CONTAINER_NAME} systemctl stop nupst
|
docker exec ${CONTAINER_NAME} systemctl stop nupst
|
||||||
echo ""
|
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 "
|
docker exec ${CONTAINER_NAME} bash -c "
|
||||||
cd /opt/nupst
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y
|
||||||
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
|
|
||||||
"
|
"
|
||||||
|
|
||||||
echo "→ Checking service status after migration..."
|
echo "→ Checking service status after migration..."
|
||||||
|
233
test/showcase.ts
Normal file
233
test/showcase.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Showcase test for NUPST CLI outputs
|
||||||
|
* Demonstrates all the beautiful colored output features
|
||||||
|
*
|
||||||
|
* Run with: deno run --allow-all test/showcase.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger, type ITableColumn } from '../ts/logger.ts';
|
||||||
|
import { theme, symbols, getBatteryColor, formatPowerStatus } from '../ts/colors.ts';
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
logger.highlight('NUPST CLI OUTPUT SHOWCASE');
|
||||||
|
logger.dim('Demonstrating beautiful, colored terminal output');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 1. Basic Logging Methods ===
|
||||||
|
logger.logBoxTitle('Basic Logging Methods', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.log('Normal log message (default color)');
|
||||||
|
logger.success('Success message with ✓ symbol');
|
||||||
|
logger.error('Error message with ✗ symbol');
|
||||||
|
logger.warn('Warning message with ⚠ symbol');
|
||||||
|
logger.info('Info message with ℹ symbol');
|
||||||
|
logger.dim('Dim/secondary text for less important info');
|
||||||
|
logger.highlight('Highlighted/bold text for emphasis');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 2. Colored Boxes ===
|
||||||
|
logger.logBoxTitle('Colored Box Styles', 60);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Boxes can be styled with different colors:');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Success Box (Green)', [
|
||||||
|
'Used for successful operations',
|
||||||
|
'Installation complete, service started, etc.',
|
||||||
|
], 60, 'success');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Error Box (Red)', [
|
||||||
|
'Used for critical errors and failures',
|
||||||
|
'Configuration errors, connection failures, etc.',
|
||||||
|
], 60, 'error');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Warning Box (Yellow)', [
|
||||||
|
'Used for warnings and deprecations',
|
||||||
|
'Old command format, missing config, etc.',
|
||||||
|
], 60, 'warning');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
logger.logBox('Info Box (Cyan)', [
|
||||||
|
'Used for informational messages',
|
||||||
|
'Version info, update available, etc.',
|
||||||
|
], 60, 'info');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 3. Status Symbols ===
|
||||||
|
logger.logBoxTitle('Status Symbols', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.running} Service Running`);
|
||||||
|
logger.logBoxLine(`${symbols.stopped} Service Stopped`);
|
||||||
|
logger.logBoxLine(`${symbols.starting} Service Starting`);
|
||||||
|
logger.logBoxLine(`${symbols.unknown} Status Unknown`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.success} Operation Successful`);
|
||||||
|
logger.logBoxLine(`${symbols.error} Operation Failed`);
|
||||||
|
logger.logBoxLine(`${symbols.warning} Warning Condition`);
|
||||||
|
logger.logBoxLine(`${symbols.info} Information`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 4. Battery Level Colors ===
|
||||||
|
logger.logBoxTitle('Battery Level Color Coding', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Battery levels are color-coded:');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(85)('85%')} - Good (green, ≥60%)`);
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(45)('45%')} - Medium (yellow, 30-60%)`);
|
||||||
|
logger.logBoxLine(` ${getBatteryColor(15)('15%')} - Critical (red, <30%)`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 5. Power Status Formatting ===
|
||||||
|
logger.logBoxTitle('Power Status Formatting', 60, 'info');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('online')}`);
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('onBattery')}`);
|
||||||
|
logger.logBoxLine(`Status: ${formatPowerStatus('unknown')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 6. Table Formatting ===
|
||||||
|
const upsColumns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id' },
|
||||||
|
{ header: 'Name', key: 'name' },
|
||||||
|
{ header: 'Host', key: 'host' },
|
||||||
|
{ header: 'Status', key: 'status', color: (v) => {
|
||||||
|
if (v.includes('Online')) return theme.success(v);
|
||||||
|
if (v.includes('Battery')) return theme.warning(v);
|
||||||
|
return theme.dim(v);
|
||||||
|
}},
|
||||||
|
{ header: 'Battery', key: 'battery', align: 'right', color: (v) => {
|
||||||
|
const pct = parseInt(v);
|
||||||
|
return getBatteryColor(pct)(v);
|
||||||
|
}},
|
||||||
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const upsData = [
|
||||||
|
{
|
||||||
|
id: 'ups-1',
|
||||||
|
name: 'Main UPS',
|
||||||
|
host: '192.168.1.10',
|
||||||
|
status: 'Online',
|
||||||
|
battery: '95%',
|
||||||
|
runtime: '45 min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ups-2',
|
||||||
|
name: 'Backup UPS',
|
||||||
|
host: '192.168.1.11',
|
||||||
|
status: 'On Battery',
|
||||||
|
battery: '42%',
|
||||||
|
runtime: '12 min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ups-3',
|
||||||
|
name: 'Critical UPS',
|
||||||
|
host: '192.168.1.12',
|
||||||
|
status: 'On Battery',
|
||||||
|
battery: '18%',
|
||||||
|
runtime: '5 min',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.logTable(upsColumns, upsData, 'UPS Devices');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 7. Group Table ===
|
||||||
|
const groupColumns: ITableColumn[] = [
|
||||||
|
{ header: 'ID', key: 'id' },
|
||||||
|
{ header: 'Name', key: 'name' },
|
||||||
|
{ header: 'Mode', key: 'mode' },
|
||||||
|
{ header: 'UPS Count', key: 'count', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const groupData = [
|
||||||
|
{ id: 'dc-1', name: 'Data Center 1', mode: 'redundant', count: '3' },
|
||||||
|
{ id: 'office', name: 'Office Servers', mode: 'nonRedundant', count: '2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.logTable(groupColumns, groupData, 'UPS Groups');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 8. Service Status Example ===
|
||||||
|
logger.logBoxTitle('Service Status', 70, 'success');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Status: ${symbols.running} ${theme.statusActive('Active (Running)')}`);
|
||||||
|
logger.logBoxLine(`Enabled: ${symbols.success} ${theme.success('Yes')}`);
|
||||||
|
logger.logBoxLine(`Uptime: 2 days, 5 hours, 23 minutes`);
|
||||||
|
logger.logBoxLine(`PID: ${theme.dim('12345')}`);
|
||||||
|
logger.logBoxLine(`Memory: ${theme.dim('45.2 MB')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 9. Configuration Example ===
|
||||||
|
logger.logBoxTitle('Configuration', 70);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`UPS Devices: ${theme.highlight('3')}`);
|
||||||
|
logger.logBoxLine(`Groups: ${theme.highlight('2')}`);
|
||||||
|
logger.logBoxLine(`Check Interval: ${theme.dim('30 seconds')}`);
|
||||||
|
logger.logBoxLine(`Config File: ${theme.path('/etc/nupst/config.json')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 10. Update Available Example ===
|
||||||
|
logger.logBoxTitle('Update Available', 70, 'warning');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Current Version: ${theme.dim('4.0.1')}`);
|
||||||
|
logger.logBoxLine(`Latest Version: ${theme.highlight('4.0.2')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Run ${theme.command('sudo nupst update')} to update`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === 11. Error Example ===
|
||||||
|
logger.logBoxTitle('Error Example', 70, 'error');
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`${symbols.error} Failed to connect to UPS at 192.168.1.10`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Possible causes:');
|
||||||
|
logger.logBoxLine(` ${theme.dim('• UPS is offline or unreachable')}`);
|
||||||
|
logger.logBoxLine(` ${theme.dim('• Incorrect SNMP community string')}`);
|
||||||
|
logger.logBoxLine(` ${theme.dim('• Firewall blocking port 161')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(`Try: ${theme.command('nupst ups test --debug')}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// === Final Summary ===
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
logger.success('CLI Output Showcase Complete!');
|
||||||
|
logger.dim('All color and formatting features demonstrated');
|
||||||
|
console.log('═'.repeat(80));
|
||||||
|
console.log('');
|
114
ts/cli.ts
114
ts/cli.ts
@@ -1,6 +1,7 @@
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { Nupst } from './nupst.ts';
|
import { Nupst } from './nupst.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
|
import { theme, symbols } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling CLI commands
|
* Class for handling CLI commands
|
||||||
@@ -475,58 +476,83 @@ export class NupstCli {
|
|||||||
* Display help message
|
* Display help message
|
||||||
*/
|
*/
|
||||||
private showHelp(): void {
|
private showHelp(): void {
|
||||||
logger.log(`
|
console.log('');
|
||||||
NUPST - UPS Shutdown Tool
|
logger.highlight('NUPST - UPS Shutdown Tool');
|
||||||
|
logger.dim('Deno-powered UPS monitoring and shutdown automation');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Usage:
|
// Usage section
|
||||||
nupst <command> [options]
|
logger.log(theme.info('Usage:'));
|
||||||
|
logger.log(` ${theme.command('nupst')} ${theme.dim('<command> [options]')}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Commands:
|
// Main commands section
|
||||||
service <subcommand> - Manage systemd service
|
logger.log(theme.info('Commands:'));
|
||||||
ups <subcommand> - Manage UPS devices
|
this.printCommand('service <subcommand>', 'Manage systemd service');
|
||||||
group <subcommand> - Manage UPS groups
|
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
||||||
config [show] - Display current configuration
|
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
||||||
update - Update NUPST from repository (requires root)
|
this.printCommand('config [show]', 'Display current configuration');
|
||||||
uninstall - Completely remove NUPST from system (requires root)
|
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
||||||
help, --help, -h - Show this help message
|
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
||||||
--version, -v - Show version information
|
this.printCommand('help, --help, -h', 'Show this help message');
|
||||||
|
this.printCommand('--version, -v', 'Show version information');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Service Subcommands:
|
// Service subcommands
|
||||||
nupst service enable - Install and enable systemd service (requires root)
|
logger.log(theme.info('Service Subcommands:'));
|
||||||
nupst service disable - Stop and disable systemd service (requires root)
|
this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)'));
|
||||||
nupst service start - Start the systemd service
|
this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)'));
|
||||||
nupst service stop - Stop the systemd service
|
this.printCommand('nupst service start', 'Start the systemd service');
|
||||||
nupst service restart - Restart the systemd service
|
this.printCommand('nupst service stop', 'Stop the systemd service');
|
||||||
nupst service status - Show service and UPS status
|
this.printCommand('nupst service restart', 'Restart the systemd service');
|
||||||
nupst service logs - Show service logs in real-time
|
this.printCommand('nupst service status', 'Show service and UPS status');
|
||||||
nupst service start-daemon - Start daemon process directly
|
this.printCommand('nupst service logs', 'Show service logs in real-time');
|
||||||
|
this.printCommand('nupst service start-daemon', 'Start daemon process directly');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
UPS Subcommands:
|
// UPS subcommands
|
||||||
nupst ups add - Add a new UPS device
|
logger.log(theme.info('UPS Subcommands:'));
|
||||||
nupst ups edit [id] - Edit a UPS device (default if no ID)
|
this.printCommand('nupst ups add', 'Add a new UPS device');
|
||||||
nupst ups remove <id> - Remove a UPS device by ID
|
this.printCommand('nupst ups edit [id]', 'Edit a UPS device (default if no ID)');
|
||||||
nupst ups list (or ls) - List all configured UPS devices
|
this.printCommand('nupst ups remove <id>', 'Remove a UPS device by ID');
|
||||||
nupst ups test - Test UPS connections
|
this.printCommand('nupst ups list (or ls)', 'List all configured UPS devices');
|
||||||
|
this.printCommand('nupst ups test', 'Test UPS connections');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Group Subcommands:
|
// Group subcommands
|
||||||
nupst group add - Add a new UPS group
|
logger.log(theme.info('Group Subcommands:'));
|
||||||
nupst group edit <id> - Edit an existing UPS group
|
this.printCommand('nupst group add', 'Add a new UPS group');
|
||||||
nupst group remove <id> - Remove a UPS group by ID
|
this.printCommand('nupst group edit <id>', 'Edit an existing UPS group');
|
||||||
nupst group list (or ls) - List all UPS groups
|
this.printCommand('nupst group remove <id>', 'Remove a UPS group by ID');
|
||||||
|
this.printCommand('nupst group list (or ls)', 'List all UPS groups');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Options:
|
// Options
|
||||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
logger.log(theme.info('Options:'));
|
||||||
(Example: nupst ups test --debug)
|
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
||||||
|
logger.dim(' (Example: nupst ups test --debug)');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Examples:
|
// Examples
|
||||||
nupst service enable - Install and start the service
|
logger.log(theme.info('Examples:'));
|
||||||
nupst ups add - Add a new UPS interactively
|
logger.dim(' nupst service enable # Install and start the service');
|
||||||
nupst group list - Show all configured groups
|
logger.dim(' nupst ups add # Add a new UPS interactively');
|
||||||
nupst config - Display current configuration
|
logger.dim(' nupst group list # Show all configured groups');
|
||||||
|
logger.dim(' nupst config # Display current configuration');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
Note: Old command format (e.g., 'nupst add') still works but is deprecated.
|
// Note about deprecated commands
|
||||||
Use the new format (e.g., 'nupst ups add') going forward.
|
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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to print a command with description
|
||||||
|
*/
|
||||||
|
private printCommand(command: string, description: string, extra?: string): void {
|
||||||
|
const paddedCommand = command.padEnd(30);
|
||||||
|
logger.log(` ${theme.command(paddedCommand)} ${description}${extra ? ' ' + extra : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
88
ts/colors.ts
Normal file
88
ts/colors.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Color theme and styling utilities for NUPST CLI
|
||||||
|
* Uses Deno standard library colors module
|
||||||
|
*/
|
||||||
|
import * as colors from '@std/fmt/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color theme for consistent CLI styling
|
||||||
|
*/
|
||||||
|
export const theme = {
|
||||||
|
// Message types
|
||||||
|
error: colors.red,
|
||||||
|
warning: colors.yellow,
|
||||||
|
success: colors.green,
|
||||||
|
info: colors.cyan,
|
||||||
|
dim: colors.dim,
|
||||||
|
highlight: colors.bold,
|
||||||
|
|
||||||
|
// Status indicators
|
||||||
|
statusActive: (text: string) => colors.green(colors.bold(text)),
|
||||||
|
statusInactive: (text: string) => colors.red(text),
|
||||||
|
statusWarning: (text: string) => colors.yellow(text),
|
||||||
|
statusUnknown: (text: string) => colors.dim(text),
|
||||||
|
|
||||||
|
// Battery level colors
|
||||||
|
batteryGood: colors.green, // > 60%
|
||||||
|
batteryMedium: colors.yellow, // 30-60%
|
||||||
|
batteryCritical: colors.red, // < 30%
|
||||||
|
|
||||||
|
// Box borders
|
||||||
|
borderSuccess: colors.green,
|
||||||
|
borderError: colors.red,
|
||||||
|
borderWarning: colors.yellow,
|
||||||
|
borderInfo: colors.cyan,
|
||||||
|
borderDefault: (text: string) => text, // No color
|
||||||
|
|
||||||
|
// Command/code highlighting
|
||||||
|
command: colors.cyan,
|
||||||
|
code: colors.dim,
|
||||||
|
path: colors.blue,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status symbols with colors
|
||||||
|
*/
|
||||||
|
export const symbols = {
|
||||||
|
success: colors.green('✓'),
|
||||||
|
error: colors.red('✗'),
|
||||||
|
warning: colors.yellow('⚠'),
|
||||||
|
info: colors.cyan('ℹ'),
|
||||||
|
running: colors.green('●'),
|
||||||
|
stopped: colors.red('○'),
|
||||||
|
starting: colors.yellow('◐'),
|
||||||
|
unknown: colors.dim('◯'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for battery level
|
||||||
|
*/
|
||||||
|
export function getBatteryColor(percentage: number): (text: string) => string {
|
||||||
|
if (percentage >= 60) return theme.batteryGood;
|
||||||
|
if (percentage >= 30) return theme.batteryMedium;
|
||||||
|
return theme.batteryCritical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for runtime remaining
|
||||||
|
*/
|
||||||
|
export function getRuntimeColor(minutes: number): (text: string) => string {
|
||||||
|
if (minutes >= 20) return theme.batteryGood;
|
||||||
|
if (minutes >= 10) return theme.batteryMedium;
|
||||||
|
return theme.batteryCritical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format UPS power status with color
|
||||||
|
*/
|
||||||
|
export function formatPowerStatus(status: 'online' | 'onBattery' | 'unknown'): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'online':
|
||||||
|
return theme.success('Online');
|
||||||
|
case 'onBattery':
|
||||||
|
return theme.warning('On Battery');
|
||||||
|
case 'unknown':
|
||||||
|
default:
|
||||||
|
return theme.dim('Unknown');
|
||||||
|
}
|
||||||
|
}
|
184
ts/daemon.ts
184
ts/daemon.ts
@@ -6,6 +6,7 @@ import { promisify } from 'node:util';
|
|||||||
import { NupstSnmp } from './snmp/manager.ts';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import type { ISnmpConfig } from './snmp/types.ts';
|
import type { ISnmpConfig } from './snmp/types.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
|
import { MigrationRunner } from './migrations/index.ts';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -49,6 +50,8 @@ export interface IGroupConfig {
|
|||||||
* Configuration interface for the daemon
|
* Configuration interface for the daemon
|
||||||
*/
|
*/
|
||||||
export interface INupstConfig {
|
export interface INupstConfig {
|
||||||
|
/** Configuration format version */
|
||||||
|
version?: string;
|
||||||
/** UPS devices configuration */
|
/** UPS devices configuration */
|
||||||
upsDevices: IUpsConfig[];
|
upsDevices: IUpsConfig[];
|
||||||
/** Groups configuration */
|
/** Groups configuration */
|
||||||
@@ -56,10 +59,12 @@ export interface INupstConfig {
|
|||||||
/** Check interval in milliseconds */
|
/** Check interval in milliseconds */
|
||||||
checkInterval: number;
|
checkInterval: number;
|
||||||
|
|
||||||
// Legacy fields for backward compatibility
|
// Legacy fields for backward compatibility (will be migrated away)
|
||||||
/** SNMP configuration settings (legacy) */
|
/** UPS list (v3 format - legacy) */
|
||||||
|
upsList?: IUpsConfig[];
|
||||||
|
/** SNMP configuration settings (v1 format - legacy) */
|
||||||
snmp?: ISnmpConfig;
|
snmp?: ISnmpConfig;
|
||||||
/** Threshold settings (legacy) */
|
/** Threshold settings (v1 format - legacy) */
|
||||||
thresholds?: {
|
thresholds?: {
|
||||||
/** Shutdown when battery below this percentage */
|
/** Shutdown when battery below this percentage */
|
||||||
battery: number;
|
battery: number;
|
||||||
@@ -91,6 +96,7 @@ export class NupstDaemon {
|
|||||||
|
|
||||||
/** Default configuration */
|
/** Default configuration */
|
||||||
private readonly DEFAULT_CONFIG: INupstConfig = {
|
private readonly DEFAULT_CONFIG: INupstConfig = {
|
||||||
|
version: '4.0',
|
||||||
upsDevices: [
|
upsDevices: [
|
||||||
{
|
{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
@@ -153,29 +159,16 @@ export class NupstDaemon {
|
|||||||
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
||||||
const parsedConfig = JSON.parse(configData);
|
const parsedConfig = JSON.parse(configData);
|
||||||
|
|
||||||
// Handle legacy configuration format
|
// Run migrations to upgrade config format if needed
|
||||||
if (!parsedConfig.upsDevices && parsedConfig.snmp) {
|
const migrationRunner = new MigrationRunner();
|
||||||
// Convert legacy format to new format
|
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
||||||
this.config = {
|
|
||||||
upsDevices: [
|
|
||||||
{
|
|
||||||
id: 'default',
|
|
||||||
name: 'Default UPS',
|
|
||||||
snmp: parsedConfig.snmp,
|
|
||||||
thresholds: parsedConfig.thresholds,
|
|
||||||
groups: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
groups: [],
|
|
||||||
checkInterval: parsedConfig.checkInterval,
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.log('Legacy configuration format detected. Converting to multi-UPS format.');
|
// Save migrated config back to disk if any migrations ran
|
||||||
|
if (migrated) {
|
||||||
// Save the new format
|
this.config = migratedConfig;
|
||||||
await this.saveConfig(this.config);
|
await this.saveConfig(this.config);
|
||||||
} else {
|
} else {
|
||||||
this.config = parsedConfig;
|
this.config = migratedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.config;
|
return this.config;
|
||||||
@@ -202,8 +195,17 @@ export class NupstDaemon {
|
|||||||
if (!fs.existsSync(configDir)) {
|
if (!fs.existsSync(configDir)) {
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
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('┌─ Configuration Saved ─────────────────────┐');
|
||||||
console.log(`│ Location: ${this.CONFIG_PATH}`);
|
console.log(`│ Location: ${this.CONFIG_PATH}`);
|
||||||
@@ -353,8 +355,9 @@ export class NupstDaemon {
|
|||||||
logger.log('Starting UPS monitoring...');
|
logger.log('Starting UPS monitoring...');
|
||||||
|
|
||||||
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
||||||
logger.error('No UPS devices found in configuration. Monitoring stopped.');
|
logger.warn('No UPS devices found in configuration. Daemon will remain idle...');
|
||||||
this.isRunning = false;
|
// Don't exit - enter idle monitoring mode instead
|
||||||
|
await this.idleMonitoring();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,6 +893,133 @@ export class NupstDaemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Idle monitoring loop when no UPS devices are configured
|
||||||
|
* Watches for config changes and reloads when detected
|
||||||
|
*/
|
||||||
|
private async idleMonitoring(): Promise<void> {
|
||||||
|
const IDLE_CHECK_INTERVAL = 60000; // Check every 60 seconds
|
||||||
|
let lastConfigCheck = Date.now();
|
||||||
|
const CONFIG_CHECK_INTERVAL = 60000; // Check config every minute
|
||||||
|
|
||||||
|
logger.log('Entering idle monitoring mode...');
|
||||||
|
logger.log('Daemon will check for config changes every 60 seconds');
|
||||||
|
|
||||||
|
// Start file watcher for hot-reload
|
||||||
|
this.watchConfigFile();
|
||||||
|
|
||||||
|
while (this.isRunning) {
|
||||||
|
try {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
// Periodically check if config has been updated
|
||||||
|
if (currentTime - lastConfigCheck >= CONFIG_CHECK_INTERVAL) {
|
||||||
|
try {
|
||||||
|
// Try to load config
|
||||||
|
const newConfig = await this.loadConfig();
|
||||||
|
|
||||||
|
// Check if we now have UPS devices configured
|
||||||
|
if (newConfig.upsDevices && newConfig.upsDevices.length > 0) {
|
||||||
|
logger.success('Configuration updated! UPS devices found. Starting monitoring...');
|
||||||
|
this.initializeUpsStatus();
|
||||||
|
// Exit idle mode and start monitoring
|
||||||
|
await this.monitor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Config still doesn't exist or invalid, continue waiting
|
||||||
|
}
|
||||||
|
|
||||||
|
lastConfigCheck = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sleep(IDLE_CHECK_INTERVAL);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error during idle monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
await this.sleep(IDLE_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('Idle monitoring stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch config file for changes and reload automatically
|
||||||
|
*/
|
||||||
|
private watchConfigFile(): void {
|
||||||
|
try {
|
||||||
|
// Use Deno's file watcher to monitor config file
|
||||||
|
const configDir = path.dirname(this.CONFIG_PATH);
|
||||||
|
|
||||||
|
// Spawn a background watcher (non-blocking)
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const watcher = Deno.watchFs(configDir);
|
||||||
|
|
||||||
|
logger.log('Config file watcher started');
|
||||||
|
|
||||||
|
for await (const event of watcher) {
|
||||||
|
// Only respond to modify events on the config file
|
||||||
|
if (
|
||||||
|
event.kind === 'modify' &&
|
||||||
|
event.paths.some((p) => p.includes('config.json'))
|
||||||
|
) {
|
||||||
|
logger.info('Config file changed, reloading...');
|
||||||
|
await this.reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop watching if daemon stopped
|
||||||
|
if (!this.isRunning) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Watcher error - not critical, just log it
|
||||||
|
logger.dim(
|
||||||
|
`Config watcher stopped: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't start the watcher, just log and continue
|
||||||
|
// The periodic check will still work
|
||||||
|
logger.dim('Could not start config file watcher, using periodic checks only');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload configuration and restart monitoring if needed
|
||||||
|
*/
|
||||||
|
private async reloadConfig(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const oldDeviceCount = this.config.upsDevices?.length || 0;
|
||||||
|
|
||||||
|
// Load the new configuration
|
||||||
|
await this.loadConfig();
|
||||||
|
const newDeviceCount = this.config.upsDevices?.length || 0;
|
||||||
|
|
||||||
|
if (newDeviceCount > 0 && oldDeviceCount === 0) {
|
||||||
|
logger.success(`Configuration reloaded! Found ${newDeviceCount} UPS device(s)`);
|
||||||
|
logger.info('Monitoring will start automatically...');
|
||||||
|
} else if (newDeviceCount !== oldDeviceCount) {
|
||||||
|
logger.success(
|
||||||
|
`Configuration reloaded! UPS devices: ${oldDeviceCount} → ${newDeviceCount}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reinitialize UPS status tracking
|
||||||
|
this.initializeUpsStatus();
|
||||||
|
} else {
|
||||||
|
logger.success('Configuration reloaded successfully');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to reload config: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sleep for the specified milliseconds
|
* Sleep for the specified milliseconds
|
||||||
*/
|
*/
|
||||||
|
220
ts/logger.ts
220
ts/logger.ts
@@ -1,9 +1,38 @@
|
|||||||
|
import { theme, symbols } from './colors.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table column alignment options
|
||||||
|
*/
|
||||||
|
export type TColumnAlign = 'left' | 'right' | 'center';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table column definition
|
||||||
|
*/
|
||||||
|
export interface ITableColumn {
|
||||||
|
/** Column header text */
|
||||||
|
header: string;
|
||||||
|
/** Column key in data object */
|
||||||
|
key: string;
|
||||||
|
/** Column alignment (default: left) */
|
||||||
|
align?: TColumnAlign;
|
||||||
|
/** Column width (auto-calculated if not specified) */
|
||||||
|
width?: number;
|
||||||
|
/** Color function to apply to cell values */
|
||||||
|
color?: (value: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Box style types with colors
|
||||||
|
*/
|
||||||
|
export type TBoxStyle = 'default' | 'success' | 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple logger class that provides consistent formatting for log messages
|
* A simple logger class that provides consistent formatting for log messages
|
||||||
* including support for logboxes with title, lines, and closing
|
* including support for logboxes with title, lines, and closing
|
||||||
*/
|
*/
|
||||||
export class Logger {
|
export class Logger {
|
||||||
private currentBoxWidth: number | null = null;
|
private currentBoxWidth: number | null = null;
|
||||||
|
private currentBoxStyle: TBoxStyle = 'default';
|
||||||
private static instance: Logger;
|
private static instance: Logger;
|
||||||
|
|
||||||
/** Default width to use when no width is specified */
|
/** Default width to use when no width is specified */
|
||||||
@@ -36,36 +65,83 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an error message
|
* Log an error message (red with ✗ symbol)
|
||||||
* @param message Error message to log
|
* @param message Error message to log
|
||||||
*/
|
*/
|
||||||
public error(message: string): void {
|
public error(message: string): void {
|
||||||
console.error(message);
|
console.error(`${symbols.error} ${theme.error(message)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a warning message with a warning emoji
|
* Log a warning message (yellow with ⚠ symbol)
|
||||||
* @param message Warning message to log
|
* @param message Warning message to log
|
||||||
*/
|
*/
|
||||||
public warn(message: string): void {
|
public warn(message: string): void {
|
||||||
console.warn(`⚠️ ${message}`);
|
console.warn(`${symbols.warning} ${theme.warning(message)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a success message with a checkmark
|
* Log a success message (green with ✓ symbol)
|
||||||
* @param message Success message to log
|
* @param message Success message to log
|
||||||
*/
|
*/
|
||||||
public success(message: string): void {
|
public success(message: string): void {
|
||||||
console.log(`✓ ${message}`);
|
console.log(`${symbols.success} ${theme.success(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an info message (cyan with ℹ symbol)
|
||||||
|
* @param message Info message to log
|
||||||
|
*/
|
||||||
|
public info(message: string): void {
|
||||||
|
console.log(`${symbols.info} ${theme.info(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a dim/secondary message
|
||||||
|
* @param message Message to log in dim style
|
||||||
|
*/
|
||||||
|
public dim(message: string): void {
|
||||||
|
console.log(theme.dim(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a highlighted/bold message
|
||||||
|
* @param message Message to highlight
|
||||||
|
*/
|
||||||
|
public highlight(message: string): void {
|
||||||
|
console.log(theme.highlight(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color function for box based on style
|
||||||
|
*/
|
||||||
|
private getBoxColor(style: TBoxStyle): (text: string) => string {
|
||||||
|
switch (style) {
|
||||||
|
case 'success':
|
||||||
|
return theme.borderSuccess;
|
||||||
|
case 'error':
|
||||||
|
return theme.borderError;
|
||||||
|
case 'warning':
|
||||||
|
return theme.borderWarning;
|
||||||
|
case 'info':
|
||||||
|
return theme.borderInfo;
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return theme.borderDefault;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a logbox title and set the current box width
|
* Log a logbox title and set the current box width
|
||||||
* @param title Title of the logbox
|
* @param title Title of the logbox
|
||||||
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
||||||
|
* @param style Box style for coloring (default, success, error, warning, info)
|
||||||
*/
|
*/
|
||||||
public logBoxTitle(title: string, width?: number): void {
|
public logBoxTitle(title: string, width?: number, style?: TBoxStyle): void {
|
||||||
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
||||||
|
this.currentBoxStyle = style || 'default';
|
||||||
|
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Create the title line with appropriate padding
|
// Create the title line with appropriate padding
|
||||||
const paddedTitle = ` ${title} `;
|
const paddedTitle = ` ${title} `;
|
||||||
@@ -74,7 +150,7 @@ export class Logger {
|
|||||||
// Title line: ┌─ Title ───┐
|
// Title line: ┌─ Title ───┐
|
||||||
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
||||||
|
|
||||||
console.log(titleLine);
|
console.log(colorFn(titleLine));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,17 +165,21 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Calculate the available space for content
|
// Calculate the available space for content (use visible length)
|
||||||
const availableSpace = boxWidth - 2; // Account for left and right borders
|
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||||
|
const visibleLen = this.visibleLength(content);
|
||||||
|
|
||||||
if (content.length <= availableSpace - 1) {
|
if (visibleLen <= availableSpace - 1) {
|
||||||
// If content fits with at least one space for the right border stripe
|
// If content fits with at least one space for the right border stripe
|
||||||
const padding = availableSpace - content.length - 1;
|
const padding = availableSpace - visibleLen - 1;
|
||||||
console.log(`│ ${content}${' '.repeat(padding)}│`);
|
const line = `│ ${content}${' '.repeat(padding)}│`;
|
||||||
|
console.log(colorFn(line));
|
||||||
} else {
|
} else {
|
||||||
// Content is too long, let it flow out of boundaries.
|
// Content is too long, let it flow out of boundaries.
|
||||||
console.log(`│ ${content}`);
|
const line = `│ ${content}`;
|
||||||
|
console.log(colorFn(line));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +189,15 @@ export class Logger {
|
|||||||
*/
|
*/
|
||||||
public logBoxEnd(width?: number): void {
|
public logBoxEnd(width?: number): void {
|
||||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Create the bottom border: └────────┘
|
// Create the bottom border: └────────┘
|
||||||
console.log(`└${'─'.repeat(boxWidth - 2)}┘`);
|
const bottomLine = `└${'─'.repeat(boxWidth - 2)}┘`;
|
||||||
|
console.log(colorFn(bottomLine));
|
||||||
|
|
||||||
// Reset the current box width
|
// Reset the current box width and style
|
||||||
this.currentBoxWidth = null;
|
this.currentBoxWidth = null;
|
||||||
|
this.currentBoxStyle = 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,9 +205,10 @@ export class Logger {
|
|||||||
* @param title Title of the logbox
|
* @param title Title of the logbox
|
||||||
* @param lines Array of content lines
|
* @param lines Array of content lines
|
||||||
* @param width Width of the logbox, defaults to DEFAULT_WIDTH
|
* @param width Width of the logbox, defaults to DEFAULT_WIDTH
|
||||||
|
* @param style Box style for coloring
|
||||||
*/
|
*/
|
||||||
public logBox(title: string, lines: string[], width?: number): void {
|
public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void {
|
||||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
|
this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
this.logBoxLine(line);
|
this.logBoxLine(line);
|
||||||
@@ -141,6 +225,108 @@ export class Logger {
|
|||||||
public logDivider(width?: number, character: string = '─'): void {
|
public logDivider(width?: number, character: string = '─'): void {
|
||||||
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip ANSI color codes from string for accurate length calculation
|
||||||
|
*/
|
||||||
|
private stripAnsi(text: string): string {
|
||||||
|
// Remove ANSI escape codes
|
||||||
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get visible length of string (excluding ANSI codes)
|
||||||
|
*/
|
||||||
|
private visibleLength(text: string): number {
|
||||||
|
return this.stripAnsi(text).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align text within a column (handles ANSI color codes correctly)
|
||||||
|
*/
|
||||||
|
private alignText(text: string, width: number, align: TColumnAlign = 'left'): string {
|
||||||
|
const visibleLen = this.visibleLength(text);
|
||||||
|
|
||||||
|
if (visibleLen >= width) {
|
||||||
|
// Text is too long, truncate the visible part
|
||||||
|
const stripped = this.stripAnsi(text);
|
||||||
|
return stripped.substring(0, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = width - visibleLen;
|
||||||
|
|
||||||
|
switch (align) {
|
||||||
|
case 'right':
|
||||||
|
return ' '.repeat(padding) + text;
|
||||||
|
case 'center': {
|
||||||
|
const leftPad = Math.floor(padding / 2);
|
||||||
|
const rightPad = padding - leftPad;
|
||||||
|
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
||||||
|
}
|
||||||
|
case 'left':
|
||||||
|
default:
|
||||||
|
return text + ' '.repeat(padding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a formatted table
|
||||||
|
* @param columns Column definitions
|
||||||
|
* @param rows Array of data objects
|
||||||
|
* @param title Optional table title
|
||||||
|
*/
|
||||||
|
public logTable(columns: ITableColumn[], rows: Record<string, string>[], title?: string): void {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.dim('No data to display');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
const columnWidths = columns.map((col) => {
|
||||||
|
if (col.width) return col.width;
|
||||||
|
|
||||||
|
// Auto-calculate width based on header and data (use visible length)
|
||||||
|
let maxWidth = this.visibleLength(col.header);
|
||||||
|
for (const row of rows) {
|
||||||
|
const value = String(row[col.key] || '');
|
||||||
|
maxWidth = Math.max(maxWidth, this.visibleLength(value));
|
||||||
|
}
|
||||||
|
return maxWidth;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total table width
|
||||||
|
const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + (columns.length * 3) + 1;
|
||||||
|
|
||||||
|
// Print title if provided
|
||||||
|
if (title) {
|
||||||
|
this.logBoxTitle(title, totalWidth);
|
||||||
|
} else {
|
||||||
|
// Print top border
|
||||||
|
console.log('┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print header row
|
||||||
|
const headerCells = columns.map((col, i) =>
|
||||||
|
theme.highlight(this.alignText(col.header, columnWidths[i], col.align))
|
||||||
|
);
|
||||||
|
console.log('│ ' + headerCells.join(' │ ') + ' │');
|
||||||
|
|
||||||
|
// Print separator
|
||||||
|
console.log('├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤');
|
||||||
|
|
||||||
|
// Print data rows
|
||||||
|
for (const row of rows) {
|
||||||
|
const cells = columns.map((col, i) => {
|
||||||
|
const value = String(row[col.key] || '');
|
||||||
|
const aligned = this.alignText(value, columnWidths[i], col.align);
|
||||||
|
return col.color ? col.color(aligned) : aligned;
|
||||||
|
});
|
||||||
|
console.log('│ ' + cells.join(' │ ') + ' │');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print bottom border
|
||||||
|
console.log('└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export a singleton instance for easy use
|
// Export a singleton instance for easy use
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
45
ts/migrations/migration-v3-to-v4.ts
Normal file
45
ts/migrations/migration-v3-to-v4.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { BaseMigration } from './base-migration.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from v3 (upsList) to v4 (upsDevices)
|
||||||
|
*
|
||||||
|
* Detects v3 format:
|
||||||
|
* {
|
||||||
|
* upsList: [ ... ],
|
||||||
|
* groups: [ ... ],
|
||||||
|
* checkInterval: 30000
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Converts to:
|
||||||
|
* {
|
||||||
|
* version: "4.0",
|
||||||
|
* upsDevices: [ ... ], // renamed from upsList
|
||||||
|
* groups: [ ... ],
|
||||||
|
* checkInterval: 30000
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
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 instead of upsDevices
|
||||||
|
return !!config.upsList && !config.upsDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(config: any): Promise<any> {
|
||||||
|
logger.info(`${this.getName()}: Renaming upsList to upsDevices...`);
|
||||||
|
|
||||||
|
const migrated = {
|
||||||
|
version: this.toVersion,
|
||||||
|
upsDevices: config.upsList, // Rename upsList → upsDevices
|
||||||
|
groups: config.groups || [],
|
||||||
|
checkInterval: config.checkInterval || 30000,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.success(`${this.getName()}: Migration complete`);
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
}
|
@@ -15,17 +15,17 @@ export class NupstSystemd {
|
|||||||
|
|
||||||
/** Template for the systemd service file */
|
/** Template for the systemd service file */
|
||||||
private readonly serviceTemplate = `[Unit]
|
private readonly serviceTemplate = `[Unit]
|
||||||
Description=Node.js UPS Shutdown Tool for Multiple UPS Devices
|
Description=NUPST - Deno-powered UPS Monitoring Tool
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/opt/nupst/bin/nupst daemon-start
|
ExecStart=/usr/local/bin/nupst service start-daemon
|
||||||
Restart=always
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
User=root
|
User=root
|
||||||
Group=root
|
Group=root
|
||||||
Environment=PATH=/usr/bin:/usr/local/bin
|
Environment=PATH=/usr/bin:/usr/local/bin
|
||||||
Environment=NODE_ENV=production
|
WorkingDirectory=/opt/nupst
|
||||||
WorkingDirectory=/tmp
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
Reference in New Issue
Block a user