Compare commits
36 Commits
v3.1.2-pre
...
v4.0.8
Author | SHA1 | Date | |
---|---|---|---|
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 |
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
|
# Compiled Deno binaries (built by scripts/compile-all.sh)
|
||||||
dist*/
|
dist/binaries/
|
||||||
|
|
||||||
# Dependencies
|
# Deno cache and lock file
|
||||||
|
.deno/
|
||||||
|
deno.lock
|
||||||
|
|
||||||
|
# Legacy Node.js artifacts (v3.x and earlier - kept for safety)
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Bundled Node.js binaries
|
|
||||||
vendor/
|
vendor/
|
||||||
|
dist_ts/
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
@@ -18,4 +21,5 @@ npm-debug.log*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
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
|
|
402
changelog.md
402
changelog.md
@@ -1,17 +1,190 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-03-28 - 3.1.2 - fix(cli/ups-handler)
|
||||||
|
|
||||||
Improve UPS device listing table formatting for better column alignment
|
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.
|
- Adjusted header spacing for the Host column and overall table alignment in the UPS handler output.
|
||||||
|
|
||||||
## 2025-03-28 - 3.1.1 - fix(cli)
|
## 2025-03-28 - 3.1.1 - fix(cli)
|
||||||
|
|
||||||
Improve table header formatting in group and UPS listings
|
Improve table header formatting in group and UPS listings
|
||||||
|
|
||||||
- Adjusted column padding in group listing for proper alignment
|
- Adjusted column padding in group listing for proper alignment
|
||||||
- Fixed UPS table header spacing for consistent CLI output
|
- Fixed UPS table header spacing for consistent CLI output
|
||||||
|
|
||||||
## 2025-03-28 - 3.1.0 - feat(cli)
|
## 2025-03-28 - 3.1.0 - feat(cli)
|
||||||
|
|
||||||
Refactor CLI commands to use dedicated handlers for UPS, group, and service management
|
Refactor CLI commands to use dedicated handlers for UPS, group, and service management
|
||||||
|
|
||||||
- Extracted UPS-related CLI logic into a new UpsHandler
|
- Extracted UPS-related CLI logic into a new UpsHandler
|
||||||
@@ -21,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
|
- Exposed getters for the new handlers in the Nupst class
|
||||||
|
|
||||||
## 2025-03-28 - 3.0.1 - fix(cli)
|
## 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.
|
- Deleted the unused promptForUniqueUpsId method from ts/cli.ts.
|
||||||
- Updated UPS configuration to generate a unique ID directly using helpers.shortId().
|
- Updated UPS configuration to generate a unique ID directly using helpers.shortId().
|
||||||
- Improved code clarity by removing unnecessary interactive prompts for UPS IDs.
|
- Improved code clarity by removing unnecessary interactive prompts for UPS IDs.
|
||||||
|
|
||||||
## 2025-03-28 - 3.0.0 - BREAKING CHANGE(core)
|
## 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
|
Add multi-UPS support and group management; update CLI, configuration and documentation to support
|
||||||
- Added group management commands (group add, edit, delete, list) with redundant and non-redundant modes
|
multiple UPS devices with group modes
|
||||||
- Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group subcommands
|
|
||||||
|
- 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
|
- Updated readme and documentation to reflect new configuration structure and features
|
||||||
- Enhanced logging and status display for multiple UPS devices
|
- Enhanced logging and status display for multiple UPS devices
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.17 - fix(logger)
|
## 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.
|
- 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.
|
- Ensures that logBoxLine uses the previously set width when no new width is provided.
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.16 - fix(cli)
|
## 2025-03-26 - 2.6.16 - fix(cli)
|
||||||
|
|
||||||
Improve CLI logging consistency by replacing direct console output with unified logger calls.
|
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
|
- Replaced console.log and console.error with logger.log and logger.error in CLI commands
|
||||||
@@ -50,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
|
- Enhanced consistency of log output throughout the ts/cli.ts file
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.15 - fix(logger)
|
## 2025-03-26 - 2.6.15 - fix(logger)
|
||||||
|
|
||||||
Replace direct console logging with unified logger interface for consistent formatting
|
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
|
- Integrate logBox formatting for structured output and consistent log presentation
|
||||||
- Update test expectations in test.logger.ts to check for standardized error messages
|
- Update test expectations in test.logger.ts to check for standardized error messages
|
||||||
- Refactor logging calls throughout the codebase for improved clarity and maintainability
|
- Refactor logging calls throughout the codebase for improved clarity and maintainability
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.14 - fix(systemd)
|
## 2025-03-26 - 2.6.14 - fix(systemd)
|
||||||
|
|
||||||
Shorten closing log divider in systemd service installation output for consistent formatting.
|
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.
|
- Replaced the overly long footer with a shorter one in ts/systemd.ts.
|
||||||
- This change improves log readability without affecting functionality.
|
- This change improves log readability without affecting functionality.
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.13 - fix(cli)
|
## 2025-03-26 - 2.6.13 - fix(cli)
|
||||||
|
|
||||||
Fix CLI update output box formatting
|
Fix CLI update output box formatting
|
||||||
|
|
||||||
- Adjusted the closing box line in the update process log messages for consistent visual formatting
|
- Adjusted the closing box line in the update process log messages for consistent visual formatting
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.12 - fix(systemd)
|
## 2025-03-26 - 2.6.12 - fix(systemd)
|
||||||
|
|
||||||
Adjust logging border in systemd service installation output
|
Adjust logging border in systemd service installation output
|
||||||
|
|
||||||
- Updated the closing border line for consistent output formatting in ts/systemd.ts
|
- Updated the closing border line for consistent output formatting in ts/systemd.ts
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.11 - fix(cli, systemd)
|
## 2025-03-26 - 2.6.11 - fix(cli, systemd)
|
||||||
|
|
||||||
Adjust log formatting for consistent output in CLI and systemd commands
|
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.
|
- Fixed spacing issues in service installation and status log messages in the systemd module.
|
||||||
- Revised output formatting in the CLI to improve message clarity.
|
- Revised output formatting in the CLI to improve message clarity.
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.10 - fix(daemon)
|
## 2025-03-26 - 2.6.10 - fix(daemon)
|
||||||
|
|
||||||
Adjust console log box formatting for consistent output in daemon status messages
|
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
|
- Improved visual consistency in log messages
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.9 - fix(cli)
|
## 2025-03-26 - 2.6.9 - fix(cli)
|
||||||
|
|
||||||
Improve console output formatting for status banners and logging messages
|
Improve console output formatting for status banners and logging messages
|
||||||
|
|
||||||
- Standardize banner messages in daemon status updates
|
- Standardize banner messages in daemon status updates
|
||||||
@@ -93,19 +285,23 @@ Improve console output formatting for status banners and logging messages
|
|||||||
- Update UPS connection and status banners in systemd
|
- Update UPS connection and status banners in systemd
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.8 - fix(cli)
|
## 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
|
- Standardize whitespace and formatting in ts/cli.ts for consistency
|
||||||
- Refine argument filtering for debug mode and prompt messages
|
- Refine argument filtering for debug mode and prompt messages
|
||||||
- Remove unused 'dgram' import from ts/snmp/manager.ts
|
- Remove unused 'dgram' import from ts/snmp/manager.ts
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.7 - fix(setup.sh)
|
## 2025-03-26 - 2.6.7 - fix(setup.sh)
|
||||||
|
|
||||||
Clarify net-snmp dependency installation message in setup.sh
|
Clarify net-snmp dependency installation message in setup.sh
|
||||||
|
|
||||||
- Updated echo statement to indicate installation of net-snmp along with 2 subdependencies
|
- Updated echo statement to indicate installation of net-snmp along with 2 subdependencies
|
||||||
- Improves clarity on dependency installation during setup
|
- Improves clarity on dependency installation during setup
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.6 - fix(setup.sh)
|
## 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
|
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
|
- Replace use of the npm binary with direct execution of npm-cli.js
|
||||||
@@ -113,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
|
- Simplify cleanup by removing unnecessary PATH modifications
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.5 - fix(daemon, setup)
|
## 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
|
- Use execFileAsync to execute shutdown commands reliably
|
||||||
- Add multiple fallback alternatives for shutdown and emergency shutdown handling
|
- 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
|
- Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.4 - fix(setup)
|
## 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.
|
- Remove existing package-lock.json along with node_modules to prevent stale artifacts.
|
||||||
- Back up the original package.json before modifying it.
|
- Back up the original package.json before modifying it.
|
||||||
@@ -129,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.
|
- Restore the original package.json if the installation fails.
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.3 - fix(setup)
|
## 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
|
- Removed full production dependency install in favor of installing only net-snmp@3.20.0
|
||||||
- Added verification step to confirm net-snmp installation
|
- Added verification step to confirm net-snmp installation
|
||||||
- Generate a minimal package-lock.json if one does not exist
|
- Generate a minimal package-lock.json if one does not exist
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.2 - fix(setup/readme)
|
## 2025-03-26 - 2.6.2 - fix(setup/readme)
|
||||||
|
|
||||||
Improve force update instructions and dependency installation process in setup.sh and readme.md
|
Improve force update instructions and dependency installation process in setup.sh and readme.md
|
||||||
|
|
||||||
- Clarify force update commands with explicit paths in readme.md
|
- Clarify force update commands with explicit paths in readme.md
|
||||||
@@ -143,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
|
- Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.1 - fix(setup)
|
## 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.
|
- Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution.
|
||||||
- Log Node.js and npm versions for debugging purposes.
|
- Log Node.js and npm versions for debugging purposes.
|
||||||
- Restore the original PATH after installing dependencies.
|
- Restore the original PATH after installing dependencies.
|
||||||
|
|
||||||
## 2025-03-26 - 2.6.0 - feat(setup)
|
## 2025-03-26 - 2.6.0 - feat(setup)
|
||||||
|
|
||||||
Add --force update flag to setup script and update installation instructions
|
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
|
- Implemented --force option in setup.sh to force-update Node.js binary and dependencies
|
||||||
@@ -157,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
|
- Modified ts/cli.ts update command to pass the --force flag to setup.sh
|
||||||
|
|
||||||
## 2025-03-26 - 2.5.2 - fix(installer)
|
## 2025-03-26 - 2.5.2 - fix(installer)
|
||||||
|
|
||||||
Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic
|
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
|
- Moved net-snmp from devDependencies to dependencies in package.json
|
||||||
- Updated setup.sh to install production dependencies and handle installation errors gracefully
|
- Updated setup.sh to install production dependencies and handle installation errors gracefully
|
||||||
- Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts
|
- Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts
|
||||||
- Revised README to clarify minimal runtime dependencies and secure SNMP features
|
- Revised README to clarify minimal runtime dependencies and secure SNMP features
|
||||||
|
|
||||||
## 2025-03-25 - 2.5.1 - fix(snmp)
|
## 2025-03-25 - 2.5.1 - fix(snmp)
|
||||||
|
|
||||||
Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion.
|
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.
|
- Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager.
|
||||||
|
|
||||||
## 2025-03-25 - 2.5.0 - feat(cli)
|
## 2025-03-25 - 2.5.0 - feat(cli)
|
||||||
|
|
||||||
Automatically restart running NUPST service after configuration changes in interactive setup
|
Automatically restart running NUPST service after configuration changes in interactive setup
|
||||||
|
|
||||||
- Added restartServiceIfRunning() to check and restart the service if it's active.
|
- Added restartServiceIfRunning() to check and restart the service if it's active.
|
||||||
- Invoked the restart function post-setup to apply configuration changes immediately.
|
- Invoked the restart function post-setup to apply configuration changes immediately.
|
||||||
|
|
||||||
## 2025-03-25 - 2.4.8 - fix(installer)
|
## 2025-03-25 - 2.4.8 - fix(installer)
|
||||||
|
|
||||||
Improve Git dependency handling and repository cloning in install.sh
|
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.
|
- Add explicit check for git installation and prompt the user interactively if git is missing.
|
||||||
@@ -185,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.
|
- Ensure proper cloning of the repository when running the installer outside the repo.
|
||||||
|
|
||||||
## 2025-03-25 - 2.4.7 - fix(readme)
|
## 2025-03-25 - 2.4.7 - fix(readme)
|
||||||
|
|
||||||
Update installation instructions to combine download and execution into a single command for clarity
|
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
|
- Method 1 now uses a unified one-line command to download and run the install script
|
||||||
|
|
||||||
## 2025-03-25 - 2.4.6 - fix(installer)
|
## 2025-03-25 - 2.4.6 - fix(installer)
|
||||||
|
|
||||||
Improve installation instructions for interactive and non-interactive setups
|
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
|
- Updated readme.md to include three distinct installation methods with clear command examples
|
||||||
|
|
||||||
## 2025-03-25 - 2.4.5 - fix(install)
|
## 2025-03-25 - 2.4.5 - fix(install)
|
||||||
|
|
||||||
Improve interactive terminal detection and update installation instructions
|
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
|
- Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for
|
||||||
- Updated README.md quick install instructions to recommend process substitution and clarify auto-yes usage
|
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)
|
## 2025-03-25 - 2.4.4 - fix(install)
|
||||||
|
|
||||||
Improve interactive mode detection and non-interactive installation handling in install.sh
|
Improve interactive mode detection and non-interactive installation handling in install.sh
|
||||||
|
|
||||||
- Detect and warn when running without a controlling terminal
|
- Detect and warn when running without a controlling terminal
|
||||||
@@ -210,86 +429,116 @@ Improve interactive mode detection and non-interactive installation handling in
|
|||||||
- Clarify installation instructions in readme for interactive and non-interactive modes
|
- Clarify installation instructions in readme for interactive and non-interactive modes
|
||||||
|
|
||||||
## 2025-03-25 - 2.4.3 - fix(readme)
|
## 2025-03-25 - 2.4.3 - fix(readme)
|
||||||
|
|
||||||
Update Quick Install command syntax in readme for auto-yes installation
|
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)
|
## 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
|
- 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
|
- Updated references in the daemon to call its own shutdown method instead of the SNMP manager
|
||||||
- Removed redundant initiateShutdown method from the SNMP manager
|
- Removed redundant initiateShutdown method from the SNMP manager
|
||||||
|
|
||||||
## 2025-03-25 - 2.4.1 - fix(docs)
|
## 2025-03-25 - 2.4.1 - fix(docs)
|
||||||
|
|
||||||
Update readme with detailed legal and trademark guidance
|
Update readme with detailed legal and trademark guidance
|
||||||
|
|
||||||
- Clarified legal section by adding trademark and company information
|
- Clarified legal section by adding trademark and company information
|
||||||
- Ensured users understand that licensing terms do not imply endorsement by the company
|
- Ensured users understand that licensing terms do not imply endorsement by the company
|
||||||
|
|
||||||
## 2025-03-25 - 2.4.0 - feat(installer)
|
## 2025-03-25 - 2.4.0 - feat(installer)
|
||||||
|
|
||||||
Add auto-yes flag to installer and update installation documentation
|
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
|
- 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)
|
## 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
|
- 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
|
- Enhanced CLI service setup flow to offer starting the NUPST service immediately after installation
|
||||||
|
|
||||||
## 2025-03-25 - 2.2.0 - feat(cli)
|
## 2025-03-25 - 2.2.0 - feat(cli)
|
||||||
|
|
||||||
Add 'config' command to display current configuration and update CLI help
|
Add 'config' command to display current configuration and update CLI help
|
||||||
|
|
||||||
- Introduce new 'config' command to show SNMP settings, thresholds, and configuration file location
|
- Introduce new 'config' command to show SNMP settings, thresholds, and configuration file location
|
||||||
- Update help text to include details for 'nupst config' command
|
- Update help text to include details for 'nupst config' command
|
||||||
|
|
||||||
## 2025-03-25 - 2.1.0 - feat(cli)
|
## 2025-03-25 - 2.1.0 - feat(cli)
|
||||||
|
|
||||||
Add uninstall command to CLI and update shutdown delay for graceful VM shutdown
|
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
|
- 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
|
- 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)
|
## 2025-03-25 - 2.0.1 - fix(cli/systemd)
|
||||||
|
|
||||||
Fix status command to pass debug flag and improve systemd status logging output
|
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/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)
|
## 2025-03-25 - 2.0.0 - BREAKING CHANGE(snmp)
|
||||||
|
|
||||||
refactor: update SNMP type definitions and interface names for consistency
|
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.
|
- Renamed SnmpConfig to ISnmpConfig, OIDSet to IOidSet, UpsStatus to IUpsStatus, and UpsModel to
|
||||||
- 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.
|
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)
|
## 2025-03-25 - 1.10.1 - fix(systemd/readme)
|
||||||
|
|
||||||
Improve README documentation and fix UPS status retrieval in systemd service
|
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
|
- Updated README features and installation instructions to clarify SNMP version support, UPS models,
|
||||||
- Modified default SNMP host to '192.168.1.100' and added 'upsModel' property in configuration examples
|
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
|
- 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
|
- Fixed systemd.ts to use a test configuration with an appropriate timeout when fetching UPS status
|
||||||
|
|
||||||
## 2025-03-25 - 1.10.0 - feat(core)
|
## 2025-03-25 - 1.10.0 - feat(core)
|
||||||
|
|
||||||
Add update checking and version logging across startup components
|
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 daemon.ts, log version info on startup and check for updates in the background using npm
|
||||||
- In nupst.ts, implement getVersion, checkForUpdates, getUpdateStatus, and compareVersions functions with update notifications
|
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
|
- Establish bidirectional reference between Nupst and NupstSnmp to support version logging
|
||||||
- Update systemd service status output to include version information
|
- Update systemd service status output to include version information
|
||||||
|
|
||||||
## 2025-03-25 - 1.9.0 - feat(cli)
|
## 2025-03-25 - 1.9.0 - feat(cli)
|
||||||
|
|
||||||
Add update command to CLI to update NUPST from repository and refresh the systemd service
|
Add update command to CLI to update NUPST from repository and refresh the systemd service
|
||||||
|
|
||||||
- Integrate 'update' subcommand in CLI command parser
|
- Integrate 'update' subcommand in CLI command parser
|
||||||
- Update documentation and help output to include new command
|
- 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)
|
## 2025-03-25 - 1.8.2 - fix(cli)
|
||||||
|
|
||||||
Refactor logs command to use child_process spawn for real-time log tailing
|
Refactor logs command to use child_process spawn for real-time log tailing
|
||||||
|
|
||||||
- Replaced execSync call with spawn to properly follow logs
|
- Replaced execSync call with spawn to properly follow logs
|
||||||
@@ -297,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
|
- Await the child process exit to ensure clean shutdown of the CLI log command
|
||||||
|
|
||||||
## 2025-03-25 - 1.8.1 - fix(systemd)
|
## 2025-03-25 - 1.8.1 - fix(systemd)
|
||||||
|
|
||||||
Update ExecStart in systemd service template to use /opt/nupst/bin/nupst for daemon startup
|
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
|
- Ensures the service uses the correct binary installation path
|
||||||
|
|
||||||
## 2025-03-25 - 1.8.0 - feat(core)
|
## 2025-03-25 - 1.8.0 - feat(core)
|
||||||
|
|
||||||
Enhance SNMP module and interactive CLI setup for UPS shutdown
|
Enhance SNMP module and interactive CLI setup for UPS shutdown
|
||||||
|
|
||||||
- Refactored SNMP packet parsing and encoding utilities for clearer error handling and debugging
|
- Refactored SNMP packet parsing and encoding utilities for clearer error handling and debugging
|
||||||
@@ -311,22 +563,28 @@ Enhance SNMP module and interactive CLI setup for UPS shutdown
|
|||||||
- Expanded test coverage with simulated SNMP responses for various response types
|
- Expanded test coverage with simulated SNMP responses for various response types
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.6 - fix(core)
|
## 2025-03-25 - 1.7.6 - fix(core)
|
||||||
|
|
||||||
Refactor SNMP, systemd, and CLI modules to improve error handling, logging, and code clarity
|
Refactor SNMP, systemd, and CLI modules to improve error handling, logging, and code clarity
|
||||||
|
|
||||||
- Removed unused dependency 'net-snmp' from package.json
|
- Removed unused dependency 'net-snmp' from package.json
|
||||||
- Extracted helper functions for SNMP packet creation and parsing (using SnmpEncoder, SnmpPacketCreator and SnmpPacketParser)
|
- Extracted helper functions for SNMP packet creation and parsing (using SnmpEncoder,
|
||||||
- Improved debug logging and added detailed documentation comments across SNMP, systemd, CLI and daemon modules
|
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
|
- 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
|
- Updated test suite to use proper modular methods from the new SNMP utilities
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.5 - fix(cli)
|
## 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
|
- Call enableDebug() on SNMP client earlier in command parsing
|
||||||
- Pass debug flag to 'daemon-start' and 'test' commands for consistent debug output
|
- Pass debug flag to 'daemon-start' and 'test' commands for consistent debug output
|
||||||
- Update package version from 1.7.3 to 1.7.4
|
- Update package version from 1.7.3 to 1.7.4
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.3 - fix(SNMP)
|
## 2025-03-25 - 1.7.3 - fix(SNMP)
|
||||||
|
|
||||||
Refine SNMP packet creation and response parsing for more reliable UPS status monitoring
|
Refine SNMP packet creation and response parsing for more reliable UPS status monitoring
|
||||||
|
|
||||||
- Improve error handling and fallback logic when parsing SNMP responses
|
- Improve error handling and fallback logic when parsing SNMP responses
|
||||||
@@ -334,13 +592,16 @@ Refine SNMP packet creation and response parsing for more reliable UPS status mo
|
|||||||
- Enhance test coverage for various UPS scenarios
|
- Enhance test coverage for various UPS scenarios
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.2 - fix(core)
|
## 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
|
- Improved fallback and error handling in SNMP response parsing
|
||||||
- Enhanced logging messages in daemon and systemd service management
|
- Enhanced logging messages in daemon and systemd service management
|
||||||
- Minor refactoring for better maintainability without functional changes
|
- Minor refactoring for better maintainability without functional changes
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.1 - fix(snmp-cli)
|
## 2025-03-25 - 1.7.1 - fix(snmp-cli)
|
||||||
|
|
||||||
Improve SNMP response parsing and CLI UPS connection timeout handling
|
Improve SNMP response parsing and CLI UPS connection timeout handling
|
||||||
|
|
||||||
- Expand parsing loop in SNMP responses to capture Gauge32 and Timeticks values
|
- Expand parsing loop in SNMP responses to capture Gauge32 and Timeticks values
|
||||||
@@ -348,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
|
- Configure CLI test commands to use a shortened timeout for UPS connection tests
|
||||||
|
|
||||||
## 2025-03-25 - 1.7.0 - feat(SNMP/UPS)
|
## 2025-03-25 - 1.7.0 - feat(SNMP/UPS)
|
||||||
|
|
||||||
Add UPS model selection and custom OIDs support to handle different UPS brands
|
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
|
- 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
|
- 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
|
- Extend default configuration with an upsModel property for consistent behavior
|
||||||
|
|
||||||
## 2025-03-25 - 1.6.0 - feat(cli,snmp)
|
## 2025-03-25 - 1.6.0 - feat(cli,snmp)
|
||||||
|
|
||||||
Enhance debug logging and add debug mode support in CLI and SNMP modules
|
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
|
- Enable debug flags (--debug, -d) in CLI to trigger detailed SNMP logging
|
||||||
@@ -364,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
|
- Improve timeout and discovery logging details for streamlined troubleshooting
|
||||||
|
|
||||||
## 2025-03-25 - 1.5.0 - feat(cli)
|
## 2025-03-25 - 1.5.0 - feat(cli)
|
||||||
|
|
||||||
Enhance CLI output: display SNMPv3 auth/priv details and support timeout customization during setup
|
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
|
- Display authentication and privacy protocol details when SNMP version is 3
|
||||||
@@ -372,10 +637,11 @@ Enhance CLI output: display SNMPv3 auth/priv details and support timeout customi
|
|||||||
- Allow users to customize SNMP timeout during interactive setup
|
- Allow users to customize SNMP timeout during interactive setup
|
||||||
|
|
||||||
## 2025-03-25 - 1.4.1 - fix(version)
|
## 2025-03-25 - 1.4.1 - fix(version)
|
||||||
|
|
||||||
Bump patch version for consistency with commit info
|
Bump patch version for consistency with commit info
|
||||||
|
|
||||||
|
|
||||||
## 2025-03-25 - 1.4.0 - feat(snmp)
|
## 2025-03-25 - 1.4.0 - feat(snmp)
|
||||||
|
|
||||||
Implement native SNMPv3 support with simulated encryption and enhanced authentication handling.
|
Implement native SNMPv3 support with simulated encryption and enhanced authentication handling.
|
||||||
|
|
||||||
- Add fully native SNMPv3 GET request implementation replacing the snmpwalk fallback
|
- Add fully native SNMPv3 GET request implementation replacing the snmpwalk fallback
|
||||||
@@ -384,12 +650,14 @@ Implement native SNMPv3 support with simulated encryption and enhanced authentic
|
|||||||
- Introduce detailed security parameter management for SNMPv3
|
- Introduce detailed security parameter management for SNMPv3
|
||||||
|
|
||||||
## 2025-03-25 - 1.3.1 - fix(cli)
|
## 2025-03-25 - 1.3.1 - fix(cli)
|
||||||
|
|
||||||
Remove redundant SNMP tools checks in CLI and Systemd modules
|
Remove redundant SNMP tools checks in CLI and Systemd modules
|
||||||
|
|
||||||
- Eliminate unnecessary snmpwalk dependency checks in the test command and interactive setup flow.
|
- Eliminate unnecessary snmpwalk dependency checks in the test command and interactive setup flow.
|
||||||
- Adjust systemd configuration file check to avoid external dependency verification.
|
- Adjust systemd configuration file check to avoid external dependency verification.
|
||||||
|
|
||||||
## 2025-03-25 - 1.3.0 - feat(cli)
|
## 2025-03-25 - 1.3.0 - feat(cli)
|
||||||
|
|
||||||
add test command to verify UPS SNMP configuration and connectivity
|
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.
|
- Introduce a new 'test' command in the CLI to check the SNMP configuration and UPS connection.
|
||||||
@@ -397,6 +665,7 @@ add test command to verify UPS SNMP configuration and connectivity
|
|||||||
- Output UPS status details and compare against defined shutdown thresholds.
|
- Output UPS status details and compare against defined shutdown thresholds.
|
||||||
|
|
||||||
## 2025-03-25 - 1.2.6 - fix(cli)
|
## 2025-03-25 - 1.2.6 - fix(cli)
|
||||||
|
|
||||||
Refactor interactive setup to use dynamic import for readline and ensure proper cleanup
|
Refactor interactive setup to use dynamic import for readline and ensure proper cleanup
|
||||||
|
|
||||||
- Replaced synchronous require() with async import for ESM compatibility
|
- Replaced synchronous require() with async import for ESM compatibility
|
||||||
@@ -404,13 +673,16 @@ Refactor interactive setup to use dynamic import for readline and ensure proper
|
|||||||
- Enhanced error logging by outputting error.message
|
- Enhanced error logging by outputting error.message
|
||||||
|
|
||||||
## 2025-03-25 - 1.2.5 - fix(error-handling)
|
## 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
|
- 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
|
- 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
|
- Enhance log messages for service installation, startup, and status retrieval for clearer debugging
|
||||||
|
|
||||||
## 2025-03-25 - 1.2.4 - fix(cli/daemon)
|
## 2025-03-25 - 1.2.4 - fix(cli/daemon)
|
||||||
|
|
||||||
Improve logging and user feedback in interactive setup and UPS monitoring
|
Improve logging and user feedback in interactive setup and UPS monitoring
|
||||||
|
|
||||||
- Refactor configuration summary output in the interactive setup for clearer display
|
- Refactor configuration summary output in the interactive setup for clearer display
|
||||||
@@ -418,17 +690,20 @@ Improve logging and user feedback in interactive setup and UPS monitoring
|
|||||||
- Improve error messages and user guidance during configuration and monitoring
|
- Improve error messages and user guidance during configuration and monitoring
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.3 - fix(nupst)
|
## 2025-03-24 - 1.2.3 - fix(nupst)
|
||||||
|
|
||||||
No changes
|
No changes
|
||||||
|
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.2 - fix(bin/nupst)
|
## 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
|
- 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
|
- Set project root to '/opt/nupst' when script is run via symlink from /usr/local/bin
|
||||||
- Add debugging comments to assist with path resolution
|
- Add debugging comments to assist with path resolution
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.1 - fix(bin)
|
## 2025-03-24 - 1.2.1 - fix(bin)
|
||||||
|
|
||||||
Simplify Node.js binary detection in installation script
|
Simplify Node.js binary detection in installation script
|
||||||
|
|
||||||
- Directly set Node binary path to vendor/node-linux-x64/bin/node
|
- Directly set Node binary path to vendor/node-linux-x64/bin/node
|
||||||
@@ -436,59 +711,78 @@ Simplify Node.js binary detection in installation script
|
|||||||
- Fallback to system Node if vendor binary is not found
|
- Fallback to system Node if vendor binary is not found
|
||||||
|
|
||||||
## 2025-03-24 - 1.2.0 - feat(installer)
|
## 2025-03-24 - 1.2.0 - feat(installer)
|
||||||
|
|
||||||
Improve Node.js binary detection and dynamic LTS version retrieval in setup scripts
|
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
|
- Enhanced bin/nupst to search multiple possible locations for the Node.js binary and fallback to
|
||||||
- Updated setup.sh to fetch the latest LTS Node.js version from nodejs.org and use a fallback version when the request fails
|
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)
|
## 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
|
- 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
|
- Updated error messages to clearly indicate failure in downloading a valid package
|
||||||
- Ensured installation halts if essential files are missing
|
- Ensured installation halts if essential files are missing
|
||||||
|
|
||||||
## 2025-03-24 - 1.1.1 - fix(package.json)
|
## 2025-03-24 - 1.1.1 - fix(package.json)
|
||||||
|
|
||||||
Remove unused prepublishOnly script and update files field in package.json
|
Remove unused prepublishOnly script and update files field in package.json
|
||||||
|
|
||||||
- Removed prepublishOnly build trigger
|
- Removed prepublishOnly build trigger
|
||||||
- Updated files list to accurately include intended directories and files
|
- Updated files list to accurately include intended directories and files
|
||||||
|
|
||||||
## 2025-03-24 - 1.1.0 - feat(installer-setup)
|
## 2025-03-24 - 1.1.0 - feat(installer-setup)
|
||||||
|
|
||||||
Enhance installer and setup scripts for improved global installation and artifact management
|
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
|
- 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)
|
## 2025-03-24 - 1.0.1 - fix(version)
|
||||||
|
|
||||||
Bump version to 1.0.1
|
Bump version to 1.0.1
|
||||||
|
|
||||||
- Updated commitinfo data to reflect the new patch version.
|
- Updated commitinfo data to reflect the new patch version.
|
||||||
- Synchronized version information between commitinfo file and package metadata.
|
- Synchronized version information between commitinfo file and package metadata.
|
||||||
|
|
||||||
## 2025-03-24 - 1.0.1 - fix(build)
|
## 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
|
- Replaced 'tsc' with 'tsbuild tsfolders --allowimplicitany' in package.json
|
||||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
- Updated .gitignore to reflect new compiled distribution folder pattern
|
||||||
- Updated changelog to document build improvements and regenerated type definitions
|
- Updated changelog to document build improvements and regenerated type definitions
|
||||||
|
|
||||||
## 2025-03-24 - 1.0.1 - fix(build)
|
## 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
|
- Replaced 'tsc' command with tsbuild in package.json
|
||||||
- Updated .gitignore to reflect new compiled distribution folder pattern
|
- 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)
|
## 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.
|
- 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.
|
- Improved the generated CLI declarations and overall distribution build.
|
||||||
|
|
||||||
## 2025-03-23 - 1.0.0 - initial setup
|
## 2025-03-23 - 1.0.0 - initial setup
|
||||||
|
|
||||||
This range covers the early commits that mainly established the repository structure.
|
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": "4.0.8",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
508
install.sh
508
install.sh
@@ -1,44 +1,69 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# NUPST Installer Script
|
# NUPST Installer Script (v4.0+)
|
||||||
# Downloads and installs NUPST globally on the system
|
# Downloads and installs pre-compiled NUPST binary from Gitea releases
|
||||||
# Can be used directly with curl:
|
#
|
||||||
# Without auto-installing dependencies:
|
# Usage:
|
||||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
# Direct piped installation (recommended):
|
||||||
# With auto-installing dependencies:
|
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y
|
#
|
||||||
#
|
# With version specification:
|
||||||
|
# curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0
|
||||||
|
#
|
||||||
# Options:
|
# 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
|
# Parse command line arguments
|
||||||
AUTO_YES=0
|
while [[ $# -gt 0 ]]; do
|
||||||
SHOW_HELP=0
|
case $1 in
|
||||||
|
|
||||||
for arg in "$@"; do
|
|
||||||
case $arg in
|
|
||||||
-y|--yes)
|
|
||||||
AUTO_YES=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
-h|--help)
|
||||||
SHOW_HELP=1
|
SHOW_HELP=1
|
||||||
shift
|
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
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $SHOW_HELP -eq 1 ]; then
|
if [ $SHOW_HELP -eq 1 ]; then
|
||||||
echo "NUPST Installer Script"
|
echo "NUPST Installer Script (v4.0+)"
|
||||||
|
echo "Downloads and installs pre-compiled NUPST binary"
|
||||||
|
echo ""
|
||||||
echo "Usage: $0 [options]"
|
echo "Usage: $0 [options]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
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., v4.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 v4.0.0"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -48,249 +73,248 @@ if [ "$EUID" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect if script is being piped or run directly
|
# Helper function to detect OS and architecture
|
||||||
PIPED=0
|
detect_platform() {
|
||||||
INTERACTIVE=1
|
local os=$(uname -s)
|
||||||
if [ ! -t 0 ]; then
|
local arch=$(uname -m)
|
||||||
# Being piped, need to clone the repo
|
|
||||||
PIPED=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if stdin is a terminal
|
# Map OS
|
||||||
if [ ! -t 0 ] || [ ! -t 1 ]; then
|
case "$os" in
|
||||||
# Either stdin or stdout is not a terminal, check if -y was provided
|
Linux)
|
||||||
if [ $AUTO_YES -ne 1 ]; then
|
os_name="linux"
|
||||||
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
|
|
||||||
;;
|
;;
|
||||||
fedora|rhel|centos|almalinux|rocky)
|
Darwin)
|
||||||
echo "Installing git using dnf/yum..."
|
os_name="macos"
|
||||||
if command -v dnf &> /dev/null; then
|
|
||||||
dnf install -y git
|
|
||||||
else
|
|
||||||
yum install -y git
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
arch|manjaro|endeavouros|garuda)
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
echo "Installing git using pacman..."
|
os_name="windows"
|
||||||
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
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unsupported OS: $OS"
|
echo "Error: Unsupported operating system: $os"
|
||||||
echo "Please install git manually and run the installer again."
|
echo "Supported: Linux, macOS, Windows"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Check if git was installed successfully
|
# Map architecture
|
||||||
if ! command -v git &> /dev/null; then
|
case "$arch" in
|
||||||
echo "Failed to install git. Please install git manually and run the installer again."
|
x86_64|amd64)
|
||||||
exit 1
|
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
|
fi
|
||||||
|
|
||||||
echo "Git installed successfully."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Define installation directory
|
# Get latest release version from Gitea API
|
||||||
INSTALL_DIR="/opt/nupst"
|
get_latest_version() {
|
||||||
REPO_URL="https://code.foss.global/serve.zone/nupst.git"
|
echo "Fetching latest release version from Gitea..." >&2
|
||||||
|
|
||||||
# Check if git is installed - needed for both piped and direct execution
|
local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest"
|
||||||
if ! command -v git &> /dev/null; then
|
local response=$(curl -sSL "$api_url" 2>/dev/null)
|
||||||
echo "Git is required but not installed."
|
|
||||||
|
if [ $? -ne 0 ] || [ -z "$response" ]; then
|
||||||
if [ $AUTO_YES -eq 1 ]; then
|
echo "Error: Failed to fetch latest release information from Gitea API" >&2
|
||||||
echo "Auto-installing git (-y flag provided)..."
|
echo "URL: $api_url" >&2
|
||||||
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"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $PIPED -eq 1 ]; then
|
# Extract tag_name from JSON response
|
||||||
echo "Installing NUPST from remote repository..."
|
local version=$(echo "$response" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
|
||||||
# Check if installation directory exists
|
if [ -z "$version" ]; then
|
||||||
if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then
|
echo "Error: Could not determine latest version from API response" >&2
|
||||||
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."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set script directory to the cloned repo
|
echo "$version"
|
||||||
SCRIPT_DIR="$INSTALL_DIR"
|
}
|
||||||
|
|
||||||
|
# Main installation process
|
||||||
|
echo "================================================"
|
||||||
|
echo " NUPST Installation Script (v4.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
|
else
|
||||||
# Running directly from within the repo or downloaded script
|
VERSION=$(get_latest_version)
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
echo "Installing latest version: $VERSION"
|
||||||
|
fi
|
||||||
# When running from a downloaded script in a different location
|
echo ""
|
||||||
# we need to clone the repository first
|
|
||||||
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
|
# Construct download URL
|
||||||
echo "Running installer from downloaded script outside repository."
|
DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||||
echo "Will clone the repository to $INSTALL_DIR..."
|
echo "Download URL: $DOWNLOAD_URL"
|
||||||
|
echo ""
|
||||||
# Create installation directory if needed
|
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
# Check if installation directory exists
|
||||||
echo "Removing previous installation at $INSTALL_DIR..."
|
SERVICE_WAS_RUNNING=0
|
||||||
rm -rf "$INSTALL_DIR"
|
OLD_NODE_INSTALL=0
|
||||||
fi
|
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
mkdir -p "$INSTALL_DIR"
|
# Check if this is an old Node.js-based installation
|
||||||
|
if [ -f "$INSTALL_DIR/package.json" ] || [ -d "$INSTALL_DIR/node_modules" ]; then
|
||||||
# Clone the repository
|
OLD_NODE_INSTALL=1
|
||||||
echo "Cloning NUPST repository to $INSTALL_DIR..."
|
echo "Detected old Node.js-based NUPST installation (v3.x or earlier)"
|
||||||
git clone --depth 1 $REPO_URL "$INSTALL_DIR"
|
echo "This installer will migrate to the new Deno-based binary version (v4.0+)"
|
||||||
|
echo ""
|
||||||
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"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Updating existing installation at $INSTALL_DIR..."
|
||||||
|
|
||||||
|
# Check if service exists (enabled or running) and stop it if active
|
||||||
|
if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then
|
||||||
|
SERVICE_WAS_RUNNING=1
|
||||||
|
if systemctl is-active --quiet nupst 2>/dev/null; then
|
||||||
|
echo "Stopping NUPST service..."
|
||||||
|
systemctl stop nupst
|
||||||
|
else
|
||||||
|
echo "Service is installed but not currently running (will be updated)..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up old Node.js installation files
|
||||||
|
if [ $OLD_NODE_INSTALL -eq 1 ]; then
|
||||||
|
echo "Cleaning up old Node.js installation files..."
|
||||||
|
rm -rf "$INSTALL_DIR/node_modules" 2>/dev/null || true
|
||||||
|
rm -rf "$INSTALL_DIR/vendor" 2>/dev/null || true
|
||||||
|
rm -rf "$INSTALL_DIR/dist_ts" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/package.json" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/package-lock.json" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/pnpm-lock.yaml" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/tsconfig.json" 2>/dev/null || true
|
||||||
|
rm -f "$INSTALL_DIR/setup.sh" 2>/dev/null || true
|
||||||
|
rm -rf "$INSTALL_DIR/bin" 2>/dev/null || true
|
||||||
|
echo "Old installation files removed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Creating installation directory: $INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run setup script
|
# Download binary
|
||||||
echo "Running setup script..."
|
echo "Downloading NUPST binary..."
|
||||||
if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then
|
TEMP_FILE="$INSTALL_DIR/nupst.download"
|
||||||
echo "ERROR: Setup script not found at $SCRIPT_DIR/setup.sh"
|
curl -sSL "$DOWNLOAD_URL" -o "$TEMP_FILE"
|
||||||
echo "Current directory: $(pwd)"
|
|
||||||
echo "Script directory: $SCRIPT_DIR"
|
if [ $? -ne 0 ]; then
|
||||||
ls -la "$SCRIPT_DIR"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bash "$SCRIPT_DIR/setup.sh"
|
# Check if download was successful (file exists and not empty)
|
||||||
|
if [ ! -s "$TEMP_FILE" ]; then
|
||||||
# Install globally
|
echo "Error: Downloaded file is empty or does not exist"
|
||||||
echo "Installing NUPST globally..."
|
rm -f "$TEMP_FILE"
|
||||||
ln -sf "$SCRIPT_DIR/bin/nupst" /usr/local/bin/nupst
|
exit 1
|
||||||
|
|
||||||
# Installation completed
|
|
||||||
if [ $PIPED -eq 1 ]; then
|
|
||||||
echo "NUPST has been installed globally at $INSTALL_DIR"
|
|
||||||
else
|
|
||||||
echo "NUPST has been installed globally."
|
|
||||||
fi
|
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 ""
|
||||||
|
|
||||||
|
# Update systemd service file if migrating from v3
|
||||||
|
if [ $SERVICE_WAS_RUNNING -eq 1 ] && [ $OLD_NODE_INSTALL -eq 1 ]; then
|
||||||
|
echo "Updating systemd service file for v4..."
|
||||||
|
$BINARY_PATH service enable > /dev/null 2>&1
|
||||||
|
echo "Service file updated."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restart service if it was running before update
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
if [ $OLD_NODE_INSTALL -eq 1 ]; then
|
||||||
|
echo "Migration from v3.x to v4.0 successful!"
|
||||||
|
echo ""
|
||||||
|
echo "What changed:"
|
||||||
|
echo " • Node.js runtime removed (now a self-contained binary)"
|
||||||
|
echo " • Faster startup and lower memory usage"
|
||||||
|
echo " • CLI commands now use subcommand structure"
|
||||||
|
echo " (old commands still work with deprecation warnings)"
|
||||||
|
echo ""
|
||||||
|
echo "See readme for migration details: https://code.foss.global/serve.zone/nupst#migration-from-v3x"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installation details:"
|
||||||
|
echo " 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 ""
|
||||||
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.2",
|
|
||||||
"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
685
readme.md
685
readme.md
@@ -1,6 +1,10 @@
|
|||||||
# NUPST - Node.js UPS Shutdown Tool
|
# NUPST - Network UPS Shutdown Tool
|
||||||
|
|
||||||
NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiates system shutdown when power outages are detected and battery levels are low.
|
NUPST is a lightweight, self-contained command-line tool that monitors SNMP-enabled UPS devices and
|
||||||
|
initiates system shutdown when power outages are detected and battery levels are low.
|
||||||
|
|
||||||
|
**Version 4.0+** is powered by Deno and distributed as pre-compiled binaries requiring zero
|
||||||
|
dependencies.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -8,52 +12,56 @@ NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiate
|
|||||||
- **Group Management**: Organize UPS devices into groups with different operating modes
|
- **Group Management**: Organize UPS devices into groups with different operating modes
|
||||||
- **Redundant Mode**: Only shutdown when ALL UPS devices in a group are in critical condition
|
- **Redundant Mode**: Only shutdown when ALL UPS devices in a group are in critical condition
|
||||||
- **Non-Redundant Mode**: Shutdown when ANY UPS device in a group is in critical condition
|
- **Non-Redundant Mode**: Shutdown when ANY UPS device in a group is in critical condition
|
||||||
- Monitors UPS devices using SNMP (v1, v2c, and v3 supported)
|
- **SNMP Protocol Support**: Full support for SNMP v1, v2c, and v3 with authentication and
|
||||||
- Automatic shutdown when battery level falls below threshold
|
encryption
|
||||||
- Automatic shutdown when runtime remaining falls below threshold
|
- **Multiple UPS Brands**: Works with CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv, and custom
|
||||||
- Supports multiple UPS brands (CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv)
|
OID configurations
|
||||||
- Simple systemd service integration
|
- **Systemd Integration**: Simple service installation and management
|
||||||
- Regular status logging for monitoring
|
- **Real-time Monitoring**: Live status updates and log viewing
|
||||||
- Real-time log viewing with journalctl
|
- **Zero Dependencies**: Single self-contained binary with no runtime requirements
|
||||||
- Version checking and automatic updates
|
- **Cross-Platform**: Binaries available for Linux (x64, ARM64), macOS (Intel, Apple Silicon), and
|
||||||
- Self-contained - includes its own Node.js runtime
|
Windows
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Quick Install (One-line command)
|
### Quick Install (Recommended)
|
||||||
|
|
||||||
|
The easiest way to install NUPST is using the automated installer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Method 1: Download and run (most reliable across all environments)
|
# One-line installation
|
||||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh && sudo bash nupst-install.sh && rm nupst-install.sh
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
The installer will:
|
||||||
# Method 2: Pipe with automatic yes for dependencies (non-interactive)
|
|
||||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y
|
1. Auto-detect your platform (OS and architecture)
|
||||||
```
|
2. Download the latest pre-compiled binary from releases
|
||||||
|
3. Install to `/opt/nupst/nupst`
|
||||||
|
4. Create a symlink in `/usr/local/bin/nupst` for global access
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
Download the appropriate binary for your platform from the
|
||||||
|
[releases page](https://code.foss.global/serve.zone/nupst/releases):
|
||||||
|
|
||||||
|
- **Linux x64**: `nupst-linux-x64`
|
||||||
|
- **Linux ARM64**: `nupst-linux-arm64`
|
||||||
|
- **macOS Intel**: `nupst-macos-x64`
|
||||||
|
- **macOS Apple Silicon**: `nupst-macos-arm64`
|
||||||
|
- **Windows x64**: `nupst-windows-x64.exe`
|
||||||
|
|
||||||
|
Then install manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Method 3: Process substitution (only on systems that support /dev/fd/)
|
# Download binary (replace with your platform)
|
||||||
# Note: This may fail on some systems with "No such file or directory" errors
|
curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/v4.0.0/nupst-linux-x64 -o nupst
|
||||||
sudo bash <(curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Direct from Git
|
# Make executable
|
||||||
|
chmod +x nupst
|
||||||
|
|
||||||
```bash
|
# Move to system path
|
||||||
# Clone the repository
|
sudo mv nupst /usr/local/bin/nupst
|
||||||
git clone https://code.foss.global/serve.zone/nupst.git
|
|
||||||
cd nupst
|
|
||||||
|
|
||||||
# Option 1: Quick install (requires root privileges)
|
|
||||||
sudo ./install.sh
|
|
||||||
|
|
||||||
# Option 1a: Quick install with auto-yes for dependencies
|
|
||||||
sudo ./install.sh -y
|
|
||||||
|
|
||||||
# Option 2: Manual setup
|
|
||||||
./setup.sh
|
|
||||||
sudo ln -s $(pwd)/bin/nupst /usr/local/bin/nupst
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Installation Options
|
### Installation Options
|
||||||
@@ -61,14 +69,19 @@ sudo ln -s $(pwd)/bin/nupst /usr/local/bin/nupst
|
|||||||
The installer script (`install.sh`) supports the following options:
|
The installer script (`install.sh`) supports the following options:
|
||||||
|
|
||||||
```
|
```
|
||||||
-y, --yes Automatically answer yes to all prompts (like installing git)
|
-h, --help Show help message
|
||||||
-h, --help Show the help message
|
--version VERSION Install specific version (e.g., --version v4.0.0)
|
||||||
|
--install-dir DIR Custom installation directory (default: /opt/nupst)
|
||||||
```
|
```
|
||||||
|
|
||||||
### From NPM
|
Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @serve.zone/nupst
|
# Install specific version
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0
|
||||||
|
|
||||||
|
# Custom installation directory
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --install-dir /usr/local/nupst
|
||||||
```
|
```
|
||||||
|
|
||||||
## System Changes
|
## System Changes
|
||||||
@@ -77,101 +90,141 @@ When installed, NUPST makes the following changes to your system:
|
|||||||
|
|
||||||
### File System Changes
|
### File System Changes
|
||||||
|
|
||||||
| Path | Description |
|
| Path | Description |
|
||||||
|------|-------------|
|
| ----------------------------------- | -------------------------------------- |
|
||||||
| `/opt/nupst/` | Main installation directory containing the NUPST files |
|
| `/opt/nupst/nupst` | Pre-compiled binary (default location) |
|
||||||
| `/etc/nupst/config.json` | Configuration file |
|
| `/etc/nupst/config.json` | Configuration file |
|
||||||
| `/usr/local/bin/nupst` | Symlink to the NUPST executable |
|
| `/usr/local/bin/nupst` | Symlink to the NUPST binary |
|
||||||
| `/etc/systemd/system/nupst.service` | Systemd service file (when enabled) |
|
| `/etc/systemd/system/nupst.service` | Systemd service file (when enabled) |
|
||||||
|
|
||||||
### Service Changes
|
### Service Changes
|
||||||
|
|
||||||
- Creates and enables a systemd service called `nupst.service` (when enabled with `nupst enable`)
|
- Creates and enables a systemd service called `nupst.service` (when enabled with
|
||||||
|
`nupst service enable`)
|
||||||
- The service runs with root permissions to allow system shutdown capabilities
|
- The service runs with root permissions to allow system shutdown capabilities
|
||||||
|
|
||||||
### Network Access
|
### Network Access
|
||||||
|
|
||||||
- NUPST only communicates with your UPS device via SNMP (default port 161)
|
- NUPST only communicates with your UPS device via SNMP (default port 161)
|
||||||
- Brief connections to npmjs.org to check for updates
|
- No external network connections required after installation
|
||||||
|
|
||||||
## Uninstallation
|
## Uninstallation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using the CLI tool:
|
# Disable and remove service first
|
||||||
sudo nupst uninstall
|
sudo nupst service disable
|
||||||
|
|
||||||
# If installed from git repository:
|
# Remove binary and config
|
||||||
cd /path/to/nupst
|
sudo rm /usr/local/bin/nupst
|
||||||
|
sudo rm /opt/nupst/nupst
|
||||||
|
sudo rm -rf /etc/nupst/
|
||||||
|
|
||||||
|
# Or use the uninstall script if installed from git
|
||||||
sudo ./uninstall.sh
|
sudo ./uninstall.sh
|
||||||
|
|
||||||
# If installed from npm:
|
|
||||||
npm uninstall -g @serve.zone/nupst
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The uninstaller will:
|
|
||||||
- Stop and disable the systemd service (if installed)
|
|
||||||
- Remove the systemd service file from `/etc/systemd/system/nupst.service`
|
|
||||||
- Remove the symlink from `/usr/local/bin/nupst`
|
|
||||||
- Optionally remove configuration files from `/etc/nupst/`
|
|
||||||
- Remove the repository directory from `/opt/nupst/` (when using `nupst uninstall`)
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
### Command Structure (v4.0+)
|
||||||
NUPST - Node.js UPS Shutdown Tool
|
|
||||||
|
|
||||||
Usage:
|
NUPST v4.0 uses a subcommand structure for better organization:
|
||||||
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 - Network UPS Shutdown Tool
|
||||||
nupst logs - Show logs of the systemd service in real-time
|
Version: 4.0.0
|
||||||
nupst stop - Stop the systemd service
|
|
||||||
nupst start - Start the systemd service
|
Usage: nupst <command> [subcommand] [options]
|
||||||
nupst status - Show status of the systemd service and UPS status
|
|
||||||
|
Service Management:
|
||||||
|
nupst service enable - Install and enable the systemd service
|
||||||
|
nupst service disable - Stop and disable the systemd service
|
||||||
|
nupst service start - Start the systemd service
|
||||||
|
nupst service stop - Stop the systemd service
|
||||||
|
nupst service restart - Restart the systemd service
|
||||||
|
nupst service status - Show service and UPS status
|
||||||
|
nupst service logs - Show service logs in real-time
|
||||||
|
nupst service start-daemon - Start daemon directly (for testing)
|
||||||
|
|
||||||
UPS Management:
|
UPS Management:
|
||||||
nupst add - Add a new UPS device
|
nupst ups add - Add a new UPS device
|
||||||
nupst edit [id] - Edit an existing UPS (default UPS if no ID provided)
|
nupst ups edit [id] - Edit a UPS device (prompts if no ID)
|
||||||
nupst delete <id> - Delete a UPS by ID
|
nupst ups remove <id> - Remove a UPS device by ID
|
||||||
nupst list - List all configured UPS devices
|
nupst ups list - List all configured UPS devices
|
||||||
nupst setup - Alias for 'nupst edit' (backward compatibility)
|
nupst ups test - Test UPS connections
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Options:
|
Group Management:
|
||||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
nupst group add - Add a new UPS group
|
||||||
(Example: nupst test --debug)
|
nupst group edit <id> - Edit a UPS group
|
||||||
|
nupst group remove <id> - Remove a UPS group
|
||||||
|
nupst group list - List all UPS groups
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
nupst config show - Display current configuration
|
||||||
|
|
||||||
|
Global Options:
|
||||||
|
--version, -v - Show version information
|
||||||
|
--help, -h - Show help message
|
||||||
|
--debug, -d - Enable debug mode for detailed logging
|
||||||
|
|
||||||
|
Aliases (for backward compatibility):
|
||||||
|
nupst ls - Alias for 'nupst ups list'
|
||||||
|
nupst rm <id> - Alias for 'nupst ups remove'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Quick Start Guide
|
||||||
|
|
||||||
|
1. **Install NUPST** (see Installation section above)
|
||||||
|
|
||||||
|
2. **Add your first UPS device:**
|
||||||
|
```bash
|
||||||
|
sudo nupst ups add
|
||||||
|
```
|
||||||
|
Follow the interactive prompts to configure your UPS.
|
||||||
|
|
||||||
|
3. **Test the configuration:**
|
||||||
|
```bash
|
||||||
|
nupst ups test
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Enable the service:**
|
||||||
|
```bash
|
||||||
|
sudo nupst service enable
|
||||||
|
sudo nupst service start
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Check status:**
|
||||||
|
```bash
|
||||||
|
nupst service status
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **View logs:**
|
||||||
|
```bash
|
||||||
|
nupst service logs
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
NUPST supports monitoring multiple UPS devices organized into groups. You can set up your UPS devices using the interactive commands:
|
NUPST supports monitoring multiple UPS devices organized into groups. The configuration file is
|
||||||
|
located at `/etc/nupst/config.json`.
|
||||||
|
|
||||||
|
### Interactive Configuration
|
||||||
|
|
||||||
|
The easiest way to configure NUPST is through the interactive commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add a new UPS device
|
# Add a new UPS device
|
||||||
nupst add
|
sudo nupst ups add
|
||||||
|
|
||||||
# Create a new group
|
# Create a group
|
||||||
nupst group add
|
sudo nupst group add
|
||||||
|
|
||||||
# Assign UPS devices to groups
|
# Assign UPS devices to groups
|
||||||
nupst group edit <group-id>
|
sudo nupst group edit <group-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration File Structure
|
### Configuration File Structure
|
||||||
|
|
||||||
The configuration file is located at `/etc/nupst/config.json`. Here's an example of a multi-UPS configuration:
|
Here's an example configuration with multiple UPS devices in a redundant group:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -217,7 +270,7 @@ The configuration file is located at `/etc/nupst/config.json`. Here's an example
|
|||||||
"id": "datacenter",
|
"id": "datacenter",
|
||||||
"name": "Data Center",
|
"name": "Data Center",
|
||||||
"mode": "redundant",
|
"mode": "redundant",
|
||||||
"description": "Main data center UPS group"
|
"description": "Main data center UPS group with redundant power"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -225,153 +278,381 @@ The configuration file is located at `/etc/nupst/config.json`. Here's an example
|
|||||||
|
|
||||||
### Configuration Fields
|
### Configuration Fields
|
||||||
|
|
||||||
|
#### Global Settings
|
||||||
|
|
||||||
- `checkInterval`: How often to check UPS status in milliseconds (default: 30000)
|
- `checkInterval`: How often to check UPS status in milliseconds (default: 30000)
|
||||||
- `upsDevices`: Array of UPS device configurations
|
|
||||||
- `id`: Unique identifier for the UPS
|
#### UPS Device Settings
|
||||||
- `name`: Friendly name for the UPS
|
|
||||||
- `snmp`: SNMP connection settings
|
- `id`: Unique identifier for the UPS
|
||||||
- `host`: IP address of your UPS (default: 127.0.0.1)
|
- `name`: Friendly name for the UPS
|
||||||
- `port`: SNMP port (default: 161)
|
- `groups`: Array of group IDs this UPS belongs to
|
||||||
- `version`: SNMP version (1, 2, or 3)
|
|
||||||
- `timeout`: Timeout in milliseconds (default: 5000)
|
**SNMP Configuration:**
|
||||||
- `upsModel`: The UPS model ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom')
|
|
||||||
- For SNMPv1/v2c:
|
- `host`: IP address or hostname of your UPS
|
||||||
- `community`: SNMP community string (default: public)
|
- `port`: SNMP port (default: 161)
|
||||||
- For SNMPv3:
|
- `version`: SNMP version (1, 2, or 3)
|
||||||
- `securityLevel`: Security level ('noAuthNoPriv', 'authNoPriv', or 'authPriv')
|
- `timeout`: Timeout in milliseconds (default: 5000)
|
||||||
- `username`: SNMPv3 username
|
- `upsModel`: UPS brand ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom')
|
||||||
- `authProtocol`: Authentication protocol ('MD5' or 'SHA')
|
|
||||||
- `authKey`: Authentication password/key
|
**For SNMPv1/v2c:**
|
||||||
- `privProtocol`: Privacy/encryption protocol ('DES' or 'AES')
|
|
||||||
- `privKey`: Privacy password/key
|
- `community`: SNMP community string (default: "public")
|
||||||
- For custom UPS models:
|
|
||||||
- `customOIDs`: Object containing custom OIDs for your UPS:
|
**For SNMPv3:**
|
||||||
- `POWER_STATUS`: OID for power status
|
|
||||||
- `BATTERY_CAPACITY`: OID for battery capacity percentage
|
- `securityLevel`: 'noAuthNoPriv', 'authNoPriv', or 'authPriv'
|
||||||
- `BATTERY_RUNTIME`: OID for runtime remaining in minutes
|
- `username`: SNMPv3 username
|
||||||
- `thresholds`: When to trigger shutdown
|
- `authProtocol`: 'MD5' or 'SHA'
|
||||||
- `battery`: Battery percentage threshold (default: 60%)
|
- `authKey`: Authentication password
|
||||||
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
- `privProtocol`: 'DES' or 'AES' (for authPriv level)
|
||||||
- `groups`: Array of group IDs this UPS belongs to
|
- `privKey`: Privacy/encryption password
|
||||||
- `groups`: Array of group configurations
|
|
||||||
- `id`: Unique identifier for the group
|
**For Custom UPS Models:**
|
||||||
- `name`: Friendly name for the group
|
|
||||||
- `mode`: Group operating mode ('redundant' or 'nonRedundant')
|
- `customOIDs`: Custom OID mappings
|
||||||
- `description`: Optional description of the group
|
- `POWER_STATUS`: OID for AC power status
|
||||||
|
- `BATTERY_CAPACITY`: OID for battery percentage
|
||||||
|
- `BATTERY_RUNTIME`: OID for runtime remaining (minutes)
|
||||||
|
|
||||||
|
**Shutdown Thresholds:**
|
||||||
|
|
||||||
|
- `battery`: Battery percentage threshold (default: 60%)
|
||||||
|
- `runtime`: Runtime minutes threshold (default: 20 minutes)
|
||||||
|
|
||||||
|
#### Group Settings
|
||||||
|
|
||||||
|
- `id`: Unique identifier for the group
|
||||||
|
- `name`: Friendly name for the group
|
||||||
|
- `mode`: Operating mode ('redundant' or 'nonRedundant')
|
||||||
|
- `description`: Optional description
|
||||||
|
|
||||||
### Group Modes
|
### Group Modes
|
||||||
|
|
||||||
- **Redundant Mode**: The system will only initiate shutdown if ALL UPS devices in the group are in critical condition (below threshold). This is ideal for redundant power setups where one UPS can keep systems running.
|
- **Redundant Mode**: System shuts down only when ALL UPS devices in the group are critical. Ideal
|
||||||
|
for setups with backup UPS units where one can maintain power.
|
||||||
- **Non-Redundant Mode**: The system will initiate shutdown if ANY UPS device in the group is in critical condition. This is useful for scenarios where all UPS devices must be operational for the system to function properly.
|
|
||||||
|
- **Non-Redundant Mode**: System shuts down when ANY UPS device in the group is critical. Used when
|
||||||
|
all UPS devices must be operational for system stability.
|
||||||
|
|
||||||
## Setup as a Service
|
## Setup as a Service
|
||||||
|
|
||||||
To set up NUPST as a systemd service:
|
Enable NUPST as a systemd service for automatic monitoring:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo nupst enable
|
# Enable and start service
|
||||||
sudo nupst start
|
sudo nupst service enable
|
||||||
```
|
sudo nupst service start
|
||||||
|
|
||||||
To check the status:
|
# Check status
|
||||||
|
nupst service status
|
||||||
|
|
||||||
```bash
|
# View real-time logs
|
||||||
nupst status
|
nupst service logs
|
||||||
```
|
|
||||||
|
|
||||||
To view logs in real-time:
|
# Stop service
|
||||||
|
sudo nupst service stop
|
||||||
|
|
||||||
```bash
|
# Disable service
|
||||||
nupst logs
|
sudo nupst service disable
|
||||||
```
|
```
|
||||||
|
|
||||||
## Updating NUPST
|
## Updating NUPST
|
||||||
|
|
||||||
NUPST checks for updates automatically and will notify you when an update is available. To update to the latest version:
|
### Automatic Update
|
||||||
|
|
||||||
|
Re-run the installer to update to the latest version:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo nupst update
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
The installer will:
|
||||||
1. Pull the latest changes from the git repository
|
|
||||||
2. Run the installation scripts
|
|
||||||
3. Force-update Node.js and all dependencies, even if they already exist
|
|
||||||
4. Refresh the systemd service configuration
|
|
||||||
5. Restart the service if it was running
|
|
||||||
|
|
||||||
You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code:
|
1. Download the latest binary
|
||||||
|
2. Replace the existing installation
|
||||||
|
3. Preserve your configuration at `/etc/nupst/config.json`
|
||||||
|
4. Restart the service if it was running
|
||||||
|
|
||||||
|
### Manual Update
|
||||||
|
|
||||||
|
1. Download the latest binary from [releases](https://code.foss.global/serve.zone/nupst/releases)
|
||||||
|
2. Replace the existing binary:
|
||||||
|
```bash
|
||||||
|
sudo nupst service stop
|
||||||
|
sudo mv nupst-linux-x64 /opt/nupst/nupst # adjust for your platform
|
||||||
|
sudo chmod +x /opt/nupst/nupst
|
||||||
|
sudo nupst service start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Checking
|
||||||
|
|
||||||
|
Check your current version:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If you're in the nupst directory:
|
nupst --version
|
||||||
bash ./setup.sh --force
|
|
||||||
|
|
||||||
# If you're in another directory, specify the full path:
|
|
||||||
bash /opt/nupst/setup.sh --force
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
NUPST was designed with security in mind:
|
NUPST is designed with security as a priority:
|
||||||
|
|
||||||
### Minimal Dependencies
|
### Architecture Security
|
||||||
|
|
||||||
- **Minimal Runtime Dependencies**: NUPST uses only one carefully selected NPM package (net-snmp) to minimize the attack surface and avoid supply chain risks while providing robust SNMP functionality.
|
- **Single Binary**: Self-contained executable with no external dependencies
|
||||||
- **Self-contained Node.js**: NUPST ships with its own Node.js binary, isolated from the system's Node.js installation. This ensures:
|
- **No Runtime Dependencies**: Unlike v3.x (Node.js), v4.0+ requires no runtime environment
|
||||||
- No dependency on system Node.js versions
|
- **Minimal Attack Surface**: Compiled Deno binary with only essential SNMP functionality
|
||||||
- Minimal external libraries that could become compromised
|
- **No Supply Chain Risk**: Pre-compiled binaries verified with SHA256 checksums
|
||||||
- Consistent, tested environment for execution
|
- **Isolated Execution**: Runs with minimal required privileges
|
||||||
- Reduced risk of dependency-based attacks
|
|
||||||
|
|
||||||
### Implementation Security
|
### SNMP Security
|
||||||
|
|
||||||
- **Privilege Separation**: Only specific commands that require elevated permissions (`enable`, `disable`, `update`) check for root access; all other functionality runs with minimal privileges.
|
- **SNMPv3 Support**: Full authentication and encryption support
|
||||||
- **Limited Network Access**: NUPST only communicates with the UPS device over SNMP and contacts npmjs.org only to check for updates.
|
- `noAuthNoPriv`: Basic access (no security)
|
||||||
- **Isolated Execution**: The application runs in its working directory (`/opt/nupst`) or specified installation location, minimizing the impact on the rest of the system.
|
- `authNoPriv`: Authentication without encryption
|
||||||
|
- `authPriv`: Full authentication and encryption (recommended)
|
||||||
### SNMP Security Features
|
- **Authentication**: MD5 or SHA protocols
|
||||||
|
- **Encryption**: DES or AES privacy protocols
|
||||||
- **SNMPv3 Support with Secure Authentication and Privacy**:
|
- **Secure Defaults**: Automatic timeout adjustment based on security level
|
||||||
- Three security levels available:
|
|
||||||
- `noAuthNoPriv`: No authentication or encryption (basic access)
|
|
||||||
- `authNoPriv`: Authentication without encryption (verifies identity)
|
|
||||||
- `authPriv`: Full authentication and encryption (most secure)
|
|
||||||
- Authentication protocols: MD5 or SHA
|
|
||||||
- Privacy/encryption protocols: DES or AES
|
|
||||||
- Automatic fallback mechanisms for compatibility
|
|
||||||
- Context support for segmented SNMP deployments
|
|
||||||
- Configurable timeouts based on security level
|
|
||||||
- **Graceful degradation**: If authentication or privacy details are missing or invalid, NUPST will automatically fall back to a lower security level while logging appropriate warnings.
|
|
||||||
- **Interactive setup**: Guided setup process to properly configure SNMPv3 security settings with clear explanations of each security option.
|
|
||||||
|
|
||||||
### Installation Security
|
### Installation Security
|
||||||
|
|
||||||
- The installation script can be reviewed before execution (`curl -sSL [url] | less`)
|
- **Checksum Verification**: SHA256SUMS.txt provided for all releases
|
||||||
- All setup scripts download only verified versions and check integrity
|
- **Transparent Installation**: Standard locations with clear documentation
|
||||||
- Installation is transparent and places files in standard locations (`/opt/nupst`, `/usr/local/bin`, `/etc/systemd/system`)
|
- **Minimal Permissions**: Only systemd operations require root access
|
||||||
- Automatically detects platform architecture and OS for proper binary selection
|
- **Source Available**: Full source code available for audit
|
||||||
- Installs production dependencies locally without requiring global npm packages
|
|
||||||
|
|
||||||
### Audit and Review
|
### Network Security
|
||||||
|
|
||||||
The codebase is small, focused, and designed to be easily auditable. All code is open source and available for review.
|
- **Local-Only Communication**: Only connects to UPS devices on local network
|
||||||
|
- **No Telemetry**: No data sent to external servers
|
||||||
|
- **No Update Checks**: Manual update process only
|
||||||
|
|
||||||
|
### Verifying Downloads
|
||||||
|
|
||||||
|
All releases include SHA256 checksums:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download binary and checksums
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/v4.0.0/nupst-linux-x64 -o nupst
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/v4.0.0/SHA256SUMS.txt -o SHA256SUMS.txt
|
||||||
|
|
||||||
|
# Verify checksum
|
||||||
|
sha256sum -c SHA256SUMS.txt --ignore-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from v3.x
|
||||||
|
|
||||||
|
If you're upgrading from NUPST v3.x (Node.js-based) to v4.0 (Deno-based), the migration is
|
||||||
|
straightforward using the install.sh script.
|
||||||
|
|
||||||
|
### Quick Migration
|
||||||
|
|
||||||
|
The installer script automatically handles the entire migration while preserving your configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the installer (handles stop/update/restart automatically)
|
||||||
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
nupst service status
|
||||||
|
```
|
||||||
|
|
||||||
|
**That's it!** The installer automatically:
|
||||||
|
|
||||||
|
- Detects your v3.x installation
|
||||||
|
- Stops the running service
|
||||||
|
- Replaces the binary with v4.0
|
||||||
|
- Restarts the service
|
||||||
|
- Preserves your `/etc/nupst/config.json` (fully compatible, no changes needed)
|
||||||
|
|
||||||
|
### Key Changes in v4.0
|
||||||
|
|
||||||
|
- **Runtime**: Node.js → Deno
|
||||||
|
- **Distribution**: Git repository + npm packages → Pre-compiled binaries
|
||||||
|
- **Installation**: Clone + setup.sh → Download binary via install.sh
|
||||||
|
- **Dependencies**: Node.js + npm packages → Zero dependencies (self-contained binary)
|
||||||
|
- **CLI Structure**: Flat commands → Subcommand structure (backward compatible)
|
||||||
|
- **Updates**: `nupst update` → Re-run install.sh
|
||||||
|
- **Footprint**: Single ~80MB self-contained binary (vs repo + node_modules in v3.x)
|
||||||
|
- **Startup**: Seconds → Milliseconds
|
||||||
|
|
||||||
|
### Command Mapping
|
||||||
|
|
||||||
|
v4.0 uses a new subcommand structure, but **old commands still work** with deprecation warnings:
|
||||||
|
|
||||||
|
| v3.x Command | v4.0 Command | Notes |
|
||||||
|
| ------------------- | ----------------------- | ---------------------- |
|
||||||
|
| `nupst enable` | `nupst service enable` | Old works with warning |
|
||||||
|
| `nupst disable` | `nupst service disable` | Old works with warning |
|
||||||
|
| `nupst start` | `nupst service start` | Old works with warning |
|
||||||
|
| `nupst stop` | `nupst service stop` | Old works with warning |
|
||||||
|
| `nupst status` | `nupst service status` | Old works with warning |
|
||||||
|
| `nupst logs` | `nupst service logs` | Old works with warning |
|
||||||
|
| `nupst add` | `nupst ups add` | Old works with warning |
|
||||||
|
| `nupst edit [id]` | `nupst ups edit [id]` | Old works with warning |
|
||||||
|
| `nupst delete <id>` | `nupst ups remove <id>` | Old works with warning |
|
||||||
|
| `nupst list` | `nupst ups list` | Old works with warning |
|
||||||
|
| `nupst test` | `nupst ups test` | Old works with warning |
|
||||||
|
| `nupst config` | `nupst config show` | Old works with warning |
|
||||||
|
|
||||||
|
**New aliases:** `nupst ls` (list UPS devices), `nupst rm <id>` (remove UPS device)
|
||||||
|
|
||||||
|
### Configuration Compatibility
|
||||||
|
|
||||||
|
✅ **Fully Compatible:**
|
||||||
|
|
||||||
|
- Configuration file format: `/etc/nupst/config.json`
|
||||||
|
- All SNMP settings (host, port, community, version, security)
|
||||||
|
- UPS device configurations (IDs, names, thresholds, groups)
|
||||||
|
- Group configurations (redundant/non-redundant modes)
|
||||||
|
- Supported UPS models (CyberPower, APC, Eaton, TrippLite, Liebert, custom OIDs)
|
||||||
|
|
||||||
|
### Troubleshooting Migration
|
||||||
|
|
||||||
|
**Service won't start after migration:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Re-enable service to update systemd file
|
||||||
|
sudo nupst service disable
|
||||||
|
sudo nupst service enable
|
||||||
|
sudo nupst service start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Binary won't execute:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod +x /opt/nupst/nupst
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command not found:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recreate symlink
|
||||||
|
sudo ln -sf /opt/nupst/nupst /usr/local/bin/nupst
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Binary Won't Execute
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure it's executable
|
||||||
|
chmod +x /opt/nupst/nupst
|
||||||
|
|
||||||
|
# Check architecture matches your system
|
||||||
|
uname -m # Should match binary (x86_64 = x64, aarch64 = arm64)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check service status
|
||||||
|
sudo systemctl status nupst
|
||||||
|
|
||||||
|
# Check logs for errors
|
||||||
|
sudo journalctl -u nupst -n 50
|
||||||
|
|
||||||
|
# Verify configuration
|
||||||
|
nupst config show
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't Connect to UPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test SNMP connectivity
|
||||||
|
nupst ups test --debug
|
||||||
|
|
||||||
|
# Check network connectivity
|
||||||
|
ping <ups-ip-address>
|
||||||
|
|
||||||
|
# Verify SNMP port is accessible
|
||||||
|
nc -zv <ups-ip-address> 161
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Denied Errors
|
||||||
|
|
||||||
|
Most operations that modify the system require root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Service management
|
||||||
|
sudo nupst service enable
|
||||||
|
sudo nupst service start
|
||||||
|
|
||||||
|
# Configuration changes
|
||||||
|
sudo nupst ups add
|
||||||
|
sudo nupst group add
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
|
||||||
|
- [Deno](https://deno.land/) v1.x or later
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://code.foss.global/serve.zone/nupst.git
|
||||||
|
cd nupst
|
||||||
|
|
||||||
|
# Run directly with Deno
|
||||||
|
deno run --allow-all mod.ts help
|
||||||
|
|
||||||
|
# Compile for current platform
|
||||||
|
deno compile --allow-all --output nupst mod.ts
|
||||||
|
|
||||||
|
# Compile for all platforms
|
||||||
|
bash scripts/compile-all.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno test --allow-all tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Issues**: [Report bugs or request features](https://code.foss.global/serve.zone/nupst/issues)
|
||||||
|
- **Documentation**: [Full documentation](https://code.foss.global/serve.zone/nupst)
|
||||||
|
- **Source Code**: [View source](https://code.foss.global/serve.zone/nupst)
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
This repository contains open-source code licensed under the MIT License. A copy of the MIT License
|
||||||
|
can be found in the [license](license) file within this repository.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks,
|
||||||
|
service marks, or product names of the project, except as required for reasonable and customary use
|
||||||
|
in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated
|
||||||
|
with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture
|
||||||
|
Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these
|
||||||
|
trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be
|
||||||
|
approved in writing by Task Venture Capital GmbH.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH Registered at District court Bremen HRB 35230 HB, Germany
|
||||||
Registered at District court Bremen HRB 35230 HB, Germany
|
|
||||||
|
|
||||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or if you require further information, please contact us via email at
|
||||||
|
hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its
|
||||||
|
terms, and understand that the licensing of the code does not imply endorsement by Task Venture
|
||||||
|
Capital GmbH of any derivative works.
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
# NUPST Migration Plan: Node.js → Deno v4.0.0
|
# NUPST Migration Plan: Node.js → Deno v4.0.0
|
||||||
|
|
||||||
**Migration Goal**: Convert NUPST from Node.js to Deno with single-executable distribution
|
**Migration Goal**: Convert NUPST from Node.js to Deno with single-executable distribution
|
||||||
**Version**: 3.1.2 → 4.0.0 (breaking changes)
|
**Version**: 3.1.2 → 4.0.0 (breaking changes) **Platforms**: Linux x64/ARM64, macOS x64/ARM64,
|
||||||
**Platforms**: Linux x64/ARM64, macOS x64/ARM64, Windows x64
|
Windows x64
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
## Phase 1: Dependency Migration (4-6 hours)
|
## Phase 1: Dependency Migration (4-6 hours)
|
||||||
|
|
||||||
### 1.1 Analyze Current Dependencies
|
### 1.1 Analyze Current Dependencies
|
||||||
|
|
||||||
- [ ] List all production dependencies from `package.json`
|
- [ ] List all production dependencies from `package.json`
|
||||||
- Current: `net-snmp@3.20.0`
|
- Current: `net-snmp@3.20.0`
|
||||||
- [ ] List all dev dependencies to be removed
|
- [ ] List all dev dependencies to be removed
|
||||||
- `@git.zone/tsbuild`, `@git.zone/tsrun`, `@git.zone/tstest`, `@push.rocks/qenv`, `@push.rocks/tapbundle`, `@types/node`
|
- `@git.zone/tsbuild`, `@git.zone/tsrun`, `@git.zone/tstest`, `@push.rocks/qenv`,
|
||||||
|
`@push.rocks/tapbundle`, `@types/node`
|
||||||
- [ ] Identify Node.js built-in module usage
|
- [ ] Identify Node.js built-in module usage
|
||||||
- `child_process` (execSync)
|
- `child_process` (execSync)
|
||||||
- `https` (for version checking)
|
- `https` (for version checking)
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
- `path` (join, dirname, resolve)
|
- `path` (join, dirname, resolve)
|
||||||
|
|
||||||
### 1.2 Create Deno Configuration
|
### 1.2 Create Deno Configuration
|
||||||
|
|
||||||
- [ ] Create `deno.json` with project configuration
|
- [ ] Create `deno.json` with project configuration
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -68,15 +71,20 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 1.3 Update Import Statements
|
### 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/snmp/manager.ts`: Change `import * as snmp from 'net-snmp'` to
|
||||||
- [ ] `ts/nupst.ts`: Change `import * as https from 'https'` to `import * as https from "node:https"`
|
`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 `fs` imports and update to `node:fs`
|
||||||
- [ ] Search for all `path` imports and update to `node:path`
|
- [ ] Search for all `path` imports and update to `node:path`
|
||||||
- [ ] Update all relative imports to use `.ts` extension instead of `.js`
|
- [ ] Update all relative imports to use `.ts` extension instead of `.js`
|
||||||
- Example: `'./nupst.js'` → `'./nupst.ts'`
|
- Example: `'./nupst.js'` → `'./nupst.ts'`
|
||||||
|
|
||||||
### 1.4 Test npm: Specifier Compatibility
|
### 1.4 Test npm: Specifier Compatibility
|
||||||
|
|
||||||
- [ ] Create test file: `tests/snmp_compatibility_test.ts`
|
- [ ] Create test file: `tests/snmp_compatibility_test.ts`
|
||||||
- [ ] Test SNMP v1 connection with npm:net-snmp
|
- [ ] Test SNMP v1 connection with npm:net-snmp
|
||||||
- [ ] Test SNMP v2c connection with npm:net-snmp
|
- [ ] Test SNMP v2c connection with npm:net-snmp
|
||||||
@@ -88,6 +96,7 @@
|
|||||||
## Phase 2: Code Structure Refactoring (3-4 hours)
|
## Phase 2: Code Structure Refactoring (3-4 hours)
|
||||||
|
|
||||||
### 2.1 Create Main Entry Point
|
### 2.1 Create Main Entry Point
|
||||||
|
|
||||||
- [ ] Create `mod.ts` as main Deno entry point:
|
- [ ] Create `mod.ts` as main Deno entry point:
|
||||||
```typescript
|
```typescript
|
||||||
#!/usr/bin/env -S deno run --allow-all
|
#!/usr/bin/env -S deno run --allow-all
|
||||||
@@ -111,7 +120,9 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 Update All Import Extensions
|
### 2.2 Update All Import Extensions
|
||||||
|
|
||||||
Files to update (change .js → .ts in imports):
|
Files to update (change .js → .ts in imports):
|
||||||
|
|
||||||
- [ ] `ts/index.ts`
|
- [ ] `ts/index.ts`
|
||||||
- [ ] `ts/cli.ts` (imports from ./nupst.js, ./logger.js)
|
- [ ] `ts/cli.ts` (imports from ./nupst.js, ./logger.js)
|
||||||
- [ ] `ts/nupst.ts` (imports from ./snmp/manager.js, ./daemon.js, etc.)
|
- [ ] `ts/nupst.ts` (imports from ./snmp/manager.js, ./daemon.js, etc.)
|
||||||
@@ -127,10 +138,13 @@ Files to update (change .js → .ts in imports):
|
|||||||
- [ ] `ts/logger.ts`
|
- [ ] `ts/logger.ts`
|
||||||
|
|
||||||
### 2.3 Update process.argv References
|
### 2.3 Update process.argv References
|
||||||
- [ ] `ts/cli.ts`: Replace `process.argv` with `Deno.args` (adjust indexing: process.argv[2] → Deno.args[0])
|
|
||||||
|
- [ ] `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)
|
- [ ] Update parseAndExecute method to work with Deno.args (0-indexed vs 2-indexed)
|
||||||
|
|
||||||
### 2.4 Update File System Operations
|
### 2.4 Update File System Operations
|
||||||
|
|
||||||
- [ ] Search for `fs.readFileSync()` → Consider using `Deno.readTextFile()` or keep node:fs
|
- [ ] 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.writeFileSync()` → Consider using `Deno.writeTextFile()` or keep node:fs
|
||||||
- [ ] Search for `fs.existsSync()` → Keep node:fs or use Deno.stat
|
- [ ] Search for `fs.existsSync()` → Keep node:fs or use Deno.stat
|
||||||
@@ -138,10 +152,12 @@ Files to update (change .js → .ts in imports):
|
|||||||
- [ ] Decision: Keep node:fs for consistency or migrate to Deno APIs?
|
- [ ] Decision: Keep node:fs for consistency or migrate to Deno APIs?
|
||||||
|
|
||||||
### 2.5 Update Path Operations
|
### 2.5 Update Path Operations
|
||||||
|
|
||||||
- [ ] Verify all `path.join()`, `path.resolve()`, `path.dirname()` work with node:path
|
- [ ] Verify all `path.join()`, `path.resolve()`, `path.dirname()` work with node:path
|
||||||
- [ ] Consider using `@std/path` from JSR for better Deno integration
|
- [ ] Consider using `@std/path` from JSR for better Deno integration
|
||||||
|
|
||||||
### 2.6 Handle __dirname and __filename
|
### 2.6 Handle __dirname and __filename
|
||||||
|
|
||||||
- [ ] Find all `__dirname` usage
|
- [ ] Find all `__dirname` usage
|
||||||
- [ ] Replace with `import.meta.dirname` (Deno) or `dirname(fromFileUrl(import.meta.url))`
|
- [ ] Replace with `import.meta.dirname` (Deno) or `dirname(fromFileUrl(import.meta.url))`
|
||||||
- [ ] Find all `__filename` usage
|
- [ ] Find all `__filename` usage
|
||||||
@@ -152,7 +168,9 @@ Files to update (change .js → .ts in imports):
|
|||||||
## Phase 3: CLI Command Simplification (3-4 hours)
|
## Phase 3: CLI Command Simplification (3-4 hours)
|
||||||
|
|
||||||
### 3.1 Design New Command Structure
|
### 3.1 Design New Command Structure
|
||||||
|
|
||||||
Current → New mapping:
|
Current → New mapping:
|
||||||
|
|
||||||
```
|
```
|
||||||
OLD NEW
|
OLD NEW
|
||||||
=== ===
|
=== ===
|
||||||
@@ -184,6 +202,7 @@ nupst help → nupst help / nupst --help
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 Update CLI Parser (ts/cli.ts)
|
### 3.2 Update CLI Parser (ts/cli.ts)
|
||||||
|
|
||||||
- [ ] Refactor `parseAndExecute()` to handle new command structure
|
- [ ] Refactor `parseAndExecute()` to handle new command structure
|
||||||
- [ ] Add `service` subcommand handler
|
- [ ] Add `service` subcommand handler
|
||||||
- [ ] Add `ups` subcommand handler
|
- [ ] Add `ups` subcommand handler
|
||||||
@@ -195,11 +214,13 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Add `--json` flag for machine-readable output (future enhancement)
|
- [ ] Add `--json` flag for machine-readable output (future enhancement)
|
||||||
|
|
||||||
### 3.3 Update Command Handlers
|
### 3.3 Update Command Handlers
|
||||||
|
|
||||||
- [ ] `ts/cli/service-handler.ts`: Update method names if needed
|
- [ ] `ts/cli/service-handler.ts`: Update method names if needed
|
||||||
- [ ] `ts/cli/ups-handler.ts`: Rename `delete()` → `remove()`, remove `setup` method
|
- [ ] `ts/cli/ups-handler.ts`: Rename `delete()` → `remove()`, remove `setup` method
|
||||||
- [ ] `ts/cli/group-handler.ts`: Rename `delete()` → `remove()`
|
- [ ] `ts/cli/group-handler.ts`: Rename `delete()` → `remove()`
|
||||||
|
|
||||||
### 3.4 Improve Help Messages
|
### 3.4 Improve Help Messages
|
||||||
|
|
||||||
- [ ] Update `showHelp()` in ts/cli.ts with new command structure
|
- [ ] Update `showHelp()` in ts/cli.ts with new command structure
|
||||||
- [ ] Update `showGroupHelp()` in ts/cli.ts
|
- [ ] Update `showGroupHelp()` in ts/cli.ts
|
||||||
- [ ] Add `showServiceHelp()` method
|
- [ ] Add `showServiceHelp()` method
|
||||||
@@ -208,6 +229,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Include usage examples in help text
|
- [ ] Include usage examples in help text
|
||||||
|
|
||||||
### 3.5 Add Version Command
|
### 3.5 Add Version Command
|
||||||
|
|
||||||
- [ ] Read version from deno.json
|
- [ ] Read version from deno.json
|
||||||
- [ ] Create `--version` handler in CLI
|
- [ ] Create `--version` handler in CLI
|
||||||
- [ ] Display version with build info
|
- [ ] Display version with build info
|
||||||
@@ -217,6 +239,7 @@ nupst help → nupst help / nupst --help
|
|||||||
## Phase 4: Compilation & Distribution (2-3 hours)
|
## Phase 4: Compilation & Distribution (2-3 hours)
|
||||||
|
|
||||||
### 4.1 Create Compilation Script
|
### 4.1 Create Compilation Script
|
||||||
|
|
||||||
- [ ] Create directory: `scripts/`
|
- [ ] Create directory: `scripts/`
|
||||||
- [ ] Create `scripts/compile-all.sh`:
|
- [ ] Create `scripts/compile-all.sh`:
|
||||||
```bash
|
```bash
|
||||||
@@ -261,12 +284,14 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Make script executable: `chmod +x scripts/compile-all.sh`
|
- [ ] Make script executable: `chmod +x scripts/compile-all.sh`
|
||||||
|
|
||||||
### 4.2 Test Local Compilation
|
### 4.2 Test Local Compilation
|
||||||
|
|
||||||
- [ ] Run `deno task compile` to compile for all platforms
|
- [ ] Run `deno task compile` to compile for all platforms
|
||||||
- [ ] Verify all 5 binaries are created
|
- [ ] Verify all 5 binaries are created
|
||||||
- [ ] Check binary sizes (should be reasonable, < 100MB each)
|
- [ ] Check binary sizes (should be reasonable, < 100MB each)
|
||||||
- [ ] Test local binary on current platform: `./dist/binaries/nupst-linux-x64 --version`
|
- [ ] Test local binary on current platform: `./dist/binaries/nupst-linux-x64 --version`
|
||||||
|
|
||||||
### 4.3 Update Installation Scripts
|
### 4.3 Update Installation Scripts
|
||||||
|
|
||||||
- [ ] Update `install.sh`:
|
- [ ] Update `install.sh`:
|
||||||
- Remove Node.js download logic (lines dealing with vendor/node-*)
|
- Remove Node.js download logic (lines dealing with vendor/node-*)
|
||||||
- Add detection for binary download from GitHub releases
|
- Add detection for binary download from GitHub releases
|
||||||
@@ -283,6 +308,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- Update paths to new binary location
|
- Update paths to new binary location
|
||||||
|
|
||||||
### 4.4 Update Systemd Service
|
### 4.4 Update Systemd Service
|
||||||
|
|
||||||
- [ ] Update systemd service file path in `ts/systemd.ts`
|
- [ ] Update systemd service file path in `ts/systemd.ts`
|
||||||
- [ ] Verify ExecStart points to correct binary location: `/opt/nupst/bin/nupst daemon-start`
|
- [ ] Verify ExecStart points to correct binary location: `/opt/nupst/bin/nupst daemon-start`
|
||||||
- [ ] Remove Node.js environment variables if any
|
- [ ] Remove Node.js environment variables if any
|
||||||
@@ -293,6 +319,7 @@ nupst help → nupst help / nupst --help
|
|||||||
## Phase 5: Testing & Validation (4-6 hours)
|
## Phase 5: Testing & Validation (4-6 hours)
|
||||||
|
|
||||||
### 5.1 Create Deno Test Suite
|
### 5.1 Create Deno Test Suite
|
||||||
|
|
||||||
- [ ] Create `tests/` directory (or migrate from existing `test/`)
|
- [ ] Create `tests/` directory (or migrate from existing `test/`)
|
||||||
- [ ] Create `tests/snmp_test.ts`: Test SNMP manager functionality
|
- [ ] Create `tests/snmp_test.ts`: Test SNMP manager functionality
|
||||||
- [ ] Create `tests/config_test.ts`: Test configuration loading/saving
|
- [ ] Create `tests/config_test.ts`: Test configuration loading/saving
|
||||||
@@ -302,6 +329,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Use Deno's built-in test runner (`Deno.test()`)
|
- [ ] Use Deno's built-in test runner (`Deno.test()`)
|
||||||
|
|
||||||
### 5.2 Unit Tests
|
### 5.2 Unit Tests
|
||||||
|
|
||||||
- [ ] Test SNMP connection with mock responses
|
- [ ] Test SNMP connection with mock responses
|
||||||
- [ ] Test configuration validation
|
- [ ] Test configuration validation
|
||||||
- [ ] Test UPS status parsing for different models
|
- [ ] Test UPS status parsing for different models
|
||||||
@@ -310,6 +338,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Test version comparison logic
|
- [ ] Test version comparison logic
|
||||||
|
|
||||||
### 5.3 Integration Tests
|
### 5.3 Integration Tests
|
||||||
|
|
||||||
- [ ] Test CLI command parsing for all commands
|
- [ ] Test CLI command parsing for all commands
|
||||||
- [ ] Test config file creation and updates
|
- [ ] Test config file creation and updates
|
||||||
- [ ] Test UPS add/edit/remove operations
|
- [ ] Test UPS add/edit/remove operations
|
||||||
@@ -317,6 +346,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Mock systemd operations for testing
|
- [ ] Mock systemd operations for testing
|
||||||
|
|
||||||
### 5.4 Binary Testing
|
### 5.4 Binary Testing
|
||||||
|
|
||||||
- [ ] Test compiled binary on Linux x64
|
- [ ] Test compiled binary on Linux x64
|
||||||
- [ ] Test compiled binary on Linux ARM64 (if available)
|
- [ ] Test compiled binary on Linux ARM64 (if available)
|
||||||
- [ ] Test compiled binary on macOS x64 (if available)
|
- [ ] Test compiled binary on macOS x64 (if available)
|
||||||
@@ -327,6 +357,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Test systemd integration with compiled binary
|
- [ ] Test systemd integration with compiled binary
|
||||||
|
|
||||||
### 5.5 Performance Testing
|
### 5.5 Performance Testing
|
||||||
|
|
||||||
- [ ] Measure binary size for each platform
|
- [ ] Measure binary size for each platform
|
||||||
- [ ] Measure startup time: `time ./nupst-linux-x64 --version`
|
- [ ] Measure startup time: `time ./nupst-linux-x64 --version`
|
||||||
- [ ] Measure memory footprint during daemon operation
|
- [ ] Measure memory footprint during daemon operation
|
||||||
@@ -334,6 +365,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Document performance metrics
|
- [ ] Document performance metrics
|
||||||
|
|
||||||
### 5.6 Upgrade Path Testing
|
### 5.6 Upgrade Path Testing
|
||||||
|
|
||||||
- [ ] Create test with v3.x config
|
- [ ] Create test with v3.x config
|
||||||
- [ ] Verify v4.x can read existing config
|
- [ ] Verify v4.x can read existing config
|
||||||
- [ ] Test migration from old commands to new commands
|
- [ ] Test migration from old commands to new commands
|
||||||
@@ -344,6 +376,7 @@ nupst help → nupst help / nupst --help
|
|||||||
## Phase 6: Distribution Strategy (2-3 hours)
|
## Phase 6: Distribution Strategy (2-3 hours)
|
||||||
|
|
||||||
### 6.1 GitHub Actions Workflow
|
### 6.1 GitHub Actions Workflow
|
||||||
|
|
||||||
- [ ] Create `.github/workflows/release.yml`:
|
- [ ] Create `.github/workflows/release.yml`:
|
||||||
```yaml
|
```yaml
|
||||||
name: Release
|
name: Release
|
||||||
@@ -373,6 +406,7 @@ nupst help → nupst help / nupst --help
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 6.2 Update package.json for npm
|
### 6.2 Update package.json for npm
|
||||||
|
|
||||||
- [ ] Update version to 4.0.0
|
- [ ] Update version to 4.0.0
|
||||||
- [ ] Update description to mention Deno
|
- [ ] Update description to mention Deno
|
||||||
- [ ] Add postinstall script to symlink appropriate binary:
|
- [ ] Add postinstall script to symlink appropriate binary:
|
||||||
@@ -398,6 +432,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Create `bin/nupst-npm-wrapper.js` as entry point
|
- [ ] Create `bin/nupst-npm-wrapper.js` as entry point
|
||||||
|
|
||||||
### 6.3 Verify Distribution Methods
|
### 6.3 Verify Distribution Methods
|
||||||
|
|
||||||
- [ ] Test GitHub release download and installation
|
- [ ] Test GitHub release download and installation
|
||||||
- [ ] Test npm install from tarball
|
- [ ] Test npm install from tarball
|
||||||
- [ ] Test direct install.sh script
|
- [ ] Test direct install.sh script
|
||||||
@@ -408,6 +443,7 @@ nupst help → nupst help / nupst --help
|
|||||||
## Phase 7: Documentation Updates (2-3 hours)
|
## Phase 7: Documentation Updates (2-3 hours)
|
||||||
|
|
||||||
### 7.1 Update README.md
|
### 7.1 Update README.md
|
||||||
|
|
||||||
- [ ] Remove Node.js requirements section
|
- [ ] Remove Node.js requirements section
|
||||||
- [ ] Update features list (mention Deno, single executable)
|
- [ ] Update features list (mention Deno, single executable)
|
||||||
- [ ] Update installation methods:
|
- [ ] Update installation methods:
|
||||||
@@ -422,6 +458,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Update uninstallation instructions
|
- [ ] Update uninstallation instructions
|
||||||
|
|
||||||
### 7.2 Create MIGRATION.md
|
### 7.2 Create MIGRATION.md
|
||||||
|
|
||||||
- [ ] Create detailed migration guide from v3.x to v4.x
|
- [ ] Create detailed migration guide from v3.x to v4.x
|
||||||
- [ ] List all breaking changes:
|
- [ ] List all breaking changes:
|
||||||
1. CLI command structure reorganization
|
1. CLI command structure reorganization
|
||||||
@@ -434,6 +471,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Add rollback instructions
|
- [ ] Add rollback instructions
|
||||||
|
|
||||||
### 7.3 Update CHANGELOG.md
|
### 7.3 Update CHANGELOG.md
|
||||||
|
|
||||||
- [ ] Add v4.0.0 section with all breaking changes
|
- [ ] Add v4.0.0 section with all breaking changes
|
||||||
- [ ] List new features (Deno, single executable)
|
- [ ] List new features (Deno, single executable)
|
||||||
- [ ] List improvements (startup time, binary size)
|
- [ ] List improvements (startup time, binary size)
|
||||||
@@ -441,6 +479,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Migration guide reference
|
- [ ] Migration guide reference
|
||||||
|
|
||||||
### 7.4 Update Help Text
|
### 7.4 Update Help Text
|
||||||
|
|
||||||
- [ ] Ensure all help commands show new structure
|
- [ ] Ensure all help commands show new structure
|
||||||
- [ ] Add examples for common operations
|
- [ ] Add examples for common operations
|
||||||
- [ ] Include migration notes in help output
|
- [ ] Include migration notes in help output
|
||||||
@@ -450,6 +489,7 @@ nupst help → nupst help / nupst --help
|
|||||||
## Phase 8: Cleanup & Finalization (1 hour)
|
## Phase 8: Cleanup & Finalization (1 hour)
|
||||||
|
|
||||||
### 8.1 Remove Obsolete Files
|
### 8.1 Remove Obsolete Files
|
||||||
|
|
||||||
- [ ] Delete `vendor/` directory (Node.js binaries)
|
- [ ] Delete `vendor/` directory (Node.js binaries)
|
||||||
- [ ] Delete `dist/` directory (old compiled JS)
|
- [ ] Delete `dist/` directory (old compiled JS)
|
||||||
- [ ] Delete `dist_ts/` directory (old compiled TS)
|
- [ ] Delete `dist_ts/` directory (old compiled TS)
|
||||||
@@ -460,6 +500,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Delete `pnpm-lock.yaml`
|
- [ ] Delete `pnpm-lock.yaml`
|
||||||
|
|
||||||
### 8.2 Update Git Configuration
|
### 8.2 Update Git Configuration
|
||||||
|
|
||||||
- [ ] Update `.gitignore`:
|
- [ ] Update `.gitignore`:
|
||||||
```
|
```
|
||||||
# Deno
|
# Deno
|
||||||
@@ -480,6 +521,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Create `.denoignore` if needed
|
- [ ] Create `.denoignore` if needed
|
||||||
|
|
||||||
### 8.3 Final Validation
|
### 8.3 Final Validation
|
||||||
|
|
||||||
- [ ] Run `deno check mod.ts` - verify no type errors
|
- [ ] Run `deno check mod.ts` - verify no type errors
|
||||||
- [ ] Run `deno lint` - verify code quality
|
- [ ] Run `deno lint` - verify code quality
|
||||||
- [ ] Run `deno fmt --check` - verify formatting
|
- [ ] Run `deno fmt --check` - verify formatting
|
||||||
@@ -488,6 +530,7 @@ nupst help → nupst help / nupst --help
|
|||||||
- [ ] Test each binary manually
|
- [ ] Test each binary manually
|
||||||
|
|
||||||
### 8.4 Prepare for Release
|
### 8.4 Prepare for Release
|
||||||
|
|
||||||
- [ ] Create git tag: `v4.0.0`
|
- [ ] Create git tag: `v4.0.0`
|
||||||
- [ ] Push to main branch
|
- [ ] Push to main branch
|
||||||
- [ ] Push tags to trigger release workflow
|
- [ ] Push tags to trigger release workflow
|
||||||
@@ -502,6 +545,7 @@ nupst help → nupst help / nupst --help
|
|||||||
## Rollback Strategy
|
## Rollback Strategy
|
||||||
|
|
||||||
If critical issues are discovered:
|
If critical issues are discovered:
|
||||||
|
|
||||||
- [ ] Keep `v3.1.2` tag available for rollback
|
- [ ] Keep `v3.1.2` tag available for rollback
|
||||||
- [ ] Create `v3-stable` branch for continued v3 maintenance
|
- [ ] Create `v3-stable` branch for continued v3 maintenance
|
||||||
- [ ] Update install.sh to offer v3/v4 choice
|
- [ ] Update install.sh to offer v3/v4 choice
|
||||||
@@ -547,6 +591,7 @@ If critical issues are discovered:
|
|||||||
## Notes & Decisions
|
## Notes & Decisions
|
||||||
|
|
||||||
### Key Decisions Made:
|
### Key Decisions Made:
|
||||||
|
|
||||||
1. ✅ Use npm:net-snmp (no pure Deno SNMP library available)
|
1. ✅ Use npm:net-snmp (no pure Deno SNMP library available)
|
||||||
2. ✅ Major version bump to 4.0.0 (breaking changes)
|
2. ✅ Major version bump to 4.0.0 (breaking changes)
|
||||||
3. ✅ CLI reorganization with subcommands
|
3. ✅ CLI reorganization with subcommands
|
||||||
@@ -554,12 +599,14 @@ If critical issues are discovered:
|
|||||||
5. ✅ 5 platform targets (Windows ARM not supported by Deno yet)
|
5. ✅ 5 platform targets (Windows ARM not supported by Deno yet)
|
||||||
|
|
||||||
### Open Questions:
|
### Open Questions:
|
||||||
|
|
||||||
- [ ] Should we keep tsconfig.json for npm package compatibility?
|
- [ ] Should we keep tsconfig.json for npm package compatibility?
|
||||||
- [ ] Should we fully migrate to Deno APIs (Deno.readFile) or keep node:fs?
|
- [ ] Should we fully migrate to Deno APIs (Deno.readFile) or keep node:fs?
|
||||||
- [ ] Should we remove the `bin/nupst` wrapper or keep it?
|
- [ ] Should we remove the `bin/nupst` wrapper or keep it?
|
||||||
- [ ] Should setup.sh be completely removed or kept for dependencies?
|
- [ ] Should setup.sh be completely removed or kept for dependencies?
|
||||||
|
|
||||||
### Risk Areas:
|
### Risk Areas:
|
||||||
|
|
||||||
- ⚠️ SNMP native addon compatibility in compiled binaries (HIGH PRIORITY TO TEST)
|
- ⚠️ SNMP native addon compatibility in compiled binaries (HIGH PRIORITY TO TEST)
|
||||||
- ⚠️ Systemd integration with new binary structure
|
- ⚠️ Systemd integration with new binary structure
|
||||||
- ⚠️ Config migration from v3 to v4
|
- ⚠️ Config migration from v3 to v4
|
||||||
|
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`
|
233
test/showcase.ts
Normal file
233
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('');
|
@@ -1,14 +1,14 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { assert, assertEquals } from 'jsr:@std/assert@^1.0.0';
|
||||||
import { Logger } from '../ts/logger.js';
|
import { Logger } from '../ts/logger.ts';
|
||||||
|
|
||||||
// Create a Logger instance for testing
|
// Create a Logger instance for testing
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
|
|
||||||
tap.test('should create a logger instance', async () => {
|
Deno.test('should create a logger instance', () => {
|
||||||
expect(logger instanceof Logger).toBeTruthy();
|
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
|
// We're not testing console output directly, just ensuring no errors
|
||||||
logger.log('Regular log message');
|
logger.log('Regular log message');
|
||||||
logger.error('Error message');
|
logger.error('Error message');
|
||||||
@@ -16,104 +16,104 @@ tap.test('should log messages with different log levels', async () => {
|
|||||||
logger.success('Success message');
|
logger.success('Success message');
|
||||||
|
|
||||||
// Just assert that the test runs without errors
|
// 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
|
// Just ensuring no errors occur
|
||||||
logger.logBoxTitle('Test Box', 40);
|
logger.logBoxTitle('Test Box', 40);
|
||||||
logger.logBoxLine('This is a test line');
|
logger.logBoxLine('This is a test line');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
// Just assert that the test runs without errors
|
// 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);
|
logger.logBoxTitle('Width Test', 45);
|
||||||
|
|
||||||
// These should use the width from the title
|
// These should use the width from the title
|
||||||
logger.logBoxLine('Line 1');
|
logger.logBoxLine('Line 1');
|
||||||
logger.logBoxLine('Line 2');
|
logger.logBoxLine('Line 2');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
let errorThrown = false;
|
let errorThrown = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This should work fine after the reset in logBoxEnd
|
// This should work fine after the reset in logBoxEnd
|
||||||
logger.logBoxTitle('New Box', 30);
|
logger.logBoxTitle('New Box', 30);
|
||||||
logger.logBoxLine('New line');
|
logger.logBoxLine('New line');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
errorThrown = true;
|
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
|
// This should automatically use the default width instead of throwing
|
||||||
let errorThrown = false;
|
let errorThrown = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.logBoxLine('This should use default width');
|
logger.logBoxLine('This should use default width');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
errorThrown = true;
|
errorThrown = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify no error was thrown
|
// 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
|
// Just ensuring no errors occur
|
||||||
logger.logBox('Complete Box', [
|
logger.logBox('Complete Box', [
|
||||||
'Line 1',
|
'Line 1',
|
||||||
'Line 2',
|
'Line 2',
|
||||||
'Line 3'
|
'Line 3',
|
||||||
], 40);
|
], 40);
|
||||||
|
|
||||||
// Just assert that the test runs without errors
|
// 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
|
// Just ensuring no errors occur when content is too long
|
||||||
logger.logBox('Truncation Test', [
|
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);
|
], 30);
|
||||||
|
|
||||||
// Just assert that the test runs without errors
|
// 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
|
// Just ensuring no errors occur
|
||||||
logger.logDivider(30);
|
logger.logDivider(30);
|
||||||
logger.logDivider(20, '*');
|
logger.logDivider(20, '*');
|
||||||
|
|
||||||
// Just assert that the test runs without errors
|
// 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
|
// This should use the default width
|
||||||
logger.logDivider(undefined, '-');
|
logger.logDivider(undefined, '-');
|
||||||
|
|
||||||
// Just assert that the test runs without errors
|
// 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');
|
console.log('\n=== LOGGER DEMO ===\n');
|
||||||
|
|
||||||
// Basic logging
|
// Basic logging
|
||||||
logger.log('Regular log message');
|
logger.log('Regular log message');
|
||||||
logger.error('Error message');
|
logger.error('Error message');
|
||||||
logger.warn('Warning message');
|
logger.warn('Warning message');
|
||||||
logger.success('Success message');
|
logger.success('Success message');
|
||||||
|
|
||||||
// Logbox with title, content lines, and end
|
// Logbox with title, content lines, and end
|
||||||
logger.logBoxTitle('Configuration Loaded', 50);
|
logger.logBoxTitle('Configuration Loaded', 50);
|
||||||
logger.logBoxLine('SNMP Settings:');
|
logger.logBoxLine('SNMP Settings:');
|
||||||
@@ -121,40 +121,37 @@ tap.test('Logger Demo', async () => {
|
|||||||
logger.logBoxLine(' Port: 161');
|
logger.logBoxLine(' Port: 161');
|
||||||
logger.logBoxLine(' Version: 1');
|
logger.logBoxLine(' Version: 1');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
// Complete logbox in one call
|
// Complete logbox in one call
|
||||||
logger.logBox('UPS Status', [
|
logger.logBox('UPS Status', [
|
||||||
'Power Status: onBattery',
|
'Power Status: onBattery',
|
||||||
'Battery Capacity: 75%',
|
'Battery Capacity: 75%',
|
||||||
'Runtime Remaining: 30 minutes'
|
'Runtime Remaining: 30 minutes',
|
||||||
], 45);
|
], 45);
|
||||||
|
|
||||||
// Logbox with content that's too long for the width
|
// Logbox with content that's too long for the width
|
||||||
logger.logBox('Truncation Example', [
|
logger.logBox('Truncation Example', [
|
||||||
'This line is short enough to fit within the box width',
|
'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);
|
], 40);
|
||||||
|
|
||||||
// Demonstrating logbox width being remembered
|
// Demonstrating logbox width being remembered
|
||||||
logger.logBoxTitle('Width Persistence Example', 60);
|
logger.logBoxTitle('Width Persistence Example', 60);
|
||||||
logger.logBoxLine('These lines use the width from the title');
|
logger.logBoxLine('These lines use the width from the title');
|
||||||
logger.logBoxLine('No need to specify the width again');
|
logger.logBoxLine('No need to specify the width again');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
// Demonstrating default width
|
// Demonstrating default width
|
||||||
console.log('\nDefault Width Example:');
|
console.log('\nDefault Width Example:');
|
||||||
logger.logBoxLine('This line uses the default width');
|
logger.logBoxLine('This line uses the default width');
|
||||||
logger.logBoxLine('Still using default width');
|
logger.logBoxLine('Still using default width');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
// Divider example
|
// Divider example
|
||||||
logger.log('\nDivider example:');
|
logger.log('\nDivider example:');
|
||||||
logger.logDivider(30);
|
logger.logDivider(30);
|
||||||
logger.logDivider(30, '*');
|
logger.logDivider(30, '*');
|
||||||
logger.logDivider(undefined, '=');
|
logger.logDivider(undefined, '=');
|
||||||
|
|
||||||
expect(true).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export the default tap object
|
assert(true);
|
||||||
export default tap.start();
|
});
|
||||||
|
68
test/test.ts
68
test/test.ts
@@ -1,52 +1,53 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0';
|
||||||
import { NupstSnmp } from '../ts/snmp/manager.js';
|
import { NupstSnmp } from '../ts/snmp/manager.ts';
|
||||||
import type { ISnmpConfig, IUpsStatus } from '../ts/snmp/types.js';
|
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/');
|
const testQenv = new qenv.Qenv('./', '.nogit/');
|
||||||
|
|
||||||
// Create an SNMP instance with debug enabled
|
// Create an SNMP instance with debug enabled
|
||||||
const snmp = new NupstSnmp(true);
|
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 testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1');
|
||||||
const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3');
|
const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3');
|
||||||
|
|
||||||
tap.test('should log config', async () => {
|
Deno.test('should log config', () => {
|
||||||
console.log(testConfigV1);
|
console.log(testConfigV1);
|
||||||
|
assert(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with real UPS using the configuration from .nogit/env.json
|
// 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 {
|
try {
|
||||||
console.log('Testing with real UPS configuration...');
|
console.log('Testing with real UPS configuration...');
|
||||||
|
|
||||||
// Extract the correct SNMP config from the test 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('SNMP Config:');
|
||||||
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
||||||
console.log(` Version: SNMPv${snmpConfig.version}`);
|
console.log(` Version: SNMPv${snmpConfig.version}`);
|
||||||
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
||||||
|
|
||||||
// Use a short timeout for testing
|
// Use a short timeout for testing
|
||||||
const testSnmpConfig = {
|
const testSnmpConfig = {
|
||||||
...snmpConfig,
|
...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
|
// Try to get the UPS status
|
||||||
const status = await snmp.getUpsStatus(testSnmpConfig);
|
const status = await snmp.getUpsStatus(testSnmpConfig);
|
||||||
|
|
||||||
console.log('UPS Status:');
|
console.log('UPS Status:');
|
||||||
console.log(` Power Status: ${status.powerStatus}`);
|
console.log(` Power Status: ${status.powerStatus}`);
|
||||||
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||||
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||||
|
|
||||||
// Just make sure we got valid data types back
|
// Just make sure we got valid data types back
|
||||||
expect(status).toBeTruthy();
|
assertExists(status);
|
||||||
expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus);
|
assert(['online', 'onBattery', 'unknown'].includes(status.powerStatus));
|
||||||
expect(typeof status.batteryCapacity).toEqual('number');
|
assertEquals(typeof status.batteryCapacity, 'number');
|
||||||
expect(typeof status.batteryRuntime).toEqual('number');
|
assertEquals(typeof status.batteryRuntime, 'number');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Real UPS test failed:', error);
|
console.log('Real UPS test failed:', error);
|
||||||
// Skip the test if we can't connect to the real UPS
|
// 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 {
|
try {
|
||||||
console.log('Testing with real UPS configuration...');
|
console.log('Testing with real UPS configuration...');
|
||||||
|
|
||||||
// Extract the correct SNMP config from the test 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('SNMP Config:');
|
||||||
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
console.log(` Host: ${snmpConfig.host}:${snmpConfig.port}`);
|
||||||
console.log(` Version: SNMPv${snmpConfig.version}`);
|
console.log(` Version: SNMPv${snmpConfig.version}`);
|
||||||
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
console.log(` UPS Model: ${snmpConfig.upsModel}`);
|
||||||
|
|
||||||
// Use a short timeout for testing
|
// Use a short timeout for testing
|
||||||
const testSnmpConfig = {
|
const testSnmpConfig = {
|
||||||
...snmpConfig,
|
...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
|
// Try to get the UPS status
|
||||||
const status = await snmp.getUpsStatus(testSnmpConfig);
|
const status = await snmp.getUpsStatus(testSnmpConfig);
|
||||||
|
|
||||||
console.log('UPS Status:');
|
console.log('UPS Status:');
|
||||||
console.log(` Power Status: ${status.powerStatus}`);
|
console.log(` Power Status: ${status.powerStatus}`);
|
||||||
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
console.log(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||||
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
console.log(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||||
|
|
||||||
// Just make sure we got valid data types back
|
// Just make sure we got valid data types back
|
||||||
expect(status).toBeTruthy();
|
assertExists(status);
|
||||||
expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus);
|
assert(['online', 'onBattery', 'unknown'].includes(status.powerStatus));
|
||||||
expect(typeof status.batteryCapacity).toEqual('number');
|
assertEquals(typeof status.batteryCapacity, 'number');
|
||||||
expect(typeof status.batteryRuntime).toEqual('number');
|
assertEquals(typeof status.batteryRuntime, 'number');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Real UPS test failed:', error);
|
console.log('Real UPS test failed:', error);
|
||||||
// Skip the test if we can't connect to the real UPS
|
// Skip the test if we can't connect to the real UPS
|
||||||
console.log('Skipping this test since the UPS might not be available');
|
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 = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/nupst',
|
name: denoConfig.name,
|
||||||
version: '3.1.2',
|
version: denoConfig.version,
|
||||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
description: 'Deno-powered UPS monitoring tool for SNMP-enabled UPS devices',
|
||||||
}
|
};
|
||||||
|
524
ts/cli.ts
524
ts/cli.ts
@@ -1,6 +1,7 @@
|
|||||||
import { execSync } from 'child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { Nupst } from './nupst.js';
|
import { Nupst } from './nupst.ts';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.ts';
|
||||||
|
import { theme, symbols } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling CLI commands
|
* Class for handling CLI commands
|
||||||
@@ -21,7 +22,7 @@ export class NupstCli {
|
|||||||
* @param args Command line arguments (process.argv)
|
* @param args Command line arguments (process.argv)
|
||||||
*/
|
*/
|
||||||
public async parseAndExecute(args: string[]): Promise<void> {
|
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);
|
const debugOptions = this.extractDebugOptions(args);
|
||||||
if (debugOptions.debugMode) {
|
if (debugOptions.debugMode) {
|
||||||
logger.log('Debug mode enabled');
|
logger.log('Debug mode enabled');
|
||||||
@@ -29,6 +30,12 @@ export class NupstCli {
|
|||||||
this.nupst.getSnmp().enableDebug();
|
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)
|
// Get the command (default to help if none provided)
|
||||||
const command = debugOptions.cleanedArgs[2] || 'help';
|
const command = debugOptions.cleanedArgs[2] || 'help';
|
||||||
const commandArgs = debugOptions.cleanedArgs.slice(3);
|
const commandArgs = debugOptions.cleanedArgs.slice(3);
|
||||||
@@ -56,23 +63,104 @@ export class NupstCli {
|
|||||||
* @param commandArgs Additional command arguments
|
* @param commandArgs Additional command arguments
|
||||||
* @param debugMode Whether debug mode is enabled
|
* @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
|
// Get access to the handlers
|
||||||
const upsHandler = this.nupst.getUpsHandler();
|
const upsHandler = this.nupst.getUpsHandler();
|
||||||
const groupHandler = this.nupst.getGroupHandler();
|
const groupHandler = this.nupst.getGroupHandler();
|
||||||
const serviceHandler = this.nupst.getServiceHandler();
|
const serviceHandler = this.nupst.getServiceHandler();
|
||||||
|
|
||||||
|
// 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': // Alias
|
||||||
|
case 'delete': { // Backward compatibility
|
||||||
|
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
|
// Handle group subcommands
|
||||||
if (command === 'group') {
|
if (command === 'group') {
|
||||||
const subcommand = commandArgs[0] || 'list';
|
const subcommand = commandArgs[0] || 'list';
|
||||||
const subcommandArgs = commandArgs.slice(1);
|
const subcommandArgs = commandArgs.slice(1);
|
||||||
|
|
||||||
switch (subcommand) {
|
switch (subcommand) {
|
||||||
case 'add':
|
case 'add':
|
||||||
await groupHandler.add();
|
await groupHandler.add();
|
||||||
break;
|
break;
|
||||||
|
case 'edit': {
|
||||||
case 'edit':
|
|
||||||
const groupId = subcommandArgs[0];
|
const groupId = subcommandArgs[0];
|
||||||
if (!groupId) {
|
if (!groupId) {
|
||||||
logger.error('Group ID is required for edit command');
|
logger.error('Group ID is required for edit command');
|
||||||
@@ -81,21 +169,23 @@ export class NupstCli {
|
|||||||
}
|
}
|
||||||
await groupHandler.edit(groupId);
|
await groupHandler.edit(groupId);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'delete':
|
case 'remove':
|
||||||
const groupIdToDelete = subcommandArgs[0];
|
case 'rm': // Alias
|
||||||
if (!groupIdToDelete) {
|
case 'delete': { // Backward compatibility
|
||||||
logger.error('Group ID is required for delete command');
|
const groupIdToRemove = subcommandArgs[0];
|
||||||
|
if (!groupIdToRemove) {
|
||||||
|
logger.error('Group ID is required for remove command');
|
||||||
this.showGroupHelp();
|
this.showGroupHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await groupHandler.delete(groupIdToDelete);
|
await groupHandler.remove(groupIdToRemove);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'list':
|
case 'list':
|
||||||
|
case 'ls': // Alias
|
||||||
await groupHandler.list();
|
await groupHandler.list();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.showGroupHelp();
|
this.showGroupHelp();
|
||||||
break;
|
break;
|
||||||
@@ -103,82 +193,102 @@ export class NupstCli {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle main commands
|
// Handle config subcommand
|
||||||
|
if (command === 'config') {
|
||||||
|
const subcommand = commandArgs[0] || 'show';
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case 'show':
|
||||||
|
case 'display':
|
||||||
|
await this.showConfig();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await this.showConfig();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle top-level commands and backward compatibility
|
||||||
switch (command) {
|
switch (command) {
|
||||||
|
// Backward compatibility - old UPS commands
|
||||||
case 'add':
|
case 'add':
|
||||||
|
logger.log("Note: 'nupst add' is deprecated. Use 'nupst ups add' instead.");
|
||||||
await upsHandler.add();
|
await upsHandler.add();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'edit':
|
case 'edit':
|
||||||
const upsId = commandArgs[0];
|
logger.log("Note: 'nupst edit' is deprecated. Use 'nupst ups edit' instead.");
|
||||||
await upsHandler.edit(upsId);
|
await upsHandler.edit(commandArgs[0]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
const upsIdToDelete = commandArgs[0];
|
logger.log("Note: 'nupst delete' is deprecated. Use 'nupst ups remove' instead.");
|
||||||
if (!upsIdToDelete) {
|
if (!commandArgs[0]) {
|
||||||
logger.error('UPS ID is required for delete command');
|
logger.error('UPS ID is required for delete command');
|
||||||
this.showHelp();
|
this.showHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await upsHandler.delete(upsIdToDelete);
|
await upsHandler.remove(commandArgs[0]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'list':
|
case 'list':
|
||||||
|
logger.log("Note: 'nupst list' is deprecated. Use 'nupst ups list' instead.");
|
||||||
await upsHandler.list();
|
await upsHandler.list();
|
||||||
break;
|
break;
|
||||||
|
case 'test':
|
||||||
case 'setup':
|
logger.log("Note: 'nupst test' is deprecated. Use 'nupst ups test' instead.");
|
||||||
// Backward compatibility: setup is now an alias for edit with no specific UPS ID
|
await upsHandler.test(debugMode);
|
||||||
|
break;
|
||||||
|
case 'setup':
|
||||||
|
logger.log("Note: 'nupst setup' is deprecated. Use 'nupst ups edit' instead.");
|
||||||
await upsHandler.edit(undefined);
|
await upsHandler.edit(undefined);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Backward compatibility - old service commands
|
||||||
case 'enable':
|
case 'enable':
|
||||||
|
logger.log("Note: 'nupst enable' is deprecated. Use 'nupst service enable' instead.");
|
||||||
await serviceHandler.enable();
|
await serviceHandler.enable();
|
||||||
break;
|
break;
|
||||||
|
case 'disable':
|
||||||
|
logger.log("Note: 'nupst disable' is deprecated. Use 'nupst service disable' instead.");
|
||||||
|
await serviceHandler.disable();
|
||||||
|
break;
|
||||||
|
case 'start':
|
||||||
|
logger.log("Note: 'nupst start' is deprecated. Use 'nupst service start' instead.");
|
||||||
|
await serviceHandler.start();
|
||||||
|
break;
|
||||||
|
case 'stop':
|
||||||
|
logger.log("Note: 'nupst stop' is deprecated. Use 'nupst service stop' instead.");
|
||||||
|
await serviceHandler.stop();
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
logger.log("Note: 'nupst status' is deprecated. Use 'nupst service status' instead.");
|
||||||
|
await serviceHandler.status();
|
||||||
|
break;
|
||||||
|
case 'logs':
|
||||||
|
logger.log("Note: 'nupst logs' is deprecated. Use 'nupst service logs' instead.");
|
||||||
|
await serviceHandler.logs();
|
||||||
|
break;
|
||||||
case 'daemon-start':
|
case 'daemon-start':
|
||||||
|
logger.log(
|
||||||
|
"Note: 'nupst daemon-start' is deprecated. Use 'nupst service start-daemon' instead.",
|
||||||
|
);
|
||||||
await serviceHandler.daemonStart(debugMode);
|
await serviceHandler.daemonStart(debugMode);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'logs':
|
// Top-level commands (no changes)
|
||||||
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;
|
|
||||||
|
|
||||||
case 'update':
|
case 'update':
|
||||||
await serviceHandler.update();
|
await serviceHandler.update();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'uninstall':
|
case 'uninstall':
|
||||||
await serviceHandler.uninstall();
|
await serviceHandler.uninstall();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'config':
|
|
||||||
await this.showConfig();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'help':
|
case 'help':
|
||||||
|
case '--help':
|
||||||
|
case '-h':
|
||||||
|
this.showHelp();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
logger.error(`Unknown command: ${command}`);
|
||||||
|
logger.log('');
|
||||||
this.showHelp();
|
this.showHelp();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -192,7 +302,7 @@ export class NupstCli {
|
|||||||
// Try to load configuration
|
// Try to load configuration
|
||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
const errorBoxWidth = 45;
|
const errorBoxWidth = 45;
|
||||||
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
logger.logBoxTitle('Configuration Error', errorBoxWidth);
|
||||||
logger.logBoxLine('No configuration found.');
|
logger.logBoxLine('No configuration found.');
|
||||||
@@ -225,8 +335,12 @@ export class NupstCli {
|
|||||||
logger.logBoxLine(`${ups.name} (${ups.id}):`);
|
logger.logBoxLine(`${ups.name} (${ups.id}):`);
|
||||||
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||||
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`);
|
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`);
|
||||||
logger.logBoxLine(` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`);
|
logger.logBoxLine(
|
||||||
logger.logBoxLine(` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`);
|
` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
|
||||||
|
);
|
||||||
|
logger.logBoxLine(
|
||||||
|
` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`,
|
||||||
|
);
|
||||||
logger.logBoxLine('');
|
logger.logBoxLine('');
|
||||||
}
|
}
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
@@ -241,60 +355,77 @@ export class NupstCli {
|
|||||||
if (group.description) {
|
if (group.description) {
|
||||||
logger.logBoxLine(` Description: ${group.description}`);
|
logger.logBoxLine(` Description: ${group.description}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// List UPS devices in this group
|
// List UPS devices in this group
|
||||||
const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(group.id));
|
const upsInGroup = config.upsDevices.filter((ups) =>
|
||||||
logger.logBoxLine(` UPS Devices: ${upsInGroup.length > 0 ?
|
ups.groups && ups.groups.includes(group.id)
|
||||||
upsInGroup.map(ups => ups.name).join(', ') : 'None'}`);
|
);
|
||||||
|
logger.logBoxLine(
|
||||||
|
` UPS Devices: ${
|
||||||
|
upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : 'None'
|
||||||
|
}`,
|
||||||
|
);
|
||||||
logger.logBoxLine('');
|
logger.logBoxLine('');
|
||||||
}
|
}
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy single UPS configuration
|
// Legacy single UPS configuration
|
||||||
// SNMP Settings
|
if (!config.snmp) {
|
||||||
logger.logBoxLine('SNMP Settings:');
|
logger.logBoxLine('Error: Legacy configuration missing SNMP settings');
|
||||||
logger.logBoxLine(` Host: ${config.snmp.host}`);
|
} else {
|
||||||
logger.logBoxLine(` Port: ${config.snmp.port}`);
|
// SNMP Settings
|
||||||
logger.logBoxLine(` Version: ${config.snmp.version}`);
|
logger.logBoxLine('SNMP Settings:');
|
||||||
logger.logBoxLine(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
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) {
|
if (config.snmp.version === 1 || config.snmp.version === 2) {
|
||||||
logger.logBoxLine(` Community: ${config.snmp.community}`);
|
logger.logBoxLine(` Community: ${config.snmp.community}`);
|
||||||
} else if (config.snmp.version === 3) {
|
} else if (config.snmp.version === 3) {
|
||||||
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
|
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
|
||||||
logger.logBoxLine(` Username: ${config.snmp.username}`);
|
logger.logBoxLine(` Username: ${config.snmp.username}`);
|
||||||
|
|
||||||
// Show auth and privacy details based on security level
|
// Show auth and privacy details based on security level
|
||||||
if (
|
if (
|
||||||
config.snmp.securityLevel === 'authNoPriv' ||
|
config.snmp.securityLevel === 'authNoPriv' ||
|
||||||
config.snmp.securityLevel === 'authPriv'
|
config.snmp.securityLevel === 'authPriv'
|
||||||
) {
|
) {
|
||||||
logger.logBoxLine(` Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
|
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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.snmp.securityLevel === 'authPriv') {
|
// Show OIDs if custom model is selected
|
||||||
logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
|
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'}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show timeout value
|
|
||||||
logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// Thresholds
|
||||||
logger.logBoxLine('Thresholds:');
|
if (!config.thresholds) {
|
||||||
logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
|
logger.logBoxLine('Error: Legacy configuration missing threshold settings');
|
||||||
logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
|
} else {
|
||||||
|
logger.logBoxLine('Thresholds:');
|
||||||
|
logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
|
||||||
|
logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
|
||||||
|
}
|
||||||
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
||||||
|
|
||||||
// Configuration file location
|
// Configuration file location
|
||||||
@@ -320,53 +451,160 @@ export class NupstCli {
|
|||||||
logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`);
|
logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`);
|
||||||
logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
|
logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
// Ignore errors checking service status
|
// Ignore errors checking service status
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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('Deno-powered UPS monitoring tool');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display help message
|
* Display help message
|
||||||
*/
|
*/
|
||||||
private showHelp(): void {
|
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('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('');
|
||||||
|
|
||||||
|
// 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('');
|
||||||
|
|
||||||
|
// Note about deprecated commands
|
||||||
|
logger.warn('Note: Old command format (e.g., \'nupst add\') still works but is deprecated.');
|
||||||
|
logger.dim(' Use the new format (e.g., \'nupst ups add\') going forward.');
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(`
|
logger.log(`
|
||||||
NUPST - Node.js UPS Shutdown Tool
|
NUPST - Service Management Commands
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
nupst enable - Install and enable the systemd service (requires root)
|
nupst service <subcommand>
|
||||||
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
|
|
||||||
|
|
||||||
UPS Management:
|
Subcommands:
|
||||||
nupst add - Add a new UPS device
|
enable - Install and enable the systemd service (requires root)
|
||||||
nupst edit [id] - Edit an existing UPS (default UPS if no ID provided)
|
disable - Stop and disable the systemd service (requires root)
|
||||||
nupst delete <id> - Delete a UPS by ID
|
start - Start the systemd service
|
||||||
nupst list - List all configured UPS devices
|
stop - Stop the systemd service
|
||||||
nupst setup - Alias for 'nupst edit' (backward compatibility)
|
restart - Restart the systemd service
|
||||||
|
status - Show service status and UPS information
|
||||||
Group Management:
|
logs - Show service logs in real-time
|
||||||
nupst group list - List all UPS groups
|
start-daemon - Start the daemon process directly (for testing)
|
||||||
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
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--debug, -d - Enable debug mode for detailed SNMP logging
|
--debug, -d - Enable debug mode for detailed logging
|
||||||
(Example: nupst test --debug)
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 +616,21 @@ Options:
|
|||||||
NUPST - Group Management Commands
|
NUPST - Group Management Commands
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
nupst group list - List all UPS groups
|
nupst group <subcommand> [arguments]
|
||||||
nupst group add - Add a new UPS group
|
|
||||||
nupst group edit <id> - Edit an existing UPS group
|
Subcommands:
|
||||||
nupst group delete <id> - Delete a UPS group
|
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:
|
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'
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { Nupst } from '../nupst.js';
|
import process from 'node:process';
|
||||||
import { logger } from '../logger.js';
|
import { Nupst } from '../nupst.ts';
|
||||||
import * as helpers from '../helpers/index.js';
|
import { logger } from '../logger.ts';
|
||||||
import { type IGroupConfig } from '../daemon.js';
|
import * as helpers from '../helpers/index.ts';
|
||||||
|
import { type IGroupConfig } from '../daemon.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling group-related CLI commands
|
* Class for handling group-related CLI commands
|
||||||
@@ -34,10 +35,10 @@ export class GroupHandler {
|
|||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current configuration
|
// Get current configuration
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Check if multi-UPS config
|
// Check if multi-UPS config
|
||||||
if (!config.groups || !Array.isArray(config.groups)) {
|
if (!config.groups || !Array.isArray(config.groups)) {
|
||||||
// Legacy or missing groups configuration
|
// Legacy or missing groups configuration
|
||||||
@@ -48,11 +49,11 @@ export class GroupHandler {
|
|||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display group list
|
// Display group list
|
||||||
const boxWidth = 60;
|
const boxWidth = 60;
|
||||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
logger.logBoxTitle('UPS Groups', boxWidth);
|
||||||
|
|
||||||
if (config.groups.length === 0) {
|
if (config.groups.length === 0) {
|
||||||
logger.logBoxLine('No UPS groups configured.');
|
logger.logBoxLine('No UPS groups configured.');
|
||||||
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
||||||
@@ -61,34 +62,36 @@ export class GroupHandler {
|
|||||||
logger.logBoxLine('');
|
logger.logBoxLine('');
|
||||||
logger.logBoxLine('ID | Name | Mode | UPS Devices');
|
logger.logBoxLine('ID | Name | Mode | UPS Devices');
|
||||||
logger.logBoxLine('-----------+----------------------+--------------+----------------');
|
logger.logBoxLine('-----------+----------------------+--------------+----------------');
|
||||||
|
|
||||||
for (const group of config.groups) {
|
for (const group of config.groups) {
|
||||||
const id = group.id.padEnd(10, ' ').substring(0, 10);
|
const id = group.id.padEnd(10, ' ').substring(0, 10);
|
||||||
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
|
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
|
||||||
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
|
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
|
||||||
|
|
||||||
// Count UPS devices in this group
|
// Count UPS devices in this group
|
||||||
const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id));
|
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
|
||||||
const upsCount = upsInGroup.length;
|
const upsCount = upsInGroup.length;
|
||||||
const upsNames = upsInGroup.map(ups => ups.name).join(', ');
|
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
||||||
|
|
||||||
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
|
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} 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
|
* Add a new UPS group
|
||||||
*/
|
*/
|
||||||
public async add(): Promise<void> {
|
public async add(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Import readline module for user input
|
// Import readline module for user input
|
||||||
const readline = await import('readline');
|
const readline = await import('node:readline');
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@@ -109,54 +112,56 @@ export class GroupHandler {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current configuration
|
// Get current configuration
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Initialize groups array if not exists
|
// Initialize groups array if not exists
|
||||||
if (!config.groups) {
|
if (!config.groups) {
|
||||||
config.groups = [];
|
config.groups = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if upsDevices is initialized
|
// Check if upsDevices is initialized
|
||||||
if (!config.upsDevices) {
|
if (!config.upsDevices) {
|
||||||
config.upsDevices = [];
|
config.upsDevices = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('\nNUPST Add Group');
|
logger.log('\nNUPST Add Group');
|
||||||
logger.log('==============\n');
|
logger.log('==============\n');
|
||||||
logger.log('This will guide you through creating a new UPS group.\n');
|
logger.log('This will guide you through creating a new UPS group.\n');
|
||||||
|
|
||||||
// Generate a new unique group ID
|
// Generate a new unique group ID
|
||||||
const groupId = helpers.shortId();
|
const groupId = helpers.shortId();
|
||||||
|
|
||||||
// Get group name
|
// Get group name
|
||||||
const name = await prompt('Group Name: ');
|
const name = await prompt('Group Name: ');
|
||||||
|
|
||||||
// Get group mode
|
// Get group mode
|
||||||
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
|
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
|
||||||
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||||
|
|
||||||
// Get optional description
|
// Get optional description
|
||||||
const description = await prompt('Group Description (optional): ');
|
const description = await prompt('Group Description (optional): ');
|
||||||
|
|
||||||
// Create the new group
|
// Create the new group
|
||||||
const newGroup: IGroupConfig = {
|
const newGroup: IGroupConfig = {
|
||||||
id: groupId,
|
id: groupId,
|
||||||
name: name || `Group-${groupId}`,
|
name: name || `Group-${groupId}`,
|
||||||
mode,
|
mode,
|
||||||
description: description || undefined
|
description: description || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the group to the configuration
|
// Add the group to the configuration
|
||||||
config.groups.push(newGroup);
|
config.groups.push(newGroup);
|
||||||
|
|
||||||
// Save the configuration
|
// Save the configuration
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
// Display summary
|
// Display summary
|
||||||
const boxWidth = 45;
|
const boxWidth = 45;
|
||||||
logger.logBoxTitle('Group Created', boxWidth);
|
logger.logBoxTitle('Group Created', boxWidth);
|
||||||
@@ -167,30 +172,32 @@ export class GroupHandler {
|
|||||||
logger.logBoxLine(`Description: ${newGroup.description}`);
|
logger.logBoxLine(`Description: ${newGroup.description}`);
|
||||||
}
|
}
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
// Check if there are UPS devices to assign to this group
|
// Check if there are UPS devices to assign to this group
|
||||||
if (config.upsDevices.length > 0) {
|
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') {
|
if (assignUps.toLowerCase() === 'y') {
|
||||||
await this.assignUpsToGroup(newGroup.id, config, prompt);
|
await this.assignUpsToGroup(newGroup.id, config, prompt);
|
||||||
|
|
||||||
// Save again after assigning UPS devices
|
// Save again after assigning UPS devices
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if service is running and restart it if needed
|
// Check if service is running and restart it if needed
|
||||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||||
|
|
||||||
logger.log('\nGroup setup complete!');
|
logger.log('\nGroup setup complete!');
|
||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
rl.close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
* Edit an existing UPS group
|
||||||
* @param groupId ID of the group to edit
|
* @param groupId ID of the group to edit
|
||||||
@@ -198,7 +205,7 @@ export class GroupHandler {
|
|||||||
public async edit(groupId: string): Promise<void> {
|
public async edit(groupId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Import readline module for user input
|
// Import readline module for user input
|
||||||
const readline = await import('readline');
|
const readline = await import('node:readline');
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@@ -219,57 +226,61 @@ export class GroupHandler {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current configuration
|
// Get current configuration
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Check if groups are initialized
|
// Check if groups are initialized
|
||||||
if (!config.groups || !Array.isArray(config.groups)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the group to edit
|
// 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) {
|
if (groupIndex === -1) {
|
||||||
logger.error(`Group with ID "${groupId}" not found.`);
|
logger.error(`Group with ID "${groupId}" not found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = config.groups[groupIndex];
|
const group = config.groups[groupIndex];
|
||||||
|
|
||||||
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
|
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
|
||||||
logger.log('==============================================\n');
|
logger.log('==============================================\n');
|
||||||
|
|
||||||
// Edit group name
|
// Edit group name
|
||||||
const newName = await prompt(`Group Name [${group.name}]: `);
|
const newName = await prompt(`Group Name [${group.name}]: `);
|
||||||
if (newName.trim()) {
|
if (newName.trim()) {
|
||||||
group.name = newName;
|
group.name = newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit group mode
|
// Edit group mode
|
||||||
const currentMode = group.mode || 'redundant';
|
const currentMode = group.mode || 'redundant';
|
||||||
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
|
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
|
||||||
if (modeInput.trim()) {
|
if (modeInput.trim()) {
|
||||||
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit description
|
// Edit description
|
||||||
const currentDesc = group.description || '';
|
const currentDesc = group.description || '';
|
||||||
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
|
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
|
||||||
if (newDesc.trim() || newDesc === '') {
|
if (newDesc.trim() || newDesc === '') {
|
||||||
group.description = newDesc.trim() || undefined;
|
group.description = newDesc.trim() || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the group in the configuration
|
// Update the group in the configuration
|
||||||
config.groups[groupIndex] = group;
|
config.groups[groupIndex] = group;
|
||||||
|
|
||||||
// Save the configuration
|
// Save the configuration
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
// Display summary
|
// Display summary
|
||||||
const boxWidth = 45;
|
const boxWidth = 45;
|
||||||
logger.logBoxTitle('Group Updated', boxWidth);
|
logger.logBoxTitle('Group Updated', boxWidth);
|
||||||
@@ -280,80 +291,87 @@ export class GroupHandler {
|
|||||||
logger.logBoxLine(`Description: ${group.description}`);
|
logger.logBoxLine(`Description: ${group.description}`);
|
||||||
}
|
}
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
// Edit UPS assignments if requested
|
// 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') {
|
if (editAssignments.toLowerCase() === 'y') {
|
||||||
await this.assignUpsToGroup(group.id, config, prompt);
|
await this.assignUpsToGroup(group.id, config, prompt);
|
||||||
|
|
||||||
// Save again after editing assignments
|
// Save again after editing assignments
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if service is running and restart it if needed
|
// Check if service is running and restart it if needed
|
||||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||||
|
|
||||||
logger.log('\nGroup edit complete!');
|
logger.log('\nGroup edit complete!');
|
||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
rl.close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
* Delete an existing UPS group
|
||||||
* @param groupId ID of the group to delete
|
* @param groupId ID of the group to delete
|
||||||
*/
|
*/
|
||||||
public async delete(groupId: string): Promise<void> {
|
public async remove(groupId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Try to load configuration
|
// Try to load configuration
|
||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current configuration
|
// Get current configuration
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Check if groups are initialized
|
// Check if groups are initialized
|
||||||
if (!config.groups || !Array.isArray(config.groups)) {
|
if (!config.groups || !Array.isArray(config.groups)) {
|
||||||
logger.error('No groups configured.');
|
logger.error('No groups configured.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the group to delete
|
// 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) {
|
if (groupIndex === -1) {
|
||||||
logger.error(`Group with ID "${groupId}" not found.`);
|
logger.error(`Group with ID "${groupId}" not found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupToDelete = config.groups[groupIndex];
|
const groupToDelete = config.groups[groupIndex];
|
||||||
|
|
||||||
// Get confirmation before deleting
|
// Get confirmation before deleting
|
||||||
const readline = await import('readline');
|
const readline = await import('node:readline');
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const confirm = await new Promise<string>(resolve => {
|
const confirm = await new Promise<string>((resolve) => {
|
||||||
rl.question(`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => {
|
rl.question(
|
||||||
resolve(answer.toLowerCase());
|
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
|
||||||
});
|
(answer) => {
|
||||||
|
resolve(answer.toLowerCase());
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.close();
|
rl.close();
|
||||||
|
|
||||||
if (confirm !== 'y' && confirm !== 'yes') {
|
if (confirm !== 'y' && confirm !== 'yes') {
|
||||||
logger.log('Deletion cancelled.');
|
logger.log('Deletion cancelled.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove this group from all UPS device group assignments
|
// Remove this group from all UPS device group assignments
|
||||||
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
||||||
for (const ups of config.upsDevices) {
|
for (const ups of config.upsDevices) {
|
||||||
@@ -363,19 +381,21 @@ export class GroupHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the group from the array
|
// Remove the group from the array
|
||||||
config.groups.splice(groupIndex, 1);
|
config.groups.splice(groupIndex, 1);
|
||||||
|
|
||||||
// Save the configuration
|
// Save the configuration
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
|
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
|
||||||
|
|
||||||
// Check if service is running and restart it if needed
|
// Check if service is running and restart it if needed
|
||||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||||
} catch (error) {
|
} 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 +408,18 @@ export class GroupHandler {
|
|||||||
public async assignUpsToGroups(
|
public async assignUpsToGroups(
|
||||||
ups: any,
|
ups: any,
|
||||||
groups: any[],
|
groups: any[],
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Initialize groups array if it doesn't exist
|
// Initialize groups array if it doesn't exist
|
||||||
if (!ups.groups) {
|
if (!ups.groups) {
|
||||||
ups.groups = [];
|
ups.groups = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show current group assignments
|
// Show current group assignments
|
||||||
logger.log('\nCurrent Group Assignments:');
|
logger.log('\nCurrent Group Assignments:');
|
||||||
if (ups.groups && ups.groups.length > 0) {
|
if (ups.groups && ups.groups.length > 0) {
|
||||||
for (const groupId of ups.groups) {
|
for (const groupId of ups.groups) {
|
||||||
const group = groups.find(g => g.id === groupId);
|
const group = groups.find((g) => g.id === groupId);
|
||||||
if (group) {
|
if (group) {
|
||||||
logger.log(`- ${group.name} (${group.id})`);
|
logger.log(`- ${group.name} (${group.id})`);
|
||||||
} else {
|
} else {
|
||||||
@@ -409,52 +429,56 @@ export class GroupHandler {
|
|||||||
} else {
|
} else {
|
||||||
logger.log('- None');
|
logger.log('- None');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show available groups
|
// Show available groups
|
||||||
logger.log('\nAvailable Groups:');
|
logger.log('\nAvailable Groups:');
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
logger.log('- No groups available. Use "nupst group add" to create groups.');
|
logger.log('- No groups available. Use "nupst group add" to create groups.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < groups.length; i++) {
|
for (let i = 0; i < groups.length; i++) {
|
||||||
const group = groups[i];
|
const group = groups[i];
|
||||||
const assigned = ups.groups && ups.groups.includes(group.id);
|
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
|
// 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') {
|
if (selection.toLowerCase() === 'clear') {
|
||||||
// Clear all group assignments
|
// Clear all group assignments
|
||||||
ups.groups = [];
|
ups.groups = [];
|
||||||
logger.log('All group assignments cleared.');
|
logger.log('All group assignments cleared.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selection.trim()) {
|
if (!selection.trim()) {
|
||||||
// No change if empty input
|
// No change if empty input
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process selections
|
// Process selections
|
||||||
const selections = selection.split(',').map(s => s.trim());
|
const selections = selection.split(',').map((s) => s.trim());
|
||||||
|
|
||||||
for (const sel of selections) {
|
for (const sel of selections) {
|
||||||
const index = parseInt(sel, 10) - 1;
|
const index = parseInt(sel, 10) - 1;
|
||||||
if (isNaN(index) || index < 0 || index >= groups.length) {
|
if (isNaN(index) || index < 0 || index >= groups.length) {
|
||||||
logger.error(`Invalid selection: ${sel}`);
|
logger.error(`Invalid selection: ${sel}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = groups[index];
|
const group = groups[index];
|
||||||
|
|
||||||
// Initialize groups array if needed (should already be done above)
|
// Initialize groups array if needed (should already be done above)
|
||||||
if (!ups.groups) {
|
if (!ups.groups) {
|
||||||
ups.groups = [];
|
ups.groups = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle assignment
|
// Toggle assignment
|
||||||
const groupIndex = ups.groups.indexOf(group.id);
|
const groupIndex = ups.groups.indexOf(group.id);
|
||||||
if (groupIndex === -1) {
|
if (groupIndex === -1) {
|
||||||
@@ -468,7 +492,7 @@ export class GroupHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign UPS devices to a specific group
|
* Assign UPS devices to a specific group
|
||||||
* @param groupId Group ID to assign UPS devices to
|
* @param groupId Group ID to assign UPS devices to
|
||||||
@@ -478,22 +502,24 @@ export class GroupHandler {
|
|||||||
public async assignUpsToGroup(
|
public async assignUpsToGroup(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
config: any,
|
config: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!config.upsDevices || config.upsDevices.length === 0) {
|
if (!config.upsDevices || config.upsDevices.length === 0) {
|
||||||
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
|
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = config.groups.find(g => g.id === groupId);
|
const group = config.groups.find((g: { id: string }) => g.id === groupId);
|
||||||
if (!group) {
|
if (!group) {
|
||||||
logger.error(`Group with ID "${groupId}" not found.`);
|
logger.error(`Group with ID "${groupId}" not found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show current assignments
|
// Show current assignments
|
||||||
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
|
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) {
|
if (upsInGroup.length === 0) {
|
||||||
logger.log('- None');
|
logger.log('- None');
|
||||||
} else {
|
} else {
|
||||||
@@ -501,7 +527,7 @@ export class GroupHandler {
|
|||||||
logger.log(`- ${ups.name} (${ups.id})`);
|
logger.log(`- ${ups.name} (${ups.id})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show all UPS devices
|
// Show all UPS devices
|
||||||
logger.log('\nAvailable UPS devices:');
|
logger.log('\nAvailable UPS devices:');
|
||||||
for (let i = 0; i < config.upsDevices.length; i++) {
|
for (let i = 0; i < config.upsDevices.length; i++) {
|
||||||
@@ -509,10 +535,12 @@ export class GroupHandler {
|
|||||||
const assigned = ups.groups && ups.groups.includes(groupId);
|
const assigned = ups.groups && ups.groups.includes(groupId);
|
||||||
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt for UPS selection
|
// 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') {
|
if (selection.toLowerCase() === 'clear') {
|
||||||
// Clear all UPS from this group
|
// Clear all UPS from this group
|
||||||
for (const ups of config.upsDevices) {
|
for (const ups of config.upsDevices) {
|
||||||
@@ -526,29 +554,29 @@ export class GroupHandler {
|
|||||||
logger.log(`All UPS devices removed from group "${group.name}".`);
|
logger.log(`All UPS devices removed from group "${group.name}".`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selection.trim()) {
|
if (!selection.trim()) {
|
||||||
// No change if empty input
|
// No change if empty input
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process selections
|
// Process selections
|
||||||
const selections = selection.split(',').map(s => s.trim());
|
const selections = selection.split(',').map((s) => s.trim());
|
||||||
|
|
||||||
for (const sel of selections) {
|
for (const sel of selections) {
|
||||||
const index = parseInt(sel, 10) - 1;
|
const index = parseInt(sel, 10) - 1;
|
||||||
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
|
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
|
||||||
logger.error(`Invalid selection: ${sel}`);
|
logger.error(`Invalid selection: ${sel}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ups = config.upsDevices[index];
|
const ups = config.upsDevices[index];
|
||||||
|
|
||||||
// Initialize groups array if needed
|
// Initialize groups array if needed
|
||||||
if (!ups.groups) {
|
if (!ups.groups) {
|
||||||
ups.groups = [];
|
ups.groups = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle assignment
|
// Toggle assignment
|
||||||
const groupIndex = ups.groups.indexOf(groupId);
|
const groupIndex = ups.groups.indexOf(groupId);
|
||||||
if (groupIndex === -1) {
|
if (groupIndex === -1) {
|
||||||
@@ -562,4 +590,4 @@ export class GroupHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { execSync } from 'child_process';
|
import process from 'node:process';
|
||||||
import { Nupst } from '../nupst.js';
|
import { execSync } from 'node:child_process';
|
||||||
import { logger } from '../logger.js';
|
import { Nupst } from '../nupst.ts';
|
||||||
|
import { logger } from '../logger.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling service-related CLI commands
|
* Class for handling service-related CLI commands
|
||||||
@@ -128,85 +129,57 @@ export class ServiceHandler {
|
|||||||
try {
|
try {
|
||||||
// Check if running as root
|
// Check if running as root
|
||||||
this.checkRootAccess(
|
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;
|
console.log('');
|
||||||
logger.logBoxTitle('NUPST Update Process', boxWidth);
|
logger.info('Checking for updates...');
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Update the repository
|
// Get current version
|
||||||
logger.logBoxLine('Pulling latest changes from git repository...');
|
const currentVersion = this.nupst.getVersion();
|
||||||
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
|
|
||||||
stdio: 'pipe',
|
// 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"
|
||||||
|
|
||||||
|
logger.dim(`Current version: ${currentVersion}`);
|
||||||
|
logger.dim(`Latest version: ${latestVersion}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Compare versions (both are in format "v4.0.7")
|
||||||
|
if (currentVersion === latestVersion) {
|
||||||
|
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
|
console.log('');
|
||||||
logger.logBoxLine('Running install.sh to update NUPST...');
|
logger.success(`Updated to ${latestVersion}`);
|
||||||
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
|
console.log('');
|
||||||
|
|
||||||
// 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();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.logBoxLine('Error during update process:');
|
console.log('');
|
||||||
logger.logBoxLine(`${error.message}`);
|
logger.error('Update failed');
|
||||||
logger.logBoxEnd();
|
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
console.log('');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Update failed: ${error.message}`);
|
logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +215,7 @@ export class ServiceHandler {
|
|||||||
|
|
||||||
// Ask about removing configuration
|
// Ask about removing configuration
|
||||||
const removeConfig = await prompt(
|
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
|
// Find the uninstall.sh script location
|
||||||
@@ -300,7 +273,7 @@ export class ServiceHandler {
|
|||||||
stdio: 'inherit', // Show output in the terminal
|
stdio: 'inherit', // Show output in the terminal
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Uninstall failed: ${error.message}`);
|
console.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,4 +290,4 @@ export class ServiceHandler {
|
|||||||
|
|
||||||
return { debugMode, cleanedArgs };
|
return { debugMode, cleanedArgs };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import { execSync } from 'child_process';
|
import process from 'node:process';
|
||||||
import { Nupst } from '../nupst.js';
|
import { execSync } from 'node:child_process';
|
||||||
import { logger } from '../logger.js';
|
import { Nupst } from '../nupst.ts';
|
||||||
import * as helpers from '../helpers/index.js';
|
import { logger } from '../logger.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
|
* Class for handling UPS-related CLI commands
|
||||||
@@ -24,7 +27,7 @@ export class UpsHandler {
|
|||||||
public async add(): Promise<void> {
|
public async add(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Import readline module for user input
|
// Import readline module for user input
|
||||||
const readline = await import('readline');
|
const readline = await import('node:readline');
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@@ -46,7 +49,7 @@ export class UpsHandler {
|
|||||||
rl.close();
|
rl.close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Add UPS error: ${error.message}`);
|
logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ export class UpsHandler {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
config = this.nupst.getDaemon().getConfig();
|
config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Convert old format to new format if needed
|
// Convert old format to new format if needed
|
||||||
if (!config.upsDevices) {
|
if (!config.upsDevices) {
|
||||||
// Initialize with the current config as the first UPS
|
// Initialize with the current config as the first UPS
|
||||||
@@ -75,9 +78,9 @@ export class UpsHandler {
|
|||||||
name: 'Default UPS',
|
name: 'Default UPS',
|
||||||
snmp: config.snmp,
|
snmp: config.snmp,
|
||||||
thresholds: config.thresholds,
|
thresholds: config.thresholds,
|
||||||
groups: []
|
groups: [],
|
||||||
}],
|
}],
|
||||||
groups: []
|
groups: [],
|
||||||
};
|
};
|
||||||
logger.log('Converting existing configuration to multi-UPS format.');
|
logger.log('Converting existing configuration to multi-UPS format.');
|
||||||
}
|
}
|
||||||
@@ -86,7 +89,7 @@ export class UpsHandler {
|
|||||||
config = {
|
config = {
|
||||||
checkInterval: 30000, // Default check interval
|
checkInterval: 30000, // Default check interval
|
||||||
upsDevices: [],
|
upsDevices: [],
|
||||||
groups: []
|
groups: [],
|
||||||
};
|
};
|
||||||
logger.log('No existing configuration found. Creating a new configuration.');
|
logger.log('No existing configuration found. Creating a new configuration.');
|
||||||
}
|
}
|
||||||
@@ -105,13 +108,13 @@ export class UpsHandler {
|
|||||||
community: 'public',
|
community: 'public',
|
||||||
version: 1,
|
version: 1,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
upsModel: 'cyberpower'
|
upsModel: 'cyberpower' as TUpsModel,
|
||||||
},
|
},
|
||||||
thresholds: {
|
thresholds: {
|
||||||
battery: 60,
|
battery: 60,
|
||||||
runtime: 20
|
runtime: 20,
|
||||||
},
|
},
|
||||||
groups: []
|
groups: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gather SNMP settings
|
// Gather SNMP settings
|
||||||
@@ -135,16 +138,16 @@ export class UpsHandler {
|
|||||||
config.upsDevices.push(newUps);
|
config.upsDevices.push(newUps);
|
||||||
|
|
||||||
// Save the configuration
|
// Save the configuration
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config as INupstConfig);
|
||||||
|
|
||||||
this.displayUpsConfigSummary(newUps);
|
this.displayUpsConfigSummary(newUps);
|
||||||
|
|
||||||
// Test the connection if requested
|
// Test the connection if requested
|
||||||
await this.optionallyTestConnection(newUps.snmp, prompt);
|
await this.optionallyTestConnection(newUps.snmp, prompt);
|
||||||
|
|
||||||
// Check if service is running and restart it if needed
|
// Check if service is running and restart it if needed
|
||||||
await this.restartServiceIfRunning();
|
await this.restartServiceIfRunning();
|
||||||
|
|
||||||
logger.log('\nSetup complete!');
|
logger.log('\nSetup complete!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +158,7 @@ export class UpsHandler {
|
|||||||
public async edit(upsId?: string): Promise<void> {
|
public async edit(upsId?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Import readline module for user input
|
// Import readline module for user input
|
||||||
const readline = await import('readline');
|
const readline = await import('node:readline');
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
@@ -177,7 +180,7 @@ export class UpsHandler {
|
|||||||
rl.close();
|
rl.close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Edit UPS error: ${error.message}`);
|
logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,10 +189,13 @@ export class UpsHandler {
|
|||||||
* @param upsId ID of the UPS to edit (undefined for default UPS)
|
* @param upsId ID of the UPS to edit (undefined for default UPS)
|
||||||
* @param prompt Function to prompt for user input
|
* @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('\nNUPST Edit UPS');
|
||||||
logger.log('=============\n');
|
logger.log('=============\n');
|
||||||
|
|
||||||
// Try to load existing config
|
// Try to load existing config
|
||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
@@ -205,29 +211,33 @@ export class UpsHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the config
|
// Get the config
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Convert old format to new format if needed
|
// Convert old format to new format if needed
|
||||||
if (!config.upsDevices) {
|
if (!config.upsDevices) {
|
||||||
// Initialize with the current config as the first UPS
|
// Initialize with the current config as the first UPS
|
||||||
|
if (!config.snmp || !config.thresholds) {
|
||||||
|
logger.error('Legacy configuration is missing required SNMP or threshold settings');
|
||||||
|
return;
|
||||||
|
}
|
||||||
config.upsDevices = [{
|
config.upsDevices = [{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'Default UPS',
|
name: 'Default UPS',
|
||||||
snmp: config.snmp,
|
snmp: config.snmp,
|
||||||
thresholds: config.thresholds,
|
thresholds: config.thresholds,
|
||||||
groups: []
|
groups: [],
|
||||||
}];
|
}];
|
||||||
config.groups = [];
|
config.groups = [];
|
||||||
logger.log('Converting existing configuration to multi-UPS format.');
|
logger.log('Converting existing configuration to multi-UPS format.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the UPS to edit
|
// Find the UPS to edit
|
||||||
let upsToEdit;
|
let upsToEdit;
|
||||||
if (upsId) {
|
if (upsId) {
|
||||||
// Find specific UPS by ID
|
// Find specific UPS by ID
|
||||||
upsToEdit = config.upsDevices.find(ups => ups.id === upsId);
|
upsToEdit = config.upsDevices.find((ups) => ups.id === upsId);
|
||||||
if (!upsToEdit) {
|
if (!upsToEdit) {
|
||||||
logger.error(`UPS with ID "${upsId}" not found.`);
|
logger.error(`UPS with ID "${upsId}" not found.`);
|
||||||
return;
|
return;
|
||||||
@@ -242,41 +252,41 @@ export class UpsHandler {
|
|||||||
upsToEdit = config.upsDevices[0];
|
upsToEdit = config.upsDevices[0];
|
||||||
logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
|
logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow editing UPS name
|
// Allow editing UPS name
|
||||||
const newName = await prompt(`UPS Name [${upsToEdit.name}]: `);
|
const newName = await prompt(`UPS Name [${upsToEdit.name}]: `);
|
||||||
if (newName.trim()) {
|
if (newName.trim()) {
|
||||||
upsToEdit.name = newName;
|
upsToEdit.name = newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit SNMP settings
|
// Edit SNMP settings
|
||||||
await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
|
await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
|
||||||
|
|
||||||
// Edit threshold settings
|
// Edit threshold settings
|
||||||
await this.gatherThresholdSettings(upsToEdit.thresholds, prompt);
|
await this.gatherThresholdSettings(upsToEdit.thresholds, prompt);
|
||||||
|
|
||||||
// Edit UPS model settings
|
// Edit UPS model settings
|
||||||
await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
|
await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
|
||||||
|
|
||||||
// Get access to GroupHandler for group assignments
|
// Get access to GroupHandler for group assignments
|
||||||
const groupHandler = this.nupst.getGroupHandler();
|
const groupHandler = this.nupst.getGroupHandler();
|
||||||
|
|
||||||
// Edit group assignments
|
// Edit group assignments
|
||||||
if (config.groups && config.groups.length > 0) {
|
if (config.groups && config.groups.length > 0) {
|
||||||
await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt);
|
await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the configuration
|
// Save the configuration
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
this.displayUpsConfigSummary(upsToEdit);
|
this.displayUpsConfigSummary(upsToEdit);
|
||||||
|
|
||||||
// Test the connection if requested
|
// Test the connection if requested
|
||||||
await this.optionallyTestConnection(upsToEdit.snmp, prompt);
|
await this.optionallyTestConnection(upsToEdit.snmp, prompt);
|
||||||
|
|
||||||
// Check if service is running and restart it if needed
|
// Check if service is running and restart it if needed
|
||||||
await this.restartServiceIfRunning();
|
await this.restartServiceIfRunning();
|
||||||
|
|
||||||
logger.log('\nEdit complete!');
|
logger.log('\nEdit complete!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +294,7 @@ export class UpsHandler {
|
|||||||
* Delete a UPS by ID
|
* Delete a UPS by ID
|
||||||
* @param upsId ID of the UPS to delete
|
* @param upsId ID of the UPS to delete
|
||||||
*/
|
*/
|
||||||
public async delete(upsId: string): Promise<void> {
|
public async remove(upsId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Try to load configuration
|
// Try to load configuration
|
||||||
try {
|
try {
|
||||||
@@ -297,58 +307,63 @@ export class UpsHandler {
|
|||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current configuration
|
// Get current configuration
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Check if multi-UPS config
|
// Check if multi-UPS config
|
||||||
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
||||||
logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.');
|
logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.');
|
||||||
logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.');
|
logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the UPS to delete
|
// 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) {
|
if (upsIndex === -1) {
|
||||||
logger.error(`UPS with ID "${upsId}" not found.`);
|
logger.error(`UPS with ID "${upsId}" not found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsToDelete = config.upsDevices[upsIndex];
|
const upsToDelete = config.upsDevices[upsIndex];
|
||||||
|
|
||||||
// Get confirmation before deleting
|
// Get confirmation before deleting
|
||||||
const readline = await import('readline');
|
const readline = await import('node:readline');
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const confirm = await new Promise<string>(resolve => {
|
const confirm = await new Promise<string>((resolve) => {
|
||||||
rl.question(`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `, answer => {
|
rl.question(
|
||||||
resolve(answer.toLowerCase());
|
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
|
||||||
});
|
(answer) => {
|
||||||
|
resolve(answer.toLowerCase());
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.close();
|
rl.close();
|
||||||
|
|
||||||
if (confirm !== 'y' && confirm !== 'yes') {
|
if (confirm !== 'y' && confirm !== 'yes') {
|
||||||
logger.log('Deletion cancelled.');
|
logger.log('Deletion cancelled.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the UPS from the array
|
// Remove the UPS from the array
|
||||||
config.upsDevices.splice(upsIndex, 1);
|
config.upsDevices.splice(upsIndex, 1);
|
||||||
|
|
||||||
// Save the configuration
|
// Save the configuration
|
||||||
await this.nupst.getDaemon().saveConfig(config);
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`);
|
logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`);
|
||||||
|
|
||||||
// Check if service is running and restart it if needed
|
// Check if service is running and restart it if needed
|
||||||
await this.restartServiceIfRunning();
|
await this.restartServiceIfRunning();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to delete UPS: ${error.message}`);
|
logger.error(
|
||||||
|
`Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,55 +383,69 @@ export class UpsHandler {
|
|||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current configuration
|
// Get current configuration
|
||||||
const config = this.nupst.getDaemon().getConfig();
|
const config = this.nupst.getDaemon().getConfig();
|
||||||
|
|
||||||
// Check if multi-UPS config
|
// Check if multi-UPS config
|
||||||
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
|
||||||
// Legacy single UPS configuration
|
// Legacy single UPS configuration
|
||||||
const boxWidth = 45;
|
const boxWidth = 45;
|
||||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
logger.logBoxTitle('UPS Devices', boxWidth);
|
||||||
logger.logBoxLine('Legacy single-UPS configuration detected.');
|
logger.logBoxLine('Legacy single-UPS configuration detected.');
|
||||||
|
if (!config.snmp || !config.thresholds) {
|
||||||
|
logger.logBoxLine('');
|
||||||
|
logger.logBoxLine('Error: Configuration missing SNMP or threshold settings');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
logger.logBoxLine('');
|
logger.logBoxLine('');
|
||||||
logger.logBoxLine('Default UPS:');
|
logger.logBoxLine('Default UPS:');
|
||||||
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
|
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
|
||||||
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||||
logger.logBoxLine(` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`);
|
logger.logBoxLine(
|
||||||
|
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
|
||||||
|
);
|
||||||
logger.logBoxLine('');
|
logger.logBoxLine('');
|
||||||
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
|
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
|
||||||
logger.logBoxLine('to the multi-UPS configuration format.');
|
logger.logBoxLine('to the multi-UPS configuration format.');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display UPS list
|
// Display UPS list
|
||||||
const boxWidth = 60;
|
const boxWidth = 60;
|
||||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
logger.logBoxTitle('UPS Devices', boxWidth);
|
||||||
|
|
||||||
if (config.upsDevices.length === 0) {
|
if (config.upsDevices.length === 0) {
|
||||||
logger.logBoxLine('No UPS devices configured.');
|
logger.logBoxLine('No UPS devices configured.');
|
||||||
logger.logBoxLine('Use "nupst add" to add a UPS device.');
|
logger.logBoxLine('Use "nupst add" to add a UPS device.');
|
||||||
} else {
|
} else {
|
||||||
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
|
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
|
||||||
logger.logBoxLine('');
|
logger.logBoxLine('');
|
||||||
logger.logBoxLine('ID | Name | Host | Mode | Groups');
|
logger.logBoxLine(
|
||||||
logger.logBoxLine('-----------+----------------------+-----------------+--------------+----------------');
|
'ID | Name | Host | Mode | Groups',
|
||||||
|
);
|
||||||
|
logger.logBoxLine(
|
||||||
|
'-----------+----------------------+-----------------+--------------+----------------',
|
||||||
|
);
|
||||||
|
|
||||||
for (const ups of config.upsDevices) {
|
for (const ups of config.upsDevices) {
|
||||||
const id = ups.id.padEnd(10, ' ').substring(0, 10);
|
const id = ups.id.padEnd(10, ' ').substring(0, 10);
|
||||||
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
|
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
|
||||||
const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15);
|
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 model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12);
|
||||||
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
|
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
|
||||||
|
|
||||||
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
|
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} 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 +481,7 @@ export class UpsHandler {
|
|||||||
// Handle new multi-UPS configuration format
|
// Handle new multi-UPS configuration format
|
||||||
if (config.upsDevices && config.upsDevices.length > 0) {
|
if (config.upsDevices && config.upsDevices.length > 0) {
|
||||||
logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`);
|
logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`);
|
||||||
|
|
||||||
for (let i = 0; i < config.upsDevices.length; i++) {
|
for (let i = 0; i < config.upsDevices.length; i++) {
|
||||||
const ups = config.upsDevices[i];
|
const ups = config.upsDevices[i];
|
||||||
logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`);
|
logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`);
|
||||||
@@ -465,7 +494,7 @@ export class UpsHandler {
|
|||||||
await this.testConnection(config);
|
await this.testConnection(config);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Test failed: ${error.message}`);
|
logger.error(`Test failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,11 +508,11 @@ export class UpsHandler {
|
|||||||
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
|
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
|
||||||
const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {};
|
const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {};
|
||||||
const checkInterval = config.checkInterval || 30000;
|
const checkInterval = config.checkInterval || 30000;
|
||||||
|
|
||||||
// Get UPS name and ID if available
|
// Get UPS name and ID if available
|
||||||
const upsName = config.name ? config.name : 'Default UPS';
|
const upsName = config.name ? config.name : 'Default UPS';
|
||||||
const upsId = config.id ? config.id : 'default';
|
const upsId = config.id ? config.id : 'default';
|
||||||
|
|
||||||
const boxWidth = 45;
|
const boxWidth = 45;
|
||||||
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
|
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
|
||||||
logger.logBoxLine(`UPS ID: ${upsId}`);
|
logger.logBoxLine(`UPS ID: ${upsId}`);
|
||||||
@@ -516,18 +545,22 @@ export class UpsHandler {
|
|||||||
if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
|
if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
|
||||||
logger.logBoxLine('Custom OIDs:');
|
logger.logBoxLine('Custom OIDs:');
|
||||||
logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`);
|
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(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||||
}
|
}
|
||||||
logger.logBoxLine('Thresholds:');
|
logger.logBoxLine('Thresholds:');
|
||||||
logger.logBoxLine(` Battery: ${thresholds.battery}%`);
|
logger.logBoxLine(` Battery: ${thresholds.battery}%`);
|
||||||
logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`);
|
logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`);
|
||||||
|
|
||||||
// Show group assignments if this is a UPS config
|
// Show group assignments if this is a UPS config
|
||||||
if (config.groups && Array.isArray(config.groups)) {
|
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.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
}
|
}
|
||||||
@@ -540,12 +573,12 @@ export class UpsHandler {
|
|||||||
const upsId = config.id || 'default';
|
const upsId = config.id || 'default';
|
||||||
const upsName = config.name || 'Default UPS';
|
const upsName = config.name || 'Default UPS';
|
||||||
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a test config with a short timeout
|
// Create a test config with a short timeout
|
||||||
const snmpConfig = config.snmp ? config.snmp : config.snmp;
|
const snmpConfig = config.snmp ? config.snmp : config.snmp;
|
||||||
const thresholds = config.thresholds ? config.thresholds : config.thresholds;
|
const thresholds = config.thresholds ? config.thresholds : config.thresholds;
|
||||||
|
|
||||||
const testConfig = {
|
const testConfig = {
|
||||||
...snmpConfig,
|
...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
|
||||||
@@ -568,7 +601,7 @@ export class UpsHandler {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorBoxWidth = 45;
|
const errorBoxWidth = 45;
|
||||||
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
|
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
|
||||||
logger.logBoxLine(`Error: ${error.message}`);
|
logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS.");
|
logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS.");
|
||||||
}
|
}
|
||||||
@@ -586,26 +619,26 @@ export class UpsHandler {
|
|||||||
if (status.batteryCapacity < thresholds.battery) {
|
if (status.batteryCapacity < thresholds.battery) {
|
||||||
logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
|
logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
|
||||||
logger.logBoxLine(
|
logger.logBoxLine(
|
||||||
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`
|
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
|
||||||
);
|
);
|
||||||
logger.logBoxLine(' System would initiate shutdown');
|
logger.logBoxLine(' System would initiate shutdown');
|
||||||
} else {
|
} else {
|
||||||
logger.logBoxLine('✓ Battery capacity above threshold');
|
logger.logBoxLine('✓ Battery capacity above threshold');
|
||||||
logger.logBoxLine(
|
logger.logBoxLine(
|
||||||
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`
|
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.batteryRuntime < thresholds.runtime) {
|
if (status.batteryRuntime < thresholds.runtime) {
|
||||||
logger.logBoxLine('⚠️ WARNING: Runtime below threshold');
|
logger.logBoxLine('⚠️ WARNING: Runtime below threshold');
|
||||||
logger.logBoxLine(
|
logger.logBoxLine(
|
||||||
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`
|
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
|
||||||
);
|
);
|
||||||
logger.logBoxLine(' System would initiate shutdown');
|
logger.logBoxLine(' System would initiate shutdown');
|
||||||
} else {
|
} else {
|
||||||
logger.logBoxLine('✓ Runtime above threshold');
|
logger.logBoxLine('✓ Runtime above threshold');
|
||||||
logger.logBoxLine(
|
logger.logBoxLine(
|
||||||
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`
|
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,7 +652,7 @@ export class UpsHandler {
|
|||||||
*/
|
*/
|
||||||
private async gatherSnmpSettings(
|
private async gatherSnmpSettings(
|
||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// SNMP IP Address
|
// SNMP IP Address
|
||||||
const defaultHost = snmpConfig.host || '127.0.0.1';
|
const defaultHost = snmpConfig.host || '127.0.0.1';
|
||||||
@@ -640,10 +673,9 @@ export class UpsHandler {
|
|||||||
console.log(' 3) SNMPv3 (with security features)');
|
console.log(' 3) SNMPv3 (with security features)');
|
||||||
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
|
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
|
||||||
const version = parseInt(versionInput, 10);
|
const version = parseInt(versionInput, 10);
|
||||||
snmpConfig.version =
|
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
|
||||||
versionInput.trim() && (version === 1 || version === 2 || version === 3)
|
? version
|
||||||
? version
|
: defaultVersion;
|
||||||
: defaultVersion;
|
|
||||||
|
|
||||||
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
|
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
|
||||||
// SNMP Community String (for v1/v2c)
|
// SNMP Community String (for v1/v2c)
|
||||||
@@ -663,7 +695,7 @@ export class UpsHandler {
|
|||||||
*/
|
*/
|
||||||
private async gatherSnmpV3Settings(
|
private async gatherSnmpV3Settings(
|
||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('\nSNMPv3 Security Settings:');
|
console.log('\nSNMPv3 Security Settings:');
|
||||||
|
|
||||||
@@ -721,7 +753,7 @@ export class UpsHandler {
|
|||||||
// Allow customizing the timeout value
|
// Allow customizing the timeout value
|
||||||
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
|
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
|
||||||
console.log(
|
console.log(
|
||||||
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.'
|
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.',
|
||||||
);
|
);
|
||||||
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
||||||
const timeout = parseInt(timeoutInput, 10);
|
const timeout = parseInt(timeoutInput, 10);
|
||||||
@@ -738,7 +770,7 @@ export class UpsHandler {
|
|||||||
*/
|
*/
|
||||||
private async gatherAuthenticationSettings(
|
private async gatherAuthenticationSettings(
|
||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Authentication protocol
|
// Authentication protocol
|
||||||
console.log('\nAuthentication Protocol:');
|
console.log('\nAuthentication Protocol:');
|
||||||
@@ -746,7 +778,7 @@ export class UpsHandler {
|
|||||||
console.log(' 2) SHA');
|
console.log(' 2) SHA');
|
||||||
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
|
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
|
||||||
const authProtocolInput = await prompt(
|
const authProtocolInput = await prompt(
|
||||||
`Select Authentication Protocol [${defaultAuthProtocol}]: `
|
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
|
||||||
);
|
);
|
||||||
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
|
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
|
||||||
snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
|
snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
|
||||||
@@ -764,7 +796,7 @@ export class UpsHandler {
|
|||||||
*/
|
*/
|
||||||
private async gatherPrivacySettings(
|
private async gatherPrivacySettings(
|
||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Privacy protocol
|
// Privacy protocol
|
||||||
console.log('\nPrivacy Protocol:');
|
console.log('\nPrivacy Protocol:');
|
||||||
@@ -788,31 +820,29 @@ export class UpsHandler {
|
|||||||
*/
|
*/
|
||||||
private async gatherThresholdSettings(
|
private async gatherThresholdSettings(
|
||||||
thresholds: any,
|
thresholds: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('\nShutdown Thresholds:');
|
console.log('\nShutdown Thresholds:');
|
||||||
|
|
||||||
// Battery threshold
|
// Battery threshold
|
||||||
const defaultBatteryThreshold = thresholds.battery || 60;
|
const defaultBatteryThreshold = thresholds.battery || 60;
|
||||||
const batteryThresholdInput = await prompt(
|
const batteryThresholdInput = await prompt(
|
||||||
`Battery percentage threshold [${defaultBatteryThreshold}%]: `
|
`Battery percentage threshold [${defaultBatteryThreshold}%]: `,
|
||||||
);
|
);
|
||||||
const batteryThreshold = parseInt(batteryThresholdInput, 10);
|
const batteryThreshold = parseInt(batteryThresholdInput, 10);
|
||||||
thresholds.battery =
|
thresholds.battery = batteryThresholdInput.trim() && !isNaN(batteryThreshold)
|
||||||
batteryThresholdInput.trim() && !isNaN(batteryThreshold)
|
? batteryThreshold
|
||||||
? batteryThreshold
|
: defaultBatteryThreshold;
|
||||||
: defaultBatteryThreshold;
|
|
||||||
|
|
||||||
// Runtime threshold
|
// Runtime threshold
|
||||||
const defaultRuntimeThreshold = thresholds.runtime || 20;
|
const defaultRuntimeThreshold = thresholds.runtime || 20;
|
||||||
const runtimeThresholdInput = await prompt(
|
const runtimeThresholdInput = await prompt(
|
||||||
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `
|
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `,
|
||||||
);
|
);
|
||||||
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
|
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
|
||||||
thresholds.runtime =
|
thresholds.runtime = runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
|
||||||
runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
|
? runtimeThreshold
|
||||||
? runtimeThreshold
|
: defaultRuntimeThreshold;
|
||||||
: defaultRuntimeThreshold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -822,7 +852,7 @@ export class UpsHandler {
|
|||||||
*/
|
*/
|
||||||
private async gatherUpsModelSettings(
|
private async gatherUpsModelSettings(
|
||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('\nUPS Model Selection:');
|
console.log('\nUPS Model Selection:');
|
||||||
console.log(' 1) CyberPower');
|
console.log(' 1) CyberPower');
|
||||||
@@ -832,20 +862,19 @@ export class UpsHandler {
|
|||||||
console.log(' 5) Liebert/Vertiv');
|
console.log(' 5) Liebert/Vertiv');
|
||||||
console.log(' 6) Custom (Advanced)');
|
console.log(' 6) Custom (Advanced)');
|
||||||
|
|
||||||
const defaultModelValue =
|
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
|
||||||
snmpConfig.upsModel === 'cyberpower'
|
? 1
|
||||||
? 1
|
: snmpConfig.upsModel === 'apc'
|
||||||
: snmpConfig.upsModel === 'apc'
|
? 2
|
||||||
? 2
|
: snmpConfig.upsModel === 'eaton'
|
||||||
: snmpConfig.upsModel === 'eaton'
|
? 3
|
||||||
? 3
|
: snmpConfig.upsModel === 'tripplite'
|
||||||
: snmpConfig.upsModel === 'tripplite'
|
? 4
|
||||||
? 4
|
: snmpConfig.upsModel === 'liebert'
|
||||||
: snmpConfig.upsModel === 'liebert'
|
? 5
|
||||||
? 5
|
: snmpConfig.upsModel === 'custom'
|
||||||
: snmpConfig.upsModel === 'custom'
|
? 6
|
||||||
? 6
|
: 1;
|
||||||
: 1;
|
|
||||||
|
|
||||||
const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
|
const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
|
||||||
const modelValue = parseInt(modelInput, 10) || defaultModelValue;
|
const modelValue = parseInt(modelInput, 10) || defaultModelValue;
|
||||||
@@ -892,7 +921,7 @@ export class UpsHandler {
|
|||||||
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
||||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
||||||
logger.logBoxLine(
|
logger.logBoxLine(
|
||||||
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`
|
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
|
||||||
);
|
);
|
||||||
if (ups.groups && ups.groups.length > 0) {
|
if (ups.groups && ups.groups.length > 0) {
|
||||||
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
||||||
@@ -910,10 +939,10 @@ export class UpsHandler {
|
|||||||
*/
|
*/
|
||||||
private async optionallyTestConnection(
|
private async optionallyTestConnection(
|
||||||
snmpConfig: any,
|
snmpConfig: any,
|
||||||
prompt: (question: string) => Promise<string>
|
prompt: (question: string) => Promise<string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const testConnection = await prompt(
|
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') {
|
if (testConnection.toLowerCase() === 'y') {
|
||||||
logger.log('\nTesting connection to UPS...');
|
logger.log('\nTesting connection to UPS...');
|
||||||
@@ -937,7 +966,7 @@ export class UpsHandler {
|
|||||||
const errorBoxWidth = 45;
|
const errorBoxWidth = 45;
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBoxTitle('Connection Failed!', errorBoxWidth);
|
logger.logBoxTitle('Connection Failed!', errorBoxWidth);
|
||||||
logger.logBoxLine(`Error: ${error.message}`);
|
logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
logger.log('\nPlease check your settings and try again.');
|
logger.log('\nPlease check your settings and try again.');
|
||||||
}
|
}
|
||||||
@@ -948,7 +977,7 @@ export class UpsHandler {
|
|||||||
* Check if the systemd service is running and restart it if it is
|
* Check if the systemd service is running and restart it if it is
|
||||||
* This is useful after configuration changes
|
* This is useful after configuration changes
|
||||||
*/
|
*/
|
||||||
public async restartServiceIfRunning(): Promise<void> {
|
public restartServiceIfRunning(): void {
|
||||||
try {
|
try {
|
||||||
// Check if the service is active
|
// Check if the service is active
|
||||||
const isActive =
|
const isActive =
|
||||||
@@ -972,7 +1001,9 @@ export class UpsHandler {
|
|||||||
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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('You may need to restart the service manually:');
|
||||||
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
||||||
}
|
}
|
||||||
@@ -983,4 +1014,4 @@ export class UpsHandler {
|
|||||||
// Ignore errors checking service status
|
// 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');
|
||||||
|
}
|
||||||
|
}
|
551
ts/daemon.ts
551
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 {
|
export function shortId(): string {
|
||||||
// Define the character set: a-z, A-Z, 0-9
|
// Define the character set: a-z, A-Z, 0-9
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
|
||||||
// Generate cryptographically secure random values
|
// Generate cryptographically secure random values
|
||||||
const randomValues = new Uint8Array(6);
|
const randomValues = new Uint8Array(6);
|
||||||
crypto.getRandomValues(randomValues);
|
crypto.getRandomValues(randomValues);
|
||||||
|
|
||||||
// Map each random value to a character in our set
|
// Map each random value to a character in our set
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
@@ -17,6 +17,6 @@ export function shortId(): string {
|
|||||||
const index = randomValues[i] % chars.length;
|
const index = randomValues[i] % chars.length;
|
||||||
result += chars[index];
|
result += chars[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { NupstCli } from './cli.js';
|
import { NupstCli } from './cli.ts';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.ts';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point for NUPST
|
* Main entry point for NUPST
|
||||||
@@ -13,7 +14,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the main function and handle any errors
|
// Run the main function and handle any errors
|
||||||
main().catch(error => {
|
main().catch((error) => {
|
||||||
logger.error(`Error: ${error}`);
|
logger.error(`Error: ${error}`);
|
||||||
process.exit(1);
|
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
|
* A simple logger class that provides consistent formatting for log messages
|
||||||
* including support for logboxes with title, lines, and closing
|
* including support for logboxes with title, lines, and closing
|
||||||
*/
|
*/
|
||||||
export class Logger {
|
export class Logger {
|
||||||
private currentBoxWidth: number | null = null;
|
private currentBoxWidth: number | null = null;
|
||||||
|
private currentBoxStyle: TBoxStyle = 'default';
|
||||||
private static instance: Logger;
|
private static instance: Logger;
|
||||||
|
|
||||||
/** Default width to use when no width is specified */
|
/** Default width to use when no width is specified */
|
||||||
private readonly DEFAULT_WIDTH = 60;
|
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
|
* @param message Error message to log
|
||||||
*/
|
*/
|
||||||
public error(message: string): void {
|
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
|
* @param message Warning message to log
|
||||||
*/
|
*/
|
||||||
public warn(message: string): void {
|
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
|
* @param message Success message to log
|
||||||
*/
|
*/
|
||||||
public success(message: string): void {
|
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
|
* Log a logbox title and set the current box width
|
||||||
* @param title Title of the logbox
|
* @param title Title of the logbox
|
||||||
* @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH
|
* @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.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
||||||
|
this.currentBoxStyle = style || 'default';
|
||||||
|
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Create the title line with appropriate padding
|
// Create the title line with appropriate padding
|
||||||
const paddedTitle = ` ${title} `;
|
const paddedTitle = ` ${title} `;
|
||||||
const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length;
|
const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length;
|
||||||
|
|
||||||
// Title line: ┌─ Title ───┐
|
// Title line: ┌─ Title ───┐
|
||||||
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
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
|
// No current width and no width provided, use default width
|
||||||
this.logBoxTitle('', this.DEFAULT_WIDTH);
|
this.logBoxTitle('', this.DEFAULT_WIDTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
// Calculate the available space for content
|
|
||||||
|
// Calculate the available space for content (use visible length)
|
||||||
const availableSpace = boxWidth - 2; // Account for left and right borders
|
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||||
|
const visibleLen = this.visibleLength(content);
|
||||||
if (content.length <= availableSpace - 1) {
|
|
||||||
|
if (visibleLen <= availableSpace - 1) {
|
||||||
// If content fits with at least one space for the right border stripe
|
// If content fits with at least one space for the right border stripe
|
||||||
const padding = availableSpace - content.length - 1;
|
const padding = availableSpace - visibleLen - 1;
|
||||||
console.log(`│ ${content}${' '.repeat(padding)}│`);
|
const line = `│ ${content}${' '.repeat(padding)}│`;
|
||||||
|
console.log(colorFn(line));
|
||||||
} else {
|
} else {
|
||||||
// Content is too long, let it flow out of boundaries.
|
// 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 {
|
public logBoxEnd(width?: number): void {
|
||||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||||
|
const colorFn = this.getBoxColor(this.currentBoxStyle);
|
||||||
|
|
||||||
// Create the bottom border: └────────┘
|
// Create the bottom border: └────────┘
|
||||||
console.log(`└${'─'.repeat(boxWidth - 2)}┘`);
|
const bottomLine = `└${'─'.repeat(boxWidth - 2)}┘`;
|
||||||
|
console.log(colorFn(bottomLine));
|
||||||
// Reset the current box width
|
|
||||||
|
// Reset the current box width and style
|
||||||
this.currentBoxWidth = null;
|
this.currentBoxWidth = null;
|
||||||
|
this.currentBoxStyle = 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,14 +205,15 @@ export class Logger {
|
|||||||
* @param title Title of the logbox
|
* @param title Title of the logbox
|
||||||
* @param lines Array of content lines
|
* @param lines Array of content lines
|
||||||
* @param width Width of the logbox, defaults to DEFAULT_WIDTH
|
* @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 {
|
public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void {
|
||||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
|
this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
this.logBoxLine(line);
|
this.logBoxLine(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logBoxEnd();
|
this.logBoxEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +225,109 @@ export class Logger {
|
|||||||
public logDivider(width?: number, character: string = '─'): void {
|
public logDivider(width?: number, character: string = '─'): void {
|
||||||
console.log(character.repeat(width || this.DEFAULT_WIDTH));
|
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 a singleton instance for easy use
|
||||||
export const logger = Logger.getInstance();
|
export const logger = Logger.getInstance();
|
||||||
|
54
ts/migrations/base-migration.ts
Normal file
54
ts/migrations/base-migration.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
export abstract class BaseMigration {
|
||||||
|
/**
|
||||||
|
* Migration order number
|
||||||
|
* - Order 2: v1 → v2
|
||||||
|
* - Order 3: v2 → v3
|
||||||
|
* - Order 4: v3 → v4
|
||||||
|
* etc.
|
||||||
|
*/
|
||||||
|
abstract readonly order: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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"
|
||||||
|
*/
|
||||||
|
abstract readonly toVersion: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this migration should run on the given config
|
||||||
|
*
|
||||||
|
* @param config - Raw configuration object to check
|
||||||
|
* @returns True if migration should run, false otherwise
|
||||||
|
*/
|
||||||
|
abstract shouldRun(config: any): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the migration on the given config
|
||||||
|
*
|
||||||
|
* @param config - Raw configuration object to migrate
|
||||||
|
* @returns Migrated configuration object
|
||||||
|
*/
|
||||||
|
abstract migrate(config: any): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable name for this migration
|
||||||
|
*
|
||||||
|
* @returns Migration name
|
||||||
|
*/
|
||||||
|
getName(): string {
|
||||||
|
return `Migration ${this.fromVersion} → ${this.toVersion}`;
|
||||||
|
}
|
||||||
|
}
|
10
ts/migrations/index.ts
Normal file
10
ts/migrations/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
69
ts/migrations/migration-runner.ts
Normal file
69
ts/migrations/migration-runner.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { BaseMigration } from './base-migration.ts';
|
||||||
|
import { MigrationV1ToV2 } from './migration-v1-to-v2.ts';
|
||||||
|
import { MigrationV3ToV4 } from './migration-v3-to-v4.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(),
|
||||||
|
// Add future migrations here (v4→v5, v5→v6, etc.)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sort by order to ensure they run in sequence
|
||||||
|
this.migrations.sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all applicable migrations on the config
|
||||||
|
*
|
||||||
|
* @param config - Raw configuration object to migrate
|
||||||
|
* @returns Migrated configuration and whether migrations ran
|
||||||
|
*/
|
||||||
|
async run(config: any): Promise<{ config: any; migrated: boolean }> {
|
||||||
|
let currentConfig = config;
|
||||||
|
let anyMigrationsRan = false;
|
||||||
|
|
||||||
|
logger.dim('Checking for required config migrations...');
|
||||||
|
|
||||||
|
for (const migration of this.migrations) {
|
||||||
|
const shouldRun = await migration.shouldRun(currentConfig);
|
||||||
|
|
||||||
|
if (shouldRun) {
|
||||||
|
logger.info(`Running ${migration.getName()}...`);
|
||||||
|
currentConfig = await migration.migrate(currentConfig);
|
||||||
|
anyMigrationsRan = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyMigrationsRan) {
|
||||||
|
logger.success('Configuration migrations complete');
|
||||||
|
} else {
|
||||||
|
logger.dim('No migrations needed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: currentConfig,
|
||||||
|
migrated: anyMigrationsRan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered migrations
|
||||||
|
*
|
||||||
|
* @returns Array of all migrations sorted by order
|
||||||
|
*/
|
||||||
|
getMigrations(): BaseMigration[] {
|
||||||
|
return [...this.migrations];
|
||||||
|
}
|
||||||
|
}
|
56
ts/migrations/migration-v1-to-v2.ts
Normal file
56
ts/migrations/migration-v1-to-v2.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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 order = 2;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
119
ts/migrations/migration-v3-to-v4.ts
Normal file
119
ts/migrations/migration-v3-to-v4.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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 order = 4;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
92
ts/nupst.ts
92
ts/nupst.ts
@@ -1,12 +1,12 @@
|
|||||||
import { NupstSnmp } from './snmp/manager.js';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import { NupstDaemon } from './daemon.js';
|
import { NupstDaemon } from './daemon.ts';
|
||||||
import { NupstSystemd } from './systemd.js';
|
import { NupstSystemd } from './systemd.ts';
|
||||||
import { commitinfo } from './00_commitinfo_data.js';
|
import { commitinfo } from './00_commitinfo_data.ts';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.ts';
|
||||||
import { UpsHandler } from './cli/ups-handler.js';
|
import { UpsHandler } from './cli/ups-handler.ts';
|
||||||
import { GroupHandler } from './cli/group-handler.js';
|
import { GroupHandler } from './cli/group-handler.ts';
|
||||||
import { ServiceHandler } from './cli/service-handler.js';
|
import { ServiceHandler } from './cli/service-handler.ts';
|
||||||
import * as https from 'https';
|
import * as https from 'node:https';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Nupst class that coordinates all components
|
* Main Nupst class that coordinates all components
|
||||||
@@ -31,7 +31,7 @@ export class Nupst {
|
|||||||
this.snmp.setNupst(this); // Set up bidirectional reference
|
this.snmp.setNupst(this); // Set up bidirectional reference
|
||||||
this.daemon = new NupstDaemon(this.snmp);
|
this.daemon = new NupstDaemon(this.snmp);
|
||||||
this.systemd = new NupstSystemd(this.daemon);
|
this.systemd = new NupstSystemd(this.daemon);
|
||||||
|
|
||||||
// Initialize handlers
|
// Initialize handlers
|
||||||
this.upsHandler = new UpsHandler(this);
|
this.upsHandler = new UpsHandler(this);
|
||||||
this.groupHandler = new GroupHandler(this);
|
this.groupHandler = new GroupHandler(this);
|
||||||
@@ -58,28 +58,28 @@ export class Nupst {
|
|||||||
public getSystemd(): NupstSystemd {
|
public getSystemd(): NupstSystemd {
|
||||||
return this.systemd;
|
return this.systemd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the UPS handler for UPS management
|
* Get the UPS handler for UPS management
|
||||||
*/
|
*/
|
||||||
public getUpsHandler(): UpsHandler {
|
public getUpsHandler(): UpsHandler {
|
||||||
return this.upsHandler;
|
return this.upsHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Group handler for group management
|
* Get the Group handler for group management
|
||||||
*/
|
*/
|
||||||
public getGroupHandler(): GroupHandler {
|
public getGroupHandler(): GroupHandler {
|
||||||
return this.groupHandler;
|
return this.groupHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Service handler for service management
|
* Get the Service handler for service management
|
||||||
*/
|
*/
|
||||||
public getServiceHandler(): ServiceHandler {
|
public getServiceHandler(): ServiceHandler {
|
||||||
return this.serviceHandler;
|
return this.serviceHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current version of NUPST
|
* Get the current version of NUPST
|
||||||
* @returns The current version string
|
* @returns The current version string
|
||||||
@@ -87,7 +87,7 @@ export class Nupst {
|
|||||||
public getVersion(): string {
|
public getVersion(): string {
|
||||||
return commitinfo.version;
|
return commitinfo.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an update is available
|
* Check if an update is available
|
||||||
* @returns Promise resolving to true if an update is available
|
* @returns Promise resolving to true if an update is available
|
||||||
@@ -96,39 +96,41 @@ export class Nupst {
|
|||||||
try {
|
try {
|
||||||
const latestVersion = await this.getLatestVersion();
|
const latestVersion = await this.getLatestVersion();
|
||||||
const currentVersion = this.getVersion();
|
const currentVersion = this.getVersion();
|
||||||
|
|
||||||
// Compare versions
|
// Compare versions
|
||||||
this.updateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
this.updateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||||
this.latestVersion = latestVersion;
|
this.latestVersion = latestVersion;
|
||||||
|
|
||||||
return this.updateAvailable;
|
return this.updateAvailable;
|
||||||
} catch (error) {
|
} 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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get update status information
|
* Get update status information
|
||||||
* @returns Object with update status information
|
* @returns Object with update status information
|
||||||
*/
|
*/
|
||||||
public getUpdateStatus(): {
|
public getUpdateStatus(): {
|
||||||
currentVersion: string,
|
currentVersion: string;
|
||||||
latestVersion: string,
|
latestVersion: string;
|
||||||
updateAvailable: boolean
|
updateAvailable: boolean;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
currentVersion: this.getVersion(),
|
currentVersion: this.getVersion(),
|
||||||
latestVersion: this.latestVersion || this.getVersion(),
|
latestVersion: this.latestVersion || this.getVersion(),
|
||||||
updateAvailable: this.updateAvailable
|
updateAvailable: this.updateAvailable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the latest version from npm registry
|
* Get the latest version from npm registry
|
||||||
* @returns Promise resolving to the latest version string
|
* @returns Promise resolving to the latest version string
|
||||||
*/
|
*/
|
||||||
private async getLatestVersion(): Promise<string> {
|
private getLatestVersion(): Promise<string> {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: 'registry.npmjs.org',
|
hostname: 'registry.npmjs.org',
|
||||||
@@ -136,17 +138,17 @@ export class Nupst {
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'User-Agent': `nupst/${this.getVersion()}`
|
'User-Agent': `nupst/${this.getVersion()}`,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const req = https.request(options, (res) => {
|
const req = https.request(options, (res) => {
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
res.on('data', (chunk) => {
|
res.on('data', (chunk) => {
|
||||||
data += chunk;
|
data += chunk;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const response = JSON.parse(data);
|
const response = JSON.parse(data);
|
||||||
@@ -160,15 +162,15 @@ export class Nupst {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', (error) => {
|
req.on('error', (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.end();
|
req.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two semantic version strings
|
* Compare two semantic version strings
|
||||||
* @param versionA First version
|
* @param versionA First version
|
||||||
@@ -176,39 +178,39 @@ export class Nupst {
|
|||||||
* @returns -1 if versionA < versionB, 0 if equal, 1 if versionA > versionB
|
* @returns -1 if versionA < versionB, 0 if equal, 1 if versionA > versionB
|
||||||
*/
|
*/
|
||||||
private compareVersions(versionA: string, versionB: string): number {
|
private compareVersions(versionA: string, versionB: string): number {
|
||||||
const partsA = versionA.split('.').map(part => parseInt(part, 10));
|
const partsA = versionA.split('.').map((part) => parseInt(part, 10));
|
||||||
const partsB = versionB.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++) {
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||||
const partA = i < partsA.length ? partsA[i] : 0;
|
const partA = i < partsA.length ? partsA[i] : 0;
|
||||||
const partB = i < partsB.length ? partsB[i] : 0;
|
const partB = i < partsB.length ? partsB[i] : 0;
|
||||||
|
|
||||||
if (partA > partB) return 1;
|
if (partA > partB) return 1;
|
||||||
if (partA < partB) return -1;
|
if (partA < partB) return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0; // Versions are equal
|
return 0; // Versions are equal
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the current version and update status
|
* Log the current version and update status
|
||||||
*/
|
*/
|
||||||
public logVersionInfo(checkForUpdates: boolean = true): void {
|
public logVersionInfo(checkForUpdates: boolean = true): void {
|
||||||
const version = this.getVersion();
|
const version = this.getVersion();
|
||||||
const boxWidth = 45;
|
const boxWidth = 45;
|
||||||
|
|
||||||
logger.logBoxTitle('NUPST Version', boxWidth);
|
logger.logBoxTitle('NUPST Version', boxWidth);
|
||||||
logger.logBoxLine(`Current Version: ${version}`);
|
logger.logBoxLine(`Current Version: ${version}`);
|
||||||
|
|
||||||
if (this.updateAvailable && this.latestVersion) {
|
if (this.updateAvailable && this.latestVersion) {
|
||||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} else if (checkForUpdates) {
|
} else if (checkForUpdates) {
|
||||||
logger.logBoxLine('Checking for updates...');
|
logger.logBoxLine('Checking for updates...');
|
||||||
|
|
||||||
// We can't end the box yet since we're in an async operation
|
// We can't end the box yet since we're in an async operation
|
||||||
this.checkForUpdates().then(updateAvailable => {
|
this.checkForUpdates().then((updateAvailable) => {
|
||||||
if (updateAvailable) {
|
if (updateAvailable) {
|
||||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||||
@@ -224,4 +226,4 @@ export class Nupst {
|
|||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Re-export all public types
|
// 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
|
// 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 * as snmp from 'npm:net-snmp@3.20.0';
|
||||||
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
|
import { Buffer } from 'node:buffer';
|
||||||
import { UpsOidSets } from './oid-sets.js';
|
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||||
|
import { UpsOidSets } from './oid-sets.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for SNMP communication with UPS devices
|
* Class for SNMP communication with UPS devices
|
||||||
@@ -33,7 +34,7 @@ export class NupstSnmp {
|
|||||||
// Set default OID set
|
// Set default OID set
|
||||||
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set reference to the main Nupst instance
|
* Set reference to the main Nupst instance
|
||||||
* @param nupst 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 {
|
public setNupst(nupst: any): void {
|
||||||
this.nupst = nupst;
|
this.nupst = nupst;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get reference to the main Nupst instance
|
* Get reference to the main Nupst instance
|
||||||
*/
|
*/
|
||||||
public getNupst(): any {
|
public getNupst(): any {
|
||||||
return this.nupst;
|
return this.nupst;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable debug mode
|
* Enable debug mode
|
||||||
*/
|
*/
|
||||||
@@ -70,11 +71,11 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use OIDs for the specified UPS model or default to Cyberpower
|
// Use OIDs for the specified UPS model or default to Cyberpower
|
||||||
const model = config.upsModel || 'cyberpower';
|
const model = config.upsModel || 'cyberpower';
|
||||||
this.activeOIDs = UpsOidSets.getOidSet(model);
|
this.activeOIDs = UpsOidSets.getOidSet(model);
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log(`Using OIDs for UPS model: ${model}`);
|
console.log(`Using OIDs for UPS model: ${model}`);
|
||||||
}
|
}
|
||||||
@@ -87,14 +88,16 @@ export class NupstSnmp {
|
|||||||
* @param retryCount Current retry count (unused in this implementation)
|
* @param retryCount Current retry count (unused in this implementation)
|
||||||
* @returns Promise resolving to the SNMP response value
|
* @returns Promise resolving to the SNMP response value
|
||||||
*/
|
*/
|
||||||
public async snmpGet(
|
public snmpGet(
|
||||||
oid: string,
|
oid: string,
|
||||||
config = this.DEFAULT_CONFIG,
|
config = this.DEFAULT_CONFIG,
|
||||||
retryCount = 0
|
retryCount = 0,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.debug) {
|
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);
|
console.log('Using community:', config.community);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@ export class NupstSnmp {
|
|||||||
timeout: config.timeout,
|
timeout: config.timeout,
|
||||||
transport: 'udp4',
|
transport: 'udp4',
|
||||||
idBitsSize: 32,
|
idBitsSize: 32,
|
||||||
context: config.context || ''
|
context: config.context || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set version based on config
|
// Set version based on config
|
||||||
@@ -119,23 +122,23 @@ export class NupstSnmp {
|
|||||||
|
|
||||||
// Create appropriate session based on SNMP version
|
// Create appropriate session based on SNMP version
|
||||||
let session;
|
let session;
|
||||||
|
|
||||||
if (config.version === 3) {
|
if (config.version === 3) {
|
||||||
// For SNMPv3, we need to set up authentication and privacy
|
// For SNMPv3, we need to set up authentication and privacy
|
||||||
// For SNMPv3, we need a valid security level
|
// For SNMPv3, we need a valid security level
|
||||||
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
||||||
|
|
||||||
// Create the user object with required structure for net-snmp
|
// Create the user object with required structure for net-snmp
|
||||||
const user: any = {
|
const user: any = {
|
||||||
name: config.username || ''
|
name: config.username || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set security level
|
// Set security level
|
||||||
if (securityLevel === 'noAuthNoPriv') {
|
if (securityLevel === 'noAuthNoPriv') {
|
||||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||||
} else if (securityLevel === 'authNoPriv') {
|
} else if (securityLevel === 'authNoPriv') {
|
||||||
user.level = snmp.SecurityLevel.authNoPriv;
|
user.level = snmp.SecurityLevel.authNoPriv;
|
||||||
|
|
||||||
// Set auth protocol - must provide both protocol and key
|
// Set auth protocol - must provide both protocol and key
|
||||||
if (config.authProtocol && config.authKey) {
|
if (config.authProtocol && config.authKey) {
|
||||||
if (config.authProtocol === 'MD5') {
|
if (config.authProtocol === 'MD5') {
|
||||||
@@ -153,7 +156,7 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
} else if (securityLevel === 'authPriv') {
|
} else if (securityLevel === 'authPriv') {
|
||||||
user.level = snmp.SecurityLevel.authPriv;
|
user.level = snmp.SecurityLevel.authPriv;
|
||||||
|
|
||||||
// Set auth protocol - must provide both protocol and key
|
// Set auth protocol - must provide both protocol and key
|
||||||
if (config.authProtocol && config.authKey) {
|
if (config.authProtocol && config.authKey) {
|
||||||
if (config.authProtocol === 'MD5') {
|
if (config.authProtocol === 'MD5') {
|
||||||
@@ -162,7 +165,7 @@ export class NupstSnmp {
|
|||||||
user.authProtocol = snmp.AuthProtocols.sha;
|
user.authProtocol = snmp.AuthProtocols.sha;
|
||||||
}
|
}
|
||||||
user.authKey = config.authKey;
|
user.authKey = config.authKey;
|
||||||
|
|
||||||
// Set privacy protocol - must provide both protocol and key
|
// Set privacy protocol - must provide both protocol and key
|
||||||
if (config.privProtocol && config.privKey) {
|
if (config.privProtocol && config.privKey) {
|
||||||
if (config.privProtocol === 'DES') {
|
if (config.privProtocol === 'DES') {
|
||||||
@@ -186,18 +189,20 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('SNMPv3 user configuration:', {
|
console.log('SNMPv3 user configuration:', {
|
||||||
name: user.name,
|
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',
|
authProtocol: user.authProtocol ? 'Set' : 'Not Set',
|
||||||
authKey: user.authKey ? 'Set' : 'Not Set',
|
authKey: user.authKey ? 'Set' : 'Not Set',
|
||||||
privProtocol: user.privProtocol ? '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);
|
session = snmp.createV3Session(config.host, user, options);
|
||||||
} else {
|
} else {
|
||||||
// For SNMPv1/v2c, we use the community string
|
// For SNMPv1/v2c, we use the community string
|
||||||
@@ -229,9 +234,11 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for SNMP errors in the response
|
// Check for SNMP errors in the response
|
||||||
if (varbinds[0].type === snmp.ObjectType.NoSuchObject ||
|
if (
|
||||||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
|
varbinds[0].type === snmp.ObjectType.NoSuchObject ||
|
||||||
varbinds[0].type === snmp.ObjectType.EndOfMibView) {
|
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
|
||||||
|
varbinds[0].type === snmp.ObjectType.EndOfMibView
|
||||||
|
) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
|
console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
|
||||||
}
|
}
|
||||||
@@ -245,7 +252,7 @@ export class NupstSnmp {
|
|||||||
// Handle specific types that might need conversion
|
// Handle specific types that might need conversion
|
||||||
if (Buffer.isBuffer(value)) {
|
if (Buffer.isBuffer(value)) {
|
||||||
// If value is a Buffer, try to convert it to a string if it's printable ASCII
|
// 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) {
|
if (isPrintableAscii) {
|
||||||
value = value.toString();
|
value = value.toString();
|
||||||
}
|
}
|
||||||
@@ -258,7 +265,7 @@ export class NupstSnmp {
|
|||||||
console.log('SNMP response:', {
|
console.log('SNMP response:', {
|
||||||
oid: varbinds[0].oid,
|
oid: varbinds[0].oid,
|
||||||
type: varbinds[0].type,
|
type: varbinds[0].type,
|
||||||
value: value
|
value: value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +283,7 @@ export class NupstSnmp {
|
|||||||
try {
|
try {
|
||||||
// Set active OID set based on UPS model in config
|
// Set active OID set based on UPS model in config
|
||||||
this.setActiveOIDs(config);
|
this.setActiveOIDs(config);
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
console.log('Getting UPS status with config:');
|
console.log('Getting UPS status with config:');
|
||||||
@@ -299,18 +306,30 @@ export class NupstSnmp {
|
|||||||
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all values with independent retry logic
|
// Get all values with independent retry logic
|
||||||
const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config);
|
const powerStatusValue = await this.getSNMPValueWithRetry(
|
||||||
const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0;
|
this.activeOIDs.POWER_STATUS,
|
||||||
const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0;
|
'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
|
// Determine power status - handle different values for different UPS models
|
||||||
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
|
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
|
||||||
|
|
||||||
// Convert to minutes for UPS models with different time units
|
// Convert to minutes for UPS models with different time units
|
||||||
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
powerStatus,
|
powerStatus,
|
||||||
batteryCapacity,
|
batteryCapacity,
|
||||||
@@ -321,7 +340,7 @@ export class NupstSnmp {
|
|||||||
batteryRuntime,
|
batteryRuntime,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
console.log('UPS status result:');
|
console.log('UPS status result:');
|
||||||
@@ -330,15 +349,20 @@ export class NupstSnmp {
|
|||||||
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
||||||
console.log('---------------------------------------');
|
console.log('---------------------------------------');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.error('---------------------------------------');
|
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('---------------------------------------');
|
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
|
* @returns Promise resolving to the SNMP value
|
||||||
*/
|
*/
|
||||||
private async getSNMPValueWithRetry(
|
private async getSNMPValueWithRetry(
|
||||||
oid: string,
|
oid: string,
|
||||||
description: string,
|
description: string,
|
||||||
config: ISnmpConfig
|
config: ISnmpConfig,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (oid === '') {
|
if (oid === '') {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
@@ -360,11 +384,11 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log(`Getting ${description} OID: ${oid}`);
|
console.log(`Getting ${description} OID: ${oid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const value = await this.snmpGet(oid, config);
|
const value = await this.snmpGet(oid, config);
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
@@ -373,19 +397,22 @@ export class NupstSnmp {
|
|||||||
return value;
|
return value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.debug) {
|
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 we're using SNMPv3, try with different security levels
|
||||||
if (config.version === 3) {
|
if (config.version === 3) {
|
||||||
return await this.tryFallbackSecurityLevels(oid, description, config);
|
return await this.tryFallbackSecurityLevels(oid, description, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try with standard OIDs as fallback
|
// Try with standard OIDs as fallback
|
||||||
if (config.upsModel !== 'custom') {
|
if (config.upsModel !== 'custom') {
|
||||||
return await this.tryStandardOids(oid, description, config);
|
return await this.tryStandardOids(oid, description, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a default value if all attempts fail
|
// Return a default value if all attempts fail
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log(`Using default value 0 for ${description}`);
|
console.log(`Using default value 0 for ${description}`);
|
||||||
@@ -402,14 +429,14 @@ export class NupstSnmp {
|
|||||||
* @returns Promise resolving to the SNMP value
|
* @returns Promise resolving to the SNMP value
|
||||||
*/
|
*/
|
||||||
private async tryFallbackSecurityLevels(
|
private async tryFallbackSecurityLevels(
|
||||||
oid: string,
|
oid: string,
|
||||||
description: string,
|
description: string,
|
||||||
config: ISnmpConfig
|
config: ISnmpConfig,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log(`Retrying ${description} with fallback security level...`);
|
console.log(`Retrying ${description} with fallback security level...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try with authNoPriv if current level is authPriv
|
// Try with authNoPriv if current level is authPriv
|
||||||
if (config.securityLevel === 'authPriv') {
|
if (config.securityLevel === 'authPriv') {
|
||||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
||||||
@@ -424,11 +451,14 @@ export class NupstSnmp {
|
|||||||
return value;
|
return value;
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
if (this.debug) {
|
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
|
// Try with noAuthNoPriv as a last resort
|
||||||
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
|
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
|
||||||
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
|
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
|
||||||
@@ -443,11 +473,14 @@ export class NupstSnmp {
|
|||||||
return value;
|
return value;
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
if (this.debug) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,18 +492,20 @@ export class NupstSnmp {
|
|||||||
* @returns Promise resolving to the SNMP value
|
* @returns Promise resolving to the SNMP value
|
||||||
*/
|
*/
|
||||||
private async tryStandardOids(
|
private async tryStandardOids(
|
||||||
oid: string,
|
oid: string,
|
||||||
description: string,
|
description: string,
|
||||||
config: ISnmpConfig
|
config: ISnmpConfig,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// Try RFC 1628 standard UPS MIB OIDs
|
// Try RFC 1628 standard UPS MIB OIDs
|
||||||
const standardOIDs = UpsOidSets.getStandardOids();
|
const standardOIDs = UpsOidSets.getStandardOids();
|
||||||
|
|
||||||
if (this.debug) {
|
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);
|
const standardValue = await this.snmpGet(standardOIDs[description], config);
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log(`${description} standard OID value:`, standardValue);
|
console.log(`${description} standard OID value:`, standardValue);
|
||||||
@@ -478,10 +513,13 @@ export class NupstSnmp {
|
|||||||
return standardValue;
|
return standardValue;
|
||||||
} catch (stdError) {
|
} catch (stdError) {
|
||||||
if (this.debug) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,8 +530,8 @@ export class NupstSnmp {
|
|||||||
* @returns Standardized power status
|
* @returns Standardized power status
|
||||||
*/
|
*/
|
||||||
private determinePowerStatus(
|
private determinePowerStatus(
|
||||||
upsModel: TUpsModel | undefined,
|
upsModel: TUpsModel | undefined,
|
||||||
powerStatusValue: number
|
powerStatusValue: number,
|
||||||
): 'online' | 'onBattery' | 'unknown' {
|
): 'online' | 'onBattery' | 'unknown' {
|
||||||
if (upsModel === 'cyberpower') {
|
if (upsModel === 'cyberpower') {
|
||||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
// CyberPower RMCARD205: upsBaseOutputStatus values
|
||||||
@@ -527,7 +565,7 @@ export class NupstSnmp {
|
|||||||
return 'onBattery';
|
return 'onBattery';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,25 +576,29 @@ export class NupstSnmp {
|
|||||||
* @returns Processed runtime in minutes
|
* @returns Processed runtime in minutes
|
||||||
*/
|
*/
|
||||||
private processRuntimeValue(
|
private processRuntimeValue(
|
||||||
upsModel: TUpsModel | undefined,
|
upsModel: TUpsModel | undefined,
|
||||||
batteryRuntime: number
|
batteryRuntime: number,
|
||||||
): number {
|
): number {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log('Raw runtime value:', batteryRuntime);
|
console.log('Raw runtime value:', batteryRuntime);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upsModel === 'cyberpower' && batteryRuntime > 0) {
|
if (upsModel === 'cyberpower' && batteryRuntime > 0) {
|
||||||
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
|
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
|
||||||
const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
|
const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
|
||||||
if (this.debug) {
|
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;
|
return minutes;
|
||||||
} else if (upsModel === 'eaton' && batteryRuntime > 0) {
|
} else if (upsModel === 'eaton' && batteryRuntime > 0) {
|
||||||
// Eaton: Runtime is in seconds, convert to minutes
|
// Eaton: Runtime is in seconds, convert to minutes
|
||||||
const minutes = Math.floor(batteryRuntime / 60);
|
const minutes = Math.floor(batteryRuntime / 60);
|
||||||
if (this.debug) {
|
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;
|
return minutes;
|
||||||
} else if (batteryRuntime > 10000) {
|
} else if (batteryRuntime > 10000) {
|
||||||
@@ -567,7 +609,7 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
return minutes;
|
return minutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
return batteryRuntime;
|
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
|
* OID sets for different UPS models
|
||||||
@@ -15,41 +15,41 @@ export class UpsOidSets {
|
|||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
||||||
},
|
},
|
||||||
|
|
||||||
// APC OIDs
|
// APC OIDs
|
||||||
apc: {
|
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', // Power status (1=online, 2=on battery)
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
||||||
},
|
},
|
||||||
|
|
||||||
// Eaton OIDs
|
// Eaton OIDs
|
||||||
eaton: {
|
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 (3=normal/mains, 5=battery)
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
||||||
},
|
},
|
||||||
|
|
||||||
// TrippLite OIDs
|
// TrippLite OIDs
|
||||||
tripplite: {
|
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', // Power status
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
||||||
},
|
},
|
||||||
|
|
||||||
// Liebert/Vertiv OIDs
|
// Liebert/Vertiv OIDs
|
||||||
liebert: {
|
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', // Power status
|
||||||
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
||||||
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
||||||
},
|
},
|
||||||
|
|
||||||
// Custom OIDs (to be provided by the user)
|
// Custom OIDs (to be provided by the user)
|
||||||
custom: {
|
custom: {
|
||||||
POWER_STATUS: '',
|
POWER_STATUS: '',
|
||||||
BATTERY_CAPACITY: '',
|
BATTERY_CAPACITY: '',
|
||||||
BATTERY_RUNTIME: '',
|
BATTERY_RUNTIME: '',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,9 +67,9 @@ export class UpsOidSets {
|
|||||||
*/
|
*/
|
||||||
public static getStandardOids(): Record<string, string> {
|
public static getStandardOids(): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0' // upsEstimatedMinutesRemaining
|
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
* Type definitions for SNMP module
|
* Type definitions for SNMP module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Buffer } from 'node:buffer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UPS status interface
|
* UPS status interface
|
||||||
*/
|
*/
|
||||||
@@ -47,11 +49,11 @@ export interface ISnmpConfig {
|
|||||||
timeout: number;
|
timeout: number;
|
||||||
|
|
||||||
context?: string;
|
context?: string;
|
||||||
|
|
||||||
// SNMPv1/v2c
|
// SNMPv1/v2c
|
||||||
/** Community string for SNMPv1/v2c */
|
/** Community string for SNMPv1/v2c */
|
||||||
community?: string;
|
community?: string;
|
||||||
|
|
||||||
// SNMPv3
|
// SNMPv3
|
||||||
/** Security level for SNMPv3 */
|
/** Security level for SNMPv3 */
|
||||||
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
|
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
|
||||||
@@ -65,7 +67,7 @@ export interface ISnmpConfig {
|
|||||||
privProtocol?: 'DES' | 'AES';
|
privProtocol?: 'DES' | 'AES';
|
||||||
/** Privacy key for SNMPv3 */
|
/** Privacy key for SNMPv3 */
|
||||||
privKey?: string;
|
privKey?: string;
|
||||||
|
|
||||||
// UPS model and custom OIDs
|
// UPS model and custom OIDs
|
||||||
/** UPS model for OID selection */
|
/** UPS model for OID selection */
|
||||||
upsModel?: TUpsModel;
|
upsModel?: TUpsModel;
|
||||||
@@ -89,4 +91,4 @@ export interface ISnmpV3SecurityParams {
|
|||||||
msgAuthenticationParameters: Buffer;
|
msgAuthenticationParameters: Buffer;
|
||||||
/** Privacy parameters */
|
/** Privacy parameters */
|
||||||
msgPrivacyParameters: Buffer;
|
msgPrivacyParameters: Buffer;
|
||||||
}
|
}
|
||||||
|
207
ts/systemd.ts
207
ts/systemd.ts
@@ -1,7 +1,9 @@
|
|||||||
import { promises as fs } from 'fs';
|
import process from 'node:process';
|
||||||
import { execSync } from 'child_process';
|
import { promises as fs } from 'node:fs';
|
||||||
import { NupstDaemon } from './daemon.js';
|
import { execSync } from 'node:child_process';
|
||||||
import { logger } from './logger.js';
|
import { NupstDaemon } from './daemon.ts';
|
||||||
|
import { logger } from './logger.ts';
|
||||||
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for managing systemd service
|
* Class for managing systemd service
|
||||||
@@ -14,17 +16,17 @@ export class NupstSystemd {
|
|||||||
|
|
||||||
/** Template for the systemd service file */
|
/** Template for the systemd service file */
|
||||||
private readonly serviceTemplate = `[Unit]
|
private readonly serviceTemplate = `[Unit]
|
||||||
Description=Node.js UPS Shutdown Tool for Multiple UPS Devices
|
Description=NUPST - Deno-powered UPS Monitoring Tool
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/opt/nupst/bin/nupst daemon-start
|
ExecStart=/usr/local/bin/nupst service start-daemon
|
||||||
Restart=always
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
User=root
|
User=root
|
||||||
Group=root
|
Group=root
|
||||||
Environment=PATH=/usr/bin:/usr/local/bin
|
Environment=PATH=/usr/bin:/usr/local/bin
|
||||||
Environment=NODE_ENV=production
|
WorkingDirectory=/opt/nupst
|
||||||
WorkingDirectory=/tmp
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -65,7 +67,7 @@ WantedBy=multi-user.target
|
|||||||
try {
|
try {
|
||||||
// Check if configuration exists before installing
|
// Check if configuration exists before installing
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
|
|
||||||
// Write the service file
|
// Write the service file
|
||||||
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
||||||
const boxWidth = 50;
|
const boxWidth = 50;
|
||||||
@@ -81,7 +83,7 @@ WantedBy=multi-user.target
|
|||||||
logger.logBoxLine('Service enabled to start on boot');
|
logger.logBoxLine('Service enabled to start on boot');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} 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
|
// Just rethrow the error as the message has already been displayed
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -98,14 +100,14 @@ WantedBy=multi-user.target
|
|||||||
try {
|
try {
|
||||||
// Check if configuration exists before starting
|
// Check if configuration exists before starting
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
|
|
||||||
execSync('systemctl start nupst.service');
|
execSync('systemctl start nupst.service');
|
||||||
const boxWidth = 45;
|
const boxWidth = 45;
|
||||||
logger.logBoxTitle('Service Status', boxWidth);
|
logger.logBoxTitle('Service Status', boxWidth);
|
||||||
logger.logBoxLine('NUPST service started successfully');
|
logger.logBoxLine('NUPST service started successfully');
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
} catch (error) {
|
} 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
|
// Exit with error code since configuration is required
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -118,7 +120,7 @@ WantedBy=multi-user.target
|
|||||||
* Stop the systemd service
|
* Stop the systemd service
|
||||||
* @throws Error if stop fails
|
* @throws Error if stop fails
|
||||||
*/
|
*/
|
||||||
public async stop(): Promise<void> {
|
public stop(): void {
|
||||||
try {
|
try {
|
||||||
execSync('systemctl stop nupst.service');
|
execSync('systemctl stop nupst.service');
|
||||||
logger.success('NUPST service stopped');
|
logger.success('NUPST service stopped');
|
||||||
@@ -142,10 +144,10 @@ WantedBy=multi-user.target
|
|||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
this.daemon.getNupstSnmp().enableDebug();
|
this.daemon.getNupstSnmp().enableDebug();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display version information
|
// Display version information
|
||||||
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
|
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
|
||||||
|
|
||||||
// Check if config exists first
|
// Check if config exists first
|
||||||
try {
|
try {
|
||||||
await this.checkConfigExists();
|
await this.checkConfigExists();
|
||||||
@@ -153,11 +155,13 @@ WantedBy=multi-user.target
|
|||||||
// Error message already displayed by checkConfigExists
|
// Error message already displayed by checkConfigExists
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.displayServiceStatus();
|
await this.displayServiceStatus();
|
||||||
await this.displayAllUpsStatus();
|
await this.displayAllUpsStatus();
|
||||||
} catch (error) {
|
} 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 +169,53 @@ WantedBy=multi-user.target
|
|||||||
* Display the systemd service status
|
* Display the systemd service status
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async displayServiceStatus(): Promise<void> {
|
private displayServiceStatus(): void {
|
||||||
try {
|
try {
|
||||||
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
const serviceStatus = execSync('systemctl status nupst.service').toString();
|
||||||
const boxWidth = 45;
|
const lines = serviceStatus.split('\n');
|
||||||
logger.logBoxTitle('Service Status', boxWidth);
|
|
||||||
// Process each line of the status output
|
// Parse key information from systemctl output
|
||||||
serviceStatus.split('\n').forEach(line => {
|
let isActive = false;
|
||||||
logger.logBoxLine(line);
|
let pid = '';
|
||||||
});
|
let memory = '';
|
||||||
logger.logBoxEnd();
|
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
|
||||||
|
console.log('');
|
||||||
|
if (isActive) {
|
||||||
|
console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
||||||
|
} else {
|
||||||
|
console.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)}`);
|
||||||
|
console.log(` ${details.join(' ')}`);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const boxWidth = 45;
|
console.log('');
|
||||||
logger.logBoxTitle('Service Status', boxWidth);
|
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
||||||
logger.logBoxLine('Service is not running');
|
console.log('');
|
||||||
logger.logBoxEnd();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,90 +229,93 @@ WantedBy=multi-user.target
|
|||||||
await this.daemon.loadConfig();
|
await this.daemon.loadConfig();
|
||||||
const config = this.daemon.getConfig();
|
const config = this.daemon.getConfig();
|
||||||
const snmp = this.daemon.getNupstSnmp();
|
const snmp = this.daemon.getNupstSnmp();
|
||||||
|
|
||||||
// Check if we have the new multi-UPS config format
|
// Check if we have the new multi-UPS config format
|
||||||
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||||
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
|
console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`));
|
||||||
|
|
||||||
// Show status for each UPS
|
// Show status for each UPS
|
||||||
for (const ups of config.upsDevices) {
|
for (const ups of config.upsDevices) {
|
||||||
await this.displaySingleUpsStatus(ups, snmp);
|
await this.displaySingleUpsStatus(ups, snmp);
|
||||||
}
|
}
|
||||||
} else if (config.snmp) {
|
} else if (config.snmp) {
|
||||||
// Legacy single UPS configuration
|
// Legacy single UPS configuration
|
||||||
|
console.log(theme.info('UPS Devices (1):'));
|
||||||
const legacyUps = {
|
const legacyUps = {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'Default UPS',
|
name: 'Default UPS',
|
||||||
snmp: config.snmp,
|
snmp: config.snmp,
|
||||||
thresholds: config.thresholds,
|
thresholds: config.thresholds,
|
||||||
groups: []
|
groups: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||||
} else {
|
} else {
|
||||||
logger.error('No UPS devices found in configuration');
|
console.log('');
|
||||||
|
console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`);
|
||||||
|
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
||||||
|
console.log('');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const boxWidth = 45;
|
console.log('');
|
||||||
logger.logBoxTitle('UPS Status', boxWidth);
|
console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`);
|
||||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
|
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||||
logger.logBoxEnd();
|
console.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display status of a single UPS
|
* Display status of a single UPS
|
||||||
* @param ups UPS configuration
|
* @param ups UPS configuration
|
||||||
* @param snmp SNMP manager
|
* @param snmp SNMP manager
|
||||||
*/
|
*/
|
||||||
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
|
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();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a test config with a short timeout
|
// Create a test config with a short timeout
|
||||||
const testConfig = {
|
const testConfig = {
|
||||||
...ups.snmp,
|
...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);
|
const status = await snmp.getUpsStatus(testConfig);
|
||||||
|
|
||||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
// Determine status symbol based on power status
|
||||||
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
|
let statusSymbol = symbols.unknown;
|
||||||
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
|
if (status.powerStatus === 'online') {
|
||||||
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
|
statusSymbol = symbols.running;
|
||||||
|
} else if (status.powerStatus === 'onBattery') {
|
||||||
// Show threshold status
|
statusSymbol = symbols.warning;
|
||||||
logger.logBoxLine('');
|
}
|
||||||
logger.logBoxLine('Thresholds:');
|
|
||||||
logger.logBoxLine(` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
|
// Display UPS name and power status
|
||||||
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
|
console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
||||||
}`);
|
|
||||||
logger.logBoxLine(` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
|
// Display battery with color coding
|
||||||
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
}`);
|
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
||||||
|
console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||||
logger.logBoxEnd();
|
|
||||||
|
// Display host info
|
||||||
|
console.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;
|
||||||
|
});
|
||||||
|
console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
// Display error for this UPS
|
||||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`);
|
console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
||||||
logger.logBoxEnd();
|
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||||
|
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
|
console.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +328,7 @@ WantedBy=multi-user.target
|
|||||||
await this.stopService();
|
await this.stopService();
|
||||||
await this.disableService();
|
await this.disableService();
|
||||||
await this.removeServiceFile();
|
await this.removeServiceFile();
|
||||||
|
|
||||||
// Reload systemd daemon
|
// Reload systemd daemon
|
||||||
execSync('systemctl daemon-reload');
|
execSync('systemctl daemon-reload');
|
||||||
logger.log('Systemd daemon reloaded');
|
logger.log('Systemd daemon reloaded');
|
||||||
@@ -304,7 +343,7 @@ WantedBy=multi-user.target
|
|||||||
* Stop the service if it's running
|
* Stop the service if it's running
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async stopService(): Promise<void> {
|
private stopService(): void {
|
||||||
try {
|
try {
|
||||||
logger.log('Stopping NUPST service...');
|
logger.log('Stopping NUPST service...');
|
||||||
execSync('systemctl stop nupst.service');
|
execSync('systemctl stop nupst.service');
|
||||||
@@ -318,7 +357,7 @@ WantedBy=multi-user.target
|
|||||||
* Disable the service
|
* Disable the service
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async disableService(): Promise<void> {
|
private disableService(): void {
|
||||||
try {
|
try {
|
||||||
logger.log('Disabling NUPST service...');
|
logger.log('Disabling NUPST service...');
|
||||||
execSync('systemctl disable nupst.service');
|
execSync('systemctl disable nupst.service');
|
||||||
@@ -340,4 +379,4 @@ WantedBy=multi-user.target
|
|||||||
logger.log('Service file did not exist');
|
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