Compare commits

...

3 Commits

Author SHA1 Message Date
a87710144c fix(migration): detect flat structure in upsDevices for proper v3→v4 migration
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
CI / Build All Platforms (push) Successful in 52s
Release / build-and-release (push) Successful in 50s
The previous migration only checked for upsList field, but saveConfig()
strips upsList when saving, creating a race condition. If the daemon
restarted with a partially-migrated config (upsDevices with flat structure),
the migration wouldn't run because it only looked for upsList.

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

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

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

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

Benefits:
- Works in all environments (piped, non-interactive, containers)
- Fixes nupst update command
- Cleaner user experience
- Matches standard install script patterns (homebrew, rustup, etc.)
2025-10-19 21:37:41 +00:00
fb4d776bdd fix(migration): properly transform v3 flat structure to v4 nested snmp config
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 8s
Release / build-and-release (push) Successful in 48s
CI / Build All Platforms (push) Successful in 51s
The v3→v4 migration was only renaming upsList to upsDevices without
transforming the device structure. V3 had a flat structure with SNMP
fields directly on the device object, while v4 expects a nested 'snmp'
object.

This commit fixes the migration to:
- Move host, port, community, version, etc. into nested snmp object
- Convert version from string to number
- Add default timeout (5000ms)
- Create thresholds object with defaults
- Preserve all SNMPv1, v2c, and v3 authentication fields

Also includes install.sh fix for better non-interactive handling.
2025-10-19 21:32:55 +00:00
5 changed files with 262 additions and 112 deletions

View File

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

View File

@@ -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
echo ""

View File

@@ -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

View 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 ""

View File

@@ -4,19 +4,38 @@ import { logger } from '../logger.ts';
/**
* Migration from v3 (upsList) to v4 (upsDevices)
*
* Detects v3 format:
* Transforms v3 format with flat SNMP config:
* {
* upsList: [ ... ],
* groups: [ ... ],
* checkInterval: 30000
* upsList: [
* {
* id: "ups-1",
* name: "UPS 1",
* host: "192.168.1.1",
* port: 161,
* community: "public",
* version: "1" // string
* }
* ]
* }
*
* Converts to:
* To v4 format with nested SNMP config:
* {
* version: "4.0",
* upsDevices: [ ... ], // renamed from upsList
* groups: [ ... ],
* checkInterval: 30000
* 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 {
@@ -25,21 +44,76 @@ export class MigrationV3ToV4 extends BaseMigration {
readonly toVersion = '4.0';
async shouldRun(config: any): Promise<boolean> {
// V3 format has upsList instead of upsDevices
return !!config.upsList && !config.upsDevices;
// 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()}: Renaming upsList to upsDevices...`);
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: config.upsList, // Rename upsList → upsDevices
upsDevices: transformedDevices,
groups: config.groups || [],
checkInterval: config.checkInterval || 30000,
};
logger.success(`${this.getName()}: Migration complete`);
logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`);
return migrated;
}
}