Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60eadaf6a1 | |||
| bd52ba4cb2 | |||
| a3d6a8b75d | |||
| fbd71b1f3b | |||
| 6481572981 | |||
| 0dc14a6ea1 | |||
| dea344e6ba | |||
| f81f5957ab | |||
| 281d3fbbeb | |||
| c1cb136a7d | |||
| b80275a594 | |||
| b64a515c94 | |||
| 68c4eb6480 | |||
| 6c8f6ac33f | |||
| ffa491c7a1 | |||
| 777d48d82e | |||
| b7a0bbcf6d | |||
| fbe1cd64cb | |||
| 9ba50da73c | |||
| 684319983d | |||
| 18bd9f6cda | |||
| f03c683d02 | |||
| f750299780 | |||
| ca1039408d | |||
| df3e0b9424 |
31
.gitea/release-template.md
Normal file
31
.gitea/release-template.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## NUPST {{VERSION}}
|
||||||
|
|
||||||
|
Pre-compiled binaries for multiple platforms.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Option 1: Via npm (recommended)
|
||||||
|
```bash
|
||||||
|
npm install -g @serve.zone/nupst
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: Via installer script
|
||||||
|
```bash
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: Direct binary download
|
||||||
|
Download the appropriate binary for your platform from the assets below and make it executable.
|
||||||
|
|
||||||
|
### Supported Platforms
|
||||||
|
- Linux x86_64 (x64)
|
||||||
|
- Linux ARM64 (aarch64)
|
||||||
|
- macOS x86_64 (Intel)
|
||||||
|
- macOS ARM64 (Apple Silicon)
|
||||||
|
- Windows x86_64
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
SHA256 checksums are provided in `SHA256SUMS.txt` for binary verification.
|
||||||
|
|
||||||
|
### npm Package
|
||||||
|
The npm package includes automatic binary detection and installation for your platform.
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Set up Deno
|
- name: Set up Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.x
|
deno-version: v2.x
|
||||||
|
|
||||||
- name: Check TypeScript types
|
- name: Check TypeScript types
|
||||||
run: deno check mod.ts
|
run: deno check mod.ts
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
- name: Set up Deno
|
- name: Set up Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.x
|
deno-version: v2.x
|
||||||
|
|
||||||
- name: Compile for current platform
|
- name: Compile for current platform
|
||||||
run: |
|
run: |
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
- name: Set up Deno
|
- name: Set up Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.x
|
deno-version: v2.x
|
||||||
|
|
||||||
- name: Compile all platform binaries
|
- name: Compile all platform binaries
|
||||||
run: bash scripts/compile-all.sh
|
run: bash scripts/compile-all.sh
|
||||||
|
|||||||
129
.gitea/workflows/npm-publish.yml
Normal file
129
.gitea/workflows/npm-publish.yml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
name: Publish to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
npm-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Deno
|
||||||
|
uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: v2.x
|
||||||
|
|
||||||
|
- name: Setup Node.js for npm publishing
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org/'
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Publishing version: $VERSION"
|
||||||
|
|
||||||
|
- name: Verify deno.json version matches tag
|
||||||
|
run: |
|
||||||
|
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
|
||||||
|
TAG_VERSION="${{ steps.version.outputs.version_number }}"
|
||||||
|
echo "deno.json version: $DENO_VERSION"
|
||||||
|
echo "Tag version: $TAG_VERSION"
|
||||||
|
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
|
||||||
|
echo "ERROR: Version mismatch!"
|
||||||
|
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Compile binaries for npm package
|
||||||
|
run: |
|
||||||
|
echo "Compiling binaries for npm package..."
|
||||||
|
deno task compile
|
||||||
|
echo ""
|
||||||
|
echo "Binary sizes:"
|
||||||
|
ls -lh dist/binaries/
|
||||||
|
|
||||||
|
- name: Generate SHA256 checksums
|
||||||
|
run: |
|
||||||
|
cd dist/binaries
|
||||||
|
sha256sum * > SHA256SUMS
|
||||||
|
cat SHA256SUMS
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
- name: Sync package.json version
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version_number }}"
|
||||||
|
echo "Syncing package.json to version ${VERSION}..."
|
||||||
|
npm version ${VERSION} --no-git-tag-version --allow-same-version
|
||||||
|
echo "package.json version: $(grep '"version"' package.json | head -1)"
|
||||||
|
|
||||||
|
- name: Create npm package
|
||||||
|
run: |
|
||||||
|
echo "Creating npm package..."
|
||||||
|
npm pack
|
||||||
|
echo ""
|
||||||
|
echo "Package created:"
|
||||||
|
ls -lh *.tgz
|
||||||
|
|
||||||
|
- 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 || true
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Verify npm package
|
||||||
|
run: |
|
||||||
|
echo "Waiting for npm propagation..."
|
||||||
|
sleep 30
|
||||||
|
echo ""
|
||||||
|
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"
|
||||||
|
|
||||||
|
- name: Publish Summary
|
||||||
|
run: |
|
||||||
|
echo "================================================"
|
||||||
|
echo " npm Publish Complete!"
|
||||||
|
echo "================================================"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Package: @serve.zone/nupst"
|
||||||
|
echo "✅ Version: ${{ steps.version.outputs.version }}"
|
||||||
|
echo ""
|
||||||
|
echo "Installation:"
|
||||||
|
echo " npm install -g @serve.zone/nupst"
|
||||||
|
echo ""
|
||||||
|
echo "Registry:"
|
||||||
|
echo " https://www.npmjs.com/package/@serve.zone/nupst"
|
||||||
|
echo ""
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: Set up Deno
|
- name: Set up Deno
|
||||||
uses: denoland/setup-deno@v1
|
uses: denoland/setup-deno@v1
|
||||||
with:
|
with:
|
||||||
deno-version: v1.x
|
deno-version: v2.x
|
||||||
|
|
||||||
- name: Get version from tag
|
- name: Get version from tag
|
||||||
id: version
|
id: version
|
||||||
|
|||||||
54
.npmignore
Normal file
54
.npmignore
Normal 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
108
bin/nupst-wrapper.js
Normal 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();
|
||||||
39
changelog.md
39
changelog.md
@@ -1,5 +1,44 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-23 - 5.1.10 - fix(config)
|
||||||
|
Synchronize deno.json version with package.json, tidy formatting, and add local tooling settings
|
||||||
|
|
||||||
|
- Bumped deno.json version to 5.1.9 to match package.json/commitinfo
|
||||||
|
- Reformatted deno.json arrays (lint, fmt, compilerOptions) for readability
|
||||||
|
- Added .claude/settings.local.json for local development/tooling permissions (no runtime behaviour changes)
|
||||||
|
|
||||||
|
## 2025-10-23 - 5.1.9 - fix(dev)
|
||||||
|
Add local assistant permissions/settings file (.claude/settings.local.json)
|
||||||
|
|
||||||
|
- Added .claude/settings.local.json containing local assistant permission configuration used for development tasks (deno check, deno lint/format, npm/pack, running packaged binaries, etc.)
|
||||||
|
- This is a development/local configuration file and does not change runtime behavior or product code paths
|
||||||
|
- Patch version bump recommended
|
||||||
|
|
||||||
|
## 2025-10-23 - 5.1.2 - fix(scripts)
|
||||||
|
Add build script to package.json and include local dev tool settings
|
||||||
|
|
||||||
|
- Add a 'build' script to package.json (no-op placeholder) to provide an explicit build step
|
||||||
|
- Minor scripts section formatting tidy in package.json
|
||||||
|
- Add a hidden local settings file for development tooling permissions to the repository (local-only configuration)
|
||||||
|
|
||||||
|
## 2025-10-23 - 5.1.1 - fix(tooling)
|
||||||
|
Add .claude/settings.local.json with local automation permissions
|
||||||
|
|
||||||
|
- Add .claude/settings.local.json to specify allowed permissions for local automated tasks
|
||||||
|
- Grants permissions for various developer/CI actions (deno check/lint/fmt, npm/npm pack, selective Bash commands, WebFetch to docs.deno.com and code.foss.global, and file/read/replace helpers)
|
||||||
|
- This is a developer/local tooling config only and does not change runtime code or package behavior
|
||||||
|
|
||||||
|
## 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**
|
||||||
|
|||||||
11
deno.json
11
deno.json
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "5.0.1",
|
"version": "5.1.10",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-all mod.ts",
|
"dev": "deno run --allow-all mod.ts",
|
||||||
"compile": "deno task compile:all",
|
"compile": "deno task compile:all",
|
||||||
@@ -14,7 +15,9 @@
|
|||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"tags": ["recommended"]
|
"tags": [
|
||||||
|
"recommended"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fmt": {
|
"fmt": {
|
||||||
@@ -25,7 +28,9 @@
|
|||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["deno.window"],
|
"lib": [
|
||||||
|
"deno.window"
|
||||||
|
],
|
||||||
"strict": true
|
"strict": true
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
96
install.sh
96
install.sh
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# NUPST Installer Script (v4.0+)
|
# NUPST Installer Script (v5.0+)
|
||||||
# Downloads and installs pre-compiled NUPST binary from Gitea releases
|
# Downloads and installs pre-compiled NUPST binary from Gitea releases
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
#
|
#
|
||||||
# With version specification:
|
# With version specification:
|
||||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0
|
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0
|
||||||
#
|
#
|
||||||
# Options:
|
# Options:
|
||||||
# -h, --help Show this help message
|
# -h, --help Show this help message
|
||||||
@@ -48,14 +48,14 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ $SHOW_HELP -eq 1 ]; then
|
if [ $SHOW_HELP -eq 1 ]; then
|
||||||
echo "NUPST Installer Script (v4.0+)"
|
echo "NUPST Installer Script (v5.0+)"
|
||||||
echo "Downloads and installs pre-compiled NUPST binary"
|
echo "Downloads and installs pre-compiled NUPST binary"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Usage: $0 [options]"
|
echo "Usage: $0 [options]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " -h, --help Show this help message"
|
echo " -h, --help Show this help message"
|
||||||
echo " --version VERSION Install specific version (e.g., v4.0.0)"
|
echo " --version VERSION Install specific version (e.g., v5.0.0)"
|
||||||
echo " --install-dir DIR Installation directory (default: /opt/nupst)"
|
echo " --install-dir DIR Installation directory (default: /opt/nupst)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
@@ -63,7 +63,7 @@ if [ $SHOW_HELP -eq 1 ]; then
|
|||||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||||
echo ""
|
echo ""
|
||||||
echo " # Install specific version"
|
echo " # Install specific version"
|
||||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0"
|
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ get_latest_version() {
|
|||||||
|
|
||||||
# Main installation process
|
# Main installation process
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo " NUPST Installation Script (v4.0+)"
|
echo " NUPST Installation Script (v5.0+)"
|
||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -169,51 +169,26 @@ DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BIN
|
|||||||
echo "Download URL: $DOWNLOAD_URL"
|
echo "Download URL: $DOWNLOAD_URL"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if installation directory exists
|
# Check if service is running and stop it
|
||||||
SERVICE_WAS_RUNNING=0
|
SERVICE_WAS_RUNNING=0
|
||||||
OLD_NODE_INSTALL=0
|
if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
# Clean up old Node.js installation files
|
|
||||||
if [ $OLD_NODE_INSTALL -eq 1 ]; then
|
|
||||||
echo "Cleaning up old Node.js installation files..."
|
|
||||||
rm -rf "$INSTALL_DIR/node_modules" 2>/dev/null || true
|
|
||||||
rm -rf "$INSTALL_DIR/vendor" 2>/dev/null || true
|
|
||||||
rm -rf "$INSTALL_DIR/dist_ts" 2>/dev/null || true
|
|
||||||
rm -f "$INSTALL_DIR/package.json" 2>/dev/null || true
|
|
||||||
rm -f "$INSTALL_DIR/package-lock.json" 2>/dev/null || true
|
|
||||||
rm -f "$INSTALL_DIR/pnpm-lock.yaml" 2>/dev/null || true
|
|
||||||
rm -f "$INSTALL_DIR/tsconfig.json" 2>/dev/null || true
|
|
||||||
rm -f "$INSTALL_DIR/setup.sh" 2>/dev/null || true
|
|
||||||
rm -rf "$INSTALL_DIR/bin" 2>/dev/null || true
|
|
||||||
echo "Old installation files removed."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Creating installation directory: $INSTALL_DIR"
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Clean installation directory - ensure only binary exists
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
echo "Cleaning installation directory: $INSTALL_DIR"
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create fresh installation directory
|
||||||
|
echo "Creating installation directory: $INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
# Download binary
|
# Download binary
|
||||||
echo "Downloading NUPST binary..."
|
echo "Downloading NUPST binary..."
|
||||||
TEMP_FILE="$INSTALL_DIR/nupst.download"
|
TEMP_FILE="$INSTALL_DIR/nupst.download"
|
||||||
@@ -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,18 +246,10 @@ 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..."
|
||||||
systemctl start nupst
|
systemctl restart nupst
|
||||||
echo "Service restarted successfully."
|
echo "Service restarted successfully."
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
@@ -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
1
npmextra.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
64
package.json
Normal file
64
package.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "@serve.zone/nupst",
|
||||||
|
"version": "5.1.10",
|
||||||
|
"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'",
|
||||||
|
"build": "echo 'no build needed'"
|
||||||
|
},
|
||||||
|
"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/"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||||
|
}
|
||||||
243
readme.md
243
readme.md
@@ -1,8 +1,12 @@
|
|||||||
# ⚡ NUPST - Network UPS Shutdown Tool
|
# ⚡ NUPST - Network UPS Shutdown Tool
|
||||||
|
|
||||||
**Keep your systems safe when the power goes out.** NUPST is a lightweight, battle-tested command-line tool that monitors SNMP-enabled UPS devices and orchestrates graceful system shutdowns during power emergencies. Distributed as self-contained binaries with zero runtime dependencies for maximum reliability.
|
**Keep your systems safe when the power goes out.** NUPST is a lightweight, battle-tested
|
||||||
|
command-line tool that monitors SNMP-enabled UPS devices and orchestrates graceful system shutdowns
|
||||||
|
during power emergencies. Distributed as self-contained binaries with zero runtime dependencies for
|
||||||
|
maximum reliability.
|
||||||
|
|
||||||
**Version 5.0+** is powered by Deno and distributed as single pre-compiled binaries—no installation, no setup, just run.
|
**Version 5.0+** is powered by Deno and distributed as single pre-compiled binaries—no installation,
|
||||||
|
no setup, just run.
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
@@ -15,12 +19,18 @@
|
|||||||
- Runtime threshold triggers
|
- Runtime threshold triggers
|
||||||
- Power status change triggers
|
- Power status change triggers
|
||||||
- Configurable shutdown delays
|
- Configurable shutdown delays
|
||||||
- **🌐 Universal SNMP Support**: Full support for SNMP v1, v2c, and v3 with authentication and encryption
|
- **🌐 Universal SNMP Support**: Full support for SNMP v1, v2c, and v3 with authentication and
|
||||||
- **🏭 Multiple UPS Brands**: Works with CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv, and custom OID configurations
|
encryption
|
||||||
|
- **🏭 Multiple UPS Brands**: Works with CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv, and
|
||||||
|
custom OID configurations
|
||||||
- **🚀 Systemd Integration**: Simple service installation and management
|
- **🚀 Systemd Integration**: Simple service installation and management
|
||||||
- **📊 Real-time Monitoring**: Live status updates with detailed action and group information
|
- **📊 Real-time Monitoring**: Live status updates with detailed action and group information
|
||||||
- **📦 Self-Contained Binary**: Single executable with zero runtime dependencies—just download and run
|
- **🌐 HTTP API**: Optional HTTP server for JSON status export with authentication
|
||||||
- **🖥️ Cross-Platform**: Binaries available for Linux (x64, ARM64), macOS (Intel, Apple Silicon), and Windows
|
- **⚡ Power Metrics**: Monitor output load, power (watts), voltage, and current for all UPS devices
|
||||||
|
- **📦 Self-Contained Binary**: Single executable with zero runtime dependencies—just download and
|
||||||
|
run
|
||||||
|
- **🖥️ Cross-Platform**: Binaries available for Linux (x64, ARM64), macOS (Intel, Apple Silicon),
|
||||||
|
and Windows
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -52,7 +62,26 @@ nupst service status
|
|||||||
|
|
||||||
## 📥 Installation
|
## 📥 Installation
|
||||||
|
|
||||||
### Automated Installer (Recommended)
|
### Via npm (NEW! - Recommended)
|
||||||
|
|
||||||
|
Install NUPST globally using npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @serve.zone/nupst
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
|
||||||
|
- Automatic platform detection and binary download
|
||||||
|
- Downloads only the binary for your platform (~400-500MB)
|
||||||
|
- Easy updates via `npm update -g @serve.zone/nupst`
|
||||||
|
- Version management with npm
|
||||||
|
- Works with Node.js >=14
|
||||||
|
|
||||||
|
**Note:** The installation will download the appropriate binary from GitHub releases during the
|
||||||
|
postinstall step.
|
||||||
|
|
||||||
|
### Automated Installer Script
|
||||||
|
|
||||||
The installer script handles everything automatically:
|
The installer script handles everything automatically:
|
||||||
|
|
||||||
@@ -61,6 +90,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
|||||||
```
|
```
|
||||||
|
|
||||||
**What it does:**
|
**What it does:**
|
||||||
|
|
||||||
1. Detects your platform (OS and architecture)
|
1. Detects your platform (OS and architecture)
|
||||||
2. Downloads the latest pre-compiled binary
|
2. Downloads the latest pre-compiled binary
|
||||||
3. Installs to `/opt/nupst/nupst`
|
3. Installs to `/opt/nupst/nupst`
|
||||||
@@ -84,10 +114,11 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
|||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
Download the appropriate binary for your platform from [releases](https://code.foss.global/serve.zone/nupst/releases):
|
Download the appropriate binary for your platform from
|
||||||
|
[releases](https://code.foss.global/serve.zone/nupst/releases):
|
||||||
|
|
||||||
| Platform | Binary |
|
| Platform | Binary |
|
||||||
|----------|--------|
|
| ------------------- | ----------------------- |
|
||||||
| Linux x64 | `nupst-linux-x64` |
|
| Linux x64 | `nupst-linux-x64` |
|
||||||
| Linux ARM64 | `nupst-linux-arm64` |
|
| Linux ARM64 | `nupst-linux-arm64` |
|
||||||
| macOS Intel | `nupst-macos-x64` |
|
| macOS Intel | `nupst-macos-x64` |
|
||||||
@@ -158,7 +189,8 @@ nupst group list # List all groups
|
|||||||
|
|
||||||
### Action Management 🆕
|
### Action Management 🆕
|
||||||
|
|
||||||
Actions define what happens when UPS conditions are met. Actions can be attached to individual UPS devices or to groups.
|
Actions define what happens when UPS conditions are met. Actions can be attached to individual UPS
|
||||||
|
devices or to groups.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add an action to a UPS device or group
|
# Add an action to a UPS device or group
|
||||||
@@ -198,6 +230,78 @@ Add Action to UPS Main Server UPS
|
|||||||
Changes saved and will be applied automatically
|
Changes saved and will be applied automatically
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Feature Management 🆕
|
||||||
|
|
||||||
|
Optional features like the HTTP server for JSON status export:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure HTTP server feature (interactive)
|
||||||
|
nupst feature httpServer
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Enabling HTTP Server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo nupst feature httpServer
|
||||||
|
|
||||||
|
HTTP Server Feature Configuration
|
||||||
|
Configure the HTTP server to expose UPS status as JSON
|
||||||
|
|
||||||
|
HTTP Server is currently: DISABLED
|
||||||
|
|
||||||
|
Enable or disable HTTP server? (enable/disable/cancel): enable
|
||||||
|
|
||||||
|
HTTP Server Port [8080]: 8080
|
||||||
|
URL Path [/ups-status]: /ups-status
|
||||||
|
Generated new authentication token
|
||||||
|
|
||||||
|
✓ HTTP Server Configuration
|
||||||
|
|
||||||
|
Status: ENABLED
|
||||||
|
Port: 8080
|
||||||
|
Path: /ups-status
|
||||||
|
Auth Token: abc123xyz789def456
|
||||||
|
|
||||||
|
Usage examples:
|
||||||
|
curl -H "Authorization: Bearer abc123xyz789def456" http://localhost:8080/ups-status
|
||||||
|
curl "http://localhost:8080/ups-status?token=abc123xyz789def456"
|
||||||
|
|
||||||
|
⚠ IMPORTANT: Save the authentication token securely!
|
||||||
|
|
||||||
|
Service is running. Restart to apply changes? (Y/n): Y
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query UPS Status via HTTP:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Bearer token in header
|
||||||
|
curl -H "Authorization: Bearer abc123xyz789def456" \
|
||||||
|
http://localhost:8080/ups-status
|
||||||
|
|
||||||
|
# Using token as query parameter
|
||||||
|
curl "http://localhost:8080/ups-status?token=abc123xyz789def456"
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "ups-main",
|
||||||
|
"name": "Main Server UPS",
|
||||||
|
"powerStatus": "online",
|
||||||
|
"batteryCapacity": 100,
|
||||||
|
"batteryRuntime": 45,
|
||||||
|
"outputLoad": 23,
|
||||||
|
"outputPower": 115,
|
||||||
|
"outputVoltage": 230.5,
|
||||||
|
"outputCurrent": 0.5,
|
||||||
|
"lastStatusChange": 1729685123456,
|
||||||
|
"lastCheckTime": 1729685153456
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -214,14 +318,21 @@ nupst config show # Display current configuration
|
|||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to configure is through interactive commands, but you can also edit the JSON directly.
|
NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to configure is through
|
||||||
|
interactive commands, but you can also edit the JSON directly.
|
||||||
|
|
||||||
### Example Configuration (v4.1+)
|
### Example Configuration (v4.1+)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "4.1",
|
"version": "4.2",
|
||||||
"checkInterval": 30000,
|
"checkInterval": 30000,
|
||||||
|
"httpServer": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8080,
|
||||||
|
"path": "/ups-status",
|
||||||
|
"authToken": "abc123xyz789def456"
|
||||||
|
},
|
||||||
"upsDevices": [
|
"upsDevices": [
|
||||||
{
|
{
|
||||||
"id": "ups-main",
|
"id": "ups-main",
|
||||||
@@ -298,8 +409,9 @@ NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to confi
|
|||||||
|
|
||||||
#### Global Settings
|
#### Global Settings
|
||||||
|
|
||||||
- **`version`**: Config format version (current: "4.1")
|
- **`version`**: Config format version (current: "4.2")
|
||||||
- **`checkInterval`**: Polling interval in milliseconds (default: 30000)
|
- **`checkInterval`**: Polling interval in milliseconds (default: 30000)
|
||||||
|
- **`httpServer`**: Optional HTTP server configuration (see HTTP Server Configuration below)
|
||||||
|
|
||||||
#### UPS Device Settings
|
#### UPS Device Settings
|
||||||
|
|
||||||
@@ -311,7 +423,7 @@ NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to confi
|
|||||||
**SNMP Configuration:**
|
**SNMP Configuration:**
|
||||||
|
|
||||||
| Field | Description | Values |
|
| Field | Description | Values |
|
||||||
|-------|-------------|--------|
|
| ----------- | ----------------------- | -------------------------------------------------------------- |
|
||||||
| `host` | IP address or hostname | e.g., "192.168.1.100" |
|
| `host` | IP address or hostname | e.g., "192.168.1.100" |
|
||||||
| `port` | SNMP port | Default: 161 |
|
| `port` | SNMP port | Default: 161 |
|
||||||
| `version` | SNMP version | 1, 2, or 3 |
|
| `version` | SNMP version | 1, 2, or 3 |
|
||||||
@@ -322,7 +434,7 @@ NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to confi
|
|||||||
**SNMPv3 Security:**
|
**SNMPv3 Security:**
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|-------|-------------|
|
| --------------- | ------------------------------------------- |
|
||||||
| `securityLevel` | 'noAuthNoPriv', 'authNoPriv', or 'authPriv' |
|
| `securityLevel` | 'noAuthNoPriv', 'authNoPriv', or 'authPriv' |
|
||||||
| `username` | SNMPv3 username |
|
| `username` | SNMPv3 username |
|
||||||
| `authProtocol` | 'MD5' or 'SHA' |
|
| `authProtocol` | 'MD5' or 'SHA' |
|
||||||
@@ -349,7 +461,7 @@ Actions define automated responses to UPS conditions:
|
|||||||
**Action Fields:**
|
**Action Fields:**
|
||||||
|
|
||||||
| Field | Description | Values |
|
| Field | Description | Values |
|
||||||
|-------|-------------|--------|
|
| --------------- | -------------------------------- | -------------------------------------- |
|
||||||
| `type` | Action type | Currently only 'shutdown' |
|
| `type` | Action type | Currently only 'shutdown' |
|
||||||
| `thresholds` | Battery and runtime limits | `{ battery: 0-100, runtime: minutes }` |
|
| `thresholds` | Battery and runtime limits | `{ battery: 0-100, runtime: minutes }` |
|
||||||
| `triggerMode` | When to trigger action | See Trigger Modes below |
|
| `triggerMode` | When to trigger action | See Trigger Modes below |
|
||||||
@@ -358,7 +470,7 @@ Actions define automated responses to UPS conditions:
|
|||||||
**Trigger Modes:**
|
**Trigger Modes:**
|
||||||
|
|
||||||
| Mode | Description |
|
| Mode | Description |
|
||||||
|------|-------------|
|
| --------------------------- | -------------------------------------------------------------------------- |
|
||||||
| `onlyPowerChanges` | Trigger only when power status changes (on battery → online or vice versa) |
|
| `onlyPowerChanges` | Trigger only when power status changes (on battery → online or vice versa) |
|
||||||
| `onlyThresholds` | Trigger only when battery or runtime thresholds are violated |
|
| `onlyThresholds` | Trigger only when battery or runtime thresholds are violated |
|
||||||
| `powerChangesAndThresholds` | Trigger only when power changes AND thresholds are violated |
|
| `powerChangesAndThresholds` | Trigger only when power changes AND thresholds are violated |
|
||||||
@@ -380,8 +492,53 @@ Groups allow coordinated management of multiple UPS devices:
|
|||||||
|
|
||||||
**Group Modes:**
|
**Group Modes:**
|
||||||
|
|
||||||
- **`redundant`**: System shuts down only when ALL UPS devices in the group are critical. Perfect for setups with backup UPS units.
|
- **`redundant`**: System shuts down only when ALL UPS devices in the group are critical. Perfect
|
||||||
- **`nonRedundant`**: System shuts down when ANY UPS device in the group is critical. Used when all UPS devices must be operational.
|
for setups with backup UPS units.
|
||||||
|
- **`nonRedundant`**: System shuts down when ANY UPS device in the group is critical. Used when all
|
||||||
|
UPS devices must be operational.
|
||||||
|
|
||||||
|
#### HTTP Server Configuration 🆕
|
||||||
|
|
||||||
|
Enable optional HTTP server for JSON status export with authentication:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"port": 8080,
|
||||||
|
"path": "/ups-status",
|
||||||
|
"authToken": "abc123xyz789def456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP Server Fields:**
|
||||||
|
|
||||||
|
| Field | Description | Default |
|
||||||
|
| ----------- | ------------------------------------------------ | -------------- |
|
||||||
|
| `enabled` | Whether HTTP server is enabled | `false` |
|
||||||
|
| `port` | TCP port for HTTP server | `8080` |
|
||||||
|
| `path` | URL path for status endpoint | `/ups-status` |
|
||||||
|
| `authToken` | Authentication token (required for all requests) | Auto-generated |
|
||||||
|
|
||||||
|
**Authentication Methods:**
|
||||||
|
|
||||||
|
The HTTP server supports two authentication methods:
|
||||||
|
|
||||||
|
1. **Bearer Token** (Header): `Authorization: Bearer <token>`
|
||||||
|
2. **Query Parameter**: `?token=<token>`
|
||||||
|
|
||||||
|
**Security Features:**
|
||||||
|
|
||||||
|
- Token-based authentication required for all requests
|
||||||
|
- Returns 401 Unauthorized for invalid/missing tokens
|
||||||
|
- Serves cached data from monitoring loop (no extra SNMP queries)
|
||||||
|
- No CORS headers (local network only)
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
|
||||||
|
- Integration with monitoring systems (Prometheus, Grafana, etc.)
|
||||||
|
- Custom dashboards and visualizations
|
||||||
|
- Mobile apps and web interfaces
|
||||||
|
- Home automation systems
|
||||||
|
|
||||||
### Supported UPS Models
|
### Supported UPS Models
|
||||||
|
|
||||||
@@ -408,7 +565,8 @@ For custom UPS models, specify `customOIDs`:
|
|||||||
|
|
||||||
### Status Display
|
### Status Display
|
||||||
|
|
||||||
The status command shows comprehensive information about your UPS devices, groups, and configured actions:
|
The status command shows comprehensive information about your UPS devices, groups, and configured
|
||||||
|
actions:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ nupst service status
|
$ nupst service status
|
||||||
@@ -440,6 +598,7 @@ nupst service logs
|
|||||||
```
|
```
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
|
|
||||||
```
|
```
|
||||||
[2025-01-15 10:30:15] ℹ NUPST daemon started
|
[2025-01-15 10:30:15] ℹ NUPST daemon started
|
||||||
[2025-01-15 10:30:15] ✓ Connected to Main Server UPS (192.168.1.100)
|
[2025-01-15 10:30:15] ✓ Connected to Main Server UPS (192.168.1.100)
|
||||||
@@ -465,7 +624,7 @@ NUPST is designed with security as a priority:
|
|||||||
Full SNMPv3 support with authentication and encryption:
|
Full SNMPv3 support with authentication and encryption:
|
||||||
|
|
||||||
| Security Level | Description |
|
| Security Level | Description |
|
||||||
|----------------|-------------|
|
| -------------- | ------------------------------------------------------ |
|
||||||
| `noAuthNoPriv` | No authentication, no encryption (not recommended) |
|
| `noAuthNoPriv` | No authentication, no encryption (not recommended) |
|
||||||
| `authNoPriv` | MD5/SHA authentication without encryption |
|
| `authNoPriv` | MD5/SHA authentication without encryption |
|
||||||
| `authPriv` | Full authentication + DES/AES encryption (recommended) |
|
| `authPriv` | Full authentication + DES/AES encryption (recommended) |
|
||||||
@@ -489,6 +648,12 @@ Full SNMPv3 support with authentication and encryption:
|
|||||||
- **Local-Only Communication**: Only connects to UPS devices on local network
|
- **Local-Only Communication**: Only connects to UPS devices on local network
|
||||||
- **No Telemetry**: No data sent to external servers
|
- **No Telemetry**: No data sent to external servers
|
||||||
- **No Auto-Updates**: Manual update process only
|
- **No Auto-Updates**: Manual update process only
|
||||||
|
- **HTTP Server** (optional):
|
||||||
|
- Disabled by default
|
||||||
|
- Token-based authentication required
|
||||||
|
- Local network access only (no CORS)
|
||||||
|
- Serves cached data (no additional SNMP queries)
|
||||||
|
- Configurable port and path
|
||||||
|
|
||||||
### Verifying Downloads
|
### Verifying Downloads
|
||||||
|
|
||||||
@@ -514,6 +679,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
|||||||
```
|
```
|
||||||
|
|
||||||
The installer will:
|
The installer will:
|
||||||
|
|
||||||
- Download the latest binary
|
- Download the latest binary
|
||||||
- Replace the existing installation
|
- Replace the existing installation
|
||||||
- Preserve your configuration
|
- Preserve your configuration
|
||||||
@@ -540,7 +706,8 @@ sudo nupst service start
|
|||||||
nupst --version
|
nupst --version
|
||||||
```
|
```
|
||||||
|
|
||||||
Visit the [releases page](https://code.foss.global/serve.zone/nupst/releases) for the latest version.
|
Visit the [releases page](https://code.foss.global/serve.zone/nupst/releases) for the latest
|
||||||
|
version.
|
||||||
|
|
||||||
## 🗑️ Uninstallation
|
## 🗑️ Uninstallation
|
||||||
|
|
||||||
@@ -639,7 +806,7 @@ When installed, NUPST makes the following changes:
|
|||||||
### File System
|
### File System
|
||||||
|
|
||||||
| Path | Description |
|
| Path | Description |
|
||||||
|------|-------------|
|
| ----------------------------------- | -------------------- |
|
||||||
| `/opt/nupst/nupst` | Pre-compiled binary |
|
| `/opt/nupst/nupst` | Pre-compiled binary |
|
||||||
| `/usr/local/bin/nupst` | Symlink to binary |
|
| `/usr/local/bin/nupst` | Symlink to binary |
|
||||||
| `/etc/nupst/config.json` | Configuration file |
|
| `/etc/nupst/config.json` | Configuration file |
|
||||||
@@ -653,7 +820,7 @@ When installed, NUPST makes the following changes:
|
|||||||
### Network
|
### Network
|
||||||
|
|
||||||
- Outbound SNMP to UPS devices (default port 161)
|
- Outbound SNMP to UPS devices (default port 161)
|
||||||
- No inbound connections required
|
- Optional inbound HTTP server (disabled by default, configurable port)
|
||||||
- No external internet connections
|
- No external internet connections
|
||||||
|
|
||||||
## 🚀 Migration from v3.x
|
## 🚀 Migration from v3.x
|
||||||
@@ -666,6 +833,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
|||||||
```
|
```
|
||||||
|
|
||||||
**The installer automatically:**
|
**The installer automatically:**
|
||||||
|
|
||||||
- Detects v3.x installation
|
- Detects v3.x installation
|
||||||
- Stops the service
|
- Stops the service
|
||||||
- Replaces Node.js version with Deno binary
|
- Replaces Node.js version with Deno binary
|
||||||
@@ -675,7 +843,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
|||||||
### Key Changes in v4.x
|
### Key Changes in v4.x
|
||||||
|
|
||||||
| Aspect | v3.x | v4.x |
|
| Aspect | v3.x | v4.x |
|
||||||
|--------|------|------|
|
| ------------------------ | -------------------------- | ----------------------------- |
|
||||||
| **Runtime** | Node.js + npm | Deno |
|
| **Runtime** | Node.js + npm | Deno |
|
||||||
| **Distribution** | Git repo + npm install | Pre-compiled binaries |
|
| **Distribution** | Git repo + npm install | Pre-compiled binaries |
|
||||||
| **Runtime Dependencies** | node_modules required | Zero (self-contained) |
|
| **Runtime Dependencies** | node_modules required | Zero (self-contained) |
|
||||||
@@ -689,6 +857,7 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
|||||||
Your v3.x configuration is **fully compatible**. The migration system automatically converts:
|
Your v3.x configuration is **fully compatible**. The migration system automatically converts:
|
||||||
|
|
||||||
**v4.0 format** (UPS-level thresholds):
|
**v4.0 format** (UPS-level thresholds):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "4.0",
|
"version": "4.0",
|
||||||
@@ -700,6 +869,7 @@ Your v3.x configuration is **fully compatible**. The migration system automatica
|
|||||||
```
|
```
|
||||||
|
|
||||||
**v4.1 format** (action-based thresholds):
|
**v4.1 format** (action-based thresholds):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "4.1",
|
"version": "4.1",
|
||||||
@@ -771,19 +941,28 @@ nupst/
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT
|
||||||
|
License can be found in the [license](license) file within this repository.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks,
|
||||||
|
service marks, or product names of the project, except as required for reasonable and customary use
|
||||||
|
in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated
|
||||||
|
with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture
|
||||||
|
Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these
|
||||||
|
trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be
|
||||||
|
approved in writing by Task Venture Capital GmbH.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH Registered at District court Bremen HRB 35230 HB, Germany
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or if you require further information, please contact us via email at
|
||||||
|
hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its
|
||||||
|
terms, and understand that the licensing of the code does not imply endorsement by Task Venture
|
||||||
|
Capital GmbH of any derivative works.
|
||||||
|
|||||||
231
scripts/install-binary.js
Normal file
231
scripts/install-binary.js
Normal 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
BIN
serve.zone-nupst-5.0.5.tgz
Normal file
Binary file not shown.
@@ -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.10',
|
||||||
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'
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
59
ts/cli.ts
59
ts/cli.ts
@@ -223,6 +223,24 @@ export class NupstCli {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle feature subcommands
|
||||||
|
if (command === 'feature') {
|
||||||
|
const subcommand = commandArgs[0];
|
||||||
|
const featureHandler = this.nupst.getFeatureHandler();
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case 'httpServer':
|
||||||
|
case 'http-server':
|
||||||
|
case 'http':
|
||||||
|
await featureHandler.configureHttpServer();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.showFeatureHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle config subcommand
|
// Handle config subcommand
|
||||||
if (command === 'config') {
|
if (command === 'config') {
|
||||||
const subcommand = commandArgs[0] || 'show';
|
const subcommand = commandArgs[0] || 'show';
|
||||||
@@ -294,6 +312,26 @@ export class NupstCli {
|
|||||||
` ${theme.path('/etc/nupst/config.json')}`,
|
` ${theme.path('/etc/nupst/config.json')}`,
|
||||||
], 60, 'info');
|
], 60, 'info');
|
||||||
|
|
||||||
|
// HTTP Server Status (if configured)
|
||||||
|
if (config.httpServer) {
|
||||||
|
const serverStatus = config.httpServer.enabled
|
||||||
|
? theme.success('Enabled')
|
||||||
|
: theme.dim('Disabled');
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.logBox('HTTP Server', [
|
||||||
|
`Status: ${serverStatus}`,
|
||||||
|
...(config.httpServer.enabled ? [
|
||||||
|
`Port: ${theme.highlight(String(config.httpServer.port))}`,
|
||||||
|
`Path: ${theme.highlight(config.httpServer.path)}`,
|
||||||
|
`Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`,
|
||||||
|
'',
|
||||||
|
theme.dim('Usage:'),
|
||||||
|
` curl -H "Authorization: Bearer TOKEN" http://localhost:${config.httpServer.port}${config.httpServer.path}`,
|
||||||
|
] : []),
|
||||||
|
], 70, config.httpServer.enabled ? 'success' : 'default');
|
||||||
|
}
|
||||||
|
|
||||||
// UPS Devices Table
|
// UPS Devices Table
|
||||||
if (config.upsDevices.length > 0) {
|
if (config.upsDevices.length > 0) {
|
||||||
const upsRows = config.upsDevices.map((ups) => ({
|
const upsRows = config.upsDevices.map((ups) => ({
|
||||||
@@ -466,6 +504,7 @@ export class NupstCli {
|
|||||||
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
||||||
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
||||||
this.printCommand('action <subcommand>', 'Manage UPS actions');
|
this.printCommand('action <subcommand>', 'Manage UPS actions');
|
||||||
|
this.printCommand('feature <subcommand>', 'Manage optional features');
|
||||||
this.printCommand('config [show]', 'Display current configuration');
|
this.printCommand('config [show]', 'Display current configuration');
|
||||||
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
||||||
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
||||||
@@ -509,6 +548,11 @@ export class NupstCli {
|
|||||||
this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)');
|
this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// Feature subcommands
|
||||||
|
logger.log(theme.info('Feature Subcommands:'));
|
||||||
|
this.printCommand('nupst feature httpServer', 'Configure HTTP server for JSON status export');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
logger.log(theme.info('Options:'));
|
logger.log(theme.info('Options:'));
|
||||||
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
||||||
@@ -632,6 +676,21 @@ Examples:
|
|||||||
nupst action add default - Add a new action to UPS or group 'default'
|
nupst action add default - Add a new action to UPS or group 'default'
|
||||||
nupst action remove default 0 - Remove action at index 0 from UPS or group 'default'
|
nupst action remove default 0 - Remove action at index 0 from UPS or group 'default'
|
||||||
nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1'
|
nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1'
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showFeatureHelp(): void {
|
||||||
|
logger.log(`
|
||||||
|
NUPST - Feature Management Commands
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
nupst feature <subcommand>
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
httpServer - Configure HTTP server for JSON status export
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
nupst feature httpServer - Enable/disable HTTP server with interactive setup
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
213
ts/cli/feature-handler.ts
Normal file
213
ts/cli/feature-handler.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { Nupst } from '../nupst.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
import { theme } from '../colors.ts';
|
||||||
|
import * as helpers from '../helpers/index.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling feature-related CLI commands
|
||||||
|
* Provides interface for managing optional features like HTTP server
|
||||||
|
*/
|
||||||
|
export class FeatureHandler {
|
||||||
|
private readonly nupst: Nupst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new feature handler
|
||||||
|
* @param nupst Reference to the main Nupst instance
|
||||||
|
*/
|
||||||
|
constructor(nupst: Nupst) {
|
||||||
|
this.nupst = nupst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure HTTP server feature
|
||||||
|
*/
|
||||||
|
public async configureHttpServer(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const readline = await import('node:readline');
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = (question: string): Promise<string> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer: string) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.runHttpServerConfig(prompt);
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
process.stdin.destroy();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the interactive HTTP server configuration process
|
||||||
|
* @param prompt Function to prompt for user input
|
||||||
|
*/
|
||||||
|
private async runHttpServerConfig(prompt: (question: string) => Promise<string>): Promise<void> {
|
||||||
|
logger.log('');
|
||||||
|
logger.logBoxTitle('HTTP Server Feature Configuration', 60);
|
||||||
|
logger.logBoxLine('Configure the HTTP server to expose UPS status as JSON');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
await this.nupst.getDaemon().loadConfig();
|
||||||
|
config = this.nupst.getDaemon().getConfig();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('No configuration found. Please run "nupst ups add" first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show current status
|
||||||
|
if (config.httpServer?.enabled) {
|
||||||
|
logger.info('HTTP Server is currently: ' + theme.success('ENABLED'));
|
||||||
|
logger.log(` Port: ${theme.highlight(String(config.httpServer.port))}`);
|
||||||
|
logger.log(` Path: ${theme.highlight(config.httpServer.path)}`);
|
||||||
|
logger.log(` Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`);
|
||||||
|
logger.log('');
|
||||||
|
} else {
|
||||||
|
logger.info('HTTP Server is currently: ' + theme.dim('DISABLED'));
|
||||||
|
logger.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask enable/disable
|
||||||
|
const action = await prompt('Enable or disable HTTP server? (enable/disable/cancel): ');
|
||||||
|
|
||||||
|
if (action.toLowerCase() === 'cancel' || action.toLowerCase() === 'c') {
|
||||||
|
logger.log('Cancelled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.toLowerCase() === 'disable' || action.toLowerCase() === 'd') {
|
||||||
|
// Disable HTTP server
|
||||||
|
config.httpServer = {
|
||||||
|
enabled: false,
|
||||||
|
port: config.httpServer?.port || 8080,
|
||||||
|
path: config.httpServer?.path || '/ups-status',
|
||||||
|
authToken: config.httpServer?.authToken || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.success('HTTP Server disabled');
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
await this.restartServiceIfRunning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.toLowerCase() !== 'enable' && action.toLowerCase() !== 'e') {
|
||||||
|
logger.error('Invalid option. Please enter "enable", "disable", or "cancel".');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable - gather configuration
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
const portInput = await prompt(`HTTP Server Port [${config.httpServer?.port || 8080}]: `);
|
||||||
|
const port = portInput ? parseInt(portInput, 10) : (config.httpServer?.port || 8080);
|
||||||
|
|
||||||
|
if (isNaN(port) || port < 1 || port > 65535) {
|
||||||
|
logger.error('Invalid port number. Must be between 1 and 65535.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathInput = await prompt(`URL Path [${config.httpServer?.path || '/ups-status'}]: `);
|
||||||
|
const path = pathInput || config.httpServer?.path || '/ups-status';
|
||||||
|
|
||||||
|
// Ensure path starts with /
|
||||||
|
const finalPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
|
||||||
|
// Generate or reuse auth token
|
||||||
|
let authToken = config.httpServer?.authToken;
|
||||||
|
if (!authToken) {
|
||||||
|
// Generate new random token
|
||||||
|
authToken = helpers.shortId() + helpers.shortId() + helpers.shortId();
|
||||||
|
logger.log('');
|
||||||
|
logger.info('Generated new authentication token');
|
||||||
|
} else {
|
||||||
|
const regenerate = await prompt('Regenerate authentication token? (y/N): ');
|
||||||
|
if (regenerate.toLowerCase() === 'y' || regenerate.toLowerCase() === 'yes') {
|
||||||
|
authToken = helpers.shortId() + helpers.shortId() + helpers.shortId();
|
||||||
|
logger.info('Generated new authentication token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save configuration
|
||||||
|
config.httpServer = {
|
||||||
|
enabled: true,
|
||||||
|
port,
|
||||||
|
path: finalPath,
|
||||||
|
authToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
|
// Display summary
|
||||||
|
logger.log('');
|
||||||
|
logger.logBoxTitle('HTTP Server Configuration', 70, 'success');
|
||||||
|
logger.logBoxLine(`Status: ${theme.success('ENABLED')}`);
|
||||||
|
logger.logBoxLine(`Port: ${theme.highlight(String(port))}`);
|
||||||
|
logger.logBoxLine(`Path: ${theme.highlight(finalPath)}`);
|
||||||
|
logger.logBoxLine(`Auth Token: ${theme.warning(authToken)}`);
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine(theme.dim('Usage examples:'));
|
||||||
|
logger.logBoxLine(` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`);
|
||||||
|
logger.logBoxLine(` curl "http://localhost:${port}${finalPath}?token=${authToken}"`);
|
||||||
|
logger.logBoxEnd();
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
logger.warn('IMPORTANT: Save the authentication token securely!');
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
await this.restartServiceIfRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart the service if it's currently running
|
||||||
|
*/
|
||||||
|
private async restartServiceIfRunning(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
logger.log('');
|
||||||
|
const readline = await import('node:readline');
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question('Service is running. Restart to apply changes? (Y/n): ', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
||||||
|
logger.info('Restarting service...');
|
||||||
|
execSync('sudo systemctl restart nupst.service');
|
||||||
|
logger.success('Service restarted successfully');
|
||||||
|
} else {
|
||||||
|
logger.warn('Changes will take effect on next service restart');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors - service might not be installed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
ts/daemon.ts
55
ts/daemon.ts
@@ -10,6 +10,7 @@ import { MigrationRunner } from './migrations/index.ts';
|
|||||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
import type { IActionConfig } from './actions/base-action.ts';
|
import type { IActionConfig } from './actions/base-action.ts';
|
||||||
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
|
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
|
||||||
|
import { NupstHttpServer } from './http-server.ts';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -46,6 +47,20 @@ export interface IGroupConfig {
|
|||||||
actions?: IActionConfig[];
|
actions?: IActionConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Server configuration interface
|
||||||
|
*/
|
||||||
|
export interface IHttpServerConfig {
|
||||||
|
/** Whether HTTP server is enabled */
|
||||||
|
enabled: boolean;
|
||||||
|
/** Port to listen on */
|
||||||
|
port: number;
|
||||||
|
/** URL path for the endpoint */
|
||||||
|
path: string;
|
||||||
|
/** Authentication token */
|
||||||
|
authToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration interface for the daemon
|
* Configuration interface for the daemon
|
||||||
*/
|
*/
|
||||||
@@ -58,6 +73,8 @@ export interface INupstConfig {
|
|||||||
groups: IGroupConfig[];
|
groups: IGroupConfig[];
|
||||||
/** Check interval in milliseconds */
|
/** Check interval in milliseconds */
|
||||||
checkInterval: number;
|
checkInterval: number;
|
||||||
|
/** HTTP Server configuration */
|
||||||
|
httpServer?: IHttpServerConfig;
|
||||||
|
|
||||||
// Legacy fields for backward compatibility (will be migrated away)
|
// Legacy fields for backward compatibility (will be migrated away)
|
||||||
/** UPS list (v3 format - legacy) */
|
/** UPS list (v3 format - legacy) */
|
||||||
@@ -82,6 +99,10 @@ export interface IUpsStatus {
|
|||||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||||
batteryCapacity: number;
|
batteryCapacity: number;
|
||||||
batteryRuntime: number;
|
batteryRuntime: number;
|
||||||
|
outputLoad: number; // Load percentage (0-100%)
|
||||||
|
outputPower: number; // Power in watts
|
||||||
|
outputVoltage: number; // Voltage in volts
|
||||||
|
outputCurrent: number; // Current in amps
|
||||||
lastStatusChange: number;
|
lastStatusChange: number;
|
||||||
lastCheckTime: number;
|
lastCheckTime: number;
|
||||||
}
|
}
|
||||||
@@ -139,6 +160,7 @@ export class NupstDaemon {
|
|||||||
private snmp: NupstSnmp;
|
private snmp: NupstSnmp;
|
||||||
private isRunning: boolean = false;
|
private isRunning: boolean = false;
|
||||||
private upsStatus: Map<string, IUpsStatus> = new Map();
|
private upsStatus: Map<string, IUpsStatus> = new Map();
|
||||||
|
private httpServer?: NupstHttpServer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new daemon instance with the given SNMP manager
|
* Create a new daemon instance with the given SNMP manager
|
||||||
@@ -278,6 +300,21 @@ export class NupstDaemon {
|
|||||||
// Initialize UPS status tracking
|
// Initialize UPS status tracking
|
||||||
this.initializeUpsStatus();
|
this.initializeUpsStatus();
|
||||||
|
|
||||||
|
// Start HTTP server if configured
|
||||||
|
if (this.config.httpServer?.enabled && this.config.httpServer.authToken) {
|
||||||
|
try {
|
||||||
|
this.httpServer = new NupstHttpServer(
|
||||||
|
this.config.httpServer.port,
|
||||||
|
this.config.httpServer.path,
|
||||||
|
this.config.httpServer.authToken,
|
||||||
|
() => this.upsStatus
|
||||||
|
);
|
||||||
|
this.httpServer.start();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to start HTTP server: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start UPS monitoring
|
// Start UPS monitoring
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
await this.monitor();
|
await this.monitor();
|
||||||
@@ -304,6 +341,10 @@ export class NupstDaemon {
|
|||||||
powerStatus: 'unknown',
|
powerStatus: 'unknown',
|
||||||
batteryCapacity: 100,
|
batteryCapacity: 100,
|
||||||
batteryRuntime: 999, // High value as default
|
batteryRuntime: 999, // High value as default
|
||||||
|
outputLoad: 0,
|
||||||
|
outputPower: 0,
|
||||||
|
outputVoltage: 0,
|
||||||
|
outputCurrent: 0,
|
||||||
lastStatusChange: Date.now(),
|
lastStatusChange: Date.now(),
|
||||||
lastCheckTime: 0,
|
lastCheckTime: 0,
|
||||||
});
|
});
|
||||||
@@ -377,6 +418,12 @@ export class NupstDaemon {
|
|||||||
*/
|
*/
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
logger.log('Stopping NUPST daemon...');
|
logger.log('Stopping NUPST daemon...');
|
||||||
|
|
||||||
|
// Stop HTTP server if running
|
||||||
|
if (this.httpServer) {
|
||||||
|
this.httpServer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +484,10 @@ export class NupstDaemon {
|
|||||||
powerStatus: 'unknown',
|
powerStatus: 'unknown',
|
||||||
batteryCapacity: 100,
|
batteryCapacity: 100,
|
||||||
batteryRuntime: 999,
|
batteryRuntime: 999,
|
||||||
|
outputLoad: 0,
|
||||||
|
outputPower: 0,
|
||||||
|
outputVoltage: 0,
|
||||||
|
outputCurrent: 0,
|
||||||
lastStatusChange: Date.now(),
|
lastStatusChange: Date.now(),
|
||||||
lastCheckTime: 0,
|
lastCheckTime: 0,
|
||||||
});
|
});
|
||||||
@@ -456,6 +507,10 @@ export class NupstDaemon {
|
|||||||
powerStatus: status.powerStatus,
|
powerStatus: status.powerStatus,
|
||||||
batteryCapacity: status.batteryCapacity,
|
batteryCapacity: status.batteryCapacity,
|
||||||
batteryRuntime: status.batteryRuntime,
|
batteryRuntime: status.batteryRuntime,
|
||||||
|
outputLoad: status.outputLoad,
|
||||||
|
outputPower: status.outputPower,
|
||||||
|
outputVoltage: status.outputVoltage,
|
||||||
|
outputCurrent: status.outputCurrent,
|
||||||
lastCheckTime: currentTime,
|
lastCheckTime: currentTime,
|
||||||
lastStatusChange: currentStatus?.lastStatusChange || currentTime,
|
lastStatusChange: currentStatus?.lastStatusChange || currentTime,
|
||||||
};
|
};
|
||||||
|
|||||||
113
ts/http-server.ts
Normal file
113
ts/http-server.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import * as http from 'node:http';
|
||||||
|
import { URL } from 'node:url';
|
||||||
|
import { logger } from './logger.ts';
|
||||||
|
import type { IUpsStatus } from './daemon.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Server for exposing UPS status as JSON
|
||||||
|
* Serves cached data from the daemon's monitoring loop
|
||||||
|
*/
|
||||||
|
export class NupstHttpServer {
|
||||||
|
private server?: http.Server;
|
||||||
|
private port: number;
|
||||||
|
private path: string;
|
||||||
|
private authToken: string;
|
||||||
|
private getUpsStatus: () => Map<string, IUpsStatus>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new HTTP server instance
|
||||||
|
* @param port Port to listen on
|
||||||
|
* @param path URL path for the endpoint
|
||||||
|
* @param authToken Authentication token required for access
|
||||||
|
* @param getUpsStatus Function to retrieve cached UPS status
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
port: number,
|
||||||
|
path: string,
|
||||||
|
authToken: string,
|
||||||
|
getUpsStatus: () => Map<string, IUpsStatus>
|
||||||
|
) {
|
||||||
|
this.port = port;
|
||||||
|
this.path = path;
|
||||||
|
this.authToken = authToken;
|
||||||
|
this.getUpsStatus = getUpsStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify authentication token from request
|
||||||
|
* Supports both Bearer token in Authorization header and token query parameter
|
||||||
|
* @param req HTTP request
|
||||||
|
* @returns True if authenticated, false otherwise
|
||||||
|
*/
|
||||||
|
private isAuthenticated(req: http.IncomingMessage): boolean {
|
||||||
|
// Check Authorization header (Bearer token)
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader?.startsWith('Bearer ')) {
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
return token === this.authToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check token query parameter
|
||||||
|
if (req.url) {
|
||||||
|
const url = new URL(req.url, `http://localhost:${this.port}`);
|
||||||
|
const tokenParam = url.searchParams.get('token');
|
||||||
|
return tokenParam === this.authToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the HTTP server
|
||||||
|
*/
|
||||||
|
public start(): void {
|
||||||
|
this.server = http.createServer((req, res) => {
|
||||||
|
// Parse URL
|
||||||
|
const reqUrl = new URL(req.url || '/', `http://localhost:${this.port}`);
|
||||||
|
|
||||||
|
if (reqUrl.pathname === this.path && req.method === 'GET') {
|
||||||
|
// Check authentication
|
||||||
|
if (!this.isAuthenticated(req)) {
|
||||||
|
res.writeHead(401, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'WWW-Authenticate': 'Bearer'
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cached status (no refresh)
|
||||||
|
const statusMap = this.getUpsStatus();
|
||||||
|
const statusArray = Array.from(statusMap.values());
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify(statusArray, null, 2));
|
||||||
|
} else {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Not Found' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.listen(this.port, () => {
|
||||||
|
logger.success(`HTTP server started on port ${this.port} at ${this.path}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on('error', (error: any) => {
|
||||||
|
logger.error(`HTTP server error: ${error.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the HTTP server
|
||||||
|
*/
|
||||||
|
public stop(): void {
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close(() => {
|
||||||
|
logger.log('HTTP server stopped');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
ts/nupst.ts
28
ts/nupst.ts
@@ -1,12 +1,13 @@
|
|||||||
import { NupstSnmp } from './snmp/manager.ts';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import { NupstDaemon } from './daemon.ts';
|
import { NupstDaemon } from './daemon.ts';
|
||||||
import { NupstSystemd } from './systemd.ts';
|
import { NupstSystemd } from './systemd.ts';
|
||||||
import { commitinfo } from './00_commitinfo_data.ts';
|
import denoConfig from '../deno.json' with { type: 'json' };
|
||||||
import { logger } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
import { UpsHandler } from './cli/ups-handler.ts';
|
import { UpsHandler } from './cli/ups-handler.ts';
|
||||||
import { GroupHandler } from './cli/group-handler.ts';
|
import { GroupHandler } from './cli/group-handler.ts';
|
||||||
import { ServiceHandler } from './cli/service-handler.ts';
|
import { ServiceHandler } from './cli/service-handler.ts';
|
||||||
import { ActionHandler } from './cli/action-handler.ts';
|
import { ActionHandler } from './cli/action-handler.ts';
|
||||||
|
import { FeatureHandler } from './cli/feature-handler.ts';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +22,7 @@ export class Nupst {
|
|||||||
private readonly groupHandler: GroupHandler;
|
private readonly groupHandler: GroupHandler;
|
||||||
private readonly serviceHandler: ServiceHandler;
|
private readonly serviceHandler: ServiceHandler;
|
||||||
private readonly actionHandler: ActionHandler;
|
private readonly actionHandler: ActionHandler;
|
||||||
|
private readonly featureHandler: FeatureHandler;
|
||||||
private updateAvailable: boolean = false;
|
private updateAvailable: boolean = false;
|
||||||
private latestVersion: string = '';
|
private latestVersion: string = '';
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ export class Nupst {
|
|||||||
this.groupHandler = new GroupHandler(this);
|
this.groupHandler = new GroupHandler(this);
|
||||||
this.serviceHandler = new ServiceHandler(this);
|
this.serviceHandler = new ServiceHandler(this);
|
||||||
this.actionHandler = new ActionHandler(this);
|
this.actionHandler = new ActionHandler(this);
|
||||||
|
this.featureHandler = new FeatureHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,12 +93,19 @@ export class Nupst {
|
|||||||
return this.actionHandler;
|
return this.actionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Feature handler for feature management
|
||||||
|
*/
|
||||||
|
public getFeatureHandler(): FeatureHandler {
|
||||||
|
return this.featureHandler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current version of NUPST
|
* Get the current version of NUPST
|
||||||
* @returns The current version string
|
* @returns The current version string
|
||||||
*/
|
*/
|
||||||
public getVersion(): string {
|
public getVersion(): string {
|
||||||
return commitinfo.version;
|
return denoConfig.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,8 +153,8 @@ export class Nupst {
|
|||||||
private getLatestVersion(): Promise<string> {
|
private getLatestVersion(): Promise<string> {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: 'registry.npmjs.org',
|
hostname: 'code.foss.global',
|
||||||
path: '/@serve.zone/nupst',
|
path: '/api/v1/repos/serve.zone/nupst/releases/latest',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@@ -162,10 +172,14 @@ export class Nupst {
|
|||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const response = JSON.parse(data);
|
const response = JSON.parse(data);
|
||||||
if (response['dist-tags'] && response['dist-tags'].latest) {
|
if (response.tag_name) {
|
||||||
resolve(response['dist-tags'].latest);
|
// Strip 'v' prefix from tag name (e.g., "v5.1.7" -> "5.1.7")
|
||||||
|
const version = response.tag_name.startsWith('v')
|
||||||
|
? response.tag_name.substring(1)
|
||||||
|
: response.tag_name;
|
||||||
|
resolve(version);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Failed to parse version from npm registry response'));
|
reject(new Error('Failed to parse version from Gitea API response'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as snmp from 'npm:net-snmp@3.20.0';
|
import * as snmp from 'npm:net-snmp@3.26.0';
|
||||||
import { Buffer } from 'node:buffer';
|
import { Buffer } from 'node:buffer';
|
||||||
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||||
import { UpsOidSets } from './oid-sets.ts';
|
import { UpsOidSets } from './oid-sets.ts';
|
||||||
@@ -304,6 +304,10 @@ export class NupstSnmp {
|
|||||||
console.log(' Power Status:', this.activeOIDs.POWER_STATUS);
|
console.log(' Power Status:', this.activeOIDs.POWER_STATUS);
|
||||||
console.log(' Battery Capacity:', this.activeOIDs.BATTERY_CAPACITY);
|
console.log(' Battery Capacity:', this.activeOIDs.BATTERY_CAPACITY);
|
||||||
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
||||||
|
console.log(' Output Load:', this.activeOIDs.OUTPUT_LOAD);
|
||||||
|
console.log(' Output Power:', this.activeOIDs.OUTPUT_POWER);
|
||||||
|
console.log(' Output Voltage:', this.activeOIDs.OUTPUT_VOLTAGE);
|
||||||
|
console.log(' Output Current:', this.activeOIDs.OUTPUT_CURRENT);
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,20 +328,65 @@ export class NupstSnmp {
|
|||||||
config,
|
config,
|
||||||
) || 0;
|
) || 0;
|
||||||
|
|
||||||
|
// Get power draw metrics
|
||||||
|
const outputLoad = await this.getSNMPValueWithRetry(
|
||||||
|
this.activeOIDs.OUTPUT_LOAD,
|
||||||
|
'output load',
|
||||||
|
config,
|
||||||
|
) || 0;
|
||||||
|
const outputPower = await this.getSNMPValueWithRetry(
|
||||||
|
this.activeOIDs.OUTPUT_POWER,
|
||||||
|
'output power',
|
||||||
|
config,
|
||||||
|
) || 0;
|
||||||
|
const outputVoltage = await this.getSNMPValueWithRetry(
|
||||||
|
this.activeOIDs.OUTPUT_VOLTAGE,
|
||||||
|
'output voltage',
|
||||||
|
config,
|
||||||
|
) || 0;
|
||||||
|
const outputCurrent = await this.getSNMPValueWithRetry(
|
||||||
|
this.activeOIDs.OUTPUT_CURRENT,
|
||||||
|
'output current',
|
||||||
|
config,
|
||||||
|
) || 0;
|
||||||
|
|
||||||
// Determine power status - handle different values for different UPS models
|
// Determine power status - handle different values for different UPS models
|
||||||
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
|
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
|
||||||
|
|
||||||
// Convert to minutes for UPS models with different time units
|
// Convert to minutes for UPS models with different time units
|
||||||
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
||||||
|
|
||||||
|
// Process power metrics with vendor-specific scaling
|
||||||
|
const processedVoltage = this.processVoltageValue(config.upsModel, outputVoltage);
|
||||||
|
const processedCurrent = this.processCurrentValue(config.upsModel, outputCurrent);
|
||||||
|
|
||||||
|
// Calculate power from voltage × current if not provided by UPS
|
||||||
|
let processedPower = outputPower;
|
||||||
|
if (outputPower === 0 && processedVoltage > 0 && processedCurrent > 0) {
|
||||||
|
processedPower = Math.round(processedVoltage * processedCurrent);
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(
|
||||||
|
`Calculated power from V×I: ${processedVoltage}V × ${processedCurrent}A = ${processedPower}W`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
powerStatus,
|
powerStatus,
|
||||||
batteryCapacity,
|
batteryCapacity,
|
||||||
batteryRuntime: processedRuntime,
|
batteryRuntime: processedRuntime,
|
||||||
|
outputLoad,
|
||||||
|
outputPower: processedPower,
|
||||||
|
outputVoltage: processedVoltage,
|
||||||
|
outputCurrent: processedCurrent,
|
||||||
raw: {
|
raw: {
|
||||||
powerStatus: powerStatusValue,
|
powerStatus: powerStatusValue,
|
||||||
batteryCapacity,
|
batteryCapacity,
|
||||||
batteryRuntime,
|
batteryRuntime,
|
||||||
|
outputLoad,
|
||||||
|
outputPower,
|
||||||
|
outputVoltage,
|
||||||
|
outputCurrent,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -347,6 +396,10 @@ export class NupstSnmp {
|
|||||||
console.log(' Power Status:', result.powerStatus);
|
console.log(' Power Status:', result.powerStatus);
|
||||||
console.log(' Battery Capacity:', result.batteryCapacity + '%');
|
console.log(' Battery Capacity:', result.batteryCapacity + '%');
|
||||||
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
||||||
|
console.log(' Output Load:', result.outputLoad + '%');
|
||||||
|
console.log(' Output Power:', result.outputPower, 'watts');
|
||||||
|
console.log(' Output Voltage:', result.outputVoltage, 'volts');
|
||||||
|
console.log(' Output Current:', result.outputCurrent, 'amps');
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,4 +655,74 @@ export class NupstSnmp {
|
|||||||
|
|
||||||
return batteryRuntime;
|
return batteryRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process voltage value based on UPS model
|
||||||
|
* @param upsModel UPS model
|
||||||
|
* @param outputVoltage Raw output voltage value
|
||||||
|
* @returns Processed voltage in volts
|
||||||
|
*/
|
||||||
|
private processVoltageValue(
|
||||||
|
upsModel: TUpsModel | undefined,
|
||||||
|
outputVoltage: number,
|
||||||
|
): number {
|
||||||
|
if (this.debug) {
|
||||||
|
console.log('Raw voltage value:', outputVoltage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upsModel === 'cyberpower' && outputVoltage > 0) {
|
||||||
|
// CyberPower: Voltage is in 0.1V, convert to volts
|
||||||
|
const volts = outputVoltage / 10;
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(
|
||||||
|
`Converting CyberPower voltage from ${outputVoltage} (0.1V) to ${volts} volts`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return volts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputVoltage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process current value based on UPS model
|
||||||
|
* @param upsModel UPS model
|
||||||
|
* @param outputCurrent Raw output current value
|
||||||
|
* @returns Processed current in amps
|
||||||
|
*/
|
||||||
|
private processCurrentValue(
|
||||||
|
upsModel: TUpsModel | undefined,
|
||||||
|
outputCurrent: number,
|
||||||
|
): number {
|
||||||
|
if (this.debug) {
|
||||||
|
console.log('Raw current value:', outputCurrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upsModel === 'cyberpower' && outputCurrent > 0) {
|
||||||
|
// CyberPower: Current is in 0.1A, convert to amps
|
||||||
|
const amps = outputCurrent / 10;
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(
|
||||||
|
`Converting CyberPower current from ${outputCurrent} (0.1A) to ${amps} amps`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return amps;
|
||||||
|
} else if ((upsModel === 'tripplite' || upsModel === 'liebert') && outputCurrent > 0) {
|
||||||
|
// RFC 1628 standard: Current is in 0.1A, convert to amps
|
||||||
|
const amps = outputCurrent / 10;
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(
|
||||||
|
`Converting RFC 1628 current from ${outputCurrent} (0.1A) to ${amps} amps`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return amps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eaton XUPS-MIB and APC PowerNet report current directly in RMS Amps (no scaling needed)
|
||||||
|
if ((upsModel === 'eaton' || upsModel === 'apc') && this.debug && outputCurrent > 0) {
|
||||||
|
console.log(`${upsModel.toUpperCase()} current already in RMS Amps: ${outputCurrent}A`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputCurrent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,28 +14,40 @@ export class UpsOidSets {
|
|||||||
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
|
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
||||||
|
OUTPUT_LOAD: '1.3.6.1.4.1.3808.1.1.1.4.2.3.0', // upsAdvanceOutputLoad (percentage)
|
||||||
|
OUTPUT_POWER: '1.3.6.1.4.1.3808.1.1.1.4.2.5.0', // upsAdvanceOutputPower (watts)
|
||||||
|
OUTPUT_VOLTAGE: '1.3.6.1.4.1.3808.1.1.1.4.2.1.0', // upsAdvanceOutputVoltage (0.1V scale)
|
||||||
|
OUTPUT_CURRENT: '1.3.6.1.4.1.3808.1.1.1.4.2.4.0', // upsAdvanceOutputCurrent (0.1A scale)
|
||||||
POWER_STATUS_VALUES: {
|
POWER_STATUS_VALUES: {
|
||||||
online: 2, // upsBaseOutputStatus: 2=onLine
|
online: 2, // upsBaseOutputStatus: 2=onLine
|
||||||
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
|
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// APC OIDs
|
// APC OIDs (PowerNet MIB)
|
||||||
apc: {
|
apc: {
|
||||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
|
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
||||||
|
OUTPUT_LOAD: '1.3.6.1.4.1.318.1.1.1.4.2.3.0', // upsAdvOutputLoad (percentage)
|
||||||
|
OUTPUT_POWER: '1.3.6.1.4.1.318.1.1.1.4.2.8.0', // upsAdvOutputActivePower (watts)
|
||||||
|
OUTPUT_VOLTAGE: '1.3.6.1.4.1.318.1.1.1.4.2.1.0', // upsAdvOutputVoltage
|
||||||
|
OUTPUT_CURRENT: '1.3.6.1.4.1.318.1.1.1.4.2.4.0', // upsAdvOutputCurrent
|
||||||
POWER_STATUS_VALUES: {
|
POWER_STATUS_VALUES: {
|
||||||
online: 2, // upsBasicOutputStatus: 2=onLine
|
online: 2, // upsBasicOutputStatus: 2=onLine
|
||||||
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
|
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Eaton OIDs
|
// Eaton OIDs (XUPS-MIB)
|
||||||
eaton: {
|
eaton: {
|
||||||
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
|
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
||||||
|
OUTPUT_LOAD: '1.3.6.1.4.1.534.1.4.4.1.8.1', // xupsOutputPercentLoad (phase 1)
|
||||||
|
OUTPUT_POWER: '1.3.6.1.4.1.534.1.4.4.1.4.1', // xupsOutputWatts (phase 1)
|
||||||
|
OUTPUT_VOLTAGE: '1.3.6.1.4.1.534.1.4.4.1.2.1', // xupsOutputVoltage (phase 1)
|
||||||
|
OUTPUT_CURRENT: '1.3.6.1.4.1.534.1.4.4.1.3.1', // xupsOutputCurrent (phase 1)
|
||||||
POWER_STATUS_VALUES: {
|
POWER_STATUS_VALUES: {
|
||||||
online: 3, // xupsOutputSource: 3=normal (mains power)
|
online: 3, // xupsOutputSource: 3=normal (mains power)
|
||||||
onBattery: 5, // xupsOutputSource: 5=battery
|
onBattery: 5, // xupsOutputSource: 5=battery
|
||||||
@@ -47,6 +59,10 @@ export class UpsOidSets {
|
|||||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
|
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
||||||
|
OUTPUT_LOAD: '1.3.6.1.2.1.33.1.4.4.1.5.1', // RFC 1628: upsOutputPercentLoad
|
||||||
|
OUTPUT_POWER: '1.3.6.1.2.1.33.1.4.4.1.4.1', // RFC 1628: upsOutputPower (watts)
|
||||||
|
OUTPUT_VOLTAGE: '1.3.6.1.2.1.33.1.4.4.1.2.1', // RFC 1628: upsOutputVoltage
|
||||||
|
OUTPUT_CURRENT: '1.3.6.1.2.1.33.1.4.4.1.3.1', // RFC 1628: upsOutputCurrent (0.1A scale)
|
||||||
POWER_STATUS_VALUES: {
|
POWER_STATUS_VALUES: {
|
||||||
online: 2, // tlUpsOutputSource: 2=normal (mains power)
|
online: 2, // tlUpsOutputSource: 2=normal (mains power)
|
||||||
onBattery: 3, // tlUpsOutputSource: 3=onBattery
|
onBattery: 3, // tlUpsOutputSource: 3=onBattery
|
||||||
@@ -58,6 +74,10 @@ export class UpsOidSets {
|
|||||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
|
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
||||||
|
OUTPUT_LOAD: '1.3.6.1.2.1.33.1.4.4.1.5.1', // RFC 1628: upsOutputPercentLoad
|
||||||
|
OUTPUT_POWER: '1.3.6.1.2.1.33.1.4.4.1.4.1', // RFC 1628: upsOutputPower (watts)
|
||||||
|
OUTPUT_VOLTAGE: '1.3.6.1.2.1.33.1.4.4.1.2.1', // RFC 1628: upsOutputVoltage
|
||||||
|
OUTPUT_CURRENT: '1.3.6.1.2.1.33.1.4.4.1.3.1', // RFC 1628: upsOutputCurrent (0.1A scale)
|
||||||
POWER_STATUS_VALUES: {
|
POWER_STATUS_VALUES: {
|
||||||
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
|
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
|
||||||
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
|
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
|
||||||
@@ -69,6 +89,10 @@ export class UpsOidSets {
|
|||||||
POWER_STATUS: '',
|
POWER_STATUS: '',
|
||||||
BATTERY_CAPACITY: '',
|
BATTERY_CAPACITY: '',
|
||||||
BATTERY_RUNTIME: '',
|
BATTERY_RUNTIME: '',
|
||||||
|
OUTPUT_LOAD: '',
|
||||||
|
OUTPUT_POWER: '',
|
||||||
|
OUTPUT_VOLTAGE: '',
|
||||||
|
OUTPUT_CURRENT: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,6 +114,10 @@ export class UpsOidSets {
|
|||||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
|
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
|
||||||
|
'output load': '1.3.6.1.2.1.33.1.4.4.1.5.1', // upsOutputPercentLoad (indexed by line)
|
||||||
|
'output power': '1.3.6.1.2.1.33.1.4.4.1.4.1', // upsOutputPower in watts (indexed by line)
|
||||||
|
'output voltage': '1.3.6.1.2.1.33.1.4.4.1.2.1', // upsOutputVoltage (indexed by line)
|
||||||
|
'output current': '1.3.6.1.2.1.33.1.4.4.1.3.1', // upsOutputCurrent in 0.1A (indexed by line)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ export interface IUpsStatus {
|
|||||||
batteryCapacity: number;
|
batteryCapacity: number;
|
||||||
/** Remaining runtime in minutes */
|
/** Remaining runtime in minutes */
|
||||||
batteryRuntime: number;
|
batteryRuntime: number;
|
||||||
|
/** Output load percentage (0-100) */
|
||||||
|
outputLoad: number;
|
||||||
|
/** Output power in watts */
|
||||||
|
outputPower: number;
|
||||||
|
/** Output voltage in volts */
|
||||||
|
outputVoltage: number;
|
||||||
|
/** Output current in amps */
|
||||||
|
outputCurrent: number;
|
||||||
/** Raw values from SNMP responses */
|
/** Raw values from SNMP responses */
|
||||||
raw: Record<string, any>;
|
raw: Record<string, any>;
|
||||||
}
|
}
|
||||||
@@ -28,6 +36,14 @@ export interface IOidSet {
|
|||||||
BATTERY_CAPACITY: string;
|
BATTERY_CAPACITY: string;
|
||||||
/** OID for battery runtime */
|
/** OID for battery runtime */
|
||||||
BATTERY_RUNTIME: string;
|
BATTERY_RUNTIME: string;
|
||||||
|
/** OID for output load percentage */
|
||||||
|
OUTPUT_LOAD: string;
|
||||||
|
/** OID for output power in watts */
|
||||||
|
OUTPUT_POWER: string;
|
||||||
|
/** OID for output voltage */
|
||||||
|
OUTPUT_VOLTAGE: string;
|
||||||
|
/** OID for output current */
|
||||||
|
OUTPUT_CURRENT: string;
|
||||||
/** Power status value mappings */
|
/** Power status value mappings */
|
||||||
POWER_STATUS_VALUES?: {
|
POWER_STATUS_VALUES?: {
|
||||||
/** SNMP value that indicates UPS is online (on AC power) */
|
/** SNMP value that indicates UPS is online (on AC power) */
|
||||||
|
|||||||
@@ -355,6 +355,9 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||||
|
|
||||||
|
// Display power metrics
|
||||||
|
logger.log(` Load: ${theme.highlight(status.outputLoad + '%')} Power: ${theme.highlight(status.outputPower + 'W')} Voltage: ${theme.highlight(status.outputVoltage + 'V')} Current: ${theme.highlight(status.outputCurrent + 'A')}`);
|
||||||
|
|
||||||
// Display host info
|
// Display host info
|
||||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user