Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 782c8c9555 | |||
| 463c32ebba | |||
| 51aa68ff8d | |||
| cb34ae5041 | |||
| 165c7d29bb | |||
| ff2dc00f31 |
@@ -5,19 +5,23 @@ 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)
|
||||
@@ -25,7 +29,9 @@ Download the appropriate binary for your platform from the assets below and make
|
||||
- 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.
|
||||
|
||||
@@ -9,7 +9,8 @@ import { spawn } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { platform, arch } from 'os';
|
||||
import { arch, platform } from 'os';
|
||||
import process from "node:process";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -25,12 +26,12 @@ function getBinaryName() {
|
||||
const platformMap = {
|
||||
'darwin': 'macos',
|
||||
'linux': 'linux',
|
||||
'win32': 'windows'
|
||||
'win32': 'windows',
|
||||
};
|
||||
|
||||
const archMap = {
|
||||
'x64': 'x64',
|
||||
'arm64': 'arm64'
|
||||
'arm64': 'arm64',
|
||||
};
|
||||
|
||||
const mappedPlatform = platformMap[plat];
|
||||
@@ -76,7 +77,7 @@ function executeBinary() {
|
||||
// Spawn the binary with all arguments passed through
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: 'inherit',
|
||||
shell: false
|
||||
shell: false,
|
||||
});
|
||||
|
||||
// Handle child process events
|
||||
@@ -95,7 +96,7 @@ function executeBinary() {
|
||||
|
||||
// Forward signals to child process
|
||||
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
||||
signals.forEach(signal => {
|
||||
signals.forEach((signal) => {
|
||||
process.on(signal, () => {
|
||||
if (!child.killed) {
|
||||
child.kill(signal);
|
||||
|
||||
120
changelog.md
120
changelog.md
@@ -1,69 +1,131 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-29 - 5.2.1 - fix(cli(ups-handler), systemd)
|
||||
add type guards and null checks for UPS configs; improve SNMP handling and prompts; guard version display
|
||||
## 2026-01-29 - 5.2.4 - fix()
|
||||
no changes
|
||||
|
||||
- Introduce a type guard ('id' in config && 'name' in config) to distinguish IUpsConfig from legacy INupstConfig and route fields (snmp, checkInterval, name, id) accordingly.
|
||||
- displayTestConfig now handles missing SNMP by logging 'Not configured' and returning, computes checkInterval/upsName/upsId correctly, and uses groups only for true UPS configs.
|
||||
- testConnection now safely derives snmpConfig for both config types, throws if SNMP is missing, and caps test timeout to 10s for probes.
|
||||
- Clear auth/priv credentials by setting undefined (instead of empty strings) when disabling security levels to avoid invalid/empty string values.
|
||||
- Expanded customOIDs to include OUTPUT_LOAD, OUTPUT_POWER, OUTPUT_VOLTAGE, OUTPUT_CURRENT with defaults; trim prompt input and document RFC 1628 fallbacks.
|
||||
- systemd.displayVersionInfo: guard against missing nupst (silent return) and avoid errors when printing version info; use ignored catch variables for clarity.
|
||||
- No files changed in the provided git diff; no commit or version bump required.
|
||||
|
||||
## 2026-01-29 - 5.2.3 - fix(core)
|
||||
fix lint/type issues and small refactors
|
||||
|
||||
- Add missing node:process imports in bin and scripts to ensure process is available
|
||||
- Remove unused imports and unused type imports (e.g. writeFileSync, IActionConfig) to reduce noise
|
||||
- Make some methods synchronous (service update, webhook call) to match actual usage
|
||||
- Tighten SNMP typings and linting: added deno-lint-ignore comments, renamed unused params with leading underscore, and use `as const` for securityLevel fallbacks
|
||||
- Improve error handling variable naming in systemd (use error instead of _error)
|
||||
- Annotate ANSI regex with deno-lint-ignore no-control-regex and remove unused color/symbol imports across CLI/daemon/logger
|
||||
|
||||
## 2026-01-29 - 5.2.2 - fix(core)
|
||||
tidy formatting and minor fixes across CLI, SNMP, HTTP server, migrations and packaging
|
||||
|
||||
- Normalize import ordering and improve logger/string formatting across many CLI handlers, daemon, systemd, actions and tests
|
||||
- Apply formatting tidies: trailing commas, newline fixes, and more consistent multiline strings
|
||||
- Allow BaseMigration methods to return either sync or async results (shouldRun/migrate signatures updated)
|
||||
- Improve SNMP manager and HTTP server logging/error messages and tighten some typings (raw SNMP types, server error typing)
|
||||
- Small robustness and messaging improvements in npm installer and wrapper (platform/arch mapping, error outputs)
|
||||
- Update tests and documentation layout/formatting for readability
|
||||
|
||||
## 2026-01-29 - 5.2.1 - fix(cli(ups-handler), systemd)
|
||||
|
||||
add type guards and null checks for UPS configs; improve SNMP handling and prompts; guard version
|
||||
display
|
||||
|
||||
- Introduce a type guard ('id' in config && 'name' in config) to distinguish IUpsConfig from legacy
|
||||
INupstConfig and route fields (snmp, checkInterval, name, id) accordingly.
|
||||
- displayTestConfig now handles missing SNMP by logging 'Not configured' and returning, computes
|
||||
checkInterval/upsName/upsId correctly, and uses groups only for true UPS configs.
|
||||
- testConnection now safely derives snmpConfig for both config types, throws if SNMP is missing, and
|
||||
caps test timeout to 10s for probes.
|
||||
- Clear auth/priv credentials by setting undefined (instead of empty strings) when disabling
|
||||
security levels to avoid invalid/empty string values.
|
||||
- Expanded customOIDs to include OUTPUT_LOAD, OUTPUT_POWER, OUTPUT_VOLTAGE, OUTPUT_CURRENT with
|
||||
defaults; trim prompt input and document RFC 1628 fallbacks.
|
||||
- systemd.displayVersionInfo: guard against missing nupst (silent return) and avoid errors when
|
||||
printing version info; use ignored catch variables for clarity.
|
||||
|
||||
## 2026-01-29 - 5.2.0 - feat(core)
|
||||
Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors
|
||||
|
||||
- Add ts/constants.ts to centralize timing, SNMP, webhook, script, shutdown and UI constants and replace magic numbers across the codebase
|
||||
- Introduce helpers/prompt.ts with createPrompt() and withPrompt() and refactor CLI handlers to use these helpers (cleaner prompt lifecycle)
|
||||
- Add webhook action support: ts/actions/webhook-action.ts, IWebhookPayload type, and export from ts/actions/index.ts
|
||||
- Enhance ShutdownAction safety checks (only trigger onBattery, stricter transition rules) and use constants/UI widths for displays
|
||||
- Refactor SNMP manager to use logger instead of console, pull SNMP defaults from constants, improved debug output, and add INupstAccessor interface to break circular dependency (Nupst now implements the interface)
|
||||
- Update many CLI and core types (stronger typing for configs/actions), expand tests and update README and npmextra.json to document new features
|
||||
Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with
|
||||
safety and SNMP refactors
|
||||
|
||||
- Add ts/constants.ts to centralize timing, SNMP, webhook, script, shutdown and UI constants and
|
||||
replace magic numbers across the codebase
|
||||
- Introduce helpers/prompt.ts with createPrompt() and withPrompt() and refactor CLI handlers to use
|
||||
these helpers (cleaner prompt lifecycle)
|
||||
- Add webhook action support: ts/actions/webhook-action.ts, IWebhookPayload type, and export from
|
||||
ts/actions/index.ts
|
||||
- Enhance ShutdownAction safety checks (only trigger onBattery, stricter transition rules) and use
|
||||
constants/UI widths for displays
|
||||
- Refactor SNMP manager to use logger instead of console, pull SNMP defaults from constants,
|
||||
improved debug output, and add INupstAccessor interface to break circular dependency (Nupst now
|
||||
implements the interface)
|
||||
- Update many CLI and core types (stronger typing for configs/actions), expand tests and update
|
||||
README and npmextra.json to document new features
|
||||
|
||||
## 2025-11-09 - 5.1.11 - fix(readme)
|
||||
Update README installation instructions to recommend automated installer script and clarify npm installation
|
||||
|
||||
- Replace the previous 'Via npm (NEW! - Recommended)' section with a clear 'Automated Installer Script (Recommended)' section and example curl installer.
|
||||
- Move npm installation instructions into an 'Alternative: Via npm' subsection and clarify that the npm package downloads the appropriate pre-compiled binary for the platform during installation.
|
||||
Update README installation instructions to recommend automated installer script and clarify npm
|
||||
installation
|
||||
|
||||
- Replace the previous 'Via npm (NEW! - Recommended)' section with a clear 'Automated Installer
|
||||
Script (Recommended)' section and example curl installer.
|
||||
- Move npm installation instructions into an 'Alternative: Via npm' subsection and clarify that the
|
||||
npm package downloads the appropriate pre-compiled binary for the platform during installation.
|
||||
- Remove the 'NEW!' badge and streamline notes about binary downloads and installation methods.
|
||||
|
||||
## 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)
|
||||
- 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
|
||||
- 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)
|
||||
- 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)
|
||||
- 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 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 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "5.2.1",
|
||||
"version": "5.2.4",
|
||||
"exports": "./mod.ts",
|
||||
"nodeModulesDir": "auto",
|
||||
"tasks": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "5.2.1",
|
||||
"version": "5.2.4",
|
||||
"description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies",
|
||||
"keywords": [
|
||||
"ups",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
1. **Prompt Utility (`ts/helpers/prompt.ts`)**
|
||||
- Extracted readline/prompt pattern from all CLI handlers
|
||||
- Provides `createPrompt()` and `withPrompt()` helper functions
|
||||
- Used in: `ups-handler.ts`, `group-handler.ts`, `service-handler.ts`, `action-handler.ts`, `feature-handler.ts`
|
||||
- Used in: `ups-handler.ts`, `group-handler.ts`, `service-handler.ts`, `action-handler.ts`,
|
||||
`feature-handler.ts`
|
||||
|
||||
2. **Constants File (`ts/constants.ts`)**
|
||||
- Centralized all magic numbers (timeouts, intervals, thresholds)
|
||||
@@ -21,7 +22,8 @@
|
||||
### Phase 2 - Type Safety
|
||||
|
||||
4. **Circular Dependency Fix (`ts/interfaces/nupst-accessor.ts`)**
|
||||
- Created `INupstAccessor` interface to break the circular dependency between `Nupst` and `NupstSnmp`
|
||||
- Created `INupstAccessor` interface to break the circular dependency between `Nupst` and
|
||||
`NupstSnmp`
|
||||
- `NupstSnmp.nupst` property now uses the interface instead of `any`
|
||||
|
||||
5. **Webhook Payload Interface (`ts/actions/webhook-action.ts`)**
|
||||
@@ -30,11 +32,13 @@
|
||||
|
||||
6. **CLI Handler Type Safety**
|
||||
- Replaced `any` types in `ups-handler.ts` and `group-handler.ts` with proper interfaces
|
||||
- Uses: `IUpsConfig`, `INupstConfig`, `ISnmpConfig`, `IActionConfig`, `IThresholds`, `ISnmpUpsStatus`
|
||||
- Uses: `IUpsConfig`, `INupstConfig`, `ISnmpConfig`, `IActionConfig`, `IThresholds`,
|
||||
`ISnmpUpsStatus`
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **SNMP Manager**: Uses `INupstAccessor` interface (not direct `Nupst` reference) to avoid circular imports
|
||||
- **SNMP Manager**: Uses `INupstAccessor` interface (not direct `Nupst` reference) to avoid circular
|
||||
imports
|
||||
- **CLI Handlers**: All use the `helpers.withPrompt()` utility for interactive input
|
||||
- **Constants**: All timing values should be referenced from `ts/constants.ts`
|
||||
- **Actions**: Use `IActionConfig` from `ts/actions/base-action.ts` for action configuration
|
||||
|
||||
40
readme.md
40
readme.md
@@ -10,7 +10,11 @@ no setup, just run.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit
|
||||
[community.foss.global/](https://community.foss.global/). This is the central community hub for all
|
||||
issue reporting. Developers who sign and comply with our contribution agreement and go through
|
||||
identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull
|
||||
Requests directly.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@@ -131,7 +135,8 @@ Alternatively, NUPST can be installed via npm:
|
||||
npm install -g @serve.zone/nupst
|
||||
```
|
||||
|
||||
**Note:** This method downloads the appropriate pre-compiled binary for your platform during installation.
|
||||
**Note:** This method downloads the appropriate pre-compiled binary for your platform during
|
||||
installation.
|
||||
|
||||
### Verify Installation
|
||||
|
||||
@@ -475,7 +480,7 @@ Actions define automated responses to UPS conditions:
|
||||
**Webhook-Specific Fields:**
|
||||
|
||||
| Field | Description | Values |
|
||||
| ---------------- | -------------------------- | ---------------- |
|
||||
| ---------------- | --------------------- | --------------- |
|
||||
| `webhookUrl` | URL to call | HTTP/HTTPS URL |
|
||||
| `webhookMethod` | HTTP method | 'POST' or 'GET' |
|
||||
| `webhookTimeout` | Request timeout in ms | Default: 10000 |
|
||||
@@ -483,7 +488,7 @@ Actions define automated responses to UPS conditions:
|
||||
**Script-Specific Fields:**
|
||||
|
||||
| Field | Description | Values |
|
||||
| --------------- | ---------------------------------- | ----------------------- |
|
||||
| --------------- | -------------------------------- | ------------------- |
|
||||
| `scriptPath` | Script filename in `/etc/nupst/` | Must end with `.sh` |
|
||||
| `scriptTimeout` | Execution timeout in ms | Default: 60000 |
|
||||
|
||||
@@ -874,7 +879,8 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
||||
|
||||
### Configuration Compatibility
|
||||
|
||||
Your v3.x configuration is **fully compatible**. The migration system automatically converts older formats to the current version.
|
||||
Your v3.x configuration is **fully compatible**. The migration system automatically converts older
|
||||
formats to the current version.
|
||||
|
||||
## 💻 Development
|
||||
|
||||
@@ -927,21 +933,31 @@ nupst/
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can
|
||||
be found in the [LICENSE](./LICENSE) 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.
|
||||
**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
|
||||
|
||||
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 or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
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 or third parties, 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 or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the
|
||||
guidelines of the respective third-party owners, and any usage must be approved in writing.
|
||||
Third-party trademarks used herein are the property of their respective owners and used only in a
|
||||
descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or 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.
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
// deno-lint-ignore-file no-unused-vars
|
||||
|
||||
/**
|
||||
* 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 { arch, platform } from 'os';
|
||||
import { chmodSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import https from 'https';
|
||||
import { pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import { createWriteStream } from 'fs';
|
||||
import process from "node:process";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -29,12 +31,12 @@ function getBinaryInfo() {
|
||||
const platformMap = {
|
||||
'darwin': 'macos',
|
||||
'linux': 'linux',
|
||||
'win32': 'windows'
|
||||
'win32': 'windows',
|
||||
};
|
||||
|
||||
const archMap = {
|
||||
'x64': 'x64',
|
||||
'arm64': 'arm64'
|
||||
'arm64': 'arm64',
|
||||
};
|
||||
|
||||
const mappedPlatform = platformMap[plat];
|
||||
@@ -54,7 +56,7 @@ function getBinaryInfo() {
|
||||
platform: mappedPlatform,
|
||||
arch: mappedArch,
|
||||
binaryName,
|
||||
originalPlatform: plat
|
||||
originalPlatform: plat,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -122,7 +124,9 @@ async function main() {
|
||||
const binaryInfo = getBinaryInfo();
|
||||
|
||||
if (!binaryInfo.supported) {
|
||||
console.error(`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`);
|
||||
console.error(
|
||||
`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`,
|
||||
);
|
||||
console.error('');
|
||||
console.error('Supported platforms:');
|
||||
console.error(' • Linux (x64, arm64)');
|
||||
@@ -185,7 +189,9 @@ async function main() {
|
||||
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');
|
||||
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)) {
|
||||
@@ -225,7 +231,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// Run the installation
|
||||
main().catch(err => {
|
||||
main().catch((err) => {
|
||||
console.error(`❌ Installation failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
# Manual Docker Testing Scripts
|
||||
|
||||
This directory contains scripts for manually testing NUPST installation and migration in Docker containers with systemd support.
|
||||
This directory contains scripts for manually testing NUPST installation and migration in Docker
|
||||
containers with systemd support.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -15,12 +16,14 @@ This directory contains scripts for manually testing NUPST installation and migr
|
||||
Creates a Docker container with systemd and installs NUPST v3.
|
||||
|
||||
**What it does:**
|
||||
|
||||
- Creates Ubuntu 22.04 container with systemd enabled
|
||||
- Installs NUPST v3 from commit `806f81c6` (last v3 version)
|
||||
- Enables and starts the systemd service
|
||||
- Leaves container running for testing
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
chmod +x 01-setup-v3-container.sh
|
||||
./01-setup-v3-container.sh
|
||||
@@ -33,6 +36,7 @@ chmod +x 01-setup-v3-container.sh
|
||||
Tests the migration from v3 to v4.
|
||||
|
||||
**What it does:**
|
||||
|
||||
- Checks current v3 installation
|
||||
- Pulls v4 code from `migration/deno-v4` branch
|
||||
- Runs install.sh (should auto-detect and migrate)
|
||||
@@ -40,6 +44,7 @@ Tests the migration from v3 to v4.
|
||||
- Tests basic commands
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
chmod +x 02-test-v3-to-v4-migration.sh
|
||||
./02-test-v3-to-v4-migration.sh
|
||||
@@ -52,6 +57,7 @@ chmod +x 02-test-v3-to-v4-migration.sh
|
||||
Removes the test container.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
chmod +x 03-cleanup.sh
|
||||
./03-cleanup.sh
|
||||
@@ -134,16 +140,19 @@ docker rm -f nupst-test-v3
|
||||
## Troubleshooting
|
||||
|
||||
### Container won't start
|
||||
|
||||
- Ensure Docker daemon is running
|
||||
- Check you have privileged access
|
||||
- Try: `docker logs nupst-test-v3`
|
||||
|
||||
### Systemd not working in container
|
||||
|
||||
- Requires Linux host (not macOS/Windows)
|
||||
- Needs `--privileged` and cgroup volume mounts
|
||||
- Check: `docker exec nupst-test-v3 systemctl --version`
|
||||
|
||||
### Migration fails
|
||||
|
||||
- Check logs: `docker exec nupst-test-v3 journalctl -xe`
|
||||
- Verify install.sh ran: `docker exec nupst-test-v3 ls -la /opt/nupst/`
|
||||
- Check service: `docker exec nupst-test-v3 systemctl status nupst`
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* Run with: deno run --allow-all test/showcase.ts
|
||||
*/
|
||||
|
||||
import { logger, type ITableColumn } from '../ts/logger.ts';
|
||||
import { theme, symbols, getBatteryColor, formatPowerStatus } from '../ts/colors.ts';
|
||||
import { type ITableColumn, logger } from '../ts/logger.ts';
|
||||
import { formatPowerStatus, getBatteryColor, symbols, theme } from '../ts/colors.ts';
|
||||
|
||||
console.log('');
|
||||
console.log('═'.repeat(80));
|
||||
@@ -38,31 +38,51 @@ logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Success Box (Green)', [
|
||||
logger.logBox(
|
||||
'Success Box (Green)',
|
||||
[
|
||||
'Used for successful operations',
|
||||
'Installation complete, service started, etc.',
|
||||
], 60, 'success');
|
||||
],
|
||||
60,
|
||||
'success',
|
||||
);
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Error Box (Red)', [
|
||||
logger.logBox(
|
||||
'Error Box (Red)',
|
||||
[
|
||||
'Used for critical errors and failures',
|
||||
'Configuration errors, connection failures, etc.',
|
||||
], 60, 'error');
|
||||
],
|
||||
60,
|
||||
'error',
|
||||
);
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Warning Box (Yellow)', [
|
||||
logger.logBox(
|
||||
'Warning Box (Yellow)',
|
||||
[
|
||||
'Used for warnings and deprecations',
|
||||
'Old command format, missing config, etc.',
|
||||
], 60, 'warning');
|
||||
],
|
||||
60,
|
||||
'warning',
|
||||
);
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Info Box (Cyan)', [
|
||||
logger.logBox(
|
||||
'Info Box (Cyan)',
|
||||
[
|
||||
'Used for informational messages',
|
||||
'Version info, update available, etc.',
|
||||
], 60, 'info');
|
||||
],
|
||||
60,
|
||||
'info',
|
||||
);
|
||||
|
||||
console.log('');
|
||||
|
||||
@@ -112,15 +132,24 @@ const upsColumns: ITableColumn[] = [
|
||||
{ header: 'ID', key: 'id' },
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Host', key: 'host' },
|
||||
{ header: 'Status', key: 'status', color: (v) => {
|
||||
{
|
||||
header: 'Status',
|
||||
key: 'status',
|
||||
color: (v) => {
|
||||
if (v.includes('Online')) return theme.success(v);
|
||||
if (v.includes('Battery')) return theme.warning(v);
|
||||
return theme.dim(v);
|
||||
}},
|
||||
{ header: 'Battery', key: 'battery', align: 'right', color: (v) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Battery',
|
||||
key: 'battery',
|
||||
align: 'right',
|
||||
color: (v) => {
|
||||
const pct = parseInt(v);
|
||||
return getBatteryColor(pct)(v);
|
||||
}},
|
||||
},
|
||||
},
|
||||
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||
];
|
||||
|
||||
|
||||
47
test/test.ts
47
test/test.ts
@@ -1,9 +1,9 @@
|
||||
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0';
|
||||
import { NupstSnmp } from '../ts/snmp/manager.ts';
|
||||
import { UpsOidSets } from '../ts/snmp/oid-sets.ts';
|
||||
import type { ISnmpConfig, TUpsModel, IOidSet } from '../ts/snmp/types.ts';
|
||||
import type { IOidSet, ISnmpConfig, TUpsModel } from '../ts/snmp/types.ts';
|
||||
import { shortId } from '../ts/helpers/shortid.ts';
|
||||
import { TIMING, SNMP, THRESHOLDS, HTTP_SERVER, UI } from '../ts/constants.ts';
|
||||
import { HTTP_SERVER, SNMP, THRESHOLDS, TIMING, UI } from '../ts/constants.ts';
|
||||
import { Action, type IActionContext } from '../ts/actions/base-action.ts';
|
||||
|
||||
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
|
||||
@@ -56,8 +56,14 @@ Deno.test('SNMP constants: port is 161', () => {
|
||||
});
|
||||
|
||||
Deno.test('SNMP constants: timeouts increase with security level', () => {
|
||||
assert(SNMP.TIMEOUT_NO_AUTH_MS <= SNMP.TIMEOUT_AUTH_MS, 'Auth timeout should be >= noAuth timeout');
|
||||
assert(SNMP.TIMEOUT_AUTH_MS <= SNMP.TIMEOUT_AUTH_PRIV_MS, 'AuthPriv timeout should be >= Auth timeout');
|
||||
assert(
|
||||
SNMP.TIMEOUT_NO_AUTH_MS <= SNMP.TIMEOUT_AUTH_MS,
|
||||
'Auth timeout should be >= noAuth timeout',
|
||||
);
|
||||
assert(
|
||||
SNMP.TIMEOUT_AUTH_MS <= SNMP.TIMEOUT_AUTH_PRIV_MS,
|
||||
'AuthPriv timeout should be >= Auth timeout',
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test('THRESHOLDS constants: defaults are reasonable', () => {
|
||||
@@ -92,21 +98,21 @@ Deno.test('UpsOidSets: all models have OID sets', () => {
|
||||
Deno.test('UpsOidSets: all non-custom models have complete OIDs', () => {
|
||||
const requiredOids = ['POWER_STATUS', 'BATTERY_CAPACITY', 'BATTERY_RUNTIME', 'OUTPUT_LOAD'];
|
||||
|
||||
for (const model of UPS_MODELS.filter(m => m !== 'custom')) {
|
||||
for (const model of UPS_MODELS.filter((m) => m !== 'custom')) {
|
||||
const oidSet = UpsOidSets.getOidSet(model);
|
||||
|
||||
for (const oid of requiredOids) {
|
||||
const value = oidSet[oid as keyof IOidSet];
|
||||
assert(
|
||||
typeof value === 'string' && value.length > 0,
|
||||
`${model} should have non-empty ${oid}`
|
||||
`${model} should have non-empty ${oid}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test('UpsOidSets: power status values defined for non-custom models', () => {
|
||||
for (const model of UPS_MODELS.filter(m => m !== 'custom')) {
|
||||
for (const model of UPS_MODELS.filter((m) => m !== 'custom')) {
|
||||
const oidSet = UpsOidSets.getOidSet(model);
|
||||
assertExists(oidSet.POWER_STATUS_VALUES, `${model} should have POWER_STATUS_VALUES`);
|
||||
assertExists(oidSet.POWER_STATUS_VALUES?.online, `${model} should have online value`);
|
||||
@@ -200,11 +206,11 @@ Deno.test('Action.shouldExecute: onlyPowerChanges mode', () => {
|
||||
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
||||
true
|
||||
true,
|
||||
);
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({ triggerReason: 'thresholdViolation' })),
|
||||
false
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -218,13 +224,13 @@ Deno.test('Action.shouldExecute: onlyThresholds mode', () => {
|
||||
// Below thresholds - should execute
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({ batteryCapacity: 50, batteryRuntime: 10 })),
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
// Above thresholds - should not execute
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({ batteryCapacity: 100, batteryRuntime: 60 })),
|
||||
false
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -248,7 +254,7 @@ Deno.test('Action.shouldExecute: powerChangesAndThresholds mode (default)', () =
|
||||
// Power change - should execute
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
// Threshold violation - should execute
|
||||
@@ -257,7 +263,7 @@ Deno.test('Action.shouldExecute: powerChangesAndThresholds mode (default)', () =
|
||||
triggerReason: 'thresholdViolation',
|
||||
batteryCapacity: 50,
|
||||
})),
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
// No power change and above thresholds - should not execute
|
||||
@@ -267,7 +273,7 @@ Deno.test('Action.shouldExecute: powerChangesAndThresholds mode (default)', () =
|
||||
batteryCapacity: 100,
|
||||
batteryRuntime: 60,
|
||||
})),
|
||||
false
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -279,11 +285,11 @@ Deno.test('Action.shouldExecute: anyChange mode always returns true', () => {
|
||||
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
||||
true
|
||||
true,
|
||||
);
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({ triggerReason: 'thresholdViolation' })),
|
||||
true
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -339,7 +345,7 @@ async function testUpsConnection(
|
||||
assertExists(status, 'Status should exist');
|
||||
assert(
|
||||
['online', 'onBattery', 'unknown'].includes(status.powerStatus),
|
||||
`Power status should be valid: ${status.powerStatus}`
|
||||
`Power status should be valid: ${status.powerStatus}`,
|
||||
);
|
||||
assertEquals(typeof status.batteryCapacity, 'number', 'Battery capacity should be a number');
|
||||
assertEquals(typeof status.batteryRuntime, 'number', 'Battery runtime should be a number');
|
||||
@@ -347,9 +353,12 @@ async function testUpsConnection(
|
||||
// Validate ranges
|
||||
assert(
|
||||
status.batteryCapacity >= 0 && status.batteryCapacity <= 100,
|
||||
`Battery capacity should be 0-100: ${status.batteryCapacity}`
|
||||
`Battery capacity should be 0-100: ${status.batteryCapacity}`,
|
||||
);
|
||||
assert(
|
||||
status.batteryRuntime >= 0,
|
||||
`Battery runtime should be non-negative: ${status.batteryRuntime}`,
|
||||
);
|
||||
assert(status.batteryRuntime >= 0, `Battery runtime should be non-negative: ${status.batteryRuntime}`);
|
||||
}
|
||||
|
||||
// Create SNMP instance for integration tests
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '5.2.1',
|
||||
version: '5.2.4',
|
||||
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
||||
import process from 'node:process';
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
|
||||
import { Action, type IActionContext } from './base-action.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -26,7 +26,11 @@ export class ScriptAction extends Action {
|
||||
async execute(context: IActionContext): Promise<void> {
|
||||
// Check if we should execute based on trigger mode
|
||||
if (!this.shouldExecute(context)) {
|
||||
logger.info(`Script action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
|
||||
logger.info(
|
||||
`Script action skipped (trigger mode: ${
|
||||
this.config.triggerMode || 'powerChangesAndThresholds'
|
||||
})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ export class ShutdownAction extends Action {
|
||||
// CRITICAL SAFETY CHECK: Shutdown should NEVER trigger unless UPS is on battery
|
||||
// A low battery while on grid power is not an emergency (the battery is charging)
|
||||
if (context.powerStatus !== 'onBattery') {
|
||||
logger.info(`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`);
|
||||
logger.info(
|
||||
`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -54,7 +56,9 @@ export class ShutdownAction extends Action {
|
||||
if (context.triggerReason === 'powerStatusChange') {
|
||||
// 'onlyThresholds' mode ignores power status changes
|
||||
if (mode === 'onlyThresholds') {
|
||||
logger.info('Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change');
|
||||
logger.info(
|
||||
'Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,15 +75,22 @@ export class ShutdownAction extends Action {
|
||||
// the daemon for testing, or the UPS may have been on battery for a while.
|
||||
// Only trigger if mode explicitly includes power changes.
|
||||
if (prev === 'unknown') {
|
||||
if (mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' || mode === 'anyChange') {
|
||||
logger.info('Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)');
|
||||
if (
|
||||
mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' ||
|
||||
mode === 'anyChange'
|
||||
) {
|
||||
logger.info(
|
||||
'Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Other transitions (e.g., onBattery → onBattery) should not trigger
|
||||
logger.info(`Shutdown action skipped: non-emergency transition (${prev} → ${context.powerStatus})`);
|
||||
logger.info(
|
||||
`Shutdown action skipped: non-emergency transition (${prev} → ${context.powerStatus})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -98,7 +109,11 @@ export class ShutdownAction extends Action {
|
||||
async execute(context: IActionContext): Promise<void> {
|
||||
// Check if we should execute based on trigger mode and thresholds
|
||||
if (!this.shouldExecute(context)) {
|
||||
logger.info(`Shutdown action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
|
||||
logger.info(
|
||||
`Shutdown action skipped (trigger mode: ${
|
||||
this.config.triggerMode || 'powerChangesAndThresholds'
|
||||
})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import { URL } from 'node:url';
|
||||
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
|
||||
import { Action, type IActionContext } from './base-action.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
import { WEBHOOK } from '../constants.ts';
|
||||
|
||||
@@ -46,7 +46,11 @@ export class WebhookAction extends Action {
|
||||
async execute(context: IActionContext): Promise<void> {
|
||||
// Check if we should execute based on trigger mode
|
||||
if (!this.shouldExecute(context)) {
|
||||
logger.info(`Webhook action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
|
||||
logger.info(
|
||||
`Webhook action skipped (trigger mode: ${
|
||||
this.config.triggerMode || 'powerChangesAndThresholds'
|
||||
})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +81,7 @@ export class WebhookAction extends Action {
|
||||
* @param method HTTP method (GET or POST)
|
||||
* @param timeout Request timeout in milliseconds
|
||||
*/
|
||||
private async callWebhook(
|
||||
private callWebhook(
|
||||
context: IActionContext,
|
||||
method: 'GET' | 'POST',
|
||||
timeout: number,
|
||||
|
||||
99
ts/cli.ts
99
ts/cli.ts
@@ -1,7 +1,7 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from './nupst.ts';
|
||||
import { logger, type ITableColumn } from './logger.ts';
|
||||
import { theme, symbols } from './colors.ts';
|
||||
import { type ITableColumn, logger } from './logger.ts';
|
||||
import { theme } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Class for handling CLI commands
|
||||
@@ -287,10 +287,15 @@ export class NupstCli {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (_error) {
|
||||
logger.logBox('Configuration Error', [
|
||||
logger.logBox(
|
||||
'Configuration Error',
|
||||
[
|
||||
'No configuration found.',
|
||||
"Please run 'nupst ups add' first to create a configuration.",
|
||||
], 50, 'error');
|
||||
],
|
||||
50,
|
||||
'error',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -303,14 +308,19 @@ export class NupstCli {
|
||||
|
||||
// Overview Box
|
||||
logger.log('');
|
||||
logger.logBox('NUPST Configuration', [
|
||||
logger.logBox(
|
||||
'NUPST Configuration',
|
||||
[
|
||||
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
|
||||
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
|
||||
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
|
||||
'',
|
||||
theme.dim('Configuration File:'),
|
||||
` ${theme.path('/etc/nupst/config.json')}`,
|
||||
], 60, 'info');
|
||||
],
|
||||
60,
|
||||
'info',
|
||||
);
|
||||
|
||||
// HTTP Server Status (if configured)
|
||||
if (config.httpServer) {
|
||||
@@ -319,17 +329,24 @@ export class NupstCli {
|
||||
: theme.dim('Disabled');
|
||||
|
||||
logger.log('');
|
||||
logger.logBox('HTTP Server', [
|
||||
logger.logBox(
|
||||
'HTTP Server',
|
||||
[
|
||||
`Status: ${serverStatus}`,
|
||||
...(config.httpServer.enabled ? [
|
||||
...(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');
|
||||
]
|
||||
: []),
|
||||
],
|
||||
70,
|
||||
config.httpServer.enabled ? 'success' : 'default',
|
||||
);
|
||||
}
|
||||
|
||||
// UPS Devices Table
|
||||
@@ -394,14 +411,21 @@ export class NupstCli {
|
||||
// === Legacy Single UPS Configuration ===
|
||||
|
||||
if (!config.snmp) {
|
||||
logger.logBox('Configuration Error', [
|
||||
logger.logBox(
|
||||
'Configuration Error',
|
||||
[
|
||||
'Error: Legacy configuration missing SNMP settings',
|
||||
], 60, 'error');
|
||||
],
|
||||
60,
|
||||
'error',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
logger.logBox('NUPST Configuration (Legacy)', [
|
||||
logger.logBox(
|
||||
'NUPST Configuration (Legacy)',
|
||||
[
|
||||
theme.warning('Legacy single-UPS configuration format'),
|
||||
'',
|
||||
theme.dim('SNMP Settings:'),
|
||||
@@ -411,24 +435,21 @@ export class NupstCli {
|
||||
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
||||
...(config.snmp.version === 1 || config.snmp.version === 2
|
||||
? [` Community: ${config.snmp.community}`]
|
||||
: []
|
||||
),
|
||||
: []),
|
||||
...(config.snmp.version === 3
|
||||
? [
|
||||
` Security Level: ${config.snmp.securityLevel}`,
|
||||
` Username: ${config.snmp.username}`,
|
||||
...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv'
|
||||
...(config.snmp.securityLevel === 'authNoPriv' ||
|
||||
config.snmp.securityLevel === 'authPriv'
|
||||
? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`]
|
||||
: []
|
||||
),
|
||||
: []),
|
||||
...(config.snmp.securityLevel === 'authPriv'
|
||||
? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`]
|
||||
: []
|
||||
),
|
||||
: []),
|
||||
` Timeout: ${config.snmp.timeout / 1000} seconds`,
|
||||
]
|
||||
: []
|
||||
),
|
||||
: []),
|
||||
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
|
||||
? [
|
||||
theme.dim('Custom OIDs:'),
|
||||
@@ -436,8 +457,7 @@ export class NupstCli {
|
||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
|
||||
]
|
||||
: []
|
||||
),
|
||||
: []),
|
||||
'',
|
||||
|
||||
` Check Interval: ${config.checkInterval / 1000} seconds`,
|
||||
@@ -447,7 +467,10 @@ export class NupstCli {
|
||||
'',
|
||||
theme.warning('Note: Using legacy single-UPS configuration format.'),
|
||||
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
|
||||
], 70, 'warning');
|
||||
],
|
||||
70,
|
||||
'warning',
|
||||
);
|
||||
}
|
||||
|
||||
// Service Status
|
||||
@@ -458,10 +481,15 @@ export class NupstCli {
|
||||
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
|
||||
|
||||
logger.log('');
|
||||
logger.logBox('Service Status', [
|
||||
logger.logBox(
|
||||
'Service Status',
|
||||
[
|
||||
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
|
||||
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
|
||||
], 50, isActive ? 'success' : 'default');
|
||||
],
|
||||
50,
|
||||
isActive ? 'success' : 'default',
|
||||
);
|
||||
logger.log('');
|
||||
} catch (_error) {
|
||||
// Ignore errors checking service status
|
||||
@@ -514,8 +542,16 @@ export class NupstCli {
|
||||
|
||||
// Service subcommands
|
||||
logger.log(theme.info('Service Subcommands:'));
|
||||
this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)'));
|
||||
this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)'));
|
||||
this.printCommand(
|
||||
'nupst service enable',
|
||||
'Install and enable systemd service',
|
||||
theme.dim('(requires root)'),
|
||||
);
|
||||
this.printCommand(
|
||||
'nupst service disable',
|
||||
'Stop and disable systemd service',
|
||||
theme.dim('(requires root)'),
|
||||
);
|
||||
this.printCommand('nupst service start', 'Start the systemd service');
|
||||
this.printCommand('nupst service stop', 'Stop the systemd service');
|
||||
this.printCommand('nupst service restart', 'Restart the systemd service');
|
||||
@@ -545,7 +581,10 @@ export class NupstCli {
|
||||
logger.log(theme.info('Action Subcommands:'));
|
||||
this.printCommand('nupst action add <target-id>', 'Add a new action to a UPS or group');
|
||||
this.printCommand('nupst action remove <target-id> <index>', 'Remove an action by index');
|
||||
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('');
|
||||
|
||||
// Feature subcommands
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import process from 'node:process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { theme, symbols } from '../colors.ts';
|
||||
import { type ITableColumn, logger } from '../logger.ts';
|
||||
import { symbols, theme } from '../colors.ts';
|
||||
import type { IActionConfig } from '../actions/base-action.ts';
|
||||
import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
|
||||
import type { IGroupConfig, IUpsConfig } from '../daemon.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,9 @@ export class ActionHandler {
|
||||
if (!ups && !group) {
|
||||
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||
logger.log(
|
||||
` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`,
|
||||
);
|
||||
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||
logger.log('');
|
||||
process.exit(1);
|
||||
@@ -90,12 +92,16 @@ export class ActionHandler {
|
||||
// Trigger mode
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('Trigger mode:')}`);
|
||||
logger.log(` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`);
|
||||
logger.log(
|
||||
` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`,
|
||||
);
|
||||
logger.log(
|
||||
` ${theme.dim('2)')} onlyThresholds - Trigger only when thresholds are violated`,
|
||||
);
|
||||
logger.log(
|
||||
` ${theme.dim('3)')} powerChangesAndThresholds - Trigger on power change AND thresholds`,
|
||||
` ${
|
||||
theme.dim('3)')
|
||||
} powerChangesAndThresholds - Trigger on power change AND thresholds`,
|
||||
);
|
||||
logger.log(` ${theme.dim('4)')} anyChange - Trigger on any status change`);
|
||||
const triggerChoice = await prompt(` ${theme.dim('Choice')} ${theme.dim('[2]:')} `);
|
||||
@@ -158,7 +164,9 @@ export class ActionHandler {
|
||||
if (!targetId || !actionIndexStr) {
|
||||
logger.error('Target ID and action index are required');
|
||||
logger.log(
|
||||
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id|group-id> <action-index>')}`,
|
||||
` ${theme.dim('Usage:')} ${
|
||||
theme.command('nupst action remove <ups-id|group-id> <action-index>')
|
||||
}`,
|
||||
);
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
|
||||
@@ -182,7 +190,9 @@ export class ActionHandler {
|
||||
if (!ups && !group) {
|
||||
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||
logger.log(
|
||||
` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`,
|
||||
);
|
||||
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||
logger.log('');
|
||||
process.exit(1);
|
||||
@@ -200,7 +210,9 @@ export class ActionHandler {
|
||||
|
||||
if (actionIndex >= target!.actions.length) {
|
||||
logger.error(
|
||||
`Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
|
||||
`Invalid action index. ${targetType} '${targetName}' has ${
|
||||
target!.actions.length
|
||||
} action(s) (index 0-${target!.actions.length - 1})`,
|
||||
);
|
||||
logger.log('');
|
||||
logger.log(
|
||||
@@ -220,7 +232,9 @@ export class ActionHandler {
|
||||
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
|
||||
if (removedAction.thresholds) {
|
||||
logger.log(
|
||||
` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
|
||||
` ${
|
||||
theme.dim('Thresholds:')
|
||||
} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
|
||||
);
|
||||
}
|
||||
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
|
||||
@@ -248,8 +262,12 @@ export class ActionHandler {
|
||||
if (!ups && !group) {
|
||||
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||
logger.log(
|
||||
` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`,
|
||||
);
|
||||
logger.log(
|
||||
` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`,
|
||||
);
|
||||
logger.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -287,7 +305,9 @@ export class ActionHandler {
|
||||
logger.log(` ${theme.dim('No actions configured')}`);
|
||||
logger.log('');
|
||||
logger.log(
|
||||
` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
|
||||
` ${theme.dim('Add an action:')} ${
|
||||
theme.command('nupst action add <ups-id|group-id>')
|
||||
}`,
|
||||
);
|
||||
logger.log('');
|
||||
}
|
||||
@@ -308,7 +328,9 @@ export class ActionHandler {
|
||||
targetType: 'UPS' | 'Group',
|
||||
): void {
|
||||
logger.log(
|
||||
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
|
||||
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${
|
||||
theme.dim(`(${target.id})`)
|
||||
}`,
|
||||
);
|
||||
logger.log('');
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import process from 'node:process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
@@ -29,7 +28,9 @@ export class FeatureHandler {
|
||||
await this.runHttpServerConfig(prompt);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +150,9 @@ export class FeatureHandler {
|
||||
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 -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`,
|
||||
);
|
||||
logger.logBoxLine(` curl "http://localhost:${port}${finalPath}?token=${authToken}"`);
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
@@ -165,7 +168,8 @@ export class FeatureHandler {
|
||||
*/
|
||||
private async restartServiceIfRunning(): Promise<void> {
|
||||
try {
|
||||
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
||||
const isActive =
|
||||
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
||||
|
||||
if (isActive) {
|
||||
logger.log('');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import process from 'node:process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { type ITableColumn, logger } from '../logger.ts';
|
||||
import { theme } from '../colors.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
import type { IGroupConfig, IUpsConfig, INupstConfig } from '../daemon.ts';
|
||||
import type { IGroupConfig, INupstConfig, IUpsConfig } from '../daemon.ts';
|
||||
|
||||
/**
|
||||
* Class for handling group-related CLI commands
|
||||
@@ -29,10 +28,15 @@ export class GroupHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.logBox('Configuration Error', [
|
||||
logger.logBox(
|
||||
'Configuration Error',
|
||||
[
|
||||
'No configuration found.',
|
||||
"Please run 'nupst ups add' first to create a configuration.",
|
||||
], 50, 'error');
|
||||
],
|
||||
50,
|
||||
'error',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,21 +45,35 @@ export class GroupHandler {
|
||||
|
||||
// Check if multi-UPS config
|
||||
if (!config.groups || !Array.isArray(config.groups)) {
|
||||
logger.logBox('UPS Groups', [
|
||||
logger.logBox(
|
||||
'UPS Groups',
|
||||
[
|
||||
'No groups configured.',
|
||||
'',
|
||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
||||
], 50, 'info');
|
||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${
|
||||
theme.dim('to add a group')
|
||||
}`,
|
||||
],
|
||||
50,
|
||||
'info',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display group list with modern table
|
||||
if (config.groups.length === 0) {
|
||||
logger.logBox('UPS Groups', [
|
||||
logger.logBox(
|
||||
'UPS Groups',
|
||||
[
|
||||
'No UPS groups configured.',
|
||||
'',
|
||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
||||
], 60, 'info');
|
||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${
|
||||
theme.dim('to add a group')
|
||||
}`,
|
||||
],
|
||||
60,
|
||||
'info',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ export class ServiceHandler {
|
||||
/**
|
||||
* Update NUPST from repository and refresh systemd service
|
||||
*/
|
||||
public async update(): Promise<void> {
|
||||
public update(): void {
|
||||
try {
|
||||
// Check if running as root
|
||||
this.checkRootAccess(
|
||||
@@ -147,8 +147,12 @@ export class ServiceHandler {
|
||||
const latestVersion = release.tag_name; // e.g., "v4.0.7"
|
||||
|
||||
// Normalize versions for comparison (ensure both have "v" prefix)
|
||||
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
|
||||
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
|
||||
const normalizedCurrent = currentVersion.startsWith('v')
|
||||
? currentVersion
|
||||
: `v${currentVersion}`;
|
||||
const normalizedLatest = latestVersion.startsWith('v')
|
||||
? latestVersion
|
||||
: `v${latestVersion}`;
|
||||
|
||||
logger.dim(`Current version: ${normalizedCurrent}`);
|
||||
logger.dim(`Latest version: ${normalizedLatest}`);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import process from 'node:process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { type ITableColumn, logger } from '../logger.ts';
|
||||
import { theme } from '../colors.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
import type { ISnmpConfig, TUpsModel, IUpsStatus as ISnmpUpsStatus } from '../snmp/types.ts';
|
||||
import type { INupstConfig, IUpsConfig, IUpsStatus } from '../daemon.ts';
|
||||
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus, TUpsModel } from '../snmp/types.ts';
|
||||
import type { INupstConfig, IUpsConfig } from '../daemon.ts';
|
||||
import type { IActionConfig } from '../actions/base-action.ts';
|
||||
|
||||
/**
|
||||
@@ -343,10 +343,15 @@ export class UpsHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.logBox('Configuration Error', [
|
||||
logger.logBox(
|
||||
'Configuration Error',
|
||||
[
|
||||
'No configuration found.',
|
||||
"Please run 'nupst ups add' first to create a configuration.",
|
||||
], 50, 'error');
|
||||
],
|
||||
50,
|
||||
'error',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,31 +361,38 @@ export class UpsHandler {
|
||||
// Check if multi-UPS config
|
||||
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
||||
// Legacy single UPS configuration
|
||||
logger.logBox('UPS Devices', [
|
||||
logger.logBox(
|
||||
'UPS Devices',
|
||||
[
|
||||
'Legacy single-UPS configuration detected.',
|
||||
'',
|
||||
...(!config.snmp
|
||||
? ['Error: Configuration missing SNMP settings']
|
||||
: [
|
||||
...(!config.snmp ? ['Error: Configuration missing SNMP settings'] : [
|
||||
'Default UPS:',
|
||||
` Host: ${config.snmp.host}:${config.snmp.port}`,
|
||||
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
||||
'',
|
||||
'Use "nupst ups add" to add more UPS devices and migrate',
|
||||
'to the multi-UPS configuration format.',
|
||||
]
|
||||
),
|
||||
], 60, 'warning');
|
||||
]),
|
||||
],
|
||||
60,
|
||||
'warning',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display UPS list with modern table
|
||||
if (config.upsDevices.length === 0) {
|
||||
logger.logBox('UPS Devices', [
|
||||
logger.logBox(
|
||||
'UPS Devices',
|
||||
[
|
||||
'No UPS devices configured.',
|
||||
'',
|
||||
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
|
||||
], 60, 'info');
|
||||
],
|
||||
60,
|
||||
'info',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -569,8 +581,6 @@ export class UpsHandler {
|
||||
logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||
logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
} catch (error) {
|
||||
const errorBoxWidth = 45;
|
||||
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
|
||||
@@ -975,10 +985,15 @@ export class UpsHandler {
|
||||
}
|
||||
|
||||
// Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes
|
||||
if (action.triggerMode === 'onlyThresholds' || action.triggerMode === 'powerChangesAndThresholds') {
|
||||
if (
|
||||
action.triggerMode === 'onlyThresholds' ||
|
||||
action.triggerMode === 'powerChangesAndThresholds'
|
||||
) {
|
||||
logger.log('');
|
||||
logger.info('Action Thresholds:');
|
||||
logger.dim('Action will trigger when battery or runtime falls below these values (while on battery)');
|
||||
logger.dim(
|
||||
'Action will trigger when battery or runtime falls below these values (while on battery)',
|
||||
);
|
||||
|
||||
const batteryInput = await prompt('Battery threshold percentage [60]: ');
|
||||
const battery = parseInt(batteryInput, 10);
|
||||
@@ -995,7 +1010,11 @@ export class UpsHandler {
|
||||
}
|
||||
|
||||
actions.push(action as IActionConfig);
|
||||
logger.success(`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
|
||||
logger.success(
|
||||
`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${
|
||||
action.triggerMode || 'powerChangesAndThresholds'
|
||||
})`,
|
||||
);
|
||||
|
||||
const more = await prompt('Add another action? (y/N): ');
|
||||
addMore = more.toLowerCase() === 'y';
|
||||
|
||||
52
ts/daemon.ts
52
ts/daemon.ts
@@ -5,13 +5,13 @@ import { exec, execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { NupstSnmp } from './snmp/manager.ts';
|
||||
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts';
|
||||
import { logger, type ITableColumn } from './logger.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { MigrationRunner } from './migrations/index.ts';
|
||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||
import { formatPowerStatus, getBatteryColor, getRuntimeColor, theme } from './colors.ts';
|
||||
import type { IActionConfig } from './actions/base-action.ts';
|
||||
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
|
||||
import { NupstHttpServer } from './http-server.ts';
|
||||
import { TIMING, THRESHOLDS, UI } from './constants.ts';
|
||||
import { THRESHOLDS, TIMING, UI } from './constants.ts';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -155,7 +155,7 @@ export class NupstDaemon {
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: TIMING.CHECK_INTERVAL_MS, // Check every 30 seconds
|
||||
}
|
||||
};
|
||||
|
||||
private config: INupstConfig;
|
||||
private snmp: NupstSnmp;
|
||||
@@ -249,7 +249,12 @@ export class NupstDaemon {
|
||||
* Helper method to log configuration errors consistently
|
||||
*/
|
||||
private logConfigError(message: string): void {
|
||||
logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
|
||||
logger.logBox(
|
||||
'Configuration Error',
|
||||
[message, "Please run 'nupst setup' first to create a configuration."],
|
||||
45,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,11 +316,15 @@ export class NupstDaemon {
|
||||
this.config.httpServer.port,
|
||||
this.config.httpServer.path,
|
||||
this.config.httpServer.authToken,
|
||||
() => this.upsStatus
|
||||
() => this.upsStatus,
|
||||
);
|
||||
this.httpServer.start();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to start HTTP server: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Failed to start HTTP server: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +373,6 @@ export class NupstDaemon {
|
||||
* Log the loaded configuration settings
|
||||
*/
|
||||
private logConfigLoaded(): void {
|
||||
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Configuration Loaded', 70, 'success');
|
||||
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||
@@ -375,7 +383,9 @@ export class NupstDaemon {
|
||||
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
||||
logger.info(`UPS Devices (${this.config.upsDevices.length}):`);
|
||||
|
||||
const upsColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||
const upsColumns: Array<
|
||||
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
||||
> = [
|
||||
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||
@@ -400,7 +410,9 @@ export class NupstDaemon {
|
||||
if (this.config.groups && this.config.groups.length > 0) {
|
||||
logger.info(`Groups (${this.config.groups.length}):`);
|
||||
|
||||
const groupColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||
const groupColumns: Array<
|
||||
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
||||
> = [
|
||||
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
||||
@@ -583,7 +595,9 @@ export class NupstDaemon {
|
||||
logger.log('');
|
||||
|
||||
// Build table data
|
||||
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||
const columns: Array<
|
||||
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
||||
> = [
|
||||
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
||||
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||
{ header: 'Power Status', key: 'powerStatus', align: 'left' },
|
||||
@@ -609,10 +623,6 @@ export class NupstDaemon {
|
||||
logger.log('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Build action context from UPS state
|
||||
* @param ups UPS configuration
|
||||
@@ -796,7 +806,9 @@ export class NupstDaemon {
|
||||
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Shutdown Monitoring Active', UI.WIDE_BOX_WIDTH, 'warning');
|
||||
logger.logBoxLine(`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`);
|
||||
logger.logBoxLine(
|
||||
`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`,
|
||||
);
|
||||
logger.logBoxLine(`Check interval: ${TIMING.SHUTDOWN_CHECK_INTERVAL_MS / 1000} seconds`);
|
||||
logger.logBoxLine(`Max monitoring time: ${TIMING.MAX_SHUTDOWN_MONITORING_MS / 1000} seconds`);
|
||||
logger.logBoxEnd();
|
||||
@@ -808,7 +820,9 @@ export class NupstDaemon {
|
||||
logger.info('Checking UPS status during shutdown...');
|
||||
|
||||
// Build table for UPS status during shutdown
|
||||
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
||||
const columns: Array<
|
||||
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
||||
> = [
|
||||
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
||||
{ header: 'Battery', key: 'battery', align: 'right' },
|
||||
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||
@@ -991,7 +1005,9 @@ export class NupstDaemon {
|
||||
let lastConfigCheck = Date.now();
|
||||
|
||||
logger.log('Entering idle monitoring mode...');
|
||||
logger.log(`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`);
|
||||
logger.log(
|
||||
`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`,
|
||||
);
|
||||
|
||||
// Start file watcher for hot-reload
|
||||
this.watchConfigFile();
|
||||
|
||||
@@ -25,7 +25,7 @@ export class NupstHttpServer {
|
||||
port: number,
|
||||
path: string,
|
||||
authToken: string,
|
||||
getUpsStatus: () => Map<string, IUpsStatus>
|
||||
getUpsStatus: () => Map<string, IUpsStatus>,
|
||||
) {
|
||||
this.port = port;
|
||||
this.path = path;
|
||||
@@ -70,7 +70,7 @@ export class NupstHttpServer {
|
||||
if (!this.isAuthenticated(req)) {
|
||||
res.writeHead(401, {
|
||||
'Content-Type': 'application/json',
|
||||
'WWW-Authenticate': 'Bearer'
|
||||
'WWW-Authenticate': 'Bearer',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
||||
return;
|
||||
@@ -82,7 +82,7 @@ export class NupstHttpServer {
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
res.end(JSON.stringify(statusArray, null, 2));
|
||||
} else {
|
||||
@@ -95,7 +95,7 @@ export class NupstHttpServer {
|
||||
logger.success(`HTTP server started on port ${this.port} at ${this.path}`);
|
||||
});
|
||||
|
||||
this.server.on('error', (error: any) => {
|
||||
this.server.on('error', (error: Error) => {
|
||||
logger.error(`HTTP server error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { theme, symbols } from './colors.ts';
|
||||
import { symbols, theme } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Table column alignment options
|
||||
@@ -230,7 +230,8 @@ export class Logger {
|
||||
* Strip ANSI color codes from string for accurate length calculation
|
||||
*/
|
||||
private stripAnsi(text: string): string {
|
||||
// Remove ANSI escape codes
|
||||
// Remove ANSI escape codes (intentional control character regex)
|
||||
// deno-lint-ignore no-control-regex
|
||||
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ export abstract class BaseMigration {
|
||||
* @param config - Raw configuration object to check (unknown schema for migrations)
|
||||
* @returns True if migration should run, false otherwise
|
||||
*/
|
||||
abstract shouldRun(config: Record<string, unknown>): Promise<boolean>;
|
||||
abstract shouldRun(
|
||||
config: Record<string, unknown>,
|
||||
): boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Perform the migration on the given config
|
||||
@@ -39,7 +41,9 @@ export abstract class BaseMigration {
|
||||
* @param config - Raw configuration object to migrate (unknown schema for migrations)
|
||||
* @returns Migrated configuration object
|
||||
*/
|
||||
abstract migrate(config: Record<string, unknown>): Promise<Record<string, unknown>>;
|
||||
abstract migrate(
|
||||
config: Record<string, unknown>,
|
||||
): Record<string, unknown> | Promise<Record<string, unknown>>;
|
||||
|
||||
/**
|
||||
* Get human-readable name for this migration
|
||||
|
||||
@@ -23,12 +23,12 @@ export class MigrationV1ToV2 extends BaseMigration {
|
||||
readonly fromVersion = '1.x';
|
||||
readonly toVersion = '2.0';
|
||||
|
||||
async shouldRun(config: any): Promise<boolean> {
|
||||
shouldRun(config: Record<string, unknown>): boolean {
|
||||
// V1 format has snmp field directly at root, no upsDevices or upsList
|
||||
return !!config.snmp && !config.upsDevices && !config.upsList;
|
||||
}
|
||||
|
||||
async migrate(config: any): Promise<any> {
|
||||
migrate(config: Record<string, unknown>): Record<string, unknown> {
|
||||
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
|
||||
|
||||
const migrated = {
|
||||
|
||||
@@ -42,15 +42,16 @@ export class MigrationV3ToV4 extends BaseMigration {
|
||||
readonly fromVersion = '3.x';
|
||||
readonly toVersion = '4.0';
|
||||
|
||||
async shouldRun(config: any): Promise<boolean> {
|
||||
shouldRun(config: Record<string, unknown>): boolean {
|
||||
// V3 format has upsList OR has upsDevices with flat structure (host at top level)
|
||||
if (config.upsList && !config.upsDevices) {
|
||||
return true; // Classic v3 with upsList
|
||||
}
|
||||
|
||||
// Check if upsDevices exists but has flat structure (v3 format)
|
||||
if (config.upsDevices && config.upsDevices.length > 0) {
|
||||
const firstDevice = config.upsDevices[0];
|
||||
const upsDevices = config.upsDevices as Array<Record<string, unknown>> | undefined;
|
||||
if (upsDevices && upsDevices.length > 0) {
|
||||
const firstDevice = upsDevices[0];
|
||||
// V3 has host at top level, v4 has it nested in snmp object
|
||||
return !!firstDevice.host && !firstDevice.snmp;
|
||||
}
|
||||
@@ -58,17 +59,17 @@ export class MigrationV3ToV4 extends BaseMigration {
|
||||
return false;
|
||||
}
|
||||
|
||||
async migrate(config: any): Promise<any> {
|
||||
migrate(config: Record<string, unknown>): Record<string, unknown> {
|
||||
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
|
||||
logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`);
|
||||
|
||||
// Get devices from either upsList or upsDevices (for partially migrated configs)
|
||||
const sourceDevices = config.upsList || config.upsDevices;
|
||||
const sourceDevices = (config.upsList || config.upsDevices) as Array<Record<string, unknown>>;
|
||||
|
||||
// Transform each UPS device from v3 flat structure to v4 nested structure
|
||||
const transformedDevices = sourceDevices.map((device: any) => {
|
||||
const transformedDevices = sourceDevices.map((device: Record<string, unknown>) => {
|
||||
// Build SNMP config object
|
||||
const snmpConfig: any = {
|
||||
const snmpConfig: Record<string, unknown> = {
|
||||
host: device.host,
|
||||
port: device.port || 161,
|
||||
version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version,
|
||||
@@ -112,7 +113,9 @@ export class MigrationV3ToV4 extends BaseMigration {
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`);
|
||||
logger.success(
|
||||
`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`,
|
||||
);
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
||||
readonly fromVersion = '4.0';
|
||||
readonly toVersion = '4.1';
|
||||
|
||||
async shouldRun(config: Record<string, unknown>): Promise<boolean> {
|
||||
shouldRun(config: Record<string, unknown>): boolean {
|
||||
// Run if config is version 4.0
|
||||
if (config.version === '4.0') {
|
||||
return true;
|
||||
@@ -65,7 +65,7 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
||||
return false;
|
||||
}
|
||||
|
||||
async migrate(config: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
migrate(config: Record<string, unknown>): Record<string, unknown> {
|
||||
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
|
||||
logger.dim(` - Moving thresholds from UPS level to action level`);
|
||||
logger.dim(` - Creating default shutdown actions from existing thresholds`);
|
||||
@@ -81,7 +81,9 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
||||
};
|
||||
|
||||
// If device has thresholds at UPS level, convert to shutdown action
|
||||
const deviceThresholds = device.thresholds as { battery: number; runtime: number } | undefined;
|
||||
const deviceThresholds = device.thresholds as
|
||||
| { battery: number; runtime: number }
|
||||
| undefined;
|
||||
if (deviceThresholds) {
|
||||
migrated.actions = [
|
||||
{
|
||||
|
||||
@@ -94,7 +94,8 @@ export class NupstSnmp {
|
||||
public snmpGet(
|
||||
oid: string,
|
||||
config = this.DEFAULT_CONFIG,
|
||||
retryCount = 0,
|
||||
_retryCount = 0,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.debug) {
|
||||
@@ -105,6 +106,7 @@ export class NupstSnmp {
|
||||
}
|
||||
|
||||
// Create SNMP options based on configuration
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const options: any = {
|
||||
port: config.port,
|
||||
retries: SNMP.RETRIES, // Number of retries
|
||||
@@ -132,6 +134,7 @@ export class NupstSnmp {
|
||||
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
||||
|
||||
// Create the user object with required structure for net-snmp
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const user: any = {
|
||||
name: config.username || '',
|
||||
};
|
||||
@@ -197,7 +200,11 @@ export class NupstSnmp {
|
||||
const levelName = Object.keys(snmp.SecurityLevel).find((key) =>
|
||||
snmp.SecurityLevel[key] === user.level
|
||||
);
|
||||
logger.dim(`SNMPv3 user configuration: name=${user.name}, level=${levelName}, authProtocol=${user.authProtocol ? 'Set' : 'Not Set'}, privProtocol=${user.privProtocol ? 'Set' : 'Not Set'}`);
|
||||
logger.dim(
|
||||
`SNMPv3 user configuration: name=${user.name}, level=${levelName}, authProtocol=${
|
||||
user.authProtocol ? 'Set' : 'Not Set'
|
||||
}, privProtocol=${user.privProtocol ? 'Set' : 'Not Set'}`,
|
||||
);
|
||||
}
|
||||
|
||||
session = snmp.createV3Session(config.host, user, options);
|
||||
@@ -210,7 +217,8 @@ export class NupstSnmp {
|
||||
const oids = [oid];
|
||||
|
||||
// Send the GET request
|
||||
session.get(oids, (error: any, varbinds: any[]) => {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
session.get(oids, (error: Error | null, varbinds: any[]) => {
|
||||
// Close the session to release resources
|
||||
session.close();
|
||||
|
||||
@@ -259,7 +267,9 @@ export class NupstSnmp {
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
logger.dim(`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`);
|
||||
logger.dim(
|
||||
`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`,
|
||||
);
|
||||
}
|
||||
|
||||
resolve(value);
|
||||
@@ -422,6 +432,7 @@ export class NupstSnmp {
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
): Promise<any> {
|
||||
if (oid === '') {
|
||||
if (this.debug) {
|
||||
@@ -476,6 +487,7 @@ export class NupstSnmp {
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
): Promise<any> {
|
||||
if (this.debug) {
|
||||
logger.dim(`Retrying ${description} with fallback security level...`);
|
||||
@@ -483,7 +495,7 @@ export class NupstSnmp {
|
||||
|
||||
// Try with authNoPriv if current level is authPriv
|
||||
if (config.securityLevel === 'authPriv') {
|
||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as const };
|
||||
try {
|
||||
if (this.debug) {
|
||||
logger.dim(`Retrying with authNoPriv security level`);
|
||||
@@ -496,7 +508,9 @@ export class NupstSnmp {
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
logger.error(
|
||||
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
|
||||
`Retry failed for ${description}: ${
|
||||
retryError instanceof Error ? retryError.message : String(retryError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -504,7 +518,7 @@ export class NupstSnmp {
|
||||
|
||||
// Try with noAuthNoPriv as a last resort
|
||||
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
|
||||
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
|
||||
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as const };
|
||||
try {
|
||||
if (this.debug) {
|
||||
logger.dim(`Retrying with noAuthNoPriv security level`);
|
||||
@@ -517,7 +531,9 @@ export class NupstSnmp {
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
logger.error(
|
||||
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
|
||||
`Retry failed for ${description}: ${
|
||||
retryError instanceof Error ? retryError.message : String(retryError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -528,15 +544,16 @@ export class NupstSnmp {
|
||||
|
||||
/**
|
||||
* Try standard OIDs as fallback
|
||||
* @param oid OID to query
|
||||
* @param _oid Original OID (unused, kept for method signature consistency)
|
||||
* @param description Description of the value for logging
|
||||
* @param config SNMP configuration
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async tryStandardOids(
|
||||
oid: string,
|
||||
_oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
): Promise<any> {
|
||||
try {
|
||||
// Try RFC 1628 standard UPS MIB OIDs
|
||||
@@ -556,7 +573,9 @@ export class NupstSnmp {
|
||||
} catch (stdError) {
|
||||
if (this.debug) {
|
||||
logger.error(
|
||||
`Standard OID retry failed for ${description}: ${stdError instanceof Error ? stdError.message : String(stdError)}`,
|
||||
`Standard OID retry failed for ${description}: ${
|
||||
stdError instanceof Error ? stdError.message : String(stdError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface IUpsStatus {
|
||||
/** Output current in amps */
|
||||
outputCurrent: number;
|
||||
/** Raw values from SNMP responses */
|
||||
raw: Record<string, any>;
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import process from 'node:process';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { NupstDaemon, type IUpsConfig } from './daemon.ts';
|
||||
import { type IUpsConfig, NupstDaemon } from './daemon.ts';
|
||||
import { NupstSnmp } from './snmp/manager.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||
import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Class for managing systemd service
|
||||
@@ -54,7 +54,11 @@ WantedBy=multi-user.target
|
||||
logger.log('');
|
||||
logger.error('No configuration found');
|
||||
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
|
||||
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
|
||||
logger.log(
|
||||
` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${
|
||||
theme.dim('to create a configuration')
|
||||
}`,
|
||||
);
|
||||
logger.log('');
|
||||
throw new Error('Configuration not found');
|
||||
}
|
||||
@@ -155,16 +159,22 @@ WantedBy=multi-user.target
|
||||
const updateStatus = nupst.getUpdateStatus();
|
||||
logger.log('');
|
||||
logger.log(
|
||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`,
|
||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${
|
||||
theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)
|
||||
}`,
|
||||
);
|
||||
logger.log(
|
||||
` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`,
|
||||
);
|
||||
logger.log(` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`);
|
||||
} else {
|
||||
logger.log('');
|
||||
logger.log(
|
||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`,
|
||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${
|
||||
theme.success('Up to date')
|
||||
}`,
|
||||
);
|
||||
}
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
// If version check fails, show at least the current version
|
||||
try {
|
||||
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||
@@ -242,9 +252,15 @@ WantedBy=multi-user.target
|
||||
// Display beautiful status
|
||||
logger.log('');
|
||||
if (isActive) {
|
||||
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
||||
logger.log(
|
||||
`${symbols.running} ${theme.success('Service:')} ${
|
||||
theme.statusActive('active (running)')
|
||||
}`,
|
||||
);
|
||||
} else {
|
||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
|
||||
logger.log(
|
||||
`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (pid || memory || cpu) {
|
||||
@@ -255,10 +271,11 @@ WantedBy=multi-user.target
|
||||
logger.log(` ${details.join(' ')}`);
|
||||
}
|
||||
logger.log('');
|
||||
|
||||
} catch (error) {
|
||||
logger.log('');
|
||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
||||
logger.log(
|
||||
`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`,
|
||||
);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
@@ -309,7 +326,9 @@ WantedBy=multi-user.target
|
||||
} else {
|
||||
logger.log('');
|
||||
logger.warn('No UPS devices configured');
|
||||
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
||||
logger.log(
|
||||
` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
|
||||
);
|
||||
logger.log('');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -344,7 +363,9 @@ WantedBy=multi-user.target
|
||||
}
|
||||
|
||||
// Display UPS name and power status
|
||||
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
||||
logger.log(
|
||||
` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`,
|
||||
);
|
||||
|
||||
// Display battery with color coding
|
||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||
@@ -352,16 +373,27 @@ WantedBy=multi-user.target
|
||||
// Get threshold from actions (if any action has thresholds defined)
|
||||
const actionWithThresholds = ups.actions?.find((action) => action.thresholds);
|
||||
const batteryThreshold = actionWithThresholds?.thresholds?.battery;
|
||||
const batterySymbol = batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
|
||||
const batterySymbol =
|
||||
batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
|
||||
? symbols.success
|
||||
: batteryThreshold !== undefined
|
||||
? symbols.warning
|
||||
: '';
|
||||
|
||||
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')}`);
|
||||
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
|
||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||
@@ -381,7 +413,9 @@ WantedBy=multi-user.target
|
||||
for (const action of ups.actions) {
|
||||
let actionDesc = `${action.type}`;
|
||||
if (action.thresholds) {
|
||||
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||
actionDesc += ` (${
|
||||
action.triggerMode || 'onlyThresholds'
|
||||
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||
if (action.shutdownDelay) {
|
||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||
}
|
||||
@@ -398,10 +432,11 @@ WantedBy=multi-user.target
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
|
||||
} catch (error) {
|
||||
// Display error for this UPS
|
||||
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
||||
logger.log(
|
||||
` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`,
|
||||
);
|
||||
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||
logger.log('');
|
||||
@@ -426,7 +461,9 @@ WantedBy=multi-user.target
|
||||
// Display group name and mode
|
||||
const modeColor = group.mode === 'redundant' ? theme.success : theme.warning;
|
||||
logger.log(
|
||||
` ${symbols.info} ${theme.highlight(group.name)} ${theme.dim(`(${modeColor(group.mode)})`)}`,
|
||||
` ${symbols.info} ${theme.highlight(group.name)} ${
|
||||
theme.dim(`(${modeColor(group.mode)})`)
|
||||
}`,
|
||||
);
|
||||
|
||||
// Display description if present
|
||||
@@ -451,7 +488,9 @@ WantedBy=multi-user.target
|
||||
for (const action of group.actions) {
|
||||
let actionDesc = `${action.type}`;
|
||||
if (action.thresholds) {
|
||||
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||
actionDesc += ` (${
|
||||
action.triggerMode || 'onlyThresholds'
|
||||
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||
if (action.shutdownDelay) {
|
||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user