Compare commits
82 Commits
Author | SHA1 | Date | |
---|---|---|---|
f750299780 | |||
ca1039408d | |||
df3e0b9424 | |||
c8e5960abd | |||
7304a62357 | |||
a5a88e53ba | |||
73bc271c59 | |||
1e98181e71 | |||
eb5a8185ae | |||
ef3d3f3fa3 | |||
34e6e850ad | |||
992a776fd2 | |||
3e15a2d52f | |||
d1a3576d31 | |||
1ca05e879b | |||
9c6fa37eb8 | |||
ff433b2256 | |||
263d69aef1 | |||
b6b7b43161 | |||
316c66c344 | |||
4debda856b | |||
0e7bcab499 | |||
7bf65d8495 | |||
f2ce0180d3 | |||
8c1be6555f | |||
1a5558e91f | |||
611a9ddd19 | |||
afd026d08c | |||
2c8ea44d40 | |||
32bd27b849 | |||
a7113d0387 | |||
61d4e9037a | |||
caced2718f | |||
8516056f84 | |||
07ec9d7595 | |||
d14ba1dd65 | |||
7d595fa175 | |||
df417432b0 | |||
e5f1ebf343 | |||
3ff0dd7ac8 | |||
bb87316dd3 | |||
d6e0a1a274 | |||
95fa4f8b0b | |||
c2f2f1e2ee | |||
936f86c346 | |||
7ff1a7da36 | |||
a87710144c | |||
23fd5cc5cd | |||
fb4d776bdd | |||
88ad16c638 | |||
016681b77b | |||
49f7a7da8b | |||
f8269a1cb7 | |||
b37e1aae6c | |||
7076829747 | |||
1387ca262b | |||
684f034aee | |||
a63ec16d63 | |||
85f34cf96a | |||
4d28614e08 | |||
567c7be7c5 | |||
a897a7c780 | |||
accf137216 | |||
c3441946cb | |||
37ccbf58fd | |||
071ded9c41 | |||
b935087d50 | |||
e1383097b2 | |||
dff0ea610b | |||
4faa10c494 | |||
c2d39cc19a | |||
9ccbbbdc37 | |||
1705ffe2be | |||
968cbbd8fc | |||
a2ae9960b6 | |||
df6a44d5d9 | |||
9efcc4b437 | |||
5903ae71be | |||
a649c598ad | |||
5f4f3ecbc3 | |||
806f81c6a0 | |||
88e353eec6 |
179
.gitea/workflows/README.md
Normal file
179
.gitea/workflows/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Gitea Actions Workflows
|
||||
|
||||
This directory contains automated workflows for NUPST's CI/CD pipeline.
|
||||
|
||||
## Workflows
|
||||
|
||||
### CI Workflow (`ci.yml`)
|
||||
|
||||
**Triggers:**
|
||||
|
||||
- Push to `main` branch
|
||||
- Push to `migration/**` branches
|
||||
- Pull requests to `main`
|
||||
|
||||
**Jobs:**
|
||||
|
||||
1. **Type Check & Lint**
|
||||
- Runs `deno check` for TypeScript validation
|
||||
- Runs `deno lint` (continues on error)
|
||||
- Runs `deno fmt --check` (continues on error)
|
||||
|
||||
2. **Build Test (Current Platform)**
|
||||
- Compiles for Linux x86_64 (host platform)
|
||||
- Tests binary execution (`--version` and `help`)
|
||||
|
||||
3. **Build All Platforms** (Main/Tags only)
|
||||
- Compiles all 5 platform binaries
|
||||
- Uploads as artifacts (30-day retention)
|
||||
- Only runs on `main` branch or tags
|
||||
|
||||
### Release Workflow (`release.yml`)
|
||||
|
||||
**Triggers:**
|
||||
|
||||
- Push tags matching `v*` (e.g., `v4.0.0`)
|
||||
|
||||
**Jobs:**
|
||||
|
||||
1. **Version Verification**
|
||||
- Extracts version from tag
|
||||
- Verifies `deno.json` version matches tag
|
||||
- Fails if mismatch detected
|
||||
|
||||
2. **Compilation**
|
||||
- Compiles binaries for all 5 platforms:
|
||||
- `nupst-linux-x64` (Linux x86_64)
|
||||
- `nupst-linux-arm64` (Linux ARM64)
|
||||
- `nupst-macos-x64` (macOS Intel)
|
||||
- `nupst-macos-arm64` (macOS Apple Silicon)
|
||||
- `nupst-windows-x64.exe` (Windows x64)
|
||||
|
||||
3. **Checksums**
|
||||
- Generates SHA256 checksums for all binaries
|
||||
- Creates `SHA256SUMS.txt`
|
||||
|
||||
4. **Release Creation**
|
||||
- Creates Gitea release with tag
|
||||
- Extracts release notes from CHANGELOG.md (if exists)
|
||||
- Uploads all binaries + checksums as release assets
|
||||
|
||||
## Creating a Release
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Update version in `deno.json`:
|
||||
```json
|
||||
{
|
||||
"version": "4.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
2. Update `CHANGELOG.md` with release notes (optional but recommended)
|
||||
|
||||
3. Commit all changes:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "chore: bump version to 4.0.0"
|
||||
```
|
||||
|
||||
### Release Process
|
||||
|
||||
1. Create and push a tag matching the version:
|
||||
```bash
|
||||
git tag v4.0.0
|
||||
git push origin v4.0.0
|
||||
```
|
||||
|
||||
2. Gitea Actions will automatically:
|
||||
- Verify version consistency
|
||||
- Compile all platform binaries
|
||||
- Generate checksums
|
||||
- Create release with binaries attached
|
||||
|
||||
3. Monitor the workflow:
|
||||
- Go to: `https://code.foss.global/serve.zone/nupst/actions`
|
||||
- Check the "Release" workflow run
|
||||
|
||||
### Manual Release (Fallback)
|
||||
|
||||
If workflows fail, you can create a release manually:
|
||||
|
||||
```bash
|
||||
# Compile all binaries
|
||||
bash scripts/compile-all.sh
|
||||
|
||||
# Generate checksums
|
||||
cd dist/binaries
|
||||
sha256sum * > SHA256SUMS.txt
|
||||
cd ../..
|
||||
|
||||
# Create release on Gitea UI
|
||||
# Upload binaries manually
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Version Mismatch Error
|
||||
|
||||
If the release workflow fails with "Version mismatch":
|
||||
|
||||
- Ensure `deno.json` version matches the git tag
|
||||
- Example: tag `v4.0.0` requires `"version": "4.0.0"` in deno.json
|
||||
|
||||
### Compilation Errors
|
||||
|
||||
If compilation fails:
|
||||
|
||||
1. Test locally: `bash scripts/compile-all.sh`
|
||||
2. Check Deno version compatibility
|
||||
3. Review TypeScript errors: `deno check mod.ts`
|
||||
|
||||
### Upload Failures
|
||||
|
||||
If binary upload fails:
|
||||
|
||||
1. Check Gitea Actions permissions
|
||||
2. Verify `GITHUB_TOKEN` secret exists (auto-provided by Gitea)
|
||||
3. Try manual release creation
|
||||
|
||||
## Workflow Secrets
|
||||
|
||||
The workflows use the following secrets:
|
||||
|
||||
- `GITHUB_TOKEN` - Auto-provided by Gitea Actions (no setup needed)
|
||||
|
||||
## Development
|
||||
|
||||
### Testing Workflows Locally
|
||||
|
||||
You can test compilation locally:
|
||||
|
||||
```bash
|
||||
# Install Deno
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
|
||||
# Test type checking
|
||||
deno check mod.ts
|
||||
|
||||
# Test compilation
|
||||
bash scripts/compile-all.sh
|
||||
|
||||
# Test binary
|
||||
./dist/binaries/nupst-linux-x64 --version
|
||||
```
|
||||
|
||||
### Modifying Workflows
|
||||
|
||||
After modifying workflows:
|
||||
|
||||
1. Test syntax: Use a YAML validator
|
||||
2. Commit changes: `git add .gitea/workflows/`
|
||||
3. Push to feature branch first to test CI
|
||||
4. Merge to main once verified
|
||||
|
||||
## Links
|
||||
|
||||
- Gitea Actions Documentation: https://docs.gitea.com/usage/actions/overview
|
||||
- Deno Compile Documentation: https://docs.deno.com/runtime/manual/tools/compiler
|
||||
- NUPST Repository: https://code.foss.global/serve.zone/nupst
|
84
.gitea/workflows/ci.yml
Normal file
84
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'migration/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Type Check & Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: Check TypeScript types
|
||||
run: deno check mod.ts
|
||||
|
||||
- name: Lint code
|
||||
run: deno lint
|
||||
continue-on-error: true
|
||||
|
||||
- name: Format check
|
||||
run: deno fmt --check
|
||||
continue-on-error: true
|
||||
|
||||
build:
|
||||
name: Build Test (Current Platform)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: Compile for current platform
|
||||
run: |
|
||||
echo "Testing compilation for Linux x86_64..."
|
||||
deno compile --allow-all --no-check \
|
||||
--output nupst-test \
|
||||
--target x86_64-unknown-linux-gnu mod.ts
|
||||
|
||||
- name: Test binary execution
|
||||
run: |
|
||||
chmod +x nupst-test
|
||||
./nupst-test --version
|
||||
./nupst-test help
|
||||
|
||||
build-all:
|
||||
name: Build All Platforms
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: Compile all platform binaries
|
||||
run: bash scripts/compile-all.sh
|
||||
|
||||
- name: Upload all binaries as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nupst-binaries.zip
|
||||
path: dist/binaries/*
|
||||
retention-days: 30
|
249
.gitea/workflows/release.yml
Normal file
249
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,249 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||
echo "Building version: $VERSION"
|
||||
|
||||
- name: Verify deno.json version matches tag
|
||||
run: |
|
||||
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
|
||||
TAG_VERSION="${{ steps.version.outputs.version_number }}"
|
||||
echo "deno.json version: $DENO_VERSION"
|
||||
echo "Tag version: $TAG_VERSION"
|
||||
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "ERROR: Version mismatch!"
|
||||
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compile binaries for all platforms
|
||||
run: |
|
||||
echo "================================================"
|
||||
echo " NUPST Release Compilation"
|
||||
echo " Version: ${{ steps.version.outputs.version }}"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Clean up old binaries and create fresh directory
|
||||
rm -rf dist/binaries
|
||||
mkdir -p dist/binaries
|
||||
echo "→ Cleaned old binaries from dist/binaries"
|
||||
echo ""
|
||||
|
||||
# Linux x86_64
|
||||
echo "→ Compiling for Linux x86_64..."
|
||||
deno compile --allow-all --no-check \
|
||||
--output dist/binaries/nupst-linux-x64 \
|
||||
--target x86_64-unknown-linux-gnu mod.ts
|
||||
echo " ✓ Linux x86_64 complete"
|
||||
|
||||
# Linux ARM64
|
||||
echo "→ Compiling for Linux ARM64..."
|
||||
deno compile --allow-all --no-check \
|
||||
--output dist/binaries/nupst-linux-arm64 \
|
||||
--target aarch64-unknown-linux-gnu mod.ts
|
||||
echo " ✓ Linux ARM64 complete"
|
||||
|
||||
# macOS x86_64
|
||||
echo "→ Compiling for macOS x86_64..."
|
||||
deno compile --allow-all --no-check \
|
||||
--output dist/binaries/nupst-macos-x64 \
|
||||
--target x86_64-apple-darwin mod.ts
|
||||
echo " ✓ macOS x86_64 complete"
|
||||
|
||||
# macOS ARM64
|
||||
echo "→ Compiling for macOS ARM64..."
|
||||
deno compile --allow-all --no-check \
|
||||
--output dist/binaries/nupst-macos-arm64 \
|
||||
--target aarch64-apple-darwin mod.ts
|
||||
echo " ✓ macOS ARM64 complete"
|
||||
|
||||
# Windows x86_64
|
||||
echo "→ Compiling for Windows x86_64..."
|
||||
deno compile --allow-all --no-check \
|
||||
--output dist/binaries/nupst-windows-x64.exe \
|
||||
--target x86_64-pc-windows-msvc mod.ts
|
||||
echo " ✓ Windows x86_64 complete"
|
||||
|
||||
echo ""
|
||||
echo "All binaries compiled successfully!"
|
||||
ls -lh dist/binaries/
|
||||
|
||||
- name: Generate SHA256 checksums
|
||||
run: |
|
||||
cd dist/binaries
|
||||
sha256sum * > SHA256SUMS.txt
|
||||
cat SHA256SUMS.txt
|
||||
cd ../..
|
||||
|
||||
- name: Extract changelog for this version
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
# Check if CHANGELOG.md exists
|
||||
if [ ! -f CHANGELOG.md ]; then
|
||||
echo "No CHANGELOG.md found, using default release notes"
|
||||
cat > /tmp/release_notes.md << EOF
|
||||
## NUPST $VERSION
|
||||
|
||||
Pre-compiled binaries for multiple platforms.
|
||||
|
||||
### Installation
|
||||
|
||||
Use the installation script:
|
||||
\`\`\`bash
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||
\`\`\`
|
||||
|
||||
Or download the binary for your platform and make it executable.
|
||||
|
||||
### Supported Platforms
|
||||
- Linux x86_64 (x64)
|
||||
- Linux ARM64 (aarch64)
|
||||
- macOS x86_64 (Intel)
|
||||
- macOS ARM64 (Apple Silicon)
|
||||
- Windows x86_64
|
||||
|
||||
### Checksums
|
||||
SHA256 checksums are provided in SHA256SUMS.txt
|
||||
EOF
|
||||
else
|
||||
# Try to extract section for this version from CHANGELOG.md
|
||||
# This is a simple extraction - adjust based on your CHANGELOG format
|
||||
awk "/## \[$VERSION\]/,/## \[/" CHANGELOG.md | sed '$d' > /tmp/release_notes.md || cat > /tmp/release_notes.md << EOF
|
||||
## NUPST $VERSION
|
||||
|
||||
See CHANGELOG.md for full details.
|
||||
|
||||
### Installation
|
||||
|
||||
Use the installation script:
|
||||
\`\`\`bash
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||
\`\`\`
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Release notes:"
|
||||
cat /tmp/release_notes.md
|
||||
|
||||
- name: Delete existing release if it exists
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
echo "Checking for existing release $VERSION..."
|
||||
|
||||
# Try to get existing release by tag
|
||||
EXISTING_RELEASE_ID=$(curl -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/tags/$VERSION" \
|
||||
| jq -r '.id // empty')
|
||||
|
||||
if [ -n "$EXISTING_RELEASE_ID" ]; then
|
||||
echo "Found existing release (ID: $EXISTING_RELEASE_ID), deleting..."
|
||||
curl -X DELETE -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/$EXISTING_RELEASE_ID"
|
||||
echo "Existing release deleted"
|
||||
sleep 2
|
||||
else
|
||||
echo "No existing release found, proceeding with creation"
|
||||
fi
|
||||
|
||||
- name: Create Gitea Release
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE_NOTES=$(cat /tmp/release_notes.md)
|
||||
|
||||
# Create the release
|
||||
echo "Creating release for $VERSION..."
|
||||
RELEASE_ID=$(curl -X POST -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"$VERSION\",
|
||||
\"name\": \"NUPST $VERSION\",
|
||||
\"body\": $(jq -Rs . /tmp/release_notes.md),
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}" | jq -r '.id')
|
||||
|
||||
echo "Release created with ID: $RELEASE_ID"
|
||||
|
||||
# Upload binaries as release assets
|
||||
for binary in dist/binaries/*; do
|
||||
filename=$(basename "$binary")
|
||||
echo "Uploading $filename..."
|
||||
curl -X POST -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$binary" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/$RELEASE_ID/assets?name=$filename"
|
||||
done
|
||||
|
||||
echo "All assets uploaded successfully"
|
||||
|
||||
- name: Clean up old releases
|
||||
run: |
|
||||
echo "Cleaning up old releases (keeping only last 3)..."
|
||||
|
||||
# Fetch all releases sorted by creation date
|
||||
RELEASES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases" | \
|
||||
jq -r 'sort_by(.created_at) | reverse | .[3:] | .[].id')
|
||||
|
||||
# Delete old releases
|
||||
if [ -n "$RELEASES" ]; then
|
||||
echo "Found releases to delete:"
|
||||
for release_id in $RELEASES; do
|
||||
echo " Deleting release ID: $release_id"
|
||||
curl -X DELETE -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/$release_id"
|
||||
done
|
||||
echo "Old releases deleted successfully"
|
||||
else
|
||||
echo "No old releases to delete (less than 4 releases total)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
- name: Release Summary
|
||||
run: |
|
||||
echo "================================================"
|
||||
echo " Release ${{ steps.version.outputs.version }} Complete!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Binaries published:"
|
||||
ls -lh dist/binaries/
|
||||
echo ""
|
||||
echo "Release URL:"
|
||||
echo "https://code.foss.global/serve.zone/nupst/releases/tag/${{ steps.version.outputs.version }}"
|
||||
echo ""
|
||||
echo "Installation command:"
|
||||
echo "curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||
echo ""
|
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,15 +1,18 @@
|
||||
# Build
|
||||
dist*/
|
||||
# Compiled Deno binaries (built by scripts/compile-all.sh)
|
||||
dist/binaries/
|
||||
|
||||
# Dependencies
|
||||
# Deno cache and lock file
|
||||
.deno/
|
||||
deno.lock
|
||||
|
||||
# Legacy Node.js artifacts (v3.x and earlier - kept for safety)
|
||||
node_modules/
|
||||
|
||||
# Bundled Node.js binaries
|
||||
vendor/
|
||||
dist_ts/
|
||||
npm-debug.log*
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
@@ -18,4 +21,5 @@ npm-debug.log*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.nogit/
|
||||
# Development
|
||||
.nogit/
|
||||
|
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
71
.serena/project.yml
Normal file
71
.serena/project.yml
Normal file
@@ -0,0 +1,71 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: 'utf-8'
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ''
|
||||
|
||||
project_name: 'nupst'
|
96
bin/nupst
96
bin/nupst
@@ -1,96 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NUPST Launcher Script
|
||||
# This script detects architecture and OS, then runs NUPST with the appropriate Node.js binary
|
||||
|
||||
# First, handle symlinks correctly
|
||||
REAL_SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
|
||||
SCRIPT_DIR=$(dirname "$REAL_SCRIPT_PATH")
|
||||
|
||||
# For debugging
|
||||
# echo "Script path: $REAL_SCRIPT_PATH"
|
||||
# echo "Script dir: $SCRIPT_DIR"
|
||||
|
||||
# If we're run via symlink from /usr/local/bin, use the hardcoded installation path
|
||||
if [[ "$SCRIPT_DIR" == "/usr/local/bin" ]]; then
|
||||
PROJECT_ROOT="/opt/nupst"
|
||||
else
|
||||
# Otherwise, use relative path from script location
|
||||
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )"
|
||||
fi
|
||||
|
||||
# For debugging
|
||||
# echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
# Detect architecture and OS
|
||||
ARCH=$(uname -m)
|
||||
OS=$(uname -s)
|
||||
|
||||
# Determine Node.js binary location based on architecture and OS
|
||||
NODE_BIN=""
|
||||
case "$OS" in
|
||||
Linux)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-linux-arm64/bin/node"
|
||||
;;
|
||||
*)
|
||||
# Use system Node as fallback for other architectures
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation for unsupported architecture: $ARCH"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-x64/bin/node"
|
||||
;;
|
||||
arm64)
|
||||
NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-arm64/bin/node"
|
||||
;;
|
||||
*)
|
||||
# Use system Node as fallback for other architectures
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation for unsupported architecture: $ARCH"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
# Use system Node as fallback for other operating systems
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation for unsupported OS: $OS"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# If binary doesn't exist, try system Node as fallback
|
||||
if [ -z "$NODE_BIN" ] || [ ! -f "$NODE_BIN" ]; then
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_BIN="node"
|
||||
echo "Using system Node.js installation"
|
||||
else
|
||||
echo "Error: Node.js binary not found for $OS-$ARCH"
|
||||
echo "Please run the setup script or install Node.js manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run NUPST with the Node.js binary
|
||||
if [ -f "$PROJECT_ROOT/dist_ts/index.js" ]; then
|
||||
exec "$NODE_BIN" "$PROJECT_ROOT/dist_ts/index.js" "$@"
|
||||
elif [ -f "$PROJECT_ROOT/dist/index.js" ]; then
|
||||
exec "$NODE_BIN" "$PROJECT_ROOT/dist/index.js" "$@"
|
||||
else
|
||||
echo "Error: Could not find NUPST's index.js file."
|
||||
echo "Please run the setup script to download the required files."
|
||||
exit 1
|
||||
fi
|
407
changelog.md
407
changelog.md
@@ -1,12 +1,190 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime
|
||||
|
||||
**MAJOR RELEASE: NUPST v4.0 is a complete rewrite powered by Deno**
|
||||
|
||||
This release fundamentally changes NUPST's architecture from Node.js-based to Deno-based,
|
||||
distributed as pre-compiled binaries. This is a **breaking change** in terms of installation and
|
||||
distribution, but configuration files from v3.x are **fully compatible**.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
**Installation & Distribution:**
|
||||
|
||||
- **Removed**: Node.js runtime dependency - NUPST no longer requires Node.js
|
||||
- **Removed**: npm package distribution (no longer published to npmjs.org)
|
||||
- **Removed**: `bin/nupst` wrapper script
|
||||
- **Removed**: `setup.sh` dependency installation
|
||||
- **Removed**: All Node.js-related files (package.json, tsconfig.json, pnpm-lock.yaml,
|
||||
npmextra.json)
|
||||
- **Changed**: Installation now downloads pre-compiled binaries instead of cloning repository
|
||||
- **Changed**: Binary-based distribution (~340MB self-contained executables)
|
||||
|
||||
**CLI Structure (Backward Compatible):**
|
||||
|
||||
- **Changed**: Commands now use subcommand structure (e.g., `nupst service enable` instead of
|
||||
`nupst enable`)
|
||||
- **Maintained**: Old command format still works with deprecation warnings for smooth migration
|
||||
- **Added**: Aliases for common commands (`nupst ls`, `nupst rm`)
|
||||
|
||||
### New Features
|
||||
|
||||
**Distribution & Installation:**
|
||||
|
||||
- Pre-compiled binaries for 5 platforms:
|
||||
- Linux x86_64
|
||||
- Linux ARM64
|
||||
- macOS x86_64 (Intel)
|
||||
- macOS ARM64 (Apple Silicon)
|
||||
- Windows x86_64
|
||||
- Automated binary releases via Gitea Actions
|
||||
- SHA256 checksum verification for all releases
|
||||
- Installation from Gitea releases via updated `install.sh`
|
||||
- Zero dependencies - completely self-contained binaries
|
||||
- Cross-platform compilation from single codebase
|
||||
|
||||
**CI/CD Automation:**
|
||||
|
||||
- Gitea Actions workflows for continuous integration
|
||||
- Automated release workflow triggered by git tags
|
||||
- Automatic binary compilation for all platforms on release
|
||||
- Type checking and linting in CI pipeline
|
||||
- Build verification on every push
|
||||
|
||||
**CLI Improvements:**
|
||||
|
||||
- New hierarchical command structure with subcommands
|
||||
- `nupst service` - Service management (enable, disable, start, stop, restart, status, logs)
|
||||
- `nupst ups` - UPS device management (add, edit, remove, list, test)
|
||||
- `nupst group` - Group management (add, edit, remove, list)
|
||||
- `nupst config show` - Display configuration
|
||||
- `nupst --version` - Show version information
|
||||
- Better help messages organized by category
|
||||
- Backward compatibility maintained with deprecation warnings
|
||||
|
||||
**Technical Improvements:**
|
||||
|
||||
- Deno runtime for modern TypeScript/JavaScript execution
|
||||
- Native TypeScript support without compilation step
|
||||
- Faster startup and execution compared to Node.js
|
||||
- Smaller memory footprint
|
||||
- Built-in permissions system
|
||||
- No build step required for development
|
||||
|
||||
### Migration Guide
|
||||
|
||||
**For Users:**
|
||||
|
||||
1. Stop existing v3.x service: `sudo nupst disable`
|
||||
2. Install v4.0 using new installer:
|
||||
`curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y`
|
||||
3. Your configuration at `/etc/nupst/config.json` is preserved and fully compatible
|
||||
4. Enable service with new CLI: `sudo nupst service enable && sudo nupst service start`
|
||||
5. Update systemd commands to use new syntax (old syntax still works with warnings)
|
||||
|
||||
**Configuration Compatibility:**
|
||||
|
||||
- All configuration files from v3.x work without modification
|
||||
- No changes to `/etc/nupst/config.json` format
|
||||
- All SNMP settings, thresholds, and group configurations preserved
|
||||
|
||||
**Command Mapping:**
|
||||
|
||||
```bash
|
||||
# Old (v3.x) → New (v4.0)
|
||||
nupst enable → nupst service enable
|
||||
nupst disable → nupst service disable
|
||||
nupst start → nupst service start
|
||||
nupst stop → nupst service stop
|
||||
nupst status → nupst service status
|
||||
nupst logs → nupst service logs
|
||||
nupst add → nupst ups add
|
||||
nupst edit [id] → nupst ups edit [id]
|
||||
nupst delete <id> → nupst ups remove <id>
|
||||
nupst list → nupst ups list
|
||||
nupst test → nupst ups test
|
||||
nupst group add → nupst group add (unchanged)
|
||||
nupst group edit <id> → nupst group edit <id> (unchanged)
|
||||
nupst group delete <id> → nupst group remove <id>
|
||||
nupst group list → nupst group list (unchanged)
|
||||
nupst config → nupst config show
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
**Commit History:**
|
||||
|
||||
- `df6a44d`: Complete migration with Gitea Actions workflows and install.sh updates
|
||||
- `9efcc4b`: CLI reorganization with subcommand structure
|
||||
- `5903ae7`: Cross-platform compilation scripts
|
||||
- `a649c59`: Deno migration with npm: and node: specifiers
|
||||
- `5f4f3ec`: Initial migration to Deno
|
||||
|
||||
**Files Changed:**
|
||||
|
||||
- Removed: 11 files (package.json, tsconfig.json, pnpm-lock.yaml, npmextra.json, bin/nupst,
|
||||
setup.sh)
|
||||
- Added: 3 Gitea Actions workflows (ci.yml, release.yml, README.md)
|
||||
- Modified: 14 TypeScript files for Deno compatibility
|
||||
- Updated: install.sh, .gitignore, readme.md
|
||||
- Net reduction: -10,242 lines (93% reduction in repository size)
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- Runtime: Deno v1.x (bundled in binary, no installation required)
|
||||
- SNMP: npm:net-snmp@3.20.0 (bundled in binary via npm: specifier)
|
||||
- Node.js built-ins: Accessed via node: specifier (node:fs, node:child_process, etc.)
|
||||
|
||||
### Benefits
|
||||
|
||||
**For Users:**
|
||||
|
||||
- **Faster Installation**: Download single binary instead of cloning repo + installing Node.js + npm
|
||||
dependencies
|
||||
- **Zero Dependencies**: No Node.js or npm required on target system
|
||||
- **Smaller Footprint**: Single binary vs repo + Node.js + node_modules
|
||||
- **Easier Updates**: Download new binary instead of git pull + npm install
|
||||
- **Better Security**: No npm supply chain risks, binary checksums provided
|
||||
- **Platform Support**: Official binaries for all major platforms
|
||||
|
||||
**For Developers:**
|
||||
|
||||
- **Modern Tooling**: Native TypeScript support without build configuration
|
||||
- **Faster Development**: No compilation step during development
|
||||
- **CI/CD Automation**: Automated releases and testing
|
||||
- **Cleaner Codebase**: 93% reduction in configuration files
|
||||
- **Cross-Platform**: Compile for all platforms from any platform
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Windows ARM64 not supported (Deno limitation)
|
||||
- Binary sizes are larger (~340MB) due to bundled runtime (trade-off for zero dependencies)
|
||||
- First-time execution may be slower on some systems (binary extraction)
|
||||
|
||||
### Acknowledgments
|
||||
|
||||
This release represents a complete modernization of NUPST's infrastructure while maintaining full
|
||||
backward compatibility for user configurations. Special thanks to the Deno team for creating an
|
||||
excellent runtime that made this migration possible.
|
||||
|
||||
---
|
||||
|
||||
## 2025-03-28 - 3.1.2 - fix(cli/ups-handler)
|
||||
|
||||
Improve UPS device listing table formatting for better column alignment
|
||||
|
||||
- Adjusted header spacing for the Host column and overall table alignment in the UPS handler output.
|
||||
|
||||
## 2025-03-28 - 3.1.1 - fix(cli)
|
||||
|
||||
Improve table header formatting in group and UPS listings
|
||||
|
||||
- Adjusted column padding in group listing for proper alignment
|
||||
- Fixed UPS table header spacing for consistent CLI output
|
||||
|
||||
## 2025-03-28 - 3.1.0 - feat(cli)
|
||||
|
||||
Refactor CLI commands to use dedicated handlers for UPS, group, and service management
|
||||
|
||||
- Extracted UPS-related CLI logic into a new UpsHandler
|
||||
@@ -16,28 +194,38 @@ Refactor CLI commands to use dedicated handlers for UPS, group, and service mana
|
||||
- Exposed getters for the new handlers in the Nupst class
|
||||
|
||||
## 2025-03-28 - 3.0.1 - fix(cli)
|
||||
Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module and replacing it with the shortId helper.
|
||||
|
||||
Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module
|
||||
and replacing it with the shortId helper.
|
||||
|
||||
- Deleted the unused promptForUniqueUpsId method from ts/cli.ts.
|
||||
- Updated UPS configuration to generate a unique ID directly using helpers.shortId().
|
||||
- Improved code clarity by removing unnecessary interactive prompts for UPS IDs.
|
||||
|
||||
## 2025-03-28 - 3.0.0 - BREAKING CHANGE(core)
|
||||
Add multi-UPS support and group management; update CLI, configuration and documentation to support multiple UPS devices with group modes
|
||||
|
||||
- Implemented multi-UPS configuration with an array of UPS devices and groups in the configuration file
|
||||
- Added group management commands (group add, edit, delete, list) with redundant and non-redundant modes
|
||||
- Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group subcommands
|
||||
Add multi-UPS support and group management; update CLI, configuration and documentation to support
|
||||
multiple UPS devices with group modes
|
||||
|
||||
- Implemented multi-UPS configuration with an array of UPS devices and groups in the configuration
|
||||
file
|
||||
- Added group management commands (group add, edit, delete, list) with redundant and non-redundant
|
||||
modes
|
||||
- Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group
|
||||
subcommands
|
||||
- Updated readme and documentation to reflect new configuration structure and features
|
||||
- Enhanced logging and status display for multiple UPS devices
|
||||
|
||||
## 2025-03-26 - 2.6.17 - fix(logger)
|
||||
Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set width.
|
||||
|
||||
Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set
|
||||
width.
|
||||
|
||||
- Removed the reset of currentBoxWidth in logBoxEnd to allow persistent width across logbox calls.
|
||||
- Ensures that logBoxLine uses the previously set width when no new width is provided.
|
||||
|
||||
## 2025-03-26 - 2.6.16 - fix(cli)
|
||||
|
||||
Improve CLI logging consistency by replacing direct console output with unified logger calls.
|
||||
|
||||
- Replaced console.log and console.error with logger.log and logger.error in CLI commands
|
||||
@@ -45,42 +233,51 @@ Improve CLI logging consistency by replacing direct console output with unified
|
||||
- Enhanced consistency of log output throughout the ts/cli.ts file
|
||||
|
||||
## 2025-03-26 - 2.6.15 - fix(logger)
|
||||
|
||||
Replace direct console logging with unified logger interface for consistent formatting
|
||||
|
||||
- Substitute console.log, console.error, and related calls with logger methods in cli, daemon, systemd, nupst, and index modules
|
||||
- Substitute console.log, console.error, and related calls with logger methods in cli, daemon,
|
||||
systemd, nupst, and index modules
|
||||
- Integrate logBox formatting for structured output and consistent log presentation
|
||||
- Update test expectations in test.logger.ts to check for standardized error messages
|
||||
- Refactor logging calls throughout the codebase for improved clarity and maintainability
|
||||
|
||||
## 2025-03-26 - 2.6.14 - fix(systemd)
|
||||
|
||||
Shorten closing log divider in systemd service installation output for consistent formatting.
|
||||
|
||||
- Replaced the overly long footer with a shorter one in ts/systemd.ts.
|
||||
- This change improves log readability without affecting functionality.
|
||||
|
||||
## 2025-03-26 - 2.6.13 - fix(cli)
|
||||
|
||||
Fix CLI update output box formatting
|
||||
|
||||
- Adjusted the closing box line in the update process log messages for consistent visual formatting
|
||||
|
||||
## 2025-03-26 - 2.6.12 - fix(systemd)
|
||||
|
||||
Adjust logging border in systemd service installation output
|
||||
|
||||
- Updated the closing border line for consistent output formatting in ts/systemd.ts
|
||||
|
||||
## 2025-03-26 - 2.6.11 - fix(cli, systemd)
|
||||
|
||||
Adjust log formatting for consistent output in CLI and systemd commands
|
||||
|
||||
- Fixed spacing issues in service installation and status log messages in the systemd module.
|
||||
- Revised output formatting in the CLI to improve message clarity.
|
||||
|
||||
## 2025-03-26 - 2.6.10 - fix(daemon)
|
||||
|
||||
Adjust console log box formatting for consistent output in daemon status messages
|
||||
|
||||
- Updated closing box borders to align properly in configuration error, periodic updates, and UPS status logs
|
||||
- Updated closing box borders to align properly in configuration error, periodic updates, and UPS
|
||||
status logs
|
||||
- Improved visual consistency in log messages
|
||||
|
||||
## 2025-03-26 - 2.6.9 - fix(cli)
|
||||
|
||||
Improve console output formatting for status banners and logging messages
|
||||
|
||||
- Standardize banner messages in daemon status updates
|
||||
@@ -88,19 +285,23 @@ Improve console output formatting for status banners and logging messages
|
||||
- Update UPS connection and status banners in systemd
|
||||
|
||||
## 2025-03-26 - 2.6.8 - fix(cli)
|
||||
Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager
|
||||
|
||||
Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP
|
||||
manager
|
||||
|
||||
- Standardize whitespace and formatting in ts/cli.ts for consistency
|
||||
- Refine argument filtering for debug mode and prompt messages
|
||||
- Remove unused 'dgram' import from ts/snmp/manager.ts
|
||||
|
||||
## 2025-03-26 - 2.6.7 - fix(setup.sh)
|
||||
|
||||
Clarify net-snmp dependency installation message in setup.sh
|
||||
|
||||
- Updated echo statement to indicate installation of net-snmp along with 2 subdependencies
|
||||
- Improves clarity on dependency installation during setup
|
||||
|
||||
## 2025-03-26 - 2.6.6 - fix(setup.sh)
|
||||
|
||||
Improve setup script to detect and execute npm-cli.js directly using the Node.js binary
|
||||
|
||||
- Replace use of the npm binary with direct execution of npm-cli.js
|
||||
@@ -108,14 +309,18 @@ Improve setup script to detect and execute npm-cli.js directly using the Node.js
|
||||
- Simplify cleanup by removing unnecessary PATH modifications
|
||||
|
||||
## 2025-03-26 - 2.6.5 - fix(daemon, setup)
|
||||
Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths
|
||||
|
||||
Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm
|
||||
paths
|
||||
|
||||
- Use execFileAsync to execute shutdown commands reliably
|
||||
- Add multiple fallback alternatives for shutdown and emergency shutdown handling
|
||||
- Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH
|
||||
|
||||
## 2025-03-26 - 2.6.4 - fix(setup)
|
||||
Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation.
|
||||
|
||||
Improve installation process in setup script by cleaning up package files and ensuring a minimal
|
||||
net-snmp dependency installation.
|
||||
|
||||
- Remove existing package-lock.json along with node_modules to prevent stale artifacts.
|
||||
- Back up the original package.json before modifying it.
|
||||
@@ -124,13 +329,16 @@ Improve installation process in setup script by cleaning up package files and en
|
||||
- Restore the original package.json if the installation fails.
|
||||
|
||||
## 2025-03-26 - 2.6.3 - fix(setup)
|
||||
Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control.
|
||||
|
||||
Update setup script to install only net-snmp dependency and create a minimal package-lock.json for
|
||||
better dependency control.
|
||||
|
||||
- Removed full production dependency install in favor of installing only net-snmp@3.20.0
|
||||
- Added verification step to confirm net-snmp installation
|
||||
- Generate a minimal package-lock.json if one does not exist
|
||||
|
||||
## 2025-03-26 - 2.6.2 - fix(setup/readme)
|
||||
|
||||
Improve force update instructions and dependency installation process in setup.sh and readme.md
|
||||
|
||||
- Clarify force update commands with explicit paths in readme.md
|
||||
@@ -138,13 +346,16 @@ Improve force update instructions and dependency installation process in setup.s
|
||||
- Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions
|
||||
|
||||
## 2025-03-26 - 2.6.1 - fix(setup)
|
||||
Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards.
|
||||
|
||||
Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log
|
||||
Node and npm versions, and restore the original PATH afterwards.
|
||||
|
||||
- Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution.
|
||||
- Log Node.js and npm versions for debugging purposes.
|
||||
- Restore the original PATH after installing dependencies.
|
||||
|
||||
## 2025-03-26 - 2.6.0 - feat(setup)
|
||||
|
||||
Add --force update flag to setup script and update installation instructions
|
||||
|
||||
- Implemented --force option in setup.sh to force-update Node.js binary and dependencies
|
||||
@@ -152,27 +363,33 @@ Add --force update flag to setup script and update installation instructions
|
||||
- Modified ts/cli.ts update command to pass the --force flag to setup.sh
|
||||
|
||||
## 2025-03-26 - 2.5.2 - fix(installer)
|
||||
|
||||
Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic
|
||||
|
||||
- Enhanced bin/nupst to detect OS and architecture (Linux and Darwin) and fall back to system Node.js for unsupported platforms
|
||||
- Enhanced bin/nupst to detect OS and architecture (Linux and Darwin) and fall back to system
|
||||
Node.js for unsupported platforms
|
||||
- Moved net-snmp from devDependencies to dependencies in package.json
|
||||
- Updated setup.sh to install production dependencies and handle installation errors gracefully
|
||||
- Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts
|
||||
- Revised README to clarify minimal runtime dependencies and secure SNMP features
|
||||
|
||||
## 2025-03-25 - 2.5.1 - fix(snmp)
|
||||
|
||||
Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion.
|
||||
|
||||
- Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery status.
|
||||
- Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery
|
||||
status.
|
||||
- Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager.
|
||||
|
||||
## 2025-03-25 - 2.5.0 - feat(cli)
|
||||
|
||||
Automatically restart running NUPST service after configuration changes in interactive setup
|
||||
|
||||
- Added restartServiceIfRunning() to check and restart the service if it's active.
|
||||
- Invoked the restart function post-setup to apply configuration changes immediately.
|
||||
|
||||
## 2025-03-25 - 2.4.8 - fix(installer)
|
||||
|
||||
Improve Git dependency handling and repository cloning in install.sh
|
||||
|
||||
- Add explicit check for git installation and prompt the user interactively if git is missing.
|
||||
@@ -180,23 +397,30 @@ Improve Git dependency handling and repository cloning in install.sh
|
||||
- Ensure proper cloning of the repository when running the installer outside the repo.
|
||||
|
||||
## 2025-03-25 - 2.4.7 - fix(readme)
|
||||
|
||||
Update installation instructions to combine download and execution into a single command for clarity
|
||||
|
||||
- Method 1 now uses a unified one-line command to download and run the install script
|
||||
|
||||
## 2025-03-25 - 2.4.6 - fix(installer)
|
||||
|
||||
Improve installation instructions for interactive and non-interactive setups
|
||||
|
||||
- Changed install.sh to require explicit download of the install script and updated error messages for non-interactive modes
|
||||
- Changed install.sh to require explicit download of the install script and updated error messages
|
||||
for non-interactive modes
|
||||
- Updated readme.md to include three distinct installation methods with clear command examples
|
||||
|
||||
## 2025-03-25 - 2.4.5 - fix(install)
|
||||
|
||||
Improve interactive terminal detection and update installation instructions
|
||||
|
||||
- Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for both interactive and non-interactive installations
|
||||
- Updated README.md quick install instructions to recommend process substitution and clarify auto-yes usage
|
||||
- Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for
|
||||
both interactive and non-interactive installations
|
||||
- Updated README.md quick install instructions to recommend process substitution and clarify
|
||||
auto-yes usage
|
||||
|
||||
## 2025-03-25 - 2.4.4 - fix(install)
|
||||
|
||||
Improve interactive mode detection and non-interactive installation handling in install.sh
|
||||
|
||||
- Detect and warn when running without a controlling terminal
|
||||
@@ -205,86 +429,116 @@ Improve interactive mode detection and non-interactive installation handling in
|
||||
- Clarify installation instructions in readme for interactive and non-interactive modes
|
||||
|
||||
## 2025-03-25 - 2.4.3 - fix(readme)
|
||||
|
||||
Update Quick Install command syntax in readme for auto-yes installation
|
||||
|
||||
- Changed installation command to use: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -c "bash -s -- -y"
|
||||
- Changed installation command to use: curl -sSL
|
||||
https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -c "bash -s --
|
||||
-y"
|
||||
|
||||
## 2025-03-25 - 2.4.2 - fix(daemon)
|
||||
Refactor shutdown initiation logic in daemon by moving the initiateShutdown and monitorDuringShutdown methods from the SNMP manager to the daemon, and update calls accordingly
|
||||
|
||||
Refactor shutdown initiation logic in daemon by moving the initiateShutdown and
|
||||
monitorDuringShutdown methods from the SNMP manager to the daemon, and update calls accordingly
|
||||
|
||||
- Moved initiateShutdown and monitorDuringShutdown to the daemon class for improved cohesion
|
||||
- Updated references in the daemon to call its own shutdown method instead of the SNMP manager
|
||||
- Removed redundant initiateShutdown method from the SNMP manager
|
||||
|
||||
## 2025-03-25 - 2.4.1 - fix(docs)
|
||||
|
||||
Update readme with detailed legal and trademark guidance
|
||||
|
||||
- Clarified legal section by adding trademark and company information
|
||||
- Ensured users understand that licensing terms do not imply endorsement by the company
|
||||
|
||||
## 2025-03-25 - 2.4.0 - feat(installer)
|
||||
|
||||
Add auto-yes flag to installer and update installation documentation
|
||||
|
||||
- Enhance install.sh to parse -y/--yes and -h/--help options, automating git installation when auto-yes is provided
|
||||
- Enhance install.sh to parse -y/--yes and -h/--help options, automating git installation when
|
||||
auto-yes is provided
|
||||
- Improve user prompts for dependency installation and provide clearer instructions
|
||||
- Update readme.md to document new installer options and enhanced file system and service changes details
|
||||
- Update readme.md to document new installer options and enhanced file system and service changes
|
||||
details
|
||||
|
||||
## 2025-03-25 - 2.3.0 - feat(installer/cli)
|
||||
Add OS detection and git auto-installation support to install.sh and improve service setup prompt in CLI
|
||||
|
||||
- Implemented helper functions in install.sh to detect OS type and automatically install git if missing
|
||||
Add OS detection and git auto-installation support to install.sh and improve service setup prompt in
|
||||
CLI
|
||||
|
||||
- Implemented helper functions in install.sh to detect OS type and automatically install git if
|
||||
missing
|
||||
- Prompt user for git installation if not present before cloning the repository
|
||||
- Enhanced CLI service setup flow to offer starting the NUPST service immediately after installation
|
||||
|
||||
## 2025-03-25 - 2.2.0 - feat(cli)
|
||||
|
||||
Add 'config' command to display current configuration and update CLI help
|
||||
|
||||
- Introduce new 'config' command to show SNMP settings, thresholds, and configuration file location
|
||||
- Update help text to include details for 'nupst config' command
|
||||
|
||||
## 2025-03-25 - 2.1.0 - feat(cli)
|
||||
|
||||
Add uninstall command to CLI and update shutdown delay for graceful VM shutdown
|
||||
|
||||
- Implement uninstall command in ts/cli.ts that locates and executes uninstall.sh with user prompts
|
||||
- Update uninstall.sh to support environment variables for configuration and repository removal
|
||||
- Increase shutdown delay in ts/snmp/manager.ts from 1 minute to 5 minutes to allow VMs more time to shut down
|
||||
- Increase shutdown delay in ts/snmp/manager.ts from 1 minute to 5 minutes to allow VMs more time to
|
||||
shut down
|
||||
|
||||
## 2025-03-25 - 2.0.1 - fix(cli/systemd)
|
||||
|
||||
Fix status command to pass debug flag and improve systemd status logging output
|
||||
|
||||
- ts/cli.ts: Now extracts debug options from process arguments and passes debug mode to getStatus.
|
||||
- ts/systemd.ts: Updated getStatus to accept a debugMode parameter, enabling detailed SNMP debug logging, explicitly reloading configuration, and printing connection details.
|
||||
- ts/systemd.ts: Updated getStatus to accept a debugMode parameter, enabling detailed SNMP debug
|
||||
logging, explicitly reloading configuration, and printing connection details.
|
||||
|
||||
## 2025-03-25 - 2.0.0 - BREAKING CHANGE(snmp)
|
||||
|
||||
refactor: update SNMP type definitions and interface names for consistency
|
||||
|
||||
- Renamed SnmpConfig to ISnmpConfig, OIDSet to IOidSet, UpsStatus to IUpsStatus, and UpsModel to TUpsModel in ts/snmp/types.ts.
|
||||
- Updated internal references in ts/daemon.ts, ts/snmp/index.ts, ts/snmp/manager.ts, ts/snmp/oid-sets.ts, ts/snmp/packet-creator.ts, and ts/snmp/packet-parser.ts to use the new interface names.
|
||||
- Renamed SnmpConfig to ISnmpConfig, OIDSet to IOidSet, UpsStatus to IUpsStatus, and UpsModel to
|
||||
TUpsModel in ts/snmp/types.ts.
|
||||
- Updated internal references in ts/daemon.ts, ts/snmp/index.ts, ts/snmp/manager.ts,
|
||||
ts/snmp/oid-sets.ts, ts/snmp/packet-creator.ts, and ts/snmp/packet-parser.ts to use the new
|
||||
interface names.
|
||||
|
||||
## 2025-03-25 - 1.10.1 - fix(systemd/readme)
|
||||
|
||||
Improve README documentation and fix UPS status retrieval in systemd service
|
||||
|
||||
- Updated README features and installation instructions to clarify SNMP version support, UPS models, and configuration
|
||||
- Modified default SNMP host to '192.168.1.100' and added 'upsModel' property in configuration examples
|
||||
- Updated README features and installation instructions to clarify SNMP version support, UPS models,
|
||||
and configuration
|
||||
- Modified default SNMP host to '192.168.1.100' and added 'upsModel' property in configuration
|
||||
examples
|
||||
- Enhanced instructions for real-time log viewing and update process in README
|
||||
- Fixed systemd.ts to use a test configuration with an appropriate timeout when fetching UPS status
|
||||
|
||||
## 2025-03-25 - 1.10.0 - feat(core)
|
||||
|
||||
Add update checking and version logging across startup components
|
||||
|
||||
- In daemon.ts, log version info on startup and check for updates in the background using npm registry response
|
||||
- In nupst.ts, implement getVersion, checkForUpdates, getUpdateStatus, and compareVersions functions with update notifications
|
||||
- In daemon.ts, log version info on startup and check for updates in the background using npm
|
||||
registry response
|
||||
- In nupst.ts, implement getVersion, checkForUpdates, getUpdateStatus, and compareVersions functions
|
||||
with update notifications
|
||||
- Establish bidirectional reference between Nupst and NupstSnmp to support version logging
|
||||
- Update systemd service status output to include version information
|
||||
|
||||
## 2025-03-25 - 1.9.0 - feat(cli)
|
||||
|
||||
Add update command to CLI to update NUPST from repository and refresh the systemd service
|
||||
|
||||
- Integrate 'update' subcommand in CLI command parser
|
||||
- Update documentation and help output to include new command
|
||||
- Implement update process that fetches changes from git, runs install.sh/setup.sh, and refreshes systemd service if installed
|
||||
- Implement update process that fetches changes from git, runs install.sh/setup.sh, and refreshes
|
||||
systemd service if installed
|
||||
|
||||
## 2025-03-25 - 1.8.2 - fix(cli)
|
||||
|
||||
Refactor logs command to use child_process spawn for real-time log tailing
|
||||
|
||||
- Replaced execSync call with spawn to properly follow logs
|
||||
@@ -292,12 +546,15 @@ Refactor logs command to use child_process spawn for real-time log tailing
|
||||
- Await the child process exit to ensure clean shutdown of the CLI log command
|
||||
|
||||
## 2025-03-25 - 1.8.1 - fix(systemd)
|
||||
|
||||
Update ExecStart in systemd service template to use /opt/nupst/bin/nupst for daemon startup
|
||||
|
||||
- Changed ExecStart from '/usr/bin/nupst daemon-start' to '/opt/nupst/bin/nupst daemon-start' in the systemd service file
|
||||
- Changed ExecStart from '/usr/bin/nupst daemon-start' to '/opt/nupst/bin/nupst daemon-start' in the
|
||||
systemd service file
|
||||
- Ensures the service uses the correct binary installation path
|
||||
|
||||
## 2025-03-25 - 1.8.0 - feat(core)
|
||||
|
||||
Enhance SNMP module and interactive CLI setup for UPS shutdown
|
||||
|
||||
- Refactored SNMP packet parsing and encoding utilities for clearer error handling and debugging
|
||||
@@ -306,22 +563,28 @@ Enhance SNMP module and interactive CLI setup for UPS shutdown
|
||||
- Expanded test coverage with simulated SNMP responses for various response types
|
||||
|
||||
## 2025-03-25 - 1.7.6 - fix(core)
|
||||
|
||||
Refactor SNMP, systemd, and CLI modules to improve error handling, logging, and code clarity
|
||||
|
||||
- Removed unused dependency 'net-snmp' from package.json
|
||||
- Extracted helper functions for SNMP packet creation and parsing (using SnmpEncoder, SnmpPacketCreator and SnmpPacketParser)
|
||||
- Improved debug logging and added detailed documentation comments across SNMP, systemd, CLI and daemon modules
|
||||
- Extracted helper functions for SNMP packet creation and parsing (using SnmpEncoder,
|
||||
SnmpPacketCreator and SnmpPacketParser)
|
||||
- Improved debug logging and added detailed documentation comments across SNMP, systemd, CLI and
|
||||
daemon modules
|
||||
- Refactored systemd service management to extract status display and service disabling logic
|
||||
- Updated test suite to use proper modular methods from the new SNMP utilities
|
||||
|
||||
## 2025-03-25 - 1.7.5 - fix(cli)
|
||||
Enable SNMP debug mode in CLI commands and update debug flag handling in daemon-start and test; bump version to 1.7.4
|
||||
|
||||
Enable SNMP debug mode in CLI commands and update debug flag handling in daemon-start and test; bump
|
||||
version to 1.7.4
|
||||
|
||||
- Call enableDebug() on SNMP client earlier in command parsing
|
||||
- Pass debug flag to 'daemon-start' and 'test' commands for consistent debug output
|
||||
- Update package version from 1.7.3 to 1.7.4
|
||||
|
||||
## 2025-03-25 - 1.7.3 - fix(SNMP)
|
||||
|
||||
Refine SNMP packet creation and response parsing for more reliable UPS status monitoring
|
||||
|
||||
- Improve error handling and fallback logic when parsing SNMP responses
|
||||
@@ -329,13 +592,16 @@ Refine SNMP packet creation and response parsing for more reliable UPS status mo
|
||||
- Enhance test coverage for various UPS scenarios
|
||||
|
||||
## 2025-03-25 - 1.7.2 - fix(core)
|
||||
Refactor internal SNMP response parsing and enhance daemon logging for improved error reporting and clarity.
|
||||
|
||||
Refactor internal SNMP response parsing and enhance daemon logging for improved error reporting and
|
||||
clarity.
|
||||
|
||||
- Improved fallback and error handling in SNMP response parsing
|
||||
- Enhanced logging messages in daemon and systemd service management
|
||||
- Minor refactoring for better maintainability without functional changes
|
||||
|
||||
## 2025-03-25 - 1.7.1 - fix(snmp-cli)
|
||||
|
||||
Improve SNMP response parsing and CLI UPS connection timeout handling
|
||||
|
||||
- Expand parsing loop in SNMP responses to capture Gauge32 and Timeticks values
|
||||
@@ -343,14 +609,17 @@ Improve SNMP response parsing and CLI UPS connection timeout handling
|
||||
- Configure CLI test commands to use a shortened timeout for UPS connection tests
|
||||
|
||||
## 2025-03-25 - 1.7.0 - feat(SNMP/UPS)
|
||||
|
||||
Add UPS model selection and custom OIDs support to handle different UPS brands
|
||||
|
||||
- Introduce distinct OID sets for CyberPower, APC, Eaton, TrippLite, Liebert, and a custom option
|
||||
- Update interactive setup to prompt for UPS model selection and custom OID entry when needed
|
||||
- Refactor SNMP status retrieval to dynamically select the appropriate OIDs based on the configured UPS model
|
||||
- Refactor SNMP status retrieval to dynamically select the appropriate OIDs based on the configured
|
||||
UPS model
|
||||
- Extend default configuration with an upsModel property for consistent behavior
|
||||
|
||||
## 2025-03-25 - 1.6.0 - feat(cli,snmp)
|
||||
|
||||
Enhance debug logging and add debug mode support in CLI and SNMP modules
|
||||
|
||||
- Enable debug flags (--debug, -d) in CLI to trigger detailed SNMP logging
|
||||
@@ -359,6 +628,7 @@ Enhance debug logging and add debug mode support in CLI and SNMP modules
|
||||
- Improve timeout and discovery logging details for streamlined troubleshooting
|
||||
|
||||
## 2025-03-25 - 1.5.0 - feat(cli)
|
||||
|
||||
Enhance CLI output: display SNMPv3 auth/priv details and support timeout customization during setup
|
||||
|
||||
- Display authentication and privacy protocol details when SNMP version is 3
|
||||
@@ -367,10 +637,11 @@ Enhance CLI output: display SNMPv3 auth/priv details and support timeout customi
|
||||
- Allow users to customize SNMP timeout during interactive setup
|
||||
|
||||
## 2025-03-25 - 1.4.1 - fix(version)
|
||||
|
||||
Bump patch version for consistency with commit info
|
||||
|
||||
|
||||
## 2025-03-25 - 1.4.0 - feat(snmp)
|
||||
|
||||
Implement native SNMPv3 support with simulated encryption and enhanced authentication handling.
|
||||
|
||||
- Add fully native SNMPv3 GET request implementation replacing the snmpwalk fallback
|
||||
@@ -379,12 +650,14 @@ Implement native SNMPv3 support with simulated encryption and enhanced authentic
|
||||
- Introduce detailed security parameter management for SNMPv3
|
||||
|
||||
## 2025-03-25 - 1.3.1 - fix(cli)
|
||||
|
||||
Remove redundant SNMP tools checks in CLI and Systemd modules
|
||||
|
||||
- Eliminate unnecessary snmpwalk dependency checks in the test command and interactive setup flow.
|
||||
- Adjust systemd configuration file check to avoid external dependency verification.
|
||||
|
||||
## 2025-03-25 - 1.3.0 - feat(cli)
|
||||
|
||||
add test command to verify UPS SNMP configuration and connectivity
|
||||
|
||||
- Introduce a new 'test' command in the CLI to check the SNMP configuration and UPS connection.
|
||||
@@ -392,6 +665,7 @@ add test command to verify UPS SNMP configuration and connectivity
|
||||
- Output UPS status details and compare against defined shutdown thresholds.
|
||||
|
||||
## 2025-03-25 - 1.2.6 - fix(cli)
|
||||
|
||||
Refactor interactive setup to use dynamic import for readline and ensure proper cleanup
|
||||
|
||||
- Replaced synchronous require() with async import for ESM compatibility
|
||||
@@ -399,13 +673,16 @@ Refactor interactive setup to use dynamic import for readline and ensure proper
|
||||
- Enhanced error logging by outputting error.message
|
||||
|
||||
## 2025-03-25 - 1.2.5 - fix(error-handling)
|
||||
Improve error handling in CLI, daemon, and systemd lifecycle management with enhanced logging for configuration issues
|
||||
|
||||
Improve error handling in CLI, daemon, and systemd lifecycle management with enhanced logging for
|
||||
configuration issues
|
||||
|
||||
- Wrap daemon and service start commands in try-catch blocks to properly handle and log errors
|
||||
- Throw explicit errors when configuration file is missing instead of silently defaulting
|
||||
- Enhance log messages for service installation, startup, and status retrieval for clearer debugging
|
||||
|
||||
## 2025-03-25 - 1.2.4 - fix(cli/daemon)
|
||||
|
||||
Improve logging and user feedback in interactive setup and UPS monitoring
|
||||
|
||||
- Refactor configuration summary output in the interactive setup for clearer display
|
||||
@@ -413,17 +690,20 @@ Improve logging and user feedback in interactive setup and UPS monitoring
|
||||
- Improve error messages and user guidance during configuration and monitoring
|
||||
|
||||
## 2025-03-24 - 1.2.3 - fix(nupst)
|
||||
|
||||
No changes
|
||||
|
||||
|
||||
## 2025-03-24 - 1.2.2 - fix(bin/nupst)
|
||||
Improve symlink resolution in launcher script to correctly determine project root based on execution path.
|
||||
|
||||
Improve symlink resolution in launcher script to correctly determine project root based on execution
|
||||
path.
|
||||
|
||||
- Replace directory determination with readlink for accurate symlink resolution
|
||||
- Set project root to '/opt/nupst' when script is run via symlink from /usr/local/bin
|
||||
- Add debugging comments to assist with path resolution
|
||||
|
||||
## 2025-03-24 - 1.2.1 - fix(bin)
|
||||
|
||||
Simplify Node.js binary detection in installation script
|
||||
|
||||
- Directly set Node binary path to vendor/node-linux-x64/bin/node
|
||||
@@ -431,59 +711,78 @@ Simplify Node.js binary detection in installation script
|
||||
- Fallback to system Node if vendor binary is not found
|
||||
|
||||
## 2025-03-24 - 1.2.0 - feat(installer)
|
||||
|
||||
Improve Node.js binary detection and dynamic LTS version retrieval in setup scripts
|
||||
|
||||
- Enhanced bin/nupst to search multiple possible locations for the Node.js binary and fallback to system node if necessary
|
||||
- Updated setup.sh to fetch the latest LTS Node.js version from nodejs.org and use a fallback version when the request fails
|
||||
- Enhanced bin/nupst to search multiple possible locations for the Node.js binary and fallback to
|
||||
system node if necessary
|
||||
- Updated setup.sh to fetch the latest LTS Node.js version from nodejs.org and use a fallback
|
||||
version when the request fails
|
||||
|
||||
## 2025-03-24 - 1.1.2 - fix(setup.sh)
|
||||
Improve error handling in setup.sh: exit immediately when the downloaded npm package lacks the dist_ts directory, removing the fallback build-from-source mechanism.
|
||||
|
||||
Improve error handling in setup.sh: exit immediately when the downloaded npm package lacks the
|
||||
dist_ts directory, removing the fallback build-from-source mechanism.
|
||||
|
||||
- Removed BUILD_FROM_SOURCE logic that attempted to build from source on missing dist_ts directory
|
||||
- Updated error messages to clearly indicate failure in downloading a valid package
|
||||
- Ensured installation halts if essential files are missing
|
||||
|
||||
## 2025-03-24 - 1.1.1 - fix(package.json)
|
||||
|
||||
Remove unused prepublishOnly script and update files field in package.json
|
||||
|
||||
- Removed prepublishOnly build trigger
|
||||
- Updated files list to accurately include intended directories and files
|
||||
|
||||
## 2025-03-24 - 1.1.0 - feat(installer-setup)
|
||||
|
||||
Enhance installer and setup scripts for improved global installation and artifact management
|
||||
|
||||
- Detect piped installation in install.sh, clone repository automatically, and clean up previous installations
|
||||
- Detect piped installation in install.sh, clone repository automatically, and clean up previous
|
||||
installations
|
||||
- Update readme.md with correct repository URL and clearer installation instructions
|
||||
- Improve setup.sh to remove existing dist_ts, download build artifacts from the npm registry, and simplify dependency installation
|
||||
- Improve setup.sh to remove existing dist_ts, download build artifacts from the npm registry, and
|
||||
simplify dependency installation
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(version)
|
||||
|
||||
Bump version to 1.0.1
|
||||
|
||||
- Updated commitinfo data to reflect the new patch version.
|
||||
- Synchronized version information between commitinfo file and package metadata.
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(build)
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and adjust distribution paths in .gitignore
|
||||
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and adjust distribution paths in
|
||||
.gitignore
|
||||
|
||||
- Replaced 'tsc' with 'tsbuild tsfolders --allowimplicitany' in package.json
|
||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
||||
- Updated changelog to document build improvements and regenerated type definitions
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(build)
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type definitions for CLI, daemon, index, nupst, snmp, and systemd modules
|
||||
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type
|
||||
definitions for CLI, daemon, index, nupst, snmp, and systemd modules
|
||||
|
||||
- Replaced 'tsc' command with tsbuild in package.json
|
||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
||||
- Added new dist_ts files including .d.ts type definitions and compiled JavaScript for multiple modules
|
||||
- Added new dist_ts files including .d.ts type definitions and compiled JavaScript for multiple
|
||||
modules
|
||||
|
||||
## 2025-03-24 - 1.0.1 - fix(build)
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type definitions for CLI, daemon, nupst, snmp, and systemd modules.
|
||||
|
||||
Update build script to use 'tsbuild tsfolders --allowimplicitany' and regenerate distribution type
|
||||
definitions for CLI, daemon, nupst, snmp, and systemd modules.
|
||||
|
||||
- Replaced the 'tsc' command with 'tsbuild tsfolders --allowimplicitany' in package.json.
|
||||
- Added new dist_ts files including type definitions (d.ts) and compiled JavaScript for CLI, daemon, index, nupst, snmp, and systemd.
|
||||
- Added new dist_ts files including type definitions (d.ts) and compiled JavaScript for CLI, daemon,
|
||||
index, nupst, snmp, and systemd.
|
||||
- Improved the generated CLI declarations and overall distribution build.
|
||||
|
||||
## 2025-03-23 - 1.0.0 - initial setup
|
||||
|
||||
This range covers the early commits that mainly established the repository structure.
|
||||
|
||||
- Initial repository commit with basic project initialization.
|
||||
- Initial repository commit with basic project initialization.
|
||||
|
36
deno.json
Normal file
36
deno.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "5.0.3",
|
||||
"exports": "./mod.ts",
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-all mod.ts",
|
||||
"compile": "deno task compile:all",
|
||||
"compile:all": "bash scripts/compile-all.sh",
|
||||
"test": "deno test --allow-all test/",
|
||||
"test:watch": "deno test --allow-all --watch test/",
|
||||
"check": "deno check mod.ts",
|
||||
"fmt": "deno fmt",
|
||||
"lint": "deno lint"
|
||||
},
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": ["recommended"]
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"useTabs": false,
|
||||
"lineWidth": 100,
|
||||
"indentWidth": 2,
|
||||
"semiColons": true,
|
||||
"singleQuote": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.window"],
|
||||
"strict": true
|
||||
},
|
||||
"imports": {
|
||||
"@std/cli": "jsr:@std/cli@^1.0.0",
|
||||
"@std/fmt": "jsr:@std/fmt@^1.0.0",
|
||||
"@std/path": "jsr:@std/path@^1.0.0"
|
||||
}
|
||||
}
|
122
example-action.sh
Normal file
122
example-action.sh
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
# NUPST Action Script Example
|
||||
# Copy this to /etc/nupst/ and customize for your needs
|
||||
#
|
||||
# This script is called by NUPST when power events or threshold violations occur.
|
||||
# It receives UPS state information via environment variables and command-line arguments.
|
||||
|
||||
# ==============================================================================
|
||||
# ARGUMENTS (positional parameters)
|
||||
# ==============================================================================
|
||||
# $1 = Power Status (online|onBattery|unknown)
|
||||
# $2 = Battery Capacity (percentage, 0-100)
|
||||
# $3 = Battery Runtime (estimated minutes remaining)
|
||||
|
||||
POWER_STATUS=$1
|
||||
BATTERY_CAPACITY=$2
|
||||
BATTERY_RUNTIME=$3
|
||||
|
||||
# ==============================================================================
|
||||
# ENVIRONMENT VARIABLES
|
||||
# ==============================================================================
|
||||
# NUPST_UPS_ID - Unique UPS identifier
|
||||
# NUPST_UPS_NAME - Human-readable UPS name
|
||||
# NUPST_POWER_STATUS - Current power status
|
||||
# NUPST_BATTERY_CAPACITY - Battery percentage (0-100)
|
||||
# NUPST_BATTERY_RUNTIME - Estimated runtime in minutes
|
||||
# NUPST_THRESHOLDS_EXCEEDED - "true" if below configured thresholds
|
||||
# NUPST_TRIGGER_REASON - "powerStatusChange" or "thresholdViolation"
|
||||
# NUPST_BATTERY_THRESHOLD - Configured battery threshold percentage
|
||||
# NUPST_RUNTIME_THRESHOLD - Configured runtime threshold in minutes
|
||||
# NUPST_TIMESTAMP - Unix timestamp (milliseconds since epoch)
|
||||
|
||||
# ==============================================================================
|
||||
# EXAMPLE: Log the event
|
||||
# ==============================================================================
|
||||
LOG_FILE="/var/log/nupst-actions.log"
|
||||
|
||||
echo "========================================" >> "$LOG_FILE"
|
||||
echo "NUPST Action Triggered: $(date)" >> "$LOG_FILE"
|
||||
echo "----------------------------------------" >> "$LOG_FILE"
|
||||
echo "UPS: $NUPST_UPS_NAME ($NUPST_UPS_ID)" >> "$LOG_FILE"
|
||||
echo "Power Status: $POWER_STATUS" >> "$LOG_FILE"
|
||||
echo "Battery: $BATTERY_CAPACITY%" >> "$LOG_FILE"
|
||||
echo "Runtime: $BATTERY_RUNTIME minutes" >> "$LOG_FILE"
|
||||
echo "Trigger Reason: $NUPST_TRIGGER_REASON" >> "$LOG_FILE"
|
||||
echo "Thresholds Exceeded: $NUPST_THRESHOLDS_EXCEEDED" >> "$LOG_FILE"
|
||||
echo "========================================" >> "$LOG_FILE"
|
||||
|
||||
# ==============================================================================
|
||||
# EXAMPLE: Send email notification
|
||||
# ==============================================================================
|
||||
# if [ "$NUPST_TRIGGER_REASON" = "thresholdViolation" ]; then
|
||||
# echo "ALERT: UPS $NUPST_UPS_NAME battery critical!" | \
|
||||
# mail -s "UPS Battery Critical" admin@example.com
|
||||
# fi
|
||||
|
||||
# ==============================================================================
|
||||
# EXAMPLE: Gracefully shutdown virtual machines
|
||||
# ==============================================================================
|
||||
# if [ "$NUPST_POWER_STATUS" = "onBattery" ] && [ "$NUPST_THRESHOLDS_EXCEEDED" = "true" ]; then
|
||||
# echo "Shutting down VMs..." >> "$LOG_FILE"
|
||||
# # virsh shutdown vm1
|
||||
# # virsh shutdown vm2
|
||||
# # Wait for VMs to shutdown
|
||||
# # sleep 120
|
||||
# fi
|
||||
|
||||
# ==============================================================================
|
||||
# EXAMPLE: Call external API/service
|
||||
# ==============================================================================
|
||||
# curl -X POST https://monitoring.example.com/ups-alert \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d "{
|
||||
# \"upsId\": \"$NUPST_UPS_ID\",
|
||||
# \"upsName\": \"$NUPST_UPS_NAME\",
|
||||
# \"powerStatus\": \"$POWER_STATUS\",
|
||||
# \"batteryCapacity\": $BATTERY_CAPACITY,
|
||||
# \"batteryRuntime\": $BATTERY_RUNTIME,
|
||||
# \"triggerReason\": \"$NUPST_TRIGGER_REASON\"
|
||||
# }"
|
||||
|
||||
# ==============================================================================
|
||||
# EXAMPLE: Remote shutdown via SSH with password
|
||||
# ==============================================================================
|
||||
# You can implement custom shutdown logic for remote systems
|
||||
# that require password authentication or webhooks
|
||||
#
|
||||
# if [ "$NUPST_THRESHOLDS_EXCEEDED" = "true" ]; then
|
||||
# # Call a webhook with a secret password/token
|
||||
# curl -X POST "https://remote-server.local/shutdown?token=YOUR_SECRET_TOKEN"
|
||||
#
|
||||
# # Or use SSH with password (requires sshpass)
|
||||
# # sshpass -p 'your-password' ssh user@remote-server 'sudo shutdown -h +5'
|
||||
# fi
|
||||
|
||||
# ==============================================================================
|
||||
# EXAMPLE: Conditional logic based on battery level
|
||||
# ==============================================================================
|
||||
# if [ "$BATTERY_CAPACITY" -lt 20 ]; then
|
||||
# echo "Battery critically low! Immediate action needed." >> "$LOG_FILE"
|
||||
# elif [ "$BATTERY_CAPACITY" -lt 50 ]; then
|
||||
# echo "Battery low. Preparing for shutdown." >> "$LOG_FILE"
|
||||
# else
|
||||
# echo "Battery acceptable. Monitoring." >> "$LOG_FILE"
|
||||
# fi
|
||||
|
||||
# ==============================================================================
|
||||
# EXAMPLE: Different actions for different trigger reasons
|
||||
# ==============================================================================
|
||||
# case "$NUPST_TRIGGER_REASON" in
|
||||
# powerStatusChange)
|
||||
# echo "Power status changed to: $POWER_STATUS" >> "$LOG_FILE"
|
||||
# # Send notification but don't take drastic action yet
|
||||
# ;;
|
||||
# thresholdViolation)
|
||||
# echo "Thresholds violated! Taking emergency action." >> "$LOG_FILE"
|
||||
# # Initiate graceful shutdowns, save data, etc.
|
||||
# ;;
|
||||
# esac
|
||||
|
||||
# Exit with success
|
||||
exit 0
|
461
install.sh
461
install.sh
@@ -1,44 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NUPST Installer Script
|
||||
# Downloads and installs NUPST globally on the system
|
||||
# Can be used directly with curl:
|
||||
# Without auto-installing dependencies:
|
||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||
# With auto-installing dependencies:
|
||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y
|
||||
#
|
||||
# NUPST Installer Script (v5.0+)
|
||||
# Downloads and installs pre-compiled NUPST binary from Gitea releases
|
||||
#
|
||||
# Usage:
|
||||
# Direct piped installation (recommended):
|
||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||
#
|
||||
# With version specification:
|
||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0
|
||||
#
|
||||
# Options:
|
||||
# -y, --yes Automatically answer yes to all prompts
|
||||
# -h, --help Show this help message
|
||||
# -h, --help Show this help message
|
||||
# --version VERSION Install specific version (e.g., v4.0.0)
|
||||
# --install-dir DIR Installation directory (default: /opt/nupst)
|
||||
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
SHOW_HELP=0
|
||||
SPECIFIED_VERSION=""
|
||||
INSTALL_DIR="/opt/nupst"
|
||||
GITEA_BASE_URL="https://code.foss.global"
|
||||
GITEA_REPO="serve.zone/nupst"
|
||||
|
||||
# Parse command line arguments
|
||||
AUTO_YES=0
|
||||
SHOW_HELP=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
-y|--yes)
|
||||
AUTO_YES=1
|
||||
shift
|
||||
;;
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
SHOW_HELP=1
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
SPECIFIED_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--install-dir)
|
||||
INSTALL_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
# Unknown option
|
||||
echo "Unknown option: $1"
|
||||
echo "Use -h or --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ $SHOW_HELP -eq 1 ]; then
|
||||
echo "NUPST Installer Script"
|
||||
echo "NUPST Installer Script (v5.0+)"
|
||||
echo "Downloads and installs pre-compiled NUPST binary"
|
||||
echo ""
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -y, --yes Automatically answer yes to all prompts"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " --version VERSION Install specific version (e.g., v5.0.0)"
|
||||
echo " --install-dir DIR Installation directory (default: /opt/nupst)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # Install latest version"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||
echo ""
|
||||
echo " # Install specific version"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -48,249 +73,201 @@ if [ "$EUID" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect if script is being piped or run directly
|
||||
PIPED=0
|
||||
INTERACTIVE=1
|
||||
if [ ! -t 0 ]; then
|
||||
# Being piped, need to clone the repo
|
||||
PIPED=1
|
||||
fi
|
||||
# Helper function to detect OS and architecture
|
||||
detect_platform() {
|
||||
local os=$(uname -s)
|
||||
local arch=$(uname -m)
|
||||
|
||||
# Check if stdin is a terminal
|
||||
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
||||
# Either stdin or stdout is not a terminal, check if -y was provided
|
||||
if [ $AUTO_YES -ne 1 ]; then
|
||||
echo "Script detected it's running in a non-interactive environment without -y flag."
|
||||
echo "Attempting to find a controlling terminal for interactive prompts..."
|
||||
# Try to use a controlling terminal for user input
|
||||
if [ -t 1 ]; then
|
||||
# Stdout is a terminal, use it
|
||||
exec < /dev/tty 2>/dev/null || INTERACTIVE=0
|
||||
else
|
||||
# Try to find controlling terminal
|
||||
exec < /dev/tty 2>/dev/null || INTERACTIVE=0
|
||||
fi
|
||||
|
||||
if [ $INTERACTIVE -eq 0 ]; then
|
||||
echo "ERROR: No controlling terminal available for interactive prompts."
|
||||
echo "For interactive installation (RECOMMENDED):"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh"
|
||||
echo " sudo bash nupst-install.sh"
|
||||
echo ""
|
||||
echo "For non-interactive installation with automatic dependency installation:"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
|
||||
exit 1
|
||||
else
|
||||
echo "Interactive terminal found, continuing with prompts..."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper function to detect OS type
|
||||
detect_os() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
elif type lsb_release >/dev/null 2>&1; then
|
||||
OS=$(lsb_release -si | tr '[:upper:]' '[:lower:]')
|
||||
elif [ -f /etc/lsb-release ]; then
|
||||
. /etc/lsb-release
|
||||
OS=$DISTRIB_ID
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
OS="debian"
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
if grep -q "CentOS" /etc/redhat-release; then
|
||||
OS="centos"
|
||||
elif grep -q "Fedora" /etc/redhat-release; then
|
||||
OS="fedora"
|
||||
else
|
||||
OS="rhel"
|
||||
fi
|
||||
else
|
||||
OS=$(uname -s)
|
||||
fi
|
||||
echo $OS
|
||||
}
|
||||
|
||||
# Helper function to install git
|
||||
install_git() {
|
||||
OS=$(detect_os)
|
||||
echo "Detected OS: $OS"
|
||||
|
||||
case "$OS" in
|
||||
ubuntu|debian|pop|mint|elementary|kali|zorin)
|
||||
echo "Installing git using apt..."
|
||||
apt-get update && apt-get install -y git
|
||||
# Map OS
|
||||
case "$os" in
|
||||
Linux)
|
||||
os_name="linux"
|
||||
;;
|
||||
fedora|rhel|centos|almalinux|rocky)
|
||||
echo "Installing git using dnf/yum..."
|
||||
if command -v dnf &> /dev/null; then
|
||||
dnf install -y git
|
||||
else
|
||||
yum install -y git
|
||||
fi
|
||||
Darwin)
|
||||
os_name="macos"
|
||||
;;
|
||||
arch|manjaro|endeavouros|garuda)
|
||||
echo "Installing git using pacman..."
|
||||
pacman -Sy --noconfirm git
|
||||
;;
|
||||
opensuse*|suse|sles)
|
||||
echo "Installing git using zypper..."
|
||||
zypper install -y git
|
||||
;;
|
||||
alpine)
|
||||
echo "Installing git using apk..."
|
||||
apk add git
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
os_name="windows"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported OS: $OS"
|
||||
echo "Please install git manually and run the installer again."
|
||||
echo "Error: Unsupported operating system: $os"
|
||||
echo "Supported: Linux, macOS, Windows"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if git was installed successfully
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "Failed to install git. Please install git manually and run the installer again."
|
||||
exit 1
|
||||
|
||||
# Map architecture
|
||||
case "$arch" in
|
||||
x86_64|amd64)
|
||||
arch_name="x64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
arch_name="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
echo "Supported: x86_64/amd64 (x64), aarch64/arm64 (arm64)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Construct binary name
|
||||
if [ "$os_name" = "windows" ]; then
|
||||
echo "nupst-${os_name}-${arch_name}.exe"
|
||||
else
|
||||
echo "nupst-${os_name}-${arch_name}"
|
||||
fi
|
||||
|
||||
echo "Git installed successfully."
|
||||
}
|
||||
|
||||
# Define installation directory
|
||||
INSTALL_DIR="/opt/nupst"
|
||||
REPO_URL="https://code.foss.global/serve.zone/nupst.git"
|
||||
# Get latest release version from Gitea API
|
||||
get_latest_version() {
|
||||
echo "Fetching latest release version from Gitea..." >&2
|
||||
|
||||
# Check if git is installed - needed for both piped and direct execution
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "Git is required but not installed."
|
||||
|
||||
if [ $AUTO_YES -eq 1 ]; then
|
||||
echo "Auto-installing git (-y flag provided)..."
|
||||
install_git
|
||||
elif [ $INTERACTIVE -eq 1 ]; then
|
||||
# If interactive and no -y flag, ask the user
|
||||
echo "Would you like to install git now? (y/N): "
|
||||
read -r install_git_prompt
|
||||
|
||||
if [[ "$install_git_prompt" =~ ^[Yy]$ ]]; then
|
||||
install_git
|
||||
else
|
||||
echo "Git installation skipped. Please install git manually and run the installer again."
|
||||
echo "Alternatively, you can run the installer with -y flag to automatically install git:"
|
||||
echo " sudo bash install.sh -y"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Non-interactive mode without -y flag
|
||||
echo "Error: Git is required but not installed."
|
||||
echo "In non-interactive mode, use -y flag to auto-install dependencies:"
|
||||
echo " curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y"
|
||||
local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest"
|
||||
local response=$(curl -sSL "$api_url" 2>/dev/null)
|
||||
|
||||
if [ $? -ne 0 ] || [ -z "$response" ]; then
|
||||
echo "Error: Failed to fetch latest release information from Gitea API" >&2
|
||||
echo "URL: $api_url" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $PIPED -eq 1 ]; then
|
||||
echo "Installing NUPST from remote repository..."
|
||||
|
||||
# Check if installation directory exists
|
||||
if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then
|
||||
echo "Existing installation found at $INSTALL_DIR. Updating..."
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# Try to update the repository
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to update repository. Reinstalling..."
|
||||
cd /
|
||||
rm -rf "$INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
||||
else
|
||||
echo "Repository updated successfully."
|
||||
fi
|
||||
else
|
||||
# Fresh installation
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Removing previous installation at $INSTALL_DIR..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# Create installation directory
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Clone the repository
|
||||
echo "Cloning NUPST repository to $INSTALL_DIR..."
|
||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to clone/update repository. Please check your internet connection."
|
||||
# Extract tag_name from JSON response
|
||||
local version=$(echo "$response" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Error: Could not determine latest version from API response" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set script directory to the cloned repo
|
||||
SCRIPT_DIR="$INSTALL_DIR"
|
||||
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
# Main installation process
|
||||
echo "================================================"
|
||||
echo " NUPST Installation Script (v5.0+)"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Detect platform
|
||||
BINARY_NAME=$(detect_platform)
|
||||
echo "Detected platform: $BINARY_NAME"
|
||||
echo ""
|
||||
|
||||
# Determine version to install
|
||||
if [ -n "$SPECIFIED_VERSION" ]; then
|
||||
VERSION="$SPECIFIED_VERSION"
|
||||
echo "Installing specified version: $VERSION"
|
||||
else
|
||||
# Running directly from within the repo or downloaded script
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# When running from a downloaded script in a different location
|
||||
# we need to clone the repository first
|
||||
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
|
||||
echo "Running installer from downloaded script outside repository."
|
||||
echo "Will clone the repository to $INSTALL_DIR..."
|
||||
|
||||
# Create installation directory if needed
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Removing previous installation at $INSTALL_DIR..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Clone the repository
|
||||
echo "Cloning NUPST repository to $INSTALL_DIR..."
|
||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to clone repository. Please check your internet connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update script directory to use the cloned repo
|
||||
SCRIPT_DIR="$INSTALL_DIR"
|
||||
VERSION=$(get_latest_version)
|
||||
echo "Installing latest version: $VERSION"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Construct download URL
|
||||
DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||
echo "Download URL: $DOWNLOAD_URL"
|
||||
echo ""
|
||||
|
||||
# Check if service is running and stop it
|
||||
SERVICE_WAS_RUNNING=0
|
||||
if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
|
||||
SERVICE_WAS_RUNNING=1
|
||||
if systemctl is-active --quiet nupst 2>/dev/null; then
|
||||
echo "Stopping NUPST service..."
|
||||
systemctl stop nupst
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run setup script
|
||||
echo "Running setup script..."
|
||||
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
|
||||
echo "ERROR: Setup script not found at $SCRIPT_DIR/setup.sh"
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "Script directory: $SCRIPT_DIR"
|
||||
ls -la "$SCRIPT_DIR"
|
||||
# Clean installation directory - ensure only binary exists
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Cleaning installation directory: $INSTALL_DIR"
|
||||
rm -rf "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# Create fresh installation directory
|
||||
echo "Creating installation directory: $INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Download binary
|
||||
echo "Downloading NUPST binary..."
|
||||
TEMP_FILE="$INSTALL_DIR/nupst.download"
|
||||
curl -sSL "$DOWNLOAD_URL" -o "$TEMP_FILE"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to download binary from $DOWNLOAD_URL"
|
||||
echo ""
|
||||
echo "Please check:"
|
||||
echo " 1. Your internet connection"
|
||||
echo " 2. The specified version exists: ${GITEA_BASE_URL}/${GITEA_REPO}/releases"
|
||||
echo " 3. The platform binary is available for this release"
|
||||
rm -f "$TEMP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "$SCRIPT_DIR/setup.sh"
|
||||
|
||||
# Install globally
|
||||
echo "Installing NUPST globally..."
|
||||
ln -sf "$SCRIPT_DIR/bin/nupst" /usr/local/bin/nupst
|
||||
|
||||
# Installation completed
|
||||
if [ $PIPED -eq 1 ]; then
|
||||
echo "NUPST has been installed globally at $INSTALL_DIR"
|
||||
else
|
||||
echo "NUPST has been installed globally."
|
||||
# Check if download was successful (file exists and not empty)
|
||||
if [ ! -s "$TEMP_FILE" ]; then
|
||||
echo "Error: Downloaded file is empty or does not exist"
|
||||
rm -f "$TEMP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "You can now run 'nupst' from anywhere."
|
||||
# Move to final location
|
||||
BINARY_PATH="$INSTALL_DIR/nupst"
|
||||
mv "$TEMP_FILE" "$BINARY_PATH"
|
||||
|
||||
# Make executable
|
||||
chmod +x "$BINARY_PATH"
|
||||
|
||||
echo "Binary installed successfully to: $BINARY_PATH"
|
||||
echo ""
|
||||
|
||||
# Check if /usr/local/bin is in PATH
|
||||
if [[ ":$PATH:" == *":/usr/local/bin:"* ]]; then
|
||||
BIN_DIR="/usr/local/bin"
|
||||
else
|
||||
BIN_DIR="/usr/bin"
|
||||
fi
|
||||
|
||||
# Create symlink for global access
|
||||
ln -sf "$BINARY_PATH" "$BIN_DIR/nupst"
|
||||
echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH"
|
||||
|
||||
echo ""
|
||||
|
||||
# Restart service if it was running before update
|
||||
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
|
||||
echo "Restarting NUPST service..."
|
||||
systemctl start nupst
|
||||
echo "Service restarted successfully."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "================================================"
|
||||
echo " NUPST Installation Complete!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Installation details:"
|
||||
echo " Binary location: $BINARY_PATH"
|
||||
echo " Symlink location: $BIN_DIR/nupst"
|
||||
echo " Version: $VERSION"
|
||||
echo ""
|
||||
|
||||
# Check if configuration exists
|
||||
if [ -f "/etc/nupst/config.json" ]; then
|
||||
echo "Configuration: /etc/nupst/config.json (preserved)"
|
||||
echo ""
|
||||
echo "Your existing configuration has been preserved."
|
||||
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
|
||||
echo "The service has been restarted with your current settings."
|
||||
else
|
||||
echo "Start the service with: sudo nupst service start"
|
||||
fi
|
||||
else
|
||||
echo "Get started:"
|
||||
echo " nupst --version"
|
||||
echo " nupst help"
|
||||
echo " nupst ups add # Add a UPS device"
|
||||
echo " nupst service enable # Enable systemd service"
|
||||
fi
|
||||
echo ""
|
||||
echo "To get started, try:"
|
||||
echo " nupst help"
|
||||
echo " nupst setup # To configure your UPS connection"
|
||||
|
44
mod.ts
Normal file
44
mod.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env -S deno run --allow-all
|
||||
|
||||
/**
|
||||
* NUPST - UPS Shutdown Tool
|
||||
*
|
||||
* A command-line tool for monitoring SNMP-enabled UPS devices and
|
||||
* initiating system shutdown when power conditions are critical.
|
||||
*
|
||||
* Required Permissions:
|
||||
* - --allow-net: SNMP communication with UPS devices
|
||||
* - --allow-read: Read configuration files (/etc/nupst/config.json)
|
||||
* - --allow-write: Write configuration files
|
||||
* - --allow-run: Execute system commands (systemctl, shutdown, git, bash)
|
||||
* - --allow-sys: Access system information (hostname, OS details)
|
||||
* - --allow-env: Read environment variables
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { NupstCli } from './ts/cli.ts';
|
||||
|
||||
/**
|
||||
* Main entry point for the NUPST application
|
||||
* Parses command-line arguments and executes the requested command
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
const cli = new NupstCli();
|
||||
|
||||
// Deno.args is already 0-indexed (unlike Node's process.argv which starts at index 2)
|
||||
// We need to prepend placeholder args to match the existing CLI parser expectations
|
||||
const args = ['deno', 'mod.ts', ...Deno.args];
|
||||
|
||||
await cli.parseAndExecute(args);
|
||||
}
|
||||
|
||||
// Execute main and handle errors
|
||||
if (import.meta.main) {
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
{}
|
61
package.json
61
package.json
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "3.1.1",
|
||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"nupst": "bin/nupst"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsbuild tsfolders --allowimplicitany",
|
||||
"start": "bin/nupst",
|
||||
"setup": "bash setup.sh",
|
||||
"test": "tstest test/",
|
||||
"install-global": "sudo bash install.sh",
|
||||
"uninstall": "sudo bash uninstall.sh"
|
||||
},
|
||||
"keywords": [
|
||||
"ups",
|
||||
"snmp",
|
||||
"shutdown",
|
||||
"node",
|
||||
"cli"
|
||||
],
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"net-snmp": "3.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/qenv": "^6.1.0",
|
||||
"@push.rocks/tapbundle": "^5.6.0",
|
||||
"@types/node": "^20.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"mongodb-memory-server",
|
||||
"puppeteer"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
10204
pnpm-lock.yaml
generated
10204
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
613
readme.plan.md
Normal file
613
readme.plan.md
Normal file
@@ -0,0 +1,613 @@
|
||||
# NUPST Migration Plan: Node.js → Deno v4.0.0
|
||||
|
||||
**Migration Goal**: Convert NUPST from Node.js to Deno with single-executable distribution
|
||||
**Version**: 3.1.2 → 4.0.0 (breaking changes) **Platforms**: Linux x64/ARM64, macOS x64/ARM64,
|
||||
Windows x64
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Planning & Preparation
|
||||
|
||||
- [x] Research Deno compilation targets and npm: specifier support
|
||||
- [x] Analyze current codebase structure and dependencies
|
||||
- [x] Define CLI command structure simplification
|
||||
- [x] Create detailed migration task list
|
||||
- [ ] Create feature branch: `migration/deno-v4`
|
||||
- [ ] Backup current working state with git tag: `v3.1.2-pre-deno-migration`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Dependency Migration (4-6 hours)
|
||||
|
||||
### 1.1 Analyze Current Dependencies
|
||||
|
||||
- [ ] List all production dependencies from `package.json`
|
||||
- Current: `net-snmp@3.20.0`
|
||||
- [ ] List all dev dependencies to be removed
|
||||
- `@git.zone/tsbuild`, `@git.zone/tsrun`, `@git.zone/tstest`, `@push.rocks/qenv`,
|
||||
`@push.rocks/tapbundle`, `@types/node`
|
||||
- [ ] Identify Node.js built-in module usage
|
||||
- `child_process` (execSync)
|
||||
- `https` (for version checking)
|
||||
- `fs` (readFileSync, writeFileSync, existsSync, mkdirSync)
|
||||
- `path` (join, dirname, resolve)
|
||||
|
||||
### 1.2 Create Deno Configuration
|
||||
|
||||
- [ ] Create `deno.json` with project configuration
|
||||
```json
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "4.0.0",
|
||||
"exports": "./mod.ts",
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-all mod.ts",
|
||||
"compile": "deno task compile:all",
|
||||
"compile:all": "bash scripts/compile-all.sh",
|
||||
"test": "deno test --allow-all tests/",
|
||||
"check": "deno check mod.ts"
|
||||
},
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": ["recommended"]
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"useTabs": false,
|
||||
"lineWidth": 100,
|
||||
"indentWidth": 2,
|
||||
"semiColons": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.window"],
|
||||
"strict": true
|
||||
},
|
||||
"imports": {
|
||||
"@std/cli": "jsr:@std/cli@^1.0.0",
|
||||
"@std/fmt": "jsr:@std/fmt@^1.0.0",
|
||||
"@std/path": "jsr:@std/path@^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Update Import Statements
|
||||
|
||||
- [ ] `ts/snmp/manager.ts`: Change `import * as snmp from 'net-snmp'` to
|
||||
`import * as snmp from "npm:net-snmp@3.20.0"`
|
||||
- [ ] `ts/cli.ts`: Change `import { execSync } from 'child_process'` to
|
||||
`import { execSync } from "node:child_process"`
|
||||
- [ ] `ts/nupst.ts`: Change `import * as https from 'https'` to
|
||||
`import * as https from "node:https"`
|
||||
- [ ] Search for all `fs` imports and update to `node:fs`
|
||||
- [ ] Search for all `path` imports and update to `node:path`
|
||||
- [ ] Update all relative imports to use `.ts` extension instead of `.js`
|
||||
- Example: `'./nupst.js'` → `'./nupst.ts'`
|
||||
|
||||
### 1.4 Test npm: Specifier Compatibility
|
||||
|
||||
- [ ] Create test file: `tests/snmp_compatibility_test.ts`
|
||||
- [ ] Test SNMP v1 connection with npm:net-snmp
|
||||
- [ ] Test SNMP v2c connection with npm:net-snmp
|
||||
- [ ] Test SNMP v3 connection with npm:net-snmp
|
||||
- [ ] Verify native addon loading works in compiled binary
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Code Structure Refactoring (3-4 hours)
|
||||
|
||||
### 2.1 Create Main Entry Point
|
||||
|
||||
- [ ] Create `mod.ts` as main Deno entry point:
|
||||
```typescript
|
||||
#!/usr/bin/env -S deno run --allow-all
|
||||
|
||||
/**
|
||||
* NUPST - UPS Shutdown Tool for Deno
|
||||
*
|
||||
* Required Permissions:
|
||||
* --allow-net: SNMP communication with UPS devices
|
||||
* --allow-read: Configuration file access (/etc/nupst/config.json)
|
||||
* --allow-write: Configuration file updates
|
||||
* --allow-run: System commands (systemctl, shutdown)
|
||||
* --allow-sys: System information (hostname, OS info)
|
||||
* --allow-env: Environment variables
|
||||
*/
|
||||
|
||||
import { NupstCli } from './ts/cli.ts';
|
||||
|
||||
const cli = new NupstCli();
|
||||
await cli.parseAndExecute(Deno.args);
|
||||
```
|
||||
|
||||
### 2.2 Update All Import Extensions
|
||||
|
||||
Files to update (change .js → .ts in imports):
|
||||
|
||||
- [ ] `ts/index.ts`
|
||||
- [ ] `ts/cli.ts` (imports from ./nupst.js, ./logger.js)
|
||||
- [ ] `ts/nupst.ts` (imports from ./snmp/manager.js, ./daemon.js, etc.)
|
||||
- [ ] `ts/daemon.ts` (imports from ./snmp/manager.js, ./logger.js, ./helpers/)
|
||||
- [ ] `ts/systemd.ts` (imports from ./daemon.js, ./logger.js)
|
||||
- [ ] `ts/cli/service-handler.ts`
|
||||
- [ ] `ts/cli/group-handler.ts`
|
||||
- [ ] `ts/cli/ups-handler.ts`
|
||||
- [ ] `ts/snmp/index.ts`
|
||||
- [ ] `ts/snmp/manager.ts` (imports from ./types.js, ./oid-sets.js)
|
||||
- [ ] `ts/snmp/oid-sets.ts` (imports from ./types.js)
|
||||
- [ ] `ts/helpers/index.ts`
|
||||
- [ ] `ts/logger.ts`
|
||||
|
||||
### 2.3 Update process.argv References
|
||||
|
||||
- [ ] `ts/cli.ts`: Replace `process.argv` with `Deno.args` (adjust indexing: process.argv[2] →
|
||||
Deno.args[0])
|
||||
- [ ] Update parseAndExecute method to work with Deno.args (0-indexed vs 2-indexed)
|
||||
|
||||
### 2.4 Update File System Operations
|
||||
|
||||
- [ ] Search for `fs.readFileSync()` → Consider using `Deno.readTextFile()` or keep node:fs
|
||||
- [ ] Search for `fs.writeFileSync()` → Consider using `Deno.writeTextFile()` or keep node:fs
|
||||
- [ ] Search for `fs.existsSync()` → Keep node:fs or use Deno.stat
|
||||
- [ ] Search for `fs.mkdirSync()` → Keep node:fs or use Deno.mkdir
|
||||
- [ ] Decision: Keep node:fs for consistency or migrate to Deno APIs?
|
||||
|
||||
### 2.5 Update Path Operations
|
||||
|
||||
- [ ] Verify all `path.join()`, `path.resolve()`, `path.dirname()` work with node:path
|
||||
- [ ] Consider using `@std/path` from JSR for better Deno integration
|
||||
|
||||
### 2.6 Handle __dirname and __filename
|
||||
|
||||
- [ ] Find all `__dirname` usage
|
||||
- [ ] Replace with `import.meta.dirname` (Deno) or `dirname(fromFileUrl(import.meta.url))`
|
||||
- [ ] Find all `__filename` usage
|
||||
- [ ] Replace with `import.meta.filename` or `fromFileUrl(import.meta.url)`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: CLI Command Simplification (3-4 hours)
|
||||
|
||||
### 3.1 Design New Command Structure
|
||||
|
||||
Current → New mapping:
|
||||
|
||||
```
|
||||
OLD NEW
|
||||
=== ===
|
||||
nupst enable → nupst service enable
|
||||
nupst disable → nupst service disable
|
||||
nupst daemon-start → nupst service start-daemon
|
||||
nupst logs → nupst service logs
|
||||
nupst stop → nupst service stop
|
||||
nupst start → nupst service start
|
||||
nupst status → nupst service status
|
||||
|
||||
nupst add → nupst ups add
|
||||
nupst edit [id] → nupst ups edit [id]
|
||||
nupst delete <id> → nupst ups remove <id>
|
||||
nupst list → nupst ups list
|
||||
nupst setup → nupst ups edit (removed alias)
|
||||
nupst test → nupst ups test
|
||||
|
||||
nupst group list → nupst group list
|
||||
nupst group add → nupst group add
|
||||
nupst group edit <id> → nupst group edit <id>
|
||||
nupst group delete <id> → nupst group remove <id>
|
||||
|
||||
nupst config → nupst config show
|
||||
nupst update → nupst update
|
||||
nupst uninstall → nupst uninstall
|
||||
nupst help → nupst help / nupst --help
|
||||
(new) → nupst --version
|
||||
```
|
||||
|
||||
### 3.2 Update CLI Parser (ts/cli.ts)
|
||||
|
||||
- [ ] Refactor `parseAndExecute()` to handle new command structure
|
||||
- [ ] Add `service` subcommand handler
|
||||
- [ ] Add `ups` subcommand handler
|
||||
- [ ] Keep `group` subcommand handler (already exists, just update delete→remove)
|
||||
- [ ] Add `config` subcommand handler with `show` default
|
||||
- [ ] Add `--version` flag handler
|
||||
- [ ] Update `help` command to show new structure
|
||||
- [ ] Add command aliases: `rm` → `remove`, `ls` → `list`
|
||||
- [ ] Add `--json` flag for machine-readable output (future enhancement)
|
||||
|
||||
### 3.3 Update Command Handlers
|
||||
|
||||
- [ ] `ts/cli/service-handler.ts`: Update method names if needed
|
||||
- [ ] `ts/cli/ups-handler.ts`: Rename `delete()` → `remove()`, remove `setup` method
|
||||
- [ ] `ts/cli/group-handler.ts`: Rename `delete()` → `remove()`
|
||||
|
||||
### 3.4 Improve Help Messages
|
||||
|
||||
- [ ] Update `showHelp()` in ts/cli.ts with new command structure
|
||||
- [ ] Update `showGroupHelp()` in ts/cli.ts
|
||||
- [ ] Add `showServiceHelp()` method
|
||||
- [ ] Add `showUpsHelp()` method
|
||||
- [ ] Add `showConfigHelp()` method
|
||||
- [ ] Include usage examples in help text
|
||||
|
||||
### 3.5 Add Version Command
|
||||
|
||||
- [ ] Read version from deno.json
|
||||
- [ ] Create `--version` handler in CLI
|
||||
- [ ] Display version with build info
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Compilation & Distribution (2-3 hours)
|
||||
|
||||
### 4.1 Create Compilation Script
|
||||
|
||||
- [ ] Create directory: `scripts/`
|
||||
- [ ] Create `scripts/compile-all.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VERSION=$(cat deno.json | jq -r '.version')
|
||||
BINARY_DIR="dist/binaries"
|
||||
|
||||
echo "Compiling NUPST v${VERSION} for all platforms..."
|
||||
mkdir -p "$BINARY_DIR"
|
||||
|
||||
# Linux x86_64
|
||||
echo "→ Linux x86_64..."
|
||||
deno compile --allow-all --output "$BINARY_DIR/nupst-linux-x64" \
|
||||
--target x86_64-unknown-linux-gnu mod.ts
|
||||
|
||||
# Linux ARM64
|
||||
echo "→ Linux ARM64..."
|
||||
deno compile --allow-all --output "$BINARY_DIR/nupst-linux-arm64" \
|
||||
--target aarch64-unknown-linux-gnu mod.ts
|
||||
|
||||
# macOS x86_64
|
||||
echo "→ macOS x86_64..."
|
||||
deno compile --allow-all --output "$BINARY_DIR/nupst-macos-x64" \
|
||||
--target x86_64-apple-darwin mod.ts
|
||||
|
||||
# macOS ARM64
|
||||
echo "→ macOS ARM64..."
|
||||
deno compile --allow-all --output "$BINARY_DIR/nupst-macos-arm64" \
|
||||
--target aarch64-apple-darwin mod.ts
|
||||
|
||||
# Windows x86_64
|
||||
echo "→ Windows x86_64..."
|
||||
deno compile --allow-all --output "$BINARY_DIR/nupst-windows-x64.exe" \
|
||||
--target x86_64-pc-windows-msvc mod.ts
|
||||
|
||||
echo ""
|
||||
echo "✓ Compilation complete!"
|
||||
ls -lh "$BINARY_DIR/"
|
||||
```
|
||||
- [ ] Make script executable: `chmod +x scripts/compile-all.sh`
|
||||
|
||||
### 4.2 Test Local Compilation
|
||||
|
||||
- [ ] Run `deno task compile` to compile for all platforms
|
||||
- [ ] Verify all 5 binaries are created
|
||||
- [ ] Check binary sizes (should be reasonable, < 100MB each)
|
||||
- [ ] Test local binary on current platform: `./dist/binaries/nupst-linux-x64 --version`
|
||||
|
||||
### 4.3 Update Installation Scripts
|
||||
|
||||
- [ ] Update `install.sh`:
|
||||
- Remove Node.js download logic (lines dealing with vendor/node-*)
|
||||
- Add detection for binary download from GitHub releases
|
||||
- Simplify to download appropriate binary based on OS/arch
|
||||
- Place binary in `/opt/nupst/bin/nupst`
|
||||
- Create symlink: `/usr/local/bin/nupst → /opt/nupst/bin/nupst`
|
||||
- Update to v4.0.0 in script
|
||||
- [ ] Simplify or remove `setup.sh` (no longer needed without Node.js)
|
||||
- [ ] Update `bin/nupst` launcher:
|
||||
- Option A: Keep as simple wrapper
|
||||
- Option B: Remove and symlink directly to binary
|
||||
- [ ] Update `uninstall.sh`:
|
||||
- Remove vendor directory cleanup
|
||||
- Update paths to new binary location
|
||||
|
||||
### 4.4 Update Systemd Service
|
||||
|
||||
- [ ] Update systemd service file path in `ts/systemd.ts`
|
||||
- [ ] Verify ExecStart points to correct binary location: `/opt/nupst/bin/nupst daemon-start`
|
||||
- [ ] Remove Node.js environment variables if any
|
||||
- [ ] Test service installation and startup
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Testing & Validation (4-6 hours)
|
||||
|
||||
### 5.1 Create Deno Test Suite
|
||||
|
||||
- [ ] Create `tests/` directory (or migrate from existing `test/`)
|
||||
- [ ] Create `tests/snmp_test.ts`: Test SNMP manager functionality
|
||||
- [ ] Create `tests/config_test.ts`: Test configuration loading/saving
|
||||
- [ ] Create `tests/cli_test.ts`: Test CLI parsing and command routing
|
||||
- [ ] Create `tests/daemon_test.ts`: Test daemon logic
|
||||
- [ ] Remove dependency on @git.zone/tstest and @push.rocks/tapbundle
|
||||
- [ ] Use Deno's built-in test runner (`Deno.test()`)
|
||||
|
||||
### 5.2 Unit Tests
|
||||
|
||||
- [ ] Test SNMP connection with mock responses
|
||||
- [ ] Test configuration validation
|
||||
- [ ] Test UPS status parsing for different models
|
||||
- [ ] Test group logic (redundant/non-redundant modes)
|
||||
- [ ] Test threshold checking
|
||||
- [ ] Test version comparison logic
|
||||
|
||||
### 5.3 Integration Tests
|
||||
|
||||
- [ ] Test CLI command parsing for all commands
|
||||
- [ ] Test config file creation and updates
|
||||
- [ ] Test UPS add/edit/remove operations
|
||||
- [ ] Test group add/edit/remove operations
|
||||
- [ ] Mock systemd operations for testing
|
||||
|
||||
### 5.4 Binary Testing
|
||||
|
||||
- [ ] Test compiled binary on Linux x64
|
||||
- [ ] Test compiled binary on Linux ARM64 (if available)
|
||||
- [ ] Test compiled binary on macOS x64 (if available)
|
||||
- [ ] Test compiled binary on macOS ARM64 (if available)
|
||||
- [ ] Test compiled binary on Windows x64 (if available)
|
||||
- [ ] Verify SNMP functionality works in compiled binary
|
||||
- [ ] Verify config file operations work in compiled binary
|
||||
- [ ] Test systemd integration with compiled binary
|
||||
|
||||
### 5.5 Performance Testing
|
||||
|
||||
- [ ] Measure binary size for each platform
|
||||
- [ ] Measure startup time: `time ./nupst-linux-x64 --version`
|
||||
- [ ] Measure memory footprint during daemon operation
|
||||
- [ ] Compare with Node.js version performance
|
||||
- [ ] Document performance metrics
|
||||
|
||||
### 5.6 Upgrade Path Testing
|
||||
|
||||
- [ ] Create test with v3.x config
|
||||
- [ ] Verify v4.x can read existing config
|
||||
- [ ] Test migration from old commands to new commands
|
||||
- [ ] Verify systemd service upgrade path
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Distribution Strategy (2-3 hours)
|
||||
|
||||
### 6.1 GitHub Actions Workflow
|
||||
|
||||
- [ ] Create `.github/workflows/release.yml`:
|
||||
```yaml
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: Compile binaries
|
||||
run: deno task compile
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd dist/binaries
|
||||
sha256sum * > SHA256SUMS
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: dist/binaries/*
|
||||
generate_release_notes: true
|
||||
```
|
||||
|
||||
### 6.2 Update package.json for npm
|
||||
|
||||
- [ ] Update version to 4.0.0
|
||||
- [ ] Update description to mention Deno
|
||||
- [ ] Add postinstall script to symlink appropriate binary:
|
||||
```json
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "4.0.0",
|
||||
"description": "UPS Shutdown Tool - Deno-based single executable",
|
||||
"bin": {
|
||||
"nupst": "bin/nupst-npm-wrapper.js"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"postinstall": "node bin/setup-npm-binary.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/binaries/*",
|
||||
"bin/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
- [ ] Create `bin/setup-npm-binary.js` to symlink correct binary
|
||||
- [ ] Create `bin/nupst-npm-wrapper.js` as entry point
|
||||
|
||||
### 6.3 Verify Distribution Methods
|
||||
|
||||
- [ ] Test GitHub release download and installation
|
||||
- [ ] Test npm install from tarball
|
||||
- [ ] Test direct install.sh script
|
||||
- [ ] Verify all methods create working installation
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Documentation Updates (2-3 hours)
|
||||
|
||||
### 7.1 Update README.md
|
||||
|
||||
- [ ] Remove Node.js requirements section
|
||||
- [ ] Update features list (mention Deno, single executable)
|
||||
- [ ] Update installation methods:
|
||||
- Method 1: Quick install script (updated)
|
||||
- Method 2: GitHub releases (new)
|
||||
- Method 3: npm (updated with notes)
|
||||
- [ ] Update usage section with new command structure
|
||||
- [ ] Add command mapping table (v3 → v4)
|
||||
- [ ] Update platform support matrix (note: no Windows ARM)
|
||||
- [ ] Update "System Changes" section (no vendor directory)
|
||||
- [ ] Update security section (remove Node.js mentions)
|
||||
- [ ] Update uninstallation instructions
|
||||
|
||||
### 7.2 Create MIGRATION.md
|
||||
|
||||
- [ ] Create detailed migration guide from v3.x to v4.x
|
||||
- [ ] List all breaking changes:
|
||||
1. CLI command structure reorganization
|
||||
2. No Node.js requirement
|
||||
3. Windows ARM not supported
|
||||
4. Installation path changes
|
||||
- [ ] Provide command mapping table
|
||||
- [ ] Explain config compatibility
|
||||
- [ ] Document upgrade procedure
|
||||
- [ ] Add rollback instructions
|
||||
|
||||
### 7.3 Update CHANGELOG.md
|
||||
|
||||
- [ ] Add v4.0.0 section with all breaking changes
|
||||
- [ ] List new features (Deno, single executable)
|
||||
- [ ] List improvements (startup time, binary size)
|
||||
- [ ] List removed features (Windows ARM, setup command alias)
|
||||
- [ ] Migration guide reference
|
||||
|
||||
### 7.4 Update Help Text
|
||||
|
||||
- [ ] Ensure all help commands show new structure
|
||||
- [ ] Add examples for common operations
|
||||
- [ ] Include migration notes in help output
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Cleanup & Finalization (1 hour)
|
||||
|
||||
### 8.1 Remove Obsolete Files
|
||||
|
||||
- [ ] Delete `vendor/` directory (Node.js binaries)
|
||||
- [ ] Delete `dist/` directory (old compiled JS)
|
||||
- [ ] Delete `dist_ts/` directory (old compiled TS)
|
||||
- [ ] Delete `node_modules/` directory
|
||||
- [ ] Remove or update `tsconfig.json` (decide if needed for npm compatibility)
|
||||
- [ ] Remove `setup.sh` if no longer needed
|
||||
- [ ] Remove old test files in `test/` if migrated to `tests/`
|
||||
- [ ] Delete `pnpm-lock.yaml`
|
||||
|
||||
### 8.2 Update Git Configuration
|
||||
|
||||
- [ ] Update `.gitignore`:
|
||||
```
|
||||
# Deno
|
||||
.deno/
|
||||
deno.lock
|
||||
|
||||
# Compiled binaries
|
||||
dist/binaries/
|
||||
|
||||
# Old Node.js artifacts (to be removed)
|
||||
node_modules/
|
||||
vendor/
|
||||
dist/
|
||||
dist_ts/
|
||||
pnpm-lock.yaml
|
||||
```
|
||||
- [ ] Add `deno.lock` to version control
|
||||
- [ ] Create `.denoignore` if needed
|
||||
|
||||
### 8.3 Final Validation
|
||||
|
||||
- [ ] Run `deno check mod.ts` - verify no type errors
|
||||
- [ ] Run `deno lint` - verify code quality
|
||||
- [ ] Run `deno fmt --check` - verify formatting
|
||||
- [ ] Run `deno task test` - verify all tests pass
|
||||
- [ ] Run `deno task compile` - verify all binaries compile
|
||||
- [ ] Test each binary manually
|
||||
|
||||
### 8.4 Prepare for Release
|
||||
|
||||
- [ ] Create git tag: `v4.0.0`
|
||||
- [ ] Push to main branch
|
||||
- [ ] Push tags to trigger release workflow
|
||||
- [ ] Verify GitHub Actions workflow succeeds
|
||||
- [ ] Verify binaries are attached to release
|
||||
- [ ] Test installation from GitHub release
|
||||
- [ ] Publish to npm: `npm publish`
|
||||
- [ ] Test npm installation
|
||||
|
||||
---
|
||||
|
||||
## Rollback Strategy
|
||||
|
||||
If critical issues are discovered:
|
||||
|
||||
- [ ] Keep `v3.1.2` tag available for rollback
|
||||
- [ ] Create `v3-stable` branch for continued v3 maintenance
|
||||
- [ ] Update install.sh to offer v3/v4 choice
|
||||
- [ ] Document known issues in GitHub Issues
|
||||
- [ ] Provide downgrade instructions in docs
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria Checklist
|
||||
|
||||
- [ ] ✅ All 5 platform binaries compile successfully
|
||||
- [ ] ✅ Binary sizes are reasonable (< 100MB per platform)
|
||||
- [ ] ✅ Startup time < 2 seconds
|
||||
- [ ] ✅ SNMP v1/v2c/v3 functionality verified on real UPS device
|
||||
- [ ] ✅ All CLI commands work with new structure
|
||||
- [ ] ✅ Config file compatibility maintained
|
||||
- [ ] ✅ Systemd integration works on Linux
|
||||
- [ ] ✅ Installation scripts work on fresh systems
|
||||
- [ ] ✅ npm package still installable and functional
|
||||
- [ ] ✅ All tests pass
|
||||
- [ ] ✅ Documentation is complete and accurate
|
||||
- [ ] ✅ GitHub release created with binaries
|
||||
- [ ] ✅ Migration guide tested by following it step-by-step
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
- **Phase 0**: 1 hour ✓ (in progress)
|
||||
- **Phase 1**: 4-6 hours
|
||||
- **Phase 2**: 3-4 hours
|
||||
- **Phase 3**: 3-4 hours
|
||||
- **Phase 4**: 2-3 hours
|
||||
- **Phase 5**: 4-6 hours
|
||||
- **Phase 6**: 2-3 hours
|
||||
- **Phase 7**: 2-3 hours
|
||||
- **Phase 8**: 1 hour
|
||||
|
||||
**Total Estimate**: 22-31 hours
|
||||
|
||||
---
|
||||
|
||||
## Notes & Decisions
|
||||
|
||||
### Key Decisions Made:
|
||||
|
||||
1. ✅ Use npm:net-snmp (no pure Deno SNMP library available)
|
||||
2. ✅ Major version bump to 4.0.0 (breaking changes)
|
||||
3. ✅ CLI reorganization with subcommands
|
||||
4. ✅ Keep npm publishing alongside binary distribution
|
||||
5. ✅ 5 platform targets (Windows ARM not supported by Deno yet)
|
||||
|
||||
### Open Questions:
|
||||
|
||||
- [ ] Should we keep tsconfig.json for npm package compatibility?
|
||||
- [ ] Should we fully migrate to Deno APIs (Deno.readFile) or keep node:fs?
|
||||
- [ ] Should we remove the `bin/nupst` wrapper or keep it?
|
||||
- [ ] Should setup.sh be completely removed or kept for dependencies?
|
||||
|
||||
### Risk Areas:
|
||||
|
||||
- ⚠️ SNMP native addon compatibility in compiled binaries (HIGH PRIORITY TO TEST)
|
||||
- ⚠️ Systemd integration with new binary structure
|
||||
- ⚠️ Config migration from v3 to v4
|
||||
- ⚠️ npm package installation with embedded binaries
|
66
scripts/compile-all.sh
Executable file
66
scripts/compile-all.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Get version from deno.json
|
||||
VERSION=$(cat deno.json | grep -o '"version": *"[^"]*"' | cut -d'"' -f4)
|
||||
BINARY_DIR="dist/binaries"
|
||||
|
||||
echo "================================================"
|
||||
echo " NUPST Compilation Script"
|
||||
echo " Version: ${VERSION}"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Compiling for all supported platforms..."
|
||||
echo ""
|
||||
|
||||
# Clean up old binaries and create fresh directory
|
||||
rm -rf "$BINARY_DIR"
|
||||
mkdir -p "$BINARY_DIR"
|
||||
echo "→ Cleaned old binaries from $BINARY_DIR"
|
||||
echo ""
|
||||
|
||||
# Linux x86_64
|
||||
echo "→ Compiling for Linux x86_64..."
|
||||
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-x64" \
|
||||
--target x86_64-unknown-linux-gnu mod.ts
|
||||
echo " ✓ Linux x86_64 complete"
|
||||
echo ""
|
||||
|
||||
# Linux ARM64
|
||||
echo "→ Compiling for Linux ARM64..."
|
||||
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-arm64" \
|
||||
--target aarch64-unknown-linux-gnu mod.ts
|
||||
echo " ✓ Linux ARM64 complete"
|
||||
echo ""
|
||||
|
||||
# macOS x86_64
|
||||
echo "→ Compiling for macOS x86_64..."
|
||||
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-x64" \
|
||||
--target x86_64-apple-darwin mod.ts
|
||||
echo " ✓ macOS x86_64 complete"
|
||||
echo ""
|
||||
|
||||
# macOS ARM64
|
||||
echo "→ Compiling for macOS ARM64..."
|
||||
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-arm64" \
|
||||
--target aarch64-apple-darwin mod.ts
|
||||
echo " ✓ macOS ARM64 complete"
|
||||
echo ""
|
||||
|
||||
# Windows x86_64
|
||||
echo "→ Compiling for Windows x86_64..."
|
||||
deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-windows-x64.exe" \
|
||||
--target x86_64-pc-windows-msvc mod.ts
|
||||
echo " ✓ Windows x86_64 complete"
|
||||
echo ""
|
||||
|
||||
echo "================================================"
|
||||
echo " Compilation Summary"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
ls -lh "$BINARY_DIR/" | tail -n +2
|
||||
echo ""
|
||||
echo "✓ All binaries compiled successfully!"
|
||||
echo ""
|
||||
echo "Binary location: $BINARY_DIR/"
|
||||
echo ""
|
328
setup.sh
328
setup.sh
@@ -1,328 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NUPST Setup Script
|
||||
# Downloads the appropriate Node.js binary for the current platform
|
||||
# and installs production dependencies
|
||||
|
||||
# Parse command line arguments
|
||||
FORCE_UPDATE=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--force|-f)
|
||||
FORCE_UPDATE=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
# Unknown option
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Find the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
|
||||
# Create vendor directory if it doesn't exist
|
||||
mkdir -p "$SCRIPT_DIR/vendor"
|
||||
|
||||
# Get the latest LTS Node.js version
|
||||
echo "Determining latest LTS Node.js version..."
|
||||
NODE_VERSIONS_JSON=$(curl -s https://nodejs.org/dist/index.json)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Warning: Could not fetch latest Node.js versions. Using fallback version."
|
||||
NODE_VERSION="20.11.1" # Fallback to a recent LTS version
|
||||
else
|
||||
# Extract the latest LTS version (those marked with lts field)
|
||||
NODE_VERSION=$(echo "$NODE_VERSIONS_JSON" | grep -o '"version":"v[0-9.]*".*"lts":[^,]*' | grep -v '"lts":false' | grep -o 'v[0-9.]*' | head -1 | cut -c 2-)
|
||||
|
||||
if [ -z "$NODE_VERSION" ]; then
|
||||
echo "Warning: Could not determine latest LTS version. Using fallback version."
|
||||
NODE_VERSION="20.11.1" # Fallback to a recent LTS version
|
||||
else
|
||||
echo "Latest Node.js LTS version: $NODE_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
OS=$(uname -s)
|
||||
|
||||
# Map architecture and OS to Node.js download URL
|
||||
NODE_URL=""
|
||||
NODE_DIR=""
|
||||
case "$OS" in
|
||||
Linux)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz"
|
||||
NODE_DIR="node-linux-x64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-arm64.tar.gz"
|
||||
NODE_DIR="node-linux-arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH. Please install Node.js manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-x64.tar.gz"
|
||||
NODE_DIR="node-darwin-x64"
|
||||
;;
|
||||
arm64)
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-arm64.tar.gz"
|
||||
NODE_DIR="node-darwin-arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH. Please install Node.js manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported operating system: $OS. Please install Node.js manually."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if we already have the Node.js binary
|
||||
if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ] && [ $FORCE_UPDATE -eq 0 ]; then
|
||||
echo "Node.js binary already exists for $OS-$ARCH. Skipping download."
|
||||
echo "Use --force or -f to force update Node.js."
|
||||
else
|
||||
echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..."
|
||||
|
||||
# Download and extract Node.js
|
||||
TMP_FILE="$SCRIPT_DIR/vendor/node.tar.gz"
|
||||
curl -L "$NODE_URL" -o "$TMP_FILE"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error downloading Node.js. Please check your internet connection and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create target directory
|
||||
mkdir -p "$SCRIPT_DIR/vendor/$NODE_DIR"
|
||||
|
||||
# Extract Node.js
|
||||
tar -xzf "$TMP_FILE" -C "$SCRIPT_DIR/vendor"
|
||||
|
||||
# Move extracted files to the target directory
|
||||
NODE_EXTRACT_DIR=$(find "$SCRIPT_DIR/vendor" -maxdepth 1 -name "node-v*" -type d | head -n 1)
|
||||
if [ -d "$NODE_EXTRACT_DIR" ]; then
|
||||
cp -R "$NODE_EXTRACT_DIR"/* "$SCRIPT_DIR/vendor/$NODE_DIR/"
|
||||
rm -rf "$NODE_EXTRACT_DIR"
|
||||
else
|
||||
echo "Error extracting Node.js. Please try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm "$TMP_FILE"
|
||||
|
||||
echo "Node.js v$NODE_VERSION for $OS-$ARCH has been downloaded and extracted."
|
||||
fi
|
||||
|
||||
# Remove any existing dist_ts directory
|
||||
if [ -d "$SCRIPT_DIR/dist_ts" ]; then
|
||||
echo "Removing existing dist_ts directory..."
|
||||
rm -rf "$SCRIPT_DIR/dist_ts"
|
||||
fi
|
||||
|
||||
# Download dist_ts from npm registry
|
||||
echo "Downloading dist_ts from npm registry..."
|
||||
|
||||
# Create temp directory
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
|
||||
# Get version from package.json
|
||||
if [ -f "$SCRIPT_DIR/package.json" ]; then
|
||||
echo "Reading version from package.json..."
|
||||
# Extract version using grep and cut
|
||||
VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json" | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not determine version from package.json."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Package version is $VERSION. Downloading matching package tarball..."
|
||||
else
|
||||
echo "Warning: package.json not found. Getting latest version from npm registry..."
|
||||
VERSION=$(curl -s https://registry.npmjs.org/@serve.zone/nupst | grep -o '"latest":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not determine version from npm registry."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Latest version is $VERSION. Using as fallback."
|
||||
fi
|
||||
|
||||
# First try to download with the version from package.json
|
||||
TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$VERSION.tgz"
|
||||
TARBALL_PATH="$TEMP_DIR/nupst.tgz"
|
||||
|
||||
echo "Attempting to download version $VERSION from $TARBALL_URL..."
|
||||
curl -sL "$TARBALL_URL" -o "$TARBALL_PATH"
|
||||
|
||||
# If download fails or file is empty, try to get the latest version from npm
|
||||
if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then
|
||||
echo "Package version $VERSION not found on npm registry."
|
||||
echo "Fetching latest version information from npm registry..."
|
||||
|
||||
# Get latest version from npm registry
|
||||
NPM_REGISTRY_INFO=$(curl -s https://registry.npmjs.org/@serve.zone/nupst)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Could not connect to npm registry."
|
||||
echo "Will attempt to build from source instead."
|
||||
rm -rf "$TEMP_DIR"
|
||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
||||
BUILD_FROM_SOURCE=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Extract latest version
|
||||
LATEST_VERSION=$(echo "$NPM_REGISTRY_INFO" | grep -o '"latest":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "Error: Could not determine latest version from npm registry."
|
||||
echo "Will attempt to build from source instead."
|
||||
rm -rf "$TEMP_DIR"
|
||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
||||
BUILD_FROM_SOURCE=1
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Found latest version: $LATEST_VERSION. Downloading..."
|
||||
|
||||
TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$LATEST_VERSION.tgz"
|
||||
TARBALL_PATH="$TEMP_DIR/nupst.tgz"
|
||||
|
||||
curl -sL "$TARBALL_URL" -o "$TARBALL_PATH"
|
||||
|
||||
if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then
|
||||
echo "Error: Failed to download any package version from npm registry."
|
||||
echo "Installation cannot continue without the dist_ts directory."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract the tarball
|
||||
mkdir -p "$TEMP_DIR/extract"
|
||||
tar -xzf "$TARBALL_PATH" -C "$TEMP_DIR/extract"
|
||||
|
||||
# Copy dist_ts to the installation directory
|
||||
if [ -d "$TEMP_DIR/extract/package/dist_ts" ]; then
|
||||
echo "Copying dist_ts directory to installation..."
|
||||
mkdir -p "$SCRIPT_DIR/dist_ts"
|
||||
cp -R "$TEMP_DIR/extract/package/dist_ts/"* "$SCRIPT_DIR/dist_ts/"
|
||||
else
|
||||
echo "Error: dist_ts directory not found in the downloaded npm package."
|
||||
rm -rf "$TEMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "dist_ts directory successfully downloaded from npm registry."
|
||||
|
||||
# Make launcher script executable
|
||||
chmod +x "$SCRIPT_DIR/bin/nupst"
|
||||
|
||||
# Set up Node.js binary path
|
||||
NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin"
|
||||
NODE_BIN="$NODE_BIN_DIR/node"
|
||||
NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js"
|
||||
|
||||
# Ensure we have executable permissions
|
||||
chmod +x "$NODE_BIN"
|
||||
|
||||
# Make sure the npm-cli.js exists
|
||||
if [ ! -f "$NPM_CLI_JS" ]; then
|
||||
# Try to find npm-cli.js
|
||||
NPM_CLI_JS=$(find "$NODE_BIN_DIR/.." -name "npm-cli.js" | head -1)
|
||||
|
||||
if [ -z "$NPM_CLI_JS" ]; then
|
||||
echo "Warning: Could not find npm-cli.js, npm commands may fail"
|
||||
# Set to a fallback value so code can continue
|
||||
NPM_CLI_JS="$NODE_BIN_DIR/npm"
|
||||
else
|
||||
echo "Found npm-cli.js at: $NPM_CLI_JS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display which binaries we're using
|
||||
echo "Using Node binary: $NODE_BIN"
|
||||
echo "Using NPM CLI JS: $NPM_CLI_JS"
|
||||
|
||||
# Remove existing node_modules directory and package files
|
||||
echo "Cleaning up existing installation..."
|
||||
rm -rf "$SCRIPT_DIR/node_modules"
|
||||
rm -f "$SCRIPT_DIR/package-lock.json"
|
||||
|
||||
# Back up existing package.json if it exists
|
||||
if [ -f "$SCRIPT_DIR/package.json" ]; then
|
||||
echo "Backing up existing package.json..."
|
||||
cp "$SCRIPT_DIR/package.json" "$SCRIPT_DIR/package.json.bak"
|
||||
fi
|
||||
|
||||
# Create a clean minimal package.json with ONLY net-snmp dependency
|
||||
echo "Creating minimal package.json with only net-snmp dependency..."
|
||||
VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json.bak" | head -1 | cut -d'"' -f4 || echo "2.6.3")
|
||||
echo '{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "'$VERSION'",
|
||||
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
|
||||
"main": "dist_ts/index.js",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"nupst": "bin/nupst"
|
||||
},
|
||||
"dependencies": {
|
||||
"net-snmp": "3.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"private": true
|
||||
}' > "$SCRIPT_DIR/package.json"
|
||||
|
||||
# Install ONLY net-snmp
|
||||
echo "Installing ONLY net-snmp dependency (+ 2 subdependencies)..."
|
||||
echo "Node version: $("$NODE_BIN" --version)"
|
||||
echo "Executing NPM directly with Node.js"
|
||||
|
||||
# Execute npm-cli.js directly with our Node.js binary
|
||||
"$NODE_BIN" "$NPM_CLI_JS" --prefix "$SCRIPT_DIR" install --no-audit --no-fund
|
||||
|
||||
INSTALL_STATUS=$?
|
||||
if [ $INSTALL_STATUS -ne 0 ]; then
|
||||
echo "Error: Failed to install net-snmp dependency. NUPST may not function correctly."
|
||||
echo "Restoring original package.json..."
|
||||
mv "$SCRIPT_DIR/package.json.bak" "$SCRIPT_DIR/package.json"
|
||||
exit 1
|
||||
else
|
||||
echo "net-snmp dependency installed successfully."
|
||||
# Show what's actually installed
|
||||
echo "Installed modules:"
|
||||
find "$SCRIPT_DIR/node_modules" -maxdepth 1 -type d | grep -v "^$SCRIPT_DIR/node_modules$" | sort
|
||||
|
||||
# Remove backup if successful
|
||||
rm -f "$SCRIPT_DIR/package.json.bak"
|
||||
fi
|
||||
|
||||
# No temporary files to clean up
|
||||
|
||||
echo "NUPST setup completed successfully."
|
||||
echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst"
|
||||
echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst"
|
168
test/manualdocker/00-test-fresh-v4-install.sh
Executable file
168
test/manualdocker/00-test-fresh-v4-install.sh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Test fresh v4 installation from scratch
|
||||
# Tests the most common user scenario: clean install using curl | bash
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="nupst-test-fresh-v4"
|
||||
|
||||
echo "================================================"
|
||||
echo " NUPST Fresh v4 Installation Test"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Check if container already exists
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "⚠️ Container ${CONTAINER_NAME} already exists"
|
||||
read -p "Remove and recreate? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "→ Stopping and removing existing container..."
|
||||
docker stop ${CONTAINER_NAME} 2>/dev/null || true
|
||||
docker rm ${CONTAINER_NAME} 2>/dev/null || true
|
||||
else
|
||||
echo "Exiting. Remove manually with: docker rm -f ${CONTAINER_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "→ Creating Docker container with systemd..."
|
||||
docker run -d \
|
||||
--name ${CONTAINER_NAME} \
|
||||
--privileged \
|
||||
--cgroupns=host \
|
||||
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
||||
ubuntu:22.04 \
|
||||
/bin/bash -c "apt-get update && apt-get install -y systemd systemd-sysv && exec /sbin/init"
|
||||
|
||||
echo "→ Waiting for systemd to initialize..."
|
||||
sleep 10
|
||||
|
||||
echo "→ Waiting for dpkg lock to be released..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
|
||||
echo ' Waiting for dpkg lock...'
|
||||
sleep 2
|
||||
done
|
||||
echo ' dpkg lock released'
|
||||
"
|
||||
|
||||
echo "→ Installing prerequisites (curl)..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Installing NUPST v4 using curl | bash..."
|
||||
echo " Command: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y"
|
||||
echo ""
|
||||
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " Verifying Installation"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
echo "→ Checking binary location..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
if [ -f /opt/nupst/nupst ]; then
|
||||
echo ' ✓ Binary exists at /opt/nupst/nupst'
|
||||
ls -lh /opt/nupst/nupst
|
||||
else
|
||||
echo ' ✗ Binary not found at /opt/nupst/nupst'
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Checking symlink..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
if [ -L /usr/local/bin/nupst ]; then
|
||||
echo ' ✓ Symlink exists at /usr/local/bin/nupst'
|
||||
ls -lh /usr/local/bin/nupst
|
||||
elif [ -L /usr/bin/nupst ]; then
|
||||
echo ' ✓ Symlink exists at /usr/bin/nupst'
|
||||
ls -lh /usr/bin/nupst
|
||||
else
|
||||
echo ' ✗ Symlink not found in /usr/local/bin or /usr/bin'
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Checking PATH integration..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
NUPST_PATH=\$(which nupst 2>/dev/null)
|
||||
if [ -n \"\$NUPST_PATH\" ]; then
|
||||
echo ' ✓ nupst found in PATH at: '\$NUPST_PATH
|
||||
else
|
||||
echo ' ✗ nupst not found in PATH'
|
||||
echo ' PATH contents:'
|
||||
echo \$PATH
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Testing nupst command execution..."
|
||||
docker exec ${CONTAINER_NAME} nupst --version
|
||||
|
||||
echo ""
|
||||
echo "→ Creating minimal config for service test..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
mkdir -p /etc/nupst
|
||||
cat > /etc/nupst/config.json << 'EOF'
|
||||
{
|
||||
\"version\": \"4.0\",
|
||||
\"upsDevices\": [],
|
||||
\"groups\": [],
|
||||
\"checkInterval\": 30000
|
||||
}
|
||||
EOF
|
||||
echo ' ✓ Minimal config created'
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Testing service creation..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
echo ' Running: nupst service enable'
|
||||
nupst service enable
|
||||
|
||||
if [ -f /etc/systemd/system/nupst.service ]; then
|
||||
echo ' ✓ Service file created successfully'
|
||||
else
|
||||
echo ' ✗ Service file creation failed'
|
||||
exit 1
|
||||
fi
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "→ Checking if service is enabled..."
|
||||
docker exec ${CONTAINER_NAME} systemctl is-enabled nupst
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " ✓ Fresh v4 Installation Test Complete"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Installation verified successfully:"
|
||||
echo " • Binary installed to /opt/nupst/nupst"
|
||||
echo " • Symlink created for global access"
|
||||
echo " • nupst command available in PATH"
|
||||
echo " • Command executes correctly"
|
||||
echo " • Systemd service file created"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " docker exec -it ${CONTAINER_NAME} bash"
|
||||
echo " docker exec ${CONTAINER_NAME} nupst --help"
|
||||
echo " docker exec ${CONTAINER_NAME} nupst service status"
|
||||
echo " docker stop ${CONTAINER_NAME}"
|
||||
echo " docker rm -f ${CONTAINER_NAME}"
|
||||
echo ""
|
148
test/manualdocker/01-setup-v3-container.sh
Executable file
148
test/manualdocker/01-setup-v3-container.sh
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Setup Docker container with systemd and install NUPST v3
|
||||
# This creates a container from commit 806f81c6a057a2a5da586b96a231d391f12eb1bb (v3)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="nupst-test-v3"
|
||||
V3_COMMIT="806f81c6a057a2a5da586b96a231d391f12eb1bb"
|
||||
|
||||
echo "================================================"
|
||||
echo " NUPST v3 Test Container Setup"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Check if container already exists
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "⚠️ Container ${CONTAINER_NAME} already exists"
|
||||
read -p "Remove and recreate? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "→ Stopping and removing existing container..."
|
||||
docker stop ${CONTAINER_NAME} 2>/dev/null || true
|
||||
docker rm ${CONTAINER_NAME} 2>/dev/null || true
|
||||
else
|
||||
echo "Exiting. Remove manually with: docker rm -f ${CONTAINER_NAME}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "→ Creating Docker container (will install systemd)..."
|
||||
docker run -d \
|
||||
--name ${CONTAINER_NAME} \
|
||||
--privileged \
|
||||
--cgroupns=host \
|
||||
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
||||
ubuntu:22.04 \
|
||||
/bin/bash -c "apt-get update && apt-get install -y systemd systemd-sysv && exec /sbin/init"
|
||||
|
||||
echo "→ Waiting for systemd to initialize..."
|
||||
sleep 10
|
||||
|
||||
echo "→ Waiting for dpkg lock to be released..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
|
||||
echo ' Waiting for dpkg lock...'
|
||||
sleep 2
|
||||
done
|
||||
echo ' dpkg lock released'
|
||||
"
|
||||
|
||||
echo "→ Installing prerequisites in container..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl sudo jq
|
||||
"
|
||||
|
||||
echo "→ Cloning NUPST v3 (commit ${V3_COMMIT})..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
cd /opt
|
||||
git clone https://code.foss.global/serve.zone/nupst.git
|
||||
cd nupst
|
||||
git checkout ${V3_COMMIT}
|
||||
echo 'Checked out commit:'
|
||||
git log -1 --oneline
|
||||
"
|
||||
|
||||
echo "→ Running NUPST v3 installation directly (bypassing install.sh auto-update)..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
cd /opt/nupst
|
||||
# Run setup.sh directly to avoid install.sh trying to update to v4
|
||||
bash setup.sh -y
|
||||
"
|
||||
|
||||
echo "→ Creating NUPST configuration using real UPS data from .nogit/env.json..."
|
||||
|
||||
# Check if .nogit/env.json exists
|
||||
if [ ! -f "../../.nogit/env.json" ]; then
|
||||
echo "❌ Error: .nogit/env.json not found"
|
||||
echo "This file contains test UPS credentials and is required for testing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read UPS data from .nogit/env.json and create v3 config
|
||||
docker exec ${CONTAINER_NAME} bash -c "mkdir -p /etc/nupst"
|
||||
|
||||
# Generate config from .nogit/env.json using jq
|
||||
cat ../../.nogit/env.json | jq -r '
|
||||
{
|
||||
"upsList": [
|
||||
{
|
||||
"id": "test-ups-v1",
|
||||
"name": "Test UPS (SNMP v1)",
|
||||
"host": .testConfigV1.snmp.host,
|
||||
"port": .testConfigV1.snmp.port,
|
||||
"community": .testConfigV1.snmp.community,
|
||||
"version": (.testConfigV1.snmp.version | tostring),
|
||||
"batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0",
|
||||
"onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0",
|
||||
"shutdownCommand": "echo \"Shutdown triggered for test-ups-v1\""
|
||||
},
|
||||
{
|
||||
"id": "test-ups-v3",
|
||||
"name": "Test UPS (SNMP v3)",
|
||||
"host": .testConfigV3.snmp.host,
|
||||
"port": .testConfigV3.snmp.port,
|
||||
"version": (.testConfigV3.snmp.version | tostring),
|
||||
"securityLevel": .testConfigV3.snmp.securityLevel,
|
||||
"username": .testConfigV3.snmp.username,
|
||||
"authProtocol": .testConfigV3.snmp.authProtocol,
|
||||
"authKey": .testConfigV3.snmp.authKey,
|
||||
"batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0",
|
||||
"onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0",
|
||||
"shutdownCommand": "echo \"Shutdown triggered for test-ups-v3\""
|
||||
}
|
||||
],
|
||||
"groups": []
|
||||
}' | docker exec -i ${CONTAINER_NAME} tee /etc/nupst/config.json > /dev/null
|
||||
|
||||
echo " ✓ Real UPS config created at /etc/nupst/config.json (from .nogit/env.json)"
|
||||
|
||||
echo "→ Enabling NUPST systemd service..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
nupst enable
|
||||
"
|
||||
|
||||
echo "→ Starting NUPST service..."
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
nupst start
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " ✓ NUPST v3 Container Ready"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Container name: ${CONTAINER_NAME}"
|
||||
echo "NUPST version: v3 (commit ${V3_COMMIT})"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " docker exec -it ${CONTAINER_NAME} bash"
|
||||
echo " docker exec ${CONTAINER_NAME} systemctl status nupst"
|
||||
echo " docker exec ${CONTAINER_NAME} nupst --version"
|
||||
echo " docker stop ${CONTAINER_NAME}"
|
||||
echo " docker start ${CONTAINER_NAME}"
|
||||
echo " docker rm -f ${CONTAINER_NAME}"
|
||||
echo ""
|
59
test/manualdocker/02-test-v3-to-v4-migration.sh
Executable file
59
test/manualdocker/02-test-v3-to-v4-migration.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Test migration from v3 to v4
|
||||
# Run this after 01-setup-v3-container.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="nupst-test-v3"
|
||||
|
||||
echo "================================================"
|
||||
echo " NUPST v3 → v4 Migration Test"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Check if container exists
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "❌ Container ${CONTAINER_NAME} is not running"
|
||||
echo "Run ./01-setup-v3-container.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "→ Checking current NUPST status..."
|
||||
docker exec ${CONTAINER_NAME} systemctl status nupst --no-pager || true
|
||||
echo ""
|
||||
|
||||
echo "→ Checking current version..."
|
||||
docker exec ${CONTAINER_NAME} nupst --version
|
||||
echo ""
|
||||
|
||||
echo "→ Stopping v3 service..."
|
||||
docker exec ${CONTAINER_NAME} systemctl stop nupst
|
||||
echo ""
|
||||
|
||||
echo "→ Running v4 installation from main branch (should auto-detect v3 and migrate)..."
|
||||
echo " Using: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash"
|
||||
docker exec ${CONTAINER_NAME} bash -c "
|
||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y
|
||||
"
|
||||
|
||||
echo "→ Checking service status after migration..."
|
||||
docker exec ${CONTAINER_NAME} systemctl status nupst --no-pager || true
|
||||
echo ""
|
||||
|
||||
echo "→ Checking new version..."
|
||||
docker exec ${CONTAINER_NAME} nupst --version
|
||||
echo ""
|
||||
|
||||
echo "→ Testing service commands..."
|
||||
docker exec ${CONTAINER_NAME} nupst service status || true
|
||||
echo ""
|
||||
|
||||
echo "================================================"
|
||||
echo " ✓ Migration Test Complete"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "Check logs with:"
|
||||
echo " docker exec ${CONTAINER_NAME} nupst service logs"
|
||||
echo ""
|
28
test/manualdocker/03-cleanup.sh
Executable file
28
test/manualdocker/03-cleanup.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Cleanup test container
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="nupst-test-v3"
|
||||
|
||||
echo "================================================"
|
||||
echo " Cleanup NUPST Test Container"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "→ Stopping container..."
|
||||
docker stop ${CONTAINER_NAME} 2>/dev/null || true
|
||||
|
||||
echo "→ Removing container..."
|
||||
docker rm ${CONTAINER_NAME} 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "✓ Container ${CONTAINER_NAME} removed"
|
||||
else
|
||||
echo "Container ${CONTAINER_NAME} not found"
|
||||
fi
|
||||
|
||||
echo ""
|
149
test/manualdocker/README.md
Normal file
149
test/manualdocker/README.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Manual Docker Testing Scripts
|
||||
|
||||
This directory contains scripts for manually testing NUPST installation and migration in Docker containers with systemd support.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
- Privileged access (for systemd in container)
|
||||
- Linux host (systemd container requirements)
|
||||
|
||||
## Test Scripts
|
||||
|
||||
### 1. `01-setup-v3-container.sh`
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
**Container name:** `nupst-test-v3`
|
||||
|
||||
### 2. `02-test-v3-to-v4-migration.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)
|
||||
- Verifies service is running with v4
|
||||
- Tests basic commands
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
chmod +x 02-test-v3-to-v4-migration.sh
|
||||
./02-test-v3-to-v4-migration.sh
|
||||
```
|
||||
|
||||
**Prerequisites:** Must run `01-setup-v3-container.sh` first
|
||||
|
||||
### 3. `03-cleanup.sh`
|
||||
|
||||
Removes the test container.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
chmod +x 03-cleanup.sh
|
||||
./03-cleanup.sh
|
||||
```
|
||||
|
||||
## Manual Testing Workflow
|
||||
|
||||
### Full Migration Test
|
||||
|
||||
1. **Set up v3 environment:**
|
||||
```bash
|
||||
./01-setup-v3-container.sh
|
||||
```
|
||||
|
||||
2. **Verify v3 is working:**
|
||||
```bash
|
||||
docker exec nupst-test-v3 nupst --version
|
||||
docker exec nupst-test-v3 systemctl status nupst
|
||||
```
|
||||
|
||||
3. **Test migration to v4:**
|
||||
```bash
|
||||
./02-test-v3-to-v4-migration.sh
|
||||
```
|
||||
|
||||
4. **Manual verification:**
|
||||
```bash
|
||||
# Enter container
|
||||
docker exec -it nupst-test-v3 bash
|
||||
|
||||
# Inside container:
|
||||
nupst --version # Should show v4.0.0
|
||||
nupst service status # Should show running service
|
||||
cat /etc/nupst/config.json # Config should be preserved
|
||||
systemctl status nupst # Service should be active
|
||||
```
|
||||
|
||||
5. **Cleanup:**
|
||||
```bash
|
||||
./03-cleanup.sh
|
||||
```
|
||||
|
||||
## Useful Docker Commands
|
||||
|
||||
```bash
|
||||
# Enter container shell
|
||||
docker exec -it nupst-test-v3 bash
|
||||
|
||||
# Check service status
|
||||
docker exec nupst-test-v3 systemctl status nupst
|
||||
|
||||
# View service logs
|
||||
docker exec nupst-test-v3 journalctl -u nupst -n 50
|
||||
|
||||
# Check NUPST version
|
||||
docker exec nupst-test-v3 nupst --version
|
||||
|
||||
# Run NUPST commands
|
||||
docker exec nupst-test-v3 nupst service status
|
||||
docker exec nupst-test-v3 nupst ups list
|
||||
|
||||
# Stop container
|
||||
docker stop nupst-test-v3
|
||||
|
||||
# Start container
|
||||
docker start nupst-test-v3
|
||||
|
||||
# Remove container
|
||||
docker rm -f nupst-test-v3
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The container runs with `--privileged` flag for systemd support
|
||||
- Container uses Ubuntu 22.04 as base image
|
||||
- v3 installation is from commit `806f81c6a057a2a5da586b96a231d391f12eb1bb`
|
||||
- v4 migration pulls from `migration/deno-v4` branch
|
||||
- All scripts are designed to be idempotent where possible
|
||||
|
||||
## 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`
|
@@ -1,14 +1,14 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { Logger } from '../ts/logger.js';
|
||||
import { assert, assertEquals } from 'jsr:@std/assert@^1.0.0';
|
||||
import { Logger } from '../ts/logger.ts';
|
||||
|
||||
// Create a Logger instance for testing
|
||||
const logger = new Logger();
|
||||
|
||||
tap.test('should create a logger instance', async () => {
|
||||
expect(logger instanceof Logger).toBeTruthy();
|
||||
Deno.test('should create a logger instance', () => {
|
||||
assert(logger instanceof Logger);
|
||||
});
|
||||
|
||||
tap.test('should log messages with different log levels', async () => {
|
||||
Deno.test('should log messages with different log levels', () => {
|
||||
// We're not testing console output directly, just ensuring no errors
|
||||
logger.log('Regular log message');
|
||||
logger.error('Error message');
|
||||
@@ -16,104 +16,104 @@ tap.test('should log messages with different log levels', async () => {
|
||||
logger.success('Success message');
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
assert(true);
|
||||
});
|
||||
|
||||
tap.test('should create a logbox with title, content, and end', async () => {
|
||||
Deno.test('should create a logbox with title, content, and end', () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logBoxTitle('Test Box', 40);
|
||||
logger.logBoxLine('This is a test line');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
assert(true);
|
||||
});
|
||||
|
||||
tap.test('should handle width persistence between logbox calls', async () => {
|
||||
Deno.test('should handle width persistence between logbox calls', () => {
|
||||
logger.logBoxTitle('Width Test', 45);
|
||||
|
||||
|
||||
// These should use the width from the title
|
||||
logger.logBoxLine('Line 1');
|
||||
logger.logBoxLine('Line 2');
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
let errorThrown = false;
|
||||
|
||||
|
||||
try {
|
||||
// This should work fine after the reset in logBoxEnd
|
||||
logger.logBoxTitle('New Box', 30);
|
||||
logger.logBoxLine('New line');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
errorThrown = true;
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeFalsy();
|
||||
|
||||
assertEquals(errorThrown, false);
|
||||
});
|
||||
|
||||
tap.test('should use default width when no width is specified', async () => {
|
||||
Deno.test('should use default width when no width is specified', () => {
|
||||
// This should automatically use the default width instead of throwing
|
||||
let errorThrown = false;
|
||||
|
||||
|
||||
try {
|
||||
logger.logBoxLine('This should use default width');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
errorThrown = true;
|
||||
}
|
||||
|
||||
|
||||
// Verify no error was thrown
|
||||
expect(errorThrown).toBeFalsy();
|
||||
assertEquals(errorThrown, false);
|
||||
});
|
||||
|
||||
tap.test('should create a complete logbox in one call', async () => {
|
||||
Deno.test('should create a complete logbox in one call', () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logBox('Complete Box', [
|
||||
'Line 1',
|
||||
'Line 2',
|
||||
'Line 3'
|
||||
'Line 3',
|
||||
], 40);
|
||||
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
assert(true);
|
||||
});
|
||||
|
||||
tap.test('should handle content that exceeds box width', async () => {
|
||||
Deno.test('should handle content that exceeds box width', () => {
|
||||
// Just ensuring no errors occur when content is too long
|
||||
logger.logBox('Truncation Test', [
|
||||
'This line is way too long and should be truncated because it exceeds the available space'
|
||||
'This line is way too long and should be truncated because it exceeds the available space',
|
||||
], 30);
|
||||
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
assert(true);
|
||||
});
|
||||
|
||||
tap.test('should create dividers with custom characters', async () => {
|
||||
Deno.test('should create dividers with custom characters', () => {
|
||||
// Just ensuring no errors occur
|
||||
logger.logDivider(30);
|
||||
logger.logDivider(20, '*');
|
||||
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
assert(true);
|
||||
});
|
||||
|
||||
tap.test('should create divider with default width', async () => {
|
||||
Deno.test('should create divider with default width', () => {
|
||||
// This should use the default width
|
||||
logger.logDivider(undefined, '-');
|
||||
|
||||
|
||||
// Just assert that the test runs without errors
|
||||
expect(true).toBeTruthy();
|
||||
assert(true);
|
||||
});
|
||||
|
||||
tap.test('Logger Demo', async () => {
|
||||
Deno.test('Logger Demo', () => {
|
||||
console.log('\n=== LOGGER DEMO ===\n');
|
||||
|
||||
|
||||
// Basic logging
|
||||
logger.log('Regular log message');
|
||||
logger.error('Error message');
|
||||
logger.warn('Warning message');
|
||||
logger.success('Success message');
|
||||
|
||||
|
||||
// Logbox with title, content lines, and end
|
||||
logger.logBoxTitle('Configuration Loaded', 50);
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
@@ -121,40 +121,37 @@ tap.test('Logger Demo', async () => {
|
||||
logger.logBoxLine(' Port: 161');
|
||||
logger.logBoxLine(' Version: 1');
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Complete logbox in one call
|
||||
logger.logBox('UPS Status', [
|
||||
'Power Status: onBattery',
|
||||
'Battery Capacity: 75%',
|
||||
'Runtime Remaining: 30 minutes'
|
||||
'Runtime Remaining: 30 minutes',
|
||||
], 45);
|
||||
|
||||
|
||||
// Logbox with content that's too long for the width
|
||||
logger.logBox('Truncation Example', [
|
||||
'This line is short enough to fit within the box width',
|
||||
'This line is way too long and will be truncated because it exceeds the available space for content within the logbox'
|
||||
'This line is way too long and will be truncated because it exceeds the available space for content within the logbox',
|
||||
], 40);
|
||||
|
||||
|
||||
// Demonstrating logbox width being remembered
|
||||
logger.logBoxTitle('Width Persistence Example', 60);
|
||||
logger.logBoxLine('These lines use the width from the title');
|
||||
logger.logBoxLine('No need to specify the width again');
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Demonstrating default width
|
||||
console.log('\nDefault Width Example:');
|
||||
logger.logBoxLine('This line uses the default width');
|
||||
logger.logBoxLine('Still using default width');
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Divider example
|
||||
logger.log('\nDivider example:');
|
||||
logger.logDivider(30);
|
||||
logger.logDivider(30, '*');
|
||||
logger.logDivider(undefined, '=');
|
||||
|
||||
expect(true).toBeTruthy();
|
||||
});
|
||||
|
||||
// Export the default tap object
|
||||
export default tap.start();
|
||||
assert(true);
|
||||
});
|
||||
|
233
test/test.showcase.ts
Normal file
233
test/test.showcase.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Showcase test for NUPST CLI outputs
|
||||
* Demonstrates all the beautiful colored output features
|
||||
*
|
||||
* Run with: deno run --allow-all test/showcase.ts
|
||||
*/
|
||||
|
||||
import { logger, type ITableColumn } from '../ts/logger.ts';
|
||||
import { theme, symbols, getBatteryColor, formatPowerStatus } from '../ts/colors.ts';
|
||||
|
||||
console.log('');
|
||||
console.log('═'.repeat(80));
|
||||
logger.highlight('NUPST CLI OUTPUT SHOWCASE');
|
||||
logger.dim('Demonstrating beautiful, colored terminal output');
|
||||
console.log('═'.repeat(80));
|
||||
console.log('');
|
||||
|
||||
// === 1. Basic Logging Methods ===
|
||||
logger.logBoxTitle('Basic Logging Methods', 60, 'info');
|
||||
logger.logBoxLine('');
|
||||
logger.log('Normal log message (default color)');
|
||||
logger.success('Success message with ✓ symbol');
|
||||
logger.error('Error message with ✗ symbol');
|
||||
logger.warn('Warning message with ⚠ symbol');
|
||||
logger.info('Info message with ℹ symbol');
|
||||
logger.dim('Dim/secondary text for less important info');
|
||||
logger.highlight('Highlighted/bold text for emphasis');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 2. Colored Boxes ===
|
||||
logger.logBoxTitle('Colored Box Styles', 60);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Boxes can be styled with different colors:');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Success Box (Green)', [
|
||||
'Used for successful operations',
|
||||
'Installation complete, service started, etc.',
|
||||
], 60, 'success');
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Error Box (Red)', [
|
||||
'Used for critical errors and failures',
|
||||
'Configuration errors, connection failures, etc.',
|
||||
], 60, 'error');
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Warning Box (Yellow)', [
|
||||
'Used for warnings and deprecations',
|
||||
'Old command format, missing config, etc.',
|
||||
], 60, 'warning');
|
||||
|
||||
console.log('');
|
||||
|
||||
logger.logBox('Info Box (Cyan)', [
|
||||
'Used for informational messages',
|
||||
'Version info, update available, etc.',
|
||||
], 60, 'info');
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 3. Status Symbols ===
|
||||
logger.logBoxTitle('Status Symbols', 60, 'info');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`${symbols.running} Service Running`);
|
||||
logger.logBoxLine(`${symbols.stopped} Service Stopped`);
|
||||
logger.logBoxLine(`${symbols.starting} Service Starting`);
|
||||
logger.logBoxLine(`${symbols.unknown} Status Unknown`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`${symbols.success} Operation Successful`);
|
||||
logger.logBoxLine(`${symbols.error} Operation Failed`);
|
||||
logger.logBoxLine(`${symbols.warning} Warning Condition`);
|
||||
logger.logBoxLine(`${symbols.info} Information`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 4. Battery Level Colors ===
|
||||
logger.logBoxTitle('Battery Level Color Coding', 60, 'info');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Battery levels are color-coded:');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(` ${getBatteryColor(85)('85%')} - Good (green, ≥60%)`);
|
||||
logger.logBoxLine(` ${getBatteryColor(45)('45%')} - Medium (yellow, 30-60%)`);
|
||||
logger.logBoxLine(` ${getBatteryColor(15)('15%')} - Critical (red, <30%)`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 5. Power Status Formatting ===
|
||||
logger.logBoxTitle('Power Status Formatting', 60, 'info');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`Status: ${formatPowerStatus('online')}`);
|
||||
logger.logBoxLine(`Status: ${formatPowerStatus('onBattery')}`);
|
||||
logger.logBoxLine(`Status: ${formatPowerStatus('unknown')}`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 6. Table Formatting ===
|
||||
const upsColumns: ITableColumn[] = [
|
||||
{ header: 'ID', key: 'id' },
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Host', key: 'host' },
|
||||
{ header: 'Status', key: 'status', color: (v) => {
|
||||
if (v.includes('Online')) return theme.success(v);
|
||||
if (v.includes('Battery')) return theme.warning(v);
|
||||
return theme.dim(v);
|
||||
}},
|
||||
{ header: 'Battery', key: 'battery', align: 'right', color: (v) => {
|
||||
const pct = parseInt(v);
|
||||
return getBatteryColor(pct)(v);
|
||||
}},
|
||||
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||
];
|
||||
|
||||
const upsData = [
|
||||
{
|
||||
id: 'ups-1',
|
||||
name: 'Main UPS',
|
||||
host: '192.168.1.10',
|
||||
status: 'Online',
|
||||
battery: '95%',
|
||||
runtime: '45 min',
|
||||
},
|
||||
{
|
||||
id: 'ups-2',
|
||||
name: 'Backup UPS',
|
||||
host: '192.168.1.11',
|
||||
status: 'On Battery',
|
||||
battery: '42%',
|
||||
runtime: '12 min',
|
||||
},
|
||||
{
|
||||
id: 'ups-3',
|
||||
name: 'Critical UPS',
|
||||
host: '192.168.1.12',
|
||||
status: 'On Battery',
|
||||
battery: '18%',
|
||||
runtime: '5 min',
|
||||
},
|
||||
];
|
||||
|
||||
logger.logTable(upsColumns, upsData, 'UPS Devices');
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 7. Group Table ===
|
||||
const groupColumns: ITableColumn[] = [
|
||||
{ header: 'ID', key: 'id' },
|
||||
{ header: 'Name', key: 'name' },
|
||||
{ header: 'Mode', key: 'mode' },
|
||||
{ header: 'UPS Count', key: 'count', align: 'right' },
|
||||
];
|
||||
|
||||
const groupData = [
|
||||
{ id: 'dc-1', name: 'Data Center 1', mode: 'redundant', count: '3' },
|
||||
{ id: 'office', name: 'Office Servers', mode: 'nonRedundant', count: '2' },
|
||||
];
|
||||
|
||||
logger.logTable(groupColumns, groupData, 'UPS Groups');
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 8. Service Status Example ===
|
||||
logger.logBoxTitle('Service Status', 70, 'success');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`Status: ${symbols.running} ${theme.statusActive('Active (Running)')}`);
|
||||
logger.logBoxLine(`Enabled: ${symbols.success} ${theme.success('Yes')}`);
|
||||
logger.logBoxLine(`Uptime: 2 days, 5 hours, 23 minutes`);
|
||||
logger.logBoxLine(`PID: ${theme.dim('12345')}`);
|
||||
logger.logBoxLine(`Memory: ${theme.dim('45.2 MB')}`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 9. Configuration Example ===
|
||||
logger.logBoxTitle('Configuration', 70);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`UPS Devices: ${theme.highlight('3')}`);
|
||||
logger.logBoxLine(`Groups: ${theme.highlight('2')}`);
|
||||
logger.logBoxLine(`Check Interval: ${theme.dim('30 seconds')}`);
|
||||
logger.logBoxLine(`Config File: ${theme.path('/etc/nupst/config.json')}`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 10. Update Available Example ===
|
||||
logger.logBoxTitle('Update Available', 70, 'warning');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`Current Version: ${theme.dim('4.0.1')}`);
|
||||
logger.logBoxLine(`Latest Version: ${theme.highlight('4.0.2')}`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`Run ${theme.command('sudo nupst update')} to update`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === 11. Error Example ===
|
||||
logger.logBoxTitle('Error Example', 70, 'error');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`${symbols.error} Failed to connect to UPS at 192.168.1.10`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Possible causes:');
|
||||
logger.logBoxLine(` ${theme.dim('• UPS is offline or unreachable')}`);
|
||||
logger.logBoxLine(` ${theme.dim('• Incorrect SNMP community string')}`);
|
||||
logger.logBoxLine(` ${theme.dim('• Firewall blocking port 161')}`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine(`Try: ${theme.command('nupst ups test --debug')}`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxEnd();
|
||||
|
||||
console.log('');
|
||||
|
||||
// === Final Summary ===
|
||||
console.log('═'.repeat(80));
|
||||
logger.success('CLI Output Showcase Complete!');
|
||||
logger.dim('All color and formatting features demonstrated');
|
||||
console.log('═'.repeat(80));
|
||||
console.log('');
|
68
test/test.ts
68
test/test.ts
@@ -1,52 +1,53 @@
|
||||
import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import { NupstSnmp } from '../ts/snmp/manager.js';
|
||||
import type { ISnmpConfig, IUpsStatus } from '../ts/snmp/types.js';
|
||||
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0';
|
||||
import { NupstSnmp } from '../ts/snmp/manager.ts';
|
||||
import type { ISnmpConfig } from '../ts/snmp/types.ts';
|
||||
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
|
||||
const testQenv = new qenv.Qenv('./', '.nogit/');
|
||||
|
||||
// Create an SNMP instance with debug enabled
|
||||
const snmp = new NupstSnmp(true);
|
||||
|
||||
// Load the test configuration from .nogit/env.json
|
||||
// Load the test configuration from .nogit/env.json
|
||||
const testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1');
|
||||
const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3');
|
||||
|
||||
tap.test('should log config', async () => {
|
||||
Deno.test('should log config', () => {
|
||||
console.log(testConfigV1);
|
||||
assert(true);
|
||||
});
|
||||
|
||||
// Test with real UPS using the configuration from .nogit/env.json
|
||||
tap.test('Real UPS test v1', async () => {
|
||||
Deno.test('Real UPS test v1', async () => {
|
||||
try {
|
||||
console.log('Testing with real UPS configuration...');
|
||||
|
||||
|
||||
// Extract the correct SNMP config from the test configuration
|
||||
const snmpConfig = testConfigV1.snmp;
|
||||
const snmpConfig = testConfigV1.snmp as ISnmpConfig;
|
||||
console.log('SNMP Config:');
|
||||
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
||||
console.log(` Version: SNMPv${snmpConfig.version}`);
|
||||
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
||||
|
||||
|
||||
// Use a short timeout for testing
|
||||
const testSnmpConfig = {
|
||||
const testSnmpConfig = {
|
||||
...snmpConfig,
|
||||
timeout: Math.min(snmpConfig.timeout, 10000) // Use at most 10 seconds for testing
|
||||
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
||||
};
|
||||
|
||||
|
||||
// Try to get the UPS status
|
||||
const status = await snmp.getUpsStatus(testSnmpConfig);
|
||||
|
||||
|
||||
console.log('UPS Status:');
|
||||
console.log(` Power Status: ${status.powerStatus}`);
|
||||
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
|
||||
|
||||
// Just make sure we got valid data types back
|
||||
expect(status).toBeTruthy();
|
||||
expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus);
|
||||
expect(typeof status.batteryCapacity).toEqual('number');
|
||||
expect(typeof status.batteryRuntime).toEqual('number');
|
||||
assertExists(status);
|
||||
assert(['online', 'onBattery', 'unknown'].includes(status.powerStatus));
|
||||
assertEquals(typeof status.batteryCapacity, 'number');
|
||||
assertEquals(typeof status.batteryRuntime, 'number');
|
||||
} catch (error) {
|
||||
console.log('Real UPS test failed:', error);
|
||||
// Skip the test if we can't connect to the real UPS
|
||||
@@ -54,42 +55,39 @@ tap.test('Real UPS test v1', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Real UPS test v3', async () => {
|
||||
Deno.test('Real UPS test v3', async () => {
|
||||
try {
|
||||
console.log('Testing with real UPS configuration...');
|
||||
|
||||
|
||||
// Extract the correct SNMP config from the test configuration
|
||||
const snmpConfig = testConfigV3.snmp;
|
||||
const snmpConfig = testConfigV3.snmp as ISnmpConfig;
|
||||
console.log('SNMP Config:');
|
||||
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
||||
console.log(` Version: SNMPv${snmpConfig.version}`);
|
||||
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
||||
|
||||
|
||||
// Use a short timeout for testing
|
||||
const testSnmpConfig = {
|
||||
const testSnmpConfig = {
|
||||
...snmpConfig,
|
||||
timeout: Math.min(snmpConfig.timeout, 10000) // Use at most 10 seconds for testing
|
||||
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
||||
};
|
||||
|
||||
|
||||
// Try to get the UPS status
|
||||
const status = await snmp.getUpsStatus(testSnmpConfig);
|
||||
|
||||
|
||||
console.log('UPS Status:');
|
||||
console.log(` Power Status: ${status.powerStatus}`);
|
||||
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
|
||||
|
||||
// Just make sure we got valid data types back
|
||||
expect(status).toBeTruthy();
|
||||
expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus);
|
||||
expect(typeof status.batteryCapacity).toEqual('number');
|
||||
expect(typeof status.batteryRuntime).toEqual('number');
|
||||
assertExists(status);
|
||||
assert(['online', 'onBattery', 'unknown'].includes(status.powerStatus));
|
||||
assertEquals(typeof status.batteryCapacity, 'number');
|
||||
assertEquals(typeof status.batteryRuntime, 'number');
|
||||
} catch (error) {
|
||||
console.log('Real UPS test failed:', error);
|
||||
// Skip the test if we can't connect to the real UPS
|
||||
console.log('Skipping this test since the UPS might not be available');
|
||||
}
|
||||
});
|
||||
|
||||
// Export the default tap object
|
||||
export default tap.start();
|
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
* commitinfo - reads version from deno.json
|
||||
*/
|
||||
import denoConfig from '../deno.json' with { type: 'json' };
|
||||
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '3.1.1',
|
||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
||||
}
|
||||
name: denoConfig.name,
|
||||
version: denoConfig.version,
|
||||
description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)',
|
||||
};
|
||||
|
170
ts/actions/base-action.ts
Normal file
170
ts/actions/base-action.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Base classes and interfaces for the NUPST action system
|
||||
*
|
||||
* Actions are triggered on:
|
||||
* 1. Power status changes (online ↔ onBattery)
|
||||
* 2. Threshold violations (battery/runtime cross below configured thresholds)
|
||||
*/
|
||||
|
||||
export type TPowerStatus = 'online' | 'onBattery' | 'unknown';
|
||||
|
||||
/**
|
||||
* Context provided to actions when they execute
|
||||
* Contains all relevant UPS state and trigger information
|
||||
*/
|
||||
export interface IActionContext {
|
||||
// UPS identification
|
||||
/** Unique ID of the UPS */
|
||||
upsId: string;
|
||||
/** Human-readable name of the UPS */
|
||||
upsName: string;
|
||||
|
||||
// Current state
|
||||
/** Current power status */
|
||||
powerStatus: TPowerStatus;
|
||||
/** Current battery capacity percentage (0-100) */
|
||||
batteryCapacity: number;
|
||||
/** Estimated battery runtime in minutes */
|
||||
batteryRuntime: number;
|
||||
|
||||
// State tracking
|
||||
/** Previous power status before this trigger */
|
||||
previousPowerStatus: TPowerStatus;
|
||||
|
||||
// Metadata
|
||||
/** Timestamp when this action was triggered (milliseconds since epoch) */
|
||||
timestamp: number;
|
||||
/** Reason this action was triggered */
|
||||
triggerReason: 'powerStatusChange' | 'thresholdViolation';
|
||||
}
|
||||
|
||||
/**
|
||||
* Action trigger mode - determines when an action executes
|
||||
*/
|
||||
export type TActionTriggerMode =
|
||||
| 'onlyPowerChanges' // Only on power status changes (online ↔ onBattery)
|
||||
| 'onlyThresholds' // Only when action's thresholds are exceeded
|
||||
| 'powerChangesAndThresholds' // On power changes OR threshold violations
|
||||
| 'anyChange'; // On every UPS poll/check (every ~30s)
|
||||
|
||||
/**
|
||||
* Configuration for an action
|
||||
*/
|
||||
export interface IActionConfig {
|
||||
/** Type of action to execute */
|
||||
type: 'shutdown' | 'webhook' | 'script';
|
||||
|
||||
// Trigger configuration
|
||||
/**
|
||||
* When should this action be triggered?
|
||||
* - onlyPowerChanges: Only on power status changes
|
||||
* - onlyThresholds: Only when thresholds exceeded
|
||||
* - powerChangesAndThresholds: On both (default)
|
||||
* - anyChange: On every check
|
||||
*/
|
||||
triggerMode?: TActionTriggerMode;
|
||||
|
||||
// Threshold configuration (applies to all action types)
|
||||
/** Threshold settings for this action */
|
||||
thresholds?: {
|
||||
/** Battery percentage threshold (0-100) */
|
||||
battery: number;
|
||||
/** Runtime threshold in minutes */
|
||||
runtime: number;
|
||||
};
|
||||
|
||||
// Shutdown action configuration
|
||||
/** Delay before shutdown in minutes (default: 5) */
|
||||
shutdownDelay?: number;
|
||||
/** Only execute shutdown on threshold violation, not power status changes */
|
||||
onlyOnThresholdViolation?: boolean;
|
||||
|
||||
// Webhook action configuration
|
||||
/** URL to call for webhook */
|
||||
webhookUrl?: string;
|
||||
/** HTTP method to use (default: POST) */
|
||||
webhookMethod?: 'GET' | 'POST';
|
||||
/** Timeout for webhook request in milliseconds (default: 10000) */
|
||||
webhookTimeout?: number;
|
||||
/** Only execute webhook on threshold violation */
|
||||
webhookOnlyOnThresholdViolation?: boolean;
|
||||
|
||||
// Script action configuration
|
||||
/** Path to script relative to /etc/nupst (e.g., "myaction.sh") */
|
||||
scriptPath?: string;
|
||||
/** Timeout for script execution in milliseconds (default: 60000) */
|
||||
scriptTimeout?: number;
|
||||
/** Only execute script on threshold violation */
|
||||
scriptOnlyOnThresholdViolation?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for all actions
|
||||
* Each action type must extend this class and implement execute()
|
||||
*/
|
||||
export abstract class Action {
|
||||
/** Type identifier for this action */
|
||||
abstract readonly type: string;
|
||||
|
||||
/**
|
||||
* Create a new action with the given configuration
|
||||
* @param config Action configuration
|
||||
*/
|
||||
constructor(protected config: IActionConfig) {}
|
||||
|
||||
/**
|
||||
* Execute this action with the given context
|
||||
* @param context Current UPS state and trigger information
|
||||
*/
|
||||
abstract execute(context: IActionContext): Promise<void>;
|
||||
|
||||
/**
|
||||
* Helper to check if this action should execute based on trigger mode
|
||||
* @param context Action context with current UPS state
|
||||
* @returns True if action should execute
|
||||
*/
|
||||
protected shouldExecute(context: IActionContext): boolean {
|
||||
const mode = this.config.triggerMode || 'powerChangesAndThresholds'; // Default
|
||||
|
||||
switch (mode) {
|
||||
case 'onlyPowerChanges':
|
||||
// Only execute on power status changes
|
||||
return context.triggerReason === 'powerStatusChange';
|
||||
|
||||
case 'onlyThresholds':
|
||||
// Only execute when this action's thresholds are exceeded
|
||||
if (!this.config.thresholds) return false; // No thresholds = never execute
|
||||
return this.areThresholdsExceeded(context.batteryCapacity, context.batteryRuntime);
|
||||
|
||||
case 'powerChangesAndThresholds':
|
||||
// Execute on power changes OR when thresholds exceeded
|
||||
if (context.triggerReason === 'powerStatusChange') return true;
|
||||
if (!this.config.thresholds) return false;
|
||||
return this.areThresholdsExceeded(context.batteryCapacity, context.batteryRuntime);
|
||||
|
||||
case 'anyChange':
|
||||
// Execute on every trigger (power change or threshold check)
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current battery/runtime exceeds this action's thresholds
|
||||
* @param batteryCapacity Current battery percentage
|
||||
* @param batteryRuntime Current runtime in minutes
|
||||
* @returns True if thresholds are exceeded
|
||||
*/
|
||||
protected areThresholdsExceeded(batteryCapacity: number, batteryRuntime: number): boolean {
|
||||
if (!this.config.thresholds) {
|
||||
return false; // No thresholds configured
|
||||
}
|
||||
|
||||
return (
|
||||
batteryCapacity < this.config.thresholds.battery ||
|
||||
batteryRuntime < this.config.thresholds.runtime
|
||||
);
|
||||
}
|
||||
}
|
91
ts/actions/index.ts
Normal file
91
ts/actions/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Action system exports and ActionManager
|
||||
*
|
||||
* This module provides the central coordination for the action system.
|
||||
* The ActionManager is responsible for creating and executing actions.
|
||||
*/
|
||||
|
||||
import { logger } from '../logger.ts';
|
||||
import type { Action, IActionConfig, IActionContext } from './base-action.ts';
|
||||
import { ShutdownAction } from './shutdown-action.ts';
|
||||
import { WebhookAction } from './webhook-action.ts';
|
||||
import { ScriptAction } from './script-action.ts';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { IActionConfig, IActionContext, TPowerStatus } from './base-action.ts';
|
||||
export { Action } from './base-action.ts';
|
||||
export { ShutdownAction } from './shutdown-action.ts';
|
||||
export { WebhookAction } from './webhook-action.ts';
|
||||
export { ScriptAction } from './script-action.ts';
|
||||
|
||||
/**
|
||||
* ActionManager - Coordinates action creation and execution
|
||||
*
|
||||
* Provides factory methods for creating actions from configuration
|
||||
* and orchestrates action execution with error handling.
|
||||
*/
|
||||
export class ActionManager {
|
||||
/**
|
||||
* Create an action instance from configuration
|
||||
* @param config Action configuration
|
||||
* @returns Instantiated action
|
||||
* @throws Error if action type is unknown
|
||||
*/
|
||||
static createAction(config: IActionConfig): Action {
|
||||
switch (config.type) {
|
||||
case 'shutdown':
|
||||
return new ShutdownAction(config);
|
||||
case 'webhook':
|
||||
return new WebhookAction(config);
|
||||
case 'script':
|
||||
return new ScriptAction(config);
|
||||
default:
|
||||
throw new Error(`Unknown action type: ${(config as IActionConfig).type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a sequence of actions with the given context
|
||||
* Each action runs sequentially, and failures are logged but don't stop the chain
|
||||
* @param actions Array of action configurations to execute
|
||||
* @param context Action context with UPS state
|
||||
*/
|
||||
static async executeActions(
|
||||
actions: IActionConfig[],
|
||||
context: IActionContext,
|
||||
): Promise<void> {
|
||||
if (!actions || actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
logger.logBoxTitle(`Executing ${actions.length} Action(s)`, 60, 'info');
|
||||
logger.logBoxLine(`Trigger: ${context.triggerReason}`);
|
||||
logger.logBoxLine(`UPS: ${context.upsName} (${context.upsId})`);
|
||||
logger.logBoxLine(`Power: ${context.powerStatus}`);
|
||||
logger.logBoxLine(`Battery: ${context.batteryCapacity}% / ${context.batteryRuntime} min`);
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
const actionConfig = actions[i];
|
||||
try {
|
||||
logger.info(`[${i + 1}/${actions.length}] ${actionConfig.type} action...`);
|
||||
|
||||
const action = this.createAction(actionConfig);
|
||||
await action.execute(context);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Action ${actionConfig.type} failed: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
// Continue with next action despite failure
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
logger.success('Action execution completed');
|
||||
logger.log('');
|
||||
}
|
||||
}
|
167
ts/actions/script-action.ts
Normal file
167
ts/actions/script-action.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import * as path from 'node:path';
|
||||
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 { logger } from '../logger.ts';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* ScriptAction - Executes a custom shell script from /etc/nupst/
|
||||
*
|
||||
* Runs user-provided scripts with UPS state passed as environment variables and arguments.
|
||||
* Scripts must be .sh files located in /etc/nupst/ for security.
|
||||
*/
|
||||
export class ScriptAction extends Action {
|
||||
readonly type = 'script';
|
||||
|
||||
private static readonly SCRIPT_DIR = '/etc/nupst';
|
||||
|
||||
/**
|
||||
* Execute the script action
|
||||
* @param context Action context with UPS state
|
||||
*/
|
||||
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'})`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.scriptPath) {
|
||||
logger.error('Script path not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and build script path
|
||||
const scriptPath = this.validateAndBuildScriptPath(this.config.scriptPath);
|
||||
if (!scriptPath) {
|
||||
logger.error(`Invalid script path: ${this.config.scriptPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if script exists and is executable
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
logger.error(`Script not found: ${scriptPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = this.config.scriptTimeout || 60000; // Default 60 seconds
|
||||
|
||||
logger.info(`Executing script: ${scriptPath}`);
|
||||
|
||||
try {
|
||||
await this.executeScript(scriptPath, context, timeout);
|
||||
logger.success('Script executed successfully');
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Script execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
// Don't throw - script failures shouldn't stop other actions
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate script path and build full path
|
||||
* Ensures security by preventing path traversal and limiting to /etc/nupst
|
||||
* @param scriptPath Relative script path from config
|
||||
* @returns Full validated path or null if invalid
|
||||
*/
|
||||
private validateAndBuildScriptPath(scriptPath: string): string | null {
|
||||
// Remove any leading/trailing whitespace
|
||||
scriptPath = scriptPath.trim();
|
||||
|
||||
// Reject paths with path traversal attempts
|
||||
if (scriptPath.includes('..') || scriptPath.includes('/') || scriptPath.includes('\\')) {
|
||||
logger.error('Script path must not contain directory separators or parent references');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Require .sh extension
|
||||
if (!scriptPath.endsWith('.sh')) {
|
||||
logger.error('Script must have .sh extension');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build full path
|
||||
return path.join(ScriptAction.SCRIPT_DIR, scriptPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the script with UPS state as environment variables and arguments
|
||||
* @param scriptPath Full path to the script
|
||||
* @param context Action context
|
||||
* @param timeout Execution timeout in milliseconds
|
||||
*/
|
||||
private async executeScript(
|
||||
scriptPath: string,
|
||||
context: IActionContext,
|
||||
timeout: number,
|
||||
): Promise<void> {
|
||||
// Prepare environment variables
|
||||
const env = {
|
||||
...process.env,
|
||||
NUPST_UPS_ID: context.upsId,
|
||||
NUPST_UPS_NAME: context.upsName,
|
||||
NUPST_POWER_STATUS: context.powerStatus,
|
||||
NUPST_BATTERY_CAPACITY: String(context.batteryCapacity),
|
||||
NUPST_BATTERY_RUNTIME: String(context.batteryRuntime),
|
||||
NUPST_TRIGGER_REASON: context.triggerReason,
|
||||
NUPST_TIMESTAMP: String(context.timestamp),
|
||||
// Include action's own thresholds if configured
|
||||
NUPST_BATTERY_THRESHOLD: this.config.thresholds ? String(this.config.thresholds.battery) : '',
|
||||
NUPST_RUNTIME_THRESHOLD: this.config.thresholds ? String(this.config.thresholds.runtime) : '',
|
||||
};
|
||||
|
||||
// Build command with arguments
|
||||
// Arguments: powerStatus batteryCapacity batteryRuntime
|
||||
const args = [
|
||||
context.powerStatus,
|
||||
String(context.batteryCapacity),
|
||||
String(context.batteryRuntime),
|
||||
].join(' ');
|
||||
|
||||
const command = `bash "${scriptPath}" ${args}`;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
env,
|
||||
cwd: ScriptAction.SCRIPT_DIR,
|
||||
timeout,
|
||||
});
|
||||
|
||||
// Log output
|
||||
if (stdout) {
|
||||
logger.log('Script stdout:');
|
||||
logger.dim(stdout.trim());
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
logger.warn('Script stderr:');
|
||||
logger.dim(stderr.trim());
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if it was a timeout
|
||||
if (error instanceof Error && 'killed' in error && error.killed) {
|
||||
throw new Error(`Script timed out after ${timeout}ms`);
|
||||
}
|
||||
|
||||
// Include stdout/stderr in error if available
|
||||
if (error && typeof error === 'object' && 'stdout' in error && 'stderr' in error) {
|
||||
const execError = error as { stdout: string; stderr: string };
|
||||
if (execError.stdout) {
|
||||
logger.log('Script stdout:');
|
||||
logger.dim(execError.stdout.trim());
|
||||
}
|
||||
if (execError.stderr) {
|
||||
logger.warn('Script stderr:');
|
||||
logger.dim(execError.stderr.trim());
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
142
ts/actions/shutdown-action.ts
Normal file
142
ts/actions/shutdown-action.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* ShutdownAction - Initiates system shutdown
|
||||
*
|
||||
* This action triggers a system shutdown using the standard shutdown command.
|
||||
* It includes a configurable delay to allow VMs and services to gracefully terminate.
|
||||
*/
|
||||
export class ShutdownAction extends Action {
|
||||
readonly type = 'shutdown';
|
||||
|
||||
/**
|
||||
* Execute the shutdown action
|
||||
* @param context Action context with UPS state
|
||||
*/
|
||||
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'})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const shutdownDelay = this.config.shutdownDelay || 5; // Default 5 minutes
|
||||
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Initiating System Shutdown', 60, 'error');
|
||||
logger.logBoxLine(`UPS: ${context.upsName} (${context.upsId})`);
|
||||
logger.logBoxLine(`Power Status: ${context.powerStatus}`);
|
||||
logger.logBoxLine(`Battery: ${context.batteryCapacity}%`);
|
||||
logger.logBoxLine(`Runtime: ${context.batteryRuntime} minutes`);
|
||||
logger.logBoxLine(`Trigger: ${context.triggerReason}`);
|
||||
logger.logBoxLine(`Shutdown delay: ${shutdownDelay} minutes`);
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
|
||||
try {
|
||||
await this.executeShutdownCommand(shutdownDelay);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Shutdown command failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
// Try alternative methods
|
||||
await this.tryAlternativeShutdownMethods();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the primary shutdown command
|
||||
* @param delayMinutes Minutes to delay before shutdown
|
||||
*/
|
||||
private async executeShutdownCommand(delayMinutes: number): Promise<void> {
|
||||
// Find shutdown command in common system paths
|
||||
const shutdownPaths = [
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown',
|
||||
];
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
try {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
logger.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Continue checking other paths
|
||||
}
|
||||
}
|
||||
|
||||
if (shutdownCmd) {
|
||||
// Execute shutdown command with delay to allow for VM graceful shutdown
|
||||
const message = `UPS battery critical, shutting down in ${delayMinutes} minutes`;
|
||||
logger.log(`Executing: ${shutdownCmd} -h +${delayMinutes} "${message}"`);
|
||||
|
||||
const { stdout } = await execFileAsync(shutdownCmd, [
|
||||
'-h',
|
||||
`+${delayMinutes}`,
|
||||
message,
|
||||
]);
|
||||
|
||||
logger.log(`Shutdown initiated: ${stdout}`);
|
||||
logger.log(`Allowing ${delayMinutes} minutes for VMs to shut down safely`);
|
||||
} else {
|
||||
throw new Error('Shutdown command not found in common paths');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try alternative shutdown methods if primary command fails
|
||||
*/
|
||||
private async tryAlternativeShutdownMethods(): Promise<void> {
|
||||
logger.error('Trying alternative shutdown methods...');
|
||||
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] },
|
||||
{ cmd: 'reboot', args: ['-p'] }, // Some systems allow reboot -p for power off
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// First check if command exists in common system paths
|
||||
const paths = [
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`,
|
||||
];
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
cmdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdPath) {
|
||||
logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
logger.log(`Alternative method ${alt.cmd} succeeded`);
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (_altError) {
|
||||
logger.error(`Alternative method ${alt.cmd} failed`);
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
logger.error('All shutdown methods failed');
|
||||
}
|
||||
}
|
141
ts/actions/webhook-action.ts
Normal file
141
ts/actions/webhook-action.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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 { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* WebhookAction - Calls an HTTP webhook with UPS state information
|
||||
*
|
||||
* Sends UPS status to a configured webhook URL via GET or POST.
|
||||
* This is useful for remote notifications and integrations with external systems.
|
||||
*/
|
||||
export class WebhookAction extends Action {
|
||||
readonly type = 'webhook';
|
||||
|
||||
/**
|
||||
* Execute the webhook action
|
||||
* @param context Action context with UPS state
|
||||
*/
|
||||
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'})`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.webhookUrl) {
|
||||
logger.error('Webhook URL not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
const method = this.config.webhookMethod || 'POST';
|
||||
const timeout = this.config.webhookTimeout || 10000;
|
||||
|
||||
logger.info(`Calling webhook: ${method} ${this.config.webhookUrl}`);
|
||||
|
||||
try {
|
||||
await this.callWebhook(context, method, timeout);
|
||||
logger.success('Webhook call successful');
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Webhook call failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
// Don't throw - webhook failures shouldn't stop other actions
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the webhook with UPS state data
|
||||
* @param context Action context
|
||||
* @param method HTTP method (GET or POST)
|
||||
* @param timeout Request timeout in milliseconds
|
||||
*/
|
||||
private async callWebhook(
|
||||
context: IActionContext,
|
||||
method: 'GET' | 'POST',
|
||||
timeout: number,
|
||||
): Promise<void> {
|
||||
const payload: any = {
|
||||
upsId: context.upsId,
|
||||
upsName: context.upsName,
|
||||
powerStatus: context.powerStatus,
|
||||
batteryCapacity: context.batteryCapacity,
|
||||
batteryRuntime: context.batteryRuntime,
|
||||
triggerReason: context.triggerReason,
|
||||
timestamp: context.timestamp,
|
||||
};
|
||||
|
||||
// Include action's own thresholds if configured
|
||||
if (this.config.thresholds) {
|
||||
payload.thresholds = {
|
||||
battery: this.config.thresholds.battery,
|
||||
runtime: this.config.thresholds.runtime,
|
||||
};
|
||||
}
|
||||
|
||||
const url = new URL(this.config.webhookUrl!);
|
||||
|
||||
if (method === 'GET') {
|
||||
// Append payload as query parameters for GET
|
||||
url.searchParams.append('upsId', payload.upsId);
|
||||
url.searchParams.append('upsName', payload.upsName);
|
||||
url.searchParams.append('powerStatus', payload.powerStatus);
|
||||
url.searchParams.append('batteryCapacity', String(payload.batteryCapacity));
|
||||
url.searchParams.append('batteryRuntime', String(payload.batteryRuntime));
|
||||
|
||||
url.searchParams.append('triggerReason', payload.triggerReason);
|
||||
url.searchParams.append('timestamp', String(payload.timestamp));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.protocol === 'https:' ? https : http;
|
||||
|
||||
const options: http.RequestOptions = {
|
||||
method,
|
||||
headers: method === 'POST'
|
||||
? {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'nupst',
|
||||
}
|
||||
: {
|
||||
'User-Agent': 'nupst',
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
|
||||
const req = protocol.request(url, options, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
logger.dim(`Webhook response (${res.statusCode}): ${data.substring(0, 100)}`);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Webhook returned status ${res.statusCode}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error(`Webhook request timed out after ${timeout}ms`));
|
||||
});
|
||||
|
||||
// Send POST data if applicable
|
||||
if (method === 'POST') {
|
||||
req.write(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
}
|
687
ts/cli.ts
687
ts/cli.ts
@@ -1,6 +1,7 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { Nupst } from './nupst.js';
|
||||
import { logger } from './logger.js';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from './nupst.ts';
|
||||
import { logger, type ITableColumn } from './logger.ts';
|
||||
import { theme, symbols } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Class for handling CLI commands
|
||||
@@ -21,7 +22,7 @@ export class NupstCli {
|
||||
* @param args Command line arguments (process.argv)
|
||||
*/
|
||||
public async parseAndExecute(args: string[]): Promise<void> {
|
||||
// Extract debug flag from any position
|
||||
// Extract debug and version flags from any position
|
||||
const debugOptions = this.extractDebugOptions(args);
|
||||
if (debugOptions.debugMode) {
|
||||
logger.log('Debug mode enabled');
|
||||
@@ -29,6 +30,12 @@ export class NupstCli {
|
||||
this.nupst.getSnmp().enableDebug();
|
||||
}
|
||||
|
||||
// Check for version flag
|
||||
if (debugOptions.cleanedArgs.includes('--version') || debugOptions.cleanedArgs.includes('-v')) {
|
||||
this.showVersion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the command (default to help if none provided)
|
||||
const command = debugOptions.cleanedArgs[2] || 'help';
|
||||
const commandArgs = debugOptions.cleanedArgs.slice(3);
|
||||
@@ -56,23 +63,104 @@ export class NupstCli {
|
||||
* @param commandArgs Additional command arguments
|
||||
* @param debugMode Whether debug mode is enabled
|
||||
*/
|
||||
private async executeCommand(command: string, commandArgs: string[], debugMode: boolean): Promise<void> {
|
||||
private async executeCommand(
|
||||
command: string,
|
||||
commandArgs: string[],
|
||||
debugMode: boolean,
|
||||
): Promise<void> {
|
||||
// Get access to the handlers
|
||||
const upsHandler = this.nupst.getUpsHandler();
|
||||
const groupHandler = this.nupst.getGroupHandler();
|
||||
const serviceHandler = this.nupst.getServiceHandler();
|
||||
|
||||
const actionHandler = this.nupst.getActionHandler();
|
||||
|
||||
// Handle service subcommands
|
||||
if (command === 'service') {
|
||||
const subcommand = commandArgs[0] || 'status';
|
||||
|
||||
switch (subcommand) {
|
||||
case 'enable':
|
||||
await serviceHandler.enable();
|
||||
break;
|
||||
case 'disable':
|
||||
await serviceHandler.disable();
|
||||
break;
|
||||
case 'start':
|
||||
await serviceHandler.start();
|
||||
break;
|
||||
case 'stop':
|
||||
await serviceHandler.stop();
|
||||
break;
|
||||
case 'restart':
|
||||
await serviceHandler.stop();
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2s
|
||||
await serviceHandler.start();
|
||||
break;
|
||||
case 'status':
|
||||
await serviceHandler.status();
|
||||
break;
|
||||
case 'logs':
|
||||
await serviceHandler.logs();
|
||||
break;
|
||||
case 'start-daemon':
|
||||
await serviceHandler.daemonStart(debugMode);
|
||||
break;
|
||||
default:
|
||||
this.showServiceHelp();
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle UPS subcommands
|
||||
if (command === 'ups') {
|
||||
const subcommand = commandArgs[0] || 'list';
|
||||
const subcommandArgs = commandArgs.slice(1);
|
||||
|
||||
switch (subcommand) {
|
||||
case 'add':
|
||||
await upsHandler.add();
|
||||
break;
|
||||
case 'edit': {
|
||||
const upsId = subcommandArgs[0];
|
||||
await upsHandler.edit(upsId);
|
||||
break;
|
||||
}
|
||||
case 'remove':
|
||||
case 'rm': {
|
||||
const upsIdToRemove = subcommandArgs[0];
|
||||
if (!upsIdToRemove) {
|
||||
logger.error('UPS ID is required for remove command');
|
||||
this.showUpsHelp();
|
||||
return;
|
||||
}
|
||||
await upsHandler.remove(upsIdToRemove);
|
||||
break;
|
||||
}
|
||||
case 'list':
|
||||
case 'ls': // Alias
|
||||
await upsHandler.list();
|
||||
break;
|
||||
case 'test':
|
||||
await upsHandler.test(debugMode);
|
||||
break;
|
||||
default:
|
||||
this.showUpsHelp();
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle group subcommands
|
||||
if (command === 'group') {
|
||||
const subcommand = commandArgs[0] || 'list';
|
||||
const subcommandArgs = commandArgs.slice(1);
|
||||
|
||||
|
||||
switch (subcommand) {
|
||||
case 'add':
|
||||
await groupHandler.add();
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
case 'edit': {
|
||||
const groupId = subcommandArgs[0];
|
||||
if (!groupId) {
|
||||
logger.error('Group ID is required for edit command');
|
||||
@@ -81,21 +169,22 @@ export class NupstCli {
|
||||
}
|
||||
await groupHandler.edit(groupId);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
const groupIdToDelete = subcommandArgs[0];
|
||||
if (!groupIdToDelete) {
|
||||
logger.error('Group ID is required for delete command');
|
||||
}
|
||||
case 'remove':
|
||||
case 'rm': {
|
||||
const groupIdToRemove = subcommandArgs[0];
|
||||
if (!groupIdToRemove) {
|
||||
logger.error('Group ID is required for remove command');
|
||||
this.showGroupHelp();
|
||||
return;
|
||||
}
|
||||
await groupHandler.delete(groupIdToDelete);
|
||||
await groupHandler.remove(groupIdToRemove);
|
||||
break;
|
||||
|
||||
}
|
||||
case 'list':
|
||||
case 'ls': // Alias
|
||||
await groupHandler.list();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.showGroupHelp();
|
||||
break;
|
||||
@@ -103,82 +192,69 @@ export class NupstCli {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle main commands
|
||||
switch (command) {
|
||||
case 'add':
|
||||
await upsHandler.add();
|
||||
break;
|
||||
// Handle action subcommands
|
||||
if (command === 'action') {
|
||||
const subcommand = commandArgs[0] || 'list';
|
||||
const subcommandArgs = commandArgs.slice(1);
|
||||
|
||||
case 'edit':
|
||||
const upsId = commandArgs[0];
|
||||
await upsHandler.edit(upsId);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
const upsIdToDelete = commandArgs[0];
|
||||
if (!upsIdToDelete) {
|
||||
logger.error('UPS ID is required for delete command');
|
||||
this.showHelp();
|
||||
return;
|
||||
switch (subcommand) {
|
||||
case 'add': {
|
||||
const upsId = subcommandArgs[0];
|
||||
await actionHandler.add(upsId);
|
||||
break;
|
||||
}
|
||||
await upsHandler.delete(upsIdToDelete);
|
||||
break;
|
||||
case 'remove':
|
||||
case 'rm': {
|
||||
const upsId = subcommandArgs[0];
|
||||
const actionIndex = subcommandArgs[1];
|
||||
await actionHandler.remove(upsId, actionIndex);
|
||||
break;
|
||||
}
|
||||
case 'list':
|
||||
case 'ls': { // Alias
|
||||
const upsId = subcommandArgs[0];
|
||||
await actionHandler.list(upsId);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.showActionHelp();
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'list':
|
||||
await upsHandler.list();
|
||||
break;
|
||||
// Handle config subcommand
|
||||
if (command === 'config') {
|
||||
const subcommand = commandArgs[0] || 'show';
|
||||
|
||||
case 'setup':
|
||||
// Backward compatibility: setup is now an alias for edit with no specific UPS ID
|
||||
await upsHandler.edit(undefined);
|
||||
break;
|
||||
|
||||
case 'enable':
|
||||
await serviceHandler.enable();
|
||||
break;
|
||||
|
||||
case 'daemon-start':
|
||||
await serviceHandler.daemonStart(debugMode);
|
||||
break;
|
||||
|
||||
case 'logs':
|
||||
await serviceHandler.logs();
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
await serviceHandler.stop();
|
||||
break;
|
||||
|
||||
case 'start':
|
||||
await serviceHandler.start();
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
await serviceHandler.status();
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
await serviceHandler.disable();
|
||||
break;
|
||||
|
||||
case 'test':
|
||||
await upsHandler.test(debugMode);
|
||||
break;
|
||||
switch (subcommand) {
|
||||
case 'show':
|
||||
case 'display':
|
||||
await this.showConfig();
|
||||
break;
|
||||
default:
|
||||
await this.showConfig();
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle top-level commands
|
||||
switch (command) {
|
||||
case 'update':
|
||||
await serviceHandler.update();
|
||||
break;
|
||||
|
||||
case 'uninstall':
|
||||
await serviceHandler.uninstall();
|
||||
break;
|
||||
|
||||
case 'config':
|
||||
await this.showConfig();
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
this.showHelp();
|
||||
break;
|
||||
default:
|
||||
logger.error(`Unknown command: ${command}`);
|
||||
logger.log('');
|
||||
this.showHelp();
|
||||
break;
|
||||
}
|
||||
@@ -192,181 +268,320 @@ export class NupstCli {
|
||||
// Try to load configuration
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
const errorBoxWidth = 45;
|
||||
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
||||
logger.logBoxLine('No configuration found.');
|
||||
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
||||
logger.logBoxEnd();
|
||||
} catch (_error) {
|
||||
logger.logBox('Configuration Error', [
|
||||
'No configuration found.',
|
||||
"Please run 'nupst ups add' first to create a configuration.",
|
||||
], 50, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('NUPST Configuration', boxWidth);
|
||||
|
||||
// Check if multi-UPS config
|
||||
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
||||
// Multi-UPS configuration
|
||||
logger.logBoxLine(`UPS Devices: ${config.upsDevices.length}`);
|
||||
logger.logBoxLine(`Groups: ${config.groups ? config.groups.length : 0}`);
|
||||
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Configuration File Location:');
|
||||
logger.logBoxLine(' /etc/nupst/config.json');
|
||||
logger.logBoxEnd();
|
||||
// === Multi-UPS Configuration ===
|
||||
|
||||
// Overview Box
|
||||
logger.log('');
|
||||
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');
|
||||
|
||||
// Show UPS devices
|
||||
// UPS Devices Table
|
||||
if (config.upsDevices.length > 0) {
|
||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
||||
for (const ups of config.upsDevices) {
|
||||
logger.logBoxLine(`${ups.name} (${ups.id}):`);
|
||||
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`);
|
||||
logger.logBoxLine(` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`);
|
||||
logger.logBoxLine(` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`);
|
||||
logger.logBoxLine('');
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
const upsRows = config.upsDevices.map((ups) => ({
|
||||
name: ups.name,
|
||||
id: theme.dim(ups.id),
|
||||
host: `${ups.snmp.host}:${ups.snmp.port}`,
|
||||
model: ups.snmp.upsModel || 'cyberpower',
|
||||
actions: `${(ups.actions || []).length} configured`,
|
||||
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
|
||||
}));
|
||||
|
||||
const upsColumns: ITableColumn[] = [
|
||||
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||
{ header: 'ID', key: 'id', align: 'left' },
|
||||
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||
{ header: 'Model', key: 'model', align: 'left' },
|
||||
{ header: 'Actions', key: 'actions', align: 'left' },
|
||||
{ header: 'Groups', key: 'groups', align: 'left' },
|
||||
];
|
||||
|
||||
logger.log('');
|
||||
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||
logger.log('');
|
||||
logger.logTable(upsColumns, upsRows);
|
||||
}
|
||||
|
||||
// Show groups
|
||||
// Groups Table
|
||||
if (config.groups && config.groups.length > 0) {
|
||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
||||
for (const group of config.groups) {
|
||||
logger.logBoxLine(`${group.name} (${group.id}):`);
|
||||
logger.logBoxLine(` Mode: ${group.mode}`);
|
||||
if (group.description) {
|
||||
logger.logBoxLine(` Description: ${group.description}`);
|
||||
}
|
||||
|
||||
// List UPS devices in this group
|
||||
const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(group.id));
|
||||
logger.logBoxLine(` UPS Devices: ${upsInGroup.length > 0 ?
|
||||
upsInGroup.map(ups => ups.name).join(', ') : 'None'}`);
|
||||
logger.logBoxLine('');
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
const groupRows = config.groups.map((group) => {
|
||||
const upsInGroup = config.upsDevices.filter((ups) =>
|
||||
ups.groups && ups.groups.includes(group.id)
|
||||
);
|
||||
return {
|
||||
name: group.name,
|
||||
id: theme.dim(group.id),
|
||||
mode: group.mode,
|
||||
upsCount: String(upsInGroup.length),
|
||||
ups: upsInGroup.length > 0
|
||||
? upsInGroup.map((ups) => ups.name).join(', ')
|
||||
: theme.dim('None'),
|
||||
description: group.description || theme.dim('—'),
|
||||
};
|
||||
});
|
||||
|
||||
const groupColumns: ITableColumn[] = [
|
||||
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||
{ header: 'ID', key: 'id', align: 'left' },
|
||||
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
||||
{ header: 'UPS', key: 'upsCount', align: 'right' },
|
||||
{ header: 'UPS Devices', key: 'ups', align: 'left' },
|
||||
{ header: 'Description', key: 'description', align: 'left' },
|
||||
];
|
||||
|
||||
logger.log('');
|
||||
logger.info(`UPS Groups (${config.groups.length}):`);
|
||||
logger.log('');
|
||||
logger.logTable(groupColumns, groupRows);
|
||||
}
|
||||
} else {
|
||||
// Legacy single UPS configuration
|
||||
// SNMP Settings
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
logger.logBoxLine(` Host: ${config.snmp.host}`);
|
||||
logger.logBoxLine(` Port: ${config.snmp.port}`);
|
||||
logger.logBoxLine(` Version: ${config.snmp.version}`);
|
||||
logger.logBoxLine(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||
|
||||
if (config.snmp.version === 1 || config.snmp.version === 2) {
|
||||
logger.logBoxLine(` Community: ${config.snmp.community}`);
|
||||
} else if (config.snmp.version === 3) {
|
||||
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
|
||||
logger.logBoxLine(` Username: ${config.snmp.username}`);
|
||||
|
||||
// Show auth and privacy details based on security level
|
||||
if (
|
||||
config.snmp.securityLevel === 'authNoPriv' ||
|
||||
config.snmp.securityLevel === 'authPriv'
|
||||
) {
|
||||
logger.logBoxLine(` Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
|
||||
}
|
||||
|
||||
if (config.snmp.securityLevel === 'authPriv') {
|
||||
logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
|
||||
}
|
||||
|
||||
// Show timeout value
|
||||
logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
|
||||
// === Legacy Single UPS Configuration ===
|
||||
|
||||
if (!config.snmp) {
|
||||
logger.logBox('Configuration Error', [
|
||||
'Error: Legacy configuration missing SNMP settings',
|
||||
], 60, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show OIDs if custom model is selected
|
||||
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
|
||||
logger.logBoxLine('Custom OIDs:');
|
||||
logger.logBoxLine(` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`);
|
||||
logger.logBoxLine(
|
||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`
|
||||
);
|
||||
logger.logBoxLine(` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||
}
|
||||
|
||||
// Thresholds
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
|
||||
logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
|
||||
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
||||
|
||||
// Configuration file location
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Configuration File Location:');
|
||||
logger.logBoxLine(' /etc/nupst/config.json');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Note: Using legacy single-UPS configuration format.');
|
||||
logger.logBoxLine('Consider using "nupst add" to migrate to multi-UPS format.');
|
||||
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
logger.logBox('NUPST Configuration (Legacy)', [
|
||||
theme.warning('Legacy single-UPS configuration format'),
|
||||
'',
|
||||
theme.dim('SNMP Settings:'),
|
||||
` Host: ${theme.info(config.snmp.host)}`,
|
||||
` Port: ${theme.info(String(config.snmp.port))}`,
|
||||
` Version: ${config.snmp.version}`,
|
||||
` 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'
|
||||
? [` 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:'),
|
||||
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
|
||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
|
||||
]
|
||||
: []
|
||||
),
|
||||
'',
|
||||
|
||||
` Check Interval: ${config.checkInterval / 1000} seconds`,
|
||||
'',
|
||||
theme.dim('Configuration File:'),
|
||||
` ${theme.path('/etc/nupst/config.json')}`,
|
||||
'',
|
||||
theme.warning('Note: Using legacy single-UPS configuration format.'),
|
||||
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
|
||||
], 70, 'warning');
|
||||
}
|
||||
|
||||
// Show service status
|
||||
// Service Status
|
||||
try {
|
||||
const isActive =
|
||||
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
||||
const isEnabled =
|
||||
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
|
||||
|
||||
const statusBoxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', statusBoxWidth);
|
||||
logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`);
|
||||
logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
logger.log('');
|
||||
logger.logBox('Service Status', [
|
||||
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
|
||||
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
|
||||
], 50, isActive ? 'success' : 'default');
|
||||
logger.log('');
|
||||
} catch (_error) {
|
||||
// Ignore errors checking service status
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to display configuration: ${error.message}`);
|
||||
logger.error(
|
||||
`Failed to display configuration: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display version information
|
||||
*/
|
||||
private showVersion(): void {
|
||||
const version = this.nupst.getVersion();
|
||||
logger.log(`NUPST version ${version}`);
|
||||
logger.log('Network UPS Shutdown Tool (https://nupst.serve.zone)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display help message
|
||||
*/
|
||||
private showHelp(): void {
|
||||
console.log('');
|
||||
logger.highlight('NUPST - UPS Shutdown Tool');
|
||||
logger.dim('Deno-powered UPS monitoring and shutdown automation');
|
||||
console.log('');
|
||||
|
||||
// Usage section
|
||||
logger.log(theme.info('Usage:'));
|
||||
logger.log(` ${theme.command('nupst')} ${theme.dim('<command> [options]')}`);
|
||||
console.log('');
|
||||
|
||||
// Main commands section
|
||||
logger.log(theme.info('Commands:'));
|
||||
this.printCommand('service <subcommand>', 'Manage systemd service');
|
||||
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
||||
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
||||
this.printCommand('action <subcommand>', 'Manage UPS actions');
|
||||
this.printCommand('config [show]', 'Display current configuration');
|
||||
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
||||
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
||||
this.printCommand('help, --help, -h', 'Show this help message');
|
||||
this.printCommand('--version, -v', 'Show version information');
|
||||
console.log('');
|
||||
|
||||
// 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 start', 'Start the systemd service');
|
||||
this.printCommand('nupst service stop', 'Stop the systemd service');
|
||||
this.printCommand('nupst service restart', 'Restart the systemd service');
|
||||
this.printCommand('nupst service status', 'Show service and UPS status');
|
||||
this.printCommand('nupst service logs', 'Show service logs in real-time');
|
||||
this.printCommand('nupst service start-daemon', 'Start daemon process directly');
|
||||
console.log('');
|
||||
|
||||
// UPS subcommands
|
||||
logger.log(theme.info('UPS Subcommands:'));
|
||||
this.printCommand('nupst ups add', 'Add a new UPS device');
|
||||
this.printCommand('nupst ups edit [id]', 'Edit a UPS device (default if no ID)');
|
||||
this.printCommand('nupst ups remove <id>', 'Remove a UPS device by ID');
|
||||
this.printCommand('nupst ups list (or ls)', 'List all configured UPS devices');
|
||||
this.printCommand('nupst ups test', 'Test UPS connections');
|
||||
console.log('');
|
||||
|
||||
// Group subcommands
|
||||
logger.log(theme.info('Group Subcommands:'));
|
||||
this.printCommand('nupst group add', 'Add a new UPS group');
|
||||
this.printCommand('nupst group edit <id>', 'Edit an existing UPS group');
|
||||
this.printCommand('nupst group remove <id>', 'Remove a UPS group by ID');
|
||||
this.printCommand('nupst group list (or ls)', 'List all UPS groups');
|
||||
console.log('');
|
||||
|
||||
// Action subcommands
|
||||
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)');
|
||||
console.log('');
|
||||
|
||||
// Options
|
||||
logger.log(theme.info('Options:'));
|
||||
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
||||
logger.dim(' (Example: nupst ups test --debug)');
|
||||
console.log('');
|
||||
|
||||
// Examples
|
||||
logger.log(theme.info('Examples:'));
|
||||
logger.dim(' nupst service enable # Install and start the service');
|
||||
logger.dim(' nupst ups add # Add a new UPS interactively');
|
||||
logger.dim(' nupst group list # Show all configured groups');
|
||||
logger.dim(' nupst config # Display current configuration');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to print a command with description
|
||||
*/
|
||||
private printCommand(command: string, description: string, extra?: string): void {
|
||||
const paddedCommand = command.padEnd(30);
|
||||
logger.log(` ${theme.command(paddedCommand)} ${description}${extra ? ' ' + extra : ''}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display help message for service commands
|
||||
*/
|
||||
private showServiceHelp(): void {
|
||||
logger.log(`
|
||||
NUPST - Node.js UPS Shutdown Tool
|
||||
NUPST - Service Management Commands
|
||||
|
||||
Usage:
|
||||
nupst enable - Install and enable the systemd service (requires root)
|
||||
nupst disable - Stop and uninstall the systemd service (requires root)
|
||||
nupst daemon-start - Start the daemon process directly
|
||||
nupst logs - Show logs of the systemd service
|
||||
nupst stop - Stop the systemd service
|
||||
nupst start - Start the systemd service
|
||||
nupst status - Show status of the systemd service and UPS status
|
||||
nupst service <subcommand>
|
||||
|
||||
UPS Management:
|
||||
nupst add - Add a new UPS device
|
||||
nupst edit [id] - Edit an existing UPS (default UPS if no ID provided)
|
||||
nupst delete <id> - Delete a UPS by ID
|
||||
nupst list - List all configured UPS devices
|
||||
nupst setup - Alias for 'nupst edit' (backward compatibility)
|
||||
|
||||
Group Management:
|
||||
nupst group list - List all UPS groups
|
||||
nupst group add - Add a new UPS group
|
||||
nupst group edit <id> - Edit an existing UPS group
|
||||
nupst group delete <id> - Delete a UPS group
|
||||
|
||||
System Commands:
|
||||
nupst test - Test the current configuration by connecting to all UPS devices
|
||||
nupst config - Display the current configuration
|
||||
nupst update - Update NUPST from repository and refresh systemd service (requires root)
|
||||
nupst uninstall - Completely uninstall NUPST from the system (requires root)
|
||||
nupst help - Show this help message
|
||||
Subcommands:
|
||||
enable - Install and enable the systemd service (requires root)
|
||||
disable - Stop and disable the systemd service (requires root)
|
||||
start - Start the systemd service
|
||||
stop - Stop the systemd service
|
||||
restart - Restart the systemd service
|
||||
status - Show service status and UPS information
|
||||
logs - Show service logs in real-time
|
||||
start-daemon - Start the daemon process directly (for testing)
|
||||
|
||||
Options:
|
||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
||||
(Example: nupst test --debug)
|
||||
--debug, -d - Enable debug mode for detailed logging
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display help message for UPS commands
|
||||
*/
|
||||
private showUpsHelp(): void {
|
||||
logger.log(`
|
||||
NUPST - UPS Management Commands
|
||||
|
||||
Usage:
|
||||
nupst ups <subcommand> [arguments]
|
||||
|
||||
Subcommands:
|
||||
add - Add a new UPS device interactively
|
||||
edit [id] - Edit a UPS device (edits default if no ID provided)
|
||||
remove <id> - Remove a UPS device by ID (alias: rm)
|
||||
list - List all configured UPS devices (alias: ls)
|
||||
test - Test connections to all configured UPS devices
|
||||
|
||||
Options:
|
||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
||||
|
||||
Examples:
|
||||
nupst ups add - Add a new UPS device
|
||||
nupst ups edit ups-1 - Edit UPS with ID 'ups-1'
|
||||
nupst ups remove ups-1 - Remove UPS with ID 'ups-1'
|
||||
nupst ups test --debug - Test all UPS connections with debug output
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -378,13 +593,45 @@ Options:
|
||||
NUPST - Group Management Commands
|
||||
|
||||
Usage:
|
||||
nupst group list - List all UPS groups
|
||||
nupst group add - Add a new UPS group
|
||||
nupst group edit <id> - Edit an existing UPS group
|
||||
nupst group delete <id> - Delete a UPS group
|
||||
nupst group <subcommand> [arguments]
|
||||
|
||||
Subcommands:
|
||||
add - Add a new UPS group interactively
|
||||
edit <id> - Edit an existing UPS group
|
||||
remove <id> - Remove a UPS group by ID (alias: rm)
|
||||
list - List all UPS groups (alias: ls)
|
||||
|
||||
Options:
|
||||
--debug, -d - Enable debug mode for detailed logging
|
||||
--debug, -d - Enable debug mode for detailed logging
|
||||
|
||||
Examples:
|
||||
nupst group add - Create a new group
|
||||
nupst group edit dc-1 - Edit group with ID 'dc-1'
|
||||
nupst group remove dc-1 - Remove group with ID 'dc-1'
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
private showActionHelp(): void {
|
||||
logger.log(`
|
||||
NUPST - Action Management Commands
|
||||
|
||||
Usage:
|
||||
nupst action <subcommand> [arguments]
|
||||
|
||||
Subcommands:
|
||||
add <ups-id|group-id> - Add a new action to a UPS or group interactively
|
||||
remove <ups-id|group-id> <index> - Remove an action by index (alias: rm)
|
||||
list [ups-id|group-id] - List all actions (optionally for specific target) (alias: ls)
|
||||
|
||||
Options:
|
||||
--debug, -d - Enable debug mode for detailed logging
|
||||
|
||||
Examples:
|
||||
nupst action list - List actions for all UPS devices and groups
|
||||
nupst action list default - List actions for UPS or group with ID 'default'
|
||||
nupst action add default - Add a new action to UPS or group 'default'
|
||||
nupst action remove default 0 - Remove action at index 0 from UPS or group 'default'
|
||||
nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1'
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
357
ts/cli/action-handler.ts
Normal file
357
ts/cli/action-handler.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
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 { IActionConfig } from '../actions/base-action.ts';
|
||||
import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
|
||||
|
||||
/**
|
||||
* Class for handling action-related CLI commands
|
||||
* Provides interface for managing UPS actions
|
||||
*/
|
||||
export class ActionHandler {
|
||||
private readonly nupst: Nupst;
|
||||
|
||||
/**
|
||||
* Create a new action handler
|
||||
* @param nupst Reference to the main Nupst instance
|
||||
*/
|
||||
constructor(nupst: Nupst) {
|
||||
this.nupst = nupst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new action to a UPS or group
|
||||
*/
|
||||
public async add(targetId?: string): Promise<void> {
|
||||
try {
|
||||
if (!targetId) {
|
||||
logger.error('Target ID is required');
|
||||
logger.log(
|
||||
` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
|
||||
);
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||
logger.log(` ${theme.dim('List groups:')} ${theme.command('nupst group list')}`);
|
||||
logger.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = await this.nupst.getDaemon().loadConfig();
|
||||
|
||||
// Check if it's a UPS
|
||||
const ups = config.upsDevices.find((u) => u.id === targetId);
|
||||
// Check if it's a group
|
||||
const group = config.groups?.find((g) => g.id === targetId);
|
||||
|
||||
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('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const target = ups || group;
|
||||
const targetType = ups ? 'UPS' : 'Group';
|
||||
const targetName = ups ? ups.name : group!.name;
|
||||
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const prompt = (question: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer: string) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
logger.log('');
|
||||
logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
|
||||
logger.log('');
|
||||
|
||||
// Action type (currently only shutdown is supported)
|
||||
const type = 'shutdown';
|
||||
logger.log(` ${theme.dim('Action type:')} ${theme.highlight('shutdown')}`);
|
||||
|
||||
// Battery threshold
|
||||
const batteryStr = await prompt(
|
||||
` ${theme.dim('Battery threshold')} ${theme.dim('(%):')} `,
|
||||
);
|
||||
const battery = parseInt(batteryStr, 10);
|
||||
if (isNaN(battery) || battery < 0 || battery > 100) {
|
||||
logger.error('Invalid battery threshold. Must be 0-100.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Runtime threshold
|
||||
const runtimeStr = await prompt(
|
||||
` ${theme.dim('Runtime threshold')} ${theme.dim('(minutes):')} `,
|
||||
);
|
||||
const runtime = parseInt(runtimeStr, 10);
|
||||
if (isNaN(runtime) || runtime < 0) {
|
||||
logger.error('Invalid runtime threshold. Must be >= 0.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 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('2)')} onlyThresholds - Trigger only when thresholds are violated`,
|
||||
);
|
||||
logger.log(
|
||||
` ${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]:')} `);
|
||||
const triggerModeMap: Record<string, string> = {
|
||||
'1': 'onlyPowerChanges',
|
||||
'2': 'onlyThresholds',
|
||||
'3': 'powerChangesAndThresholds',
|
||||
'4': 'anyChange',
|
||||
'': 'onlyThresholds', // Default
|
||||
};
|
||||
const triggerMode = triggerModeMap[triggerChoice] || 'onlyThresholds';
|
||||
|
||||
// Shutdown delay
|
||||
const delayStr = await prompt(
|
||||
` ${theme.dim('Shutdown delay')} ${theme.dim('(seconds) [5]:')} `,
|
||||
);
|
||||
const shutdownDelay = delayStr ? parseInt(delayStr, 10) : 5;
|
||||
if (isNaN(shutdownDelay) || shutdownDelay < 0) {
|
||||
logger.error('Invalid shutdown delay. Must be >= 0.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create the action
|
||||
const newAction: IActionConfig = {
|
||||
type,
|
||||
thresholds: {
|
||||
battery,
|
||||
runtime,
|
||||
},
|
||||
triggerMode: triggerMode as IActionConfig['triggerMode'],
|
||||
shutdownDelay,
|
||||
};
|
||||
|
||||
// Add to target (UPS or group)
|
||||
if (!target!.actions) {
|
||||
target!.actions = [];
|
||||
}
|
||||
target!.actions.push(newAction);
|
||||
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
logger.log('');
|
||||
logger.success(`Action added to ${targetType} ${targetName}`);
|
||||
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
|
||||
logger.log('');
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to add action: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an action from a UPS or group
|
||||
*/
|
||||
public async remove(targetId?: string, actionIndexStr?: string): Promise<void> {
|
||||
try {
|
||||
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>')}`,
|
||||
);
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
|
||||
logger.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const actionIndex = parseInt(actionIndexStr, 10);
|
||||
if (isNaN(actionIndex) || actionIndex < 0) {
|
||||
logger.error('Invalid action index. Must be >= 0.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = await this.nupst.getDaemon().loadConfig();
|
||||
|
||||
// Check if it's a UPS
|
||||
const ups = config.upsDevices.find((u) => u.id === targetId);
|
||||
// Check if it's a group
|
||||
const group = config.groups?.find((g) => g.id === targetId);
|
||||
|
||||
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('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const target = ups || group;
|
||||
const targetType = ups ? 'UPS' : 'Group';
|
||||
const targetName = ups ? ups.name : group!.name;
|
||||
|
||||
if (!target!.actions || target!.actions.length === 0) {
|
||||
logger.error(`No actions configured for ${targetType} '${targetName}'`);
|
||||
logger.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (actionIndex >= target!.actions.length) {
|
||||
logger.error(
|
||||
`Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
|
||||
);
|
||||
logger.log('');
|
||||
logger.log(
|
||||
` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${targetId}`)}`,
|
||||
);
|
||||
logger.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const removedAction = target!.actions[actionIndex];
|
||||
target!.actions.splice(actionIndex, 1);
|
||||
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
logger.log('');
|
||||
logger.success(`Action removed from ${targetType} ${targetName}`);
|
||||
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
|
||||
if (removedAction.thresholds) {
|
||||
logger.log(
|
||||
` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
|
||||
);
|
||||
}
|
||||
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
|
||||
logger.log('');
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to remove action: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all actions for a specific UPS/group or all devices
|
||||
*/
|
||||
public async list(targetId?: string): Promise<void> {
|
||||
try {
|
||||
const config = await this.nupst.getDaemon().loadConfig();
|
||||
|
||||
if (targetId) {
|
||||
// List actions for specific UPS or group
|
||||
const ups = config.upsDevices.find((u) => u.id === targetId);
|
||||
const group = config.groups?.find((g) => g.id === targetId);
|
||||
|
||||
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('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (ups) {
|
||||
this.displayTargetActions(ups, 'UPS');
|
||||
} else {
|
||||
this.displayTargetActions(group!, 'Group');
|
||||
}
|
||||
} else {
|
||||
// List actions for all UPS devices and groups
|
||||
logger.log('');
|
||||
logger.info('Actions for All UPS Devices and Groups');
|
||||
logger.log('');
|
||||
|
||||
let hasAnyActions = false;
|
||||
|
||||
// Display UPS actions
|
||||
for (const ups of config.upsDevices) {
|
||||
if (ups.actions && ups.actions.length > 0) {
|
||||
hasAnyActions = true;
|
||||
this.displayTargetActions(ups, 'UPS');
|
||||
}
|
||||
}
|
||||
|
||||
// Display Group actions
|
||||
for (const group of config.groups || []) {
|
||||
if (group.actions && group.actions.length > 0) {
|
||||
hasAnyActions = true;
|
||||
this.displayTargetActions(group, 'Group');
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnyActions) {
|
||||
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>')}`,
|
||||
);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to list actions: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display actions for a single UPS or Group
|
||||
*/
|
||||
private displayTargetActions(
|
||||
target: IUpsConfig | IGroupConfig,
|
||||
targetType: 'UPS' | 'Group',
|
||||
): void {
|
||||
logger.log(
|
||||
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
|
||||
);
|
||||
logger.log('');
|
||||
|
||||
if (!target.actions || target.actions.length === 0) {
|
||||
logger.log(` ${theme.dim('No actions configured')}`);
|
||||
logger.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
const columns: ITableColumn[] = [
|
||||
{ header: 'Index', key: 'index', align: 'right' },
|
||||
{ header: 'Type', key: 'type', align: 'left' },
|
||||
{ header: 'Battery', key: 'battery', align: 'right' },
|
||||
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||
{ header: 'Trigger Mode', key: 'triggerMode', align: 'left' },
|
||||
{ header: 'Delay', key: 'delay', align: 'right' },
|
||||
];
|
||||
|
||||
const rows = target.actions.map((action, index) => ({
|
||||
index: theme.dim(index.toString()),
|
||||
type: theme.highlight(action.type),
|
||||
battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'),
|
||||
runtime: action.thresholds ? `${action.thresholds.runtime}min` : theme.dim('N/A'),
|
||||
triggerMode: theme.dim(action.triggerMode || 'onlyThresholds'),
|
||||
delay: `${action.shutdownDelay || 5}s`,
|
||||
}));
|
||||
|
||||
logger.logTable(columns, rows);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
@@ -1,7 +1,9 @@
|
||||
import { Nupst } from '../nupst.js';
|
||||
import { logger } from '../logger.js';
|
||||
import * as helpers from '../helpers/index.js';
|
||||
import { type IGroupConfig } from '../daemon.js';
|
||||
import process from 'node:process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { theme } from '../colors.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
import { type IGroupConfig } from '../daemon.ts';
|
||||
|
||||
/**
|
||||
* Class for handling group-related CLI commands
|
||||
@@ -27,68 +29,79 @@ export class GroupHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
const errorBoxWidth = 45;
|
||||
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
||||
logger.logBoxLine('No configuration found.');
|
||||
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
||||
logger.logBoxEnd();
|
||||
logger.logBox('Configuration Error', [
|
||||
'No configuration found.',
|
||||
"Please run 'nupst ups add' first to create a configuration.",
|
||||
], 50, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if multi-UPS config
|
||||
if (!config.groups || !Array.isArray(config.groups)) {
|
||||
// Legacy or missing groups configuration
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
||||
logger.logBoxLine('No groups configured.');
|
||||
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
||||
logger.logBoxEnd();
|
||||
logger.logBox('UPS Groups', [
|
||||
'No groups configured.',
|
||||
'',
|
||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
||||
], 50, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// Display group list
|
||||
const boxWidth = 60;
|
||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
||||
|
||||
|
||||
// Display group list with modern table
|
||||
if (config.groups.length === 0) {
|
||||
logger.logBoxLine('No UPS groups configured.');
|
||||
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
||||
} else {
|
||||
logger.logBoxLine(`Found ${config.groups.length} group(s)`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('ID | Name | Mode | UPS Devices');
|
||||
logger.logBoxLine('-----------+----------------------+--------------+----------------');
|
||||
|
||||
for (const group of config.groups) {
|
||||
const id = group.id.padEnd(10, ' ').substring(0, 10);
|
||||
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
|
||||
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
|
||||
|
||||
// Count UPS devices in this group
|
||||
const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id));
|
||||
const upsCount = upsInGroup.length;
|
||||
const upsNames = upsInGroup.map(ups => ups.name).join(', ');
|
||||
|
||||
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
|
||||
}
|
||||
logger.logBox('UPS Groups', [
|
||||
'No UPS groups configured.',
|
||||
'',
|
||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
||||
], 60, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Prepare table data
|
||||
const rows = config.groups.map((group) => {
|
||||
// Count UPS devices in this group
|
||||
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
|
||||
const upsCount = upsInGroup.length;
|
||||
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
||||
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name || '',
|
||||
mode: group.mode || 'unknown',
|
||||
count: String(upsCount),
|
||||
devices: upsCount > 0 ? upsNames : theme.dim('None'),
|
||||
};
|
||||
});
|
||||
|
||||
const columns: ITableColumn[] = [
|
||||
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
|
||||
{ header: 'Name', key: 'name', align: 'left' },
|
||||
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
||||
{ header: 'UPS Count', key: 'count', align: 'right' },
|
||||
{ header: 'UPS Devices', key: 'devices', align: 'left' },
|
||||
];
|
||||
|
||||
logger.log('');
|
||||
logger.info(`UPS Groups (${config.groups.length}):`);
|
||||
logger.log('');
|
||||
logger.logTable(columns, rows);
|
||||
logger.log('');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list UPS groups: ${error.message}`);
|
||||
logger.error(
|
||||
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a new UPS group
|
||||
*/
|
||||
public async add(): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('readline');
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
@@ -109,54 +122,56 @@ export class GroupHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
|
||||
logger.error(
|
||||
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Initialize groups array if not exists
|
||||
if (!config.groups) {
|
||||
config.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Check if upsDevices is initialized
|
||||
if (!config.upsDevices) {
|
||||
config.upsDevices = [];
|
||||
}
|
||||
|
||||
|
||||
logger.log('\nNUPST Add Group');
|
||||
logger.log('==============\n');
|
||||
logger.log('This will guide you through creating a new UPS group.\n');
|
||||
|
||||
|
||||
// Generate a new unique group ID
|
||||
const groupId = helpers.shortId();
|
||||
|
||||
|
||||
// Get group name
|
||||
const name = await prompt('Group Name: ');
|
||||
|
||||
|
||||
// Get group mode
|
||||
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
|
||||
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||
|
||||
|
||||
// Get optional description
|
||||
const description = await prompt('Group Description (optional): ');
|
||||
|
||||
|
||||
// Create the new group
|
||||
const newGroup: IGroupConfig = {
|
||||
id: groupId,
|
||||
name: name || `Group-${groupId}`,
|
||||
mode,
|
||||
description: description || undefined
|
||||
description: description || undefined,
|
||||
};
|
||||
|
||||
|
||||
// Add the group to the configuration
|
||||
config.groups.push(newGroup);
|
||||
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
// Display summary
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Group Created', boxWidth);
|
||||
@@ -167,30 +182,33 @@ export class GroupHandler {
|
||||
logger.logBoxLine(`Description: ${newGroup.description}`);
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Check if there are UPS devices to assign to this group
|
||||
if (config.upsDevices.length > 0) {
|
||||
const assignUps = await prompt('Would you like to assign UPS devices to this group now? (y/N): ');
|
||||
const assignUps = await prompt(
|
||||
'Would you like to assign UPS devices to this group now? (y/N): ',
|
||||
);
|
||||
if (assignUps.toLowerCase() === 'y') {
|
||||
await this.assignUpsToGroup(newGroup.id, config, prompt);
|
||||
|
||||
|
||||
// Save again after assigning UPS devices
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
|
||||
|
||||
logger.log('\nGroup setup complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Add group error: ${error.message}`);
|
||||
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Edit an existing UPS group
|
||||
* @param groupId ID of the group to edit
|
||||
@@ -198,7 +216,7 @@ export class GroupHandler {
|
||||
public async edit(groupId: string): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('readline');
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
@@ -219,57 +237,61 @@ export class GroupHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
|
||||
logger.error(
|
||||
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if groups are initialized
|
||||
if (!config.groups || !Array.isArray(config.groups)) {
|
||||
logger.error('No groups configured. Please run "nupst group add" first to create a group.');
|
||||
logger.error(
|
||||
'No groups configured. Please run "nupst group add" first to create a group.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Find the group to edit
|
||||
const groupIndex = config.groups.findIndex(group => group.id === groupId);
|
||||
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
||||
if (groupIndex === -1) {
|
||||
logger.error(`Group with ID "${groupId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const group = config.groups[groupIndex];
|
||||
|
||||
|
||||
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
|
||||
logger.log('==============================================\n');
|
||||
|
||||
|
||||
// Edit group name
|
||||
const newName = await prompt(`Group Name [${group.name}]: `);
|
||||
if (newName.trim()) {
|
||||
group.name = newName;
|
||||
}
|
||||
|
||||
|
||||
// Edit group mode
|
||||
const currentMode = group.mode || 'redundant';
|
||||
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
|
||||
if (modeInput.trim()) {
|
||||
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||
}
|
||||
|
||||
|
||||
// Edit description
|
||||
const currentDesc = group.description || '';
|
||||
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
|
||||
if (newDesc.trim() || newDesc === '') {
|
||||
group.description = newDesc.trim() || undefined;
|
||||
}
|
||||
|
||||
|
||||
// Update the group in the configuration
|
||||
config.groups[groupIndex] = group;
|
||||
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
// Display summary
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Group Updated', boxWidth);
|
||||
@@ -280,80 +302,89 @@ export class GroupHandler {
|
||||
logger.logBoxLine(`Description: ${group.description}`);
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Edit UPS assignments if requested
|
||||
const editAssignments = await prompt('Would you like to edit UPS assignments for this group? (y/N): ');
|
||||
const editAssignments = await prompt(
|
||||
'Would you like to edit UPS assignments for this group? (y/N): ',
|
||||
);
|
||||
if (editAssignments.toLowerCase() === 'y') {
|
||||
await this.assignUpsToGroup(group.id, config, prompt);
|
||||
|
||||
|
||||
// Save again after editing assignments
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
}
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
|
||||
|
||||
logger.log('\nGroup edit complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Edit group error: ${error.message}`);
|
||||
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an existing UPS group
|
||||
* @param groupId ID of the group to delete
|
||||
*/
|
||||
public async delete(groupId: string): Promise<void> {
|
||||
public async remove(groupId: string): Promise<void> {
|
||||
try {
|
||||
// Try to load configuration
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
|
||||
logger.error(
|
||||
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if groups are initialized
|
||||
if (!config.groups || !Array.isArray(config.groups)) {
|
||||
logger.error('No groups configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Find the group to delete
|
||||
const groupIndex = config.groups.findIndex(group => group.id === groupId);
|
||||
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
||||
if (groupIndex === -1) {
|
||||
logger.error(`Group with ID "${groupId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const groupToDelete = config.groups[groupIndex];
|
||||
|
||||
|
||||
// Get confirmation before deleting
|
||||
const readline = await import('readline');
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const confirm = await new Promise<string>(resolve => {
|
||||
rl.question(`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => {
|
||||
resolve(answer.toLowerCase());
|
||||
});
|
||||
|
||||
const confirm = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
|
||||
(answer) => {
|
||||
resolve(answer.toLowerCase());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
rl.close();
|
||||
|
||||
process.stdin.destroy();
|
||||
|
||||
if (confirm !== 'y' && confirm !== 'yes') {
|
||||
logger.log('Deletion cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Remove this group from all UPS device group assignments
|
||||
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
||||
for (const ups of config.upsDevices) {
|
||||
@@ -363,19 +394,21 @@ export class GroupHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove the group from the array
|
||||
config.groups.splice(groupIndex, 1);
|
||||
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete group: ${error.message}`);
|
||||
logger.error(
|
||||
`Failed to delete group: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,18 +421,18 @@ export class GroupHandler {
|
||||
public async assignUpsToGroups(
|
||||
ups: any,
|
||||
groups: any[],
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Initialize groups array if it doesn't exist
|
||||
if (!ups.groups) {
|
||||
ups.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Show current group assignments
|
||||
logger.log('\nCurrent Group Assignments:');
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
for (const groupId of ups.groups) {
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
const group = groups.find((g) => g.id === groupId);
|
||||
if (group) {
|
||||
logger.log(`- ${group.name} (${group.id})`);
|
||||
} else {
|
||||
@@ -409,52 +442,56 @@ export class GroupHandler {
|
||||
} else {
|
||||
logger.log('- None');
|
||||
}
|
||||
|
||||
|
||||
// Show available groups
|
||||
logger.log('\nAvailable Groups:');
|
||||
if (groups.length === 0) {
|
||||
logger.log('- No groups available. Use "nupst group add" to create groups.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
const assigned = ups.groups && ups.groups.includes(group.id);
|
||||
logger.log(`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
||||
logger.log(
|
||||
`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Prompt for group selection
|
||||
const selection = await prompt('\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
|
||||
|
||||
const selection = await prompt(
|
||||
'\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
||||
);
|
||||
|
||||
if (selection.toLowerCase() === 'clear') {
|
||||
// Clear all group assignments
|
||||
ups.groups = [];
|
||||
logger.log('All group assignments cleared.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!selection.trim()) {
|
||||
// No change if empty input
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Process selections
|
||||
const selections = selection.split(',').map(s => s.trim());
|
||||
|
||||
const selections = selection.split(',').map((s) => s.trim());
|
||||
|
||||
for (const sel of selections) {
|
||||
const index = parseInt(sel, 10) - 1;
|
||||
if (isNaN(index) || index < 0 || index >= groups.length) {
|
||||
logger.error(`Invalid selection: ${sel}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const group = groups[index];
|
||||
|
||||
|
||||
// Initialize groups array if needed (should already be done above)
|
||||
if (!ups.groups) {
|
||||
ups.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Toggle assignment
|
||||
const groupIndex = ups.groups.indexOf(group.id);
|
||||
if (groupIndex === -1) {
|
||||
@@ -468,7 +505,7 @@ export class GroupHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assign UPS devices to a specific group
|
||||
* @param groupId Group ID to assign UPS devices to
|
||||
@@ -478,22 +515,24 @@ export class GroupHandler {
|
||||
public async assignUpsToGroup(
|
||||
groupId: string,
|
||||
config: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
if (!config.upsDevices || config.upsDevices.length === 0) {
|
||||
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
|
||||
return;
|
||||
}
|
||||
|
||||
const group = config.groups.find(g => g.id === groupId);
|
||||
|
||||
const group = config.groups.find((g: { id: string }) => g.id === groupId);
|
||||
if (!group) {
|
||||
logger.error(`Group with ID "${groupId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Show current assignments
|
||||
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
|
||||
const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(groupId));
|
||||
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) =>
|
||||
ups.groups && ups.groups.includes(groupId)
|
||||
);
|
||||
if (upsInGroup.length === 0) {
|
||||
logger.log('- None');
|
||||
} else {
|
||||
@@ -501,7 +540,7 @@ export class GroupHandler {
|
||||
logger.log(`- ${ups.name} (${ups.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show all UPS devices
|
||||
logger.log('\nAvailable UPS devices:');
|
||||
for (let i = 0; i < config.upsDevices.length; i++) {
|
||||
@@ -509,10 +548,12 @@ export class GroupHandler {
|
||||
const assigned = ups.groups && ups.groups.includes(groupId);
|
||||
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
||||
}
|
||||
|
||||
|
||||
// Prompt for UPS selection
|
||||
const selection = await prompt('\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
|
||||
|
||||
const selection = await prompt(
|
||||
'\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
||||
);
|
||||
|
||||
if (selection.toLowerCase() === 'clear') {
|
||||
// Clear all UPS from this group
|
||||
for (const ups of config.upsDevices) {
|
||||
@@ -526,29 +567,29 @@ export class GroupHandler {
|
||||
logger.log(`All UPS devices removed from group "${group.name}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!selection.trim()) {
|
||||
// No change if empty input
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Process selections
|
||||
const selections = selection.split(',').map(s => s.trim());
|
||||
|
||||
const selections = selection.split(',').map((s) => s.trim());
|
||||
|
||||
for (const sel of selections) {
|
||||
const index = parseInt(sel, 10) - 1;
|
||||
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
|
||||
logger.error(`Invalid selection: ${sel}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const ups = config.upsDevices[index];
|
||||
|
||||
|
||||
// Initialize groups array if needed
|
||||
if (!ups.groups) {
|
||||
ups.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Toggle assignment
|
||||
const groupIndex = ups.groups.indexOf(groupId);
|
||||
if (groupIndex === -1) {
|
||||
@@ -562,4 +603,4 @@ export class GroupHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { Nupst } from '../nupst.js';
|
||||
import { logger } from '../logger.js';
|
||||
import process from 'node:process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Class for handling service-related CLI commands
|
||||
@@ -128,85 +129,61 @@ export class ServiceHandler {
|
||||
try {
|
||||
// Check if running as root
|
||||
this.checkRootAccess(
|
||||
'This command must be run as root to update NUPST and refresh the systemd service.'
|
||||
'This command must be run as root to update NUPST.',
|
||||
);
|
||||
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('NUPST Update Process', boxWidth);
|
||||
logger.logBoxLine('Updating NUPST from repository...');
|
||||
|
||||
// Determine the installation directory (assuming it's either /opt/nupst or the current directory)
|
||||
const { existsSync } = await import('fs');
|
||||
let installDir = '/opt/nupst';
|
||||
|
||||
if (!existsSync(installDir)) {
|
||||
// If not installed in /opt/nupst, use the current directory
|
||||
const { dirname } = await import('path');
|
||||
installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
|
||||
logger.logBoxLine(`Using local installation directory: ${installDir}`);
|
||||
}
|
||||
console.log('');
|
||||
logger.info('Checking for updates...');
|
||||
|
||||
try {
|
||||
// 1. Update the repository
|
||||
logger.logBoxLine('Pulling latest changes from git repository...');
|
||||
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
|
||||
stdio: 'pipe',
|
||||
// Get current version
|
||||
const currentVersion = this.nupst.getVersion();
|
||||
|
||||
// Fetch latest version from Gitea API
|
||||
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
|
||||
const response = execSync(`curl -sSL ${apiUrl}`).toString();
|
||||
const release = JSON.parse(response);
|
||||
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}`;
|
||||
|
||||
logger.dim(`Current version: ${normalizedCurrent}`);
|
||||
logger.dim(`Latest version: ${normalizedLatest}`);
|
||||
console.log('');
|
||||
|
||||
// Compare normalized versions
|
||||
if (normalizedCurrent === normalizedLatest) {
|
||||
logger.success('Already up to date!');
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`New version available: ${latestVersion}`);
|
||||
logger.dim('Downloading and installing...');
|
||||
console.log('');
|
||||
|
||||
// Download and run the install script
|
||||
// This handles everything: download binary, stop service, replace, restart
|
||||
const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh';
|
||||
|
||||
execSync(`curl -sSL ${installUrl} | bash`, {
|
||||
stdio: 'inherit', // Show install script output to user
|
||||
});
|
||||
|
||||
// 2. Run the install.sh script
|
||||
logger.logBoxLine('Running install.sh to update NUPST...');
|
||||
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
|
||||
|
||||
// 3. Run the setup.sh script with force flag to update Node.js and dependencies
|
||||
logger.logBoxLine('Running setup.sh to update Node.js and dependencies...');
|
||||
execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' });
|
||||
|
||||
// 4. Refresh the systemd service
|
||||
logger.logBoxLine('Refreshing systemd service...');
|
||||
|
||||
// First check if service exists
|
||||
let serviceExists = false;
|
||||
try {
|
||||
const output = execSync('systemctl list-unit-files | grep nupst.service').toString();
|
||||
serviceExists = output.includes('nupst.service');
|
||||
} catch (error) {
|
||||
// If grep fails (service not found), serviceExists remains false
|
||||
serviceExists = false;
|
||||
}
|
||||
|
||||
if (serviceExists) {
|
||||
// Stop the service if it's running
|
||||
const isRunning =
|
||||
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
||||
if (isRunning) {
|
||||
logger.logBoxLine('Stopping nupst service...');
|
||||
execSync('systemctl stop nupst.service');
|
||||
}
|
||||
|
||||
// Reinstall the service
|
||||
logger.logBoxLine('Reinstalling systemd service...');
|
||||
await this.nupst.getSystemd().install();
|
||||
|
||||
// Restart the service if it was running
|
||||
if (isRunning) {
|
||||
logger.logBoxLine('Restarting nupst service...');
|
||||
execSync('systemctl start nupst.service');
|
||||
}
|
||||
} else {
|
||||
logger.logBoxLine('Systemd service not installed, skipping service refresh.');
|
||||
logger.logBoxLine('Run "nupst enable" to install the service.');
|
||||
}
|
||||
|
||||
logger.logBoxLine('Update completed successfully!');
|
||||
logger.logBoxEnd();
|
||||
console.log('');
|
||||
logger.success(`Updated to ${latestVersion}`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
logger.logBoxLine('Error during update process:');
|
||||
logger.logBoxLine(`${error.message}`);
|
||||
logger.logBoxEnd();
|
||||
console.log('');
|
||||
logger.error('Update failed');
|
||||
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Update failed: ${error.message}`);
|
||||
logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -236,13 +213,15 @@ export class ServiceHandler {
|
||||
});
|
||||
};
|
||||
|
||||
console.log('\nNUPST Uninstaller');
|
||||
console.log('===============');
|
||||
console.log('This will completely remove NUPST from your system.\n');
|
||||
logger.log('');
|
||||
logger.highlight('NUPST Uninstaller');
|
||||
logger.dim('===============');
|
||||
logger.log('This will completely remove NUPST from your system.');
|
||||
logger.log('');
|
||||
|
||||
// Ask about removing configuration
|
||||
const removeConfig = await prompt(
|
||||
'Do you want to remove the NUPST configuration files? (y/N): '
|
||||
'Do you want to remove the NUPST configuration files? (y/N): ',
|
||||
);
|
||||
|
||||
// Find the uninstall.sh script location
|
||||
@@ -274,17 +253,20 @@ export class ServiceHandler {
|
||||
}
|
||||
|
||||
if (!uninstallScriptPath) {
|
||||
console.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
||||
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Close readline before executing script
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
|
||||
// Execute uninstall.sh with the appropriate option
|
||||
console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`);
|
||||
logger.log('');
|
||||
logger.log(`Running uninstaller from ${uninstallScriptPath}...`);
|
||||
|
||||
// Pass the configuration removal option as an environment variable
|
||||
const env = {
|
||||
@@ -300,7 +282,7 @@ export class ServiceHandler {
|
||||
stdio: 'inherit', // Show output in the terminal
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Uninstall failed: ${error.message}`);
|
||||
logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -317,4 +299,4 @@ export class ServiceHandler {
|
||||
|
||||
return { debugMode, cleanedArgs };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { Nupst } from '../nupst.js';
|
||||
import { logger } from '../logger.js';
|
||||
import * as helpers from '../helpers/index.js';
|
||||
import process from 'node:process';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger, type ITableColumn } from '../logger.ts';
|
||||
import { theme } from '../colors.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
import type { TUpsModel } from '../snmp/types.ts';
|
||||
import type { INupstConfig } from '../daemon.ts';
|
||||
|
||||
/**
|
||||
* Class for handling UPS-related CLI commands
|
||||
@@ -24,7 +28,7 @@ export class UpsHandler {
|
||||
public async add(): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('readline');
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
@@ -44,9 +48,10 @@ export class UpsHandler {
|
||||
await this.runAddProcess(prompt);
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Add UPS error: ${error.message}`);
|
||||
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +69,7 @@ export class UpsHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Convert old format to new format if needed
|
||||
if (!config.upsDevices) {
|
||||
// Initialize with the current config as the first UPS
|
||||
@@ -72,12 +77,12 @@ export class UpsHandler {
|
||||
checkInterval: config.checkInterval,
|
||||
upsDevices: [{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds,
|
||||
groups: []
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
groups: [],
|
||||
actions: [],
|
||||
}],
|
||||
groups: []
|
||||
groups: [],
|
||||
};
|
||||
logger.log('Converting existing configuration to multi-UPS format.');
|
||||
}
|
||||
@@ -86,7 +91,7 @@ export class UpsHandler {
|
||||
config = {
|
||||
checkInterval: 30000, // Default check interval
|
||||
upsDevices: [],
|
||||
groups: []
|
||||
groups: [],
|
||||
};
|
||||
logger.log('No existing configuration found. Creating a new configuration.');
|
||||
}
|
||||
@@ -105,21 +110,19 @@ export class UpsHandler {
|
||||
community: 'public',
|
||||
version: 1,
|
||||
timeout: 5000,
|
||||
upsModel: 'cyberpower'
|
||||
upsModel: 'cyberpower' as TUpsModel,
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60,
|
||||
runtime: 20
|
||||
runtime: 20,
|
||||
},
|
||||
groups: []
|
||||
groups: [],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
// Gather SNMP settings
|
||||
await this.gatherSnmpSettings(newUps.snmp, prompt);
|
||||
|
||||
// Gather threshold settings
|
||||
await this.gatherThresholdSettings(newUps.thresholds, prompt);
|
||||
|
||||
// Gather UPS model settings
|
||||
await this.gatherUpsModelSettings(newUps.snmp, prompt);
|
||||
|
||||
@@ -131,20 +134,23 @@ export class UpsHandler {
|
||||
await groupHandler.assignUpsToGroups(newUps, config.groups, prompt);
|
||||
}
|
||||
|
||||
// Gather action settings
|
||||
await this.gatherActionSettings(newUps.actions, prompt);
|
||||
|
||||
// Add the new UPS to the config
|
||||
config.upsDevices.push(newUps);
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
await this.nupst.getDaemon().saveConfig(config as INupstConfig);
|
||||
|
||||
this.displayUpsConfigSummary(newUps);
|
||||
|
||||
// Test the connection if requested
|
||||
await this.optionallyTestConnection(newUps.snmp, prompt);
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
await this.restartServiceIfRunning();
|
||||
|
||||
|
||||
logger.log('\nSetup complete!');
|
||||
}
|
||||
|
||||
@@ -155,7 +161,7 @@ export class UpsHandler {
|
||||
public async edit(upsId?: string): Promise<void> {
|
||||
try {
|
||||
// Import readline module for user input
|
||||
const readline = await import('readline');
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
@@ -175,9 +181,10 @@ export class UpsHandler {
|
||||
await this.runEditProcess(upsId, prompt);
|
||||
} finally {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Edit UPS error: ${error.message}`);
|
||||
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,10 +193,13 @@ export class UpsHandler {
|
||||
* @param upsId ID of the UPS to edit (undefined for default UPS)
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
public async runEditProcess(upsId: string | undefined, prompt: (question: string) => Promise<string>): Promise<void> {
|
||||
public async runEditProcess(
|
||||
upsId: string | undefined,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
logger.log('\nNUPST Edit UPS');
|
||||
logger.log('=============\n');
|
||||
|
||||
|
||||
// Try to load existing config
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
@@ -205,29 +215,33 @@ export class UpsHandler {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get the config
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Convert old format to new format if needed
|
||||
if (!config.upsDevices) {
|
||||
// Initialize with the current config as the first UPS
|
||||
if (!config.snmp) {
|
||||
logger.error('Legacy configuration is missing required SNMP settings');
|
||||
return;
|
||||
}
|
||||
config.upsDevices = [{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds,
|
||||
groups: []
|
||||
groups: [],
|
||||
actions: [],
|
||||
}];
|
||||
config.groups = [];
|
||||
logger.log('Converting existing configuration to multi-UPS format.');
|
||||
}
|
||||
|
||||
|
||||
// Find the UPS to edit
|
||||
let upsToEdit;
|
||||
if (upsId) {
|
||||
// Find specific UPS by ID
|
||||
upsToEdit = config.upsDevices.find(ups => ups.id === upsId);
|
||||
upsToEdit = config.upsDevices.find((ups) => ups.id === upsId);
|
||||
if (!upsToEdit) {
|
||||
logger.error(`UPS with ID "${upsId}" not found.`);
|
||||
return;
|
||||
@@ -242,41 +256,46 @@ export class UpsHandler {
|
||||
upsToEdit = config.upsDevices[0];
|
||||
logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
|
||||
}
|
||||
|
||||
|
||||
// Allow editing UPS name
|
||||
const newName = await prompt(`UPS Name [${upsToEdit.name}]: `);
|
||||
if (newName.trim()) {
|
||||
upsToEdit.name = newName;
|
||||
}
|
||||
|
||||
|
||||
// Edit SNMP settings
|
||||
await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
|
||||
|
||||
// Edit threshold settings
|
||||
await this.gatherThresholdSettings(upsToEdit.thresholds, prompt);
|
||||
|
||||
|
||||
// Edit UPS model settings
|
||||
await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
|
||||
|
||||
|
||||
// Get access to GroupHandler for group assignments
|
||||
const groupHandler = this.nupst.getGroupHandler();
|
||||
|
||||
|
||||
// Edit group assignments
|
||||
if (config.groups && config.groups.length > 0) {
|
||||
await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt);
|
||||
}
|
||||
|
||||
|
||||
// Initialize actions array if not exists
|
||||
if (!upsToEdit.actions) {
|
||||
upsToEdit.actions = [];
|
||||
}
|
||||
|
||||
// Edit action settings
|
||||
await this.gatherActionSettings(upsToEdit.actions, prompt);
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
this.displayUpsConfigSummary(upsToEdit);
|
||||
|
||||
|
||||
// Test the connection if requested
|
||||
await this.optionallyTestConnection(upsToEdit.snmp, prompt);
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
await this.restartServiceIfRunning();
|
||||
|
||||
|
||||
logger.log('\nEdit complete!');
|
||||
}
|
||||
|
||||
@@ -284,7 +303,7 @@ export class UpsHandler {
|
||||
* Delete a UPS by ID
|
||||
* @param upsId ID of the UPS to delete
|
||||
*/
|
||||
public async delete(upsId: string): Promise<void> {
|
||||
public async remove(upsId: string): Promise<void> {
|
||||
try {
|
||||
// Try to load configuration
|
||||
try {
|
||||
@@ -297,58 +316,64 @@ export class UpsHandler {
|
||||
logger.logBoxEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if multi-UPS config
|
||||
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
||||
logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.');
|
||||
logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Find the UPS to delete
|
||||
const upsIndex = config.upsDevices.findIndex(ups => ups.id === upsId);
|
||||
const upsIndex = config.upsDevices.findIndex((ups) => ups.id === upsId);
|
||||
if (upsIndex === -1) {
|
||||
logger.error(`UPS with ID "${upsId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const upsToDelete = config.upsDevices[upsIndex];
|
||||
|
||||
|
||||
// Get confirmation before deleting
|
||||
const readline = await import('readline');
|
||||
const readline = await import('node:readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const confirm = await new Promise<string>(resolve => {
|
||||
rl.question(`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `, answer => {
|
||||
resolve(answer.toLowerCase());
|
||||
});
|
||||
|
||||
const confirm = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
|
||||
(answer) => {
|
||||
resolve(answer.toLowerCase());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
rl.close();
|
||||
|
||||
process.stdin.destroy();
|
||||
|
||||
if (confirm !== 'y' && confirm !== 'yes') {
|
||||
logger.log('Deletion cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Remove the UPS from the array
|
||||
config.upsDevices.splice(upsIndex, 1);
|
||||
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`);
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
await this.restartServiceIfRunning();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete UPS: ${error.message}`);
|
||||
logger.error(
|
||||
`Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,62 +386,73 @@ export class UpsHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
const errorBoxWidth = 45;
|
||||
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
||||
logger.logBoxLine('No configuration found.');
|
||||
logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
|
||||
logger.logBoxEnd();
|
||||
logger.logBox('Configuration Error', [
|
||||
'No configuration found.',
|
||||
"Please run 'nupst ups add' first to create a configuration.",
|
||||
], 50, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if multi-UPS config
|
||||
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
||||
// Legacy single UPS configuration
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
||||
logger.logBoxLine('Legacy single-UPS configuration detected.');
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Default UPS:');
|
||||
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
|
||||
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||
logger.logBoxLine(` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
|
||||
logger.logBoxLine('to the multi-UPS configuration format.');
|
||||
logger.logBoxEnd();
|
||||
logger.logBox('UPS Devices', [
|
||||
'Legacy single-UPS configuration detected.',
|
||||
'',
|
||||
...(!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');
|
||||
return;
|
||||
}
|
||||
|
||||
// Display UPS list
|
||||
const boxWidth = 60;
|
||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
||||
|
||||
|
||||
// Display UPS list with modern table
|
||||
if (config.upsDevices.length === 0) {
|
||||
logger.logBoxLine('No UPS devices configured.');
|
||||
logger.logBoxLine('Use "nupst add" to add a UPS device.');
|
||||
} else {
|
||||
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('ID | Name | Host | Mode | Groups');
|
||||
logger.logBoxLine('-----------+----------------------+----------------+--------------+----------------');
|
||||
|
||||
for (const ups of config.upsDevices) {
|
||||
const id = ups.id.padEnd(10, ' ').substring(0, 10);
|
||||
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
|
||||
const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15);
|
||||
const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12);
|
||||
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
|
||||
|
||||
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
|
||||
}
|
||||
logger.logBox('UPS Devices', [
|
||||
'No UPS devices configured.',
|
||||
'',
|
||||
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
|
||||
], 60, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Prepare table data
|
||||
const rows = config.upsDevices.map((ups) => ({
|
||||
id: ups.id,
|
||||
name: ups.name || '',
|
||||
host: `${ups.snmp.host}:${ups.snmp.port}`,
|
||||
model: ups.snmp.upsModel || 'cyberpower',
|
||||
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
|
||||
}));
|
||||
|
||||
const columns: ITableColumn[] = [
|
||||
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
|
||||
{ header: 'Name', key: 'name', align: 'left' },
|
||||
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||
{ header: 'Model', key: 'model', align: 'left' },
|
||||
{ header: 'Groups', key: 'groups', align: 'left' },
|
||||
];
|
||||
|
||||
logger.log('');
|
||||
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||
logger.log('');
|
||||
logger.logTable(columns, rows);
|
||||
logger.log('');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list UPS devices: ${error.message}`);
|
||||
logger.error(
|
||||
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +488,7 @@ export class UpsHandler {
|
||||
// Handle new multi-UPS configuration format
|
||||
if (config.upsDevices && config.upsDevices.length > 0) {
|
||||
logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`);
|
||||
|
||||
|
||||
for (let i = 0; i < config.upsDevices.length; i++) {
|
||||
const ups = config.upsDevices[i];
|
||||
logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`);
|
||||
@@ -465,7 +501,7 @@ export class UpsHandler {
|
||||
await this.testConnection(config);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Test failed: ${error.message}`);
|
||||
logger.error(`Test failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,15 +511,14 @@ export class UpsHandler {
|
||||
*/
|
||||
private displayTestConfig(config: any): void {
|
||||
// Check if this is a UPS device or full configuration
|
||||
const isUpsConfig = config.snmp && config.thresholds;
|
||||
const isUpsConfig = config.snmp;
|
||||
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
|
||||
const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {};
|
||||
const checkInterval = config.checkInterval || 30000;
|
||||
|
||||
|
||||
// Get UPS name and ID if available
|
||||
const upsName = config.name ? config.name : 'Default UPS';
|
||||
const upsId = config.id ? config.id : 'default';
|
||||
|
||||
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
|
||||
logger.logBoxLine(`UPS ID: ${upsId}`);
|
||||
@@ -516,18 +551,18 @@ export class UpsHandler {
|
||||
if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
|
||||
logger.logBoxLine('Custom OIDs:');
|
||||
logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`);
|
||||
logger.logBoxLine(` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`);
|
||||
logger.logBoxLine(
|
||||
` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||
);
|
||||
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||
}
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${thresholds.battery}%`);
|
||||
logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`);
|
||||
|
||||
// Show group assignments if this is a UPS config
|
||||
if (config.groups && Array.isArray(config.groups)) {
|
||||
logger.logBoxLine(`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`);
|
||||
logger.logBoxLine(
|
||||
`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
@@ -540,12 +575,11 @@ export class UpsHandler {
|
||||
const upsId = config.id || 'default';
|
||||
const upsName = config.name || 'Default UPS';
|
||||
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
||||
|
||||
|
||||
try {
|
||||
// Create a test config with a short timeout
|
||||
const snmpConfig = config.snmp ? config.snmp : config.snmp;
|
||||
const thresholds = config.thresholds ? config.thresholds : config.thresholds;
|
||||
|
||||
|
||||
const testConfig = {
|
||||
...snmpConfig,
|
||||
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
||||
@@ -561,14 +595,11 @@ export class UpsHandler {
|
||||
logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Check status against thresholds if on battery
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
this.analyzeThresholds(status, thresholds);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorBoxWidth = 45;
|
||||
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
|
||||
logger.logBoxLine(`Error: ${error.message}`);
|
||||
logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.logBoxEnd();
|
||||
logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS.");
|
||||
}
|
||||
@@ -586,26 +617,26 @@ export class UpsHandler {
|
||||
if (status.batteryCapacity < thresholds.battery) {
|
||||
logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
|
||||
logger.logBoxLine(
|
||||
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`
|
||||
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
|
||||
);
|
||||
logger.logBoxLine(' System would initiate shutdown');
|
||||
} else {
|
||||
logger.logBoxLine('✓ Battery capacity above threshold');
|
||||
logger.logBoxLine(
|
||||
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`
|
||||
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
|
||||
);
|
||||
}
|
||||
|
||||
if (status.batteryRuntime < thresholds.runtime) {
|
||||
logger.logBoxLine('⚠️ WARNING: Runtime below threshold');
|
||||
logger.logBoxLine(
|
||||
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`
|
||||
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
|
||||
);
|
||||
logger.logBoxLine(' System would initiate shutdown');
|
||||
} else {
|
||||
logger.logBoxLine('✓ Runtime above threshold');
|
||||
logger.logBoxLine(
|
||||
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`
|
||||
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -619,7 +650,7 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherSnmpSettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// SNMP IP Address
|
||||
const defaultHost = snmpConfig.host || '127.0.0.1';
|
||||
@@ -634,16 +665,16 @@ export class UpsHandler {
|
||||
|
||||
// SNMP Version
|
||||
const defaultVersion = snmpConfig.version || 1;
|
||||
console.log('\nSNMP Version:');
|
||||
console.log(' 1) SNMPv1');
|
||||
console.log(' 2) SNMPv2c');
|
||||
console.log(' 3) SNMPv3 (with security features)');
|
||||
logger.log('');
|
||||
logger.info('SNMP Version:');
|
||||
logger.dim(' 1) SNMPv1');
|
||||
logger.dim(' 2) SNMPv2c');
|
||||
logger.dim(' 3) SNMPv3 (with security features)');
|
||||
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
|
||||
const version = parseInt(versionInput, 10);
|
||||
snmpConfig.version =
|
||||
versionInput.trim() && (version === 1 || version === 2 || version === 3)
|
||||
? version
|
||||
: defaultVersion;
|
||||
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
|
||||
? version
|
||||
: defaultVersion;
|
||||
|
||||
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
|
||||
// SNMP Community String (for v1/v2c)
|
||||
@@ -663,15 +694,17 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherSnmpV3Settings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
console.log('\nSNMPv3 Security Settings:');
|
||||
logger.log('');
|
||||
logger.info('SNMPv3 Security Settings:');
|
||||
|
||||
// Security Level
|
||||
console.log('\nSecurity Level:');
|
||||
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
|
||||
console.log(' 2) authNoPriv (Authentication, No Privacy)');
|
||||
console.log(' 3) authPriv (Authentication and Privacy)');
|
||||
logger.log('');
|
||||
logger.info('Security Level:');
|
||||
logger.dim(' 1) noAuthNoPriv (No Authentication, No Privacy)');
|
||||
logger.dim(' 2) authNoPriv (Authentication, No Privacy)');
|
||||
logger.dim(' 3) authPriv (Authentication and Privacy)');
|
||||
const defaultSecLevel = snmpConfig.securityLevel
|
||||
? snmpConfig.securityLevel === 'noAuthNoPriv'
|
||||
? 1
|
||||
@@ -720,8 +753,9 @@ export class UpsHandler {
|
||||
|
||||
// Allow customizing the timeout value
|
||||
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
|
||||
console.log(
|
||||
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.'
|
||||
logger.log('');
|
||||
logger.info(
|
||||
'SNMPv3 operations with authentication and privacy may require longer timeouts.',
|
||||
);
|
||||
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
@@ -738,15 +772,16 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherAuthenticationSettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Authentication protocol
|
||||
console.log('\nAuthentication Protocol:');
|
||||
console.log(' 1) MD5');
|
||||
console.log(' 2) SHA');
|
||||
logger.log('');
|
||||
logger.info('Authentication Protocol:');
|
||||
logger.dim(' 1) MD5');
|
||||
logger.dim(' 2) SHA');
|
||||
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
|
||||
const authProtocolInput = await prompt(
|
||||
`Select Authentication Protocol [${defaultAuthProtocol}]: `
|
||||
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
|
||||
);
|
||||
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
|
||||
snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
|
||||
@@ -764,12 +799,13 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherPrivacySettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Privacy protocol
|
||||
console.log('\nPrivacy Protocol:');
|
||||
console.log(' 1) DES');
|
||||
console.log(' 2) AES');
|
||||
logger.log('');
|
||||
logger.info('Privacy Protocol:');
|
||||
logger.dim(' 1) DES');
|
||||
logger.dim(' 2) AES');
|
||||
const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
|
||||
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
|
||||
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
|
||||
@@ -781,40 +817,6 @@ export class UpsHandler {
|
||||
snmpConfig.privKey = privKey.trim() || defaultPrivKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather threshold settings
|
||||
* @param thresholds Thresholds configuration object to update
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherThresholdSettings(
|
||||
thresholds: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
): Promise<void> {
|
||||
console.log('\nShutdown Thresholds:');
|
||||
|
||||
// Battery threshold
|
||||
const defaultBatteryThreshold = thresholds.battery || 60;
|
||||
const batteryThresholdInput = await prompt(
|
||||
`Battery percentage threshold [${defaultBatteryThreshold}%]: `
|
||||
);
|
||||
const batteryThreshold = parseInt(batteryThresholdInput, 10);
|
||||
thresholds.battery =
|
||||
batteryThresholdInput.trim() && !isNaN(batteryThreshold)
|
||||
? batteryThreshold
|
||||
: defaultBatteryThreshold;
|
||||
|
||||
// Runtime threshold
|
||||
const defaultRuntimeThreshold = thresholds.runtime || 20;
|
||||
const runtimeThresholdInput = await prompt(
|
||||
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `
|
||||
);
|
||||
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
|
||||
thresholds.runtime =
|
||||
runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
|
||||
? runtimeThreshold
|
||||
: defaultRuntimeThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather UPS model settings
|
||||
* @param snmpConfig SNMP configuration object to update
|
||||
@@ -822,30 +824,30 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherUpsModelSettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
console.log('\nUPS Model Selection:');
|
||||
console.log(' 1) CyberPower');
|
||||
console.log(' 2) APC');
|
||||
console.log(' 3) Eaton');
|
||||
console.log(' 4) TrippLite');
|
||||
console.log(' 5) Liebert/Vertiv');
|
||||
console.log(' 6) Custom (Advanced)');
|
||||
logger.log('');
|
||||
logger.info('UPS Model Selection:');
|
||||
logger.dim(' 1) CyberPower');
|
||||
logger.dim(' 2) APC');
|
||||
logger.dim(' 3) Eaton');
|
||||
logger.dim(' 4) TrippLite');
|
||||
logger.dim(' 5) Liebert/Vertiv');
|
||||
logger.dim(' 6) Custom (Advanced)');
|
||||
|
||||
const defaultModelValue =
|
||||
snmpConfig.upsModel === 'cyberpower'
|
||||
? 1
|
||||
: snmpConfig.upsModel === 'apc'
|
||||
? 2
|
||||
: snmpConfig.upsModel === 'eaton'
|
||||
? 3
|
||||
: snmpConfig.upsModel === 'tripplite'
|
||||
? 4
|
||||
: snmpConfig.upsModel === 'liebert'
|
||||
? 5
|
||||
: snmpConfig.upsModel === 'custom'
|
||||
? 6
|
||||
: 1;
|
||||
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
|
||||
? 1
|
||||
: snmpConfig.upsModel === 'apc'
|
||||
? 2
|
||||
: snmpConfig.upsModel === 'eaton'
|
||||
? 3
|
||||
: snmpConfig.upsModel === 'tripplite'
|
||||
? 4
|
||||
: snmpConfig.upsModel === 'liebert'
|
||||
? 5
|
||||
: snmpConfig.upsModel === 'custom'
|
||||
? 6
|
||||
: 1;
|
||||
|
||||
const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
|
||||
const modelValue = parseInt(modelInput, 10) || defaultModelValue;
|
||||
@@ -862,8 +864,9 @@ export class UpsHandler {
|
||||
snmpConfig.upsModel = 'liebert';
|
||||
} else if (modelValue === 6) {
|
||||
snmpConfig.upsModel = 'custom';
|
||||
console.log('\nEnter custom OIDs for your UPS:');
|
||||
console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
||||
logger.log('');
|
||||
logger.info('Enter custom OIDs for your UPS:');
|
||||
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
||||
|
||||
// Custom OIDs
|
||||
const powerStatusOID = await prompt('Power Status OID: ');
|
||||
@@ -879,6 +882,151 @@ export class UpsHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather action configuration settings
|
||||
* @param actions Actions array to configure
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherActionSettings(
|
||||
actions: any[],
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
logger.log('');
|
||||
logger.info('Action Configuration (Optional):');
|
||||
logger.dim('Actions are triggered on power status changes and threshold violations.');
|
||||
logger.dim('Leave empty to use default shutdown behavior on threshold violations.');
|
||||
|
||||
const configureActions = await prompt('Configure custom actions? (y/N): ');
|
||||
if (configureActions.toLowerCase() !== 'y') {
|
||||
return; // Keep existing actions or use default
|
||||
}
|
||||
|
||||
// Clear existing actions
|
||||
actions.length = 0;
|
||||
|
||||
let addMore = true;
|
||||
while (addMore) {
|
||||
logger.log('');
|
||||
logger.info('Action Type:');
|
||||
logger.dim(' 1) Shutdown (system shutdown)');
|
||||
logger.dim(' 2) Webhook (HTTP notification)');
|
||||
logger.dim(' 3) Custom Script (run .sh file from /etc/nupst)');
|
||||
|
||||
const typeInput = await prompt('Select action type [1]: ');
|
||||
const typeValue = parseInt(typeInput, 10) || 1;
|
||||
|
||||
const action: any = {};
|
||||
|
||||
if (typeValue === 1) {
|
||||
// Shutdown action
|
||||
action.type = 'shutdown';
|
||||
|
||||
const delayInput = await prompt('Shutdown delay in minutes [5]: ');
|
||||
const delay = parseInt(delayInput, 10);
|
||||
if (delayInput.trim() && !isNaN(delay)) {
|
||||
action.shutdownDelay = delay;
|
||||
}
|
||||
} else if (typeValue === 2) {
|
||||
// Webhook action
|
||||
action.type = 'webhook';
|
||||
|
||||
const url = await prompt('Webhook URL: ');
|
||||
if (!url.trim()) {
|
||||
logger.warn('Webhook URL required, skipping action');
|
||||
continue;
|
||||
}
|
||||
action.webhookUrl = url.trim();
|
||||
|
||||
logger.log('');
|
||||
logger.info('HTTP Method:');
|
||||
logger.dim(' 1) POST (JSON body)');
|
||||
logger.dim(' 2) GET (query parameters)');
|
||||
const methodInput = await prompt('Select method [1]: ');
|
||||
action.webhookMethod = methodInput === '2' ? 'GET' : 'POST';
|
||||
|
||||
const timeoutInput = await prompt('Timeout in seconds [10]: ');
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(timeout)) {
|
||||
action.webhookTimeout = timeout * 1000; // Convert to ms
|
||||
}
|
||||
} else if (typeValue === 3) {
|
||||
// Script action
|
||||
action.type = 'script';
|
||||
|
||||
const scriptPath = await prompt('Script filename (in /etc/nupst/, must end with .sh): ');
|
||||
if (!scriptPath.trim() || !scriptPath.trim().endsWith('.sh')) {
|
||||
logger.warn('Script path must end with .sh, skipping action');
|
||||
continue;
|
||||
}
|
||||
action.scriptPath = scriptPath.trim();
|
||||
|
||||
const timeoutInput = await prompt('Script timeout in seconds [60]: ');
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(timeout)) {
|
||||
action.scriptTimeout = timeout * 1000; // Convert to ms
|
||||
}
|
||||
} else {
|
||||
logger.warn('Invalid action type, skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Configure trigger mode (applies to all action types)
|
||||
logger.log('');
|
||||
logger.info('Trigger Mode:');
|
||||
logger.dim(' 1) Power changes + thresholds (default)');
|
||||
logger.dim(' 2) Only power status changes');
|
||||
logger.dim(' 3) Only threshold violations');
|
||||
logger.dim(' 4) Any change (every ~30s check)');
|
||||
const triggerInput = await prompt('Select trigger mode [1]: ');
|
||||
const triggerValue = parseInt(triggerInput, 10) || 1;
|
||||
|
||||
switch (triggerValue) {
|
||||
case 2:
|
||||
action.triggerMode = 'onlyPowerChanges';
|
||||
break;
|
||||
case 3:
|
||||
action.triggerMode = 'onlyThresholds';
|
||||
break;
|
||||
case 4:
|
||||
action.triggerMode = 'anyChange';
|
||||
break;
|
||||
default:
|
||||
action.triggerMode = 'powerChangesAndThresholds';
|
||||
}
|
||||
|
||||
// Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes
|
||||
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)');
|
||||
|
||||
const batteryInput = await prompt('Battery threshold percentage [60]: ');
|
||||
const battery = parseInt(batteryInput, 10);
|
||||
const batteryThreshold = (batteryInput.trim() && !isNaN(battery)) ? battery : 60;
|
||||
|
||||
const runtimeInput = await prompt('Runtime threshold in minutes [20]: ');
|
||||
const runtime = parseInt(runtimeInput, 10);
|
||||
const runtimeThreshold = (runtimeInput.trim() && !isNaN(runtime)) ? runtime : 20;
|
||||
|
||||
action.thresholds = {
|
||||
battery: batteryThreshold,
|
||||
runtime: runtimeThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
actions.push(action);
|
||||
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';
|
||||
}
|
||||
|
||||
if (actions.length > 0) {
|
||||
logger.log('');
|
||||
logger.success(`${actions.length} action(s) configured`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display UPS configuration summary
|
||||
* @param ups UPS configuration
|
||||
@@ -891,9 +1039,7 @@ export class UpsHandler {
|
||||
logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
||||
logger.logBoxLine(
|
||||
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`
|
||||
);
|
||||
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
||||
} else {
|
||||
@@ -910,10 +1056,10 @@ export class UpsHandler {
|
||||
*/
|
||||
private async optionallyTestConnection(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
const testConnection = await prompt(
|
||||
'Would you like to test the connection to your UPS? (y/N): '
|
||||
'Would you like to test the connection to your UPS? (y/N): ',
|
||||
);
|
||||
if (testConnection.toLowerCase() === 'y') {
|
||||
logger.log('\nTesting connection to UPS...');
|
||||
@@ -937,7 +1083,7 @@ export class UpsHandler {
|
||||
const errorBoxWidth = 45;
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Connection Failed!', errorBoxWidth);
|
||||
logger.logBoxLine(`Error: ${error.message}`);
|
||||
logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.logBoxEnd();
|
||||
logger.log('\nPlease check your settings and try again.');
|
||||
}
|
||||
@@ -948,7 +1094,7 @@ export class UpsHandler {
|
||||
* Check if the systemd service is running and restart it if it is
|
||||
* This is useful after configuration changes
|
||||
*/
|
||||
public async restartServiceIfRunning(): Promise<void> {
|
||||
public restartServiceIfRunning(): void {
|
||||
try {
|
||||
// Check if the service is active
|
||||
const isActive =
|
||||
@@ -972,7 +1118,9 @@ export class UpsHandler {
|
||||
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.logBoxLine(`Error restarting service: ${error.message}`);
|
||||
logger.logBoxLine(
|
||||
`Error restarting service: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.logBoxLine('You may need to restart the service manually:');
|
||||
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
||||
}
|
||||
@@ -983,4 +1131,4 @@ export class UpsHandler {
|
||||
// Ignore errors checking service status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
88
ts/colors.ts
Normal file
88
ts/colors.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Color theme and styling utilities for NUPST CLI
|
||||
* Uses Deno standard library colors module
|
||||
*/
|
||||
import * as colors from '@std/fmt/colors';
|
||||
|
||||
/**
|
||||
* Color theme for consistent CLI styling
|
||||
*/
|
||||
export const theme = {
|
||||
// Message types
|
||||
error: colors.red,
|
||||
warning: colors.yellow,
|
||||
success: colors.green,
|
||||
info: colors.cyan,
|
||||
dim: colors.dim,
|
||||
highlight: colors.bold,
|
||||
|
||||
// Status indicators
|
||||
statusActive: (text: string) => colors.green(colors.bold(text)),
|
||||
statusInactive: (text: string) => colors.red(text),
|
||||
statusWarning: (text: string) => colors.yellow(text),
|
||||
statusUnknown: (text: string) => colors.dim(text),
|
||||
|
||||
// Battery level colors
|
||||
batteryGood: colors.green, // > 60%
|
||||
batteryMedium: colors.yellow, // 30-60%
|
||||
batteryCritical: colors.red, // < 30%
|
||||
|
||||
// Box borders
|
||||
borderSuccess: colors.green,
|
||||
borderError: colors.red,
|
||||
borderWarning: colors.yellow,
|
||||
borderInfo: colors.cyan,
|
||||
borderDefault: (text: string) => text, // No color
|
||||
|
||||
// Command/code highlighting
|
||||
command: colors.cyan,
|
||||
code: colors.dim,
|
||||
path: colors.blue,
|
||||
};
|
||||
|
||||
/**
|
||||
* Status symbols with colors
|
||||
*/
|
||||
export const symbols = {
|
||||
success: colors.green('✓'),
|
||||
error: colors.red('✗'),
|
||||
warning: colors.yellow('⚠'),
|
||||
info: colors.cyan('ℹ'),
|
||||
running: colors.green('●'),
|
||||
stopped: colors.red('○'),
|
||||
starting: colors.yellow('◐'),
|
||||
unknown: colors.dim('◯'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Get color for battery level
|
||||
*/
|
||||
export function getBatteryColor(percentage: number): (text: string) => string {
|
||||
if (percentage >= 60) return theme.batteryGood;
|
||||
if (percentage >= 30) return theme.batteryMedium;
|
||||
return theme.batteryCritical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for runtime remaining
|
||||
*/
|
||||
export function getRuntimeColor(minutes: number): (text: string) => string {
|
||||
if (minutes >= 20) return theme.batteryGood;
|
||||
if (minutes >= 10) return theme.batteryMedium;
|
||||
return theme.batteryCritical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format UPS power status with color
|
||||
*/
|
||||
export function formatPowerStatus(status: 'online' | 'onBattery' | 'unknown'): string {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return theme.success('Online');
|
||||
case 'onBattery':
|
||||
return theme.warning('On Battery');
|
||||
case 'unknown':
|
||||
default:
|
||||
return theme.dim('Unknown');
|
||||
}
|
||||
}
|
872
ts/daemon.ts
872
ts/daemon.ts
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
export * from './shortid.js';
|
||||
export * from './shortid.ts';
|
||||
|
@@ -5,11 +5,11 @@
|
||||
export function shortId(): string {
|
||||
// Define the character set: a-z, A-Z, 0-9
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
|
||||
|
||||
// Generate cryptographically secure random values
|
||||
const randomValues = new Uint8Array(6);
|
||||
crypto.getRandomValues(randomValues);
|
||||
|
||||
|
||||
// Map each random value to a character in our set
|
||||
let result = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
@@ -17,6 +17,6 @@ export function shortId(): string {
|
||||
const index = randomValues[i] % chars.length;
|
||||
result += chars[index];
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { NupstCli } from './cli.js';
|
||||
import { logger } from './logger.js';
|
||||
import { NupstCli } from './cli.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import process from 'node:process';
|
||||
|
||||
/**
|
||||
* Main entry point for NUPST
|
||||
@@ -13,7 +14,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// Run the main function and handle any errors
|
||||
main().catch(error => {
|
||||
main().catch((error) => {
|
||||
logger.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
244
ts/logger.ts
244
ts/logger.ts
@@ -1,11 +1,40 @@
|
||||
import { theme, symbols } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Table column alignment options
|
||||
*/
|
||||
export type TColumnAlign = 'left' | 'right' | 'center';
|
||||
|
||||
/**
|
||||
* Table column definition
|
||||
*/
|
||||
export interface ITableColumn {
|
||||
/** Column header text */
|
||||
header: string;
|
||||
/** Column key in data object */
|
||||
key: string;
|
||||
/** Column alignment (default: left) */
|
||||
align?: TColumnAlign;
|
||||
/** Column width (auto-calculated if not specified) */
|
||||
width?: number;
|
||||
/** Color function to apply to cell values */
|
||||
color?: (value: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Box style types with colors
|
||||
*/
|
||||
export type TBoxStyle = 'default' | 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
/**
|
||||
* A simple logger class that provides consistent formatting for log messages
|
||||
* including support for logboxes with title, lines, and closing
|
||||
*/
|
||||
export class Logger {
|
||||
private currentBoxWidth: number | null = null;
|
||||
private currentBoxStyle: TBoxStyle = 'default';
|
||||
private static instance: Logger;
|
||||
|
||||
|
||||
/** Default width to use when no width is specified */
|
||||
private readonly DEFAULT_WIDTH = 60;
|
||||
|
||||
@@ -36,45 +65,92 @@ export class Logger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
* Log an error message (red with ✗ symbol)
|
||||
* @param message Error message to log
|
||||
*/
|
||||
public error(message: string): void {
|
||||
console.error(message);
|
||||
console.error(`${symbols.error} ${theme.error(message)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message with a warning emoji
|
||||
* Log a warning message (yellow with ⚠ symbol)
|
||||
* @param message Warning message to log
|
||||
*/
|
||||
public warn(message: string): void {
|
||||
console.warn(`⚠️ ${message}`);
|
||||
console.warn(`${symbols.warning} ${theme.warning(message)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a success message with a checkmark
|
||||
* Log a success message (green with ✓ symbol)
|
||||
* @param message Success message to log
|
||||
*/
|
||||
public success(message: string): void {
|
||||
console.log(`✓ ${message}`);
|
||||
console.log(`${symbols.success} ${theme.success(message)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an info message (cyan with ℹ symbol)
|
||||
* @param message Info message to log
|
||||
*/
|
||||
public info(message: string): void {
|
||||
console.log(`${symbols.info} ${theme.info(message)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a dim/secondary message
|
||||
* @param message Message to log in dim style
|
||||
*/
|
||||
public dim(message: string): void {
|
||||
console.log(theme.dim(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a highlighted/bold message
|
||||
* @param message Message to highlight
|
||||
*/
|
||||
public highlight(message: string): void {
|
||||
console.log(theme.highlight(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color function for box based on style
|
||||
*/
|
||||
private getBoxColor(style: TBoxStyle): (text: string) => string {
|
||||
switch (style) {
|
||||
case 'success':
|
||||
return theme.borderSuccess;
|
||||
case 'error':
|
||||
return theme.borderError;
|
||||
case 'warning':
|
||||
return theme.borderWarning;
|
||||
case 'info':
|
||||
return theme.borderInfo;
|
||||
case 'default':
|
||||
default:
|
||||
return theme.borderDefault;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a logbox title and set the current box width
|
||||
* @param title Title of the logbox
|
||||
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
||||
* @param style Box style for coloring (default, success, error, warning, info)
|
||||
*/
|
||||
public logBoxTitle(title: string, width?: number): void {
|
||||
public logBoxTitle(title: string, width?: number, style?: TBoxStyle): void {
|
||||
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
||||
|
||||
this.currentBoxStyle = style || 'default';
|
||||
|
||||
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||
|
||||
// Create the title line with appropriate padding
|
||||
const paddedTitle = ` ${title} `;
|
||||
const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length;
|
||||
|
||||
|
||||
// Title line: ┌─ Title ───┐
|
||||
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
||||
|
||||
console.log(titleLine);
|
||||
|
||||
console.log(colorFn(titleLine));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,19 +163,23 @@ export class Logger {
|
||||
// No current width and no width provided, use default width
|
||||
this.logBoxTitle('', this.DEFAULT_WIDTH);
|
||||
}
|
||||
|
||||
|
||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||
|
||||
// Calculate the available space for content
|
||||
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||
|
||||
// Calculate the available space for content (use visible length)
|
||||
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||
|
||||
if (content.length <= availableSpace - 1) {
|
||||
const visibleLen = this.visibleLength(content);
|
||||
|
||||
if (visibleLen <= availableSpace - 1) {
|
||||
// If content fits with at least one space for the right border stripe
|
||||
const padding = availableSpace - content.length - 1;
|
||||
console.log(`│ ${content}${' '.repeat(padding)}│`);
|
||||
const padding = availableSpace - visibleLen - 1;
|
||||
const line = `│ ${content}${' '.repeat(padding)}│`;
|
||||
console.log(colorFn(line));
|
||||
} else {
|
||||
// Content is too long, let it flow out of boundaries.
|
||||
console.log(`│ ${content}`);
|
||||
const line = `│ ${content}`;
|
||||
console.log(colorFn(line));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,12 +189,15 @@ export class Logger {
|
||||
*/
|
||||
public logBoxEnd(width?: number): void {
|
||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||
|
||||
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||
|
||||
// Create the bottom border: └────────┘
|
||||
console.log(`└${'─'.repeat(boxWidth - 2)}┘`);
|
||||
|
||||
// Reset the current box width
|
||||
const bottomLine = `└${'─'.repeat(boxWidth - 2)}┘`;
|
||||
console.log(colorFn(bottomLine));
|
||||
|
||||
// Reset the current box width and style
|
||||
this.currentBoxWidth = null;
|
||||
this.currentBoxStyle = 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,14 +205,15 @@ export class Logger {
|
||||
* @param title Title of the logbox
|
||||
* @param lines Array of content lines
|
||||
* @param width Width of the logbox, defaults to DEFAULT_WIDTH
|
||||
* @param style Box style for coloring
|
||||
*/
|
||||
public logBox(title: string, lines: string[], width?: number): void {
|
||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
|
||||
|
||||
public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void {
|
||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style);
|
||||
|
||||
for (const line of lines) {
|
||||
this.logBoxLine(line);
|
||||
}
|
||||
|
||||
|
||||
this.logBoxEnd();
|
||||
}
|
||||
|
||||
@@ -141,7 +225,109 @@ export class Logger {
|
||||
public logDivider(width?: number, character: string = '─'): void {
|
||||
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip ANSI color codes from string for accurate length calculation
|
||||
*/
|
||||
private stripAnsi(text: string): string {
|
||||
// Remove ANSI escape codes
|
||||
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible length of string (excluding ANSI codes)
|
||||
*/
|
||||
private visibleLength(text: string): number {
|
||||
return this.stripAnsi(text).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Align text within a column (handles ANSI color codes correctly)
|
||||
*/
|
||||
private alignText(text: string, width: number, align: TColumnAlign = 'left'): string {
|
||||
const visibleLen = this.visibleLength(text);
|
||||
|
||||
if (visibleLen >= width) {
|
||||
// Text is too long, truncate the visible part
|
||||
const stripped = this.stripAnsi(text);
|
||||
return stripped.substring(0, width);
|
||||
}
|
||||
|
||||
const padding = width - visibleLen;
|
||||
|
||||
switch (align) {
|
||||
case 'right':
|
||||
return ' '.repeat(padding) + text;
|
||||
case 'center': {
|
||||
const leftPad = Math.floor(padding / 2);
|
||||
const rightPad = padding - leftPad;
|
||||
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
||||
}
|
||||
case 'left':
|
||||
default:
|
||||
return text + ' '.repeat(padding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a formatted table
|
||||
* @param columns Column definitions
|
||||
* @param rows Array of data objects
|
||||
* @param title Optional table title
|
||||
*/
|
||||
public logTable(columns: ITableColumn[], rows: Record<string, string>[], title?: string): void {
|
||||
if (rows.length === 0) {
|
||||
this.dim('No data to display');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate column widths
|
||||
const columnWidths = columns.map((col) => {
|
||||
if (col.width) return col.width;
|
||||
|
||||
// Auto-calculate width based on header and data (use visible length)
|
||||
let maxWidth = this.visibleLength(col.header);
|
||||
for (const row of rows) {
|
||||
const value = String(row[col.key] || '');
|
||||
maxWidth = Math.max(maxWidth, this.visibleLength(value));
|
||||
}
|
||||
return maxWidth;
|
||||
});
|
||||
|
||||
// Calculate total table width
|
||||
const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + (columns.length * 3) + 1;
|
||||
|
||||
// Print title if provided
|
||||
if (title) {
|
||||
this.logBoxTitle(title, totalWidth);
|
||||
} else {
|
||||
// Print top border
|
||||
console.log('┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐');
|
||||
}
|
||||
|
||||
// Print header row
|
||||
const headerCells = columns.map((col, i) =>
|
||||
theme.highlight(this.alignText(col.header, columnWidths[i], col.align))
|
||||
);
|
||||
console.log('│ ' + headerCells.join(' │ ') + ' │');
|
||||
|
||||
// Print separator
|
||||
console.log('├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤');
|
||||
|
||||
// Print data rows
|
||||
for (const row of rows) {
|
||||
const cells = columns.map((col, i) => {
|
||||
const value = String(row[col.key] || '');
|
||||
const aligned = this.alignText(value, columnWidths[i], col.align);
|
||||
return col.color ? col.color(aligned) : aligned;
|
||||
});
|
||||
console.log('│ ' + cells.join(' │ ') + ' │');
|
||||
}
|
||||
|
||||
// Print bottom border
|
||||
console.log('└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘');
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance for easy use
|
||||
export const logger = Logger.getInstance();
|
||||
export const logger = Logger.getInstance();
|
||||
|
67
ts/migrations/base-migration.ts
Normal file
67
ts/migrations/base-migration.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Abstract base class for configuration migrations
|
||||
*
|
||||
* Each migration represents an upgrade from one config version to another.
|
||||
* Migrations run in order based on the `order` field, allowing users to jump
|
||||
* multiple versions (e.g., v1 → v4 runs migrations 2, 3, and 4).
|
||||
*/
|
||||
/**
|
||||
* Abstract base class for configuration migrations
|
||||
*
|
||||
* Each migration represents an upgrade from one config version to another.
|
||||
* Migrations run in order based on the `toVersion` field, allowing users to jump
|
||||
* multiple versions (e.g., v1 → v4 runs migrations 2, 3, and 4).
|
||||
*/
|
||||
export abstract class BaseMigration {
|
||||
/**
|
||||
* Source version this migration upgrades from
|
||||
* e.g., "1.x", "3.x"
|
||||
*/
|
||||
abstract readonly fromVersion: string;
|
||||
|
||||
/**
|
||||
* Target version this migration upgrades to
|
||||
* e.g., "2.0", "4.0", "4.1"
|
||||
*/
|
||||
abstract readonly toVersion: string;
|
||||
|
||||
/**
|
||||
* Check if this migration should run on the given config
|
||||
*
|
||||
* @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>;
|
||||
|
||||
/**
|
||||
* Perform the migration on the given config
|
||||
*
|
||||
* @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>>;
|
||||
|
||||
/**
|
||||
* Get human-readable name for this migration
|
||||
*
|
||||
* @returns Migration name
|
||||
*/
|
||||
getName(): string {
|
||||
return `Migration ${this.fromVersion} → ${this.toVersion}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse version string into a comparable number
|
||||
* Supports formats like "2.0", "4.1", etc.
|
||||
* Returns a number like 2.0, 4.1 for sorting
|
||||
*
|
||||
* @returns Parsed version number for ordering
|
||||
*/
|
||||
getVersionOrder(): number {
|
||||
const parsed = parseFloat(this.toVersion);
|
||||
if (isNaN(parsed)) {
|
||||
throw new Error(`Invalid version format: ${this.toVersion}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
}
|
11
ts/migrations/index.ts
Normal file
11
ts/migrations/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Configuration migrations module
|
||||
*
|
||||
* Exports the migration system for upgrading configs between versions.
|
||||
*/
|
||||
|
||||
export { BaseMigration } from './base-migration.ts';
|
||||
export { MigrationRunner } from './migration-runner.ts';
|
||||
export { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||
export { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
||||
export { MigrationV4_0ToV4_1 } from './migration-v4.0-to-v4.1.ts';
|
75
ts/migrations/migration-runner.ts
Normal file
75
ts/migrations/migration-runner.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||
import { MigrationV3ToV4 } from './migration-v3-to-v4.ts';
|
||||
import { MigrationV4_0ToV4_1 } from './migration-v4.0-to-v4.1.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration runner
|
||||
*
|
||||
* Discovers all available migrations, sorts them by order,
|
||||
* and runs applicable migrations in sequence.
|
||||
*/
|
||||
export class MigrationRunner {
|
||||
private migrations: BaseMigration[];
|
||||
|
||||
constructor() {
|
||||
// Register all migrations here
|
||||
this.migrations = [
|
||||
new MigrationV1ToV2(),
|
||||
new MigrationV3ToV4(),
|
||||
new MigrationV4_0ToV4_1(),
|
||||
// Add future migrations here (v4.3, v4.4, etc.)
|
||||
];
|
||||
|
||||
// Sort by version order to ensure they run in sequence
|
||||
this.migrations.sort((a, b) => a.getVersionOrder() - b.getVersionOrder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all applicable migrations on the config
|
||||
*
|
||||
* @param config - Raw configuration object to migrate
|
||||
* @returns Migrated configuration and whether migrations ran
|
||||
*/
|
||||
async run(
|
||||
config: Record<string, unknown>,
|
||||
): Promise<{ config: Record<string, unknown>; migrated: boolean }> {
|
||||
let currentConfig = config;
|
||||
let anyMigrationsRan = false;
|
||||
|
||||
for (const migration of this.migrations) {
|
||||
const shouldRun = await migration.shouldRun(currentConfig);
|
||||
|
||||
if (shouldRun) {
|
||||
// Only show "checking" message when we actually need to migrate
|
||||
if (!anyMigrationsRan) {
|
||||
logger.dim('Checking for required config migrations...');
|
||||
}
|
||||
logger.info(`Running ${migration.getName()}...`);
|
||||
currentConfig = await migration.migrate(currentConfig);
|
||||
anyMigrationsRan = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyMigrationsRan) {
|
||||
logger.success('Configuration migrations complete');
|
||||
} else {
|
||||
logger.success('config format ok');
|
||||
}
|
||||
|
||||
return {
|
||||
config: currentConfig,
|
||||
migrated: anyMigrationsRan,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered migrations
|
||||
*
|
||||
* @returns Array of all migrations sorted by order
|
||||
*/
|
||||
getMigrations(): BaseMigration[] {
|
||||
return [...this.migrations];
|
||||
}
|
||||
}
|
55
ts/migrations/migration-v1-to-v2.ts
Normal file
55
ts/migrations/migration-v1-to-v2.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration from v1 (single SNMP config) to v2 (upsDevices array)
|
||||
*
|
||||
* Detects old format:
|
||||
* {
|
||||
* snmp: { ... },
|
||||
* thresholds: { ... },
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*
|
||||
* Converts to:
|
||||
* {
|
||||
* version: "2.0",
|
||||
* upsDevices: [{ id: "default", name: "Default UPS", snmp: ..., thresholds: ... }],
|
||||
* groups: [],
|
||||
* checkInterval: 30000
|
||||
* }
|
||||
*/
|
||||
export class MigrationV1ToV2 extends BaseMigration {
|
||||
readonly fromVersion = '1.x';
|
||||
readonly toVersion = '2.0';
|
||||
|
||||
async shouldRun(config: any): Promise<boolean> {
|
||||
// V1 format has snmp field directly at root, no upsDevices or upsList
|
||||
return !!config.snmp && !config.upsDevices && !config.upsList;
|
||||
}
|
||||
|
||||
async migrate(config: any): Promise<any> {
|
||||
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
|
||||
|
||||
const migrated = {
|
||||
version: this.toVersion,
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds || {
|
||||
battery: 60,
|
||||
runtime: 20,
|
||||
},
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(`${this.getName()}: Migration complete`);
|
||||
return migrated;
|
||||
}
|
||||
}
|
118
ts/migrations/migration-v3-to-v4.ts
Normal file
118
ts/migrations/migration-v3-to-v4.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration from v3 (upsList) to v4 (upsDevices)
|
||||
*
|
||||
* Transforms v3 format with flat SNMP config:
|
||||
* {
|
||||
* upsList: [
|
||||
* {
|
||||
* id: "ups-1",
|
||||
* name: "UPS 1",
|
||||
* host: "192.168.1.1",
|
||||
* port: 161,
|
||||
* community: "public",
|
||||
* version: "1" // string
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* To v4 format with nested SNMP config:
|
||||
* {
|
||||
* version: "4.0",
|
||||
* upsDevices: [
|
||||
* {
|
||||
* id: "ups-1",
|
||||
* name: "UPS 1",
|
||||
* snmp: {
|
||||
* host: "192.168.1.1",
|
||||
* port: 161,
|
||||
* community: "public",
|
||||
* version: 1, // number
|
||||
* timeout: 5000
|
||||
* },
|
||||
* thresholds: { battery: 60, runtime: 20 },
|
||||
* groups: []
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export class MigrationV3ToV4 extends BaseMigration {
|
||||
readonly fromVersion = '3.x';
|
||||
readonly toVersion = '4.0';
|
||||
|
||||
async shouldRun(config: any): Promise<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];
|
||||
// V3 has host at top level, v4 has it nested in snmp object
|
||||
return !!firstDevice.host && !firstDevice.snmp;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async migrate(config: any): Promise<any> {
|
||||
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
|
||||
logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`);
|
||||
|
||||
// Get devices from either upsList or upsDevices (for partially migrated configs)
|
||||
const sourceDevices = config.upsList || config.upsDevices;
|
||||
|
||||
// Transform each UPS device from v3 flat structure to v4 nested structure
|
||||
const transformedDevices = sourceDevices.map((device: any) => {
|
||||
// Build SNMP config object
|
||||
const snmpConfig: any = {
|
||||
host: device.host,
|
||||
port: device.port || 161,
|
||||
version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version,
|
||||
timeout: device.timeout || 5000,
|
||||
};
|
||||
|
||||
// Add SNMPv1/v2c fields
|
||||
if (device.community) {
|
||||
snmpConfig.community = device.community;
|
||||
}
|
||||
|
||||
// Add SNMPv3 fields
|
||||
if (device.securityLevel) snmpConfig.securityLevel = device.securityLevel;
|
||||
if (device.username) snmpConfig.username = device.username;
|
||||
if (device.authProtocol) snmpConfig.authProtocol = device.authProtocol;
|
||||
if (device.authKey) snmpConfig.authKey = device.authKey;
|
||||
if (device.privProtocol) snmpConfig.privProtocol = device.privProtocol;
|
||||
if (device.privKey) snmpConfig.privKey = device.privKey;
|
||||
|
||||
// Add UPS model if present
|
||||
if (device.upsModel) snmpConfig.upsModel = device.upsModel;
|
||||
if (device.customOIDs) snmpConfig.customOIDs = device.customOIDs;
|
||||
|
||||
// Return v4 format with nested structure
|
||||
return {
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
snmp: snmpConfig,
|
||||
thresholds: device.thresholds || {
|
||||
battery: 60,
|
||||
runtime: 20,
|
||||
},
|
||||
groups: device.groups || [],
|
||||
};
|
||||
});
|
||||
|
||||
const migrated = {
|
||||
version: this.toVersion,
|
||||
upsDevices: transformedDevices,
|
||||
groups: config.groups || [],
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`);
|
||||
return migrated;
|
||||
}
|
||||
}
|
127
ts/migrations/migration-v4.0-to-v4.1.ts
Normal file
127
ts/migrations/migration-v4.0-to-v4.1.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { BaseMigration } from './base-migration.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
/**
|
||||
* Migration from v4.0 to v4.1
|
||||
*
|
||||
* Major changes:
|
||||
* 1. Moves thresholds from UPS level to action level
|
||||
* 2. Creates default shutdown action for UPS devices that had thresholds
|
||||
* 3. Adds empty actions array to UPS devices without actions
|
||||
* 4. Adds empty actions array to groups
|
||||
*
|
||||
* Transforms v4.0 format (with UPS-level thresholds):
|
||||
* {
|
||||
* version: "4.0",
|
||||
* upsDevices: [
|
||||
* {
|
||||
* id: "ups-1",
|
||||
* name: "UPS 1",
|
||||
* snmp: {...},
|
||||
* thresholds: { battery: 60, runtime: 20 }, // UPS-level
|
||||
* groups: []
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* To v4.1 format (with action-level thresholds):
|
||||
* {
|
||||
* version: "4.1",
|
||||
* upsDevices: [
|
||||
* {
|
||||
* id: "ups-1",
|
||||
* name: "UPS 1",
|
||||
* snmp: {...},
|
||||
* groups: [],
|
||||
* actions: [ // Thresholds moved here
|
||||
* {
|
||||
* type: "shutdown",
|
||||
* thresholds: { battery: 60, runtime: 20 },
|
||||
* triggerMode: "onlyThresholds",
|
||||
* shutdownDelay: 5
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export class MigrationV4_0ToV4_1 extends BaseMigration {
|
||||
readonly fromVersion = '4.0';
|
||||
readonly toVersion = '4.1';
|
||||
|
||||
async shouldRun(config: Record<string, unknown>): Promise<boolean> {
|
||||
// Run if config is version 4.0
|
||||
if (config.version === '4.0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also run if config has upsDevices with thresholds at UPS level (v4.0 format)
|
||||
if (Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||
const firstDevice = config.upsDevices[0] as Record<string, unknown>;
|
||||
// v4.0 has thresholds at UPS level, v4.1 has them in actions
|
||||
return firstDevice.thresholds !== undefined;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async migrate(config: Record<string, unknown>): Promise<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`);
|
||||
|
||||
// Migrate UPS devices
|
||||
const devices = (config.upsDevices as Array<Record<string, unknown>>) || [];
|
||||
const migratedDevices = devices.map((device) => {
|
||||
const migrated: Record<string, unknown> = {
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
snmp: device.snmp,
|
||||
groups: device.groups || [],
|
||||
};
|
||||
|
||||
// If device has thresholds at UPS level, convert to shutdown action
|
||||
const deviceThresholds = device.thresholds as { battery: number; runtime: number } | undefined;
|
||||
if (deviceThresholds) {
|
||||
migrated.actions = [
|
||||
{
|
||||
type: 'shutdown',
|
||||
thresholds: {
|
||||
battery: deviceThresholds.battery,
|
||||
runtime: deviceThresholds.runtime,
|
||||
},
|
||||
triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation)
|
||||
shutdownDelay: 5, // Default delay
|
||||
},
|
||||
];
|
||||
logger.dim(
|
||||
` → ${device.name}: Created shutdown action (battery: ${deviceThresholds.battery}%, runtime: ${deviceThresholds.runtime}min)`,
|
||||
);
|
||||
} else {
|
||||
// No thresholds, just add empty actions array
|
||||
migrated.actions = device.actions || [];
|
||||
}
|
||||
|
||||
return migrated;
|
||||
});
|
||||
|
||||
// Add actions to groups
|
||||
const groups = (config.groups as Array<Record<string, unknown>>) || [];
|
||||
const migratedGroups = groups.map((group) => ({
|
||||
...group,
|
||||
actions: group.actions || [],
|
||||
}));
|
||||
|
||||
const result = {
|
||||
version: this.toVersion,
|
||||
upsDevices: migratedDevices,
|
||||
groups: migratedGroups,
|
||||
checkInterval: config.checkInterval || 30000,
|
||||
};
|
||||
|
||||
logger.success(
|
||||
`${this.getName()}: Migration complete (${migratedDevices.length} devices, ${migratedGroups.length} groups updated)`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
102
ts/nupst.ts
102
ts/nupst.ts
@@ -1,12 +1,13 @@
|
||||
import { NupstSnmp } from './snmp/manager.js';
|
||||
import { NupstDaemon } from './daemon.js';
|
||||
import { NupstSystemd } from './systemd.js';
|
||||
import { commitinfo } from './00_commitinfo_data.js';
|
||||
import { logger } from './logger.js';
|
||||
import { UpsHandler } from './cli/ups-handler.js';
|
||||
import { GroupHandler } from './cli/group-handler.js';
|
||||
import { ServiceHandler } from './cli/service-handler.js';
|
||||
import * as https from 'https';
|
||||
import { NupstSnmp } from './snmp/manager.ts';
|
||||
import { NupstDaemon } from './daemon.ts';
|
||||
import { NupstSystemd } from './systemd.ts';
|
||||
import { commitinfo } from './00_commitinfo_data.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { UpsHandler } from './cli/ups-handler.ts';
|
||||
import { GroupHandler } from './cli/group-handler.ts';
|
||||
import { ServiceHandler } from './cli/service-handler.ts';
|
||||
import { ActionHandler } from './cli/action-handler.ts';
|
||||
import * as https from 'node:https';
|
||||
|
||||
/**
|
||||
* Main Nupst class that coordinates all components
|
||||
@@ -19,6 +20,7 @@ export class Nupst {
|
||||
private readonly upsHandler: UpsHandler;
|
||||
private readonly groupHandler: GroupHandler;
|
||||
private readonly serviceHandler: ServiceHandler;
|
||||
private readonly actionHandler: ActionHandler;
|
||||
private updateAvailable: boolean = false;
|
||||
private latestVersion: string = '';
|
||||
|
||||
@@ -31,11 +33,12 @@ export class Nupst {
|
||||
this.snmp.setNupst(this); // Set up bidirectional reference
|
||||
this.daemon = new NupstDaemon(this.snmp);
|
||||
this.systemd = new NupstSystemd(this.daemon);
|
||||
|
||||
|
||||
// Initialize handlers
|
||||
this.upsHandler = new UpsHandler(this);
|
||||
this.groupHandler = new GroupHandler(this);
|
||||
this.serviceHandler = new ServiceHandler(this);
|
||||
this.actionHandler = new ActionHandler(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,28 +61,35 @@ export class Nupst {
|
||||
public getSystemd(): NupstSystemd {
|
||||
return this.systemd;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the UPS handler for UPS management
|
||||
*/
|
||||
public getUpsHandler(): UpsHandler {
|
||||
return this.upsHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Group handler for group management
|
||||
*/
|
||||
public getGroupHandler(): GroupHandler {
|
||||
return this.groupHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Service handler for service management
|
||||
*/
|
||||
public getServiceHandler(): ServiceHandler {
|
||||
return this.serviceHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Action handler for action management
|
||||
*/
|
||||
public getActionHandler(): ActionHandler {
|
||||
return this.actionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current version of NUPST
|
||||
* @returns The current version string
|
||||
@@ -87,7 +97,7 @@ export class Nupst {
|
||||
public getVersion(): string {
|
||||
return commitinfo.version;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if an update is available
|
||||
* @returns Promise resolving to true if an update is available
|
||||
@@ -96,39 +106,41 @@ export class Nupst {
|
||||
try {
|
||||
const latestVersion = await this.getLatestVersion();
|
||||
const currentVersion = this.getVersion();
|
||||
|
||||
|
||||
// Compare versions
|
||||
this.updateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||
this.latestVersion = latestVersion;
|
||||
|
||||
|
||||
return this.updateAvailable;
|
||||
} catch (error) {
|
||||
logger.error(`Error checking for updates: ${error.message}`);
|
||||
logger.error(
|
||||
`Error checking for updates: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get update status information
|
||||
* @returns Object with update status information
|
||||
*/
|
||||
public getUpdateStatus(): {
|
||||
currentVersion: string,
|
||||
latestVersion: string,
|
||||
updateAvailable: boolean
|
||||
public getUpdateStatus(): {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
updateAvailable: boolean;
|
||||
} {
|
||||
return {
|
||||
currentVersion: this.getVersion(),
|
||||
latestVersion: this.latestVersion || this.getVersion(),
|
||||
updateAvailable: this.updateAvailable
|
||||
updateAvailable: this.updateAvailable,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the latest version from npm registry
|
||||
* @returns Promise resolving to the latest version string
|
||||
*/
|
||||
private async getLatestVersion(): Promise<string> {
|
||||
private getLatestVersion(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'registry.npmjs.org',
|
||||
@@ -136,17 +148,17 @@ export class Nupst {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': `nupst/${this.getVersion()}`
|
||||
}
|
||||
'User-Agent': `nupst/${this.getVersion()}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
@@ -160,15 +172,15 @@ export class Nupst {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare two semantic version strings
|
||||
* @param versionA First version
|
||||
@@ -176,39 +188,39 @@ export class Nupst {
|
||||
* @returns -1 if versionA < versionB, 0 if equal, 1 if versionA > versionB
|
||||
*/
|
||||
private compareVersions(versionA: string, versionB: string): number {
|
||||
const partsA = versionA.split('.').map(part => parseInt(part, 10));
|
||||
const partsB = versionB.split('.').map(part => parseInt(part, 10));
|
||||
|
||||
const partsA = versionA.split('.').map((part) => parseInt(part, 10));
|
||||
const partsB = versionB.split('.').map((part) => parseInt(part, 10));
|
||||
|
||||
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||
const partA = i < partsA.length ? partsA[i] : 0;
|
||||
const partB = i < partsB.length ? partsB[i] : 0;
|
||||
|
||||
|
||||
if (partA > partB) return 1;
|
||||
if (partA < partB) return -1;
|
||||
}
|
||||
|
||||
|
||||
return 0; // Versions are equal
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log the current version and update status
|
||||
*/
|
||||
public logVersionInfo(checkForUpdates: boolean = true): void {
|
||||
const version = this.getVersion();
|
||||
const boxWidth = 45;
|
||||
|
||||
|
||||
logger.logBoxTitle('NUPST Version', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${version}`);
|
||||
|
||||
|
||||
if (this.updateAvailable && this.latestVersion) {
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
} else if (checkForUpdates) {
|
||||
logger.logBoxLine('Checking for updates...');
|
||||
|
||||
|
||||
// We can't end the box yet since we're in an async operation
|
||||
this.checkForUpdates().then(updateAvailable => {
|
||||
this.checkForUpdates().then((updateAvailable) => {
|
||||
if (updateAvailable) {
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
@@ -224,4 +236,4 @@ export class Nupst {
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
// Re-export all public types
|
||||
export type { IUpsStatus, IOidSet, TUpsModel, ISnmpConfig } from './types.js';
|
||||
export type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||
|
||||
// Re-export the SNMP manager class
|
||||
export { NupstSnmp } from './manager.js';
|
||||
export { NupstSnmp } from './manager.ts';
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import * as snmp from 'net-snmp';
|
||||
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
|
||||
import { UpsOidSets } from './oid-sets.js';
|
||||
import * as snmp from 'npm:net-snmp@3.20.0';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||
import { UpsOidSets } from './oid-sets.ts';
|
||||
|
||||
/**
|
||||
* Class for SNMP communication with UPS devices
|
||||
@@ -33,7 +34,7 @@ export class NupstSnmp {
|
||||
// Set default OID set
|
||||
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set reference to the main Nupst instance
|
||||
* @param nupst Reference to the main Nupst instance
|
||||
@@ -41,14 +42,14 @@ export class NupstSnmp {
|
||||
public setNupst(nupst: any): void {
|
||||
this.nupst = nupst;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get reference to the main Nupst instance
|
||||
*/
|
||||
public getNupst(): any {
|
||||
return this.nupst;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enable debug mode
|
||||
*/
|
||||
@@ -70,11 +71,11 @@ export class NupstSnmp {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Use OIDs for the specified UPS model or default to Cyberpower
|
||||
const model = config.upsModel || 'cyberpower';
|
||||
this.activeOIDs = UpsOidSets.getOidSet(model);
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Using OIDs for UPS model: ${model}`);
|
||||
}
|
||||
@@ -87,14 +88,16 @@ export class NupstSnmp {
|
||||
* @param retryCount Current retry count (unused in this implementation)
|
||||
* @returns Promise resolving to the SNMP response value
|
||||
*/
|
||||
public async snmpGet(
|
||||
oid: string,
|
||||
config = this.DEFAULT_CONFIG,
|
||||
retryCount = 0
|
||||
public snmpGet(
|
||||
oid: string,
|
||||
config = this.DEFAULT_CONFIG,
|
||||
retryCount = 0,
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.debug) {
|
||||
console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`);
|
||||
console.log(
|
||||
`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`,
|
||||
);
|
||||
console.log('Using community:', config.community);
|
||||
}
|
||||
|
||||
@@ -105,7 +108,7 @@ export class NupstSnmp {
|
||||
timeout: config.timeout,
|
||||
transport: 'udp4',
|
||||
idBitsSize: 32,
|
||||
context: config.context || ''
|
||||
context: config.context || '',
|
||||
};
|
||||
|
||||
// Set version based on config
|
||||
@@ -119,23 +122,23 @@ export class NupstSnmp {
|
||||
|
||||
// Create appropriate session based on SNMP version
|
||||
let session;
|
||||
|
||||
|
||||
if (config.version === 3) {
|
||||
// For SNMPv3, we need to set up authentication and privacy
|
||||
// For SNMPv3, we need a valid security level
|
||||
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
||||
|
||||
|
||||
// Create the user object with required structure for net-snmp
|
||||
const user: any = {
|
||||
name: config.username || ''
|
||||
name: config.username || '',
|
||||
};
|
||||
|
||||
|
||||
// Set security level
|
||||
if (securityLevel === 'noAuthNoPriv') {
|
||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||
} else if (securityLevel === 'authNoPriv') {
|
||||
user.level = snmp.SecurityLevel.authNoPriv;
|
||||
|
||||
|
||||
// Set auth protocol - must provide both protocol and key
|
||||
if (config.authProtocol && config.authKey) {
|
||||
if (config.authProtocol === 'MD5') {
|
||||
@@ -153,7 +156,7 @@ export class NupstSnmp {
|
||||
}
|
||||
} else if (securityLevel === 'authPriv') {
|
||||
user.level = snmp.SecurityLevel.authPriv;
|
||||
|
||||
|
||||
// Set auth protocol - must provide both protocol and key
|
||||
if (config.authProtocol && config.authKey) {
|
||||
if (config.authProtocol === 'MD5') {
|
||||
@@ -162,7 +165,7 @@ export class NupstSnmp {
|
||||
user.authProtocol = snmp.AuthProtocols.sha;
|
||||
}
|
||||
user.authKey = config.authKey;
|
||||
|
||||
|
||||
// Set privacy protocol - must provide both protocol and key
|
||||
if (config.privProtocol && config.privKey) {
|
||||
if (config.privProtocol === 'DES') {
|
||||
@@ -186,18 +189,20 @@ export class NupstSnmp {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log('SNMPv3 user configuration:', {
|
||||
name: user.name,
|
||||
level: Object.keys(snmp.SecurityLevel).find(key => snmp.SecurityLevel[key] === user.level),
|
||||
level: Object.keys(snmp.SecurityLevel).find((key) =>
|
||||
snmp.SecurityLevel[key] === user.level
|
||||
),
|
||||
authProtocol: user.authProtocol ? 'Set' : 'Not Set',
|
||||
authKey: user.authKey ? 'Set' : 'Not Set',
|
||||
privProtocol: user.privProtocol ? 'Set' : 'Not Set',
|
||||
privKey: user.privKey ? 'Set' : 'Not Set'
|
||||
privKey: user.privKey ? 'Set' : 'Not Set',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
session = snmp.createV3Session(config.host, user, options);
|
||||
} else {
|
||||
// For SNMPv1/v2c, we use the community string
|
||||
@@ -229,9 +234,11 @@ export class NupstSnmp {
|
||||
}
|
||||
|
||||
// Check for SNMP errors in the response
|
||||
if (varbinds[0].type === snmp.ObjectType.NoSuchObject ||
|
||||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
|
||||
varbinds[0].type === snmp.ObjectType.EndOfMibView) {
|
||||
if (
|
||||
varbinds[0].type === snmp.ObjectType.NoSuchObject ||
|
||||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
|
||||
varbinds[0].type === snmp.ObjectType.EndOfMibView
|
||||
) {
|
||||
if (this.debug) {
|
||||
console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
|
||||
}
|
||||
@@ -245,7 +252,7 @@ export class NupstSnmp {
|
||||
// Handle specific types that might need conversion
|
||||
if (Buffer.isBuffer(value)) {
|
||||
// If value is a Buffer, try to convert it to a string if it's printable ASCII
|
||||
const isPrintableAscii = value.every(byte => byte >= 32 && byte <= 126);
|
||||
const isPrintableAscii = value.every((byte: number) => byte >= 32 && byte <= 126);
|
||||
if (isPrintableAscii) {
|
||||
value = value.toString();
|
||||
}
|
||||
@@ -258,7 +265,7 @@ export class NupstSnmp {
|
||||
console.log('SNMP response:', {
|
||||
oid: varbinds[0].oid,
|
||||
type: varbinds[0].type,
|
||||
value: value
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -276,7 +283,7 @@ export class NupstSnmp {
|
||||
try {
|
||||
// Set active OID set based on UPS model in config
|
||||
this.setActiveOIDs(config);
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('Getting UPS status with config:');
|
||||
@@ -299,18 +306,30 @@ export class NupstSnmp {
|
||||
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
||||
console.log('---------------------------------------');
|
||||
}
|
||||
|
||||
|
||||
// Get all values with independent retry logic
|
||||
const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config);
|
||||
const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0;
|
||||
const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0;
|
||||
|
||||
const powerStatusValue = await this.getSNMPValueWithRetry(
|
||||
this.activeOIDs.POWER_STATUS,
|
||||
'power status',
|
||||
config,
|
||||
);
|
||||
const batteryCapacity = await this.getSNMPValueWithRetry(
|
||||
this.activeOIDs.BATTERY_CAPACITY,
|
||||
'battery capacity',
|
||||
config,
|
||||
) || 0;
|
||||
const batteryRuntime = await this.getSNMPValueWithRetry(
|
||||
this.activeOIDs.BATTERY_RUNTIME,
|
||||
'battery runtime',
|
||||
config,
|
||||
) || 0;
|
||||
|
||||
// Determine power status - handle different values for different UPS models
|
||||
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
|
||||
|
||||
|
||||
// Convert to minutes for UPS models with different time units
|
||||
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
||||
|
||||
|
||||
const result = {
|
||||
powerStatus,
|
||||
batteryCapacity,
|
||||
@@ -321,7 +340,7 @@ export class NupstSnmp {
|
||||
batteryRuntime,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('UPS status result:');
|
||||
@@ -330,15 +349,20 @@ export class NupstSnmp {
|
||||
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
||||
console.log('---------------------------------------');
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error('---------------------------------------');
|
||||
console.error('Error getting UPS status:', error.message);
|
||||
console.error(
|
||||
'Error getting UPS status:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
console.error('---------------------------------------');
|
||||
}
|
||||
throw new Error(`Failed to get UPS status: ${error.message}`);
|
||||
throw new Error(
|
||||
`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,9 +374,9 @@ export class NupstSnmp {
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async getSNMPValueWithRetry(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
): Promise<any> {
|
||||
if (oid === '') {
|
||||
if (this.debug) {
|
||||
@@ -360,11 +384,11 @@ export class NupstSnmp {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Getting ${description} OID: ${oid}`);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const value = await this.snmpGet(oid, config);
|
||||
if (this.debug) {
|
||||
@@ -373,19 +397,22 @@ export class NupstSnmp {
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error(`Error getting ${description}:`, error.message);
|
||||
console.error(
|
||||
`Error getting ${description}:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// If we're using SNMPv3, try with different security levels
|
||||
if (config.version === 3) {
|
||||
return await this.tryFallbackSecurityLevels(oid, description, config);
|
||||
}
|
||||
|
||||
|
||||
// Try with standard OIDs as fallback
|
||||
if (config.upsModel !== 'custom') {
|
||||
return await this.tryStandardOids(oid, description, config);
|
||||
}
|
||||
|
||||
|
||||
// Return a default value if all attempts fail
|
||||
if (this.debug) {
|
||||
console.log(`Using default value 0 for ${description}`);
|
||||
@@ -402,14 +429,14 @@ export class NupstSnmp {
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async tryFallbackSecurityLevels(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
): Promise<any> {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying ${description} with fallback security level...`);
|
||||
}
|
||||
|
||||
|
||||
// Try with authNoPriv if current level is authPriv
|
||||
if (config.securityLevel === 'authPriv') {
|
||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
||||
@@ -424,11 +451,14 @@ export class NupstSnmp {
|
||||
return value;
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
console.error(`Retry failed for ${description}:`, retryError.message);
|
||||
console.error(
|
||||
`Retry failed for ${description}:`,
|
||||
retryError instanceof Error ? retryError.message : String(retryError),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try with noAuthNoPriv as a last resort
|
||||
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
|
||||
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
|
||||
@@ -443,11 +473,14 @@ export class NupstSnmp {
|
||||
return value;
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
console.error(`Retry failed for ${description}:`, retryError.message);
|
||||
console.error(
|
||||
`Retry failed for ${description}:`,
|
||||
retryError instanceof Error ? retryError.message : String(retryError),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -459,18 +492,20 @@ export class NupstSnmp {
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async tryStandardOids(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
): Promise<any> {
|
||||
try {
|
||||
// Try RFC 1628 standard UPS MIB OIDs
|
||||
const standardOIDs = UpsOidSets.getStandardOids();
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`);
|
||||
console.log(
|
||||
`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const standardValue = await this.snmpGet(standardOIDs[description], config);
|
||||
if (this.debug) {
|
||||
console.log(`${description} standard OID value:`, standardValue);
|
||||
@@ -478,56 +513,49 @@ export class NupstSnmp {
|
||||
return standardValue;
|
||||
} catch (stdError) {
|
||||
if (this.debug) {
|
||||
console.error(`Standard OID retry failed for ${description}:`, stdError.message);
|
||||
console.error(
|
||||
`Standard OID retry failed for ${description}:`,
|
||||
stdError instanceof Error ? stdError.message : String(stdError),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine power status based on UPS model and raw value
|
||||
* Uses the value mappings defined in the OID sets
|
||||
* @param upsModel UPS model
|
||||
* @param powerStatusValue Raw power status value
|
||||
* @returns Standardized power status
|
||||
*/
|
||||
private determinePowerStatus(
|
||||
upsModel: TUpsModel | undefined,
|
||||
powerStatusValue: number
|
||||
upsModel: TUpsModel | undefined,
|
||||
powerStatusValue: number,
|
||||
): 'online' | 'onBattery' | 'unknown' {
|
||||
if (upsModel === 'cyberpower') {
|
||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
||||
// 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc.
|
||||
if (powerStatusValue === 2) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 3) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else if (upsModel === 'eaton') {
|
||||
// Eaton UPS: xupsOutputSource values
|
||||
// 3=normal/mains, 5=battery, etc.
|
||||
if (powerStatusValue === 3) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 5) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else if (upsModel === 'apc') {
|
||||
// APC UPS: upsBasicOutputStatus values
|
||||
// 2=online, 3=onBattery, etc.
|
||||
if (powerStatusValue === 2) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 3) {
|
||||
return 'onBattery';
|
||||
}
|
||||
} else {
|
||||
// Default interpretation for other UPS models
|
||||
if (powerStatusValue === 1) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 2) {
|
||||
return 'onBattery';
|
||||
// Get the OID set for this UPS model
|
||||
if (upsModel && upsModel !== 'custom') {
|
||||
const oidSet = UpsOidSets.getOidSet(upsModel);
|
||||
|
||||
// Use the value mappings if available
|
||||
if (oidSet.POWER_STATUS_VALUES) {
|
||||
if (powerStatusValue === oidSet.POWER_STATUS_VALUES.online) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === oidSet.POWER_STATUS_VALUES.onBattery) {
|
||||
return 'onBattery';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback for custom or undefined models (RFC 1628 standard)
|
||||
// upsOutputSource: 3=normal (mains), 5=battery
|
||||
if (powerStatusValue === 3) {
|
||||
return 'online';
|
||||
} else if (powerStatusValue === 5) {
|
||||
return 'onBattery';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@@ -538,25 +566,29 @@ export class NupstSnmp {
|
||||
* @returns Processed runtime in minutes
|
||||
*/
|
||||
private processRuntimeValue(
|
||||
upsModel: TUpsModel | undefined,
|
||||
batteryRuntime: number
|
||||
upsModel: TUpsModel | undefined,
|
||||
batteryRuntime: number,
|
||||
): number {
|
||||
if (this.debug) {
|
||||
console.log('Raw runtime value:', batteryRuntime);
|
||||
}
|
||||
|
||||
|
||||
if (upsModel === 'cyberpower' && batteryRuntime > 0) {
|
||||
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
|
||||
if (this.debug) {
|
||||
console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`);
|
||||
console.log(
|
||||
`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`,
|
||||
);
|
||||
}
|
||||
return minutes;
|
||||
} else if (upsModel === 'eaton' && batteryRuntime > 0) {
|
||||
// Eaton: Runtime is in seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 60);
|
||||
if (this.debug) {
|
||||
console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`);
|
||||
console.log(
|
||||
`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`,
|
||||
);
|
||||
}
|
||||
return minutes;
|
||||
} else if (batteryRuntime > 10000) {
|
||||
@@ -567,7 +599,7 @@ export class NupstSnmp {
|
||||
}
|
||||
return minutes;
|
||||
}
|
||||
|
||||
|
||||
return batteryRuntime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { IOidSet, TUpsModel } from './types.js';
|
||||
import type { IOidSet, TUpsModel } from './types.ts';
|
||||
|
||||
/**
|
||||
* OID sets for different UPS models
|
||||
@@ -11,45 +11,65 @@ export class UpsOidSets {
|
||||
private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = {
|
||||
// Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11)
|
||||
cyberpower: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery)
|
||||
POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // upsBaseOutputStatus: 2=onLine
|
||||
onBattery: 3, // upsBaseOutputStatus: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// APC OIDs
|
||||
apc: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
|
||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // upsBasicOutputStatus: 2=onLine
|
||||
onBattery: 3, // upsBasicOutputStatus: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// Eaton OIDs
|
||||
eaton: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery)
|
||||
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 3, // xupsOutputSource: 3=normal (mains power)
|
||||
onBattery: 5, // xupsOutputSource: 5=battery
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// TrippLite OIDs
|
||||
tripplite: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
|
||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // tlUpsOutputSource: 2=normal (mains power)
|
||||
onBattery: 3, // tlUpsOutputSource: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// Liebert/Vertiv OIDs
|
||||
liebert: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
|
||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
||||
POWER_STATUS_VALUES: {
|
||||
online: 2, // lgpPwrOutputSource: 2=normal (mains power)
|
||||
onBattery: 3, // lgpPwrOutputSource: 3=onBattery
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// Custom OIDs (to be provided by the user)
|
||||
custom: {
|
||||
POWER_STATUS: '',
|
||||
BATTERY_CAPACITY: '',
|
||||
BATTERY_RUNTIME: '',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -67,9 +87,9 @@ export class UpsOidSets {
|
||||
*/
|
||||
public static getStandardOids(): Record<string, string> {
|
||||
return {
|
||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0' // upsEstimatedMinutesRemaining
|
||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
* Type definitions for SNMP module
|
||||
*/
|
||||
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
/**
|
||||
* UPS status interface
|
||||
*/
|
||||
@@ -26,6 +28,13 @@ export interface IOidSet {
|
||||
BATTERY_CAPACITY: string;
|
||||
/** OID for battery runtime */
|
||||
BATTERY_RUNTIME: string;
|
||||
/** Power status value mappings */
|
||||
POWER_STATUS_VALUES?: {
|
||||
/** SNMP value that indicates UPS is online (on AC power) */
|
||||
online: number;
|
||||
/** SNMP value that indicates UPS is on battery */
|
||||
onBattery: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,11 +56,11 @@ export interface ISnmpConfig {
|
||||
timeout: number;
|
||||
|
||||
context?: string;
|
||||
|
||||
|
||||
// SNMPv1/v2c
|
||||
/** Community string for SNMPv1/v2c */
|
||||
community?: string;
|
||||
|
||||
|
||||
// SNMPv3
|
||||
/** Security level for SNMPv3 */
|
||||
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
|
||||
@@ -65,7 +74,7 @@ export interface ISnmpConfig {
|
||||
privProtocol?: 'DES' | 'AES';
|
||||
/** Privacy key for SNMPv3 */
|
||||
privKey?: string;
|
||||
|
||||
|
||||
// UPS model and custom OIDs
|
||||
/** UPS model for OID selection */
|
||||
upsModel?: TUpsModel;
|
||||
@@ -89,4 +98,4 @@ export interface ISnmpV3SecurityParams {
|
||||
msgAuthenticationParameters: Buffer;
|
||||
/** Privacy parameters */
|
||||
msgPrivacyParameters: Buffer;
|
||||
}
|
||||
}
|
||||
|
383
ts/systemd.ts
383
ts/systemd.ts
@@ -1,7 +1,10 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { NupstDaemon } from './daemon.js';
|
||||
import { logger } from './logger.js';
|
||||
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 { NupstSnmp } from './snmp/manager.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||
|
||||
/**
|
||||
* Class for managing systemd service
|
||||
@@ -14,17 +17,17 @@ export class NupstSystemd {
|
||||
|
||||
/** Template for the systemd service file */
|
||||
private readonly serviceTemplate = `[Unit]
|
||||
Description=Node.js UPS Shutdown Tool for Multiple UPS Devices
|
||||
Description=NUPST - Deno-powered UPS Monitoring Tool
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/opt/nupst/bin/nupst daemon-start
|
||||
ExecStart=/usr/local/bin/nupst service start-daemon
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
User=root
|
||||
Group=root
|
||||
Environment=PATH=/usr/bin:/usr/local/bin
|
||||
Environment=NODE_ENV=production
|
||||
WorkingDirectory=/tmp
|
||||
WorkingDirectory=/opt/nupst
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -48,11 +51,11 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
await fs.access(configPath);
|
||||
} catch (error) {
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Configuration Error', boxWidth);
|
||||
logger.logBoxLine(`No configuration file found at ${configPath}`);
|
||||
logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration.");
|
||||
logger.logBoxEnd();
|
||||
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('');
|
||||
throw new Error('Configuration not found');
|
||||
}
|
||||
}
|
||||
@@ -65,7 +68,7 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
// Check if configuration exists before installing
|
||||
await this.checkConfigExists();
|
||||
|
||||
|
||||
// Write the service file
|
||||
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
||||
const boxWidth = 50;
|
||||
@@ -81,7 +84,7 @@ WantedBy=multi-user.target
|
||||
logger.logBoxLine('Service enabled to start on boot');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
if (error.message === 'Configuration not found') {
|
||||
if (error instanceof Error && error.message === 'Configuration not found') {
|
||||
// Just rethrow the error as the message has already been displayed
|
||||
throw error;
|
||||
}
|
||||
@@ -98,14 +101,14 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
// Check if configuration exists before starting
|
||||
await this.checkConfigExists();
|
||||
|
||||
|
||||
execSync('systemctl start nupst.service');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('NUPST service started successfully');
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
if (error.message === 'Configuration not found') {
|
||||
if (error instanceof Error && error.message === 'Configuration not found') {
|
||||
// Exit with error code since configuration is required
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -118,7 +121,7 @@ WantedBy=multi-user.target
|
||||
* Stop the systemd service
|
||||
* @throws Error if stop fails
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
public stop(): void {
|
||||
try {
|
||||
execSync('systemctl stop nupst.service');
|
||||
logger.success('NUPST service stopped');
|
||||
@@ -132,32 +135,72 @@ WantedBy=multi-user.target
|
||||
* Get status of the systemd service and UPS
|
||||
* @param debugMode Whether to enable debug mode for SNMP
|
||||
*/
|
||||
/**
|
||||
* Display version information and update status
|
||||
* @private
|
||||
*/
|
||||
private async displayVersionInfo(): Promise<void> {
|
||||
try {
|
||||
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||
const version = nupst.getVersion();
|
||||
|
||||
// Check for updates
|
||||
const updateAvailable = await nupst.checkForUpdates();
|
||||
|
||||
// Display version info
|
||||
if (updateAvailable) {
|
||||
const updateStatus = nupst.getUpdateStatus();
|
||||
logger.log('');
|
||||
logger.log(
|
||||
`${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')}`);
|
||||
} else {
|
||||
logger.log('');
|
||||
logger.log(
|
||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// If version check fails, show at least the current version
|
||||
try {
|
||||
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||
const version = nupst.getVersion();
|
||||
logger.log('');
|
||||
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
|
||||
} catch (_innerError) {
|
||||
// Silently fail if we can't even get the version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getStatus(debugMode: boolean = false): Promise<void> {
|
||||
try {
|
||||
// Enable debug mode if requested
|
||||
if (debugMode) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Debug Mode', boxWidth);
|
||||
logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
|
||||
logger.logBoxEnd();
|
||||
console.log('');
|
||||
logger.info('Debug Mode: SNMP debugging enabled');
|
||||
console.log('');
|
||||
this.daemon.getNupstSnmp().enableDebug();
|
||||
}
|
||||
|
||||
// Display version information
|
||||
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
|
||||
|
||||
// Check if config exists first
|
||||
|
||||
// Display version and update status first
|
||||
await this.displayVersionInfo();
|
||||
|
||||
// Check if config exists
|
||||
try {
|
||||
await this.checkConfigExists();
|
||||
} catch (error) {
|
||||
// Error message already displayed by checkConfigExists
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await this.displayServiceStatus();
|
||||
await this.displayAllUpsStatus();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get status: ${error.message}`);
|
||||
logger.error(
|
||||
`Failed to get status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,21 +208,53 @@ WantedBy=multi-user.target
|
||||
* Display the systemd service status
|
||||
* @private
|
||||
*/
|
||||
private async displayServiceStatus(): Promise<void> {
|
||||
private displayServiceStatus(): void {
|
||||
try {
|
||||
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
// Process each line of the status output
|
||||
serviceStatus.split('\n').forEach(line => {
|
||||
logger.logBoxLine(line);
|
||||
});
|
||||
logger.logBoxEnd();
|
||||
const lines = serviceStatus.split('\n');
|
||||
|
||||
// Parse key information from systemctl output
|
||||
let isActive = false;
|
||||
let pid = '';
|
||||
let memory = '';
|
||||
let cpu = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Active:')) {
|
||||
isActive = line.includes('active (running)');
|
||||
} else if (line.includes('Main PID:')) {
|
||||
const match = line.match(/Main PID:\s+(\d+)/);
|
||||
if (match) pid = match[1];
|
||||
} else if (line.includes('Memory:')) {
|
||||
const match = line.match(/Memory:\s+([\d.]+[A-Z])/);
|
||||
if (match) memory = match[1];
|
||||
} else if (line.includes('CPU:')) {
|
||||
const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/);
|
||||
if (match) cpu = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Display beautiful status
|
||||
logger.log('');
|
||||
if (isActive) {
|
||||
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
||||
} else {
|
||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
|
||||
}
|
||||
|
||||
if (pid || memory || cpu) {
|
||||
const details = [];
|
||||
if (pid) details.push(`PID: ${theme.dim(pid)}`);
|
||||
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
|
||||
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
|
||||
logger.log(` ${details.join(' ')}`);
|
||||
}
|
||||
logger.log('');
|
||||
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
logger.logBoxLine('Service is not running');
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,90 +268,198 @@ WantedBy=multi-user.target
|
||||
await this.daemon.loadConfig();
|
||||
const config = this.daemon.getConfig();
|
||||
const snmp = this.daemon.getNupstSnmp();
|
||||
|
||||
|
||||
// Check if we have the new multi-UPS config format
|
||||
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
|
||||
|
||||
logger.info(`UPS Devices (${config.upsDevices.length}):`);
|
||||
|
||||
// Show status for each UPS
|
||||
for (const ups of config.upsDevices) {
|
||||
await this.displaySingleUpsStatus(ups, snmp);
|
||||
}
|
||||
|
||||
// Display groups after UPS devices
|
||||
this.displayGroupsStatus();
|
||||
} else if (config.snmp) {
|
||||
// Legacy single UPS configuration
|
||||
const legacyUps = {
|
||||
// Legacy single UPS configuration (v1/v2 format)
|
||||
logger.info('UPS Devices (1):');
|
||||
const legacyUps: IUpsConfig = {
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds,
|
||||
groups: []
|
||||
groups: [],
|
||||
actions: config.thresholds
|
||||
? [
|
||||
{
|
||||
type: 'shutdown',
|
||||
thresholds: config.thresholds,
|
||||
triggerMode: 'onlyThresholds',
|
||||
shutdownDelay: 5,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
|
||||
|
||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
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('');
|
||||
}
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Status', boxWidth);
|
||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
|
||||
logger.logBoxEnd();
|
||||
logger.log('');
|
||||
logger.error('Failed to retrieve UPS status');
|
||||
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display status of a single UPS
|
||||
* @param ups UPS configuration
|
||||
* @param snmp SNMP manager
|
||||
*/
|
||||
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`ID: ${ups.id}`);
|
||||
logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`);
|
||||
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
// Get group names if available
|
||||
const config = this.daemon.getConfig();
|
||||
const groupNames = ups.groups.map(groupId => {
|
||||
const group = config.groups?.find(g => g.id === groupId);
|
||||
return group ? group.name : groupId;
|
||||
});
|
||||
logger.logBoxLine(`Groups: ${groupNames.join(', ')}`);
|
||||
}
|
||||
|
||||
logger.logBoxEnd();
|
||||
|
||||
private async displaySingleUpsStatus(ups: IUpsConfig, snmp: NupstSnmp): Promise<void> {
|
||||
try {
|
||||
// Create a test config with a short timeout
|
||||
const testConfig = {
|
||||
...ups.snmp,
|
||||
timeout: Math.min(ups.snmp.timeout, 10000) // Use at most 10 seconds for status check
|
||||
timeout: Math.min(ups.snmp.timeout, 10000), // Use at most 10 seconds for status check
|
||||
};
|
||||
|
||||
|
||||
const status = await snmp.getUpsStatus(testConfig);
|
||||
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
|
||||
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
|
||||
// Show threshold status
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
|
||||
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
|
||||
}`);
|
||||
logger.logBoxLine(` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
|
||||
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
|
||||
}`);
|
||||
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Determine status symbol based on power status
|
||||
let statusSymbol = symbols.unknown;
|
||||
if (status.powerStatus === 'online') {
|
||||
statusSymbol = symbols.running;
|
||||
} else if (status.powerStatus === 'onBattery') {
|
||||
statusSymbol = symbols.warning;
|
||||
}
|
||||
|
||||
// Display UPS name and power status
|
||||
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
||||
|
||||
// Display battery with color coding
|
||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||
|
||||
// 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
|
||||
? symbols.success
|
||||
: batteryThreshold !== undefined
|
||||
? symbols.warning
|
||||
: '';
|
||||
|
||||
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||
|
||||
// Display host info
|
||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||
|
||||
// Display groups if any
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
const config = this.daemon.getConfig();
|
||||
const groupNames = ups.groups.map((groupId: string) => {
|
||||
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
|
||||
return group ? group.name : groupId;
|
||||
});
|
||||
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
|
||||
}
|
||||
|
||||
// Display actions if any
|
||||
if (ups.actions && ups.actions.length > 0) {
|
||||
for (const action of ups.actions) {
|
||||
let actionDesc = `${action.type}`;
|
||||
if (action.thresholds) {
|
||||
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||
if (action.shutdownDelay) {
|
||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||
}
|
||||
actionDesc += ')';
|
||||
} else {
|
||||
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
|
||||
if (action.shutdownDelay) {
|
||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||
}
|
||||
actionDesc += ')';
|
||||
}
|
||||
logger.log(` ${theme.dim('Action:')} ${theme.info(actionDesc)}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
|
||||
} catch (error) {
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
|
||||
logger.logBoxEnd();
|
||||
// Display error for this UPS
|
||||
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('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display status of all groups
|
||||
* @private
|
||||
*/
|
||||
private displayGroupsStatus(): void {
|
||||
const config = this.daemon.getConfig();
|
||||
|
||||
if (!config.groups || config.groups.length === 0) {
|
||||
return; // No groups to display
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
logger.info(`Groups (${config.groups.length}):`);
|
||||
|
||||
for (const group of config.groups) {
|
||||
// Display group name and mode
|
||||
const modeColor = group.mode === 'redundant' ? theme.success : theme.warning;
|
||||
logger.log(
|
||||
` ${symbols.info} ${theme.highlight(group.name)} ${theme.dim(`(${modeColor(group.mode)})`)}`,
|
||||
);
|
||||
|
||||
// Display description if present
|
||||
if (group.description) {
|
||||
logger.log(` ${theme.dim(group.description)}`);
|
||||
}
|
||||
|
||||
// Display UPS devices in this group
|
||||
const upsInGroup = config.upsDevices.filter((ups) =>
|
||||
ups.groups && ups.groups.includes(group.id)
|
||||
);
|
||||
|
||||
if (upsInGroup.length > 0) {
|
||||
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
||||
logger.log(` ${theme.dim(`UPS Devices (${upsInGroup.length}):`)} ${upsNames}`);
|
||||
} else {
|
||||
logger.log(` ${theme.dim('UPS Devices: None')}`);
|
||||
}
|
||||
|
||||
// Display actions if any
|
||||
if (group.actions && group.actions.length > 0) {
|
||||
for (const action of group.actions) {
|
||||
let actionDesc = `${action.type}`;
|
||||
if (action.thresholds) {
|
||||
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||
if (action.shutdownDelay) {
|
||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||
}
|
||||
actionDesc += ')';
|
||||
} else {
|
||||
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
|
||||
if (action.shutdownDelay) {
|
||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||
}
|
||||
actionDesc += ')';
|
||||
}
|
||||
logger.log(` ${theme.dim('Action:')} ${theme.info(actionDesc)}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +472,7 @@ WantedBy=multi-user.target
|
||||
await this.stopService();
|
||||
await this.disableService();
|
||||
await this.removeServiceFile();
|
||||
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
logger.log('Systemd daemon reloaded');
|
||||
@@ -304,7 +487,7 @@ WantedBy=multi-user.target
|
||||
* Stop the service if it's running
|
||||
* @private
|
||||
*/
|
||||
private async stopService(): Promise<void> {
|
||||
private stopService(): void {
|
||||
try {
|
||||
logger.log('Stopping NUPST service...');
|
||||
execSync('systemctl stop nupst.service');
|
||||
@@ -318,7 +501,7 @@ WantedBy=multi-user.target
|
||||
* Disable the service
|
||||
* @private
|
||||
*/
|
||||
private async disableService(): Promise<void> {
|
||||
private disableService(): void {
|
||||
try {
|
||||
logger.log('Disabling NUPST service...');
|
||||
execSync('systemctl disable nupst.service');
|
||||
@@ -340,4 +523,4 @@ WantedBy=multi-user.target
|
||||
logger.log('Service file did not exist');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user