Compare commits

...

17 Commits

Author SHA1 Message Date
9ba50da73c 5.1.0
Some checks failed
Release / build-and-release (push) Failing after 3s
2025-10-22 14:18:09 +00:00
684319983d feat(packaging): Add npm packaging and installer: wrapper, postinstall downloader, publish workflow, and packaging files 2025-10-22 14:18:09 +00:00
18bd9f6cda fix(install): add error checking for binary move and chmod operations
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 47s
CI / Build All Platforms (push) Successful in 50s
- Check if mv command succeeds
- Verify binary exists after move
- Check if chmod succeeds
- Exit with error instead of continuing on failure
2025-10-20 13:33:00 +00:00
f03c683d02 fix(install): correct installation order for updates
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 42s
CI / Build All Platforms (push) Successful in 47s
- Stop service first
- Remove /opt/nupst
- Create fresh directory
- Download binary
- Ensures clean installation without leaving empty directories
2025-10-20 13:28:56 +00:00
f750299780 fix(install): simplify installation to only binary in /opt/nupst
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 46s
CI / Build All Platforms (push) Successful in 48s
- Remove all conditional migration logic
- Always completely clean /opt/nupst before installation
- Ensures only NUPST binary exists in installation directory
- Simplified service restart logic
2025-10-20 13:24:03 +00:00
ca1039408d chore(release): bump version to 5.0.2
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 42s
CI / Build All Platforms (push) Successful in 47s
2025-10-20 13:09:20 +00:00
df3e0b9424 fix: import process from node:process in script-action
Some checks failed
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 5s
CI / Build All Platforms (push) Has been cancelled
Fixes TS2580 error where process was undefined
2025-10-20 13:08:43 +00:00
c8e5960abd chore(release): bump version to 5.0.1
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 45s
CI / Build All Platforms (push) Successful in 48s
2025-10-20 13:07:07 +00:00
7304a62357 chore(release): bump version to 5.0.0
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 41s
CI / Build All Platforms (push) Successful in 47s
BREAKING CHANGE: Deprecated CLI commands removed

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

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

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

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

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

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

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

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

The config file watcher (daemon.ts:986) uses Deno.watchFs() to monitor
/etc/nupst/config.json and automatically calls reloadConfig() when changes
are detected. No restart needed.
2025-10-20 12:42:31 +00:00
17 changed files with 1377 additions and 607 deletions

183
.github/workflows/npm-publish.yml vendored Normal file
View File

@@ -0,0 +1,183 @@
name: Publish to npm
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g., 5.0.6)'
required: true
type: string
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
# Checkout the repository
- name: Checkout code
uses: actions/checkout@v4
# Setup Deno
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
# Setup Node.js for npm publishing
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org/'
# Compile binaries for all platforms
- name: Compile binaries
run: |
echo "Compiling binaries for all platforms..."
deno task compile
echo ""
echo "Binary sizes:"
ls -lh dist/binaries/
# Update version in package.json if triggered manually
- name: Update version in package.json
if: github.event_name == 'workflow_dispatch'
run: |
VERSION=${{ github.event.inputs.version }}
echo "Updating package.json to version ${VERSION}"
npm version ${VERSION} --no-git-tag-version
# Extract version from tag if triggered by tag push
- name: Extract version from tag
if: startsWith(github.ref, 'refs/tags/')
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "Extracted version: ${VERSION}"
# Ensure versions are synchronized
- name: Sync versions
run: |
if [ -n "${VERSION}" ]; then
echo "Syncing version ${VERSION} across files..."
# Update deno.json
sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" deno.json
# Update package.json
npm version ${VERSION} --no-git-tag-version --allow-same-version
echo "Updated versions:"
echo "deno.json: $(grep '"version"' deno.json)"
echo "package.json: $(grep '"version"' package.json | head -1)"
fi
# Generate SHA256 checksums for binaries
- name: Generate checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS
echo "Checksums generated:"
cat SHA256SUMS
cd ../..
# Create npm package
- name: Create npm package
run: |
echo "Creating npm package..."
npm pack
echo ""
echo "Package created:"
ls -lh *.tgz
# Test package installation locally
- name: Test local installation
run: |
echo "Testing local package installation..."
PACKAGE_FILE=$(ls *.tgz)
npm install -g ${PACKAGE_FILE}
echo ""
echo "Testing nupst command:"
nupst --version || echo "Note: Binary execution may fail in CI environment"
echo ""
echo "Checking installed files:"
npm ls -g @serve.zone/nupst
# Publish to npm (only on tag push or manual trigger)
- name: Publish to npm
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "Publishing to npm registry..."
npm publish --access public
echo ""
echo "✅ Successfully published @serve.zone/nupst to npm!"
echo ""
echo "Package info:"
npm view @serve.zone/nupst
# Create GitHub Release (only on tag push)
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: |
dist/binaries/nupst-*
dist/binaries/SHA256SUMS
*.tgz
generate_release_notes: true
body: |
## NUPST ${{ env.VERSION }}
### Installation
#### Via npm (recommended)
```bash
npm install -g @serve.zone/nupst
```
#### Direct download
Download the appropriate binary for your platform from the assets below.
### Platform Support
- Linux x64 / ARM64
- macOS x64 / ARM64 (Apple Silicon)
- Windows x64
### Checksums
SHA256 checksums are available in `SHA256SUMS` file.
# Verify the published package
verify:
needs: build-and-publish
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
- name: Wait for npm propagation
run: sleep 30
- name: Verify npm package
run: |
echo "Verifying published package..."
npm view @serve.zone/nupst
echo ""
echo "Testing installation from npm:"
npm install -g @serve.zone/nupst
echo ""
echo "Package installed successfully!"
which nupst || echo "Binary location check skipped"

54
.npmignore Normal file
View File

@@ -0,0 +1,54 @@
# Source code (not needed for binary distribution)
/ts/
/test/
mod.ts
*.ts
# Development files
.git/
.gitea/
.claude/
.serena/
.nogit/
.github/
deno.json
deno.lock
tsconfig.json
# Scripts not needed for npm
/scripts/compile-all.sh
install.sh
uninstall.sh
example-action.sh
# Documentation files not needed for npm package
readme.plan.md
readme.hints.md
npm-publish-instructions.md
docs/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Keep only the install-binary.js in scripts/
/scripts/*
!/scripts/install-binary.js
# Exclude all dist directory (binaries will be downloaded during install)
/dist/
# Logs and temporary files
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Other
node_modules/
.env
.env.*

108
bin/nupst-wrapper.js Normal file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* NUPST npm wrapper
* This script executes the appropriate pre-compiled binary based on the current platform
*/
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
import { platform, arch } from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Get the binary name for the current platform
*/
function getBinaryName() {
const plat = platform();
const architecture = arch();
// Map Node's platform/arch to our binary naming
const platformMap = {
'darwin': 'macos',
'linux': 'linux',
'win32': 'windows'
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64'
};
const mappedPlatform = platformMap[plat];
const mappedArch = archMap[architecture];
if (!mappedPlatform || !mappedArch) {
console.error(`Error: Unsupported platform/architecture: ${plat}/${architecture}`);
console.error('Supported platforms: Linux, macOS, Windows');
console.error('Supported architectures: x64, arm64');
process.exit(1);
}
// Construct binary name
let binaryName = `nupst-${mappedPlatform}-${mappedArch}`;
if (plat === 'win32') {
binaryName += '.exe';
}
return binaryName;
}
/**
* Execute the binary
*/
function executeBinary() {
const binaryName = getBinaryName();
const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName);
// Check if binary exists
if (!existsSync(binaryPath)) {
console.error(`Error: Binary not found at ${binaryPath}`);
console.error('This might happen if:');
console.error('1. The postinstall script failed to run');
console.error('2. The platform is not supported');
console.error('3. The package was not installed correctly');
console.error('');
console.error('Try reinstalling the package:');
console.error(' npm uninstall -g @serve.zone/nupst');
console.error(' npm install -g @serve.zone/nupst');
process.exit(1);
}
// Spawn the binary with all arguments passed through
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
shell: false
});
// Handle child process events
child.on('error', (err) => {
console.error(`Error executing nupst: ${err.message}`);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
});
// Forward signals to child process
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
signals.forEach(signal => {
process.on(signal, () => {
if (!child.killed) {
child.kill(signal);
}
});
});
}
// Execute
executeBinary();

View File

@@ -1,5 +1,16 @@
# Changelog # Changelog
## 2025-10-22 - 5.1.0 - feat(packaging)
Add npm packaging and installer: wrapper, postinstall downloader, publish workflow, and packaging files
- Add package.json (v5.0.5) and npm packaging metadata to publish @serve.zone/nupst
- Include a small Node.js wrapper (bin/nupst-wrapper.js) to execute platform-specific precompiled binaries
- Add postinstall script (scripts/install-binary.js) that downloads the correct binary for the current platform and sets executable permissions
- Add GitHub Actions workflow (.github/workflows/npm-publish.yml) to build binaries, pack and publish to npm, and create releases
- Add .npmignore to keep source, tests and dev files out of npm package; keep only runtime installer and wrapper
- Move example action script into docs (docs/example-action.sh) and remove the top-level example-action.sh
- Include generated npm package artifact (serve.zone-nupst-5.0.5.tgz) and npmextra.json
## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime ## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime
**MAJOR RELEASE: NUPST v4.0 is a complete rewrite powered by Deno** **MAJOR RELEASE: NUPST v4.0 is a complete rewrite powered by Deno**

View File

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

View File

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

1
npmextra.json Normal file
View File

@@ -0,0 +1 @@
{}

62
package.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "@serve.zone/nupst",
"version": "5.1.0",
"description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies",
"keywords": [
"ups",
"snmp",
"power",
"shutdown",
"monitoring",
"cyberpower",
"apc",
"eaton",
"tripplite",
"liebert",
"vertiv",
"battery",
"backup"
],
"homepage": "https://code.foss.global/serve.zone/nupst",
"bugs": {
"url": "https://code.foss.global/serve.zone/nupst/issues"
},
"repository": {
"type": "git",
"url": "git+https://code.foss.global/serve.zone/nupst.git"
},
"author": "Serve Zone",
"license": "MIT",
"type": "module",
"bin": {
"nupst": "./bin/nupst-wrapper.js"
},
"scripts": {
"postinstall": "node scripts/install-binary.js",
"prepublishOnly": "echo 'Publishing NUPST binaries to npm...'",
"test": "echo 'Tests are run with Deno: deno task test'"
},
"files": [
"bin/",
"scripts/install-binary.js",
"readme.md",
"license",
"changelog.md"
],
"engines": {
"node": ">=14.0.0"
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}

1022
readme.md

File diff suppressed because it is too large Load Diff

231
scripts/install-binary.js Normal file
View File

@@ -0,0 +1,231 @@
#!/usr/bin/env node
/**
* NUPST npm postinstall script
* Downloads the appropriate binary for the current platform from GitHub releases
*/
import { platform, arch } from 'os';
import { existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import https from 'https';
import { pipeline } from 'stream';
import { promisify } from 'util';
import { createWriteStream } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const streamPipeline = promisify(pipeline);
// Configuration
const REPO_BASE = 'https://code.foss.global/serve.zone/nupst';
const VERSION = process.env.npm_package_version || '5.0.5';
function getBinaryInfo() {
const plat = platform();
const architecture = arch();
const platformMap = {
'darwin': 'macos',
'linux': 'linux',
'win32': 'windows'
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64'
};
const mappedPlatform = platformMap[plat];
const mappedArch = archMap[architecture];
if (!mappedPlatform || !mappedArch) {
return { supported: false, platform: plat, arch: architecture };
}
let binaryName = `nupst-${mappedPlatform}-${mappedArch}`;
if (plat === 'win32') {
binaryName += '.exe';
}
return {
supported: true,
platform: mappedPlatform,
arch: mappedArch,
binaryName,
originalPlatform: plat
};
}
function downloadFile(url, destination) {
return new Promise((resolve, reject) => {
console.log(`Downloading from: ${url}`);
// Follow redirects
const download = (url, redirectCount = 0) => {
if (redirectCount > 5) {
reject(new Error('Too many redirects'));
return;
}
https.get(url, (response) => {
if (response.statusCode === 301 || response.statusCode === 302) {
console.log(`Following redirect to: ${response.headers.location}`);
download(response.headers.location, redirectCount + 1);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`));
return;
}
const totalSize = parseInt(response.headers['content-length'], 10);
let downloadedSize = 0;
let lastProgress = 0;
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const progress = Math.round((downloadedSize / totalSize) * 100);
// Only log every 10% to reduce noise
if (progress >= lastProgress + 10) {
console.log(`Download progress: ${progress}%`);
lastProgress = progress;
}
});
const file = createWriteStream(destination);
pipeline(response, file, (err) => {
if (err) {
reject(err);
} else {
console.log('Download complete!');
resolve();
}
});
}).on('error', reject);
};
download(url);
});
}
async function main() {
console.log('===========================================');
console.log(' NUPST - Binary Installation');
console.log('===========================================');
console.log('');
const binaryInfo = getBinaryInfo();
if (!binaryInfo.supported) {
console.error(`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`);
console.error('');
console.error('Supported platforms:');
console.error(' • Linux (x64, arm64)');
console.error(' • macOS (x64, arm64)');
console.error(' • Windows (x64)');
console.error('');
console.error('If you believe your platform should be supported, please file an issue:');
console.error(' https://code.foss.global/serve.zone/nupst/issues');
process.exit(1);
}
console.log(`Platform: ${binaryInfo.platform} (${binaryInfo.originalPlatform})`);
console.log(`Architecture: ${binaryInfo.arch}`);
console.log(`Binary: ${binaryInfo.binaryName}`);
console.log(`Version: ${VERSION}`);
console.log('');
// Create dist/binaries directory if it doesn't exist
const binariesDir = join(__dirname, '..', 'dist', 'binaries');
if (!existsSync(binariesDir)) {
console.log('Creating binaries directory...');
mkdirSync(binariesDir, { recursive: true });
}
const binaryPath = join(binariesDir, binaryInfo.binaryName);
// Check if binary already exists and skip download
if (existsSync(binaryPath)) {
console.log('✓ Binary already exists, skipping download');
} else {
// Construct download URL
// Try release URL first, fall back to raw branch if needed
const releaseUrl = `${REPO_BASE}/releases/download/v${VERSION}/${binaryInfo.binaryName}`;
const fallbackUrl = `${REPO_BASE}/raw/branch/main/dist/binaries/${binaryInfo.binaryName}`;
console.log('Downloading platform-specific binary...');
console.log('This may take a moment depending on your connection speed.');
console.log('');
try {
// Try downloading from release
await downloadFile(releaseUrl, binaryPath);
} catch (err) {
console.log(`Release download failed: ${err.message}`);
console.log('Trying fallback URL...');
try {
// Try fallback URL
await downloadFile(fallbackUrl, binaryPath);
} catch (fallbackErr) {
console.error(`❌ Error: Failed to download binary`);
console.error(` Primary URL: ${releaseUrl}`);
console.error(` Fallback URL: ${fallbackUrl}`);
console.error('');
console.error('This might be because:');
console.error('1. The release has not been created yet');
console.error('2. Network connectivity issues');
console.error('3. The version specified does not exist');
console.error('');
console.error('You can try:');
console.error('1. Installing from source: https://code.foss.global/serve.zone/nupst');
console.error('2. Downloading the binary manually from the releases page');
console.error('3. Using the install script: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash');
// Clean up partial download
if (existsSync(binaryPath)) {
unlinkSync(binaryPath);
}
process.exit(1);
}
}
console.log(`✓ Binary downloaded successfully`);
}
// On Unix-like systems, ensure the binary is executable
if (binaryInfo.originalPlatform !== 'win32') {
try {
console.log('Setting executable permissions...');
chmodSync(binaryPath, 0o755);
console.log('✓ Binary permissions updated');
} catch (err) {
console.error(`⚠️ Warning: Could not set executable permissions: ${err.message}`);
console.error(' You may need to manually run:');
console.error(` chmod +x ${binaryPath}`);
}
}
console.log('');
console.log('✅ NUPST installation completed successfully!');
console.log('');
console.log('You can now use NUPST by running:');
console.log(' nupst --help');
console.log('');
console.log('For initial setup, run:');
console.log(' sudo nupst ups add');
console.log('');
console.log('===========================================');
}
// Run the installation
main().catch(err => {
console.error(`❌ Installation failed: ${err.message}`);
process.exit(1);
});

BIN
serve.zone-nupst-5.0.5.tgz Normal file

Binary file not shown.

View File

@@ -1,10 +1,8 @@
/** /**
* commitinfo - reads version from deno.json * autocreated commitinfo by @push.rocks/commitinfo
*/ */
import denoConfig from '../deno.json' with { type: 'json' };
export const commitinfo = { export const commitinfo = {
name: denoConfig.name, name: '@serve.zone/nupst',
version: denoConfig.version, version: '5.1.0',
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)', description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
}; }

View File

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

View File

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

View File

@@ -152,10 +152,7 @@ export class ActionHandler {
logger.log(''); logger.log('');
logger.success(`Action added to ${targetType} ${targetName}`); logger.success(`Action added to ${targetType} ${targetName}`);
logger.log(''); logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
logger.log(
` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`,
);
logger.log(''); logger.log('');
} finally { } finally {
rl.close(); rl.close();
@@ -241,10 +238,7 @@ export class ActionHandler {
` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`, ` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
); );
} }
logger.log(''); logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
logger.log(
` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`,
);
logger.log(''); logger.log('');
} catch (error) { } catch (error) {
logger.error( logger.error(

View File

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