Compare commits
	
		
			107 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b37e1aae6c | |||
| 7076829747 | |||
| 1387ca262b | |||
| 684f034aee | |||
| a63ec16d63 | |||
| 85f34cf96a | |||
| 4d28614e08 | |||
| 567c7be7c5 | |||
| a897a7c780 | |||
| accf137216 | |||
| c3441946cb | |||
| 37ccbf58fd | |||
| 071ded9c41 | |||
| b935087d50 | |||
| e1383097b2 | |||
| dff0ea610b | |||
| 4faa10c494 | |||
| c2d39cc19a | |||
| 9ccbbbdc37 | |||
| 1705ffe2be | |||
| 968cbbd8fc | |||
| a2ae9960b6 | |||
| df6a44d5d9 | |||
| 9efcc4b437 | |||
| 5903ae71be | |||
| a649c598ad | |||
| 5f4f3ecbc3 | |||
| 806f81c6a0 | |||
| 88e353eec6 | |||
| 80ff1b1230 | |||
| 1075335497 | |||
| eafb5207a4 | |||
| 9969e0f703 | |||
| ac4b2c95f3 | |||
| c593d76ead | |||
| 01ccf2d080 | |||
| 0e55f22dad | |||
| bd3042de25 | |||
| 456351ca34 | |||
| 00afa317ef | |||
| 45ee8208b5 | |||
| 39bf3e2239 | |||
| f3de3f0618 | |||
| 03056d279d | |||
| f860f39e59 | |||
| fa4516de3b | |||
| 539547beb8 | |||
| 6eb92959ec | |||
| 4af9af0845 | |||
| f7e12cdcbb | |||
| 002498b91b | |||
| 459911fe5f | |||
| 9859a02ea2 | |||
| 65444b6d25 | |||
| d049e8741f | |||
| 1123a99aea | |||
| d01e878310 | |||
| 588aeabf4b | |||
| 87005e72f1 | |||
| f799c2ee66 | |||
| 1a029ba493 | |||
| 5b756dd223 | |||
| 4cac599a58 | |||
| be6a7314c3 | |||
| 83ba9c2611 | |||
| 22ab472e58 | |||
| 9a77030377 | |||
| ceff285ff5 | |||
| d8bfbf0be3 | |||
| 3e6b883b38 | |||
| 47ef918128 | |||
| 5951638967 | |||
| b06e2b2273 | |||
| cc1cfe894c | |||
| da49b7a5bf | |||
| 4de6081a74 | |||
| 5a13e49803 | |||
| 2737fca294 | |||
| 896233914f | |||
| 5bb775b17d | |||
| ae8219acf7 | |||
| 4ad383884c | |||
| 65a9d1c798 | |||
| f583e1466f | |||
| 9d893a97b6 | |||
| aa52d5e9f6 | |||
| 623b7ee51f | |||
| 897e86ad60 | |||
| ed78db20e2 | |||
| bd00dfe02c | |||
| 55c040df82 | |||
| e68654a022 | |||
| 89a5d23d2f | |||
| f9aa1cfd2f | |||
| e47f316d0a | |||
| 901127f784 | |||
| dc4fd5afba | |||
| a7ced10f92 | |||
| 9b9e009523 | |||
| 1819b6827a | |||
| bd5b85f6b0 | |||
| c7db209da7 | |||
| bbb8f4a22c | |||
| ebc6f65fa9 | |||
| 0a459f9cd0 | |||
| cf231e9785 | |||
| edce110c8a | 
							
								
								
									
										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 "" | ||||||
							
								
								
									
										16
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.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 | ||||||
|  |  | ||||||
|  | # Development | ||||||
| .nogit/ | .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' | ||||||
							
								
								
									
										49
									
								
								bin/nupst
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								bin/nupst
									
									
									
									
									
								
							| @@ -1,49 +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" |  | ||||||
|  |  | ||||||
| # Set Node.js binary path directly |  | ||||||
| NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node" |  | ||||||
|  |  | ||||||
| # If binary doesn't exist, try system Node as fallback |  | ||||||
| if [ ! -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 at $NODE_BIN" |  | ||||||
|     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 |  | ||||||
							
								
								
									
										608
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										608
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,35 +1,544 @@ | |||||||
| # 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) | ||||||
|  |  | ||||||
|  | Improve UPS device listing table formatting for better column alignment | ||||||
|  |  | ||||||
|  | - Adjusted header spacing for the Host column and overall table alignment in the UPS handler output. | ||||||
|  |  | ||||||
|  | ## 2025-03-28 - 3.1.1 - fix(cli) | ||||||
|  |  | ||||||
|  | Improve table header formatting in group and UPS listings | ||||||
|  |  | ||||||
|  | - Adjusted column padding in group listing for proper alignment | ||||||
|  | - Fixed UPS table header spacing for consistent CLI output | ||||||
|  |  | ||||||
|  | ## 2025-03-28 - 3.1.0 - feat(cli) | ||||||
|  |  | ||||||
|  | Refactor CLI commands to use dedicated handlers for UPS, group, and service management | ||||||
|  |  | ||||||
|  | - Extracted UPS-related CLI logic into a new UpsHandler | ||||||
|  | - Introduced GroupHandler to manage UPS groups commands | ||||||
|  | - Added ServiceHandler for systemd service operations | ||||||
|  | - Updated CLI routing in cli.ts to delegate commands to the new handlers | ||||||
|  | - Exposed getters for the new handlers in the Nupst class | ||||||
|  |  | ||||||
|  | ## 2025-03-28 - 3.0.1 - fix(cli) | ||||||
|  |  | ||||||
|  | Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module | ||||||
|  | and replacing it with the shortId helper. | ||||||
|  |  | ||||||
|  | - Deleted the unused promptForUniqueUpsId method from ts/cli.ts. | ||||||
|  | - Updated UPS configuration to generate a unique ID directly using helpers.shortId(). | ||||||
|  | - Improved code clarity by removing unnecessary interactive prompts for UPS IDs. | ||||||
|  |  | ||||||
|  | ## 2025-03-28 - 3.0.0 - BREAKING CHANGE(core) | ||||||
|  |  | ||||||
|  | Add multi-UPS support and group management; update CLI, configuration and documentation to support | ||||||
|  | multiple UPS devices with group modes | ||||||
|  |  | ||||||
|  | - Implemented multi-UPS configuration with an array of UPS devices and groups in the configuration | ||||||
|  |   file | ||||||
|  | - Added group management commands (group add, edit, delete, list) with redundant and non-redundant | ||||||
|  |   modes | ||||||
|  | - Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group | ||||||
|  |   subcommands | ||||||
|  | - Updated readme and documentation to reflect new configuration structure and features | ||||||
|  | - Enhanced logging and status display for multiple UPS devices | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.17 - fix(logger) | ||||||
|  |  | ||||||
|  | Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set | ||||||
|  | width. | ||||||
|  |  | ||||||
|  | - Removed the reset of currentBoxWidth in logBoxEnd to allow persistent width across logbox calls. | ||||||
|  | - Ensures that logBoxLine uses the previously set width when no new width is provided. | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.16 - fix(cli) | ||||||
|  |  | ||||||
|  | Improve CLI logging consistency by replacing direct console output with unified logger calls. | ||||||
|  |  | ||||||
|  | - Replaced console.log and console.error with logger.log and logger.error in CLI commands | ||||||
|  | - Standardized debug, error, and status messages using logger's logbox utilities | ||||||
|  | - Enhanced consistency of log output throughout the ts/cli.ts file | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.15 - fix(logger) | ||||||
|  |  | ||||||
|  | Replace direct console logging with unified logger interface for consistent formatting | ||||||
|  |  | ||||||
|  | - Substitute console.log, console.error, and related calls with logger methods in cli, daemon, | ||||||
|  |   systemd, nupst, and index modules | ||||||
|  | - Integrate logBox formatting for structured output and consistent log presentation | ||||||
|  | - Update test expectations in test.logger.ts to check for standardized error messages | ||||||
|  | - Refactor logging calls throughout the codebase for improved clarity and maintainability | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.14 - fix(systemd) | ||||||
|  |  | ||||||
|  | Shorten closing log divider in systemd service installation output for consistent formatting. | ||||||
|  |  | ||||||
|  | - Replaced the overly long footer with a shorter one in ts/systemd.ts. | ||||||
|  | - This change improves log readability without affecting functionality. | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.13 - fix(cli) | ||||||
|  |  | ||||||
|  | Fix CLI update output box formatting | ||||||
|  |  | ||||||
|  | - Adjusted the closing box line in the update process log messages for consistent visual formatting | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.12 - fix(systemd) | ||||||
|  |  | ||||||
|  | Adjust logging border in systemd service installation output | ||||||
|  |  | ||||||
|  | - Updated the closing border line for consistent output formatting in ts/systemd.ts | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.11 - fix(cli, systemd) | ||||||
|  |  | ||||||
|  | Adjust log formatting for consistent output in CLI and systemd commands | ||||||
|  |  | ||||||
|  | - Fixed spacing issues in service installation and status log messages in the systemd module. | ||||||
|  | - Revised output formatting in the CLI to improve message clarity. | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.10 - fix(daemon) | ||||||
|  |  | ||||||
|  | Adjust console log box formatting for consistent output in daemon status messages | ||||||
|  |  | ||||||
|  | - Updated closing box borders to align properly in configuration error, periodic updates, and UPS | ||||||
|  |   status logs | ||||||
|  | - Improved visual consistency in log messages | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.9 - fix(cli) | ||||||
|  |  | ||||||
|  | Improve console output formatting for status banners and logging messages | ||||||
|  |  | ||||||
|  | - Standardize banner messages in daemon status updates | ||||||
|  | - Refine version information banner in nupst logging | ||||||
|  | - Update UPS connection and status banners in systemd | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.8 - fix(cli) | ||||||
|  |  | ||||||
|  | Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP | ||||||
|  | manager | ||||||
|  |  | ||||||
|  | - Standardize whitespace and formatting in ts/cli.ts for consistency | ||||||
|  | - Refine argument filtering for debug mode and prompt messages | ||||||
|  | - Remove unused 'dgram' import from ts/snmp/manager.ts | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.7 - fix(setup.sh) | ||||||
|  |  | ||||||
|  | Clarify net-snmp dependency installation message in setup.sh | ||||||
|  |  | ||||||
|  | - Updated echo statement to indicate installation of net-snmp along with 2 subdependencies | ||||||
|  | - Improves clarity on dependency installation during setup | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.6 - fix(setup.sh) | ||||||
|  |  | ||||||
|  | Improve setup script to detect and execute npm-cli.js directly using the Node.js binary | ||||||
|  |  | ||||||
|  | - Replace use of the npm binary with direct execution of npm-cli.js | ||||||
|  | - Add fallback logic to locate npm-cli.js when not found at the expected path | ||||||
|  | - Simplify cleanup by removing unnecessary PATH modifications | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.5 - fix(daemon, setup) | ||||||
|  |  | ||||||
|  | Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm | ||||||
|  | paths | ||||||
|  |  | ||||||
|  | - Use execFileAsync to execute shutdown commands reliably | ||||||
|  | - Add multiple fallback alternatives for shutdown and emergency shutdown handling | ||||||
|  | - Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.4 - fix(setup) | ||||||
|  |  | ||||||
|  | Improve installation process in setup script by cleaning up package files and ensuring a minimal | ||||||
|  | net-snmp dependency installation. | ||||||
|  |  | ||||||
|  | - Remove existing package-lock.json along with node_modules to prevent stale artifacts. | ||||||
|  | - Back up the original package.json before modifying it. | ||||||
|  | - Create a minimal package.json with only the net-snmp dependency based on the backed-up version. | ||||||
|  | - Use a clean install to guarantee that only net-snmp is installed. | ||||||
|  | - Restore the original package.json if the installation fails. | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.3 - fix(setup) | ||||||
|  |  | ||||||
|  | Update setup script to install only net-snmp dependency and create a minimal package-lock.json for | ||||||
|  | better dependency control. | ||||||
|  |  | ||||||
|  | - Removed full production dependency install in favor of installing only net-snmp@3.20.0 | ||||||
|  | - Added verification step to confirm net-snmp installation | ||||||
|  | - Generate a minimal package-lock.json if one does not exist | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.2 - fix(setup/readme) | ||||||
|  |  | ||||||
|  | Improve force update instructions and dependency installation process in setup.sh and readme.md | ||||||
|  |  | ||||||
|  | - Clarify force update commands with explicit paths in readme.md | ||||||
|  | - Remove existing node_modules before installing dependencies in setup.sh | ||||||
|  | - Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.1 - fix(setup) | ||||||
|  |  | ||||||
|  | Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log | ||||||
|  | Node and npm versions, and restore the original PATH afterwards. | ||||||
|  |  | ||||||
|  | - Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution. | ||||||
|  | - Log Node.js and npm versions for debugging purposes. | ||||||
|  | - Restore the original PATH after installing dependencies. | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.0 - feat(setup) | ||||||
|  |  | ||||||
|  | Add --force update flag to setup script and update installation instructions | ||||||
|  |  | ||||||
|  | - Implemented --force option in setup.sh to force-update Node.js binary and dependencies | ||||||
|  | - Updated readme.md to document the --force flag and revised update steps | ||||||
|  | - Modified ts/cli.ts update command to pass the --force flag to setup.sh | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.5.2 - fix(installer) | ||||||
|  |  | ||||||
|  | Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic | ||||||
|  |  | ||||||
|  | - Enhanced bin/nupst to detect OS and architecture (Linux and Darwin) and fall back to system | ||||||
|  |   Node.js for unsupported platforms | ||||||
|  | - Moved net-snmp from devDependencies to dependencies in package.json | ||||||
|  | - Updated setup.sh to install production dependencies and handle installation errors gracefully | ||||||
|  | - Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts | ||||||
|  | - Revised README to clarify minimal runtime dependencies and secure SNMP features | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.5.1 - fix(snmp) | ||||||
|  |  | ||||||
|  | Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion. | ||||||
|  |  | ||||||
|  | - Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery | ||||||
|  |   status. | ||||||
|  | - Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager. | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.5.0 - feat(cli) | ||||||
|  |  | ||||||
|  | Automatically restart running NUPST service after configuration changes in interactive setup | ||||||
|  |  | ||||||
|  | - Added restartServiceIfRunning() to check and restart the service if it's active. | ||||||
|  | - Invoked the restart function post-setup to apply configuration changes immediately. | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.8 - fix(installer) | ||||||
|  |  | ||||||
|  | Improve Git dependency handling and repository cloning in install.sh | ||||||
|  |  | ||||||
|  | - Add explicit check for git installation and prompt the user interactively if git is missing. | ||||||
|  | - Auto-install git when '-y' flag is provided in non-interactive mode. | ||||||
|  | - Ensure proper cloning of the repository when running the installer outside the repo. | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.7 - fix(readme) | ||||||
|  |  | ||||||
|  | Update installation instructions to combine download and execution into a single command for clarity | ||||||
|  |  | ||||||
|  | - Method 1 now uses a unified one-line command to download and run the install script | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.6 - fix(installer) | ||||||
|  |  | ||||||
|  | Improve installation instructions for interactive and non-interactive setups | ||||||
|  |  | ||||||
|  | - Changed install.sh to require explicit download of the install script and updated error messages | ||||||
|  |   for non-interactive modes | ||||||
|  | - Updated readme.md to include three distinct installation methods with clear command examples | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.5 - fix(install) | ||||||
|  |  | ||||||
|  | Improve interactive terminal detection and update installation instructions | ||||||
|  |  | ||||||
|  | - Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for | ||||||
|  |   both interactive and non-interactive installations | ||||||
|  | - Updated README.md quick install instructions to recommend process substitution and clarify | ||||||
|  |   auto-yes usage | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.4 - fix(install) | ||||||
|  |  | ||||||
|  | Improve interactive mode detection and non-interactive installation handling in install.sh | ||||||
|  |  | ||||||
|  | - Detect and warn when running without a controlling terminal | ||||||
|  | - Attempt to use /dev/tty for user input when possible | ||||||
|  | - Update prompts and error messages for auto-installation of dependencies | ||||||
|  | - Clarify installation instructions in readme for interactive and non-interactive modes | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.3 - fix(readme) | ||||||
|  |  | ||||||
|  | Update Quick Install command syntax in readme for auto-yes installation | ||||||
|  |  | ||||||
|  | - Changed installation command to use: curl -sSL | ||||||
|  |   https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -c "bash -s -- | ||||||
|  |   -y" | ||||||
|  |  | ||||||
|  | ## 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 | ||||||
|  |  | ||||||
|  | - Moved initiateShutdown and monitorDuringShutdown to the daemon class for improved cohesion | ||||||
|  | - Updated references in the daemon to call its own shutdown method instead of the SNMP manager | ||||||
|  | - Removed redundant initiateShutdown method from the SNMP manager | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.1 - fix(docs) | ||||||
|  |  | ||||||
|  | Update readme with detailed legal and trademark guidance | ||||||
|  |  | ||||||
|  | - Clarified legal section by adding trademark and company information | ||||||
|  | - Ensured users understand that licensing terms do not imply endorsement by the company | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.0 - feat(installer) | ||||||
|  |  | ||||||
|  | Add auto-yes flag to installer and update installation documentation | ||||||
|  |  | ||||||
|  | - Enhance install.sh to parse -y/--yes and -h/--help options, automating git installation when | ||||||
|  |   auto-yes is provided | ||||||
|  | - 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 | ||||||
|  |  | ||||||
|  | ## 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 | ||||||
|  | - Prompt user for git installation if not present before cloning the repository | ||||||
|  | - Enhanced CLI service setup flow to offer starting the NUPST service immediately after installation | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.2.0 - feat(cli) | ||||||
|  |  | ||||||
|  | Add 'config' command to display current configuration and update CLI help | ||||||
|  |  | ||||||
|  | - Introduce new 'config' command to show SNMP settings, thresholds, and configuration file location | ||||||
|  | - Update help text to include details for 'nupst config' command | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.1.0 - feat(cli) | ||||||
|  |  | ||||||
|  | Add uninstall command to CLI and update shutdown delay for graceful VM shutdown | ||||||
|  |  | ||||||
|  | - Implement uninstall command in ts/cli.ts that locates and executes uninstall.sh with user prompts | ||||||
|  | - Update uninstall.sh to support environment variables for configuration and repository removal | ||||||
|  | - Increase shutdown delay in ts/snmp/manager.ts from 1 minute to 5 minutes to allow VMs more time to | ||||||
|  |   shut down | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.0.1 - fix(cli/systemd) | ||||||
|  |  | ||||||
|  | Fix status command to pass debug flag and improve systemd status logging output | ||||||
|  |  | ||||||
|  | - ts/cli.ts: Now extracts debug options from process arguments and passes debug mode to getStatus. | ||||||
|  | - ts/systemd.ts: Updated getStatus to accept a debugMode parameter, enabling detailed SNMP debug | ||||||
|  |   logging, explicitly reloading configuration, and printing connection details. | ||||||
|  |  | ||||||
| ## 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 | ||||||
| @@ -37,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 | ||||||
| @@ -51,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 | ||||||
| @@ -74,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 | ||||||
| @@ -88,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 | ||||||
| @@ -104,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 | ||||||
| @@ -112,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 | ||||||
| @@ -124,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. | ||||||
| @@ -137,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 | ||||||
| @@ -144,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 | ||||||
| @@ -158,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 | ||||||
| @@ -176,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.1", | ||||||
|  |   "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" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										457
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										457
									
								
								install.sh
									
									
									
									
									
								
							| @@ -1,8 +1,88 @@ | |||||||
| #!/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: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash | # | ||||||
|  | # Usage: | ||||||
|  | #   Direct piped installation (recommended): | ||||||
|  | #     curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash | ||||||
|  | # | ||||||
|  | #   With version specification: | ||||||
|  | #     curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v4.0.0 | ||||||
|  | # | ||||||
|  | #   Non-interactive mode (auto-confirm): | ||||||
|  | #     curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y | ||||||
|  | # | ||||||
|  | #   Downloaded script: | ||||||
|  | #     curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh | ||||||
|  | #     sudo bash nupst-install.sh | ||||||
|  | # | ||||||
|  | # Options: | ||||||
|  | #   -y, --yes              Automatically answer yes to all prompts | ||||||
|  | #   -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 | ||||||
|  | AUTO_YES=0 | ||||||
|  | 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 | ||||||
|  | while [[ $# -gt 0 ]]; do | ||||||
|  |   case $1 in | ||||||
|  |     -y|--yes) | ||||||
|  |       AUTO_YES=1 | ||||||
|  |       shift | ||||||
|  |       ;; | ||||||
|  |     -h|--help) | ||||||
|  |       SHOW_HELP=1 | ||||||
|  |       shift | ||||||
|  |       ;; | ||||||
|  |     --version) | ||||||
|  |       SPECIFIED_VERSION="$2" | ||||||
|  |       shift 2 | ||||||
|  |       ;; | ||||||
|  |     --install-dir) | ||||||
|  |       INSTALL_DIR="$2" | ||||||
|  |       shift 2 | ||||||
|  |       ;; | ||||||
|  |     *) | ||||||
|  |       echo "Unknown option: $1" | ||||||
|  |       echo "Use -h or --help for usage information" | ||||||
|  |       exit 1 | ||||||
|  |       ;; | ||||||
|  |   esac | ||||||
|  | done | ||||||
|  |  | ||||||
|  | if [ $SHOW_HELP -eq 1 ]; then | ||||||
|  |   echo "NUPST Installer Script (v4.0+)" | ||||||
|  |   echo "Downloads and installs pre-compiled NUPST binary" | ||||||
|  |   echo "" | ||||||
|  |   echo "Usage: $0 [options]" | ||||||
|  |   echo "" | ||||||
|  |   echo "Options:" | ||||||
|  |   echo "  -y, --yes              Automatically answer yes to all prompts" | ||||||
|  |   echo "  -h, --help             Show this help message" | ||||||
|  |   echo "  --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" | ||||||
|  |   echo "" | ||||||
|  |   echo "  # Non-interactive installation" | ||||||
|  |   echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y" | ||||||
|  |   exit 0 | ||||||
|  | fi | ||||||
|  |  | ||||||
| # Check if running as root | # Check if running as root | ||||||
| if [ "$EUID" -ne 0 ]; then | if [ "$EUID" -ne 0 ]; then | ||||||
| @@ -11,87 +91,314 @@ if [ "$EUID" -ne 0 ]; then | |||||||
| fi | fi | ||||||
|  |  | ||||||
| # Detect if script is being piped or run directly | # Detect if script is being piped or run directly | ||||||
| PIPED=0 | INTERACTIVE=1 | ||||||
| if [ ! -t 0 ]; then | if [ ! -t 0 ] || [ ! -t 1 ]; then | ||||||
|   # Being piped, need to clone the repo |   # Either stdin or stdout is not a terminal | ||||||
|   PIPED=1 |   if [ $AUTO_YES -ne 1 ]; then | ||||||
| fi |     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 | ||||||
|  |     exec < /dev/tty 2>/dev/null || INTERACTIVE=0 | ||||||
|  |  | ||||||
| # Define installation directory |     if [ $INTERACTIVE -eq 0 ]; then | ||||||
| INSTALL_DIR="/opt/nupst" |       echo "ERROR: No controlling terminal available for interactive prompts." | ||||||
| REPO_URL="https://code.foss.global/serve.zone/nupst.git" |       echo "" | ||||||
|  |       echo "For interactive installation (RECOMMENDED):" | ||||||
| if [ $PIPED -eq 1 ]; then |       echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh" | ||||||
|   echo "Installing NUPST from remote repository..." |       echo "  sudo bash nupst-install.sh" | ||||||
|    |       echo "" | ||||||
|   # Check if git is installed |       echo "For non-interactive installation with auto-confirm:" | ||||||
|   if ! command -v git &> /dev/null; then |       echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y" | ||||||
|     echo "Git is required but not installed. Please install git first." |       exit 1 | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   # Check if installation directory exists |  | ||||||
|   if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then |  | ||||||
|     echo "Existing installation found at $INSTALL_DIR. Updating..." |  | ||||||
|     cd "$INSTALL_DIR" |  | ||||||
|      |  | ||||||
|     # Try to update the repository |  | ||||||
|     git fetch origin |  | ||||||
|     git reset --hard origin/main |  | ||||||
|      |  | ||||||
|     if [ $? -ne 0 ]; then |  | ||||||
|       echo "Failed to update repository. Reinstalling..." |  | ||||||
|       cd / |  | ||||||
|       rm -rf "$INSTALL_DIR" |  | ||||||
|       mkdir -p "$INSTALL_DIR" |  | ||||||
|       git clone --depth 1 $REPO_URL "$INSTALL_DIR" |  | ||||||
|     else |     else | ||||||
|       echo "Repository updated successfully." |       echo "Interactive terminal found, continuing with prompts..." | ||||||
|     fi |     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 |   fi | ||||||
|  | fi | ||||||
|  |  | ||||||
|   if [ $? -ne 0 ]; then | # Helper function to detect OS and architecture | ||||||
|     echo "Failed to clone/update repository. Please check your internet connection." | detect_platform() { | ||||||
|  |   local os=$(uname -s) | ||||||
|  |   local arch=$(uname -m) | ||||||
|  |  | ||||||
|  |   # Map OS | ||||||
|  |   case "$os" in | ||||||
|  |     Linux) | ||||||
|  |       os_name="linux" | ||||||
|  |       ;; | ||||||
|  |     Darwin) | ||||||
|  |       os_name="macos" | ||||||
|  |       ;; | ||||||
|  |     MINGW*|MSYS*|CYGWIN*) | ||||||
|  |       os_name="windows" | ||||||
|  |       ;; | ||||||
|  |     *) | ||||||
|  |       echo "Error: Unsupported operating system: $os" | ||||||
|  |       echo "Supported: Linux, macOS, Windows" | ||||||
|  |       exit 1 | ||||||
|  |       ;; | ||||||
|  |   esac | ||||||
|  |  | ||||||
|  |   # Map architecture | ||||||
|  |   case "$arch" in | ||||||
|  |     x86_64|amd64) | ||||||
|  |       arch_name="x64" | ||||||
|  |       ;; | ||||||
|  |     aarch64|arm64) | ||||||
|  |       arch_name="arm64" | ||||||
|  |       ;; | ||||||
|  |     *) | ||||||
|  |       echo "Error: Unsupported architecture: $arch" | ||||||
|  |       echo "Supported: x86_64/amd64 (x64), aarch64/arm64 (arm64)" | ||||||
|  |       exit 1 | ||||||
|  |       ;; | ||||||
|  |   esac | ||||||
|  |  | ||||||
|  |   # Construct binary name | ||||||
|  |   if [ "$os_name" = "windows" ]; then | ||||||
|  |     echo "nupst-${os_name}-${arch_name}.exe" | ||||||
|  |   else | ||||||
|  |     echo "nupst-${os_name}-${arch_name}" | ||||||
|  |   fi | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Get latest release version from Gitea API | ||||||
|  | get_latest_version() { | ||||||
|  |   echo "Fetching latest release version from Gitea..." >&2 | ||||||
|  |  | ||||||
|  |   local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest" | ||||||
|  |   local response=$(curl -sSL "$api_url" 2>/dev/null) | ||||||
|  |  | ||||||
|  |   if [ $? -ne 0 ] || [ -z "$response" ]; then | ||||||
|  |     echo "Error: Failed to fetch latest release information from Gitea API" >&2 | ||||||
|  |     echo "URL: $api_url" >&2 | ||||||
|     exit 1 |     exit 1 | ||||||
|   fi |   fi | ||||||
|  |  | ||||||
|   # Set script directory to the cloned repo |   # Extract tag_name from JSON response | ||||||
|   SCRIPT_DIR="$INSTALL_DIR" |   local version=$(echo "$response" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4) | ||||||
| else |  | ||||||
|   # Running directly from within the repo |  | ||||||
|   SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Run setup script |   if [ -z "$version" ]; then | ||||||
| echo "Running setup script..." |     echo "Error: Could not determine latest version from API response" >&2 | ||||||
| bash "$SCRIPT_DIR/setup.sh" |     exit 1 | ||||||
|  |   fi | ||||||
|  |  | ||||||
| # Install globally |   echo "$version" | ||||||
| echo "Installing NUPST globally..." | } | ||||||
| ln -sf "$SCRIPT_DIR/bin/nupst" /usr/local/bin/nupst |  | ||||||
|  |  | ||||||
| # Installation completed | # Main installation process | ||||||
| if [ $PIPED -eq 1 ]; then | echo "================================================" | ||||||
|   echo "NUPST has been installed globally at $INSTALL_DIR" | echo "  NUPST Installation Script (v4.0+)" | ||||||
| else | echo "================================================" | ||||||
|   echo "NUPST has been installed globally." | echo "" | ||||||
| fi |  | ||||||
|  | # Detect platform | ||||||
| echo "You can now run 'nupst' from anywhere." | 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 | ||||||
|  |   VERSION=$(get_latest_version) | ||||||
|  |   echo "Installing latest version: $VERSION" | ||||||
|  | fi | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Construct download URL | ||||||
|  | DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BINARY_NAME}" | ||||||
|  | echo "Download URL: $DOWNLOAD_URL" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Check if installation directory exists | ||||||
|  | SERVICE_WAS_RUNNING=0 | ||||||
|  | OLD_NODE_INSTALL=0 | ||||||
|  |  | ||||||
|  | if [ -d "$INSTALL_DIR" ]; then | ||||||
|  |   # Check if this is an old Node.js-based installation | ||||||
|  |   if [ -f "$INSTALL_DIR/package.json" ] || [ -d "$INSTALL_DIR/node_modules" ]; then | ||||||
|  |     OLD_NODE_INSTALL=1 | ||||||
|  |     echo "Detected old Node.js-based NUPST installation (v3.x or earlier)" | ||||||
|  |     echo "This installer will migrate to the new Deno-based binary version (v4.0+)" | ||||||
|  |     echo "" | ||||||
|  |   fi | ||||||
|  |  | ||||||
|  |   if [ $AUTO_YES -eq 0 ] && [ $INTERACTIVE -eq 1 ]; then | ||||||
|  |     if [ $OLD_NODE_INSTALL -eq 1 ]; then | ||||||
|  |       echo "This will replace your Node.js installation with a pre-compiled binary." | ||||||
|  |       echo "Your configuration in /etc/nupst/config.json will be preserved." | ||||||
|  |       echo "" | ||||||
|  |     fi | ||||||
|  |     echo "Installation directory already exists: $INSTALL_DIR" | ||||||
|  |     echo "Do you want to update/reinstall? (Y/n): " | ||||||
|  |     read -r update_confirm | ||||||
|  |  | ||||||
|  |     if [[ "$update_confirm" =~ ^[Nn]$ ]]; then | ||||||
|  |       echo "Installation cancelled." | ||||||
|  |       exit 0 | ||||||
|  |     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 | ||||||
|  |   if [ $AUTO_YES -eq 0 ] && [ $INTERACTIVE -eq 1 ]; then | ||||||
|  |     echo "NUPST will be installed to: $INSTALL_DIR" | ||||||
|  |     echo "Continue? (Y/n): " | ||||||
|  |     read -r install_confirm | ||||||
|  |  | ||||||
|  |     if [[ "$install_confirm" =~ ^[Nn]$ ]]; then | ||||||
|  |       echo "Installation cancelled." | ||||||
|  |       exit 0 | ||||||
|  |     fi | ||||||
|  |   fi | ||||||
|  |  | ||||||
|  |   echo "Creating installation directory: $INSTALL_DIR" | ||||||
|  |   mkdir -p "$INSTALL_DIR" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Download binary | ||||||
|  | echo "Downloading NUPST binary..." | ||||||
|  | TEMP_FILE="$INSTALL_DIR/nupst.download" | ||||||
|  | curl -sSL "$DOWNLOAD_URL" -o "$TEMP_FILE" | ||||||
|  |  | ||||||
|  | if [ $? -ne 0 ]; then | ||||||
|  |   echo "Error: Failed to download binary from $DOWNLOAD_URL" | ||||||
|  |   echo "" | ||||||
|  |   echo "Please check:" | ||||||
|  |   echo "  1. Your internet connection" | ||||||
|  |   echo "  2. The specified version exists: ${GITEA_BASE_URL}/${GITEA_REPO}/releases" | ||||||
|  |   echo "  3. The platform binary is available for this release" | ||||||
|  |   rm -f "$TEMP_FILE" | ||||||
|  |   exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Check if download was successful (file exists and not empty) | ||||||
|  | if [ ! -s "$TEMP_FILE" ]; then | ||||||
|  |   echo "Error: Downloaded file is empty or does not exist" | ||||||
|  |   rm -f "$TEMP_FILE" | ||||||
|  |   exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # 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 | ||||||
|  | if [ $AUTO_YES -eq 0 ] && [ $INTERACTIVE -eq 1 ]; then | ||||||
|  |   echo "Create symlink in $BIN_DIR for global access? (Y/n): " | ||||||
|  |   read -r symlink_confirm | ||||||
|  |  | ||||||
|  |   if [[ ! "$symlink_confirm" =~ ^[Nn]$ ]]; then | ||||||
|  |     ln -sf "$BINARY_PATH" "$BIN_DIR/nupst" | ||||||
|  |     echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH" | ||||||
|  |   else | ||||||
|  |     echo "Symlink creation skipped." | ||||||
|  |     echo "To use NUPST, run: $BINARY_PATH" | ||||||
|  |     echo "Or manually create symlink: sudo ln -sf $BINARY_PATH $BIN_DIR/nupst" | ||||||
|  |   fi | ||||||
|  | else | ||||||
|  |   ln -sf "$BINARY_PATH" "$BIN_DIR/nupst" | ||||||
|  |   echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | 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" |  | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								license
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								license
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | The MIT License (MIT) | ||||||
|  |  | ||||||
|  | Copyright (c) 2016 Task Venture Capital GmbH | ||||||
|  |  | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | in the Software without restriction, including without limitation the rights | ||||||
|  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | copies of the Software, and to permit persons to whom the Software is | ||||||
|  | furnished to do so, subject to the following conditions: | ||||||
|  |  | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  |  | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | SOFTWARE. | ||||||
							
								
								
									
										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 @@ | |||||||
| {} |  | ||||||
							
								
								
									
										58
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,58 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "@serve.zone/nupst", |  | ||||||
|   "version": "2.0.0", |  | ||||||
|   "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": {}, |  | ||||||
|   "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" |  | ||||||
|     ] |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										10187
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10187
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										727
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										727
									
								
								readme.md
									
									
									
									
									
								
							| @@ -1,213 +1,666 @@ | |||||||
| # 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 | ||||||
|  |  | ||||||
| - Monitors UPS devices using SNMP (v1, v2c, and v3 supported) | - **Multi-UPS Support**: Monitor and manage multiple UPS devices from a single installation | ||||||
| - Automatic shutdown when battery level falls below threshold | - **Group Management**: Organize UPS devices into groups with different operating modes | ||||||
| - Automatic shutdown when runtime remaining falls below threshold |   - **Redundant Mode**: Only shutdown when ALL UPS devices in a group are in critical condition | ||||||
| - Supports multiple UPS brands (CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv) |   - **Non-Redundant Mode**: Shutdown when ANY UPS device in a group is in critical condition | ||||||
| - Simple systemd service integration | - **SNMP Protocol Support**: Full support for SNMP v1, v2c, and v3 with authentication and | ||||||
| - Regular status logging for monitoring |   encryption | ||||||
| - Real-time log viewing with journalctl | - **Multiple UPS Brands**: Works with CyberPower, APC, Eaton, TrippLite, Liebert/Vertiv, and custom | ||||||
| - Version checking and automatic updates |   OID configurations | ||||||
| - Self-contained - includes its own Node.js runtime | - **Systemd Integration**: Simple service installation and management | ||||||
|  | - **Real-time Monitoring**: Live status updates and log viewing | ||||||
|  | - **Zero Dependencies**: Single self-contained binary with no runtime requirements | ||||||
|  | - **Cross-Platform**: Binaries available for Linux (x64, ARM64), macOS (Intel, Apple Silicon), and | ||||||
|  |   Windows | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
| ### Quick Install (One-line command) | ### Quick Install (Recommended) | ||||||
|  |  | ||||||
|  | The easiest way to install NUPST is using the automated installer: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| # Install directly without cloning the repository (requires root privileges) | # Download and run installer (most reliable) | ||||||
| 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 -o nupst-install.sh | ||||||
|  | sudo bash nupst-install.sh | ||||||
|  | rm nupst-install.sh | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Direct from Git |  | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| # Clone the repository | # One-line installation (non-interactive with auto-confirm) | ||||||
| git clone https://code.foss.global/serve.zone/nupst.git | curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y | ||||||
| cd nupst |  | ||||||
|  |  | ||||||
| # Option 1: Quick install (requires root privileges) |  | ||||||
| sudo ./install.sh |  | ||||||
|  |  | ||||||
| # Option 2: Manual setup |  | ||||||
| ./setup.sh |  | ||||||
| sudo ln -s $(pwd)/bin/nupst /usr/local/bin/nupst |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### From NPM | The installer will: | ||||||
|  |  | ||||||
|  | 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 | ||||||
| npm install -g @serve.zone/nupst | # Download binary (replace with your platform) | ||||||
|  | curl -sSL https://code.foss.global/serve.zone/nupst/releases/download/v4.0.0/nupst-linux-x64 -o nupst | ||||||
|  |  | ||||||
|  | # Make executable | ||||||
|  | chmod +x nupst | ||||||
|  |  | ||||||
|  | # Move to system path | ||||||
|  | sudo mv nupst /usr/local/bin/nupst | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ### Installation Options | ||||||
|  |  | ||||||
|  | The installer script (`install.sh`) supports the following options: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | -y, --yes                Automatically answer yes to all prompts | ||||||
|  | -h, --help               Show help message | ||||||
|  | --version VERSION        Install specific version (e.g., --version v4.0.0) | ||||||
|  | --install-dir DIR        Custom installation directory (default: /opt/nupst) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Examples: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # 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 | ||||||
|  |  | ||||||
|  | When installed, NUPST makes the following changes to your system: | ||||||
|  |  | ||||||
|  | ### File System Changes | ||||||
|  |  | ||||||
|  | | Path                                | Description                            | | ||||||
|  | | ----------------------------------- | -------------------------------------- | | ||||||
|  | | `/opt/nupst/nupst`                  | Pre-compiled binary (default location) | | ||||||
|  | | `/etc/nupst/config.json`            | Configuration file                     | | ||||||
|  | | `/usr/local/bin/nupst`              | Symlink to the NUPST binary            | | ||||||
|  | | `/etc/systemd/system/nupst.service` | Systemd service file (when enabled)    | | ||||||
|  |  | ||||||
|  | ### Service Changes | ||||||
|  |  | ||||||
|  | - 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 | ||||||
|  |  | ||||||
|  | ### Network Access | ||||||
|  |  | ||||||
|  | - NUPST only communicates with your UPS device via SNMP (default port 161) | ||||||
|  | - No external network connections required after installation | ||||||
|  |  | ||||||
| ## Uninstallation | ## Uninstallation | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| # If installed from git repository: | # Disable and remove service first | ||||||
| cd /path/to/nupst | sudo nupst service disable | ||||||
|  |  | ||||||
|  | # Remove binary and config | ||||||
|  | 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 |  | ||||||
| - Remove the symlink from /usr/local/bin |  | ||||||
| - Optionally remove configuration files from /etc/nupst |  | ||||||
|  |  | ||||||
| ## 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 logs           - Show logs of the systemd service in real-time |  | ||||||
|   nupst stop           - Stop the systemd service |  | ||||||
|   nupst start          - Start the systemd service |  | ||||||
|   nupst status         - Show status of the systemd service and UPS status |  | ||||||
|   nupst setup          - Run the interactive setup to configure SNMP settings |  | ||||||
|   nupst test           - Test the current configuration by connecting to the UPS |  | ||||||
|   nupst update         - Update NUPST from repository and refresh systemd service (requires root) |  | ||||||
|   nupst help           - Show this help message |  | ||||||
|  |  | ||||||
| Options: |  | ||||||
|   --debug, -d         - Enable debug mode for detailed SNMP logging |  | ||||||
|                         (Example: nupst test --debug) |  | ||||||
| ``` | ``` | ||||||
|  | NUPST - Network UPS Shutdown Tool | ||||||
|  | Version: 4.0.0 | ||||||
|  |  | ||||||
|  | Usage: nupst <command> [subcommand] [options] | ||||||
|  |  | ||||||
|  | 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: | ||||||
|  |   nupst ups add            - Add a new UPS device | ||||||
|  |   nupst ups edit [id]      - Edit a UPS device (prompts if no ID) | ||||||
|  |   nupst ups remove <id>    - Remove a UPS device by ID | ||||||
|  |   nupst ups list           - List all configured UPS devices | ||||||
|  |   nupst ups test           - Test UPS connections | ||||||
|  |  | ||||||
|  | Group Management: | ||||||
|  |   nupst group add          - Add a new UPS group | ||||||
|  |   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 provides an interactive setup to configure your UPS: | 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 | ||||||
| nupst setup | # Add a new UPS device | ||||||
|  | sudo nupst ups add | ||||||
|  |  | ||||||
|  | # Create a group | ||||||
|  | sudo nupst group add | ||||||
|  |  | ||||||
|  | # Assign UPS devices to groups | ||||||
|  | sudo nupst group edit <group-id> | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| This will guide you through setting up: | ### Configuration File Structure | ||||||
| - UPS IP address and SNMP settings |  | ||||||
| - Shutdown thresholds for battery percentage and runtime |  | ||||||
| - Monitoring interval |  | ||||||
| - Test the connection to your UPS |  | ||||||
|  |  | ||||||
| Alternatively, you can manually edit the configuration file at `/etc/nupst/config.json`. A default configuration will be created on first run: | Here's an example configuration with multiple UPS devices in a redundant group: | ||||||
|  |  | ||||||
| ```json | ```json | ||||||
| { | { | ||||||
|   "snmp": { |   "checkInterval": 30000, | ||||||
|     "host": "192.168.1.100", |   "upsDevices": [ | ||||||
|     "port": 161, |     { | ||||||
|     "community": "public", |       "id": "ups-1", | ||||||
|     "version": 1, |       "name": "Server Room UPS", | ||||||
|     "timeout": 5000, |       "snmp": { | ||||||
|     "upsModel": "cyberpower" |         "host": "192.168.1.100", | ||||||
|   }, |         "port": 161, | ||||||
|   "thresholds": { |         "community": "public", | ||||||
|     "battery": 60, |         "version": 1, | ||||||
|     "runtime": 20 |         "timeout": 5000, | ||||||
|   }, |         "upsModel": "cyberpower" | ||||||
|   "checkInterval": 30000 |       }, | ||||||
|  |       "thresholds": { | ||||||
|  |         "battery": 60, | ||||||
|  |         "runtime": 20 | ||||||
|  |       }, | ||||||
|  |       "groups": ["datacenter"] | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "id": "ups-2", | ||||||
|  |       "name": "Network Rack UPS", | ||||||
|  |       "snmp": { | ||||||
|  |         "host": "192.168.1.101", | ||||||
|  |         "port": 161, | ||||||
|  |         "community": "public", | ||||||
|  |         "version": 1, | ||||||
|  |         "timeout": 5000, | ||||||
|  |         "upsModel": "apc" | ||||||
|  |       }, | ||||||
|  |       "thresholds": { | ||||||
|  |         "battery": 50, | ||||||
|  |         "runtime": 15 | ||||||
|  |       }, | ||||||
|  |       "groups": ["datacenter"] | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "groups": [ | ||||||
|  |     { | ||||||
|  |       "id": "datacenter", | ||||||
|  |       "name": "Data Center", | ||||||
|  |       "mode": "redundant", | ||||||
|  |       "description": "Main data center UPS group with redundant power" | ||||||
|  |     } | ||||||
|  |   ] | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| - `snmp`: SNMP connection settings | ### Configuration Fields | ||||||
|   - `host`: IP address of your UPS (default: 127.0.0.1) |  | ||||||
|   - `port`: SNMP port (default: 161) | #### Global Settings | ||||||
|   - `version`: SNMP version (1, 2, or 3) |  | ||||||
|   - `timeout`: Timeout in milliseconds (default: 5000) |  | ||||||
|   - `upsModel`: The UPS model ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom') |  | ||||||
|   - For SNMPv1/v2c: |  | ||||||
|     - `community`: SNMP community string (default: public) |  | ||||||
|   - For SNMPv3: |  | ||||||
|     - `securityLevel`: Security level ('noAuthNoPriv', 'authNoPriv', or 'authPriv') |  | ||||||
|     - `username`: SNMPv3 username |  | ||||||
|     - `authProtocol`: Authentication protocol ('MD5' or 'SHA') |  | ||||||
|     - `authKey`: Authentication password/key |  | ||||||
|     - `privProtocol`: Privacy/encryption protocol ('DES' or 'AES') |  | ||||||
|     - `privKey`: Privacy password/key |  | ||||||
|   - For custom UPS models: |  | ||||||
|     - `customOIDs`: Object containing custom OIDs for your UPS: |  | ||||||
|       - `POWER_STATUS`: OID for power status |  | ||||||
|       - `BATTERY_CAPACITY`: OID for battery capacity percentage |  | ||||||
|       - `BATTERY_RUNTIME`: OID for runtime remaining in minutes |  | ||||||
| - `thresholds`: When to trigger shutdown |  | ||||||
|   - `battery`: Battery percentage threshold (default: 60%) |  | ||||||
|   - `runtime`: Runtime minutes threshold (default: 20 minutes) |  | ||||||
| - `checkInterval`: How often to check UPS status in milliseconds (default: 30000) | - `checkInterval`: How often to check UPS status in milliseconds (default: 30000) | ||||||
|  |  | ||||||
|  | #### UPS Device Settings | ||||||
|  |  | ||||||
|  | - `id`: Unique identifier for the UPS | ||||||
|  | - `name`: Friendly name for the UPS | ||||||
|  | - `groups`: Array of group IDs this UPS belongs to | ||||||
|  |  | ||||||
|  | **SNMP Configuration:** | ||||||
|  |  | ||||||
|  | - `host`: IP address or hostname of your UPS | ||||||
|  | - `port`: SNMP port (default: 161) | ||||||
|  | - `version`: SNMP version (1, 2, or 3) | ||||||
|  | - `timeout`: Timeout in milliseconds (default: 5000) | ||||||
|  | - `upsModel`: UPS brand ('cyberpower', 'apc', 'eaton', 'tripplite', 'liebert', or 'custom') | ||||||
|  |  | ||||||
|  | **For SNMPv1/v2c:** | ||||||
|  |  | ||||||
|  | - `community`: SNMP community string (default: "public") | ||||||
|  |  | ||||||
|  | **For SNMPv3:** | ||||||
|  |  | ||||||
|  | - `securityLevel`: 'noAuthNoPriv', 'authNoPriv', or 'authPriv' | ||||||
|  | - `username`: SNMPv3 username | ||||||
|  | - `authProtocol`: 'MD5' or 'SHA' | ||||||
|  | - `authKey`: Authentication password | ||||||
|  | - `privProtocol`: 'DES' or 'AES' (for authPriv level) | ||||||
|  | - `privKey`: Privacy/encryption password | ||||||
|  |  | ||||||
|  | **For Custom UPS Models:** | ||||||
|  |  | ||||||
|  | - `customOIDs`: Custom OID mappings | ||||||
|  |   - `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 | ||||||
|  |  | ||||||
|  | - **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**: 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 -s -- -y | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| This will: | The installer will: | ||||||
| 1. Pull the latest changes from the git repository |  | ||||||
| 2. Run the installation scripts | 1. Download the latest binary | ||||||
| 3. Refresh the systemd service configuration | 2. Replace the existing installation | ||||||
|  | 3. Preserve your configuration at `/etc/nupst/config.json` | ||||||
| 4. Restart the service if it was running | 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 | ||||||
|  | nupst --version | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Security | ## Security | ||||||
|  |  | ||||||
| NUPST was designed with security in mind: | NUPST is designed with security as a priority: | ||||||
|  |  | ||||||
| ### Minimal Dependencies | ### Architecture Security | ||||||
|  |  | ||||||
| - **Zero Runtime NPM Dependencies**: NUPST is built without any external NPM packages to minimize the attack surface and avoid supply chain risks. | - **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 | ||||||
|   - Zero 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) | ||||||
| - **Secure SNMPv3 Support**: Supports encrypted authentication and privacy for secure communication with the UPS device. |   - `authNoPriv`: Authentication without encryption | ||||||
| - **Isolated Execution**: The application runs in its working directory (`/opt/nupst`) or specified installation location, minimizing the impact on the rest of the system. |   - `authPriv`: Full authentication and encryption (recommended) | ||||||
|  | - **Authentication**: MD5 or SHA protocols | ||||||
|  | - **Encryption**: DES or AES privacy protocols | ||||||
|  | - **Secure Defaults**: Automatic timeout adjustment based on security level | ||||||
|  |  | ||||||
| ### 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 | ||||||
|  | - **Source Available**: Full source code available for audit | ||||||
|  |  | ||||||
| ### 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 | ||||||
|  |  | ||||||
| ## License | ### Verifying Downloads | ||||||
|  |  | ||||||
| MIT | 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 -s -- -y | ||||||
|  |  | ||||||
|  | # 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 | ||||||
|  |  | ||||||
|  | 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. | ||||||
|  |  | ||||||
|  | ### 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. | ||||||
|  |  | ||||||
|  | ### Company Information | ||||||
|  |  | ||||||
|  | Task Venture Capital GmbH 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. | ||||||
|  |  | ||||||
|  | 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. | ||||||
|   | |||||||
							
								
								
									
										613
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										613
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,613 @@ | |||||||
|  | # NUPST Migration Plan: Node.js → Deno v4.0.0 | ||||||
|  |  | ||||||
|  | **Migration Goal**: Convert NUPST from Node.js to Deno with single-executable distribution | ||||||
|  | **Version**: 3.1.2 → 4.0.0 (breaking changes) **Platforms**: Linux x64/ARM64, macOS x64/ARM64, | ||||||
|  | Windows x64 | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 0: Planning & Preparation | ||||||
|  |  | ||||||
|  | - [x] Research Deno compilation targets and npm: specifier support | ||||||
|  | - [x] Analyze current codebase structure and dependencies | ||||||
|  | - [x] Define CLI command structure simplification | ||||||
|  | - [x] Create detailed migration task list | ||||||
|  | - [ ] Create feature branch: `migration/deno-v4` | ||||||
|  | - [ ] Backup current working state with git tag: `v3.1.2-pre-deno-migration` | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 1: Dependency Migration (4-6 hours) | ||||||
|  |  | ||||||
|  | ### 1.1 Analyze Current Dependencies | ||||||
|  |  | ||||||
|  | - [ ] List all production dependencies from `package.json` | ||||||
|  |   - Current: `net-snmp@3.20.0` | ||||||
|  | - [ ] List all dev dependencies to be removed | ||||||
|  |   - `@git.zone/tsbuild`, `@git.zone/tsrun`, `@git.zone/tstest`, `@push.rocks/qenv`, | ||||||
|  |     `@push.rocks/tapbundle`, `@types/node` | ||||||
|  | - [ ] Identify Node.js built-in module usage | ||||||
|  |   - `child_process` (execSync) | ||||||
|  |   - `https` (for version checking) | ||||||
|  |   - `fs` (readFileSync, writeFileSync, existsSync, mkdirSync) | ||||||
|  |   - `path` (join, dirname, resolve) | ||||||
|  |  | ||||||
|  | ### 1.2 Create Deno Configuration | ||||||
|  |  | ||||||
|  | - [ ] Create `deno.json` with project configuration | ||||||
|  |   ```json | ||||||
|  |   { | ||||||
|  |     "name": "@serve.zone/nupst", | ||||||
|  |     "version": "4.0.0", | ||||||
|  |     "exports": "./mod.ts", | ||||||
|  |     "tasks": { | ||||||
|  |       "dev": "deno run --allow-all mod.ts", | ||||||
|  |       "compile": "deno task compile:all", | ||||||
|  |       "compile:all": "bash scripts/compile-all.sh", | ||||||
|  |       "test": "deno test --allow-all tests/", | ||||||
|  |       "check": "deno check mod.ts" | ||||||
|  |     }, | ||||||
|  |     "lint": { | ||||||
|  |       "rules": { | ||||||
|  |         "tags": ["recommended"] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "fmt": { | ||||||
|  |       "useTabs": false, | ||||||
|  |       "lineWidth": 100, | ||||||
|  |       "indentWidth": 2, | ||||||
|  |       "semiColons": true | ||||||
|  |     }, | ||||||
|  |     "compilerOptions": { | ||||||
|  |       "lib": ["deno.window"], | ||||||
|  |       "strict": true | ||||||
|  |     }, | ||||||
|  |     "imports": { | ||||||
|  |       "@std/cli": "jsr:@std/cli@^1.0.0", | ||||||
|  |       "@std/fmt": "jsr:@std/fmt@^1.0.0", | ||||||
|  |       "@std/path": "jsr:@std/path@^1.0.0" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   ``` | ||||||
|  |  | ||||||
|  | ### 1.3 Update Import Statements | ||||||
|  |  | ||||||
|  | - [ ] `ts/snmp/manager.ts`: Change `import * as snmp from 'net-snmp'` to | ||||||
|  |       `import * as snmp from "npm:net-snmp@3.20.0"` | ||||||
|  | - [ ] `ts/cli.ts`: Change `import { execSync } from 'child_process'` to | ||||||
|  |       `import { execSync } from "node:child_process"` | ||||||
|  | - [ ] `ts/nupst.ts`: Change `import * as https from 'https'` to | ||||||
|  |       `import * as https from "node:https"` | ||||||
|  | - [ ] Search for all `fs` imports and update to `node:fs` | ||||||
|  | - [ ] Search for all `path` imports and update to `node:path` | ||||||
|  | - [ ] Update all relative imports to use `.ts` extension instead of `.js` | ||||||
|  |   - Example: `'./nupst.js'` → `'./nupst.ts'` | ||||||
|  |  | ||||||
|  | ### 1.4 Test npm: Specifier Compatibility | ||||||
|  |  | ||||||
|  | - [ ] Create test file: `tests/snmp_compatibility_test.ts` | ||||||
|  | - [ ] Test SNMP v1 connection with npm:net-snmp | ||||||
|  | - [ ] Test SNMP v2c connection with npm:net-snmp | ||||||
|  | - [ ] Test SNMP v3 connection with npm:net-snmp | ||||||
|  | - [ ] Verify native addon loading works in compiled binary | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 2: Code Structure Refactoring (3-4 hours) | ||||||
|  |  | ||||||
|  | ### 2.1 Create Main Entry Point | ||||||
|  |  | ||||||
|  | - [ ] Create `mod.ts` as main Deno entry point: | ||||||
|  |   ```typescript | ||||||
|  |   #!/usr/bin/env -S deno run --allow-all | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * NUPST - UPS Shutdown Tool for Deno | ||||||
|  |    * | ||||||
|  |    * Required Permissions: | ||||||
|  |    * --allow-net: SNMP communication with UPS devices | ||||||
|  |    * --allow-read: Configuration file access (/etc/nupst/config.json) | ||||||
|  |    * --allow-write: Configuration file updates | ||||||
|  |    * --allow-run: System commands (systemctl, shutdown) | ||||||
|  |    * --allow-sys: System information (hostname, OS info) | ||||||
|  |    * --allow-env: Environment variables | ||||||
|  |    */ | ||||||
|  |  | ||||||
|  |   import { NupstCli } from './ts/cli.ts'; | ||||||
|  |  | ||||||
|  |   const cli = new NupstCli(); | ||||||
|  |   await cli.parseAndExecute(Deno.args); | ||||||
|  |   ``` | ||||||
|  |  | ||||||
|  | ### 2.2 Update All Import Extensions | ||||||
|  |  | ||||||
|  | Files to update (change .js → .ts in imports): | ||||||
|  |  | ||||||
|  | - [ ] `ts/index.ts` | ||||||
|  | - [ ] `ts/cli.ts` (imports from ./nupst.js, ./logger.js) | ||||||
|  | - [ ] `ts/nupst.ts` (imports from ./snmp/manager.js, ./daemon.js, etc.) | ||||||
|  | - [ ] `ts/daemon.ts` (imports from ./snmp/manager.js, ./logger.js, ./helpers/) | ||||||
|  | - [ ] `ts/systemd.ts` (imports from ./daemon.js, ./logger.js) | ||||||
|  | - [ ] `ts/cli/service-handler.ts` | ||||||
|  | - [ ] `ts/cli/group-handler.ts` | ||||||
|  | - [ ] `ts/cli/ups-handler.ts` | ||||||
|  | - [ ] `ts/snmp/index.ts` | ||||||
|  | - [ ] `ts/snmp/manager.ts` (imports from ./types.js, ./oid-sets.js) | ||||||
|  | - [ ] `ts/snmp/oid-sets.ts` (imports from ./types.js) | ||||||
|  | - [ ] `ts/helpers/index.ts` | ||||||
|  | - [ ] `ts/logger.ts` | ||||||
|  |  | ||||||
|  | ### 2.3 Update process.argv References | ||||||
|  |  | ||||||
|  | - [ ] `ts/cli.ts`: Replace `process.argv` with `Deno.args` (adjust indexing: process.argv[2] → | ||||||
|  |       Deno.args[0]) | ||||||
|  | - [ ] Update parseAndExecute method to work with Deno.args (0-indexed vs 2-indexed) | ||||||
|  |  | ||||||
|  | ### 2.4 Update File System Operations | ||||||
|  |  | ||||||
|  | - [ ] Search for `fs.readFileSync()` → Consider using `Deno.readTextFile()` or keep node:fs | ||||||
|  | - [ ] Search for `fs.writeFileSync()` → Consider using `Deno.writeTextFile()` or keep node:fs | ||||||
|  | - [ ] Search for `fs.existsSync()` → Keep node:fs or use Deno.stat | ||||||
|  | - [ ] Search for `fs.mkdirSync()` → Keep node:fs or use Deno.mkdir | ||||||
|  | - [ ] Decision: Keep node:fs for consistency or migrate to Deno APIs? | ||||||
|  |  | ||||||
|  | ### 2.5 Update Path Operations | ||||||
|  |  | ||||||
|  | - [ ] Verify all `path.join()`, `path.resolve()`, `path.dirname()` work with node:path | ||||||
|  | - [ ] Consider using `@std/path` from JSR for better Deno integration | ||||||
|  |  | ||||||
|  | ### 2.6 Handle __dirname and __filename | ||||||
|  |  | ||||||
|  | - [ ] Find all `__dirname` usage | ||||||
|  | - [ ] Replace with `import.meta.dirname` (Deno) or `dirname(fromFileUrl(import.meta.url))` | ||||||
|  | - [ ] Find all `__filename` usage | ||||||
|  | - [ ] Replace with `import.meta.filename` or `fromFileUrl(import.meta.url)` | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 3: CLI Command Simplification (3-4 hours) | ||||||
|  |  | ||||||
|  | ### 3.1 Design New Command Structure | ||||||
|  |  | ||||||
|  | Current → New mapping: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | OLD                          NEW | ||||||
|  | ===                          === | ||||||
|  | nupst enable                 → nupst service enable | ||||||
|  | nupst disable                → nupst service disable | ||||||
|  | nupst daemon-start           → nupst service start-daemon | ||||||
|  | nupst logs                   → nupst service logs | ||||||
|  | nupst stop                   → nupst service stop | ||||||
|  | nupst start                  → nupst service start | ||||||
|  | nupst status                 → nupst service status | ||||||
|  |  | ||||||
|  | nupst add                    → nupst ups add | ||||||
|  | nupst edit [id]              → nupst ups edit [id] | ||||||
|  | nupst delete <id>            → nupst ups remove <id> | ||||||
|  | nupst list                   → nupst ups list | ||||||
|  | nupst setup                  → nupst ups edit (removed alias) | ||||||
|  | nupst test                   → nupst ups test | ||||||
|  |  | ||||||
|  | nupst group list             → nupst group list | ||||||
|  | nupst group add              → nupst group add | ||||||
|  | nupst group edit <id>        → nupst group edit <id> | ||||||
|  | nupst group delete <id>      → nupst group remove <id> | ||||||
|  |  | ||||||
|  | nupst config                 → nupst config show | ||||||
|  | nupst update                 → nupst update | ||||||
|  | nupst uninstall              → nupst uninstall | ||||||
|  | nupst help                   → nupst help / nupst --help | ||||||
|  | (new)                        → nupst --version | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 3.2 Update CLI Parser (ts/cli.ts) | ||||||
|  |  | ||||||
|  | - [ ] Refactor `parseAndExecute()` to handle new command structure | ||||||
|  | - [ ] Add `service` subcommand handler | ||||||
|  | - [ ] Add `ups` subcommand handler | ||||||
|  | - [ ] Keep `group` subcommand handler (already exists, just update delete→remove) | ||||||
|  | - [ ] Add `config` subcommand handler with `show` default | ||||||
|  | - [ ] Add `--version` flag handler | ||||||
|  | - [ ] Update `help` command to show new structure | ||||||
|  | - [ ] Add command aliases: `rm` → `remove`, `ls` → `list` | ||||||
|  | - [ ] Add `--json` flag for machine-readable output (future enhancement) | ||||||
|  |  | ||||||
|  | ### 3.3 Update Command Handlers | ||||||
|  |  | ||||||
|  | - [ ] `ts/cli/service-handler.ts`: Update method names if needed | ||||||
|  | - [ ] `ts/cli/ups-handler.ts`: Rename `delete()` → `remove()`, remove `setup` method | ||||||
|  | - [ ] `ts/cli/group-handler.ts`: Rename `delete()` → `remove()` | ||||||
|  |  | ||||||
|  | ### 3.4 Improve Help Messages | ||||||
|  |  | ||||||
|  | - [ ] Update `showHelp()` in ts/cli.ts with new command structure | ||||||
|  | - [ ] Update `showGroupHelp()` in ts/cli.ts | ||||||
|  | - [ ] Add `showServiceHelp()` method | ||||||
|  | - [ ] Add `showUpsHelp()` method | ||||||
|  | - [ ] Add `showConfigHelp()` method | ||||||
|  | - [ ] Include usage examples in help text | ||||||
|  |  | ||||||
|  | ### 3.5 Add Version Command | ||||||
|  |  | ||||||
|  | - [ ] Read version from deno.json | ||||||
|  | - [ ] Create `--version` handler in CLI | ||||||
|  | - [ ] Display version with build info | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 4: Compilation & Distribution (2-3 hours) | ||||||
|  |  | ||||||
|  | ### 4.1 Create Compilation Script | ||||||
|  |  | ||||||
|  | - [ ] Create directory: `scripts/` | ||||||
|  | - [ ] Create `scripts/compile-all.sh`: | ||||||
|  |   ```bash | ||||||
|  |   #!/bin/bash | ||||||
|  |   set -e | ||||||
|  |  | ||||||
|  |   VERSION=$(cat deno.json | jq -r '.version') | ||||||
|  |   BINARY_DIR="dist/binaries" | ||||||
|  |  | ||||||
|  |   echo "Compiling NUPST v${VERSION} for all platforms..." | ||||||
|  |   mkdir -p "$BINARY_DIR" | ||||||
|  |  | ||||||
|  |   # Linux x86_64 | ||||||
|  |   echo "→ Linux x86_64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-linux-x64" \ | ||||||
|  |     --target x86_64-unknown-linux-gnu mod.ts | ||||||
|  |  | ||||||
|  |   # Linux ARM64 | ||||||
|  |   echo "→ Linux ARM64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-linux-arm64" \ | ||||||
|  |     --target aarch64-unknown-linux-gnu mod.ts | ||||||
|  |  | ||||||
|  |   # macOS x86_64 | ||||||
|  |   echo "→ macOS x86_64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-macos-x64" \ | ||||||
|  |     --target x86_64-apple-darwin mod.ts | ||||||
|  |  | ||||||
|  |   # macOS ARM64 | ||||||
|  |   echo "→ macOS ARM64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-macos-arm64" \ | ||||||
|  |     --target aarch64-apple-darwin mod.ts | ||||||
|  |  | ||||||
|  |   # Windows x86_64 | ||||||
|  |   echo "→ Windows x86_64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-windows-x64.exe" \ | ||||||
|  |     --target x86_64-pc-windows-msvc mod.ts | ||||||
|  |  | ||||||
|  |   echo "" | ||||||
|  |   echo "✓ Compilation complete!" | ||||||
|  |   ls -lh "$BINARY_DIR/" | ||||||
|  |   ``` | ||||||
|  | - [ ] Make script executable: `chmod +x scripts/compile-all.sh` | ||||||
|  |  | ||||||
|  | ### 4.2 Test Local Compilation | ||||||
|  |  | ||||||
|  | - [ ] Run `deno task compile` to compile for all platforms | ||||||
|  | - [ ] Verify all 5 binaries are created | ||||||
|  | - [ ] Check binary sizes (should be reasonable, < 100MB each) | ||||||
|  | - [ ] Test local binary on current platform: `./dist/binaries/nupst-linux-x64 --version` | ||||||
|  |  | ||||||
|  | ### 4.3 Update Installation Scripts | ||||||
|  |  | ||||||
|  | - [ ] Update `install.sh`: | ||||||
|  |   - Remove Node.js download logic (lines dealing with vendor/node-*) | ||||||
|  |   - Add detection for binary download from GitHub releases | ||||||
|  |   - Simplify to download appropriate binary based on OS/arch | ||||||
|  |   - Place binary in `/opt/nupst/bin/nupst` | ||||||
|  |   - Create symlink: `/usr/local/bin/nupst → /opt/nupst/bin/nupst` | ||||||
|  |   - Update to v4.0.0 in script | ||||||
|  | - [ ] Simplify or remove `setup.sh` (no longer needed without Node.js) | ||||||
|  | - [ ] Update `bin/nupst` launcher: | ||||||
|  |   - Option A: Keep as simple wrapper | ||||||
|  |   - Option B: Remove and symlink directly to binary | ||||||
|  | - [ ] Update `uninstall.sh`: | ||||||
|  |   - Remove vendor directory cleanup | ||||||
|  |   - Update paths to new binary location | ||||||
|  |  | ||||||
|  | ### 4.4 Update Systemd Service | ||||||
|  |  | ||||||
|  | - [ ] Update systemd service file path in `ts/systemd.ts` | ||||||
|  | - [ ] Verify ExecStart points to correct binary location: `/opt/nupst/bin/nupst daemon-start` | ||||||
|  | - [ ] Remove Node.js environment variables if any | ||||||
|  | - [ ] Test service installation and startup | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 5: Testing & Validation (4-6 hours) | ||||||
|  |  | ||||||
|  | ### 5.1 Create Deno Test Suite | ||||||
|  |  | ||||||
|  | - [ ] Create `tests/` directory (or migrate from existing `test/`) | ||||||
|  | - [ ] Create `tests/snmp_test.ts`: Test SNMP manager functionality | ||||||
|  | - [ ] Create `tests/config_test.ts`: Test configuration loading/saving | ||||||
|  | - [ ] Create `tests/cli_test.ts`: Test CLI parsing and command routing | ||||||
|  | - [ ] Create `tests/daemon_test.ts`: Test daemon logic | ||||||
|  | - [ ] Remove dependency on @git.zone/tstest and @push.rocks/tapbundle | ||||||
|  | - [ ] Use Deno's built-in test runner (`Deno.test()`) | ||||||
|  |  | ||||||
|  | ### 5.2 Unit Tests | ||||||
|  |  | ||||||
|  | - [ ] Test SNMP connection with mock responses | ||||||
|  | - [ ] Test configuration validation | ||||||
|  | - [ ] Test UPS status parsing for different models | ||||||
|  | - [ ] Test group logic (redundant/non-redundant modes) | ||||||
|  | - [ ] Test threshold checking | ||||||
|  | - [ ] Test version comparison logic | ||||||
|  |  | ||||||
|  | ### 5.3 Integration Tests | ||||||
|  |  | ||||||
|  | - [ ] Test CLI command parsing for all commands | ||||||
|  | - [ ] Test config file creation and updates | ||||||
|  | - [ ] Test UPS add/edit/remove operations | ||||||
|  | - [ ] Test group add/edit/remove operations | ||||||
|  | - [ ] Mock systemd operations for testing | ||||||
|  |  | ||||||
|  | ### 5.4 Binary Testing | ||||||
|  |  | ||||||
|  | - [ ] Test compiled binary on Linux x64 | ||||||
|  | - [ ] Test compiled binary on Linux ARM64 (if available) | ||||||
|  | - [ ] Test compiled binary on macOS x64 (if available) | ||||||
|  | - [ ] Test compiled binary on macOS ARM64 (if available) | ||||||
|  | - [ ] Test compiled binary on Windows x64 (if available) | ||||||
|  | - [ ] Verify SNMP functionality works in compiled binary | ||||||
|  | - [ ] Verify config file operations work in compiled binary | ||||||
|  | - [ ] Test systemd integration with compiled binary | ||||||
|  |  | ||||||
|  | ### 5.5 Performance Testing | ||||||
|  |  | ||||||
|  | - [ ] Measure binary size for each platform | ||||||
|  | - [ ] Measure startup time: `time ./nupst-linux-x64 --version` | ||||||
|  | - [ ] Measure memory footprint during daemon operation | ||||||
|  | - [ ] Compare with Node.js version performance | ||||||
|  | - [ ] Document performance metrics | ||||||
|  |  | ||||||
|  | ### 5.6 Upgrade Path Testing | ||||||
|  |  | ||||||
|  | - [ ] Create test with v3.x config | ||||||
|  | - [ ] Verify v4.x can read existing config | ||||||
|  | - [ ] Test migration from old commands to new commands | ||||||
|  | - [ ] Verify systemd service upgrade path | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 6: Distribution Strategy (2-3 hours) | ||||||
|  |  | ||||||
|  | ### 6.1 GitHub Actions Workflow | ||||||
|  |  | ||||||
|  | - [ ] Create `.github/workflows/release.yml`: | ||||||
|  |   ```yaml | ||||||
|  |   name: Release | ||||||
|  |   on: | ||||||
|  |     push: | ||||||
|  |       tags: | ||||||
|  |         - 'v*' | ||||||
|  |   jobs: | ||||||
|  |     build: | ||||||
|  |       runs-on: ubuntu-latest | ||||||
|  |       steps: | ||||||
|  |         - uses: actions/checkout@v4 | ||||||
|  |         - uses: denoland/setup-deno@v1 | ||||||
|  |           with: | ||||||
|  |             deno-version: v1.x | ||||||
|  |         - name: Compile binaries | ||||||
|  |           run: deno task compile | ||||||
|  |         - name: Generate checksums | ||||||
|  |           run: | | ||||||
|  |             cd dist/binaries | ||||||
|  |             sha256sum * > SHA256SUMS | ||||||
|  |         - name: Create Release | ||||||
|  |           uses: softprops/action-gh-release@v1 | ||||||
|  |           with: | ||||||
|  |             files: dist/binaries/* | ||||||
|  |             generate_release_notes: true | ||||||
|  |   ``` | ||||||
|  |  | ||||||
|  | ### 6.2 Update package.json for npm | ||||||
|  |  | ||||||
|  | - [ ] Update version to 4.0.0 | ||||||
|  | - [ ] Update description to mention Deno | ||||||
|  | - [ ] Add postinstall script to symlink appropriate binary: | ||||||
|  |   ```json | ||||||
|  |   { | ||||||
|  |     "name": "@serve.zone/nupst", | ||||||
|  |     "version": "4.0.0", | ||||||
|  |     "description": "UPS Shutdown Tool - Deno-based single executable", | ||||||
|  |     "bin": { | ||||||
|  |       "nupst": "bin/nupst-npm-wrapper.js" | ||||||
|  |     }, | ||||||
|  |     "type": "module", | ||||||
|  |     "scripts": { | ||||||
|  |       "postinstall": "node bin/setup-npm-binary.js" | ||||||
|  |     }, | ||||||
|  |     "files": [ | ||||||
|  |       "dist/binaries/*", | ||||||
|  |       "bin/*" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  |   ``` | ||||||
|  | - [ ] Create `bin/setup-npm-binary.js` to symlink correct binary | ||||||
|  | - [ ] Create `bin/nupst-npm-wrapper.js` as entry point | ||||||
|  |  | ||||||
|  | ### 6.3 Verify Distribution Methods | ||||||
|  |  | ||||||
|  | - [ ] Test GitHub release download and installation | ||||||
|  | - [ ] Test npm install from tarball | ||||||
|  | - [ ] Test direct install.sh script | ||||||
|  | - [ ] Verify all methods create working installation | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 7: Documentation Updates (2-3 hours) | ||||||
|  |  | ||||||
|  | ### 7.1 Update README.md | ||||||
|  |  | ||||||
|  | - [ ] Remove Node.js requirements section | ||||||
|  | - [ ] Update features list (mention Deno, single executable) | ||||||
|  | - [ ] Update installation methods: | ||||||
|  |   - Method 1: Quick install script (updated) | ||||||
|  |   - Method 2: GitHub releases (new) | ||||||
|  |   - Method 3: npm (updated with notes) | ||||||
|  | - [ ] Update usage section with new command structure | ||||||
|  | - [ ] Add command mapping table (v3 → v4) | ||||||
|  | - [ ] Update platform support matrix (note: no Windows ARM) | ||||||
|  | - [ ] Update "System Changes" section (no vendor directory) | ||||||
|  | - [ ] Update security section (remove Node.js mentions) | ||||||
|  | - [ ] Update uninstallation instructions | ||||||
|  |  | ||||||
|  | ### 7.2 Create MIGRATION.md | ||||||
|  |  | ||||||
|  | - [ ] Create detailed migration guide from v3.x to v4.x | ||||||
|  | - [ ] List all breaking changes: | ||||||
|  |   1. CLI command structure reorganization | ||||||
|  |   2. No Node.js requirement | ||||||
|  |   3. Windows ARM not supported | ||||||
|  |   4. Installation path changes | ||||||
|  | - [ ] Provide command mapping table | ||||||
|  | - [ ] Explain config compatibility | ||||||
|  | - [ ] Document upgrade procedure | ||||||
|  | - [ ] Add rollback instructions | ||||||
|  |  | ||||||
|  | ### 7.3 Update CHANGELOG.md | ||||||
|  |  | ||||||
|  | - [ ] Add v4.0.0 section with all breaking changes | ||||||
|  | - [ ] List new features (Deno, single executable) | ||||||
|  | - [ ] List improvements (startup time, binary size) | ||||||
|  | - [ ] List removed features (Windows ARM, setup command alias) | ||||||
|  | - [ ] Migration guide reference | ||||||
|  |  | ||||||
|  | ### 7.4 Update Help Text | ||||||
|  |  | ||||||
|  | - [ ] Ensure all help commands show new structure | ||||||
|  | - [ ] Add examples for common operations | ||||||
|  | - [ ] Include migration notes in help output | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 8: Cleanup & Finalization (1 hour) | ||||||
|  |  | ||||||
|  | ### 8.1 Remove Obsolete Files | ||||||
|  |  | ||||||
|  | - [ ] Delete `vendor/` directory (Node.js binaries) | ||||||
|  | - [ ] Delete `dist/` directory (old compiled JS) | ||||||
|  | - [ ] Delete `dist_ts/` directory (old compiled TS) | ||||||
|  | - [ ] Delete `node_modules/` directory | ||||||
|  | - [ ] Remove or update `tsconfig.json` (decide if needed for npm compatibility) | ||||||
|  | - [ ] Remove `setup.sh` if no longer needed | ||||||
|  | - [ ] Remove old test files in `test/` if migrated to `tests/` | ||||||
|  | - [ ] Delete `pnpm-lock.yaml` | ||||||
|  |  | ||||||
|  | ### 8.2 Update Git Configuration | ||||||
|  |  | ||||||
|  | - [ ] Update `.gitignore`: | ||||||
|  |   ``` | ||||||
|  |   # Deno | ||||||
|  |   .deno/ | ||||||
|  |   deno.lock | ||||||
|  |  | ||||||
|  |   # Compiled binaries | ||||||
|  |   dist/binaries/ | ||||||
|  |  | ||||||
|  |   # Old Node.js artifacts (to be removed) | ||||||
|  |   node_modules/ | ||||||
|  |   vendor/ | ||||||
|  |   dist/ | ||||||
|  |   dist_ts/ | ||||||
|  |   pnpm-lock.yaml | ||||||
|  |   ``` | ||||||
|  | - [ ] Add `deno.lock` to version control | ||||||
|  | - [ ] Create `.denoignore` if needed | ||||||
|  |  | ||||||
|  | ### 8.3 Final Validation | ||||||
|  |  | ||||||
|  | - [ ] Run `deno check mod.ts` - verify no type errors | ||||||
|  | - [ ] Run `deno lint` - verify code quality | ||||||
|  | - [ ] Run `deno fmt --check` - verify formatting | ||||||
|  | - [ ] Run `deno task test` - verify all tests pass | ||||||
|  | - [ ] Run `deno task compile` - verify all binaries compile | ||||||
|  | - [ ] Test each binary manually | ||||||
|  |  | ||||||
|  | ### 8.4 Prepare for Release | ||||||
|  |  | ||||||
|  | - [ ] Create git tag: `v4.0.0` | ||||||
|  | - [ ] Push to main branch | ||||||
|  | - [ ] Push tags to trigger release workflow | ||||||
|  | - [ ] Verify GitHub Actions workflow succeeds | ||||||
|  | - [ ] Verify binaries are attached to release | ||||||
|  | - [ ] Test installation from GitHub release | ||||||
|  | - [ ] Publish to npm: `npm publish` | ||||||
|  | - [ ] Test npm installation | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Rollback Strategy | ||||||
|  |  | ||||||
|  | If critical issues are discovered: | ||||||
|  |  | ||||||
|  | - [ ] Keep `v3.1.2` tag available for rollback | ||||||
|  | - [ ] Create `v3-stable` branch for continued v3 maintenance | ||||||
|  | - [ ] Update install.sh to offer v3/v4 choice | ||||||
|  | - [ ] Document known issues in GitHub Issues | ||||||
|  | - [ ] Provide downgrade instructions in docs | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Success Criteria Checklist | ||||||
|  |  | ||||||
|  | - [ ] ✅ All 5 platform binaries compile successfully | ||||||
|  | - [ ] ✅ Binary sizes are reasonable (< 100MB per platform) | ||||||
|  | - [ ] ✅ Startup time < 2 seconds | ||||||
|  | - [ ] ✅ SNMP v1/v2c/v3 functionality verified on real UPS device | ||||||
|  | - [ ] ✅ All CLI commands work with new structure | ||||||
|  | - [ ] ✅ Config file compatibility maintained | ||||||
|  | - [ ] ✅ Systemd integration works on Linux | ||||||
|  | - [ ] ✅ Installation scripts work on fresh systems | ||||||
|  | - [ ] ✅ npm package still installable and functional | ||||||
|  | - [ ] ✅ All tests pass | ||||||
|  | - [ ] ✅ Documentation is complete and accurate | ||||||
|  | - [ ] ✅ GitHub release created with binaries | ||||||
|  | - [ ] ✅ Migration guide tested by following it step-by-step | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Timeline | ||||||
|  |  | ||||||
|  | - **Phase 0**: 1 hour ✓ (in progress) | ||||||
|  | - **Phase 1**: 4-6 hours | ||||||
|  | - **Phase 2**: 3-4 hours | ||||||
|  | - **Phase 3**: 3-4 hours | ||||||
|  | - **Phase 4**: 2-3 hours | ||||||
|  | - **Phase 5**: 4-6 hours | ||||||
|  | - **Phase 6**: 2-3 hours | ||||||
|  | - **Phase 7**: 2-3 hours | ||||||
|  | - **Phase 8**: 1 hour | ||||||
|  |  | ||||||
|  | **Total Estimate**: 22-31 hours | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Notes & Decisions | ||||||
|  |  | ||||||
|  | ### Key Decisions Made: | ||||||
|  |  | ||||||
|  | 1. ✅ Use npm:net-snmp (no pure Deno SNMP library available) | ||||||
|  | 2. ✅ Major version bump to 4.0.0 (breaking changes) | ||||||
|  | 3. ✅ CLI reorganization with subcommands | ||||||
|  | 4. ✅ Keep npm publishing alongside binary distribution | ||||||
|  | 5. ✅ 5 platform targets (Windows ARM not supported by Deno yet) | ||||||
|  |  | ||||||
|  | ### Open Questions: | ||||||
|  |  | ||||||
|  | - [ ] Should we keep tsconfig.json for npm package compatibility? | ||||||
|  | - [ ] Should we fully migrate to Deno APIs (Deno.readFile) or keep node:fs? | ||||||
|  | - [ ] Should we remove the `bin/nupst` wrapper or keep it? | ||||||
|  | - [ ] Should setup.sh be completely removed or kept for dependencies? | ||||||
|  |  | ||||||
|  | ### Risk Areas: | ||||||
|  |  | ||||||
|  | - ⚠️ SNMP native addon compatibility in compiled binaries (HIGH PRIORITY TO TEST) | ||||||
|  | - ⚠️ Systemd integration with new binary structure | ||||||
|  | - ⚠️ Config migration from v3 to v4 | ||||||
|  | - ⚠️ npm package installation with embedded binaries | ||||||
							
								
								
									
										66
									
								
								scripts/compile-all.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										66
									
								
								scripts/compile-all.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # Get version from deno.json | ||||||
|  | VERSION=$(cat deno.json | grep -o '"version": *"[^"]*"' | cut -d'"' -f4) | ||||||
|  | BINARY_DIR="dist/binaries" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  NUPST Compilation Script" | ||||||
|  | echo "  Version: ${VERSION}" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | echo "Compiling for all supported platforms..." | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Clean up old binaries and create fresh directory | ||||||
|  | rm -rf "$BINARY_DIR" | ||||||
|  | mkdir -p "$BINARY_DIR" | ||||||
|  | echo "→ Cleaned old binaries from $BINARY_DIR" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Linux x86_64 | ||||||
|  | echo "→ Compiling for Linux x86_64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-x64" \ | ||||||
|  |   --target x86_64-unknown-linux-gnu mod.ts | ||||||
|  | echo "  ✓ Linux x86_64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Linux ARM64 | ||||||
|  | echo "→ Compiling for Linux ARM64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-arm64" \ | ||||||
|  |   --target aarch64-unknown-linux-gnu mod.ts | ||||||
|  | echo "  ✓ Linux ARM64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # macOS x86_64 | ||||||
|  | echo "→ Compiling for macOS x86_64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-x64" \ | ||||||
|  |   --target x86_64-apple-darwin mod.ts | ||||||
|  | echo "  ✓ macOS x86_64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # macOS ARM64 | ||||||
|  | echo "→ Compiling for macOS ARM64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-arm64" \ | ||||||
|  |   --target aarch64-apple-darwin mod.ts | ||||||
|  | echo "  ✓ macOS ARM64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Windows x86_64 | ||||||
|  | echo "→ Compiling for Windows x86_64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-windows-x64.exe" \ | ||||||
|  |   --target x86_64-pc-windows-msvc mod.ts | ||||||
|  | echo "  ✓ Windows x86_64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  Compilation Summary" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | ls -lh "$BINARY_DIR/" | tail -n +2 | ||||||
|  | echo "" | ||||||
|  | echo "✓ All binaries compiled successfully!" | ||||||
|  | echo "" | ||||||
|  | echo "Binary location: $BINARY_DIR/" | ||||||
|  | echo "" | ||||||
							
								
								
									
										227
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										227
									
								
								setup.sh
									
									
									
									
									
								
							| @@ -1,227 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| # NUPST Setup Script |  | ||||||
| # Downloads the appropriate Node.js binary for the current platform |  | ||||||
|  |  | ||||||
| # 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" ]; then |  | ||||||
|   echo "Node.js binary already exists for $OS-$ARCH. Skipping download." |  | ||||||
| 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" |  | ||||||
|  |  | ||||||
| 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" |  | ||||||
							
								
								
									
										124
									
								
								test/manualdocker/01-setup-v3-container.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										124
									
								
								test/manualdocker/01-setup-v3-container.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,124 @@ | |||||||
|  | #!/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 | ||||||
|  | " | ||||||
|  |  | ||||||
|  | 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 script..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   cd /opt/nupst | ||||||
|  |   bash install.sh -y | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Creating dummy NUPST configuration for testing..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   mkdir -p /etc/nupst | ||||||
|  |   cat > /etc/nupst/config.json << 'EOF' | ||||||
|  | { | ||||||
|  |   \"upsList\": [ | ||||||
|  |     { | ||||||
|  |       \"id\": \"test-ups\", | ||||||
|  |       \"name\": \"Test UPS\", | ||||||
|  |       \"host\": \"127.0.0.1\", | ||||||
|  |       \"port\": 161, | ||||||
|  |       \"community\": \"public\", | ||||||
|  |       \"version\": \"2c\", | ||||||
|  |       \"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'\" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   \"groups\": [] | ||||||
|  | } | ||||||
|  | EOF | ||||||
|  |   echo 'Dummy config created at /etc/nupst/config.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 "" | ||||||
							
								
								
									
										72
									
								
								test/manualdocker/02-test-v3-to-v4-migration.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										72
									
								
								test/manualdocker/02-test-v3-to-v4-migration.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | #!/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 "→ Pulling latest v4 code from migration/deno-v4 branch..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   cd /opt/nupst | ||||||
|  |   git fetch origin | ||||||
|  |   # Reset any local changes made by install.sh | ||||||
|  |   git reset --hard HEAD | ||||||
|  |   git clean -fd | ||||||
|  |   git checkout migration/deno-v4 | ||||||
|  |   git pull origin migration/deno-v4 | ||||||
|  |   echo 'Now on:' | ||||||
|  |   git log -1 --oneline | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Running install.sh (should auto-detect v3 and migrate)..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   cd /opt/nupst | ||||||
|  |   bash install.sh -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` | ||||||
							
								
								
									
										157
									
								
								test/test.logger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								test/test.logger.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | |||||||
|  | import { assert, assertEquals } from 'jsr:@std/assert@^1.0.0'; | ||||||
|  | import { Logger } from '../ts/logger.ts'; | ||||||
|  |  | ||||||
|  | // Create a Logger instance for testing | ||||||
|  | const logger = new Logger(); | ||||||
|  |  | ||||||
|  | Deno.test('should create a logger instance', () => { | ||||||
|  |   assert(logger instanceof Logger); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should log messages with different log levels', () => { | ||||||
|  |   // We're not testing console output directly, just ensuring no errors | ||||||
|  |   logger.log('Regular log message'); | ||||||
|  |   logger.error('Error message'); | ||||||
|  |   logger.warn('Warning message'); | ||||||
|  |   logger.success('Success message'); | ||||||
|  |  | ||||||
|  |   // Just assert that the test runs without errors | ||||||
|  |   assert(true); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should create a logbox with title, content, and end', () => { | ||||||
|  |   // Just ensuring no errors occur | ||||||
|  |   logger.logBoxTitle('Test Box', 40); | ||||||
|  |   logger.logBoxLine('This is a test line'); | ||||||
|  |   logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |   // Just assert that the test runs without errors | ||||||
|  |   assert(true); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should handle width persistence between logbox calls', () => { | ||||||
|  |   logger.logBoxTitle('Width Test', 45); | ||||||
|  |  | ||||||
|  |   // These should use the width from the title | ||||||
|  |   logger.logBoxLine('Line 1'); | ||||||
|  |   logger.logBoxLine('Line 2'); | ||||||
|  |   logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |   let errorThrown = false; | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     // This should work fine after the reset in logBoxEnd | ||||||
|  |     logger.logBoxTitle('New Box', 30); | ||||||
|  |     logger.logBoxLine('New line'); | ||||||
|  |     logger.logBoxEnd(); | ||||||
|  |   } catch (_error) { | ||||||
|  |     errorThrown = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   assertEquals(errorThrown, false); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should use default width when no width is specified', () => { | ||||||
|  |   // This should automatically use the default width instead of throwing | ||||||
|  |   let errorThrown = false; | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     logger.logBoxLine('This should use default width'); | ||||||
|  |     logger.logBoxEnd(); | ||||||
|  |   } catch (_error) { | ||||||
|  |     errorThrown = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Verify no error was thrown | ||||||
|  |   assertEquals(errorThrown, false); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should create a complete logbox in one call', () => { | ||||||
|  |   // Just ensuring no errors occur | ||||||
|  |   logger.logBox('Complete Box', [ | ||||||
|  |     'Line 1', | ||||||
|  |     'Line 2', | ||||||
|  |     'Line 3', | ||||||
|  |   ], 40); | ||||||
|  |  | ||||||
|  |   // Just assert that the test runs without errors | ||||||
|  |   assert(true); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should handle content that exceeds box width', () => { | ||||||
|  |   // Just ensuring no errors occur when content is too long | ||||||
|  |   logger.logBox('Truncation Test', [ | ||||||
|  |     'This line is way too long and should be truncated because it exceeds the available space', | ||||||
|  |   ], 30); | ||||||
|  |  | ||||||
|  |   // Just assert that the test runs without errors | ||||||
|  |   assert(true); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should create dividers with custom characters', () => { | ||||||
|  |   // Just ensuring no errors occur | ||||||
|  |   logger.logDivider(30); | ||||||
|  |   logger.logDivider(20, '*'); | ||||||
|  |  | ||||||
|  |   // Just assert that the test runs without errors | ||||||
|  |   assert(true); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('should create divider with default width', () => { | ||||||
|  |   // This should use the default width | ||||||
|  |   logger.logDivider(undefined, '-'); | ||||||
|  |  | ||||||
|  |   // Just assert that the test runs without errors | ||||||
|  |   assert(true); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | Deno.test('Logger Demo', () => { | ||||||
|  |   console.log('\n=== LOGGER DEMO ===\n'); | ||||||
|  |  | ||||||
|  |   // Basic logging | ||||||
|  |   logger.log('Regular log message'); | ||||||
|  |   logger.error('Error message'); | ||||||
|  |   logger.warn('Warning message'); | ||||||
|  |   logger.success('Success message'); | ||||||
|  |  | ||||||
|  |   // Logbox with title, content lines, and end | ||||||
|  |   logger.logBoxTitle('Configuration Loaded', 50); | ||||||
|  |   logger.logBoxLine('SNMP Settings:'); | ||||||
|  |   logger.logBoxLine('  Host: 127.0.0.1'); | ||||||
|  |   logger.logBoxLine('  Port: 161'); | ||||||
|  |   logger.logBoxLine('  Version: 1'); | ||||||
|  |   logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |   // Complete logbox in one call | ||||||
|  |   logger.logBox('UPS Status', [ | ||||||
|  |     'Power Status: onBattery', | ||||||
|  |     'Battery Capacity: 75%', | ||||||
|  |     'Runtime Remaining: 30 minutes', | ||||||
|  |   ], 45); | ||||||
|  |  | ||||||
|  |   // Logbox with content that's too long for the width | ||||||
|  |   logger.logBox('Truncation Example', [ | ||||||
|  |     'This line is short enough to fit within the box width', | ||||||
|  |     'This line is way too long and will be truncated because it exceeds the available space for content within the logbox', | ||||||
|  |   ], 40); | ||||||
|  |  | ||||||
|  |   // Demonstrating logbox width being remembered | ||||||
|  |   logger.logBoxTitle('Width Persistence Example', 60); | ||||||
|  |   logger.logBoxLine('These lines use the width from the title'); | ||||||
|  |   logger.logBoxLine('No need to specify the width again'); | ||||||
|  |   logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |   // Demonstrating default width | ||||||
|  |   console.log('\nDefault Width Example:'); | ||||||
|  |   logger.logBoxLine('This line uses the default width'); | ||||||
|  |   logger.logBoxLine('Still using default width'); | ||||||
|  |   logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |   // Divider example | ||||||
|  |   logger.log('\nDivider example:'); | ||||||
|  |   logger.logDivider(30); | ||||||
|  |   logger.logDivider(30, '*'); | ||||||
|  |   logger.logDivider(undefined, '='); | ||||||
|  |  | ||||||
|  |   assert(true); | ||||||
|  | }); | ||||||
							
								
								
									
										347
									
								
								test/test.ts
									
									
									
									
									
								
							
							
						
						
									
										347
									
								
								test/test.ts
									
									
									
									
									
								
							| @@ -1,306 +1,29 @@ | |||||||
| import { tap, expect } from '@push.rocks/tapbundle'; | import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0'; | ||||||
| import { NupstSnmp } from '../ts/snmp.js'; | import { NupstSnmp } from '../ts/snmp/manager.ts'; | ||||||
| import type { SnmpConfig, UpsStatus } from '../ts/snmp.js'; | import type { ISnmpConfig } from '../ts/snmp/types.ts'; | ||||||
| import { SnmpEncoder } from '../ts/snmp/encoder.js'; |  | ||||||
| import { SnmpPacketCreator } from '../ts/snmp/packet-creator.js'; |  | ||||||
| import { SnmpPacketParser } from '../ts/snmp/packet-parser.js'; |  | ||||||
|  |  | ||||||
| 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 testConfig = await testQenv.getEnvVarOnDemandAsObject('testConfig'); | const testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1'); | ||||||
|  | const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3'); | ||||||
|  |  | ||||||
| tap.test('should log config', async () => { | Deno.test('should log config', () => { | ||||||
|   console.log(testConfig); |   console.log(testConfigV1); | ||||||
| }); |   assert(true); | ||||||
|  |  | ||||||
| tap.test('SNMP packet creation and parsing test', async () => { |  | ||||||
|   // We'll test the internal methods that are now in separate classes |  | ||||||
|    |  | ||||||
|   // Test OID conversion |  | ||||||
|   const oidStr = '1.3.6.1.4.1.3808.1.1.1.4.1.1.0'; |  | ||||||
|   const oidArray = SnmpEncoder.oidToArray(oidStr); |  | ||||||
|   console.log('OID array length:', oidArray.length); |  | ||||||
|   console.log('OID array:', oidArray); |  | ||||||
|   // The OID has 14 elements after splitting |  | ||||||
|   expect(oidArray.length).toEqual(14); |  | ||||||
|   expect(oidArray[0]).toEqual(1); |  | ||||||
|   expect(oidArray[1]).toEqual(3); |  | ||||||
|    |  | ||||||
|   // Test OID encoding |  | ||||||
|   const encodedOid = SnmpEncoder.encodeOID(oidArray); |  | ||||||
|   expect(encodedOid).toBeInstanceOf(Buffer); |  | ||||||
|    |  | ||||||
|   // Test SNMP request creation |  | ||||||
|   const request = SnmpPacketCreator.createSnmpGetRequest(oidStr, 'public', true); |  | ||||||
|   expect(request).toBeInstanceOf(Buffer); |  | ||||||
|   expect(request.length).toBeGreaterThan(20); |  | ||||||
|    |  | ||||||
|   // Log the request for debugging |  | ||||||
|   console.log('SNMP Request buffer:', request.toString('hex')); |  | ||||||
|    |  | ||||||
|   // Test integer encoding |  | ||||||
|   const int = SnmpEncoder.encodeInteger(42); |  | ||||||
|   expect(int).toBeInstanceOf(Buffer); |  | ||||||
|   expect(int.length).toBeGreaterThanOrEqual(1); |  | ||||||
|    |  | ||||||
|   // Test SNMPv3 engine ID discovery message |  | ||||||
|   const discoveryMsg = SnmpPacketCreator.createDiscoveryMessage(testConfig, 1); |  | ||||||
|   expect(discoveryMsg).toBeInstanceOf(Buffer); |  | ||||||
|   expect(discoveryMsg.length).toBeGreaterThan(20); |  | ||||||
|    |  | ||||||
|   console.log('SNMPv3 Discovery message:', discoveryMsg.toString('hex')); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('SNMP response parsing simulation', async () => { |  | ||||||
|   // Create a simulated SNMP response for parsing |  | ||||||
|    |  | ||||||
|   // Simulate an INTEGER response (battery capacity) |  | ||||||
|   const intResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence, length 41 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) |  | ||||||
|     0x02, 0x01, 0x64 // Integer (value), value 100 (100%) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Simulate a Gauge32 response (battery capacity) |  | ||||||
|   const gauge32Response = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence, length 41 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) |  | ||||||
|     0x42, 0x01, 0x64 // Gauge32 (value), value 100 (100%) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Simulate a TimeTicks response (battery runtime) |  | ||||||
|   const timeTicksResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence, length 41 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) |  | ||||||
|     0x43, 0x01, 0x0f // TimeTicks (value), value 15 (0.15 seconds or 15/100 seconds) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Test parsing INTEGER response |  | ||||||
|   const intValue = SnmpPacketParser.parseSnmpResponse(intResponse, testConfig, true); |  | ||||||
|   console.log('Parsed INTEGER value:', intValue); |  | ||||||
|   expect(intValue).toEqual(100); |  | ||||||
|    |  | ||||||
|   // Test parsing Gauge32 response |  | ||||||
|   const gauge32Value = SnmpPacketParser.parseSnmpResponse(gauge32Response, testConfig, true); |  | ||||||
|   console.log('Parsed Gauge32 value:', gauge32Value); |  | ||||||
|   expect(gauge32Value).toEqual(100); |  | ||||||
|    |  | ||||||
|   // Test parsing TimeTicks response |  | ||||||
|   const timeTicksValue = SnmpPacketParser.parseSnmpResponse(timeTicksResponse, testConfig, true); |  | ||||||
|   console.log('Parsed TimeTicks value:', timeTicksValue); |  | ||||||
|   expect(timeTicksValue).toEqual(15); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('CyberPower TimeTicks conversion', async () => { |  | ||||||
|   // Test the conversion of TimeTicks to minutes for CyberPower UPS |  | ||||||
|    |  | ||||||
|   // Set up a config for CyberPower |  | ||||||
|   const cyberPowerConfig: SnmpConfig = { |  | ||||||
|     ...testConfig, |  | ||||||
|     upsModel: 'cyberpower' |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   // Create a simulated TimeTicks response with a value of 104 (104/100 seconds) |  | ||||||
|   const ticksResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime) |  | ||||||
|     0x43, 0x01, 0x68 // TimeTicks (value), value 104 (104/100 seconds) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Mock the getUpsStatus function to test our TimeTicks conversion logic |  | ||||||
|   const mockGetUpsStatus = async () => { |  | ||||||
|     // Parse the TimeTicks value from the response |  | ||||||
|     const runtime = SnmpPacketParser.parseSnmpResponse(ticksResponse, testConfig, true); |  | ||||||
|     console.log('Raw runtime value:', runtime); |  | ||||||
|      |  | ||||||
|     // Create a sample UPS status result |  | ||||||
|     const result = { |  | ||||||
|       powerStatus: 'onBattery', |  | ||||||
|       batteryCapacity: 100, |  | ||||||
|       batteryRuntime: 0, |  | ||||||
|       raw: { |  | ||||||
|         powerStatus: 2, |  | ||||||
|         batteryCapacity: 100, |  | ||||||
|         batteryRuntime: runtime, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     // Convert TimeTicks to minutes for CyberPower |  | ||||||
|     if (cyberPowerConfig.upsModel === 'cyberpower' && runtime > 0) { |  | ||||||
|       result.batteryRuntime = Math.floor(runtime / 6000); |  | ||||||
|       console.log(`Converting CyberPower runtime from ${runtime} ticks to ${result.batteryRuntime} minutes`); |  | ||||||
|     } else { |  | ||||||
|       result.batteryRuntime = runtime; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return result; |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   // Call our mock function |  | ||||||
|   const status = await mockGetUpsStatus(); |  | ||||||
|    |  | ||||||
|   // Assert the conversion worked correctly |  | ||||||
|   console.log('Final status object:', status); |  | ||||||
|   expect(status.batteryRuntime).toEqual(0); // 104 ticks / 6000 = 0.0173... rounds to 0 minutes |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('Simulate fully charged online UPS', async () => { |  | ||||||
|   // Test a realistic scenario of an online UPS with high battery capacity and ~30 mins runtime |  | ||||||
|    |  | ||||||
|   // Create simulated responses for power status (online), battery capacity (95%), runtime (30 min) |  | ||||||
|    |  | ||||||
|   // Power Status = 2 (online for CyberPower) |  | ||||||
|   const powerStatusResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x01, 0x01, 0x00, // OID (power status) |  | ||||||
|     0x02, 0x01, 0x02 // Integer (value), value 2 (online) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Battery Capacity = 95% (as Gauge32) |  | ||||||
|   const batteryCapacityResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x02, // Integer (request ID), value 2 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x01, 0x00, // OID (battery capacity) |  | ||||||
|     0x42, 0x01, 0x5F // Gauge32 (value), value 95 (95%) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Battery Runtime = 30 minutes (as TimeTicks) |  | ||||||
|   // 30 minutes = 1800 seconds = 180000 ticks (in 1/100 seconds) |  | ||||||
|   const batteryRuntimeResponse = Buffer.from([ |  | ||||||
|     0x30, 0x2c, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1f, // GetResponse |  | ||||||
|     0x02, 0x01, 0x03, // Integer (request ID), value 3 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x14, // Sequence (varbinds) |  | ||||||
|     0x30, 0x12, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime) |  | ||||||
|     0x43, 0x04, 0x00, 0x02, 0xBF, 0x20 // TimeTicks (value), value 180000 (1800 seconds = 30 minutes) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Mock the getUpsStatus function to test with our simulated data |  | ||||||
|   const mockGetUpsStatus = async () => { |  | ||||||
|     console.log('Simulating UPS status request with synthetic data'); |  | ||||||
|      |  | ||||||
|     // Create a config that specifies this is a CyberPower UPS |  | ||||||
|     const upsConfig: SnmpConfig = { |  | ||||||
|       host: '192.168.1.1', |  | ||||||
|       port: 161, |  | ||||||
|       version: 1, |  | ||||||
|       community: 'public', |  | ||||||
|       timeout: 5000, |  | ||||||
|       upsModel: 'cyberpower', |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     // Parse each simulated response |  | ||||||
|     const powerStatus = SnmpPacketParser.parseSnmpResponse(powerStatusResponse, upsConfig, true); |  | ||||||
|     console.log('Power status value:', powerStatus); |  | ||||||
|      |  | ||||||
|     const batteryCapacity = SnmpPacketParser.parseSnmpResponse(batteryCapacityResponse, upsConfig, true); |  | ||||||
|     console.log('Battery capacity value:', batteryCapacity); |  | ||||||
|      |  | ||||||
|     const batteryRuntime = SnmpPacketParser.parseSnmpResponse(batteryRuntimeResponse, upsConfig, true); |  | ||||||
|     console.log('Battery runtime value:', batteryRuntime); |  | ||||||
|      |  | ||||||
|     // Convert TimeTicks to minutes for CyberPower UPSes |  | ||||||
|     const runtimeMinutes = Math.floor(batteryRuntime / 6000); |  | ||||||
|     console.log(`Converting ${batteryRuntime} ticks to ${runtimeMinutes} minutes`); |  | ||||||
|      |  | ||||||
|     // Interpret power status for CyberPower |  | ||||||
|     // CyberPower: 2=online, 3=on battery |  | ||||||
|     let powerStatusText: 'online' | 'onBattery' | 'unknown' = 'unknown'; |  | ||||||
|     if (powerStatus === 2) { |  | ||||||
|       powerStatusText = 'online'; |  | ||||||
|     } else if (powerStatus === 3) { |  | ||||||
|       powerStatusText = 'onBattery'; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Create the status result |  | ||||||
|     const result: UpsStatus = { |  | ||||||
|       powerStatus: powerStatusText, |  | ||||||
|       batteryCapacity: batteryCapacity, |  | ||||||
|       batteryRuntime: runtimeMinutes, |  | ||||||
|       raw: { |  | ||||||
|         powerStatus, |  | ||||||
|         batteryCapacity, |  | ||||||
|         batteryRuntime, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     return result; |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   // Call our mock function |  | ||||||
|   const status = await mockGetUpsStatus(); |  | ||||||
|    |  | ||||||
|   // Assert that the values match our expectations |  | ||||||
|   console.log('UPS Status Result:', status); |  | ||||||
|   expect(status.powerStatus).toEqual('online'); |  | ||||||
|   expect(status.batteryCapacity).toEqual(95); |  | ||||||
|   expect(status.batteryRuntime).toEqual(30); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // 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', 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 = testConfig.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}`); | ||||||
| @@ -309,7 +32,7 @@ tap.test('Real UPS test', async () => { | |||||||
|     // 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 | ||||||
| @@ -321,10 +44,10 @@ tap.test('Real UPS test', async () => { | |||||||
|     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 | ||||||
| @@ -332,5 +55,39 @@ tap.test('Real UPS test', async () => { | |||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Export the default tap object | Deno.test('Real UPS test v3', async () => { | ||||||
| export default tap.start(); |   try { | ||||||
|  |     console.log('Testing with real UPS configuration...'); | ||||||
|  |  | ||||||
|  |     // Extract the correct SNMP config from the test configuration | ||||||
|  |     const snmpConfig = testConfigV3.snmp as ISnmpConfig; | ||||||
|  |     console.log('SNMP Config:'); | ||||||
|  |     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); | ||||||
|  |     console.log(`  Version: SNMPv${snmpConfig.version}`); | ||||||
|  |     console.log(`  UPS Model: ${snmpConfig.upsModel}`); | ||||||
|  |  | ||||||
|  |     // Use a short timeout for testing | ||||||
|  |     const testSnmpConfig = { | ||||||
|  |       ...snmpConfig, | ||||||
|  |       timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Try to get the UPS status | ||||||
|  |     const status = await snmp.getUpsStatus(testSnmpConfig); | ||||||
|  |  | ||||||
|  |     console.log('UPS Status:'); | ||||||
|  |     console.log(`  Power Status: ${status.powerStatus}`); | ||||||
|  |     console.log(`  Battery Capacity: ${status.batteryCapacity}%`); | ||||||
|  |     console.log(`  Runtime Remaining: ${status.batteryRuntime} minutes`); | ||||||
|  |  | ||||||
|  |     // Just make sure we got valid data types back | ||||||
|  |     assertExists(status); | ||||||
|  |     assert(['online', 'onBattery', 'unknown'].includes(status.powerStatus)); | ||||||
|  |     assertEquals(typeof status.batteryCapacity, 'number'); | ||||||
|  |     assertEquals(typeof status.batteryRuntime, 'number'); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.log('Real UPS test failed:', error); | ||||||
|  |     // Skip the test if we can't connect to the real UPS | ||||||
|  |     console.log('Skipping this test since the UPS might not be available'); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -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: '2.0.0', |   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', | ||||||
| } | }; | ||||||
|   | |||||||
							
								
								
									
										593
									
								
								ts/cli/group-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										593
									
								
								ts/cli/group-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,593 @@ | |||||||
|  | import process from 'node:process'; | ||||||
|  | import { Nupst } from '../nupst.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  | import * as helpers from '../helpers/index.ts'; | ||||||
|  | import { type IGroupConfig } from '../daemon.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class for handling group-related CLI commands | ||||||
|  |  * Provides interface for managing UPS groups | ||||||
|  |  */ | ||||||
|  | export class GroupHandler { | ||||||
|  |   private readonly nupst: Nupst; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Create a new Group handler | ||||||
|  |    * @param nupst Reference to the main Nupst instance | ||||||
|  |    */ | ||||||
|  |   constructor(nupst: Nupst) { | ||||||
|  |     this.nupst = nupst; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * List all UPS groups | ||||||
|  |    */ | ||||||
|  |   public async list(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Try to load configuration | ||||||
|  |       try { | ||||||
|  |         await this.nupst.getDaemon().loadConfig(); | ||||||
|  |       } catch (error) { | ||||||
|  |         const errorBoxWidth = 45; | ||||||
|  |         logger.logBoxTitle('Configuration Error', errorBoxWidth); | ||||||
|  |         logger.logBoxLine('No configuration found.'); | ||||||
|  |         logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); | ||||||
|  |         logger.logBoxEnd(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Get current configuration | ||||||
|  |       const config = this.nupst.getDaemon().getConfig(); | ||||||
|  |  | ||||||
|  |       // Check if multi-UPS config | ||||||
|  |       if (!config.groups || !Array.isArray(config.groups)) { | ||||||
|  |         // Legacy or missing groups configuration | ||||||
|  |         const boxWidth = 45; | ||||||
|  |         logger.logBoxTitle('UPS Groups', boxWidth); | ||||||
|  |         logger.logBoxLine('No groups configured.'); | ||||||
|  |         logger.logBoxLine('Use "nupst group add" to add a UPS group.'); | ||||||
|  |         logger.logBoxEnd(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Display group list | ||||||
|  |       const boxWidth = 60; | ||||||
|  |       logger.logBoxTitle('UPS Groups', boxWidth); | ||||||
|  |  | ||||||
|  |       if (config.groups.length === 0) { | ||||||
|  |         logger.logBoxLine('No UPS groups configured.'); | ||||||
|  |         logger.logBoxLine('Use "nupst group add" to add a UPS group.'); | ||||||
|  |       } else { | ||||||
|  |         logger.logBoxLine(`Found ${config.groups.length} group(s)`); | ||||||
|  |         logger.logBoxLine(''); | ||||||
|  |         logger.logBoxLine('ID         | Name                 | Mode         | UPS Devices'); | ||||||
|  |         logger.logBoxLine('-----------+----------------------+--------------+----------------'); | ||||||
|  |  | ||||||
|  |         for (const group of config.groups) { | ||||||
|  |           const id = group.id.padEnd(10, ' ').substring(0, 10); | ||||||
|  |           const name = (group.name || '').padEnd(20, ' ').substring(0, 20); | ||||||
|  |           const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12); | ||||||
|  |  | ||||||
|  |           // Count UPS devices in this group | ||||||
|  |           const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id)); | ||||||
|  |           const upsCount = upsInGroup.length; | ||||||
|  |           const upsNames = upsInGroup.map((ups) => ups.name).join(', '); | ||||||
|  |  | ||||||
|  |           logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       logger.logBoxEnd(); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Add a new UPS group | ||||||
|  |    */ | ||||||
|  |   public async add(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Import readline module for user input | ||||||
|  |       const readline = await import('node:readline'); | ||||||
|  |  | ||||||
|  |       const rl = readline.createInterface({ | ||||||
|  |         input: process.stdin, | ||||||
|  |         output: process.stdout, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Helper function to prompt for input | ||||||
|  |       const prompt = (question: string): Promise<string> => { | ||||||
|  |         return new Promise((resolve) => { | ||||||
|  |           rl.question(question, (answer: string) => { | ||||||
|  |             resolve(answer); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         // Try to load configuration | ||||||
|  |         try { | ||||||
|  |           await this.nupst.getDaemon().loadConfig(); | ||||||
|  |         } catch (error) { | ||||||
|  |           logger.error( | ||||||
|  |             'No configuration found. Please run "nupst setup" first to create a configuration.', | ||||||
|  |           ); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Get current configuration | ||||||
|  |         const config = this.nupst.getDaemon().getConfig(); | ||||||
|  |  | ||||||
|  |         // Initialize groups array if not exists | ||||||
|  |         if (!config.groups) { | ||||||
|  |           config.groups = []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check if upsDevices is initialized | ||||||
|  |         if (!config.upsDevices) { | ||||||
|  |           config.upsDevices = []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         logger.log('\nNUPST Add Group'); | ||||||
|  |         logger.log('==============\n'); | ||||||
|  |         logger.log('This will guide you through creating a new UPS group.\n'); | ||||||
|  |  | ||||||
|  |         // Generate a new unique group ID | ||||||
|  |         const groupId = helpers.shortId(); | ||||||
|  |  | ||||||
|  |         // Get group name | ||||||
|  |         const name = await prompt('Group Name: '); | ||||||
|  |  | ||||||
|  |         // Get group mode | ||||||
|  |         const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: '); | ||||||
|  |         const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant'; | ||||||
|  |  | ||||||
|  |         // Get optional description | ||||||
|  |         const description = await prompt('Group Description (optional): '); | ||||||
|  |  | ||||||
|  |         // Create the new group | ||||||
|  |         const newGroup: IGroupConfig = { | ||||||
|  |           id: groupId, | ||||||
|  |           name: name || `Group-${groupId}`, | ||||||
|  |           mode, | ||||||
|  |           description: description || undefined, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Add the group to the configuration | ||||||
|  |         config.groups.push(newGroup); | ||||||
|  |  | ||||||
|  |         // Save the configuration | ||||||
|  |         await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |  | ||||||
|  |         // Display summary | ||||||
|  |         const boxWidth = 45; | ||||||
|  |         logger.logBoxTitle('Group Created', boxWidth); | ||||||
|  |         logger.logBoxLine(`ID: ${newGroup.id}`); | ||||||
|  |         logger.logBoxLine(`Name: ${newGroup.name}`); | ||||||
|  |         logger.logBoxLine(`Mode: ${newGroup.mode}`); | ||||||
|  |         if (newGroup.description) { | ||||||
|  |           logger.logBoxLine(`Description: ${newGroup.description}`); | ||||||
|  |         } | ||||||
|  |         logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |         // Check if there are UPS devices to assign to this group | ||||||
|  |         if (config.upsDevices.length > 0) { | ||||||
|  |           const assignUps = await prompt( | ||||||
|  |             'Would you like to assign UPS devices to this group now? (y/N): ', | ||||||
|  |           ); | ||||||
|  |           if (assignUps.toLowerCase() === 'y') { | ||||||
|  |             await this.assignUpsToGroup(newGroup.id, config, prompt); | ||||||
|  |  | ||||||
|  |             // Save again after assigning UPS devices | ||||||
|  |             await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check if service is running and restart it if needed | ||||||
|  |         this.nupst.getUpsHandler().restartServiceIfRunning(); | ||||||
|  |  | ||||||
|  |         logger.log('\nGroup setup complete!'); | ||||||
|  |       } finally { | ||||||
|  |         rl.close(); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Edit an existing UPS group | ||||||
|  |    * @param groupId ID of the group to edit | ||||||
|  |    */ | ||||||
|  |   public async edit(groupId: string): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Import readline module for user input | ||||||
|  |       const readline = await import('node:readline'); | ||||||
|  |  | ||||||
|  |       const rl = readline.createInterface({ | ||||||
|  |         input: process.stdin, | ||||||
|  |         output: process.stdout, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Helper function to prompt for input | ||||||
|  |       const prompt = (question: string): Promise<string> => { | ||||||
|  |         return new Promise((resolve) => { | ||||||
|  |           rl.question(question, (answer: string) => { | ||||||
|  |             resolve(answer); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         // Try to load configuration | ||||||
|  |         try { | ||||||
|  |           await this.nupst.getDaemon().loadConfig(); | ||||||
|  |         } catch (error) { | ||||||
|  |           logger.error( | ||||||
|  |             'No configuration found. Please run "nupst setup" first to create a configuration.', | ||||||
|  |           ); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Get current configuration | ||||||
|  |         const config = this.nupst.getDaemon().getConfig(); | ||||||
|  |  | ||||||
|  |         // Check if groups are initialized | ||||||
|  |         if (!config.groups || !Array.isArray(config.groups)) { | ||||||
|  |           logger.error( | ||||||
|  |             'No groups configured. Please run "nupst group add" first to create a group.', | ||||||
|  |           ); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Find the group to edit | ||||||
|  |         const groupIndex = config.groups.findIndex((group) => group.id === groupId); | ||||||
|  |         if (groupIndex === -1) { | ||||||
|  |           logger.error(`Group with ID "${groupId}" not found.`); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const group = config.groups[groupIndex]; | ||||||
|  |  | ||||||
|  |         logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`); | ||||||
|  |         logger.log('==============================================\n'); | ||||||
|  |  | ||||||
|  |         // Edit group name | ||||||
|  |         const newName = await prompt(`Group Name [${group.name}]: `); | ||||||
|  |         if (newName.trim()) { | ||||||
|  |           group.name = newName; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Edit group mode | ||||||
|  |         const currentMode = group.mode || 'redundant'; | ||||||
|  |         const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `); | ||||||
|  |         if (modeInput.trim()) { | ||||||
|  |           group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant'; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Edit description | ||||||
|  |         const currentDesc = group.description || ''; | ||||||
|  |         const newDesc = await prompt(`Group Description [${currentDesc}]: `); | ||||||
|  |         if (newDesc.trim() || newDesc === '') { | ||||||
|  |           group.description = newDesc.trim() || undefined; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Update the group in the configuration | ||||||
|  |         config.groups[groupIndex] = group; | ||||||
|  |  | ||||||
|  |         // Save the configuration | ||||||
|  |         await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |  | ||||||
|  |         // Display summary | ||||||
|  |         const boxWidth = 45; | ||||||
|  |         logger.logBoxTitle('Group Updated', boxWidth); | ||||||
|  |         logger.logBoxLine(`ID: ${group.id}`); | ||||||
|  |         logger.logBoxLine(`Name: ${group.name}`); | ||||||
|  |         logger.logBoxLine(`Mode: ${group.mode}`); | ||||||
|  |         if (group.description) { | ||||||
|  |           logger.logBoxLine(`Description: ${group.description}`); | ||||||
|  |         } | ||||||
|  |         logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |         // Edit UPS assignments if requested | ||||||
|  |         const editAssignments = await prompt( | ||||||
|  |           'Would you like to edit UPS assignments for this group? (y/N): ', | ||||||
|  |         ); | ||||||
|  |         if (editAssignments.toLowerCase() === 'y') { | ||||||
|  |           await this.assignUpsToGroup(group.id, config, prompt); | ||||||
|  |  | ||||||
|  |           // Save again after editing assignments | ||||||
|  |           await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check if service is running and restart it if needed | ||||||
|  |         this.nupst.getUpsHandler().restartServiceIfRunning(); | ||||||
|  |  | ||||||
|  |         logger.log('\nGroup edit complete!'); | ||||||
|  |       } finally { | ||||||
|  |         rl.close(); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Delete an existing UPS group | ||||||
|  |    * @param groupId ID of the group to delete | ||||||
|  |    */ | ||||||
|  |   public async remove(groupId: string): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Try to load configuration | ||||||
|  |       try { | ||||||
|  |         await this.nupst.getDaemon().loadConfig(); | ||||||
|  |       } catch (error) { | ||||||
|  |         logger.error( | ||||||
|  |           'No configuration found. Please run "nupst setup" first to create a configuration.', | ||||||
|  |         ); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Get current configuration | ||||||
|  |       const config = this.nupst.getDaemon().getConfig(); | ||||||
|  |  | ||||||
|  |       // Check if groups are initialized | ||||||
|  |       if (!config.groups || !Array.isArray(config.groups)) { | ||||||
|  |         logger.error('No groups configured.'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Find the group to delete | ||||||
|  |       const groupIndex = config.groups.findIndex((group) => group.id === groupId); | ||||||
|  |       if (groupIndex === -1) { | ||||||
|  |         logger.error(`Group with ID "${groupId}" not found.`); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const groupToDelete = config.groups[groupIndex]; | ||||||
|  |  | ||||||
|  |       // Get confirmation before deleting | ||||||
|  |       const readline = await import('node:readline'); | ||||||
|  |       const rl = readline.createInterface({ | ||||||
|  |         input: process.stdin, | ||||||
|  |         output: process.stdout, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       const confirm = await new Promise<string>((resolve) => { | ||||||
|  |         rl.question( | ||||||
|  |           `Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, | ||||||
|  |           (answer) => { | ||||||
|  |             resolve(answer.toLowerCase()); | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       rl.close(); | ||||||
|  |  | ||||||
|  |       if (confirm !== 'y' && confirm !== 'yes') { | ||||||
|  |         logger.log('Deletion cancelled.'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Remove this group from all UPS device group assignments | ||||||
|  |       if (config.upsDevices && Array.isArray(config.upsDevices)) { | ||||||
|  |         for (const ups of config.upsDevices) { | ||||||
|  |           const groupIndex = ups.groups.indexOf(groupId); | ||||||
|  |           if (groupIndex !== -1) { | ||||||
|  |             ups.groups.splice(groupIndex, 1); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Remove the group from the array | ||||||
|  |       config.groups.splice(groupIndex, 1); | ||||||
|  |  | ||||||
|  |       // Save the configuration | ||||||
|  |       await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |  | ||||||
|  |       logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`); | ||||||
|  |  | ||||||
|  |       // Check if service is running and restart it if needed | ||||||
|  |       this.nupst.getUpsHandler().restartServiceIfRunning(); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Failed to delete group: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Assign UPS devices to groups | ||||||
|  |    * @param ups UPS configuration to update | ||||||
|  |    * @param groups Available groups | ||||||
|  |    * @param prompt Function to prompt for user input | ||||||
|  |    */ | ||||||
|  |   public async assignUpsToGroups( | ||||||
|  |     ups: any, | ||||||
|  |     groups: any[], | ||||||
|  |     prompt: (question: string) => Promise<string>, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     // Initialize groups array if it doesn't exist | ||||||
|  |     if (!ups.groups) { | ||||||
|  |       ups.groups = []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Show current group assignments | ||||||
|  |     logger.log('\nCurrent Group Assignments:'); | ||||||
|  |     if (ups.groups && ups.groups.length > 0) { | ||||||
|  |       for (const groupId of ups.groups) { | ||||||
|  |         const group = groups.find((g) => g.id === groupId); | ||||||
|  |         if (group) { | ||||||
|  |           logger.log(`- ${group.name} (${group.id})`); | ||||||
|  |         } else { | ||||||
|  |           logger.log(`- Unknown group (${groupId})`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       logger.log('- None'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Show available groups | ||||||
|  |     logger.log('\nAvailable Groups:'); | ||||||
|  |     if (groups.length === 0) { | ||||||
|  |       logger.log('- No groups available. Use "nupst group add" to create groups.'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (let i = 0; i < groups.length; i++) { | ||||||
|  |       const group = groups[i]; | ||||||
|  |       const assigned = ups.groups && ups.groups.includes(group.id); | ||||||
|  |       logger.log( | ||||||
|  |         `${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Prompt for group selection | ||||||
|  |     const selection = await prompt( | ||||||
|  |       '\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (selection.toLowerCase() === 'clear') { | ||||||
|  |       // Clear all group assignments | ||||||
|  |       ups.groups = []; | ||||||
|  |       logger.log('All group assignments cleared.'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!selection.trim()) { | ||||||
|  |       // No change if empty input | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Process selections | ||||||
|  |     const selections = selection.split(',').map((s) => s.trim()); | ||||||
|  |  | ||||||
|  |     for (const sel of selections) { | ||||||
|  |       const index = parseInt(sel, 10) - 1; | ||||||
|  |       if (isNaN(index) || index < 0 || index >= groups.length) { | ||||||
|  |         logger.error(`Invalid selection: ${sel}`); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const group = groups[index]; | ||||||
|  |  | ||||||
|  |       // Initialize groups array if needed (should already be done above) | ||||||
|  |       if (!ups.groups) { | ||||||
|  |         ups.groups = []; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Toggle assignment | ||||||
|  |       const groupIndex = ups.groups.indexOf(group.id); | ||||||
|  |       if (groupIndex === -1) { | ||||||
|  |         // Add to group | ||||||
|  |         ups.groups.push(group.id); | ||||||
|  |         logger.log(`Added to group: ${group.name} (${group.id})`); | ||||||
|  |       } else { | ||||||
|  |         // Remove from group | ||||||
|  |         ups.groups.splice(groupIndex, 1); | ||||||
|  |         logger.log(`Removed from group: ${group.name} (${group.id})`); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Assign UPS devices to a specific group | ||||||
|  |    * @param groupId Group ID to assign UPS devices to | ||||||
|  |    * @param config Full configuration | ||||||
|  |    * @param prompt Function to prompt for user input | ||||||
|  |    */ | ||||||
|  |   public async assignUpsToGroup( | ||||||
|  |     groupId: string, | ||||||
|  |     config: any, | ||||||
|  |     prompt: (question: string) => Promise<string>, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     if (!config.upsDevices || config.upsDevices.length === 0) { | ||||||
|  |       logger.log('No UPS devices available. Use "nupst add" to add UPS devices.'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const group = config.groups.find((g: { id: string }) => g.id === groupId); | ||||||
|  |     if (!group) { | ||||||
|  |       logger.error(`Group with ID "${groupId}" not found.`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Show current assignments | ||||||
|  |     logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`); | ||||||
|  |     const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) => | ||||||
|  |       ups.groups && ups.groups.includes(groupId) | ||||||
|  |     ); | ||||||
|  |     if (upsInGroup.length === 0) { | ||||||
|  |       logger.log('- None'); | ||||||
|  |     } else { | ||||||
|  |       for (const ups of upsInGroup) { | ||||||
|  |         logger.log(`- ${ups.name} (${ups.id})`); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Show all UPS devices | ||||||
|  |     logger.log('\nAvailable UPS devices:'); | ||||||
|  |     for (let i = 0; i < config.upsDevices.length; i++) { | ||||||
|  |       const ups = config.upsDevices[i]; | ||||||
|  |       const assigned = ups.groups && ups.groups.includes(groupId); | ||||||
|  |       logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Prompt for UPS selection | ||||||
|  |     const selection = await prompt( | ||||||
|  |       '\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (selection.toLowerCase() === 'clear') { | ||||||
|  |       // Clear all UPS from this group | ||||||
|  |       for (const ups of config.upsDevices) { | ||||||
|  |         if (ups.groups) { | ||||||
|  |           const groupIndex = ups.groups.indexOf(groupId); | ||||||
|  |           if (groupIndex !== -1) { | ||||||
|  |             ups.groups.splice(groupIndex, 1); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       logger.log(`All UPS devices removed from group "${group.name}".`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!selection.trim()) { | ||||||
|  |       // No change if empty input | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Process selections | ||||||
|  |     const selections = selection.split(',').map((s) => s.trim()); | ||||||
|  |  | ||||||
|  |     for (const sel of selections) { | ||||||
|  |       const index = parseInt(sel, 10) - 1; | ||||||
|  |       if (isNaN(index) || index < 0 || index >= config.upsDevices.length) { | ||||||
|  |         logger.error(`Invalid selection: ${sel}`); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const ups = config.upsDevices[index]; | ||||||
|  |  | ||||||
|  |       // Initialize groups array if needed | ||||||
|  |       if (!ups.groups) { | ||||||
|  |         ups.groups = []; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Toggle assignment | ||||||
|  |       const groupIndex = ups.groups.indexOf(groupId); | ||||||
|  |       if (groupIndex === -1) { | ||||||
|  |         // Add to group | ||||||
|  |         ups.groups.push(groupId); | ||||||
|  |         logger.log(`Added "${ups.name}" to group "${group.name}"`); | ||||||
|  |       } else { | ||||||
|  |         // Remove from group | ||||||
|  |         ups.groups.splice(groupIndex, 1); | ||||||
|  |         logger.log(`Removed "${ups.name}" from group "${group.name}"`); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										321
									
								
								ts/cli/service-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								ts/cli/service-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | |||||||
|  | import process from 'node:process'; | ||||||
|  | import { execSync } from 'node:child_process'; | ||||||
|  | import { Nupst } from '../nupst.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class for handling service-related CLI commands | ||||||
|  |  * Provides interface for managing systemd service | ||||||
|  |  */ | ||||||
|  | export class ServiceHandler { | ||||||
|  |   private readonly nupst: Nupst; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Create a new Service handler | ||||||
|  |    * @param nupst Reference to the main Nupst instance | ||||||
|  |    */ | ||||||
|  |   constructor(nupst: Nupst) { | ||||||
|  |     this.nupst = nupst; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Enable the service (requires root) | ||||||
|  |    */ | ||||||
|  |   public async enable(): Promise<void> { | ||||||
|  |     this.checkRootAccess('This command must be run as root.'); | ||||||
|  |     await this.nupst.getSystemd().install(); | ||||||
|  |     logger.log('NUPST service has been installed. Use "nupst start" to start the service.'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Start the daemon directly | ||||||
|  |    * @param debugMode Whether to enable debug mode | ||||||
|  |    */ | ||||||
|  |   public async daemonStart(debugMode: boolean = false): Promise<void> { | ||||||
|  |     logger.log('Starting NUPST daemon...'); | ||||||
|  |     try { | ||||||
|  |       // Enable debug mode for SNMP if requested | ||||||
|  |       if (debugMode) { | ||||||
|  |         this.nupst.getSnmp().enableDebug(); | ||||||
|  |         logger.log('SNMP debug mode enabled'); | ||||||
|  |       } | ||||||
|  |       await this.nupst.getDaemon().start(); | ||||||
|  |     } catch (error) { | ||||||
|  |       // Error is already logged and process.exit is called in daemon.start() | ||||||
|  |       // No need to handle it here | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show logs of the systemd service | ||||||
|  |    */ | ||||||
|  |   public async logs(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Use exec with spawn to properly follow logs in real-time | ||||||
|  |       const { spawn } = await import('child_process'); | ||||||
|  |       logger.log('Tailing nupst service logs (Ctrl+C to exit)...\n'); | ||||||
|  |  | ||||||
|  |       const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], { | ||||||
|  |         stdio: ['ignore', 'inherit', 'inherit'], | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Forward signals to child process | ||||||
|  |       process.on('SIGINT', () => { | ||||||
|  |         journalctl.kill('SIGINT'); | ||||||
|  |         process.exit(0); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Wait for process to exit | ||||||
|  |       await new Promise<void>((resolve) => { | ||||||
|  |         journalctl.on('exit', () => resolve()); | ||||||
|  |       }); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error(`Failed to retrieve logs: ${error}`); | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Stop the systemd service | ||||||
|  |    */ | ||||||
|  |   public async stop(): Promise<void> { | ||||||
|  |     await this.nupst.getSystemd().stop(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Start the systemd service | ||||||
|  |    */ | ||||||
|  |   public async start(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       await this.nupst.getSystemd().start(); | ||||||
|  |     } catch (error) { | ||||||
|  |       // Error will be displayed by systemd.start() | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show status of the systemd service and UPS | ||||||
|  |    */ | ||||||
|  |   public async status(): Promise<void> { | ||||||
|  |     // Extract debug options from args array | ||||||
|  |     const debugOptions = this.extractDebugOptions(process.argv); | ||||||
|  |     await this.nupst.getSystemd().getStatus(debugOptions.debugMode); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Disable the service (requires root) | ||||||
|  |    */ | ||||||
|  |   public async disable(): Promise<void> { | ||||||
|  |     this.checkRootAccess('This command must be run as root.'); | ||||||
|  |     await this.nupst.getSystemd().disable(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Check if the user has root access | ||||||
|  |    * @param errorMessage Error message to display if not root | ||||||
|  |    */ | ||||||
|  |   private checkRootAccess(errorMessage: string): void { | ||||||
|  |     if (process.getuid && process.getuid() !== 0) { | ||||||
|  |       logger.error(errorMessage); | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Update NUPST from repository and refresh systemd service | ||||||
|  |    */ | ||||||
|  |   public async update(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Check if running as root | ||||||
|  |       this.checkRootAccess( | ||||||
|  |         'This command must be run as root to update NUPST and refresh the systemd service.', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       const boxWidth = 45; | ||||||
|  |       logger.logBoxTitle('NUPST Update Process', boxWidth); | ||||||
|  |       logger.logBoxLine('Updating NUPST from repository...'); | ||||||
|  |  | ||||||
|  |       // Determine the installation directory (assuming it's either /opt/nupst or the current directory) | ||||||
|  |       const { existsSync } = await import('fs'); | ||||||
|  |       let installDir = '/opt/nupst'; | ||||||
|  |  | ||||||
|  |       if (!existsSync(installDir)) { | ||||||
|  |         // If not installed in /opt/nupst, use the current directory | ||||||
|  |         const { dirname } = await import('path'); | ||||||
|  |         installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable | ||||||
|  |         logger.logBoxLine(`Using local installation directory: ${installDir}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         // 1. Update the repository | ||||||
|  |         logger.logBoxLine('Pulling latest changes from git repository...'); | ||||||
|  |         execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, { | ||||||
|  |           stdio: 'pipe', | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // 2. Run the install.sh script | ||||||
|  |         logger.logBoxLine('Running install.sh to update NUPST...'); | ||||||
|  |         execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); | ||||||
|  |  | ||||||
|  |         // 3. Run the setup.sh script with force flag to update Node.js and dependencies | ||||||
|  |         logger.logBoxLine('Running setup.sh to update Node.js and dependencies...'); | ||||||
|  |         execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' }); | ||||||
|  |  | ||||||
|  |         // 4. Refresh the systemd service | ||||||
|  |         logger.logBoxLine('Refreshing systemd service...'); | ||||||
|  |  | ||||||
|  |         // First check if service exists | ||||||
|  |         let serviceExists = false; | ||||||
|  |         try { | ||||||
|  |           const output = execSync('systemctl list-unit-files | grep nupst.service').toString(); | ||||||
|  |           serviceExists = output.includes('nupst.service'); | ||||||
|  |         } catch (error) { | ||||||
|  |           // If grep fails (service not found), serviceExists remains false | ||||||
|  |           serviceExists = false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (serviceExists) { | ||||||
|  |           // Stop the service if it's running | ||||||
|  |           const isRunning = | ||||||
|  |             execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; | ||||||
|  |           if (isRunning) { | ||||||
|  |             logger.logBoxLine('Stopping nupst service...'); | ||||||
|  |             execSync('systemctl stop nupst.service'); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           // Reinstall the service | ||||||
|  |           logger.logBoxLine('Reinstalling systemd service...'); | ||||||
|  |           await this.nupst.getSystemd().install(); | ||||||
|  |  | ||||||
|  |           // Restart the service if it was running | ||||||
|  |           if (isRunning) { | ||||||
|  |             logger.logBoxLine('Restarting nupst service...'); | ||||||
|  |             execSync('systemctl start nupst.service'); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           logger.logBoxLine('Systemd service not installed, skipping service refresh.'); | ||||||
|  |           logger.logBoxLine('Run "nupst enable" to install the service.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         logger.logBoxLine('Update completed successfully!'); | ||||||
|  |         logger.logBoxEnd(); | ||||||
|  |       } catch (error) { | ||||||
|  |         logger.logBoxLine('Error during update process:'); | ||||||
|  |         logger.logBoxLine(`${error instanceof Error ? error.message : String(error)}`); | ||||||
|  |         logger.logBoxEnd(); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Completely uninstall NUPST from the system | ||||||
|  |    */ | ||||||
|  |   public async uninstall(): Promise<void> { | ||||||
|  |     // Check if running as root | ||||||
|  |     this.checkRootAccess('This command must be run as root.'); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Import readline module for user input | ||||||
|  |       const readline = await import('readline'); | ||||||
|  |  | ||||||
|  |       const rl = readline.createInterface({ | ||||||
|  |         input: process.stdin, | ||||||
|  |         output: process.stdout, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Helper function to prompt for input | ||||||
|  |       const prompt = (question: string): Promise<string> => { | ||||||
|  |         return new Promise((resolve) => { | ||||||
|  |           rl.question(question, (answer: string) => { | ||||||
|  |             resolve(answer); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       console.log('\nNUPST Uninstaller'); | ||||||
|  |       console.log('==============='); | ||||||
|  |       console.log('This will completely remove NUPST from your system.\n'); | ||||||
|  |  | ||||||
|  |       // Ask about removing configuration | ||||||
|  |       const removeConfig = await prompt( | ||||||
|  |         'Do you want to remove the NUPST configuration files? (y/N): ', | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // Find the uninstall.sh script location | ||||||
|  |       let uninstallScriptPath: string; | ||||||
|  |  | ||||||
|  |       // Try to determine script location based on executable path | ||||||
|  |       try { | ||||||
|  |         // For ESM, we can use import.meta.url, but since we might be in CJS | ||||||
|  |         // we'll use a more reliable approach based on process.argv[1] | ||||||
|  |         const binPath = process.argv[1]; | ||||||
|  |         const { dirname, join } = await import('path'); | ||||||
|  |         const modulePath = dirname(dirname(binPath)); | ||||||
|  |         uninstallScriptPath = join(modulePath, 'uninstall.sh'); | ||||||
|  |  | ||||||
|  |         // Check if the script exists | ||||||
|  |         const { access } = await import('fs/promises'); | ||||||
|  |         await access(uninstallScriptPath); | ||||||
|  |       } catch (error) { | ||||||
|  |         // If we can't find it in the expected location, try common installation paths | ||||||
|  |         const commonPaths = ['/opt/nupst/uninstall.sh', `${process.cwd()}/uninstall.sh`]; | ||||||
|  |         const { existsSync } = await import('fs'); | ||||||
|  |  | ||||||
|  |         uninstallScriptPath = ''; | ||||||
|  |         for (const path of commonPaths) { | ||||||
|  |           if (existsSync(path)) { | ||||||
|  |             uninstallScriptPath = path; | ||||||
|  |             break; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!uninstallScriptPath) { | ||||||
|  |           console.error('Could not locate uninstall.sh script. Aborting uninstall.'); | ||||||
|  |           rl.close(); | ||||||
|  |           process.exit(1); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Close readline before executing script | ||||||
|  |       rl.close(); | ||||||
|  |  | ||||||
|  |       // Execute uninstall.sh with the appropriate option | ||||||
|  |       console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`); | ||||||
|  |  | ||||||
|  |       // Pass the configuration removal option as an environment variable | ||||||
|  |       const env = { | ||||||
|  |         ...process.env, | ||||||
|  |         REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no', | ||||||
|  |         REMOVE_REPO: 'yes', // Always remove repo as requested | ||||||
|  |         NUPST_CLI_CALL: 'true', // Flag to indicate this is being called from CLI | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // Run the uninstall script with sudo | ||||||
|  |       execSync(`sudo bash ${uninstallScriptPath}`, { | ||||||
|  |         env, | ||||||
|  |         stdio: 'inherit', // Show output in the terminal | ||||||
|  |       }); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Extract and remove debug options from args array | ||||||
|  |    * @param args Command line arguments | ||||||
|  |    * @returns Object with debug flags and cleaned args | ||||||
|  |    */ | ||||||
|  |   private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } { | ||||||
|  |     const debugMode = args.includes('--debug') || args.includes('-d'); | ||||||
|  |     // Remove debug flags from args | ||||||
|  |     const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d'); | ||||||
|  |  | ||||||
|  |     return { debugMode, cleanedArgs }; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1017
									
								
								ts/cli/ups-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1017
									
								
								ts/cli/ups-handler.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										804
									
								
								ts/daemon.ts
									
									
									
									
									
								
							
							
						
						
									
										804
									
								
								ts/daemon.ts
									
									
									
									
									
								
							| @@ -1,11 +1,23 @@ | |||||||
| import * as fs from 'fs'; | import process from 'node:process'; | ||||||
| import * as path from 'path'; | import * as fs from 'node:fs'; | ||||||
| import { NupstSnmp, type ISnmpConfig } from './snmp.js'; | import * as path from 'node:path'; | ||||||
|  | import { exec, execFile } from 'node:child_process'; | ||||||
|  | import { promisify } from 'node:util'; | ||||||
|  | import { NupstSnmp } from './snmp/manager.ts'; | ||||||
|  | import type { ISnmpConfig } from './snmp/types.ts'; | ||||||
|  | import { logger } from './logger.ts'; | ||||||
|  |  | ||||||
|  | const execAsync = promisify(exec); | ||||||
|  | const execFileAsync = promisify(execFile); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Configuration interface for the daemon |  * UPS configuration interface | ||||||
|  */ |  */ | ||||||
| export interface INupstConfig { | export interface IUpsConfig { | ||||||
|  |   /** Unique ID for the UPS */ | ||||||
|  |   id: string; | ||||||
|  |   /** Friendly name for the UPS */ | ||||||
|  |   name: string; | ||||||
|   /** SNMP configuration settings */ |   /** SNMP configuration settings */ | ||||||
|   snmp: ISnmpConfig; |   snmp: ISnmpConfig; | ||||||
|   /** Threshold settings for initiating shutdown */ |   /** Threshold settings for initiating shutdown */ | ||||||
| @@ -15,8 +27,58 @@ export interface INupstConfig { | |||||||
|     /** Shutdown when runtime below this minutes */ |     /** Shutdown when runtime below this minutes */ | ||||||
|     runtime: number; |     runtime: number; | ||||||
|   }; |   }; | ||||||
|  |   /** Group IDs this UPS belongs to */ | ||||||
|  |   groups: string[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Group configuration interface | ||||||
|  |  */ | ||||||
|  | export interface IGroupConfig { | ||||||
|  |   /** Unique ID for the group */ | ||||||
|  |   id: string; | ||||||
|  |   /** Friendly name for the group */ | ||||||
|  |   name: string; | ||||||
|  |   /** Group operation mode */ | ||||||
|  |   mode: 'redundant' | 'nonRedundant'; | ||||||
|  |   /** Optional description */ | ||||||
|  |   description?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Configuration interface for the daemon | ||||||
|  |  */ | ||||||
|  | export interface INupstConfig { | ||||||
|  |   /** UPS devices configuration */ | ||||||
|  |   upsDevices: IUpsConfig[]; | ||||||
|  |   /** Groups configuration */ | ||||||
|  |   groups: IGroupConfig[]; | ||||||
|   /** Check interval in milliseconds */ |   /** Check interval in milliseconds */ | ||||||
|   checkInterval: number; |   checkInterval: number; | ||||||
|  |  | ||||||
|  |   // Legacy fields for backward compatibility | ||||||
|  |   /** SNMP configuration settings (legacy) */ | ||||||
|  |   snmp?: ISnmpConfig; | ||||||
|  |   /** Threshold settings (legacy) */ | ||||||
|  |   thresholds?: { | ||||||
|  |     /** Shutdown when battery below this percentage */ | ||||||
|  |     battery: number; | ||||||
|  |     /** Shutdown when runtime below this minutes */ | ||||||
|  |     runtime: number; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * UPS status tracking interface | ||||||
|  |  */ | ||||||
|  | interface IUpsStatus { | ||||||
|  |   id: string; | ||||||
|  |   name: string; | ||||||
|  |   powerStatus: 'online' | 'onBattery' | 'unknown'; | ||||||
|  |   batteryCapacity: number; | ||||||
|  |   batteryRuntime: number; | ||||||
|  |   lastStatusChange: number; | ||||||
|  |   lastCheckTime: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -29,32 +91,41 @@ export class NupstDaemon { | |||||||
|  |  | ||||||
|   /** Default configuration */ |   /** Default configuration */ | ||||||
|   private readonly DEFAULT_CONFIG: INupstConfig = { |   private readonly DEFAULT_CONFIG: INupstConfig = { | ||||||
|     snmp: { |     upsDevices: [ | ||||||
|       host: '127.0.0.1', |       { | ||||||
|       port: 161, |         id: 'default', | ||||||
|       community: 'public', |         name: 'Default UPS', | ||||||
|       version: 1, |         snmp: { | ||||||
|       timeout: 5000, |           host: '127.0.0.1', | ||||||
|       // SNMPv3 defaults (used only if version === 3) |           port: 161, | ||||||
|       securityLevel: 'authPriv', |           community: 'public', | ||||||
|       username: '', |           version: 1, | ||||||
|       authProtocol: 'SHA', |           timeout: 5000, | ||||||
|       authKey: '', |           // SNMPv3 defaults (used only if version === 3) | ||||||
|       privProtocol: 'AES', |           securityLevel: 'authPriv', | ||||||
|       privKey: '', |           username: '', | ||||||
|       // UPS model for OID selection |           authProtocol: 'SHA', | ||||||
|       upsModel: 'cyberpower' |           authKey: '', | ||||||
|     }, |           privProtocol: 'AES', | ||||||
|     thresholds: { |           privKey: '', | ||||||
|       battery: 60, // Shutdown when battery below 60% |           // UPS model for OID selection | ||||||
|       runtime: 20, // Shutdown when runtime below 20 minutes |           upsModel: 'cyberpower', | ||||||
|     }, |         }, | ||||||
|  |         thresholds: { | ||||||
|  |           battery: 60, // Shutdown when battery below 60% | ||||||
|  |           runtime: 20, // Shutdown when runtime below 20 minutes | ||||||
|  |         }, | ||||||
|  |         groups: [], | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |     groups: [], | ||||||
|     checkInterval: 30000, // Check every 30 seconds |     checkInterval: 30000, // Check every 30 seconds | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   private config: INupstConfig; |   private config: INupstConfig; | ||||||
|   private snmp: NupstSnmp; |   private snmp: NupstSnmp; | ||||||
|   private isRunning: boolean = false; |   private isRunning: boolean = false; | ||||||
|  |   private upsStatus: Map<string, IUpsStatus> = new Map(); | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Create a new daemon instance with the given SNMP manager |    * Create a new daemon instance with the given SNMP manager | ||||||
| @@ -80,14 +151,44 @@ export class NupstDaemon { | |||||||
|  |  | ||||||
|       // Read and parse config |       // Read and parse config | ||||||
|       const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8'); |       const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8'); | ||||||
|       this.config = JSON.parse(configData); |       const parsedConfig = JSON.parse(configData); | ||||||
|  |  | ||||||
|  |       // Handle legacy configuration format | ||||||
|  |       if (!parsedConfig.upsDevices && parsedConfig.snmp) { | ||||||
|  |         // Convert legacy format to new format | ||||||
|  |         this.config = { | ||||||
|  |           upsDevices: [ | ||||||
|  |             { | ||||||
|  |               id: 'default', | ||||||
|  |               name: 'Default UPS', | ||||||
|  |               snmp: parsedConfig.snmp, | ||||||
|  |               thresholds: parsedConfig.thresholds, | ||||||
|  |               groups: [], | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |           groups: [], | ||||||
|  |           checkInterval: parsedConfig.checkInterval, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         logger.log('Legacy configuration format detected. Converting to multi-UPS format.'); | ||||||
|  |  | ||||||
|  |         // Save the new format | ||||||
|  |         await this.saveConfig(this.config); | ||||||
|  |       } else { | ||||||
|  |         this.config = parsedConfig; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       return this.config; |       return this.config; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       if (error.message.includes('No configuration found')) { |       if ( | ||||||
|  |         error instanceof Error && error.message && error.message.includes('No configuration found') | ||||||
|  |       ) { | ||||||
|         throw error; // Re-throw the no configuration error |         throw error; // Re-throw the no configuration error | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.logConfigError(`Error loading configuration: ${error.message}`); |       this.logConfigError( | ||||||
|  |         `Error loading configuration: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|       throw new Error('Failed to load configuration'); |       throw new Error('Failed to load configuration'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -95,7 +196,7 @@ export class NupstDaemon { | |||||||
|   /** |   /** | ||||||
|    * Save configuration to file |    * Save configuration to file | ||||||
|    */ |    */ | ||||||
|   public async saveConfig(config: INupstConfig): Promise<void> { |   public saveConfig(config: INupstConfig): void { | ||||||
|     try { |     try { | ||||||
|       const configDir = path.dirname(this.CONFIG_PATH); |       const configDir = path.dirname(this.CONFIG_PATH); | ||||||
|       if (!fs.existsSync(configDir)) { |       if (!fs.existsSync(configDir)) { | ||||||
| @@ -118,8 +219,8 @@ export class NupstDaemon { | |||||||
|   private logConfigError(message: string): void { |   private logConfigError(message: string): void { | ||||||
|     console.error('┌─ Configuration Error ─────────────────────┐'); |     console.error('┌─ Configuration Error ─────────────────────┐'); | ||||||
|     console.error(`│ ${message}`); |     console.error(`│ ${message}`); | ||||||
|     console.error('│ Please run \'nupst setup\' first to create a configuration.'); |     console.error("│ Please run 'nupst setup' first to create a configuration."); | ||||||
|     console.error('└──────────────────────────────────────────┘'); |     console.error('└───────────────────────────────────────────┘'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -141,11 +242,11 @@ export class NupstDaemon { | |||||||
|    */ |    */ | ||||||
|   public async start(): Promise<void> { |   public async start(): Promise<void> { | ||||||
|     if (this.isRunning) { |     if (this.isRunning) { | ||||||
|       console.log('Daemon is already running'); |       logger.log('Daemon is already running'); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     console.log('Starting NUPST daemon...'); |     logger.log('Starting NUPST daemon...'); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       // Load configuration - this will throw an error if config doesn't exist |       // Load configuration - this will throw an error if config doesn't exist | ||||||
| @@ -156,48 +257,92 @@ export class NupstDaemon { | |||||||
|       this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup |       this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup | ||||||
|  |  | ||||||
|       // Check for updates in the background |       // Check for updates in the background | ||||||
|       this.snmp.getNupst().checkForUpdates().then(updateAvailable => { |       this.snmp.getNupst().checkForUpdates().then((updateAvailable: boolean) => { | ||||||
|         if (updateAvailable) { |         if (updateAvailable) { | ||||||
|           const updateStatus = this.snmp.getNupst().getUpdateStatus(); |           const updateStatus = this.snmp.getNupst().getUpdateStatus(); | ||||||
|           console.log('┌─ Update Available ───────────────────────┐'); |           const boxWidth = 45; | ||||||
|           console.log(`│ Current Version: ${updateStatus.currentVersion}`); |           logger.logBoxTitle('Update Available', boxWidth); | ||||||
|           console.log(`│ Latest Version: ${updateStatus.latestVersion}`); |           logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`); | ||||||
|           console.log('│ Run "sudo nupst update" to update'); |           logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`); | ||||||
|           console.log('└──────────────────────────────────────────┘'); |           logger.logBoxLine('Run "sudo nupst update" to update'); | ||||||
|  |           logger.logBoxEnd(); | ||||||
|         } |         } | ||||||
|       }).catch(() => {}); // Ignore errors checking for updates |       }).catch(() => {}); // Ignore errors checking for updates | ||||||
|  |  | ||||||
|  |       // Initialize UPS status tracking | ||||||
|  |       this.initializeUpsStatus(); | ||||||
|  |  | ||||||
|       // Start UPS monitoring |       // Start UPS monitoring | ||||||
|       this.isRunning = true; |       this.isRunning = true; | ||||||
|       await this.monitor(); |       await this.monitor(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       this.isRunning = false; |       this.isRunning = false; | ||||||
|       console.error(`Daemon failed to start: ${error.message}`); |       logger.error( | ||||||
|  |         `Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|       process.exit(1); // Exit with error |       process.exit(1); // Exit with error | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Initialize UPS status tracking for all UPS devices | ||||||
|  |    */ | ||||||
|  |   private initializeUpsStatus(): void { | ||||||
|  |     this.upsStatus.clear(); | ||||||
|  |  | ||||||
|  |     if (this.config.upsDevices && this.config.upsDevices.length > 0) { | ||||||
|  |       for (const ups of this.config.upsDevices) { | ||||||
|  |         this.upsStatus.set(ups.id, { | ||||||
|  |           id: ups.id, | ||||||
|  |           name: ups.name, | ||||||
|  |           powerStatus: 'unknown', | ||||||
|  |           batteryCapacity: 100, | ||||||
|  |           batteryRuntime: 999, // High value as default | ||||||
|  |           lastStatusChange: Date.now(), | ||||||
|  |           lastCheckTime: 0, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       logger.log(`Initialized status tracking for ${this.config.upsDevices.length} UPS devices`); | ||||||
|  |     } else { | ||||||
|  |       logger.error('No UPS devices found in configuration'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Log the loaded configuration settings |    * Log the loaded configuration settings | ||||||
|    */ |    */ | ||||||
|   private logConfigLoaded(): void { |   private logConfigLoaded(): void { | ||||||
|     console.log('┌─ Configuration Loaded ─────────────────────┐'); |     const boxWidth = 50; | ||||||
|     console.log('│ SNMP Settings:'); |     logger.logBoxTitle('Configuration Loaded', boxWidth); | ||||||
|     console.log(`│   Host: ${this.config.snmp.host}`); |  | ||||||
|     console.log(`│   Port: ${this.config.snmp.port}`); |     if (this.config.upsDevices && this.config.upsDevices.length > 0) { | ||||||
|     console.log(`│   Version: ${this.config.snmp.version}`); |       logger.logBoxLine(`UPS Devices: ${this.config.upsDevices.length}`); | ||||||
|     console.log('│ Thresholds:'); |       for (const ups of this.config.upsDevices) { | ||||||
|     console.log(`│   Battery: ${this.config.thresholds.battery}%`); |         logger.logBoxLine(`  - ${ups.name} (${ups.id}): ${ups.snmp.host}:${ups.snmp.port}`); | ||||||
|     console.log(`│   Runtime: ${this.config.thresholds.runtime} minutes`); |       } | ||||||
|     console.log(`│ Check Interval: ${this.config.checkInterval / 1000} seconds`); |     } else { | ||||||
|     console.log('└──────────────────────────────────────────┘'); |       logger.logBoxLine('No UPS devices configured'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.config.groups && this.config.groups.length > 0) { | ||||||
|  |       logger.logBoxLine(`Groups: ${this.config.groups.length}`); | ||||||
|  |       for (const group of this.config.groups) { | ||||||
|  |         logger.logBoxLine(`  - ${group.name} (${group.id}): ${group.mode} mode`); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       logger.logBoxLine('No Groups configured'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`); | ||||||
|  |     logger.logBoxEnd(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Stop the monitoring daemon |    * Stop the monitoring daemon | ||||||
|    */ |    */ | ||||||
|   public stop(): void { |   public stop(): void { | ||||||
|     console.log('Stopping NUPST daemon...'); |     logger.log('Stopping NUPST daemon...'); | ||||||
|     this.isRunning = false; |     this.isRunning = false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -205,87 +350,550 @@ export class NupstDaemon { | |||||||
|    * Monitor the UPS status and trigger shutdown when necessary |    * Monitor the UPS status and trigger shutdown when necessary | ||||||
|    */ |    */ | ||||||
|   private async monitor(): Promise<void> { |   private async monitor(): Promise<void> { | ||||||
|     console.log('Starting UPS monitoring...'); |     logger.log('Starting UPS monitoring...'); | ||||||
|  |  | ||||||
|  |     if (!this.config.upsDevices || this.config.upsDevices.length === 0) { | ||||||
|  |       logger.error('No UPS devices found in configuration. Monitoring stopped.'); | ||||||
|  |       this.isRunning = false; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown'; |  | ||||||
|     let lastLogTime = 0; // Track when we last logged status |     let lastLogTime = 0; // Track when we last logged status | ||||||
|     const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms) |     const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms) | ||||||
|  |  | ||||||
|     // Monitor continuously |     // Monitor continuously | ||||||
|     while (this.isRunning) { |     while (this.isRunning) { | ||||||
|       try { |       try { | ||||||
|         const status = await this.snmp.getUpsStatus(this.config.snmp); |         // Check all UPS devices | ||||||
|         const currentTime = Date.now(); |         await this.checkAllUpsDevices(); | ||||||
|         const shouldLogStatus = (currentTime - lastLogTime) >= LOG_INTERVAL; |  | ||||||
|  |  | ||||||
|         // Log status changes |         // Log periodic status update | ||||||
|         if (status.powerStatus !== lastStatus) { |         const currentTime = Date.now(); | ||||||
|           console.log('┌──────────────────────────────────────────┐'); |         if (currentTime - lastLogTime >= LOG_INTERVAL) { | ||||||
|           console.log(`│ Power status changed: ${lastStatus} → ${status.powerStatus}`); |           this.logAllUpsStatus(); | ||||||
|           console.log('└──────────────────────────────────────────┘'); |  | ||||||
|           lastStatus = status.powerStatus; |  | ||||||
|           lastLogTime = currentTime; // Reset log timer when status changes |  | ||||||
|         } |  | ||||||
|         // Log status periodically (at least every 5 minutes) |  | ||||||
|         else if (shouldLogStatus) { |  | ||||||
|           const timestamp = new Date().toISOString(); |  | ||||||
|           console.log('┌──────────────────────────────────────────┐'); |  | ||||||
|           console.log(`│ [${timestamp}] Periodic Status Update`); |  | ||||||
|           console.log(`│ Power Status: ${status.powerStatus}`); |  | ||||||
|           console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`); |  | ||||||
|           console.log('└──────────────────────────────────────────┘'); |  | ||||||
|           lastLogTime = currentTime; |           lastLogTime = currentTime; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Handle battery power status |         // Check if shutdown is required based on group configurations | ||||||
|         if (status.powerStatus === 'onBattery') { |         await this.evaluateGroupShutdownConditions(); | ||||||
|           await this.handleOnBatteryStatus(status); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Wait before next check |         // Wait before next check | ||||||
|         await this.sleep(this.config.checkInterval); |         await this.sleep(this.config.checkInterval); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         console.error('Error during UPS monitoring:', error); |         logger.error( | ||||||
|  |           `Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |         ); | ||||||
|         await this.sleep(this.config.checkInterval); |         await this.sleep(this.config.checkInterval); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     console.log('UPS monitoring stopped'); |     logger.log('UPS monitoring stopped'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Handle UPS status when running on battery |    * Check status of all UPS devices | ||||||
|    */ |    */ | ||||||
|   private async handleOnBatteryStatus(status: {  |   private async checkAllUpsDevices(): Promise<void> { | ||||||
|     powerStatus: string, |     for (const ups of this.config.upsDevices) { | ||||||
|     batteryCapacity: number, |       try { | ||||||
|     batteryRuntime: number |         const upsStatus = this.upsStatus.get(ups.id); | ||||||
|   }): Promise<void> { |         if (!upsStatus) { | ||||||
|     console.log('┌─ UPS Status ───────────────────────────────┐'); |           // Initialize status for this UPS if not exists | ||||||
|     console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min │`); |           this.upsStatus.set(ups.id, { | ||||||
|     console.log('└──────────────────────────────────────────┘'); |             id: ups.id, | ||||||
|  |             name: ups.name, | ||||||
|  |             powerStatus: 'unknown', | ||||||
|  |             batteryCapacity: 100, | ||||||
|  |             batteryRuntime: 999, | ||||||
|  |             lastStatusChange: Date.now(), | ||||||
|  |             lastCheckTime: 0, | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|     // Check battery threshold |         // Check UPS status | ||||||
|     if (status.batteryCapacity < this.config.thresholds.battery) { |         const status = await this.snmp.getUpsStatus(ups.snmp); | ||||||
|       console.log('⚠️ WARNING: Battery capacity below threshold'); |         const currentTime = Date.now(); | ||||||
|       console.log(`Current: ${status.batteryCapacity}% | Threshold: ${this.config.thresholds.battery}%`); |  | ||||||
|       await this.snmp.initiateShutdown('Battery capacity below threshold'); |         // Get the current status from the map | ||||||
|  |         const currentStatus = this.upsStatus.get(ups.id); | ||||||
|  |  | ||||||
|  |         // Update status with new values | ||||||
|  |         const updatedStatus: IUpsStatus = { | ||||||
|  |           id: ups.id, | ||||||
|  |           name: ups.name, | ||||||
|  |           powerStatus: status.powerStatus, | ||||||
|  |           batteryCapacity: status.batteryCapacity, | ||||||
|  |           batteryRuntime: status.batteryRuntime, | ||||||
|  |           lastCheckTime: currentTime, | ||||||
|  |           lastStatusChange: currentStatus?.lastStatusChange || currentTime, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Check if power status changed | ||||||
|  |         if (currentStatus && currentStatus.powerStatus !== status.powerStatus) { | ||||||
|  |           logger.logBoxTitle(`Power Status Change: ${ups.name}`, 50); | ||||||
|  |           logger.logBoxLine(`Status changed: ${currentStatus.powerStatus} → ${status.powerStatus}`); | ||||||
|  |           logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |           updatedStatus.lastStatusChange = currentTime; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Update the status in the map | ||||||
|  |         this.upsStatus.set(ups.id, updatedStatus); | ||||||
|  |       } catch (error) { | ||||||
|  |         logger.error( | ||||||
|  |           `Error checking UPS ${ups.name} (${ups.id}): ${ | ||||||
|  |             error instanceof Error ? error.message : String(error) | ||||||
|  |           }`, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log status of all UPS devices | ||||||
|  |    */ | ||||||
|  |   private logAllUpsStatus(): void { | ||||||
|  |     const timestamp = new Date().toISOString(); | ||||||
|  |     const boxWidth = 60; | ||||||
|  |     logger.logBoxTitle('Periodic Status Update', boxWidth); | ||||||
|  |     logger.logBoxLine(`Timestamp: ${timestamp}`); | ||||||
|  |     logger.logBoxLine(''); | ||||||
|  |  | ||||||
|  |     for (const [id, status] of this.upsStatus.entries()) { | ||||||
|  |       logger.logBoxLine(`UPS: ${status.name} (${id})`); | ||||||
|  |       logger.logBoxLine(`  Power Status: ${status.powerStatus}`); | ||||||
|  |       logger.logBoxLine( | ||||||
|  |         `  Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`, | ||||||
|  |       ); | ||||||
|  |       logger.logBoxLine(''); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.logBoxEnd(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Evaluate if shutdown is required based on group configurations | ||||||
|  |    */ | ||||||
|  |   private async evaluateGroupShutdownConditions(): Promise<void> { | ||||||
|  |     if (!this.config.groups || this.config.groups.length === 0) { | ||||||
|  |       // No groups defined, check individual UPS conditions | ||||||
|  |       for (const [id, status] of this.upsStatus.entries()) { | ||||||
|  |         if (status.powerStatus === 'onBattery') { | ||||||
|  |           // Find the UPS config | ||||||
|  |           const ups = this.config.upsDevices.find((u) => u.id === id); | ||||||
|  |           if (ups) { | ||||||
|  |             await this.evaluateUpsShutdownCondition(ups, status); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Check runtime threshold |     // Evaluate each group | ||||||
|     if (status.batteryRuntime < this.config.thresholds.runtime) { |     for (const group of this.config.groups) { | ||||||
|       console.log('⚠️ WARNING: Runtime below threshold'); |       // Find all UPS devices in this group | ||||||
|       console.log(`Current: ${status.batteryRuntime} min | Threshold: ${this.config.thresholds.runtime} min`); |       const upsDevicesInGroup = this.config.upsDevices.filter((ups) => | ||||||
|       await this.snmp.initiateShutdown('Runtime below threshold'); |         ups.groups && ups.groups.includes(group.id) | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (upsDevicesInGroup.length === 0) { | ||||||
|  |         // No UPS devices in this group | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (group.mode === 'redundant') { | ||||||
|  |         // Redundant mode: only shutdown if ALL UPS devices in the group are in critical condition | ||||||
|  |         await this.evaluateRedundantGroup(group, upsDevicesInGroup); | ||||||
|  |       } else { | ||||||
|  |         // Non-redundant mode: shutdown if ANY UPS device in the group is in critical condition | ||||||
|  |         await this.evaluateNonRedundantGroup(group, upsDevicesInGroup); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Evaluate a redundant group for shutdown conditions | ||||||
|  |    * In redundant mode, we only shut down if ALL UPS devices are in critical condition | ||||||
|  |    */ | ||||||
|  |   private async evaluateRedundantGroup( | ||||||
|  |     group: IGroupConfig, | ||||||
|  |     upsDevices: IUpsConfig[], | ||||||
|  |   ): Promise<void> { | ||||||
|  |     // Count UPS devices on battery and in critical condition | ||||||
|  |     let upsOnBattery = 0; | ||||||
|  |     let upsInCriticalCondition = 0; | ||||||
|  |  | ||||||
|  |     for (const ups of upsDevices) { | ||||||
|  |       const status = this.upsStatus.get(ups.id); | ||||||
|  |       if (!status) continue; | ||||||
|  |  | ||||||
|  |       if (status.powerStatus === 'onBattery') { | ||||||
|  |         upsOnBattery++; | ||||||
|  |  | ||||||
|  |         // Check if this UPS is in critical condition | ||||||
|  |         if ( | ||||||
|  |           status.batteryCapacity < ups.thresholds.battery || | ||||||
|  |           status.batteryRuntime < ups.thresholds.runtime | ||||||
|  |         ) { | ||||||
|  |           upsInCriticalCondition++; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // All UPS devices must be online for a redundant group to be considered healthy | ||||||
|  |     const allUpsCount = upsDevices.length; | ||||||
|  |  | ||||||
|  |     // If all UPS are on battery and in critical condition, shutdown | ||||||
|  |     if (upsOnBattery === allUpsCount && upsInCriticalCondition === allUpsCount) { | ||||||
|  |       logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50); | ||||||
|  |       logger.logBoxLine(`Mode: Redundant`); | ||||||
|  |       logger.logBoxLine(`All ${allUpsCount} UPS devices in critical condition`); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |       await this.initiateShutdown( | ||||||
|  |         `All UPS devices in redundant group "${group.name}" in critical condition`, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Evaluate a non-redundant group for shutdown conditions | ||||||
|  |    * In non-redundant mode, we shut down if ANY UPS device is in critical condition | ||||||
|  |    */ | ||||||
|  |   private async evaluateNonRedundantGroup( | ||||||
|  |     group: IGroupConfig, | ||||||
|  |     upsDevices: IUpsConfig[], | ||||||
|  |   ): Promise<void> { | ||||||
|  |     for (const ups of upsDevices) { | ||||||
|  |       const status = this.upsStatus.get(ups.id); | ||||||
|  |       if (!status) continue; | ||||||
|  |  | ||||||
|  |       if (status.powerStatus === 'onBattery') { | ||||||
|  |         // Check if this UPS is in critical condition | ||||||
|  |         if ( | ||||||
|  |           status.batteryCapacity < ups.thresholds.battery || | ||||||
|  |           status.batteryRuntime < ups.thresholds.runtime | ||||||
|  |         ) { | ||||||
|  |           logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50); | ||||||
|  |           logger.logBoxLine(`Mode: Non-Redundant`); | ||||||
|  |           logger.logBoxLine(`UPS ${ups.name} in critical condition`); | ||||||
|  |           logger.logBoxLine( | ||||||
|  |             `Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`, | ||||||
|  |           ); | ||||||
|  |           logger.logBoxLine( | ||||||
|  |             `Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`, | ||||||
|  |           ); | ||||||
|  |           logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |           await this.initiateShutdown( | ||||||
|  |             `UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`, | ||||||
|  |           ); | ||||||
|  |           return; // Exit after initiating shutdown | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Evaluate an individual UPS for shutdown conditions | ||||||
|  |    */ | ||||||
|  |   private async evaluateUpsShutdownCondition(ups: IUpsConfig, status: IUpsStatus): Promise<void> { | ||||||
|  |     // Only evaluate UPS devices not in any group | ||||||
|  |     if (ups.groups && ups.groups.length > 0) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Check threshold conditions | ||||||
|  |     if ( | ||||||
|  |       status.batteryCapacity < ups.thresholds.battery || | ||||||
|  |       status.batteryRuntime < ups.thresholds.runtime | ||||||
|  |     ) { | ||||||
|  |       logger.logBoxTitle(`UPS Shutdown Required: ${ups.name}`, 50); | ||||||
|  |       logger.logBoxLine( | ||||||
|  |         `Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`, | ||||||
|  |       ); | ||||||
|  |       logger.logBoxLine( | ||||||
|  |         `Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`, | ||||||
|  |       ); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |       await this.initiateShutdown(`UPS "${ups.name}" battery or runtime below threshold`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Initiate system shutdown with UPS monitoring during shutdown | ||||||
|  |    * @param reason Reason for shutdown | ||||||
|  |    */ | ||||||
|  |   public async initiateShutdown(reason: string): Promise<void> { | ||||||
|  |     logger.log(`Initiating system shutdown due to: ${reason}`); | ||||||
|  |  | ||||||
|  |     // Set a longer delay for shutdown to allow VMs and services to close | ||||||
|  |     const shutdownDelayMinutes = 5; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Find shutdown command in common system paths | ||||||
|  |       const shutdownPaths = [ | ||||||
|  |         '/sbin/shutdown', | ||||||
|  |         '/usr/sbin/shutdown', | ||||||
|  |         '/bin/shutdown', | ||||||
|  |         '/usr/bin/shutdown', | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       let shutdownCmd = ''; | ||||||
|  |       for (const path of shutdownPaths) { | ||||||
|  |         try { | ||||||
|  |           if (fs.existsSync(path)) { | ||||||
|  |             shutdownCmd = path; | ||||||
|  |             logger.log(`Found shutdown command at: ${shutdownCmd}`); | ||||||
|  |             break; | ||||||
|  |           } | ||||||
|  |         } catch (e) { | ||||||
|  |           // Continue checking other paths | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (shutdownCmd) { | ||||||
|  |         // Execute shutdown command with delay to allow for VM graceful shutdown | ||||||
|  |         logger.log( | ||||||
|  |           `Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`, | ||||||
|  |         ); | ||||||
|  |         const { stdout } = await execFileAsync(shutdownCmd, [ | ||||||
|  |           '-h', | ||||||
|  |           `+${shutdownDelayMinutes}`, | ||||||
|  |           `UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`, | ||||||
|  |         ]); | ||||||
|  |         logger.log(`Shutdown initiated: ${stdout}`); | ||||||
|  |         logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); | ||||||
|  |       } else { | ||||||
|  |         // Try using the PATH to find shutdown | ||||||
|  |         try { | ||||||
|  |           logger.log('Shutdown command not found in common paths, trying via PATH...'); | ||||||
|  |           const { stdout } = await execAsync( | ||||||
|  |             `shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, | ||||||
|  |             { | ||||||
|  |               env: process.env, // Pass the current environment | ||||||
|  |             }, | ||||||
|  |           ); | ||||||
|  |           logger.log(`Shutdown initiated: ${stdout}`); | ||||||
|  |         } catch (e) { | ||||||
|  |           throw new Error( | ||||||
|  |             `Shutdown command not found: ${e instanceof Error ? e.message : String(e)}`, | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Monitor UPS during shutdown and force immediate shutdown if battery gets too low | ||||||
|  |       logger.log('Monitoring UPS during shutdown process...'); | ||||||
|  |       await this.monitorDuringShutdown(); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error(`Failed to initiate shutdown: ${error}`); | ||||||
|  |  | ||||||
|  |       // Try alternative shutdown methods | ||||||
|  |       const alternatives = [ | ||||||
|  |         { cmd: 'poweroff', args: ['--force'] }, | ||||||
|  |         { cmd: 'halt', args: ['-p'] }, | ||||||
|  |         { cmd: 'systemctl', args: ['poweroff'] }, | ||||||
|  |         { cmd: 'reboot', args: ['-p'] }, // Some systems allow reboot -p for power off | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       for (const alt of alternatives) { | ||||||
|  |         try { | ||||||
|  |           // First check if command exists in common system paths | ||||||
|  |           const paths = [ | ||||||
|  |             `/sbin/${alt.cmd}`, | ||||||
|  |             `/usr/sbin/${alt.cmd}`, | ||||||
|  |             `/bin/${alt.cmd}`, | ||||||
|  |             `/usr/bin/${alt.cmd}`, | ||||||
|  |           ]; | ||||||
|  |  | ||||||
|  |           let cmdPath = ''; | ||||||
|  |           for (const path of paths) { | ||||||
|  |             if (fs.existsSync(path)) { | ||||||
|  |               cmdPath = path; | ||||||
|  |               break; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           if (cmdPath) { | ||||||
|  |             logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`); | ||||||
|  |             await execFileAsync(cmdPath, alt.args); | ||||||
|  |             return; // Exit if successful | ||||||
|  |           } else { | ||||||
|  |             // Try using PATH environment | ||||||
|  |             logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`); | ||||||
|  |             await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, { | ||||||
|  |               env: process.env, // Pass the current environment | ||||||
|  |             }); | ||||||
|  |             return; // Exit if successful | ||||||
|  |           } | ||||||
|  |         } catch (altError) { | ||||||
|  |           logger.error(`Alternative method ${alt.cmd} failed: ${altError}`); | ||||||
|  |           // Continue to next method | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       logger.error('All shutdown methods failed'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Monitor UPS during system shutdown | ||||||
|  |    * Force immediate shutdown if any UPS gets critically low | ||||||
|  |    */ | ||||||
|  |   private async monitorDuringShutdown(): Promise<void> { | ||||||
|  |     const EMERGENCY_RUNTIME_THRESHOLD = 5; // 5 minutes remaining is critical | ||||||
|  |     const CHECK_INTERVAL = 30000; // Check every 30 seconds during shutdown | ||||||
|  |     const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring | ||||||
|  |     const startTime = Date.now(); | ||||||
|  |  | ||||||
|  |     logger.log( | ||||||
|  |       `Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Continue monitoring until max monitoring time is reached | ||||||
|  |     while (Date.now() - startTime < MAX_MONITORING_TIME) { | ||||||
|  |       try { | ||||||
|  |         logger.log('Checking UPS status during shutdown...'); | ||||||
|  |  | ||||||
|  |         // Check all UPS devices | ||||||
|  |         for (const ups of this.config.upsDevices) { | ||||||
|  |           try { | ||||||
|  |             const status = await this.snmp.getUpsStatus(ups.snmp); | ||||||
|  |  | ||||||
|  |             logger.log( | ||||||
|  |               `UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`, | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             // If any UPS battery runtime gets critically low, force immediate shutdown | ||||||
|  |             if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) { | ||||||
|  |               logger.logBoxTitle('EMERGENCY SHUTDOWN', 50); | ||||||
|  |               logger.logBoxLine( | ||||||
|  |                 `UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`, | ||||||
|  |               ); | ||||||
|  |               logger.logBoxLine('Forcing immediate shutdown!'); | ||||||
|  |               logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |               // Force immediate shutdown | ||||||
|  |               await this.forceImmediateShutdown(); | ||||||
|  |               return; | ||||||
|  |             } | ||||||
|  |           } catch (upsError) { | ||||||
|  |             logger.error( | ||||||
|  |               `Error checking UPS ${ups.name} during shutdown: ${ | ||||||
|  |                 upsError instanceof Error ? upsError.message : String(upsError) | ||||||
|  |               }`, | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Wait before checking again | ||||||
|  |         await this.sleep(CHECK_INTERVAL); | ||||||
|  |       } catch (error) { | ||||||
|  |         logger.error( | ||||||
|  |           `Error monitoring UPS during shutdown: ${ | ||||||
|  |             error instanceof Error ? error.message : String(error) | ||||||
|  |           }`, | ||||||
|  |         ); | ||||||
|  |         await this.sleep(CHECK_INTERVAL); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.log('UPS monitoring during shutdown completed'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Force an immediate system shutdown | ||||||
|  |    */ | ||||||
|  |   private async forceImmediateShutdown(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Find shutdown command in common system paths | ||||||
|  |       const shutdownPaths = [ | ||||||
|  |         '/sbin/shutdown', | ||||||
|  |         '/usr/sbin/shutdown', | ||||||
|  |         '/bin/shutdown', | ||||||
|  |         '/usr/bin/shutdown', | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       let shutdownCmd = ''; | ||||||
|  |       for (const path of shutdownPaths) { | ||||||
|  |         if (fs.existsSync(path)) { | ||||||
|  |           shutdownCmd = path; | ||||||
|  |           logger.log(`Found shutdown command at: ${shutdownCmd}`); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (shutdownCmd) { | ||||||
|  |         logger.log(`Executing emergency shutdown: ${shutdownCmd} -h now`); | ||||||
|  |         await execFileAsync(shutdownCmd, [ | ||||||
|  |           '-h', | ||||||
|  |           'now', | ||||||
|  |           'EMERGENCY: UPS battery critically low, shutting down NOW', | ||||||
|  |         ]); | ||||||
|  |       } else { | ||||||
|  |         // Try using the PATH to find shutdown | ||||||
|  |         logger.log('Shutdown command not found in common paths, trying via PATH...'); | ||||||
|  |         await execAsync( | ||||||
|  |           'shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', | ||||||
|  |           { | ||||||
|  |             env: process.env, // Pass the current environment | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error('Emergency shutdown failed, trying alternative methods...'); | ||||||
|  |  | ||||||
|  |       // Try alternative shutdown methods in sequence | ||||||
|  |       const alternatives = [ | ||||||
|  |         { cmd: 'poweroff', args: ['--force'] }, | ||||||
|  |         { cmd: 'halt', args: ['-p'] }, | ||||||
|  |         { cmd: 'systemctl', args: ['poweroff'] }, | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       for (const alt of alternatives) { | ||||||
|  |         try { | ||||||
|  |           // Check common paths | ||||||
|  |           const paths = [ | ||||||
|  |             `/sbin/${alt.cmd}`, | ||||||
|  |             `/usr/sbin/${alt.cmd}`, | ||||||
|  |             `/bin/${alt.cmd}`, | ||||||
|  |             `/usr/bin/${alt.cmd}`, | ||||||
|  |           ]; | ||||||
|  |  | ||||||
|  |           let cmdPath = ''; | ||||||
|  |           for (const path of paths) { | ||||||
|  |             if (fs.existsSync(path)) { | ||||||
|  |               cmdPath = path; | ||||||
|  |               break; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           if (cmdPath) { | ||||||
|  |             logger.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`); | ||||||
|  |             await execFileAsync(cmdPath, alt.args); | ||||||
|  |             return; // Exit if successful | ||||||
|  |           } else { | ||||||
|  |             // Try using PATH | ||||||
|  |             logger.log(`Emergency: trying ${alt.cmd} via PATH`); | ||||||
|  |             await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, { | ||||||
|  |               env: process.env, | ||||||
|  |             }); | ||||||
|  |             return; // Exit if successful | ||||||
|  |           } | ||||||
|  |         } catch (altError) { | ||||||
|  |           // Continue to next method | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       logger.error('All emergency shutdown methods failed'); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Sleep for the specified milliseconds |    * Sleep for the specified milliseconds | ||||||
|    */ |    */ | ||||||
|   private sleep(ms: number): Promise<void> { |   private sleep(ms: number): Promise<void> { | ||||||
|     return new Promise(resolve => setTimeout(resolve, ms)); |     return new Promise((resolve) => setTimeout(resolve, ms)); | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										1
									
								
								ts/helpers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								ts/helpers/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './shortid.ts'; | ||||||
							
								
								
									
										22
									
								
								ts/helpers/shortid.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ts/helpers/shortid.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | /** | ||||||
|  |  * Generate a short unique ID of 6 alphanumeric characters | ||||||
|  |  * @returns A 6-character alphanumeric string | ||||||
|  |  */ | ||||||
|  | export function shortId(): string { | ||||||
|  |   // Define the character set: a-z, A-Z, 0-9 | ||||||
|  |   const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; | ||||||
|  |  | ||||||
|  |   // Generate cryptographically secure random values | ||||||
|  |   const randomValues = new Uint8Array(6); | ||||||
|  |   crypto.getRandomValues(randomValues); | ||||||
|  |  | ||||||
|  |   // Map each random value to a character in our set | ||||||
|  |   let result = ''; | ||||||
|  |   for (let i = 0; i < 6; i++) { | ||||||
|  |     // Use modulo to map the random byte to a character index | ||||||
|  |     const index = randomValues[i] % chars.length; | ||||||
|  |     result += chars[index]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return result; | ||||||
|  | } | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| #!/usr/bin/env node | #!/usr/bin/env node | ||||||
|  |  | ||||||
| import { NupstCli } from './cli.js'; | import { NupstCli } from './cli.ts'; | ||||||
|  | import { logger } from './logger.ts'; | ||||||
|  | import process from 'node:process'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Main entry point for NUPST |  * Main entry point for NUPST | ||||||
| @@ -12,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) => { | ||||||
|   console.error('Error:', error); |   logger.error(`Error: ${error}`); | ||||||
|   process.exit(1); |   process.exit(1); | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										147
									
								
								ts/logger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								ts/logger.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | /** | ||||||
|  |  * A simple logger class that provides consistent formatting for log messages | ||||||
|  |  * including support for logboxes with title, lines, and closing | ||||||
|  |  */ | ||||||
|  | export class Logger { | ||||||
|  |   private currentBoxWidth: number | null = null; | ||||||
|  |   private static instance: Logger; | ||||||
|  |  | ||||||
|  |   /** Default width to use when no width is specified */ | ||||||
|  |   private readonly DEFAULT_WIDTH = 60; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Creates a new Logger instance | ||||||
|  |    */ | ||||||
|  |   constructor() { | ||||||
|  |     this.currentBoxWidth = null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get the singleton logger instance | ||||||
|  |    * @returns The singleton logger instance | ||||||
|  |    */ | ||||||
|  |   public static getInstance(): Logger { | ||||||
|  |     if (!Logger.instance) { | ||||||
|  |       Logger.instance = new Logger(); | ||||||
|  |     } | ||||||
|  |     return Logger.instance; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a message | ||||||
|  |    * @param message Message to log | ||||||
|  |    */ | ||||||
|  |   public log(message: string): void { | ||||||
|  |     console.log(message); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log an error message | ||||||
|  |    * @param message Error message to log | ||||||
|  |    */ | ||||||
|  |   public error(message: string): void { | ||||||
|  |     console.error(message); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a warning message with a warning emoji | ||||||
|  |    * @param message Warning message to log | ||||||
|  |    */ | ||||||
|  |   public warn(message: string): void { | ||||||
|  |     console.warn(`⚠️ ${message}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a success message with a checkmark | ||||||
|  |    * @param message Success message to log | ||||||
|  |    */ | ||||||
|  |   public success(message: string): void { | ||||||
|  |     console.log(`✓ ${message}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a logbox title and set the current box width | ||||||
|  |    * @param title Title of the logbox | ||||||
|  |    * @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH | ||||||
|  |    */ | ||||||
|  |   public logBoxTitle(title: string, width?: number): void { | ||||||
|  |     this.currentBoxWidth = width || this.DEFAULT_WIDTH; | ||||||
|  |  | ||||||
|  |     // Create the title line with appropriate padding | ||||||
|  |     const paddedTitle = ` ${title} `; | ||||||
|  |     const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length; | ||||||
|  |  | ||||||
|  |     // Title line: ┌─ Title ───┐ | ||||||
|  |     const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`; | ||||||
|  |  | ||||||
|  |     console.log(titleLine); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a logbox line | ||||||
|  |    * @param content Content of the line | ||||||
|  |    * @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH. | ||||||
|  |    */ | ||||||
|  |   public logBoxLine(content: string, width?: number): void { | ||||||
|  |     if (!this.currentBoxWidth && !width) { | ||||||
|  |       // No current width and no width provided, use default width | ||||||
|  |       this.logBoxTitle('', this.DEFAULT_WIDTH); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; | ||||||
|  |  | ||||||
|  |     // Calculate the available space for content | ||||||
|  |     const availableSpace = boxWidth - 2; // Account for left and right borders | ||||||
|  |  | ||||||
|  |     if (content.length <= availableSpace - 1) { | ||||||
|  |       // If content fits with at least one space for the right border stripe | ||||||
|  |       const padding = availableSpace - content.length - 1; | ||||||
|  |       console.log(`│ ${content}${' '.repeat(padding)}│`); | ||||||
|  |     } else { | ||||||
|  |       // Content is too long, let it flow out of boundaries. | ||||||
|  |       console.log(`│ ${content}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a logbox end | ||||||
|  |    * @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH. | ||||||
|  |    */ | ||||||
|  |   public logBoxEnd(width?: number): void { | ||||||
|  |     const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; | ||||||
|  |  | ||||||
|  |     // Create the bottom border: └────────┘ | ||||||
|  |     console.log(`└${'─'.repeat(boxWidth - 2)}┘`); | ||||||
|  |  | ||||||
|  |     // Reset the current box width | ||||||
|  |     this.currentBoxWidth = null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a complete logbox with title, content lines, and ending | ||||||
|  |    * @param title Title of the logbox | ||||||
|  |    * @param lines Array of content lines | ||||||
|  |    * @param width Width of the logbox, defaults to DEFAULT_WIDTH | ||||||
|  |    */ | ||||||
|  |   public logBox(title: string, lines: string[], width?: number): void { | ||||||
|  |     this.logBoxTitle(title, width || this.DEFAULT_WIDTH); | ||||||
|  |  | ||||||
|  |     for (const line of lines) { | ||||||
|  |       this.logBoxLine(line); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.logBoxEnd(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a divider line | ||||||
|  |    * @param width Width of the divider, defaults to DEFAULT_WIDTH | ||||||
|  |    * @param character Character to use for the divider (default: ─) | ||||||
|  |    */ | ||||||
|  |   public logDivider(width?: number, character: string = '─'): void { | ||||||
|  |     console.log(character.repeat(width || this.DEFAULT_WIDTH)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Export a singleton instance for easy use | ||||||
|  | export const logger = Logger.getInstance(); | ||||||
							
								
								
									
										98
									
								
								ts/nupst.ts
									
									
									
									
									
								
							
							
						
						
									
										98
									
								
								ts/nupst.ts
									
									
									
									
									
								
							| @@ -1,9 +1,12 @@ | |||||||
| import { NupstSnmp } from './snmp.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 { spawn } from 'child_process'; | import { logger } from './logger.ts'; | ||||||
| import * as https from 'https'; | import { UpsHandler } from './cli/ups-handler.ts'; | ||||||
|  | import { GroupHandler } from './cli/group-handler.ts'; | ||||||
|  | import { ServiceHandler } from './cli/service-handler.ts'; | ||||||
|  | import * as https from 'node:https'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Main Nupst class that coordinates all components |  * Main Nupst class that coordinates all components | ||||||
| @@ -13,6 +16,9 @@ export class Nupst { | |||||||
|   private readonly snmp: NupstSnmp; |   private readonly snmp: NupstSnmp; | ||||||
|   private readonly daemon: NupstDaemon; |   private readonly daemon: NupstDaemon; | ||||||
|   private readonly systemd: NupstSystemd; |   private readonly systemd: NupstSystemd; | ||||||
|  |   private readonly upsHandler: UpsHandler; | ||||||
|  |   private readonly groupHandler: GroupHandler; | ||||||
|  |   private readonly serviceHandler: ServiceHandler; | ||||||
|   private updateAvailable: boolean = false; |   private updateAvailable: boolean = false; | ||||||
|   private latestVersion: string = ''; |   private latestVersion: string = ''; | ||||||
|  |  | ||||||
| @@ -20,10 +26,16 @@ export class Nupst { | |||||||
|    * Create a new Nupst instance with all necessary components |    * Create a new Nupst instance with all necessary components | ||||||
|    */ |    */ | ||||||
|   constructor() { |   constructor() { | ||||||
|  |     // Initialize core components | ||||||
|     this.snmp = new NupstSnmp(); |     this.snmp = new NupstSnmp(); | ||||||
|     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 | ||||||
|  |     this.upsHandler = new UpsHandler(this); | ||||||
|  |     this.groupHandler = new GroupHandler(this); | ||||||
|  |     this.serviceHandler = new ServiceHandler(this); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -47,6 +59,27 @@ export class Nupst { | |||||||
|     return this.systemd; |     return this.systemd; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get the UPS handler for UPS management | ||||||
|  |    */ | ||||||
|  |   public getUpsHandler(): UpsHandler { | ||||||
|  |     return this.upsHandler; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get the Group handler for group management | ||||||
|  |    */ | ||||||
|  |   public getGroupHandler(): GroupHandler { | ||||||
|  |     return this.groupHandler; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get the Service handler for service management | ||||||
|  |    */ | ||||||
|  |   public getServiceHandler(): ServiceHandler { | ||||||
|  |     return this.serviceHandler; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Get the current version of NUPST |    * Get the current version of NUPST | ||||||
|    * @returns The current version string |    * @returns The current version string | ||||||
| @@ -70,7 +103,9 @@ export class Nupst { | |||||||
|  |  | ||||||
|       return this.updateAvailable; |       return this.updateAvailable; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error(`Error checking for updates: ${error.message}`); |       logger.error( | ||||||
|  |         `Error checking for updates: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -80,14 +115,14 @@ export class Nupst { | |||||||
|    * @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, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -95,7 +130,7 @@ export class Nupst { | |||||||
|    * 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', | ||||||
| @@ -103,8 +138,8 @@ 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) => { | ||||||
| @@ -143,8 +178,8 @@ 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; | ||||||
| @@ -162,28 +197,33 @@ export class Nupst { | |||||||
|    */ |    */ | ||||||
|   public logVersionInfo(checkForUpdates: boolean = true): void { |   public logVersionInfo(checkForUpdates: boolean = true): void { | ||||||
|     const version = this.getVersion(); |     const version = this.getVersion(); | ||||||
|     console.log('┌─ NUPST Version ────────────────────────┐'); |     const boxWidth = 45; | ||||||
|     console.log(`│ Current Version: ${version}`); |  | ||||||
|  |     logger.logBoxTitle('NUPST Version', boxWidth); | ||||||
|  |     logger.logBoxLine(`Current Version: ${version}`); | ||||||
|  |  | ||||||
|     if (this.updateAvailable && this.latestVersion) { |     if (this.updateAvailable && this.latestVersion) { | ||||||
|       console.log(`│ Update Available: ${this.latestVersion}`); |       logger.logBoxLine(`Update Available: ${this.latestVersion}`); | ||||||
|       console.log('│ Run "sudo nupst update" to update'); |       logger.logBoxLine('Run "sudo nupst update" to update'); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|     } else if (checkForUpdates) { |     } else if (checkForUpdates) { | ||||||
|       console.log('│ Checking for updates...'); |       logger.logBoxLine('Checking for updates...'); | ||||||
|       this.checkForUpdates().then(updateAvailable => { |  | ||||||
|  |       // We can't end the box yet since we're in an async operation | ||||||
|  |       this.checkForUpdates().then((updateAvailable) => { | ||||||
|         if (updateAvailable) { |         if (updateAvailable) { | ||||||
|           console.log(`│ Update Available: ${this.latestVersion}`); |           logger.logBoxLine(`Update Available: ${this.latestVersion}`); | ||||||
|           console.log('│ Run "sudo nupst update" to update'); |           logger.logBoxLine('Run "sudo nupst update" to update'); | ||||||
|         } else { |         } else { | ||||||
|           console.log('│ You are running the latest version'); |           logger.logBoxLine('You are running the latest version'); | ||||||
|         } |         } | ||||||
|         console.log('└──────────────────────────────────────────┘'); |         logger.logBoxEnd(); | ||||||
|       }).catch(() => { |       }).catch(() => { | ||||||
|         console.log('│ Could not check for updates'); |         logger.logBoxLine('Could not check for updates'); | ||||||
|         console.log('└──────────────────────────────────────────┘'); |         logger.logBoxEnd(); | ||||||
|       }); |       }); | ||||||
|     } else { |     } else { | ||||||
|       console.log('└──────────────────────────────────────────┘'); |       logger.logBoxEnd(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Re-export from the snmp module |  | ||||||
|  * This file is kept for backward compatibility |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| export * from './snmp/index.js'; |  | ||||||
| @@ -1,98 +0,0 @@ | |||||||
| /** |  | ||||||
|  * SNMP encoding utilities |  | ||||||
|  * Contains helper methods for encoding SNMP data |  | ||||||
|  */ |  | ||||||
| export class SnmpEncoder { |  | ||||||
|   /** |  | ||||||
|    * Convert OID string to array of integers |  | ||||||
|    * @param oid OID string in dotted notation (e.g. "1.3.6.1.2.1") |  | ||||||
|    * @returns Array of integers representing the OID |  | ||||||
|    */ |  | ||||||
|   public static oidToArray(oid: string): number[] { |  | ||||||
|     return oid.split('.').map(n => parseInt(n, 10)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Encode an SNMP integer |  | ||||||
|    * @param value Integer value to encode |  | ||||||
|    * @returns Buffer containing the encoded integer |  | ||||||
|    */ |  | ||||||
|   public static encodeInteger(value: number): Buffer { |  | ||||||
|     const buf = Buffer.alloc(4); |  | ||||||
|     buf.writeInt32BE(value, 0); |  | ||||||
|      |  | ||||||
|     // Find first non-zero byte |  | ||||||
|     let start = 0; |  | ||||||
|     while (start < 3 && buf[start] === 0) { |  | ||||||
|       start++; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Handle negative values |  | ||||||
|     if (value < 0 && buf[start] === 0) { |  | ||||||
|       start--; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return buf.slice(start); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Encode an OID |  | ||||||
|    * @param oid Array of integers representing the OID |  | ||||||
|    * @returns Buffer containing the encoded OID |  | ||||||
|    */ |  | ||||||
|   public static encodeOID(oid: number[]): Buffer { |  | ||||||
|     // First two numbers are encoded as 40*x+y |  | ||||||
|     let encodedOid = Buffer.from([40 * (oid[0] || 0) + (oid[1] || 0)]); |  | ||||||
|      |  | ||||||
|     // Encode remaining numbers |  | ||||||
|     for (let i = 2; i < oid.length; i++) { |  | ||||||
|       const n = oid[i]; |  | ||||||
|        |  | ||||||
|       if (n < 128) { |  | ||||||
|         // Simple case: number fits in one byte |  | ||||||
|         encodedOid = Buffer.concat([encodedOid, Buffer.from([n])]); |  | ||||||
|       } else { |  | ||||||
|         // Number needs multiple bytes |  | ||||||
|         const bytes = []; |  | ||||||
|         let value = n; |  | ||||||
|          |  | ||||||
|         // Create bytes array in reverse order |  | ||||||
|         do { |  | ||||||
|           bytes.unshift(value & 0x7F); |  | ||||||
|           value >>= 7; |  | ||||||
|         } while (value > 0); |  | ||||||
|          |  | ||||||
|         // Set high bit on all but the last byte |  | ||||||
|         for (let j = 0; j < bytes.length - 1; j++) { |  | ||||||
|           bytes[j] |= 0x80; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         encodedOid = Buffer.concat([encodedOid, Buffer.from(bytes)]); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return encodedOid; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Decode an ASN.1 integer |  | ||||||
|    * @param buffer Buffer containing the encoded integer |  | ||||||
|    * @param offset Offset in the buffer |  | ||||||
|    * @param length Length of the integer in bytes |  | ||||||
|    * @returns Decoded integer value |  | ||||||
|    */ |  | ||||||
|   public static decodeInteger(buffer: Buffer, offset: number, length: number): number { |  | ||||||
|     if (length === 1) { |  | ||||||
|       return buffer[offset]; |  | ||||||
|     } else if (length === 2) { |  | ||||||
|       return buffer.readInt16BE(offset); |  | ||||||
|     } else if (length === 3) { |  | ||||||
|       return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2]; |  | ||||||
|     } else if (length === 4) { |  | ||||||
|       return buffer.readInt32BE(offset); |  | ||||||
|     } else { |  | ||||||
|       // For longer integers, we'll just return a simple value |  | ||||||
|       return buffer[offset]; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -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,12 +1,7 @@ | |||||||
| import { exec } from 'child_process'; | import * as snmp from 'npm:net-snmp@3.20.0'; | ||||||
| import { promisify } from 'util'; | import { Buffer } from 'node:buffer'; | ||||||
| import * as dgram from 'dgram'; | import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts'; | ||||||
| import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js'; | import { UpsOidSets } from './oid-sets.ts'; | ||||||
| import { UpsOidSets } from './oid-sets.js'; |  | ||||||
| import { SnmpPacketCreator } from './packet-creator.js'; |  | ||||||
| import { SnmpPacketParser } from './packet-parser.js'; |  | ||||||
|  |  | ||||||
| const execAsync = promisify(exec); |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for SNMP communication with UPS devices |  * Class for SNMP communication with UPS devices | ||||||
| @@ -17,6 +12,8 @@ export class NupstSnmp { | |||||||
|   private activeOIDs: IOidSet; |   private activeOIDs: IOidSet; | ||||||
|   // Reference to the parent Nupst instance |   // Reference to the parent Nupst instance | ||||||
|   private nupst: any; // Type 'any' to avoid circular dependency |   private nupst: any; // Type 'any' to avoid circular dependency | ||||||
|  |   // Debug mode flag | ||||||
|  |   private debug: boolean = false; | ||||||
|  |  | ||||||
|   // Default SNMP configuration |   // Default SNMP configuration | ||||||
|   private readonly DEFAULT_CONFIG: ISnmpConfig = { |   private readonly DEFAULT_CONFIG: ISnmpConfig = { | ||||||
| @@ -28,13 +25,6 @@ export class NupstSnmp { | |||||||
|     upsModel: 'cyberpower', // Default UPS model |     upsModel: 'cyberpower', // Default UPS model | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // SNMPv3 engine ID and counters |  | ||||||
|   private engineID: Buffer = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); |  | ||||||
|   private engineBoots: number = 0; |  | ||||||
|   private engineTime: number = 0; |  | ||||||
|   private requestID: number = 1; |  | ||||||
|   private debug: boolean = false; // Enable for debug output |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Create a new SNMP manager |    * Create a new SNMP manager | ||||||
|    * @param debug Whether to enable debug mode |    * @param debug Whether to enable debug mode | ||||||
| @@ -60,6 +50,14 @@ export class NupstSnmp { | |||||||
|     return this.nupst; |     return this.nupst; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Enable debug mode | ||||||
|  |    */ | ||||||
|  |   public enableDebug(): void { | ||||||
|  |     this.debug = true; | ||||||
|  |     console.log('SNMP debug mode enabled - detailed logs will be shown'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Set active OID set based on UPS model |    * Set active OID set based on UPS model | ||||||
|    * @param config SNMP configuration |    * @param config SNMP configuration | ||||||
| @@ -84,119 +82,194 @@ export class NupstSnmp { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Enable debug mode |    * Send an SNMP GET request using the net-snmp package | ||||||
|    */ |  | ||||||
|   public enableDebug(): void { |  | ||||||
|     this.debug = true; |  | ||||||
|     console.log('SNMP debug mode enabled - detailed logs will be shown'); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Send an SNMP GET request |  | ||||||
|    * @param oid OID to query |    * @param oid OID to query | ||||||
|    * @param config SNMP configuration |    * @param config SNMP configuration | ||||||
|  |    * @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(oid: string, config = this.DEFAULT_CONFIG): Promise<any> { |   public snmpGet( | ||||||
|  |     oid: string, | ||||||
|  |     config = this.DEFAULT_CONFIG, | ||||||
|  |     retryCount = 0, | ||||||
|  |   ): Promise<any> { | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       const socket = dgram.createSocket('udp4'); |  | ||||||
|        |  | ||||||
|       // Create appropriate request based on SNMP version |  | ||||||
|       let request: Buffer; |  | ||||||
|       if (config.version === 3) { |  | ||||||
|         request = SnmpPacketCreator.createSnmpV3GetRequest( |  | ||||||
|           oid,  |  | ||||||
|           config,  |  | ||||||
|           this.engineID,  |  | ||||||
|           this.engineBoots,  |  | ||||||
|           this.engineTime,  |  | ||||||
|           this.requestID++, |  | ||||||
|           this.debug |  | ||||||
|         ); |  | ||||||
|       } else { |  | ||||||
|         request = SnmpPacketCreator.createSnmpGetRequest(oid, config.community || 'public', this.debug); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.log(`Sending SNMP ${config.version === 3 ? 'v3' : ('v' + config.version)} request to ${config.host}:${config.port}`); |         console.log( | ||||||
|         console.log('Request length:', request.length); |           `Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`, | ||||||
|         console.log('First 16 bytes of request:', request.slice(0, 16).toString('hex')); |         ); | ||||||
|         console.log('Full request hex:', request.toString('hex')); |         console.log('Using community:', config.community); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Set timeout - add extra logging for debugging |       // Create SNMP options based on configuration | ||||||
|       const timeout = setTimeout(() => { |       const options: any = { | ||||||
|         socket.close(); |         port: config.port, | ||||||
|         if (this.debug) { |         retries: 2, // Number of retries | ||||||
|           console.error('---------------------------------------'); |         timeout: config.timeout, | ||||||
|           console.error('SNMP request timed out after', config.timeout, 'ms'); |         transport: 'udp4', | ||||||
|           console.error('SNMP Version:', config.version); |         idBitsSize: 32, | ||||||
|           if (config.version === 3) { |         context: config.context || '', | ||||||
|             console.error('SNMPv3 Security Level:', config.securityLevel); |       }; | ||||||
|             console.error('SNMPv3 Username:', config.username); |  | ||||||
|             console.error('SNMPv3 Auth Protocol:', config.authProtocol || 'None'); |  | ||||||
|             console.error('SNMPv3 Privacy Protocol:', config.privProtocol || 'None'); |  | ||||||
|           } |  | ||||||
|           console.error('OID:', oid); |  | ||||||
|           console.error('Host:', config.host); |  | ||||||
|           console.error('Port:', config.port); |  | ||||||
|           console.error('---------------------------------------'); |  | ||||||
|         } |  | ||||||
|         reject(new Error(`SNMP request timed out after ${config.timeout}ms`)); |  | ||||||
|       }, config.timeout); |  | ||||||
|  |  | ||||||
|       // Listen for responses |       // Set version based on config | ||||||
|       socket.on('message', (message, rinfo) => { |       if (config.version === 1) { | ||||||
|         clearTimeout(timeout); |         options.version = snmp.Version1; | ||||||
|  |       } else if (config.version === 2) { | ||||||
|  |         options.version = snmp.Version2c; | ||||||
|  |       } else { | ||||||
|  |         options.version = snmp.Version3; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Create appropriate session based on SNMP version | ||||||
|  |       let session; | ||||||
|  |  | ||||||
|  |       if (config.version === 3) { | ||||||
|  |         // For SNMPv3, we need to set up authentication and privacy | ||||||
|  |         // For SNMPv3, we need a valid security level | ||||||
|  |         const securityLevel = config.securityLevel || 'noAuthNoPriv'; | ||||||
|  |  | ||||||
|  |         // Create the user object with required structure for net-snmp | ||||||
|  |         const user: any = { | ||||||
|  |           name: config.username || '', | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Set security level | ||||||
|  |         if (securityLevel === 'noAuthNoPriv') { | ||||||
|  |           user.level = snmp.SecurityLevel.noAuthNoPriv; | ||||||
|  |         } else if (securityLevel === 'authNoPriv') { | ||||||
|  |           user.level = snmp.SecurityLevel.authNoPriv; | ||||||
|  |  | ||||||
|  |           // Set auth protocol - must provide both protocol and key | ||||||
|  |           if (config.authProtocol && config.authKey) { | ||||||
|  |             if (config.authProtocol === 'MD5') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.md5; | ||||||
|  |             } else if (config.authProtocol === 'SHA') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.sha; | ||||||
|  |             } | ||||||
|  |             user.authKey = config.authKey; | ||||||
|  |           } else { | ||||||
|  |             // Fallback to noAuthNoPriv if auth details missing | ||||||
|  |             user.level = snmp.SecurityLevel.noAuthNoPriv; | ||||||
|  |             if (this.debug) { | ||||||
|  |               console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv'); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } else if (securityLevel === 'authPriv') { | ||||||
|  |           user.level = snmp.SecurityLevel.authPriv; | ||||||
|  |  | ||||||
|  |           // Set auth protocol - must provide both protocol and key | ||||||
|  |           if (config.authProtocol && config.authKey) { | ||||||
|  |             if (config.authProtocol === 'MD5') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.md5; | ||||||
|  |             } else if (config.authProtocol === 'SHA') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.sha; | ||||||
|  |             } | ||||||
|  |             user.authKey = config.authKey; | ||||||
|  |  | ||||||
|  |             // Set privacy protocol - must provide both protocol and key | ||||||
|  |             if (config.privProtocol && config.privKey) { | ||||||
|  |               if (config.privProtocol === 'DES') { | ||||||
|  |                 user.privProtocol = snmp.PrivProtocols.des; | ||||||
|  |               } else if (config.privProtocol === 'AES') { | ||||||
|  |                 user.privProtocol = snmp.PrivProtocols.aes; | ||||||
|  |               } | ||||||
|  |               user.privKey = config.privKey; | ||||||
|  |             } else { | ||||||
|  |               // Fallback to authNoPriv if priv details missing | ||||||
|  |               user.level = snmp.SecurityLevel.authNoPriv; | ||||||
|  |               if (this.debug) { | ||||||
|  |                 console.log('Warning: Missing privProtocol or privKey, falling back to authNoPriv'); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             // Fallback to noAuthNoPriv if auth details missing | ||||||
|  |             user.level = snmp.SecurityLevel.noAuthNoPriv; | ||||||
|  |             if (this.debug) { | ||||||
|  |               console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv'); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (this.debug) { |         if (this.debug) { | ||||||
|           console.log(`Received SNMP response from ${rinfo.address}:${rinfo.port}`); |           console.log('SNMPv3 user configuration:', { | ||||||
|           console.log('Response length:', message.length); |             name: user.name, | ||||||
|           console.log('First 16 bytes of response:', message.slice(0, 16).toString('hex')); |             level: Object.keys(snmp.SecurityLevel).find((key) => | ||||||
|           console.log('Full response hex:', message.toString('hex')); |               snmp.SecurityLevel[key] === user.level | ||||||
|  |             ), | ||||||
|  |             authProtocol: user.authProtocol ? 'Set' : 'Not Set', | ||||||
|  |             authKey: user.authKey ? 'Set' : 'Not Set', | ||||||
|  |             privProtocol: user.privProtocol ? 'Set' : 'Not Set', | ||||||
|  |             privKey: user.privKey ? 'Set' : 'Not Set', | ||||||
|  |           }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         try { |         session = snmp.createV3Session(config.host, user, options); | ||||||
|           const result = SnmpPacketParser.parseSnmpResponse(message, config, this.debug); |       } else { | ||||||
|  |         // For SNMPv1/v2c, we use the community string | ||||||
|  |         session = snmp.createSession(config.host, config.community || 'public', options); | ||||||
|  |       } | ||||||
|  |  | ||||||
|           if (this.debug) { |       // Convert the OID string to an array of OIDs if multiple OIDs are needed | ||||||
|             console.log('Parsed SNMP response:', result); |       const oids = [oid]; | ||||||
|           } |  | ||||||
|  |  | ||||||
|           socket.close(); |       // Send the GET request | ||||||
|           resolve(result); |       session.get(oids, (error: any, varbinds: any[]) => { | ||||||
|         } catch (error) { |         // Close the session to release resources | ||||||
|           if (this.debug) { |         session.close(); | ||||||
|             console.error('Error parsing SNMP response:', error); |  | ||||||
|           } |  | ||||||
|           socket.close(); |  | ||||||
|           reject(error); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       // Handle errors |  | ||||||
|       socket.on('error', (error) => { |  | ||||||
|         clearTimeout(timeout); |  | ||||||
|         socket.close(); |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.error('Socket error during SNMP request:', error); |  | ||||||
|         } |  | ||||||
|         reject(error); |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       // First send the request directly without binding to a specific port |  | ||||||
|       // This lets the OS pick an available port instead of trying to bind to one |  | ||||||
|       socket.send(request, 0, request.length, config.port, config.host, (error) => { |  | ||||||
|         if (error) { |         if (error) { | ||||||
|           clearTimeout(timeout); |  | ||||||
|           socket.close(); |  | ||||||
|           if (this.debug) { |           if (this.debug) { | ||||||
|             console.error('Error sending SNMP request:', error); |             console.error('SNMP GET error:', error); | ||||||
|           } |           } | ||||||
|           reject(error); |           reject(new Error(`SNMP GET error: ${error.message || error}`)); | ||||||
|         } else if (this.debug) { |           return; | ||||||
|           console.log('SNMP request sent successfully'); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!varbinds || varbinds.length === 0) { | ||||||
|  |           if (this.debug) { | ||||||
|  |             console.error('No varbinds returned in response'); | ||||||
|  |           } | ||||||
|  |           reject(new Error('No varbinds returned in response')); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check for SNMP errors in the response | ||||||
|  |         if ( | ||||||
|  |           varbinds[0].type === snmp.ObjectType.NoSuchObject || | ||||||
|  |           varbinds[0].type === snmp.ObjectType.NoSuchInstance || | ||||||
|  |           varbinds[0].type === snmp.ObjectType.EndOfMibView | ||||||
|  |         ) { | ||||||
|  |           if (this.debug) { | ||||||
|  |             console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]); | ||||||
|  |           } | ||||||
|  |           reject(new Error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`)); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Process the response value based on its type | ||||||
|  |         let value = varbinds[0].value; | ||||||
|  |  | ||||||
|  |         // Handle specific types that might need conversion | ||||||
|  |         if (Buffer.isBuffer(value)) { | ||||||
|  |           // If value is a Buffer, try to convert it to a string if it's printable ASCII | ||||||
|  |           const isPrintableAscii = value.every((byte: number) => byte >= 32 && byte <= 126); | ||||||
|  |           if (isPrintableAscii) { | ||||||
|  |             value = value.toString(); | ||||||
|  |           } | ||||||
|  |         } else if (typeof value === 'bigint') { | ||||||
|  |           // Convert BigInt to a normal number or string if needed | ||||||
|  |           value = Number(value); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log('SNMP response:', { | ||||||
|  |             oid: varbinds[0].oid, | ||||||
|  |             type: varbinds[0].type, | ||||||
|  |             value: value, | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         resolve(value); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -234,142 +307,28 @@ export class NupstSnmp { | |||||||
|         console.log('---------------------------------------'); |         console.log('---------------------------------------'); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // For SNMPv3, we need to discover the engine ID first |  | ||||||
|       if (config.version === 3) { |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log('SNMPv3 detected, starting engine ID discovery'); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         try { |  | ||||||
|           const discoveredEngineId = await this.discoverEngineId(config); |  | ||||||
|           if (discoveredEngineId) { |  | ||||||
|             this.engineID = discoveredEngineId; |  | ||||||
|             if (this.debug) { |  | ||||||
|               console.log('Using discovered engine ID:', this.engineID.toString('hex')); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } catch (error) { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.warn('Engine ID discovery failed, using default:', error); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Helper function to get SNMP value with retry |  | ||||||
|       const getSNMPValueWithRetry = async (oid: string, description: string) => { |  | ||||||
|         if (oid === '') { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.log(`No OID provided for ${description}, skipping`); |  | ||||||
|           } |  | ||||||
|           return 0; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log(`Getting ${description} OID: ${oid}`); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         try { |  | ||||||
|           const value = await this.snmpGet(oid, config); |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.log(`${description} value:`, value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } catch (error) { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.error(`Error getting ${description}:`, error.message); |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           // If we got a timeout and it's SNMPv3, try with different security levels |  | ||||||
|           if (error.message.includes('timed out') && config.version === 3) { |  | ||||||
|             if (this.debug) { |  | ||||||
|               console.log(`Retrying ${description} with fallback settings...`); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // Create a retry config with lower security level |  | ||||||
|             if (config.securityLevel === 'authPriv') { |  | ||||||
|               const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' }; |  | ||||||
|               try { |  | ||||||
|                 if (this.debug) { |  | ||||||
|                   console.log(`Retrying with authNoPriv security level`); |  | ||||||
|                 } |  | ||||||
|                 const value = await this.snmpGet(oid, retryConfig); |  | ||||||
|                 if (this.debug) { |  | ||||||
|                   console.log(`${description} retry value:`, value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } catch (retryError) { |  | ||||||
|                 if (this.debug) { |  | ||||||
|                   console.error(`Retry failed for ${description}:`, retryError.message); |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           // If we're still having trouble, try with standard OIDs |  | ||||||
|           if (config.upsModel !== 'custom') { |  | ||||||
|             try { |  | ||||||
|               // Try RFC 1628 standard UPS MIB OIDs |  | ||||||
|               const standardOIDs = UpsOidSets.getStandardOids(); |  | ||||||
|                |  | ||||||
|               if (this.debug) { |  | ||||||
|                 console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`); |  | ||||||
|               } |  | ||||||
|                |  | ||||||
|               const standardValue = await this.snmpGet(standardOIDs[description], config); |  | ||||||
|               if (this.debug) { |  | ||||||
|                 console.log(`${description} standard OID value:`, standardValue); |  | ||||||
|               } |  | ||||||
|               return standardValue; |  | ||||||
|             } catch (stdError) { |  | ||||||
|               if (this.debug) { |  | ||||||
|                 console.error(`Standard OID retry failed for ${description}:`, stdError.message); |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           // Return a default value if all attempts fail |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.log(`Using default value 0 for ${description}`); |  | ||||||
|           } |  | ||||||
|           return 0; |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
|        |  | ||||||
|       // Get all values with independent retry logic |       // Get all values with independent retry logic | ||||||
|       const powerStatusValue = await getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status'); |       const powerStatusValue = await this.getSNMPValueWithRetry( | ||||||
|       const batteryCapacity = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity') || 0; |         this.activeOIDs.POWER_STATUS, | ||||||
|       const batteryRuntime = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime') || 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 | ||||||
|       let powerStatus: 'online' | 'onBattery' | 'unknown' = 'unknown'; |       const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue); | ||||||
|  |  | ||||||
|       // Different UPS models use different values for power status |       // Convert to minutes for UPS models with different time units | ||||||
|       if (config.upsModel === 'cyberpower') { |       const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime); | ||||||
|         // CyberPower RMCARD205: upsBaseOutputStatus values |  | ||||||
|         // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. |  | ||||||
|         if (powerStatusValue === 2) { |  | ||||||
|           powerStatus = 'online'; |  | ||||||
|         } else if (powerStatusValue === 3) { |  | ||||||
|           powerStatus = 'onBattery'; |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         // Default interpretation for other UPS models |  | ||||||
|         if (powerStatusValue === 1) { |  | ||||||
|           powerStatus = 'online'; |  | ||||||
|         } else if (powerStatusValue === 2) { |  | ||||||
|           powerStatus = 'onBattery'; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Convert TimeTicks to minutes for CyberPower runtime (value is in 1/100 seconds) |  | ||||||
|       let processedRuntime = batteryRuntime; |  | ||||||
|       if (config.upsModel === 'cyberpower' && batteryRuntime > 0) { |  | ||||||
|         // TimeTicks is in 1/100 seconds, convert to minutes |  | ||||||
|         processedRuntime = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${processedRuntime} minutes`); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const result = { |       const result = { | ||||||
|         powerStatus, |         powerStatus, | ||||||
| @@ -395,137 +354,262 @@ export class NupstSnmp { | |||||||
|     } 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)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Discover SNMP engine ID (for SNMPv3) |    * Helper method to get SNMP value with retry and fallback logic | ||||||
|    * Sends a proper discovery message to get the engine ID from the device |    * @param oid OID to query | ||||||
|  |    * @param description Description of the value for logging | ||||||
|    * @param config SNMP configuration |    * @param config SNMP configuration | ||||||
|    * @returns Promise resolving to the discovered engine ID |    * @returns Promise resolving to the SNMP value | ||||||
|    */ |    */ | ||||||
|   public async discoverEngineId(config: ISnmpConfig): Promise<Buffer> { |   private async getSNMPValueWithRetry( | ||||||
|     return new Promise((resolve, reject) => { |     oid: string, | ||||||
|       const socket = dgram.createSocket('udp4'); |     description: string, | ||||||
|  |     config: ISnmpConfig, | ||||||
|  |   ): Promise<any> { | ||||||
|  |     if (oid === '') { | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`No OID provided for ${description}, skipping`); | ||||||
|  |       } | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|       // Create a proper discovery message (SNMPv3 with noAuthNoPriv) |     if (this.debug) { | ||||||
|       const discoveryConfig: ISnmpConfig = { |       console.log(`Getting ${description} OID: ${oid}`); | ||||||
|         ...config, |     } | ||||||
|         securityLevel: 'noAuthNoPriv', |  | ||||||
|         username: '',  // Empty username for discovery |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       // Create a simple GetRequest for sysDescr (a commonly available OID) |     try { | ||||||
|       const request = SnmpPacketCreator.createDiscoveryMessage(discoveryConfig, this.requestID++); |       const value = await this.snmpGet(oid, config); | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`${description} value:`, value); | ||||||
|  |       } | ||||||
|  |       return value; | ||||||
|  |     } catch (error) { | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.error( | ||||||
|  |           `Error getting ${description}:`, | ||||||
|  |           error instanceof Error ? error.message : String(error), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // If we're using SNMPv3, try with different security levels | ||||||
|  |       if (config.version === 3) { | ||||||
|  |         return await this.tryFallbackSecurityLevels(oid, description, config); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Try with standard OIDs as fallback | ||||||
|  |       if (config.upsModel !== 'custom') { | ||||||
|  |         return await this.tryStandardOids(oid, description, config); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Return a default value if all attempts fail | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`Using default value 0 for ${description}`); | ||||||
|  |       } | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Try fallback security levels for SNMPv3 | ||||||
|  |    * @param oid OID to query | ||||||
|  |    * @param description Description of the value for logging | ||||||
|  |    * @param config SNMP configuration | ||||||
|  |    * @returns Promise resolving to the SNMP value | ||||||
|  |    */ | ||||||
|  |   private async tryFallbackSecurityLevels( | ||||||
|  |     oid: string, | ||||||
|  |     description: string, | ||||||
|  |     config: ISnmpConfig, | ||||||
|  |   ): Promise<any> { | ||||||
|  |     if (this.debug) { | ||||||
|  |       console.log(`Retrying ${description} with fallback security level...`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Try with authNoPriv if current level is authPriv | ||||||
|  |     if (config.securityLevel === 'authPriv') { | ||||||
|  |       const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' }; | ||||||
|  |       try { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`Retrying with authNoPriv security level`); | ||||||
|  |         } | ||||||
|  |         const value = await this.snmpGet(oid, retryConfig); | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`${description} retry value:`, value); | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |       } catch (retryError) { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.error( | ||||||
|  |             `Retry failed for ${description}:`, | ||||||
|  |             retryError instanceof Error ? retryError.message : String(retryError), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Try with noAuthNoPriv as a last resort | ||||||
|  |     if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') { | ||||||
|  |       const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' }; | ||||||
|  |       try { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`Retrying with noAuthNoPriv security level`); | ||||||
|  |         } | ||||||
|  |         const value = await this.snmpGet(oid, retryConfig); | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`${description} retry value:`, value); | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |       } catch (retryError) { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.error( | ||||||
|  |             `Retry failed for ${description}:`, | ||||||
|  |             retryError instanceof Error ? retryError.message : String(retryError), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Try standard OIDs as fallback | ||||||
|  |    * @param oid OID to query | ||||||
|  |    * @param description Description of the value for logging | ||||||
|  |    * @param config SNMP configuration | ||||||
|  |    * @returns Promise resolving to the SNMP value | ||||||
|  |    */ | ||||||
|  |   private async tryStandardOids( | ||||||
|  |     oid: string, | ||||||
|  |     description: string, | ||||||
|  |     config: ISnmpConfig, | ||||||
|  |   ): Promise<any> { | ||||||
|  |     try { | ||||||
|  |       // Try RFC 1628 standard UPS MIB OIDs | ||||||
|  |       const standardOIDs = UpsOidSets.getStandardOids(); | ||||||
|  |  | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.log('Sending SNMPv3 discovery message'); |         console.log( | ||||||
|         console.log('SNMPv3 Discovery message:', request.toString('hex')); |           `Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`, | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Set timeout - use a longer timeout for discovery phase |       const standardValue = await this.snmpGet(standardOIDs[description], config); | ||||||
|       const discoveryTimeout = Math.max(config.timeout, 15000); // At least 15 seconds for discovery |       if (this.debug) { | ||||||
|       const timeout = setTimeout(() => { |         console.log(`${description} standard OID value:`, standardValue); | ||||||
|         socket.close(); |       } | ||||||
|         // Fall back to default engine ID if discovery fails |       return standardValue; | ||||||
|         if (this.debug) { |     } catch (stdError) { | ||||||
|           console.error('---------------------------------------'); |       if (this.debug) { | ||||||
|           console.error('Engine ID discovery timed out after', discoveryTimeout, 'ms'); |         console.error( | ||||||
|           console.error('SNMPv3 settings:'); |           `Standard OID retry failed for ${description}:`, | ||||||
|           console.error('  Username:', config.username); |           stdError instanceof Error ? stdError.message : String(stdError), | ||||||
|           console.error('  Security Level:', config.securityLevel); |         ); | ||||||
|           console.error('  Host:', config.host); |       } | ||||||
|           console.error('  Port:', config.port); |     } | ||||||
|           console.error('Using default engine ID:', this.engineID.toString('hex')); |  | ||||||
|           console.error('---------------------------------------'); |  | ||||||
|         } |  | ||||||
|         resolve(this.engineID); |  | ||||||
|       }, discoveryTimeout); |  | ||||||
|  |  | ||||||
|       // Listen for responses |     return 0; | ||||||
|       socket.on('message', (message, rinfo) => { |  | ||||||
|         clearTimeout(timeout); |  | ||||||
|          |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log(`Received SNMPv3 discovery response from ${rinfo.address}:${rinfo.port}`); |  | ||||||
|           console.log('Response:', message.toString('hex')); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         try { |  | ||||||
|           // Extract engine ID from response |  | ||||||
|           const engineId = SnmpPacketParser.extractEngineId(message, this.debug); |  | ||||||
|           if (engineId) { |  | ||||||
|             this.engineID = engineId; // Update the engine ID |  | ||||||
|             if (this.debug) { |  | ||||||
|               console.log('Discovered engine ID:', engineId.toString('hex')); |  | ||||||
|             } |  | ||||||
|             socket.close(); |  | ||||||
|             resolve(engineId); |  | ||||||
|           } else { |  | ||||||
|             if (this.debug) { |  | ||||||
|               console.log('Could not extract engine ID, using default'); |  | ||||||
|             } |  | ||||||
|             socket.close(); |  | ||||||
|             resolve(this.engineID); |  | ||||||
|           } |  | ||||||
|         } catch (error) { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.error('Error extracting engine ID:', error); |  | ||||||
|           } |  | ||||||
|           socket.close(); |  | ||||||
|           resolve(this.engineID); // Fall back to default engine ID |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       // Handle errors |  | ||||||
|       socket.on('error', (error) => { |  | ||||||
|         clearTimeout(timeout); |  | ||||||
|         socket.close(); |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.error('Engine ID discovery socket error:', error); |  | ||||||
|         } |  | ||||||
|         resolve(this.engineID); // Fall back to default engine ID |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       // Send request directly without binding |  | ||||||
|       socket.send(request, 0, request.length, config.port, config.host, (error) => { |  | ||||||
|         if (error) { |  | ||||||
|           clearTimeout(timeout); |  | ||||||
|           socket.close(); |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.error('Error sending discovery message:', error); |  | ||||||
|           } |  | ||||||
|           resolve(this.engineID); // Fall back to default engine ID |  | ||||||
|         } else if (this.debug) { |  | ||||||
|           console.log('Discovery message sent successfully'); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Initiate system shutdown |    * Determine power status based on UPS model and raw value | ||||||
|    * @param reason Reason for shutdown |    * @param upsModel UPS model | ||||||
|  |    * @param powerStatusValue Raw power status value | ||||||
|  |    * @returns Standardized power status | ||||||
|    */ |    */ | ||||||
|   public async initiateShutdown(reason: string): Promise<void> { |   private determinePowerStatus( | ||||||
|     console.log(`Initiating system shutdown due to: ${reason}`); |     upsModel: TUpsModel | undefined, | ||||||
|     try { |     powerStatusValue: number, | ||||||
|       // Execute shutdown command |   ): 'online' | 'onBattery' | 'unknown' { | ||||||
|       const { stdout } = await execAsync('shutdown -h +1 "UPS battery critical, shutting down in 1 minute"'); |     if (upsModel === 'cyberpower') { | ||||||
|       console.log('Shutdown initiated:', stdout); |       // CyberPower RMCARD205: upsBaseOutputStatus values | ||||||
|     } catch (error) { |       // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. | ||||||
|       console.error('Failed to initiate shutdown:', error); |       if (powerStatusValue === 2) { | ||||||
|       // Try a different method if first one fails |         return 'online'; | ||||||
|       try { |       } else if (powerStatusValue === 3) { | ||||||
|         console.log('Trying alternative shutdown method...'); |         return 'onBattery'; | ||||||
|         await execAsync('poweroff --force'); |       } | ||||||
|       } catch (innerError) { |     } else if (upsModel === 'eaton') { | ||||||
|         console.error('All shutdown methods failed:', innerError); |       // Eaton UPS: xupsOutputSource values | ||||||
|  |       // 3=normal/mains, 5=battery, etc. | ||||||
|  |       if (powerStatusValue === 3) { | ||||||
|  |         return 'online'; | ||||||
|  |       } else if (powerStatusValue === 5) { | ||||||
|  |         return 'onBattery'; | ||||||
|  |       } | ||||||
|  |     } else if (upsModel === 'apc') { | ||||||
|  |       // APC UPS: upsBasicOutputStatus values | ||||||
|  |       // 2=online, 3=onBattery, etc. | ||||||
|  |       if (powerStatusValue === 2) { | ||||||
|  |         return 'online'; | ||||||
|  |       } else if (powerStatusValue === 3) { | ||||||
|  |         return 'onBattery'; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // Default interpretation for other UPS models | ||||||
|  |       if (powerStatusValue === 1) { | ||||||
|  |         return 'online'; | ||||||
|  |       } else if (powerStatusValue === 2) { | ||||||
|  |         return 'onBattery'; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return 'unknown'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Process runtime value based on UPS model | ||||||
|  |    * @param upsModel UPS model | ||||||
|  |    * @param batteryRuntime Raw battery runtime value | ||||||
|  |    * @returns Processed runtime in minutes | ||||||
|  |    */ | ||||||
|  |   private processRuntimeValue( | ||||||
|  |     upsModel: TUpsModel | undefined, | ||||||
|  |     batteryRuntime: number, | ||||||
|  |   ): number { | ||||||
|  |     if (this.debug) { | ||||||
|  |       console.log('Raw runtime value:', batteryRuntime); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (upsModel === 'cyberpower' && batteryRuntime > 0) { | ||||||
|  |       // CyberPower: TimeTicks is in 1/100 seconds, convert to minutes | ||||||
|  |       const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log( | ||||||
|  |           `Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       return minutes; | ||||||
|  |     } else if (upsModel === 'eaton' && batteryRuntime > 0) { | ||||||
|  |       // Eaton: Runtime is in seconds, convert to minutes | ||||||
|  |       const minutes = Math.floor(batteryRuntime / 60); | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log( | ||||||
|  |           `Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       return minutes; | ||||||
|  |     } else if (batteryRuntime > 10000) { | ||||||
|  |       // Generic conversion for large tick values (likely TimeTicks) | ||||||
|  |       const minutes = Math.floor(batteryRuntime / 6000); | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`Converting ${batteryRuntime} ticks to ${minutes} minutes`); | ||||||
|  |       } | ||||||
|  |       return minutes; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return batteryRuntime; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import type { IOidSet, TUpsModel } from './types.js'; | import type { IOidSet, TUpsModel } from './types.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * OID sets for different UPS models |  * OID sets for different UPS models | ||||||
| @@ -25,9 +25,9 @@ export class UpsOidSets { | |||||||
|  |  | ||||||
|     // Eaton OIDs |     // Eaton OIDs | ||||||
|     eaton: { |     eaton: { | ||||||
|       POWER_STATUS: '1.3.6.1.4.1.534.1.1.2.0', // Power status |       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', // Battery capacity in 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', // Remaining runtime in minutes |       BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds) | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     // TrippLite OIDs |     // TrippLite OIDs | ||||||
| @@ -49,7 +49,7 @@ export class UpsOidSets { | |||||||
|       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 | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -1,651 +0,0 @@ | |||||||
| import * as crypto from 'crypto'; |  | ||||||
| import type { ISnmpConfig, ISnmpV3SecurityParams } from './types.js'; |  | ||||||
| import { SnmpEncoder } from './encoder.js'; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * SNMP packet creation utilities |  | ||||||
|  * Creates SNMP request packets for different SNMP versions |  | ||||||
|  */ |  | ||||||
| export class SnmpPacketCreator { |  | ||||||
|   /** |  | ||||||
|    * Create an SNMPv1 GET request |  | ||||||
|    * @param oid OID to query |  | ||||||
|    * @param community Community string |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Buffer containing the SNMP request |  | ||||||
|    */ |  | ||||||
|   public static createSnmpGetRequest(oid: string, community: string, debug: boolean = false): Buffer { |  | ||||||
|     const oidArray = SnmpEncoder.oidToArray(oid); |  | ||||||
|     const encodedOid = SnmpEncoder.encodeOID(oidArray); |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('OID array length:', oidArray.length); |  | ||||||
|       console.log('OID array:', oidArray); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // SNMP message structure |  | ||||||
|     // Sequence |  | ||||||
|     //   Version (Integer) |  | ||||||
|     //   Community (String) |  | ||||||
|     //   PDU (GetRequest) |  | ||||||
|     //     Request ID (Integer) |  | ||||||
|     //     Error Status (Integer) |  | ||||||
|     //     Error Index (Integer) |  | ||||||
|     //     Variable Bindings (Sequence) |  | ||||||
|     //       Variable (Sequence) |  | ||||||
|     //         OID (ObjectIdentifier) |  | ||||||
|     //         Value (Null) |  | ||||||
|      |  | ||||||
|     // Use the standard method from our test that is known to work |  | ||||||
|     // Create a fixed request ID (0x00000001) to ensure deterministic behavior |  | ||||||
|     const requestId = Buffer.from([0x00, 0x00, 0x00, 0x01]); |  | ||||||
|      |  | ||||||
|     // Encode values |  | ||||||
|     const versionBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // SNMP version 1 (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const communityBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, community.length]), // ASN.1 Octet String, length |  | ||||||
|       Buffer.from(community)                // Community string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const requestIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       requestId                  // Fixed Request ID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorStatusBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Status (0 = no error) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorIndexBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Index (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const oidValueBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([encodedOid.length + 2]), // Length |  | ||||||
|       Buffer.from([0x06]),       // ASN.1 Object Identifier |  | ||||||
|       Buffer.from([encodedOid.length]), // Length |  | ||||||
|       encodedOid,                // OID |  | ||||||
|       Buffer.from([0x05, 0x00])  // Null value |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const varBindingsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([oidValueBuf.length]), // Length |  | ||||||
|       oidValueBuf                // Variable binding |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const pduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0xa0]),       // ASN.1 Context-specific Constructed 0 (GetRequest) |  | ||||||
|       Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length |  | ||||||
|       requestIdBuf,              // Request ID |  | ||||||
|       errorStatusBuf,            // Error Status |  | ||||||
|       errorIndexBuf,             // Error Index |  | ||||||
|       varBindingsBuf             // Variable Bindings |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const messageBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([versionBuf.length + communityBuf.length + pduBuf.length]), // Length |  | ||||||
|       versionBuf,                // Version |  | ||||||
|       communityBuf,              // Community |  | ||||||
|       pduBuf                     // PDU |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('SNMP Request buffer:', messageBuf.toString('hex')); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return messageBuf; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Create an SNMPv3 GET request |  | ||||||
|    * @param oid OID to query |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param engineID Engine ID |  | ||||||
|    * @param engineBoots Engine boots counter |  | ||||||
|    * @param engineTime Engine time counter |  | ||||||
|    * @param requestID Request ID |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Buffer containing the SNMP request |  | ||||||
|    */ |  | ||||||
|   public static createSnmpV3GetRequest( |  | ||||||
|     oid: string,  |  | ||||||
|     config: ISnmpConfig,  |  | ||||||
|     engineID: Buffer, |  | ||||||
|     engineBoots: number, |  | ||||||
|     engineTime: number, |  | ||||||
|     requestID: number, |  | ||||||
|     debug: boolean = false |  | ||||||
|   ): Buffer { |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Creating SNMPv3 GET request for OID:', oid); |  | ||||||
|       console.log('With config:', { |  | ||||||
|         ...config, |  | ||||||
|         authKey: config.authKey ? '***' : undefined, |  | ||||||
|         privKey: config.privKey ? '***' : undefined |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     const oidArray = SnmpEncoder.oidToArray(oid); |  | ||||||
|     const encodedOid = SnmpEncoder.encodeOID(oidArray); |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Using engine ID:', engineID.toString('hex')); |  | ||||||
|       console.log('Engine boots:', engineBoots); |  | ||||||
|       console.log('Engine time:', engineTime); |  | ||||||
|       console.log('Request ID:', requestID); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Create security parameters |  | ||||||
|     const securityParams: ISnmpV3SecurityParams = { |  | ||||||
|       msgAuthoritativeEngineID: engineID, |  | ||||||
|       msgAuthoritativeEngineBoots: engineBoots, |  | ||||||
|       msgAuthoritativeEngineTime: engineTime, |  | ||||||
|       msgUserName: config.username || '', |  | ||||||
|       msgAuthenticationParameters: Buffer.alloc(12, 0), // Will be filled in later for auth |  | ||||||
|       msgPrivacyParameters: Buffer.alloc(8, 0),  // For privacy |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     // Create the PDU (Protocol Data Unit) |  | ||||||
|     // This is wrapped within the security parameters |  | ||||||
|     const requestIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID) // Request ID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorStatusBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Status (0 = no error) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorIndexBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Index (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const oidValueBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([encodedOid.length + 2]), // Length |  | ||||||
|       Buffer.from([0x06]),       // ASN.1 Object Identifier |  | ||||||
|       Buffer.from([encodedOid.length]), // Length |  | ||||||
|       encodedOid,                // OID |  | ||||||
|       Buffer.from([0x05, 0x00])  // Null value |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const varBindingsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([oidValueBuf.length]), // Length |  | ||||||
|       oidValueBuf                // Variable binding |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const pduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0xa0]),       // ASN.1 Context-specific Constructed 0 (GetRequest) |  | ||||||
|       Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length |  | ||||||
|       requestIdBuf,              // Request ID |  | ||||||
|       errorStatusBuf,            // Error Status |  | ||||||
|       errorIndexBuf,             // Error Index |  | ||||||
|       varBindingsBuf             // Variable Bindings |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Create the security parameters |  | ||||||
|     const engineIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgAuthoritativeEngineID.length]), // ASN.1 Octet String |  | ||||||
|       securityParams.msgAuthoritativeEngineID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const engineBootsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineBoots) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const engineTimeBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineTime) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const userNameBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgUserName.length]), // ASN.1 Octet String |  | ||||||
|       Buffer.from(securityParams.msgUserName) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const authParamsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgAuthenticationParameters.length]), // ASN.1 Octet String |  | ||||||
|       securityParams.msgAuthenticationParameters |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const privParamsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgPrivacyParameters.length]), // ASN.1 Octet String |  | ||||||
|       securityParams.msgPrivacyParameters |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Security parameters sequence |  | ||||||
|     const securityParamsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([engineIdBuf.length + engineBootsBuf.length + engineTimeBuf.length +  |  | ||||||
|                    userNameBuf.length + authParamsBuf.length + privParamsBuf.length]), // Length |  | ||||||
|       engineIdBuf, |  | ||||||
|       engineBootsBuf, |  | ||||||
|       engineTimeBuf, |  | ||||||
|       userNameBuf, |  | ||||||
|       authParamsBuf, |  | ||||||
|       privParamsBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Determine security level flags |  | ||||||
|     let securityFlags = 0; |  | ||||||
|     if (config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') { |  | ||||||
|       securityFlags |= 0x01; // Authentication flag |  | ||||||
|     } |  | ||||||
|     if (config.securityLevel === 'authPriv') { |  | ||||||
|       securityFlags |= 0x02; // Privacy flag |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Set reportable flag - required for SNMPv3 |  | ||||||
|     securityFlags |= 0x04; // Reportable flag |  | ||||||
|  |  | ||||||
|     // Create SNMPv3 header |  | ||||||
|     const msgIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID) // Message ID (same as request ID for simplicity) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgMaxSizeBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(65507) // Max message size |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgFlagsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1 |  | ||||||
|       Buffer.from([securityFlags]) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgSecModelBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03]) // Security model (3 = USM) |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // SNMPv3 header |  | ||||||
|     const msgHeaderBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length |  | ||||||
|       msgIdBuf, |  | ||||||
|       msgMaxSizeBuf, |  | ||||||
|       msgFlagsBuf, |  | ||||||
|       msgSecModelBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // SNMPv3 security parameters |  | ||||||
|     const msgSecurityBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04]), // ASN.1 Octet String |  | ||||||
|       Buffer.from([securityParamsBuf.length]), // Length |  | ||||||
|       securityParamsBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Create scopedPDU |  | ||||||
|     // In SNMPv3, the PDU is wrapped in a "scoped PDU" structure |  | ||||||
|     const contextEngineBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, engineID.length]), // ASN.1 Octet String |  | ||||||
|       engineID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const contextNameBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // ASN.1 Octet String, length 0 (empty context name) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const scopedPduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), // Length |  | ||||||
|       contextEngineBuf, |  | ||||||
|       contextNameBuf, |  | ||||||
|       pduBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // For authPriv, we need to encrypt the scopedPDU |  | ||||||
|     let encryptedPdu = scopedPduBuf; |  | ||||||
|     if (config.securityLevel === 'authPriv' && config.privKey) { |  | ||||||
|       // In a real implementation, encryption would be applied here |  | ||||||
|       // For this example, we'll just simulate it |  | ||||||
|       encryptedPdu = this.simulateEncryption(scopedPduBuf, config); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Final scopedPDU (encrypted or not) |  | ||||||
|     const finalScopedPduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04]), // ASN.1 Octet String |  | ||||||
|       Buffer.from([encryptedPdu.length]), // Length |  | ||||||
|       encryptedPdu |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Combine everything for the final message |  | ||||||
|     const versionBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03])        // SNMP version 3 (3) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const messageBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([versionBuf.length + msgHeaderBuf.length + msgSecurityBuf.length + finalScopedPduBuf.length]), // Length |  | ||||||
|       versionBuf, |  | ||||||
|       msgHeaderBuf, |  | ||||||
|       msgSecurityBuf, |  | ||||||
|       finalScopedPduBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // If using authentication, calculate and insert the authentication parameters |  | ||||||
|     if ((config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') &&  |  | ||||||
|         config.authKey && config.authProtocol) { |  | ||||||
|       const authenticatedMsg = this.addAuthentication(messageBuf, config, authParamsBuf); |  | ||||||
|        |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Created authenticated SNMPv3 message'); |  | ||||||
|         console.log('Final message length:', authenticatedMsg.length); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       return authenticatedMsg; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Created SNMPv3 message without authentication'); |  | ||||||
|       console.log('Final message length:', messageBuf.length); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return messageBuf; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Simulate encryption for authPriv security level |  | ||||||
|    * In a real implementation, this would use the specified privacy protocol (DES/AES) |  | ||||||
|    * @param data Data to encrypt |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @returns Encrypted data |  | ||||||
|    */ |  | ||||||
|   private static simulateEncryption(data: Buffer, config: ISnmpConfig): Buffer { |  | ||||||
|     // This is a placeholder - in a real implementation, you would: |  | ||||||
|     // 1. Generate an initialization vector (IV) |  | ||||||
|     // 2. Use the privacy key derived from the privKey |  | ||||||
|     // 3. Apply the appropriate encryption algorithm (DES/AES) |  | ||||||
|      |  | ||||||
|     // For demonstration purposes only |  | ||||||
|     if (config.privProtocol === 'AES' && config.privKey) { |  | ||||||
|       try { |  | ||||||
|         // Create a deterministic IV for demo purposes (not secure for production) |  | ||||||
|         const iv = Buffer.alloc(16, 0); |  | ||||||
|         const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); |  | ||||||
|         for (let i = 0; i < 8; i++) { |  | ||||||
|           iv[i] = engineID[i % engineID.length]; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Create a key from the privKey (proper key localization should be used in production) |  | ||||||
|         const key = crypto.createHash('md5').update(config.privKey).digest(); |  | ||||||
|          |  | ||||||
|         // Create cipher and encrypt |  | ||||||
|         const cipher = crypto.createCipheriv('aes-128-cfb', key, iv); |  | ||||||
|         const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); |  | ||||||
|          |  | ||||||
|         return encrypted; |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn('AES encryption failed, falling back to plaintext:', error); |  | ||||||
|         return data; |  | ||||||
|       } |  | ||||||
|     } else if (config.privProtocol === 'DES' && config.privKey) { |  | ||||||
|       try { |  | ||||||
|         // Create a deterministic IV for demo purposes (not secure for production) |  | ||||||
|         const iv = Buffer.alloc(8, 0); |  | ||||||
|         const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); |  | ||||||
|         for (let i = 0; i < 8; i++) { |  | ||||||
|           iv[i] = engineID[i % engineID.length]; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Create a key from the privKey (proper key localization should be used in production) |  | ||||||
|         const key = crypto.createHash('md5').update(config.privKey).digest().slice(0, 8); |  | ||||||
|          |  | ||||||
|         // Create cipher and encrypt |  | ||||||
|         const cipher = crypto.createCipheriv('des-cbc', key, iv); |  | ||||||
|         const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); |  | ||||||
|          |  | ||||||
|         return encrypted; |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn('DES encryption failed, falling back to plaintext:', error); |  | ||||||
|         return data; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return data; // Return unencrypted data as fallback |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Add authentication to SNMPv3 message |  | ||||||
|    * @param message Message to authenticate |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param authParamsBuf Authentication parameters buffer |  | ||||||
|    * @returns Authenticated message |  | ||||||
|    */ |  | ||||||
|   private static addAuthentication(message: Buffer, config: ISnmpConfig, authParamsBuf: Buffer): Buffer { |  | ||||||
|     // In a real implementation, this would: |  | ||||||
|     // 1. Zero out the authentication parameters field |  | ||||||
|     // 2. Calculate HMAC-MD5 or HMAC-SHA1 over the entire message |  | ||||||
|     // 3. Insert the HMAC into the authentication parameters field |  | ||||||
|      |  | ||||||
|     if (!config.authKey) { |  | ||||||
|       return message; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     try { |  | ||||||
|       // Find position of auth parameters in the message |  | ||||||
|       // This is a more reliable way to find the exact position |  | ||||||
|       let authParamsPos = -1; |  | ||||||
|       for (let i = 0; i < message.length - 16; i++) { |  | ||||||
|         // Look for the auth params pattern: 0x04 0x0C 0x00 0x00... |  | ||||||
|         if (message[i] === 0x04 && message[i + 1] === 0x0C) { |  | ||||||
|           // Check if next 12 bytes are all zeros |  | ||||||
|           let allZeros = true; |  | ||||||
|           for (let j = 0; j < 12; j++) { |  | ||||||
|             if (message[i + 2 + j] !== 0) { |  | ||||||
|               allZeros = false; |  | ||||||
|               break; |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|           if (allZeros) { |  | ||||||
|             authParamsPos = i; |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       if (authParamsPos === -1) { |  | ||||||
|         return message; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Create a copy of the message with zeroed auth parameters |  | ||||||
|       const msgCopy = Buffer.from(message); |  | ||||||
|        |  | ||||||
|       // Prepare the authentication key according to RFC3414 |  | ||||||
|       // We should use the standard key localization process |  | ||||||
|       const localizedKey = this.localizeAuthKey(config.authKey,  |  | ||||||
|         Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]), |  | ||||||
|         config.authProtocol); |  | ||||||
|        |  | ||||||
|       // Calculate HMAC |  | ||||||
|       let hmac; |  | ||||||
|       if (config.authProtocol === 'SHA') { |  | ||||||
|         hmac = crypto.createHmac('sha1', localizedKey).update(msgCopy).digest().slice(0, 12); |  | ||||||
|       } else { |  | ||||||
|         // Default to MD5 |  | ||||||
|         hmac = crypto.createHmac('md5', localizedKey).update(msgCopy).digest().slice(0, 12); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Copy HMAC into original message |  | ||||||
|       hmac.copy(message, authParamsPos + 2); |  | ||||||
|        |  | ||||||
|       return message; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.warn('Authentication failed:', error); |  | ||||||
|       return message; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   /** |  | ||||||
|    * Localize authentication key according to RFC3414 |  | ||||||
|    * @param key Authentication key |  | ||||||
|    * @param engineId Engine ID |  | ||||||
|    * @param authProtocol Authentication protocol |  | ||||||
|    * @returns Localized key |  | ||||||
|    */ |  | ||||||
|   private static localizeAuthKey(key: string, engineId: Buffer, authProtocol: string = 'MD5'): Buffer { |  | ||||||
|     try { |  | ||||||
|       // Convert password to key using hash |  | ||||||
|       let initialHash; |  | ||||||
|       if (authProtocol === 'SHA') { |  | ||||||
|         initialHash = crypto.createHash('sha1'); |  | ||||||
|       } else { |  | ||||||
|         initialHash = crypto.createHash('md5'); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Generate the initial key - repeated hashing of password + padding |  | ||||||
|       const password = Buffer.from(key); |  | ||||||
|       let passwordIndex = 0; |  | ||||||
|        |  | ||||||
|       // Create a buffer of 1MB (1048576 bytes) filled with the password |  | ||||||
|       const buffer = Buffer.alloc(1048576); |  | ||||||
|       for (let i = 0; i < 1048576; i++) { |  | ||||||
|         buffer[i] = password[passwordIndex]; |  | ||||||
|         passwordIndex = (passwordIndex + 1) % password.length; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       initialHash.update(buffer); |  | ||||||
|       let initialKey = initialHash.digest(); |  | ||||||
|        |  | ||||||
|       // Localize the key with engine ID |  | ||||||
|       let localHash; |  | ||||||
|       if (authProtocol === 'SHA') { |  | ||||||
|         localHash = crypto.createHash('sha1'); |  | ||||||
|       } else { |  | ||||||
|         localHash = crypto.createHash('md5'); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       localHash.update(initialKey); |  | ||||||
|       localHash.update(engineId); |  | ||||||
|       localHash.update(initialKey); |  | ||||||
|        |  | ||||||
|       return localHash.digest(); |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error('Error localizing auth key:', error); |  | ||||||
|       // Return a fallback key |  | ||||||
|       return Buffer.from(key); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Create a discovery message for SNMPv3 engine ID discovery |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param requestID Request ID |  | ||||||
|    * @returns Discovery message |  | ||||||
|    */ |  | ||||||
|   public static createDiscoveryMessage(config: ISnmpConfig, requestID: number): Buffer { |  | ||||||
|     // Basic SNMPv3 header for discovery |  | ||||||
|     const msgIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgMaxSizeBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(65507) // Max message size |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgFlagsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1 |  | ||||||
|       Buffer.from([0x00]) // No authentication or privacy |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgSecModelBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03]) // Security model (3 = USM) |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // SNMPv3 header |  | ||||||
|     const msgHeaderBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length |  | ||||||
|       msgIdBuf, |  | ||||||
|       msgMaxSizeBuf, |  | ||||||
|       msgFlagsBuf, |  | ||||||
|       msgSecModelBuf |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Simple security parameters for discovery |  | ||||||
|     const securityBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // Empty octet string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Simple Get request for discovery |  | ||||||
|     const requestIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID + 1) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorStatusBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Status (0 = no error) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorIndexBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Index (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Empty varbinds for discovery |  | ||||||
|     const varBindingsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30, 0x00]), // Empty sequence |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const pduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0xa0]), // GetRequest |  | ||||||
|       Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), |  | ||||||
|       requestIdBuf, |  | ||||||
|       errorStatusBuf, |  | ||||||
|       errorIndexBuf, |  | ||||||
|       varBindingsBuf |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Context data |  | ||||||
|     const contextEngineBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // Empty octet string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const contextNameBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // Empty octet string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const scopedPduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), |  | ||||||
|       Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), |  | ||||||
|       contextEngineBuf, |  | ||||||
|       contextNameBuf, |  | ||||||
|       pduBuf |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Version |  | ||||||
|     const versionBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03])        // SNMP version 3 (3) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Complete message |  | ||||||
|     return Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), |  | ||||||
|       Buffer.from([versionBuf.length + msgHeaderBuf.length + securityBuf.length + scopedPduBuf.length]), |  | ||||||
|       versionBuf, |  | ||||||
|       msgHeaderBuf, |  | ||||||
|       securityBuf, |  | ||||||
|       scopedPduBuf |  | ||||||
|     ]); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,553 +0,0 @@ | |||||||
| import type { ISnmpConfig } from './types.js'; |  | ||||||
| import { SnmpEncoder } from './encoder.js'; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * SNMP packet parsing utilities |  | ||||||
|  * Parses SNMP response packets |  | ||||||
|  */ |  | ||||||
| export class SnmpPacketParser { |  | ||||||
|   /** |  | ||||||
|    * Parse an SNMP response |  | ||||||
|    * @param buffer Response buffer |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   public static parseSnmpResponse(buffer: Buffer, config: ISnmpConfig, debug: boolean = false): any { |  | ||||||
|     // Check if we have a response packet |  | ||||||
|     if (buffer[0] !== 0x30) { |  | ||||||
|       throw new Error('Invalid SNMP response format'); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // For SNMPv3, we need to handle the message differently |  | ||||||
|     if (config.version === 3) { |  | ||||||
|       return this.parseSnmpV3Response(buffer, debug); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Parsing SNMPv1/v2 response: ', buffer.toString('hex')); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     try { |  | ||||||
|       // Enhanced structured parsing approach |  | ||||||
|       // SEQUENCE header |  | ||||||
|       let pos = 0; |  | ||||||
|       if (buffer[pos] !== 0x30) { |  | ||||||
|         throw new Error('Missing SEQUENCE at start of response'); |  | ||||||
|       } |  | ||||||
|       // Skip SEQUENCE header - assume length is in single byte for simplicity |  | ||||||
|       // In a more robust implementation, we'd handle multi-byte lengths |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // VERSION |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for version'); |  | ||||||
|       } |  | ||||||
|       const versionLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + versionLength; |  | ||||||
|        |  | ||||||
|       // COMMUNITY STRING |  | ||||||
|       if (buffer[pos] !== 0x04) { |  | ||||||
|         throw new Error('Missing OCTET STRING for community'); |  | ||||||
|       } |  | ||||||
|       const communityLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + communityLength; |  | ||||||
|        |  | ||||||
|       // PDU TYPE - should be RESPONSE (0xA2) |  | ||||||
|       if (buffer[pos] !== 0xA2) { |  | ||||||
|         throw new Error(`Unexpected PDU type: 0x${buffer[pos].toString(16)}, expected 0xA2`); |  | ||||||
|       } |  | ||||||
|       // Skip PDU header |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // REQUEST ID |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for request ID'); |  | ||||||
|       } |  | ||||||
|       const requestIdLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + requestIdLength; |  | ||||||
|        |  | ||||||
|       // ERROR STATUS |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for error status'); |  | ||||||
|       } |  | ||||||
|       const errorStatusLength = buffer[pos + 1]; |  | ||||||
|       const errorStatus = SnmpEncoder.decodeInteger(buffer, pos + 2, errorStatusLength); |  | ||||||
|        |  | ||||||
|       if (errorStatus !== 0) { |  | ||||||
|         throw new Error(`SNMP error status: ${errorStatus}`); |  | ||||||
|       } |  | ||||||
|       pos += 2 + errorStatusLength; |  | ||||||
|        |  | ||||||
|       // ERROR INDEX |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for error index'); |  | ||||||
|       } |  | ||||||
|       const errorIndexLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + errorIndexLength; |  | ||||||
|        |  | ||||||
|       // VARBIND LIST |  | ||||||
|       if (buffer[pos] !== 0x30) { |  | ||||||
|         throw new Error('Missing SEQUENCE for varbind list'); |  | ||||||
|       } |  | ||||||
|       // Skip varbind list header |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // VARBIND |  | ||||||
|       if (buffer[pos] !== 0x30) { |  | ||||||
|         throw new Error('Missing SEQUENCE for varbind'); |  | ||||||
|       } |  | ||||||
|       // Skip varbind header |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // OID |  | ||||||
|       if (buffer[pos] !== 0x06) { |  | ||||||
|         throw new Error('Missing OBJECT IDENTIFIER for OID'); |  | ||||||
|       } |  | ||||||
|       const oidLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + oidLength; |  | ||||||
|        |  | ||||||
|       // VALUE - this is what we want |  | ||||||
|       const valueType = buffer[pos]; |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|        |  | ||||||
|       if (debug) { |  | ||||||
|         console.log(`Found value type: 0x${valueType.toString(16)}, length: ${valueLength}`); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       return this.parseValueByType(valueType, valueLength, buffer, pos, debug); |  | ||||||
|     } catch (error) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.error('Error in structured parsing:', error); |  | ||||||
|         console.error('Falling back to scan-based parsing method'); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       return this.scanBasedParsing(buffer, debug); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Parse value by ASN.1 type |  | ||||||
|    * @param valueType ASN.1 type |  | ||||||
|    * @param valueLength Value length |  | ||||||
|    * @param buffer Buffer containing the value |  | ||||||
|    * @param pos Position of the value in the buffer |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value |  | ||||||
|    */ |  | ||||||
|   private static parseValueByType( |  | ||||||
|     valueType: number,  |  | ||||||
|     valueLength: number,  |  | ||||||
|     buffer: Buffer,  |  | ||||||
|     pos: number,  |  | ||||||
|     debug: boolean |  | ||||||
|   ): any { |  | ||||||
|     switch (valueType) { |  | ||||||
|       case 0x02: // INTEGER |  | ||||||
|         { |  | ||||||
|           const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed INTEGER value:', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x04: // OCTET STRING |  | ||||||
|         { |  | ||||||
|           const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString(); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed OCTET STRING value:', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x05: // NULL |  | ||||||
|         if (debug) { |  | ||||||
|           console.log('Parsed NULL value'); |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|          |  | ||||||
|       case 0x06: // OBJECT IDENTIFIER (rare in a value position) |  | ||||||
|         { |  | ||||||
|           // Usually this would be encoded as a string representation |  | ||||||
|           const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString('hex'); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed OBJECT IDENTIFIER value (hex):', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x40: // IP ADDRESS |  | ||||||
|         { |  | ||||||
|           if (valueLength !== 4) { |  | ||||||
|             throw new Error(`Invalid IP address length: ${valueLength}, expected 4`); |  | ||||||
|           } |  | ||||||
|           const octets = []; |  | ||||||
|           for (let i = 0; i < 4; i++) { |  | ||||||
|             octets.push(buffer[pos + 2 + i]); |  | ||||||
|           } |  | ||||||
|           const value = octets.join('.'); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed IP ADDRESS value:', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x41: // COUNTER |  | ||||||
|       case 0x42: // GAUGE32 |  | ||||||
|       case 0x43: // TIMETICKS |  | ||||||
|       case 0x44: // OPAQUE |  | ||||||
|         { |  | ||||||
|           // All these are essentially unsigned 32-bit integers |  | ||||||
|           const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log(`Parsed ${valueType === 0x41 ? 'COUNTER'  |  | ||||||
|                         : valueType === 0x42 ? 'GAUGE32' |  | ||||||
|                         : valueType === 0x43 ? 'TIMETICKS' |  | ||||||
|                         : 'OPAQUE'} value:`, value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       default: |  | ||||||
|         if (debug) { |  | ||||||
|           console.log(`Unknown value type: 0x${valueType.toString(16)}`); |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Fallback scan-based parsing method |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   private static scanBasedParsing(buffer: Buffer, debug: boolean): any { |  | ||||||
|     // Look for various data types in the response |  | ||||||
|     // The value is near the end of the packet after the OID |  | ||||||
|      |  | ||||||
|     // We're looking for one of these: |  | ||||||
|     // 0x02 - Integer - can be at the end of a varbind |  | ||||||
|     // 0x04 - OctetString |  | ||||||
|     // 0x05 - Null |  | ||||||
|     // 0x42 - Gauge32 - special type for unsigned 32-bit integers |  | ||||||
|     // 0x43 - Timeticks - special type for time values |  | ||||||
|  |  | ||||||
|     // This algorithm performs a thorough search for data types |  | ||||||
|     // by iterating from the start and watching for varbind structures |  | ||||||
|      |  | ||||||
|     // Walk through the buffer looking for varbinds |  | ||||||
|     let i = 0; |  | ||||||
|      |  | ||||||
|     // First, find the varbinds section (0x30 sequence) |  | ||||||
|     while (i < buffer.length - 2) { |  | ||||||
|       // Look for a varbinds sequence |  | ||||||
|       if (buffer[i] === 0x30) { |  | ||||||
|         const varbindsLength = buffer[i + 1]; |  | ||||||
|         const varbindsEnd = i + 2 + varbindsLength; |  | ||||||
|          |  | ||||||
|         // Now search within the varbinds for the value |  | ||||||
|         let j = i + 2; |  | ||||||
|         while (j < varbindsEnd - 2) { |  | ||||||
|           // Look for a varbind (0x30 sequence) |  | ||||||
|           if (buffer[j] === 0x30) { |  | ||||||
|             const varbindLength = buffer[j + 1]; |  | ||||||
|             const varbindEnd = j + 2 + varbindLength; |  | ||||||
|              |  | ||||||
|             // Skip over the OID and find the value within this varbind |  | ||||||
|             let k = j + 2; |  | ||||||
|             while (k < varbindEnd - 1) { |  | ||||||
|               // First find the OID |  | ||||||
|               if (buffer[k] === 0x06) { // OID |  | ||||||
|                 const oidLength = buffer[k + 1]; |  | ||||||
|                 k += 2 + oidLength; // Skip past the OID |  | ||||||
|                  |  | ||||||
|                 // We should now be at the value |  | ||||||
|                 // Check what type it is |  | ||||||
|                 if (k < varbindEnd - 1) { |  | ||||||
|                   return this.parseValueAtPosition(buffer, k, debug); |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 // If we didn't find a value, move to next byte |  | ||||||
|                 k++; |  | ||||||
|               } else { |  | ||||||
|                 // Move to next byte |  | ||||||
|                 k++; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // Move to next varbind |  | ||||||
|             j = varbindEnd; |  | ||||||
|           } else { |  | ||||||
|             // Move to next byte |  | ||||||
|             j++; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Move to next sequence |  | ||||||
|         i = varbindsEnd; |  | ||||||
|       } else { |  | ||||||
|         // Move to next byte |  | ||||||
|         i++; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('No valid value found in SNMP response'); |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Parse value at a specific position in the buffer |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param pos Position of the value in the buffer |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   private static parseValueAtPosition(buffer: Buffer, pos: number, debug: boolean): any { |  | ||||||
|     if (buffer[pos] === 0x02) { // Integer |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Integer value:', value); |  | ||||||
|       } |  | ||||||
|       return value; |  | ||||||
|     } else if (buffer[pos] === 0x42) { // Gauge32 |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Gauge32 value:', value); |  | ||||||
|       } |  | ||||||
|       return value; |  | ||||||
|     } else if (buffer[pos] === 0x43) { // TimeTicks |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Timeticks value:', value); |  | ||||||
|       } |  | ||||||
|       return value; |  | ||||||
|     } else if (buffer[pos] === 0x04) { // OctetString |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found OctetString value'); |  | ||||||
|       } |  | ||||||
|       // Just return the string value as-is |  | ||||||
|       return buffer.slice(pos + 2, pos + 2 + valueLength).toString(); |  | ||||||
|     } else if (buffer[pos] === 0x05) { // Null |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Null value'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Parse an SNMPv3 response |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   public static parseSnmpV3Response(buffer: Buffer, debug: boolean = false): any { |  | ||||||
|     // SNMPv3 parsing is complex. In a real implementation, we would: |  | ||||||
|     // 1. Parse the header and get the security parameters |  | ||||||
|     // 2. Verify authentication if used |  | ||||||
|     // 3. Decrypt the PDU if privacy was used |  | ||||||
|     // 4. Extract the PDU and parse it |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Parsing SNMPv3 response: ', buffer.toString('hex')); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Find the scopedPDU - it should be the last OCTET STRING in the message |  | ||||||
|     let scopedPduPos = -1; |  | ||||||
|     for (let i = buffer.length - 50; i >= 0; i--) { |  | ||||||
|       if (buffer[i] === 0x04 && buffer[i + 1] > 10) { // OCTET STRING with reasonable length |  | ||||||
|         scopedPduPos = i; |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (scopedPduPos === -1) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Could not find scoped PDU in SNMPv3 response'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Skip to the PDU content |  | ||||||
|     let pduContent = buffer.slice(scopedPduPos + 2); // Skip OCTET STRING header |  | ||||||
|      |  | ||||||
|     // This improved algorithm performs a more thorough search for varbinds  |  | ||||||
|     // in the scoped PDU |  | ||||||
|      |  | ||||||
|     // First, look for the response PDU (sequence with tag 0xa2) |  | ||||||
|     let responsePdu = null; |  | ||||||
|     for (let i = 0; i < pduContent.length - 3; i++) { |  | ||||||
|       if (pduContent[i] === 0xa2) { |  | ||||||
|         // Found the response PDU |  | ||||||
|         const pduLength = pduContent[i + 1]; |  | ||||||
|         responsePdu = pduContent.slice(i, i + 2 + pduLength); |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (!responsePdu) { |  | ||||||
|       // Try to find the varbinds directly |  | ||||||
|       for (let i = 0; i < pduContent.length - 3; i++) { |  | ||||||
|         if (pduContent[i] === 0x30) { |  | ||||||
|           const seqLength = pduContent[i + 1]; |  | ||||||
|           if (i + 2 + seqLength <= pduContent.length) { |  | ||||||
|             // Check if this sequence might be the varbinds |  | ||||||
|             const possibleVarbinds = pduContent.slice(i, i + 2 + seqLength); |  | ||||||
|              |  | ||||||
|             // Look for varbind structure inside |  | ||||||
|             for (let j = 0; j < possibleVarbinds.length - 3; j++) { |  | ||||||
|               if (possibleVarbinds[j] === 0x30) { |  | ||||||
|                 // Might be a varbind - look for an OID inside |  | ||||||
|                 for (let k = j; k < j + 10 && k < possibleVarbinds.length - 1; k++) { |  | ||||||
|                   if (possibleVarbinds[k] === 0x06) { |  | ||||||
|                     // Found an OID, so this is likely the varbinds sequence |  | ||||||
|                     responsePdu = possibleVarbinds; |  | ||||||
|                     break; |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 if (responsePdu) break; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             if (responsePdu) break; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (!responsePdu) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Could not find response PDU in SNMPv3 response'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Now that we have the response PDU, search for varbinds |  | ||||||
|     // Skip the first few bytes to get past the header fields |  | ||||||
|     let varbindsPos = -1; |  | ||||||
|     for (let i = 10; i < responsePdu.length - 3; i++) { |  | ||||||
|       if (responsePdu[i] === 0x30) { |  | ||||||
|         // Check if this is the start of the varbinds |  | ||||||
|         // by seeing if it contains a varbind sequence |  | ||||||
|         for (let j = i + 2; j < i + 10 && j < responsePdu.length - 3; j++) { |  | ||||||
|           if (responsePdu[j] === 0x30) { |  | ||||||
|             varbindsPos = i; |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         if (varbindsPos !== -1) break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (varbindsPos === -1) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Could not find varbinds in SNMPv3 response'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Get the varbinds |  | ||||||
|     const varbindsLength = responsePdu[varbindsPos + 1]; |  | ||||||
|     const varbinds = responsePdu.slice(varbindsPos, varbindsPos + 2 + varbindsLength); |  | ||||||
|      |  | ||||||
|     // Now search for values inside the varbinds |  | ||||||
|     for (let i = 2; i < varbinds.length - 3; i++) { |  | ||||||
|       // Look for a varbind sequence |  | ||||||
|       if (varbinds[i] === 0x30) { |  | ||||||
|         const varbindLength = varbinds[i + 1]; |  | ||||||
|         const varbind = varbinds.slice(i, i + 2 + varbindLength); |  | ||||||
|          |  | ||||||
|         // Inside the varbind, look for the OID and then the value |  | ||||||
|         for (let j = 0; j < varbind.length - 3; j++) { |  | ||||||
|           if (varbind[j] === 0x06) { // OID |  | ||||||
|             const oidLength = varbind[j + 1]; |  | ||||||
|              |  | ||||||
|             // The value should be right after the OID |  | ||||||
|             const valuePos = j + 2 + oidLength; |  | ||||||
|             if (valuePos < varbind.length - 1) { |  | ||||||
|               // Check what type of value it is |  | ||||||
|               if (varbind[valuePos] === 0x02) { // INTEGER |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found INTEGER value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } else if (varbind[valuePos] === 0x42) { // Gauge32 |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found Gauge32 value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } else if (varbind[valuePos] === 0x43) { // TimeTicks |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found TimeTicks value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } else if (varbind[valuePos] === 0x04) { // OctetString |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = varbind.slice(valuePos + 2, valuePos + 2 + valueLength).toString(); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found OctetString value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('No valid value found in SNMPv3 response'); |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Extract engine ID from SNMPv3 response |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Extracted engine ID or null if extraction failed |  | ||||||
|    */ |  | ||||||
|   public static extractEngineId(buffer: Buffer, debug: boolean = false): Buffer | null { |  | ||||||
|     try { |  | ||||||
|       // Simple parsing to find the engine ID |  | ||||||
|       // Look for the first octet string with appropriate length |  | ||||||
|       for (let i = 0; i < buffer.length - 10; i++) { |  | ||||||
|         if (buffer[i] === 0x04) { // Octet string |  | ||||||
|           const len = buffer[i + 1]; |  | ||||||
|           if (len >= 5 && len <= 32) { // Engine IDs are typically 5-32 bytes |  | ||||||
|             // Verify this looks like an engine ID (usually starts with 0x80) |  | ||||||
|             if (buffer[i + 2] === 0x80) { |  | ||||||
|               if (debug) { |  | ||||||
|                 console.log('Found engine ID at position', i); |  | ||||||
|                 console.log('Engine ID:', buffer.slice(i + 2, i + 2 + len).toString('hex')); |  | ||||||
|               } |  | ||||||
|               return buffer.slice(i + 2, i + 2 + len); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error('Error extracting engine ID:', error); |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -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 | ||||||
|  */ |  */ | ||||||
| @@ -46,6 +48,8 @@ export interface ISnmpConfig { | |||||||
|   /** Timeout in milliseconds */ |   /** Timeout in milliseconds */ | ||||||
|   timeout: number; |   timeout: number; | ||||||
|  |  | ||||||
|  |   context?: string; | ||||||
|  |  | ||||||
|   // SNMPv1/v2c |   // SNMPv1/v2c | ||||||
|   /** Community string for SNMPv1/v2c */ |   /** Community string for SNMPv1/v2c */ | ||||||
|   community?: string; |   community?: string; | ||||||
|   | |||||||
							
								
								
									
										224
									
								
								ts/systemd.ts
									
									
									
									
									
								
							
							
						
						
									
										224
									
								
								ts/systemd.ts
									
									
									
									
									
								
							| @@ -1,6 +1,8 @@ | |||||||
| 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 { NupstDaemon } from './daemon.ts'; | ||||||
|  | import { logger } from './logger.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for managing systemd service |  * Class for managing systemd service | ||||||
| @@ -13,17 +15,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 | 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 | ||||||
| @@ -47,10 +49,11 @@ WantedBy=multi-user.target | |||||||
|     try { |     try { | ||||||
|       await fs.access(configPath); |       await fs.access(configPath); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error('┌─ Configuration Error ─────────────────────┐'); |       const boxWidth = 50; | ||||||
|       console.error(`│ No configuration file found at ${configPath}`); |       logger.logBoxTitle('Configuration Error', boxWidth); | ||||||
|       console.error('│ Please run \'nupst setup\' first to create a configuration.'); |       logger.logBoxLine(`No configuration file found at ${configPath}`); | ||||||
|       console.error('└──────────────────────────────────────────┘'); |       logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration."); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|       throw new Error('Configuration not found'); |       throw new Error('Configuration not found'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -66,23 +69,24 @@ WantedBy=multi-user.target | |||||||
|  |  | ||||||
|       // Write the service file |       // Write the service file | ||||||
|       await fs.writeFile(this.serviceFilePath, this.serviceTemplate); |       await fs.writeFile(this.serviceFilePath, this.serviceTemplate); | ||||||
|       console.log('┌─ Service Installation ─────────────────────┐'); |       const boxWidth = 50; | ||||||
|       console.log(`│ Service file created at ${this.serviceFilePath}`); |       logger.logBoxTitle('Service Installation', boxWidth); | ||||||
|  |       logger.logBoxLine(`Service file created at ${this.serviceFilePath}`); | ||||||
|  |  | ||||||
|       // Reload systemd daemon |       // Reload systemd daemon | ||||||
|       execSync('systemctl daemon-reload'); |       execSync('systemctl daemon-reload'); | ||||||
|       console.log('│ Systemd daemon reloaded'); |       logger.logBoxLine('Systemd daemon reloaded'); | ||||||
|  |  | ||||||
|       // Enable the service |       // Enable the service | ||||||
|       execSync('systemctl enable nupst.service'); |       execSync('systemctl enable nupst.service'); | ||||||
|       console.log('│ Service enabled to start on boot'); |       logger.logBoxLine('Service enabled to start on boot'); | ||||||
|       console.log('└──────────────────────────────────────────┘'); |       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; | ||||||
|       } |       } | ||||||
|       console.error('Failed to install systemd service:', error); |       logger.error(`Failed to install systemd service: ${error}`); | ||||||
|       throw error; |       throw error; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -97,15 +101,16 @@ WantedBy=multi-user.target | |||||||
|       await this.checkConfigExists(); |       await this.checkConfigExists(); | ||||||
|  |  | ||||||
|       execSync('systemctl start nupst.service'); |       execSync('systemctl start nupst.service'); | ||||||
|       console.log('┌─ Service Status ─────────────────────────┐'); |       const boxWidth = 45; | ||||||
|       console.log('│ NUPST service started successfully'); |       logger.logBoxTitle('Service Status', boxWidth); | ||||||
|       console.log('└──────────────────────────────────────────┘'); |       logger.logBoxLine('NUPST service started successfully'); | ||||||
|  |       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); | ||||||
|       } |       } | ||||||
|       console.error('Failed to start service:', error); |       logger.error(`Failed to start service: ${error}`); | ||||||
|       throw error; |       throw error; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -114,21 +119,31 @@ 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'); | ||||||
|       console.log('NUPST service stopped'); |       logger.success('NUPST service stopped'); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error('Failed to stop service:', error); |       logger.error(`Failed to stop service: ${error}`); | ||||||
|       throw error; |       throw error; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Get status of the systemd service and UPS |    * Get status of the systemd service and UPS | ||||||
|  |    * @param debugMode Whether to enable debug mode for SNMP | ||||||
|    */ |    */ | ||||||
|   public async getStatus(): Promise<void> { |   public async getStatus(debugMode: boolean = false): Promise<void> { | ||||||
|     try { |     try { | ||||||
|  |       // Enable debug mode if requested | ||||||
|  |       if (debugMode) { | ||||||
|  |         const boxWidth = 45; | ||||||
|  |         logger.logBoxTitle('Debug Mode', boxWidth); | ||||||
|  |         logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown'); | ||||||
|  |         logger.logBoxEnd(); | ||||||
|  |         this.daemon.getNupstSnmp().enableDebug(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       // Display version information |       // Display version information | ||||||
|       this.daemon.getNupstSnmp().getNupst().logVersionInfo(); |       this.daemon.getNupstSnmp().getNupst().logVersionInfo(); | ||||||
|  |  | ||||||
| @@ -141,9 +156,11 @@ WantedBy=multi-user.target | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       await this.displayServiceStatus(); |       await this.displayServiceStatus(); | ||||||
|       await this.displayUpsStatus(); |       await this.displayAllUpsStatus(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error(`Failed to get status: ${error.message}`); |       logger.error( | ||||||
|  |         `Failed to get status: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -151,45 +168,126 @@ 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(); | ||||||
|       console.log('┌─ Service Status ─────────────────────────┐'); |       const boxWidth = 45; | ||||||
|       console.log(serviceStatus.split('\n').map(line => `│ ${line}`).join('\n')); |       logger.logBoxTitle('Service Status', boxWidth); | ||||||
|       console.log('└──────────────────────────────────────────┘'); |       // Process each line of the status output | ||||||
|  |       serviceStatus.split('\n').forEach((line) => { | ||||||
|  |         logger.logBoxLine(line); | ||||||
|  |       }); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error('┌─ Service Status ─────────────────────────┐'); |       const boxWidth = 45; | ||||||
|       console.error('│ Service is not running'); |       logger.logBoxTitle('Service Status', boxWidth); | ||||||
|       console.error('└──────────────────────────────────────────┘'); |       logger.logBoxLine('Service is not running'); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Display the UPS status |    * Display all UPS statuses | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private async displayUpsStatus(): Promise<void> { |   private async displayAllUpsStatus(): Promise<void> { | ||||||
|     try { |     try { | ||||||
|  |       // Explicitly load the configuration first to ensure it's up-to-date | ||||||
|  |       await this.daemon.loadConfig(); | ||||||
|       const config = this.daemon.getConfig(); |       const config = this.daemon.getConfig(); | ||||||
|       const snmp = this.daemon.getNupstSnmp(); |       const snmp = this.daemon.getNupstSnmp(); | ||||||
|  |  | ||||||
|       // Create a test config with appropriate timeout, similar to the test command |       // Check if we have the new multi-UPS config format | ||||||
|       const snmpConfig = {  |       if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) { | ||||||
|         ...config.snmp, |         logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`); | ||||||
|         timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check |  | ||||||
|  |         // Show status for each UPS | ||||||
|  |         for (const ups of config.upsDevices) { | ||||||
|  |           await this.displaySingleUpsStatus(ups, snmp); | ||||||
|  |         } | ||||||
|  |       } else if (config.snmp) { | ||||||
|  |         // Legacy single UPS configuration | ||||||
|  |         const legacyUps = { | ||||||
|  |           id: 'default', | ||||||
|  |           name: 'Default UPS', | ||||||
|  |           snmp: config.snmp, | ||||||
|  |           thresholds: config.thresholds, | ||||||
|  |           groups: [], | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         await this.displaySingleUpsStatus(legacyUps, snmp); | ||||||
|  |       } else { | ||||||
|  |         logger.error('No UPS devices found in configuration'); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       const boxWidth = 45; | ||||||
|  |       logger.logBoxTitle('UPS Status', boxWidth); | ||||||
|  |       logger.logBoxLine( | ||||||
|  |         `Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Display status of a single UPS | ||||||
|  |    * @param ups UPS configuration | ||||||
|  |    * @param snmp SNMP manager | ||||||
|  |    */ | ||||||
|  |   private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> { | ||||||
|  |     const boxWidth = 45; | ||||||
|  |     logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth); | ||||||
|  |     logger.logBoxLine(`ID: ${ups.id}`); | ||||||
|  |     logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`); | ||||||
|  |     logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`); | ||||||
|  |  | ||||||
|  |     if (ups.groups && ups.groups.length > 0) { | ||||||
|  |       // Get group names if available | ||||||
|  |       const config = this.daemon.getConfig(); | ||||||
|  |       const groupNames = ups.groups.map((groupId: string) => { | ||||||
|  |         const group = config.groups?.find((g: { id: string }) => g.id === groupId); | ||||||
|  |         return group ? group.name : groupId; | ||||||
|  |       }); | ||||||
|  |       logger.logBoxLine(`Groups: ${groupNames.join(', ')}`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Create a test config with a short timeout | ||||||
|  |       const testConfig = { | ||||||
|  |         ...ups.snmp, | ||||||
|  |         timeout: Math.min(ups.snmp.timeout, 10000), // Use at most 10 seconds for status check | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const status = await snmp.getUpsStatus(snmpConfig); |       const status = await snmp.getUpsStatus(testConfig); | ||||||
|  |  | ||||||
|       console.log('┌─ UPS Status ───────────────────────────────┐'); |       logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); | ||||||
|       console.log(`│ Power Status: ${status.powerStatus}`); |       logger.logBoxLine(`Power Status: ${status.powerStatus}`); | ||||||
|       console.log(`│ Battery Capacity: ${status.batteryCapacity}%`); |       logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`); | ||||||
|       console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`); |       logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`); | ||||||
|       console.log('└──────────────────────────────────────────┘'); |  | ||||||
|  |       // Show threshold status | ||||||
|  |       logger.logBoxLine(''); | ||||||
|  |       logger.logBoxLine('Thresholds:'); | ||||||
|  |       logger.logBoxLine( | ||||||
|  |         `  Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${ | ||||||
|  |           status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓' | ||||||
|  |         }`, | ||||||
|  |       ); | ||||||
|  |       logger.logBoxLine( | ||||||
|  |         `  Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${ | ||||||
|  |           status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓' | ||||||
|  |         }`, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       logger.logBoxEnd(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error('┌─ UPS Status ───────────────────────────────┐'); |       logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); | ||||||
|       console.error(`│ Failed to retrieve UPS status: ${error.message}`); |       logger.logBoxLine( | ||||||
|       console.error('└──────────────────────────────────────────┘'); |         `Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       logger.logBoxEnd(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -205,10 +303,10 @@ WantedBy=multi-user.target | |||||||
|  |  | ||||||
|       // Reload systemd daemon |       // Reload systemd daemon | ||||||
|       execSync('systemctl daemon-reload'); |       execSync('systemctl daemon-reload'); | ||||||
|       console.log('Systemd daemon reloaded'); |       logger.log('Systemd daemon reloaded'); | ||||||
|       console.log('NUPST service has been successfully uninstalled'); |       logger.success('NUPST service has been successfully uninstalled'); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error('Failed to disable and uninstall service:', error); |       logger.error(`Failed to disable and uninstall service: ${error}`); | ||||||
|       throw error; |       throw error; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -217,13 +315,13 @@ 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 { | ||||||
|       console.log('Stopping NUPST service...'); |       logger.log('Stopping NUPST service...'); | ||||||
|       execSync('systemctl stop nupst.service'); |       execSync('systemctl stop nupst.service'); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       // Service might not be running, that's okay |       // Service might not be running, that's okay | ||||||
|       console.log('Service was not running or could not be stopped'); |       logger.log('Service was not running or could not be stopped'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -231,12 +329,12 @@ WantedBy=multi-user.target | |||||||
|    * Disable the service |    * Disable the service | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private async disableService(): Promise<void> { |   private disableService(): void { | ||||||
|     try { |     try { | ||||||
|       console.log('Disabling NUPST service...'); |       logger.log('Disabling NUPST service...'); | ||||||
|       execSync('systemctl disable nupst.service'); |       execSync('systemctl disable nupst.service'); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.log('Service was not enabled or could not be disabled'); |       logger.log('Service was not enabled or could not be disabled'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -246,11 +344,11 @@ WantedBy=multi-user.target | |||||||
|    */ |    */ | ||||||
|   private async removeServiceFile(): Promise<void> { |   private async removeServiceFile(): Promise<void> { | ||||||
|     if (await fs.stat(this.serviceFilePath).catch(() => null)) { |     if (await fs.stat(this.serviceFilePath).catch(() => null)) { | ||||||
|       console.log(`Removing service file ${this.serviceFilePath}...`); |       logger.log(`Removing service file ${this.serviceFilePath}...`); | ||||||
|       await fs.unlink(this.serviceFilePath); |       await fs.unlink(this.serviceFilePath); | ||||||
|       console.log('Service file removed'); |       logger.log('Service file removed'); | ||||||
|     } else { |     } else { | ||||||
|       console.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" |  | ||||||
|   ] |  | ||||||
| } |  | ||||||
							
								
								
									
										65
									
								
								uninstall.sh
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								uninstall.sh
									
									
									
									
									
								
							| @@ -5,13 +5,22 @@ | |||||||
|  |  | ||||||
| # Check if running as root | # Check if running as root | ||||||
| if [ "$EUID" -ne 0 ]; then | if [ "$EUID" -ne 0 ]; then | ||||||
|   echo "Please run as root (sudo ./uninstall.sh)" |   echo "Please run as root (sudo nupst uninstall or sudo ./uninstall.sh)" | ||||||
|   exit 1 |   exit 1 | ||||||
| fi | fi | ||||||
|  |  | ||||||
|  | # This script can be called directly or through the CLI | ||||||
|  | # When called through the CLI, environment variables are set | ||||||
|  | # REMOVE_CONFIG=yes|no - whether to remove configuration files | ||||||
|  | # REMOVE_REPO=yes|no - whether to remove the repository | ||||||
|  |  | ||||||
|  | # If not set through CLI, use defaults | ||||||
|  | REMOVE_CONFIG=${REMOVE_CONFIG:-"no"} | ||||||
|  | REMOVE_REPO=${REMOVE_REPO:-"no"} | ||||||
|  |  | ||||||
| echo "NUPST Uninstaller" | echo "NUPST Uninstaller" | ||||||
| echo "=================" | echo "=================" | ||||||
| echo "This script will completely remove NUPST from your system." | echo "This will completely remove NUPST from your system." | ||||||
|  |  | ||||||
| # Find the directory where this script is located | # Find the directory where this script is located | ||||||
| SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | ||||||
| @@ -37,20 +46,52 @@ if [ -L "/usr/local/bin/nupst" ]; then | |||||||
|   rm -f /usr/local/bin/nupst |   rm -f /usr/local/bin/nupst | ||||||
| fi | fi | ||||||
|  |  | ||||||
| # Step 3: Ask about removing configuration | # Step 3: Remove configuration if requested | ||||||
| read -p "Do you want to remove the NUPST configuration files? (y/N) " -n 1 -r | if [ "$REMOVE_CONFIG" = "yes" ]; then | ||||||
| echo |  | ||||||
| if [[ $REPLY =~ ^[Yy]$ ]]; then |  | ||||||
|   echo "Removing configuration files..." |   echo "Removing configuration files..." | ||||||
|   rm -rf /etc/nupst |   rm -rf /etc/nupst | ||||||
|  | else | ||||||
|  |   # If not called through CLI, ask user | ||||||
|  |   if [ -z "$NUPST_CLI_CALL" ]; then | ||||||
|  |     read -p "Do you want to remove the NUPST configuration files? (y/N) " -n 1 -r | ||||||
|  |     echo | ||||||
|  |     if [[ $REPLY =~ ^[Yy]$ ]]; then | ||||||
|  |       echo "Removing configuration files..." | ||||||
|  |       rm -rf /etc/nupst | ||||||
|  |     fi | ||||||
|  |   fi | ||||||
| fi | fi | ||||||
|  |  | ||||||
| # Step 4: Check if this was a git installation | # Step 4: Remove repository if requested | ||||||
| if [ -d "$SCRIPT_DIR/.git" ]; then | if [ "$REMOVE_REPO" = "yes" ]; then | ||||||
|   echo |   if [ -d "$SCRIPT_DIR/.git" ]; then | ||||||
|   echo "This appears to be a git installation. The local repository will remain intact." |     echo "Removing NUPST repository directory..." | ||||||
|   echo "If you wish to completely remove it, you can delete the directory:" |      | ||||||
|   echo "  rm -rf $SCRIPT_DIR" |     # Get parent directory to remove it after the script exits | ||||||
|  |     PARENT_DIR=$(dirname "$SCRIPT_DIR") | ||||||
|  |     REPO_NAME=$(basename "$SCRIPT_DIR") | ||||||
|  |      | ||||||
|  |     # Create a temporary cleanup script | ||||||
|  |     CLEANUP_SCRIPT=$(mktemp) | ||||||
|  |     echo "#!/bin/bash" > "$CLEANUP_SCRIPT" | ||||||
|  |     echo "sleep 1" >> "$CLEANUP_SCRIPT" | ||||||
|  |     echo "rm -rf \"$SCRIPT_DIR\"" >> "$CLEANUP_SCRIPT" | ||||||
|  |     echo "echo \"NUPST repository has been removed.\"" >> "$CLEANUP_SCRIPT" | ||||||
|  |     chmod +x "$CLEANUP_SCRIPT" | ||||||
|  |      | ||||||
|  |     # Run the cleanup script in the background | ||||||
|  |     nohup "$CLEANUP_SCRIPT" > /dev/null 2>&1 & | ||||||
|  |      | ||||||
|  |     echo "NUPST repository will be removed after uninstaller exits." | ||||||
|  |   else  | ||||||
|  |     echo "No git repository found." | ||||||
|  |   fi | ||||||
|  | else | ||||||
|  |   # If not requested, just display info | ||||||
|  |   if [ -d "$SCRIPT_DIR/.git" ]; then | ||||||
|  |     echo | ||||||
|  |     echo "NUPST repository at $SCRIPT_DIR will remain intact." | ||||||
|  |   fi | ||||||
| fi | fi | ||||||
|  |  | ||||||
| # Check for npm global installation | # Check for npm global installation | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user