Compare commits
	
		
			84 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f750299780 | |||
| ca1039408d | |||
| df3e0b9424 | |||
| c8e5960abd | |||
| 7304a62357 | |||
| a5a88e53ba | |||
| 73bc271c59 | |||
| 1e98181e71 | |||
| eb5a8185ae | |||
| ef3d3f3fa3 | |||
| 34e6e850ad | |||
| 992a776fd2 | |||
| 3e15a2d52f | |||
| d1a3576d31 | |||
| 1ca05e879b | |||
| 9c6fa37eb8 | |||
| ff433b2256 | |||
| 263d69aef1 | |||
| b6b7b43161 | |||
| 316c66c344 | |||
| 4debda856b | |||
| 0e7bcab499 | |||
| 7bf65d8495 | |||
| f2ce0180d3 | |||
| 8c1be6555f | |||
| 1a5558e91f | |||
| 611a9ddd19 | |||
| afd026d08c | |||
| 2c8ea44d40 | |||
| 32bd27b849 | |||
| a7113d0387 | |||
| 61d4e9037a | |||
| caced2718f | |||
| 8516056f84 | |||
| 07ec9d7595 | |||
| d14ba1dd65 | |||
| 7d595fa175 | |||
| df417432b0 | |||
| e5f1ebf343 | |||
| 3ff0dd7ac8 | |||
| bb87316dd3 | |||
| d6e0a1a274 | |||
| 95fa4f8b0b | |||
| c2f2f1e2ee | |||
| 936f86c346 | |||
| 7ff1a7da36 | |||
| a87710144c | |||
| 23fd5cc5cd | |||
| fb4d776bdd | |||
| 88ad16c638 | |||
| 016681b77b | |||
| 49f7a7da8b | |||
| f8269a1cb7 | |||
| b37e1aae6c | |||
| 7076829747 | |||
| 1387ca262b | |||
| 684f034aee | |||
| a63ec16d63 | |||
| 85f34cf96a | |||
| 4d28614e08 | |||
| 567c7be7c5 | |||
| a897a7c780 | |||
| accf137216 | |||
| c3441946cb | |||
| 37ccbf58fd | |||
| 071ded9c41 | |||
| b935087d50 | |||
| e1383097b2 | |||
| dff0ea610b | |||
| 4faa10c494 | |||
| c2d39cc19a | |||
| 9ccbbbdc37 | |||
| 1705ffe2be | |||
| 968cbbd8fc | |||
| a2ae9960b6 | |||
| df6a44d5d9 | |||
| 9efcc4b437 | |||
| 5903ae71be | |||
| a649c598ad | |||
| 5f4f3ecbc3 | |||
| 806f81c6a0 | |||
| 88e353eec6 | |||
| 80ff1b1230 | |||
| 1075335497 | 
							
								
								
									
										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' | ||||||
							
								
								
									
										96
									
								
								bin/nupst
									
									
									
									
									
								
							
							
						
						
									
										96
									
								
								bin/nupst
									
									
									
									
									
								
							| @@ -1,96 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| # NUPST Launcher Script |  | ||||||
| # This script detects architecture and OS, then runs NUPST with the appropriate Node.js binary |  | ||||||
|  |  | ||||||
| # First, handle symlinks correctly |  | ||||||
| REAL_SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}") |  | ||||||
| SCRIPT_DIR=$(dirname "$REAL_SCRIPT_PATH") |  | ||||||
|  |  | ||||||
| # For debugging |  | ||||||
| # echo "Script path: $REAL_SCRIPT_PATH" |  | ||||||
| # echo "Script dir: $SCRIPT_DIR" |  | ||||||
|  |  | ||||||
| # If we're run via symlink from /usr/local/bin, use the hardcoded installation path |  | ||||||
| if [[ "$SCRIPT_DIR" == "/usr/local/bin" ]]; then |  | ||||||
|   PROJECT_ROOT="/opt/nupst" |  | ||||||
| else |  | ||||||
|   # Otherwise, use relative path from script location |  | ||||||
|   PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )" |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # For debugging |  | ||||||
| # echo "Project root: $PROJECT_ROOT" |  | ||||||
|  |  | ||||||
| # Detect architecture and OS |  | ||||||
| ARCH=$(uname -m) |  | ||||||
| OS=$(uname -s) |  | ||||||
|  |  | ||||||
| # Determine Node.js binary location based on architecture and OS |  | ||||||
| NODE_BIN="" |  | ||||||
| case "$OS" in |  | ||||||
|   Linux) |  | ||||||
|     case "$ARCH" in |  | ||||||
|       x86_64) |  | ||||||
|         NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node" |  | ||||||
|         ;; |  | ||||||
|       aarch64|arm64) |  | ||||||
|         NODE_BIN="$PROJECT_ROOT/vendor/node-linux-arm64/bin/node" |  | ||||||
|         ;; |  | ||||||
|       *) |  | ||||||
|         # Use system Node as fallback for other architectures |  | ||||||
|         if command -v node &> /dev/null; then |  | ||||||
|           NODE_BIN="node" |  | ||||||
|           echo "Using system Node.js installation for unsupported architecture: $ARCH" |  | ||||||
|         fi |  | ||||||
|         ;; |  | ||||||
|     esac |  | ||||||
|     ;; |  | ||||||
|   Darwin) |  | ||||||
|     case "$ARCH" in |  | ||||||
|       x86_64) |  | ||||||
|         NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-x64/bin/node" |  | ||||||
|         ;; |  | ||||||
|       arm64) |  | ||||||
|         NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-arm64/bin/node" |  | ||||||
|         ;; |  | ||||||
|       *) |  | ||||||
|         # Use system Node as fallback for other architectures |  | ||||||
|         if command -v node &> /dev/null; then |  | ||||||
|           NODE_BIN="node" |  | ||||||
|           echo "Using system Node.js installation for unsupported architecture: $ARCH" |  | ||||||
|         fi |  | ||||||
|         ;; |  | ||||||
|     esac |  | ||||||
|     ;; |  | ||||||
|   *) |  | ||||||
|     # Use system Node as fallback for other operating systems |  | ||||||
|     if command -v node &> /dev/null; then |  | ||||||
|       NODE_BIN="node" |  | ||||||
|       echo "Using system Node.js installation for unsupported OS: $OS" |  | ||||||
|     fi |  | ||||||
|     ;; |  | ||||||
| esac |  | ||||||
|  |  | ||||||
| # If binary doesn't exist, try system Node as fallback |  | ||||||
| if [ -z "$NODE_BIN" ] || [ ! -f "$NODE_BIN" ]; then |  | ||||||
|   if command -v node &> /dev/null; then |  | ||||||
|     NODE_BIN="node" |  | ||||||
|     echo "Using system Node.js installation" |  | ||||||
|   else   |  | ||||||
|     echo "Error: Node.js binary not found for $OS-$ARCH" |  | ||||||
|     echo "Please run the setup script or install Node.js manually." |  | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Run NUPST with the Node.js binary |  | ||||||
| if [ -f "$PROJECT_ROOT/dist_ts/index.js" ]; then |  | ||||||
|   exec "$NODE_BIN" "$PROJECT_ROOT/dist_ts/index.js" "$@" |  | ||||||
| elif [ -f "$PROJECT_ROOT/dist/index.js" ]; then |  | ||||||
|   exec "$NODE_BIN" "$PROJECT_ROOT/dist/index.js" "$@" |  | ||||||
| else |  | ||||||
|   echo "Error: Could not find NUPST's index.js file." |  | ||||||
|   echo "Please run the setup script to download the required files." |  | ||||||
|   exit 1 |  | ||||||
| fi |  | ||||||
							
								
								
									
										411
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										411
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,6 +1,190 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime | ||||||
|  |  | ||||||
|  | **MAJOR RELEASE: NUPST v4.0 is a complete rewrite powered by Deno** | ||||||
|  |  | ||||||
|  | This release fundamentally changes NUPST's architecture from Node.js-based to Deno-based, | ||||||
|  | distributed as pre-compiled binaries. This is a **breaking change** in terms of installation and | ||||||
|  | distribution, but configuration files from v3.x are **fully compatible**. | ||||||
|  |  | ||||||
|  | ### Breaking Changes | ||||||
|  |  | ||||||
|  | **Installation & Distribution:** | ||||||
|  |  | ||||||
|  | - **Removed**: Node.js runtime dependency - NUPST no longer requires Node.js | ||||||
|  | - **Removed**: npm package distribution (no longer published to npmjs.org) | ||||||
|  | - **Removed**: `bin/nupst` wrapper script | ||||||
|  | - **Removed**: `setup.sh` dependency installation | ||||||
|  | - **Removed**: All Node.js-related files (package.json, tsconfig.json, pnpm-lock.yaml, | ||||||
|  |   npmextra.json) | ||||||
|  | - **Changed**: Installation now downloads pre-compiled binaries instead of cloning repository | ||||||
|  | - **Changed**: Binary-based distribution (~340MB self-contained executables) | ||||||
|  |  | ||||||
|  | **CLI Structure (Backward Compatible):** | ||||||
|  |  | ||||||
|  | - **Changed**: Commands now use subcommand structure (e.g., `nupst service enable` instead of | ||||||
|  |   `nupst enable`) | ||||||
|  | - **Maintained**: Old command format still works with deprecation warnings for smooth migration | ||||||
|  | - **Added**: Aliases for common commands (`nupst ls`, `nupst rm`) | ||||||
|  |  | ||||||
|  | ### New Features | ||||||
|  |  | ||||||
|  | **Distribution & Installation:** | ||||||
|  |  | ||||||
|  | - Pre-compiled binaries for 5 platforms: | ||||||
|  |   - Linux x86_64 | ||||||
|  |   - Linux ARM64 | ||||||
|  |   - macOS x86_64 (Intel) | ||||||
|  |   - macOS ARM64 (Apple Silicon) | ||||||
|  |   - Windows x86_64 | ||||||
|  | - Automated binary releases via Gitea Actions | ||||||
|  | - SHA256 checksum verification for all releases | ||||||
|  | - Installation from Gitea releases via updated `install.sh` | ||||||
|  | - Zero dependencies - completely self-contained binaries | ||||||
|  | - Cross-platform compilation from single codebase | ||||||
|  |  | ||||||
|  | **CI/CD Automation:** | ||||||
|  |  | ||||||
|  | - Gitea Actions workflows for continuous integration | ||||||
|  | - Automated release workflow triggered by git tags | ||||||
|  | - Automatic binary compilation for all platforms on release | ||||||
|  | - Type checking and linting in CI pipeline | ||||||
|  | - Build verification on every push | ||||||
|  |  | ||||||
|  | **CLI Improvements:** | ||||||
|  |  | ||||||
|  | - New hierarchical command structure with subcommands | ||||||
|  | - `nupst service` - Service management (enable, disable, start, stop, restart, status, logs) | ||||||
|  | - `nupst ups` - UPS device management (add, edit, remove, list, test) | ||||||
|  | - `nupst group` - Group management (add, edit, remove, list) | ||||||
|  | - `nupst config show` - Display configuration | ||||||
|  | - `nupst --version` - Show version information | ||||||
|  | - Better help messages organized by category | ||||||
|  | - Backward compatibility maintained with deprecation warnings | ||||||
|  |  | ||||||
|  | **Technical Improvements:** | ||||||
|  |  | ||||||
|  | - Deno runtime for modern TypeScript/JavaScript execution | ||||||
|  | - Native TypeScript support without compilation step | ||||||
|  | - Faster startup and execution compared to Node.js | ||||||
|  | - Smaller memory footprint | ||||||
|  | - Built-in permissions system | ||||||
|  | - No build step required for development | ||||||
|  |  | ||||||
|  | ### Migration Guide | ||||||
|  |  | ||||||
|  | **For Users:** | ||||||
|  |  | ||||||
|  | 1. Stop existing v3.x service: `sudo nupst disable` | ||||||
|  | 2. Install v4.0 using new installer: | ||||||
|  |    `curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y` | ||||||
|  | 3. Your configuration at `/etc/nupst/config.json` is preserved and fully compatible | ||||||
|  | 4. Enable service with new CLI: `sudo nupst service enable && sudo nupst service start` | ||||||
|  | 5. Update systemd commands to use new syntax (old syntax still works with warnings) | ||||||
|  |  | ||||||
|  | **Configuration Compatibility:** | ||||||
|  |  | ||||||
|  | - All configuration files from v3.x work without modification | ||||||
|  | - No changes to `/etc/nupst/config.json` format | ||||||
|  | - All SNMP settings, thresholds, and group configurations preserved | ||||||
|  |  | ||||||
|  | **Command Mapping:** | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Old (v3.x)              → New (v4.0) | ||||||
|  | nupst enable              → nupst service enable | ||||||
|  | nupst disable             → nupst service disable | ||||||
|  | nupst start               → nupst service start | ||||||
|  | nupst stop                → nupst service stop | ||||||
|  | nupst status              → nupst service status | ||||||
|  | nupst logs                → nupst service logs | ||||||
|  | nupst add                 → nupst ups add | ||||||
|  | nupst edit [id]           → nupst ups edit [id] | ||||||
|  | nupst delete <id>         → nupst ups remove <id> | ||||||
|  | nupst list                → nupst ups list | ||||||
|  | nupst test                → nupst ups test | ||||||
|  | nupst group add           → nupst group add (unchanged) | ||||||
|  | nupst group edit <id>     → nupst group edit <id> (unchanged) | ||||||
|  | nupst group delete <id>   → nupst group remove <id> | ||||||
|  | nupst group list          → nupst group list (unchanged) | ||||||
|  | nupst config              → nupst config show | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Technical Details | ||||||
|  |  | ||||||
|  | **Commit History:** | ||||||
|  |  | ||||||
|  | - `df6a44d`: Complete migration with Gitea Actions workflows and install.sh updates | ||||||
|  | - `9efcc4b`: CLI reorganization with subcommand structure | ||||||
|  | - `5903ae7`: Cross-platform compilation scripts | ||||||
|  | - `a649c59`: Deno migration with npm: and node: specifiers | ||||||
|  | - `5f4f3ec`: Initial migration to Deno | ||||||
|  |  | ||||||
|  | **Files Changed:** | ||||||
|  |  | ||||||
|  | - Removed: 11 files (package.json, tsconfig.json, pnpm-lock.yaml, npmextra.json, bin/nupst, | ||||||
|  |   setup.sh) | ||||||
|  | - Added: 3 Gitea Actions workflows (ci.yml, release.yml, README.md) | ||||||
|  | - Modified: 14 TypeScript files for Deno compatibility | ||||||
|  | - Updated: install.sh, .gitignore, readme.md | ||||||
|  | - Net reduction: -10,242 lines (93% reduction in repository size) | ||||||
|  |  | ||||||
|  | **Dependencies:** | ||||||
|  |  | ||||||
|  | - Runtime: Deno v1.x (bundled in binary, no installation required) | ||||||
|  | - SNMP: npm:net-snmp@3.20.0 (bundled in binary via npm: specifier) | ||||||
|  | - Node.js built-ins: Accessed via node: specifier (node:fs, node:child_process, etc.) | ||||||
|  |  | ||||||
|  | ### Benefits | ||||||
|  |  | ||||||
|  | **For Users:** | ||||||
|  |  | ||||||
|  | - **Faster Installation**: Download single binary instead of cloning repo + installing Node.js + npm | ||||||
|  |   dependencies | ||||||
|  | - **Zero Dependencies**: No Node.js or npm required on target system | ||||||
|  | - **Smaller Footprint**: Single binary vs repo + Node.js + node_modules | ||||||
|  | - **Easier Updates**: Download new binary instead of git pull + npm install | ||||||
|  | - **Better Security**: No npm supply chain risks, binary checksums provided | ||||||
|  | - **Platform Support**: Official binaries for all major platforms | ||||||
|  |  | ||||||
|  | **For Developers:** | ||||||
|  |  | ||||||
|  | - **Modern Tooling**: Native TypeScript support without build configuration | ||||||
|  | - **Faster Development**: No compilation step during development | ||||||
|  | - **CI/CD Automation**: Automated releases and testing | ||||||
|  | - **Cleaner Codebase**: 93% reduction in configuration files | ||||||
|  | - **Cross-Platform**: Compile for all platforms from any platform | ||||||
|  |  | ||||||
|  | ### Known Issues | ||||||
|  |  | ||||||
|  | - Windows ARM64 not supported (Deno limitation) | ||||||
|  | - Binary sizes are larger (~340MB) due to bundled runtime (trade-off for zero dependencies) | ||||||
|  | - First-time execution may be slower on some systems (binary extraction) | ||||||
|  |  | ||||||
|  | ### Acknowledgments | ||||||
|  |  | ||||||
|  | This release represents a complete modernization of NUPST's infrastructure while maintaining full | ||||||
|  | backward compatibility for user configurations. Special thanks to the Deno team for creating an | ||||||
|  | excellent runtime that made this migration possible. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## 2025-03-28 - 3.1.2 - fix(cli/ups-handler) | ||||||
|  |  | ||||||
|  | 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) | ## 2025-03-28 - 3.1.0 - feat(cli) | ||||||
|  |  | ||||||
| Refactor CLI commands to use dedicated handlers for UPS, group, and service management | Refactor CLI commands to use dedicated handlers for UPS, group, and service management | ||||||
|  |  | ||||||
| - Extracted UPS-related CLI logic into a new UpsHandler | - Extracted UPS-related CLI logic into a new UpsHandler | ||||||
| @@ -10,28 +194,38 @@ Refactor CLI commands to use dedicated handlers for UPS, group, and service mana | |||||||
| - Exposed getters for the new handlers in the Nupst class | - Exposed getters for the new handlers in the Nupst class | ||||||
|  |  | ||||||
| ## 2025-03-28 - 3.0.1 - fix(cli) | ## 2025-03-28 - 3.0.1 - fix(cli) | ||||||
| Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module and replacing it with the shortId helper. |  | ||||||
|  | Simplify UPS ID generation by removing the redundant promptForUniqueUpsId function in the CLI module | ||||||
|  | and replacing it with the shortId helper. | ||||||
|  |  | ||||||
| - Deleted the unused promptForUniqueUpsId method from ts/cli.ts. | - Deleted the unused promptForUniqueUpsId method from ts/cli.ts. | ||||||
| - Updated UPS configuration to generate a unique ID directly using helpers.shortId(). | - Updated UPS configuration to generate a unique ID directly using helpers.shortId(). | ||||||
| - Improved code clarity by removing unnecessary interactive prompts for UPS IDs. | - Improved code clarity by removing unnecessary interactive prompts for UPS IDs. | ||||||
|  |  | ||||||
| ## 2025-03-28 - 3.0.0 - BREAKING CHANGE(core) | ## 2025-03-28 - 3.0.0 - BREAKING CHANGE(core) | ||||||
| Add multi-UPS support and group management; update CLI, configuration and documentation to support multiple UPS devices with group modes |  | ||||||
|  |  | ||||||
| - Implemented multi-UPS configuration with an array of UPS devices and groups in the configuration file | Add multi-UPS support and group management; update CLI, configuration and documentation to support | ||||||
| - Added group management commands (group add, edit, delete, list) with redundant and non-redundant modes | multiple UPS devices with group modes | ||||||
| - Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group subcommands |  | ||||||
|  | - Implemented multi-UPS configuration with an array of UPS devices and groups in the configuration | ||||||
|  |   file | ||||||
|  | - Added group management commands (group add, edit, delete, list) with redundant and non-redundant | ||||||
|  |   modes | ||||||
|  | - Revamped CLI command parsing for UPS management (add, edit, delete, list, setup) and group | ||||||
|  |   subcommands | ||||||
| - Updated readme and documentation to reflect new configuration structure and features | - Updated readme and documentation to reflect new configuration structure and features | ||||||
| - Enhanced logging and status display for multiple UPS devices | - Enhanced logging and status display for multiple UPS devices | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.17 - fix(logger) | ## 2025-03-26 - 2.6.17 - fix(logger) | ||||||
| Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set width. |  | ||||||
|  | Preserve logbox width after logBoxEnd so that subsequent logBoxLine calls continue using the set | ||||||
|  | width. | ||||||
|  |  | ||||||
| - Removed the reset of currentBoxWidth in logBoxEnd to allow persistent width across logbox calls. | - Removed the reset of currentBoxWidth in logBoxEnd to allow persistent width across logbox calls. | ||||||
| - Ensures that logBoxLine uses the previously set width when no new width is provided. | - Ensures that logBoxLine uses the previously set width when no new width is provided. | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.16 - fix(cli) | ## 2025-03-26 - 2.6.16 - fix(cli) | ||||||
|  |  | ||||||
| Improve CLI logging consistency by replacing direct console output with unified logger calls. | Improve CLI logging consistency by replacing direct console output with unified logger calls. | ||||||
|  |  | ||||||
| - Replaced console.log and console.error with logger.log and logger.error in CLI commands | - Replaced console.log and console.error with logger.log and logger.error in CLI commands | ||||||
| @@ -39,42 +233,51 @@ Improve CLI logging consistency by replacing direct console output with unified | |||||||
| - Enhanced consistency of log output throughout the ts/cli.ts file | - Enhanced consistency of log output throughout the ts/cli.ts file | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.15 - fix(logger) | ## 2025-03-26 - 2.6.15 - fix(logger) | ||||||
|  |  | ||||||
| Replace direct console logging with unified logger interface for consistent formatting | Replace direct console logging with unified logger interface for consistent formatting | ||||||
|  |  | ||||||
| - Substitute console.log, console.error, and related calls with logger methods in cli, daemon, systemd, nupst, and index modules | - Substitute console.log, console.error, and related calls with logger methods in cli, daemon, | ||||||
|  |   systemd, nupst, and index modules | ||||||
| - Integrate logBox formatting for structured output and consistent log presentation | - Integrate logBox formatting for structured output and consistent log presentation | ||||||
| - Update test expectations in test.logger.ts to check for standardized error messages | - Update test expectations in test.logger.ts to check for standardized error messages | ||||||
| - Refactor logging calls throughout the codebase for improved clarity and maintainability | - Refactor logging calls throughout the codebase for improved clarity and maintainability | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.14 - fix(systemd) | ## 2025-03-26 - 2.6.14 - fix(systemd) | ||||||
|  |  | ||||||
| Shorten closing log divider in systemd service installation output for consistent formatting. | Shorten closing log divider in systemd service installation output for consistent formatting. | ||||||
|  |  | ||||||
| - Replaced the overly long footer with a shorter one in ts/systemd.ts. | - Replaced the overly long footer with a shorter one in ts/systemd.ts. | ||||||
| - This change improves log readability without affecting functionality. | - This change improves log readability without affecting functionality. | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.13 - fix(cli) | ## 2025-03-26 - 2.6.13 - fix(cli) | ||||||
|  |  | ||||||
| Fix CLI update output box formatting | Fix CLI update output box formatting | ||||||
|  |  | ||||||
| - Adjusted the closing box line in the update process log messages for consistent visual formatting | - Adjusted the closing box line in the update process log messages for consistent visual formatting | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.12 - fix(systemd) | ## 2025-03-26 - 2.6.12 - fix(systemd) | ||||||
|  |  | ||||||
| Adjust logging border in systemd service installation output | Adjust logging border in systemd service installation output | ||||||
|  |  | ||||||
| - Updated the closing border line for consistent output formatting in ts/systemd.ts | - Updated the closing border line for consistent output formatting in ts/systemd.ts | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.11 - fix(cli, systemd) | ## 2025-03-26 - 2.6.11 - fix(cli, systemd) | ||||||
|  |  | ||||||
| Adjust log formatting for consistent output in CLI and systemd commands | Adjust log formatting for consistent output in CLI and systemd commands | ||||||
|  |  | ||||||
| - Fixed spacing issues in service installation and status log messages in the systemd module. | - Fixed spacing issues in service installation and status log messages in the systemd module. | ||||||
| - Revised output formatting in the CLI to improve message clarity. | - Revised output formatting in the CLI to improve message clarity. | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.10 - fix(daemon) | ## 2025-03-26 - 2.6.10 - fix(daemon) | ||||||
|  |  | ||||||
| Adjust console log box formatting for consistent output in daemon status messages | Adjust console log box formatting for consistent output in daemon status messages | ||||||
|  |  | ||||||
| - Updated closing box borders to align properly in configuration error, periodic updates, and UPS status logs | - Updated closing box borders to align properly in configuration error, periodic updates, and UPS | ||||||
|  |   status logs | ||||||
| - Improved visual consistency in log messages | - Improved visual consistency in log messages | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.9 - fix(cli) | ## 2025-03-26 - 2.6.9 - fix(cli) | ||||||
|  |  | ||||||
| Improve console output formatting for status banners and logging messages | Improve console output formatting for status banners and logging messages | ||||||
|  |  | ||||||
| - Standardize banner messages in daemon status updates | - Standardize banner messages in daemon status updates | ||||||
| @@ -82,19 +285,23 @@ Improve console output formatting for status banners and logging messages | |||||||
| - Update UPS connection and status banners in systemd | - Update UPS connection and status banners in systemd | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.8 - fix(cli) | ## 2025-03-26 - 2.6.8 - fix(cli) | ||||||
| Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager |  | ||||||
|  | Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP | ||||||
|  | manager | ||||||
|  |  | ||||||
| - Standardize whitespace and formatting in ts/cli.ts for consistency | - Standardize whitespace and formatting in ts/cli.ts for consistency | ||||||
| - Refine argument filtering for debug mode and prompt messages | - Refine argument filtering for debug mode and prompt messages | ||||||
| - Remove unused 'dgram' import from ts/snmp/manager.ts | - Remove unused 'dgram' import from ts/snmp/manager.ts | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.7 - fix(setup.sh) | ## 2025-03-26 - 2.6.7 - fix(setup.sh) | ||||||
|  |  | ||||||
| Clarify net-snmp dependency installation message in setup.sh | Clarify net-snmp dependency installation message in setup.sh | ||||||
|  |  | ||||||
| - Updated echo statement to indicate installation of net-snmp along with 2 subdependencies | - Updated echo statement to indicate installation of net-snmp along with 2 subdependencies | ||||||
| - Improves clarity on dependency installation during setup | - Improves clarity on dependency installation during setup | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.6 - fix(setup.sh) | ## 2025-03-26 - 2.6.6 - fix(setup.sh) | ||||||
|  |  | ||||||
| Improve setup script to detect and execute npm-cli.js directly using the Node.js binary | Improve setup script to detect and execute npm-cli.js directly using the Node.js binary | ||||||
|  |  | ||||||
| - Replace use of the npm binary with direct execution of npm-cli.js | - Replace use of the npm binary with direct execution of npm-cli.js | ||||||
| @@ -102,14 +309,18 @@ Improve setup script to detect and execute npm-cli.js directly using the Node.js | |||||||
| - Simplify cleanup by removing unnecessary PATH modifications | - Simplify cleanup by removing unnecessary PATH modifications | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.5 - fix(daemon, setup) | ## 2025-03-26 - 2.6.5 - fix(daemon, setup) | ||||||
| Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths |  | ||||||
|  | Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm | ||||||
|  | paths | ||||||
|  |  | ||||||
| - Use execFileAsync to execute shutdown commands reliably | - Use execFileAsync to execute shutdown commands reliably | ||||||
| - Add multiple fallback alternatives for shutdown and emergency shutdown handling | - Add multiple fallback alternatives for shutdown and emergency shutdown handling | ||||||
| - Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH | - Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.4 - fix(setup) | ## 2025-03-26 - 2.6.4 - fix(setup) | ||||||
| Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation. |  | ||||||
|  | Improve installation process in setup script by cleaning up package files and ensuring a minimal | ||||||
|  | net-snmp dependency installation. | ||||||
|  |  | ||||||
| - Remove existing package-lock.json along with node_modules to prevent stale artifacts. | - Remove existing package-lock.json along with node_modules to prevent stale artifacts. | ||||||
| - Back up the original package.json before modifying it. | - Back up the original package.json before modifying it. | ||||||
| @@ -118,13 +329,16 @@ Improve installation process in setup script by cleaning up package files and en | |||||||
| - Restore the original package.json if the installation fails. | - Restore the original package.json if the installation fails. | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.3 - fix(setup) | ## 2025-03-26 - 2.6.3 - fix(setup) | ||||||
| Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control. |  | ||||||
|  | Update setup script to install only net-snmp dependency and create a minimal package-lock.json for | ||||||
|  | better dependency control. | ||||||
|  |  | ||||||
| - Removed full production dependency install in favor of installing only net-snmp@3.20.0 | - Removed full production dependency install in favor of installing only net-snmp@3.20.0 | ||||||
| - Added verification step to confirm net-snmp installation | - Added verification step to confirm net-snmp installation | ||||||
| - Generate a minimal package-lock.json if one does not exist | - Generate a minimal package-lock.json if one does not exist | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.2 - fix(setup/readme) | ## 2025-03-26 - 2.6.2 - fix(setup/readme) | ||||||
|  |  | ||||||
| Improve force update instructions and dependency installation process in setup.sh and readme.md | Improve force update instructions and dependency installation process in setup.sh and readme.md | ||||||
|  |  | ||||||
| - Clarify force update commands with explicit paths in readme.md | - Clarify force update commands with explicit paths in readme.md | ||||||
| @@ -132,13 +346,16 @@ Improve force update instructions and dependency installation process in setup.s | |||||||
| - Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions | - Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.1 - fix(setup) | ## 2025-03-26 - 2.6.1 - fix(setup) | ||||||
| Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards. |  | ||||||
|  | Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log | ||||||
|  | Node and npm versions, and restore the original PATH afterwards. | ||||||
|  |  | ||||||
| - Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution. | - Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution. | ||||||
| - Log Node.js and npm versions for debugging purposes. | - Log Node.js and npm versions for debugging purposes. | ||||||
| - Restore the original PATH after installing dependencies. | - Restore the original PATH after installing dependencies. | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.6.0 - feat(setup) | ## 2025-03-26 - 2.6.0 - feat(setup) | ||||||
|  |  | ||||||
| Add --force update flag to setup script and update installation instructions | Add --force update flag to setup script and update installation instructions | ||||||
|  |  | ||||||
| - Implemented --force option in setup.sh to force-update Node.js binary and dependencies | - Implemented --force option in setup.sh to force-update Node.js binary and dependencies | ||||||
| @@ -146,27 +363,33 @@ Add --force update flag to setup script and update installation instructions | |||||||
| - Modified ts/cli.ts update command to pass the --force flag to setup.sh | - Modified ts/cli.ts update command to pass the --force flag to setup.sh | ||||||
|  |  | ||||||
| ## 2025-03-26 - 2.5.2 - fix(installer) | ## 2025-03-26 - 2.5.2 - fix(installer) | ||||||
|  |  | ||||||
| Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic | Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic | ||||||
|  |  | ||||||
| - Enhanced bin/nupst to detect OS and architecture (Linux and Darwin) and fall back to system Node.js for unsupported platforms | - Enhanced bin/nupst to detect OS and architecture (Linux and Darwin) and fall back to system | ||||||
|  |   Node.js for unsupported platforms | ||||||
| - Moved net-snmp from devDependencies to dependencies in package.json | - Moved net-snmp from devDependencies to dependencies in package.json | ||||||
| - Updated setup.sh to install production dependencies and handle installation errors gracefully | - Updated setup.sh to install production dependencies and handle installation errors gracefully | ||||||
| - Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts | - Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts | ||||||
| - Revised README to clarify minimal runtime dependencies and secure SNMP features | - Revised README to clarify minimal runtime dependencies and secure SNMP features | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.5.1 - fix(snmp) | ## 2025-03-25 - 2.5.1 - fix(snmp) | ||||||
|  |  | ||||||
| Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion. | Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion. | ||||||
|  |  | ||||||
| - Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery status. | - Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery | ||||||
|  |   status. | ||||||
| - Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager. | - Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager. | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.5.0 - feat(cli) | ## 2025-03-25 - 2.5.0 - feat(cli) | ||||||
|  |  | ||||||
| Automatically restart running NUPST service after configuration changes in interactive setup | Automatically restart running NUPST service after configuration changes in interactive setup | ||||||
|  |  | ||||||
| - Added restartServiceIfRunning() to check and restart the service if it's active. | - Added restartServiceIfRunning() to check and restart the service if it's active. | ||||||
| - Invoked the restart function post-setup to apply configuration changes immediately. | - Invoked the restart function post-setup to apply configuration changes immediately. | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.8 - fix(installer) | ## 2025-03-25 - 2.4.8 - fix(installer) | ||||||
|  |  | ||||||
| Improve Git dependency handling and repository cloning in install.sh | Improve Git dependency handling and repository cloning in install.sh | ||||||
|  |  | ||||||
| - Add explicit check for git installation and prompt the user interactively if git is missing. | - Add explicit check for git installation and prompt the user interactively if git is missing. | ||||||
| @@ -174,23 +397,30 @@ Improve Git dependency handling and repository cloning in install.sh | |||||||
| - Ensure proper cloning of the repository when running the installer outside the repo. | - Ensure proper cloning of the repository when running the installer outside the repo. | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.7 - fix(readme) | ## 2025-03-25 - 2.4.7 - fix(readme) | ||||||
|  |  | ||||||
| Update installation instructions to combine download and execution into a single command for clarity | Update installation instructions to combine download and execution into a single command for clarity | ||||||
|  |  | ||||||
| - Method 1 now uses a unified one-line command to download and run the install script | - Method 1 now uses a unified one-line command to download and run the install script | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.6 - fix(installer) | ## 2025-03-25 - 2.4.6 - fix(installer) | ||||||
|  |  | ||||||
| Improve installation instructions for interactive and non-interactive setups | Improve installation instructions for interactive and non-interactive setups | ||||||
|  |  | ||||||
| - Changed install.sh to require explicit download of the install script and updated error messages for non-interactive modes | - Changed install.sh to require explicit download of the install script and updated error messages | ||||||
|  |   for non-interactive modes | ||||||
| - Updated readme.md to include three distinct installation methods with clear command examples | - Updated readme.md to include three distinct installation methods with clear command examples | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.5 - fix(install) | ## 2025-03-25 - 2.4.5 - fix(install) | ||||||
|  |  | ||||||
| Improve interactive terminal detection and update installation instructions | Improve interactive terminal detection and update installation instructions | ||||||
|  |  | ||||||
| - Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for both interactive and non-interactive installations | - Enhanced install.sh to better detect non-interactive environments and provide clearer guidance for | ||||||
| - Updated README.md quick install instructions to recommend process substitution and clarify auto-yes usage |   both interactive and non-interactive installations | ||||||
|  | - Updated README.md quick install instructions to recommend process substitution and clarify | ||||||
|  |   auto-yes usage | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.4 - fix(install) | ## 2025-03-25 - 2.4.4 - fix(install) | ||||||
|  |  | ||||||
| Improve interactive mode detection and non-interactive installation handling in install.sh | Improve interactive mode detection and non-interactive installation handling in install.sh | ||||||
|  |  | ||||||
| - Detect and warn when running without a controlling terminal | - Detect and warn when running without a controlling terminal | ||||||
| @@ -199,86 +429,116 @@ Improve interactive mode detection and non-interactive installation handling in | |||||||
| - Clarify installation instructions in readme for interactive and non-interactive modes | - Clarify installation instructions in readme for interactive and non-interactive modes | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.3 - fix(readme) | ## 2025-03-25 - 2.4.3 - fix(readme) | ||||||
|  |  | ||||||
| Update Quick Install command syntax in readme for auto-yes installation | Update Quick Install command syntax in readme for auto-yes installation | ||||||
|  |  | ||||||
| - Changed installation command to use: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -c "bash -s -- -y" | - Changed installation command to use: curl -sSL | ||||||
|  |   https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -c "bash -s -- | ||||||
|  |   -y" | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.2 - fix(daemon) | ## 2025-03-25 - 2.4.2 - fix(daemon) | ||||||
| Refactor shutdown initiation logic in daemon by moving the initiateShutdown and monitorDuringShutdown methods from the SNMP manager to the daemon, and update calls accordingly |  | ||||||
|  | Refactor shutdown initiation logic in daemon by moving the initiateShutdown and | ||||||
|  | monitorDuringShutdown methods from the SNMP manager to the daemon, and update calls accordingly | ||||||
|  |  | ||||||
| - Moved initiateShutdown and monitorDuringShutdown to the daemon class for improved cohesion | - Moved initiateShutdown and monitorDuringShutdown to the daemon class for improved cohesion | ||||||
| - Updated references in the daemon to call its own shutdown method instead of the SNMP manager | - Updated references in the daemon to call its own shutdown method instead of the SNMP manager | ||||||
| - Removed redundant initiateShutdown method from the SNMP manager | - Removed redundant initiateShutdown method from the SNMP manager | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.1 - fix(docs) | ## 2025-03-25 - 2.4.1 - fix(docs) | ||||||
|  |  | ||||||
| Update readme with detailed legal and trademark guidance | Update readme with detailed legal and trademark guidance | ||||||
|  |  | ||||||
| - Clarified legal section by adding trademark and company information | - Clarified legal section by adding trademark and company information | ||||||
| - Ensured users understand that licensing terms do not imply endorsement by the company | - Ensured users understand that licensing terms do not imply endorsement by the company | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.0 - feat(installer) | ## 2025-03-25 - 2.4.0 - feat(installer) | ||||||
|  |  | ||||||
| Add auto-yes flag to installer and update installation documentation | Add auto-yes flag to installer and update installation documentation | ||||||
|  |  | ||||||
| - Enhance install.sh to parse -y/--yes and -h/--help options, automating git installation when auto-yes is provided | - Enhance install.sh to parse -y/--yes and -h/--help options, automating git installation when | ||||||
|  |   auto-yes is provided | ||||||
| - Improve user prompts for dependency installation and provide clearer instructions | - Improve user prompts for dependency installation and provide clearer instructions | ||||||
| - Update readme.md to document new installer options and enhanced file system and service changes details | - Update readme.md to document new installer options and enhanced file system and service changes | ||||||
|  |   details | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.3.0 - feat(installer/cli) | ## 2025-03-25 - 2.3.0 - feat(installer/cli) | ||||||
| Add OS detection and git auto-installation support to install.sh and improve service setup prompt in CLI |  | ||||||
|  |  | ||||||
| - Implemented helper functions in install.sh to detect OS type and automatically install git if missing | Add OS detection and git auto-installation support to install.sh and improve service setup prompt in | ||||||
|  | CLI | ||||||
|  |  | ||||||
|  | - Implemented helper functions in install.sh to detect OS type and automatically install git if | ||||||
|  |   missing | ||||||
| - Prompt user for git installation if not present before cloning the repository | - Prompt user for git installation if not present before cloning the repository | ||||||
| - Enhanced CLI service setup flow to offer starting the NUPST service immediately after installation | - Enhanced CLI service setup flow to offer starting the NUPST service immediately after installation | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.2.0 - feat(cli) | ## 2025-03-25 - 2.2.0 - feat(cli) | ||||||
|  |  | ||||||
| Add 'config' command to display current configuration and update CLI help | Add 'config' command to display current configuration and update CLI help | ||||||
|  |  | ||||||
| - Introduce new 'config' command to show SNMP settings, thresholds, and configuration file location | - Introduce new 'config' command to show SNMP settings, thresholds, and configuration file location | ||||||
| - Update help text to include details for 'nupst config' command | - Update help text to include details for 'nupst config' command | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.1.0 - feat(cli) | ## 2025-03-25 - 2.1.0 - feat(cli) | ||||||
|  |  | ||||||
| Add uninstall command to CLI and update shutdown delay for graceful VM shutdown | Add uninstall command to CLI and update shutdown delay for graceful VM shutdown | ||||||
|  |  | ||||||
| - Implement uninstall command in ts/cli.ts that locates and executes uninstall.sh with user prompts | - Implement uninstall command in ts/cli.ts that locates and executes uninstall.sh with user prompts | ||||||
| - Update uninstall.sh to support environment variables for configuration and repository removal | - Update uninstall.sh to support environment variables for configuration and repository removal | ||||||
| - Increase shutdown delay in ts/snmp/manager.ts from 1 minute to 5 minutes to allow VMs more time to shut down | - Increase shutdown delay in ts/snmp/manager.ts from 1 minute to 5 minutes to allow VMs more time to | ||||||
|  |   shut down | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.0.1 - fix(cli/systemd) | ## 2025-03-25 - 2.0.1 - fix(cli/systemd) | ||||||
|  |  | ||||||
| Fix status command to pass debug flag and improve systemd status logging output | Fix status command to pass debug flag and improve systemd status logging output | ||||||
|  |  | ||||||
| - ts/cli.ts: Now extracts debug options from process arguments and passes debug mode to getStatus. | - ts/cli.ts: Now extracts debug options from process arguments and passes debug mode to getStatus. | ||||||
| - ts/systemd.ts: Updated getStatus to accept a debugMode parameter, enabling detailed SNMP debug logging, explicitly reloading configuration, and printing connection details. | - ts/systemd.ts: Updated getStatus to accept a debugMode parameter, enabling detailed SNMP debug | ||||||
|  |   logging, explicitly reloading configuration, and printing connection details. | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.0.0 - BREAKING CHANGE(snmp) | ## 2025-03-25 - 2.0.0 - BREAKING CHANGE(snmp) | ||||||
|  |  | ||||||
| refactor: update SNMP type definitions and interface names for consistency | refactor: update SNMP type definitions and interface names for consistency | ||||||
|  |  | ||||||
| - Renamed SnmpConfig to ISnmpConfig, OIDSet to IOidSet, UpsStatus to IUpsStatus, and UpsModel to TUpsModel in ts/snmp/types.ts. | - Renamed SnmpConfig to ISnmpConfig, OIDSet to IOidSet, UpsStatus to IUpsStatus, and UpsModel to | ||||||
| - Updated internal references in ts/daemon.ts, ts/snmp/index.ts, ts/snmp/manager.ts, ts/snmp/oid-sets.ts, ts/snmp/packet-creator.ts, and ts/snmp/packet-parser.ts to use the new interface names. |   TUpsModel in ts/snmp/types.ts. | ||||||
|  | - Updated internal references in ts/daemon.ts, ts/snmp/index.ts, ts/snmp/manager.ts, | ||||||
|  |   ts/snmp/oid-sets.ts, ts/snmp/packet-creator.ts, and ts/snmp/packet-parser.ts to use the new | ||||||
|  |   interface names. | ||||||
|  |  | ||||||
| ## 2025-03-25 - 1.10.1 - fix(systemd/readme) | ## 2025-03-25 - 1.10.1 - fix(systemd/readme) | ||||||
|  |  | ||||||
| Improve README documentation and fix UPS status retrieval in systemd service | Improve README documentation and fix UPS status retrieval in systemd service | ||||||
|  |  | ||||||
| - Updated README features and installation instructions to clarify SNMP version support, UPS models, and configuration | - Updated README features and installation instructions to clarify SNMP version support, UPS models, | ||||||
| - Modified default SNMP host to '192.168.1.100' and added 'upsModel' property in configuration examples |   and configuration | ||||||
|  | - Modified default SNMP host to '192.168.1.100' and added 'upsModel' property in configuration | ||||||
|  |   examples | ||||||
| - Enhanced instructions for real-time log viewing and update process in README | - Enhanced instructions for real-time log viewing and update process in README | ||||||
| - Fixed systemd.ts to use a test configuration with an appropriate timeout when fetching UPS status | - Fixed systemd.ts to use a test configuration with an appropriate timeout when fetching UPS status | ||||||
|  |  | ||||||
| ## 2025-03-25 - 1.10.0 - feat(core) | ## 2025-03-25 - 1.10.0 - feat(core) | ||||||
|  |  | ||||||
| Add update checking and version logging across startup components | Add update checking and version logging across startup components | ||||||
|  |  | ||||||
| - In daemon.ts, log version info on startup and check for updates in the background using npm registry response | - In daemon.ts, log version info on startup and check for updates in the background using npm | ||||||
| - In nupst.ts, implement getVersion, checkForUpdates, getUpdateStatus, and compareVersions functions with update notifications |   registry response | ||||||
|  | - In nupst.ts, implement getVersion, checkForUpdates, getUpdateStatus, and compareVersions functions | ||||||
|  |   with update notifications | ||||||
| - Establish bidirectional reference between Nupst and NupstSnmp to support version logging | - Establish bidirectional reference between Nupst and NupstSnmp to support version logging | ||||||
| - Update systemd service status output to include version information | - Update systemd service status output to include version information | ||||||
|  |  | ||||||
| ## 2025-03-25 - 1.9.0 - feat(cli) | ## 2025-03-25 - 1.9.0 - feat(cli) | ||||||
|  |  | ||||||
| Add update command to CLI to update NUPST from repository and refresh the systemd service | Add update command to CLI to update NUPST from repository and refresh the systemd service | ||||||
|  |  | ||||||
| - Integrate 'update' subcommand in CLI command parser | - Integrate 'update' subcommand in CLI command parser | ||||||
| - Update documentation and help output to include new command | - Update documentation and help output to include new command | ||||||
| - Implement update process that fetches changes from git, runs install.sh/setup.sh, and refreshes systemd service if installed | - Implement update process that fetches changes from git, runs install.sh/setup.sh, and refreshes | ||||||
|  |   systemd service if installed | ||||||
|  |  | ||||||
| ## 2025-03-25 - 1.8.2 - fix(cli) | ## 2025-03-25 - 1.8.2 - fix(cli) | ||||||
|  |  | ||||||
| Refactor logs command to use child_process spawn for real-time log tailing | Refactor logs command to use child_process spawn for real-time log tailing | ||||||
|  |  | ||||||
| - Replaced execSync call with spawn to properly follow logs | - Replaced execSync call with spawn to properly follow logs | ||||||
| @@ -286,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 | ||||||
| @@ -300,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 | ||||||
| @@ -323,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 | ||||||
| @@ -337,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 | ||||||
| @@ -353,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 | ||||||
| @@ -361,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 | ||||||
| @@ -373,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. | ||||||
| @@ -386,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 | ||||||
| @@ -393,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 | ||||||
| @@ -407,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 | ||||||
| @@ -425,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": "5.0.3", | ||||||
|  |   "exports": "./mod.ts", | ||||||
|  |   "tasks": { | ||||||
|  |     "dev": "deno run --allow-all mod.ts", | ||||||
|  |     "compile": "deno task compile:all", | ||||||
|  |     "compile:all": "bash scripts/compile-all.sh", | ||||||
|  |     "test": "deno test --allow-all test/", | ||||||
|  |     "test:watch": "deno test --allow-all --watch test/", | ||||||
|  |     "check": "deno check mod.ts", | ||||||
|  |     "fmt": "deno fmt", | ||||||
|  |     "lint": "deno lint" | ||||||
|  |   }, | ||||||
|  |   "lint": { | ||||||
|  |     "rules": { | ||||||
|  |       "tags": ["recommended"] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "fmt": { | ||||||
|  |     "useTabs": false, | ||||||
|  |     "lineWidth": 100, | ||||||
|  |     "indentWidth": 2, | ||||||
|  |     "semiColons": true, | ||||||
|  |     "singleQuote": true | ||||||
|  |   }, | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "lib": ["deno.window"], | ||||||
|  |     "strict": true | ||||||
|  |   }, | ||||||
|  |   "imports": { | ||||||
|  |     "@std/cli": "jsr:@std/cli@^1.0.0", | ||||||
|  |     "@std/fmt": "jsr:@std/fmt@^1.0.0", | ||||||
|  |     "@std/path": "jsr:@std/path@^1.0.0" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										122
									
								
								example-action.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								example-action.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # NUPST Action Script Example | ||||||
|  | # Copy this to /etc/nupst/ and customize for your needs | ||||||
|  | # | ||||||
|  | # This script is called by NUPST when power events or threshold violations occur. | ||||||
|  | # It receives UPS state information via environment variables and command-line arguments. | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # ARGUMENTS (positional parameters) | ||||||
|  | # ============================================================================== | ||||||
|  | # $1 = Power Status (online|onBattery|unknown) | ||||||
|  | # $2 = Battery Capacity (percentage, 0-100) | ||||||
|  | # $3 = Battery Runtime (estimated minutes remaining) | ||||||
|  |  | ||||||
|  | POWER_STATUS=$1 | ||||||
|  | BATTERY_CAPACITY=$2 | ||||||
|  | BATTERY_RUNTIME=$3 | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # ENVIRONMENT VARIABLES | ||||||
|  | # ============================================================================== | ||||||
|  | # NUPST_UPS_ID               - Unique UPS identifier | ||||||
|  | # NUPST_UPS_NAME             - Human-readable UPS name | ||||||
|  | # NUPST_POWER_STATUS         - Current power status | ||||||
|  | # NUPST_BATTERY_CAPACITY     - Battery percentage (0-100) | ||||||
|  | # NUPST_BATTERY_RUNTIME      - Estimated runtime in minutes | ||||||
|  | # NUPST_THRESHOLDS_EXCEEDED  - "true" if below configured thresholds | ||||||
|  | # NUPST_TRIGGER_REASON       - "powerStatusChange" or "thresholdViolation" | ||||||
|  | # NUPST_BATTERY_THRESHOLD    - Configured battery threshold percentage | ||||||
|  | # NUPST_RUNTIME_THRESHOLD    - Configured runtime threshold in minutes | ||||||
|  | # NUPST_TIMESTAMP            - Unix timestamp (milliseconds since epoch) | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # EXAMPLE: Log the event | ||||||
|  | # ============================================================================== | ||||||
|  | LOG_FILE="/var/log/nupst-actions.log" | ||||||
|  |  | ||||||
|  | echo "========================================" >> "$LOG_FILE" | ||||||
|  | echo "NUPST Action Triggered: $(date)" >> "$LOG_FILE" | ||||||
|  | echo "----------------------------------------" >> "$LOG_FILE" | ||||||
|  | echo "UPS: $NUPST_UPS_NAME ($NUPST_UPS_ID)" >> "$LOG_FILE" | ||||||
|  | echo "Power Status: $POWER_STATUS" >> "$LOG_FILE" | ||||||
|  | echo "Battery: $BATTERY_CAPACITY%" >> "$LOG_FILE" | ||||||
|  | echo "Runtime: $BATTERY_RUNTIME minutes" >> "$LOG_FILE" | ||||||
|  | echo "Trigger Reason: $NUPST_TRIGGER_REASON" >> "$LOG_FILE" | ||||||
|  | echo "Thresholds Exceeded: $NUPST_THRESHOLDS_EXCEEDED" >> "$LOG_FILE" | ||||||
|  | echo "========================================" >> "$LOG_FILE" | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # EXAMPLE: Send email notification | ||||||
|  | # ============================================================================== | ||||||
|  | # if [ "$NUPST_TRIGGER_REASON" = "thresholdViolation" ]; then | ||||||
|  | #   echo "ALERT: UPS $NUPST_UPS_NAME battery critical!" | \ | ||||||
|  | #     mail -s "UPS Battery Critical" admin@example.com | ||||||
|  | # fi | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # EXAMPLE: Gracefully shutdown virtual machines | ||||||
|  | # ============================================================================== | ||||||
|  | # if [ "$NUPST_POWER_STATUS" = "onBattery" ] && [ "$NUPST_THRESHOLDS_EXCEEDED" = "true" ]; then | ||||||
|  | #   echo "Shutting down VMs..." >> "$LOG_FILE" | ||||||
|  | #   # virsh shutdown vm1 | ||||||
|  | #   # virsh shutdown vm2 | ||||||
|  | #   # Wait for VMs to shutdown | ||||||
|  | #   # sleep 120 | ||||||
|  | # fi | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # EXAMPLE: Call external API/service | ||||||
|  | # ============================================================================== | ||||||
|  | # curl -X POST https://monitoring.example.com/ups-alert \ | ||||||
|  | #   -H "Content-Type: application/json" \ | ||||||
|  | #   -d "{ | ||||||
|  | #     \"upsId\": \"$NUPST_UPS_ID\", | ||||||
|  | #     \"upsName\": \"$NUPST_UPS_NAME\", | ||||||
|  | #     \"powerStatus\": \"$POWER_STATUS\", | ||||||
|  | #     \"batteryCapacity\": $BATTERY_CAPACITY, | ||||||
|  | #     \"batteryRuntime\": $BATTERY_RUNTIME, | ||||||
|  | #     \"triggerReason\": \"$NUPST_TRIGGER_REASON\" | ||||||
|  | #   }" | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # EXAMPLE: Remote shutdown via SSH with password | ||||||
|  | # ============================================================================== | ||||||
|  | # You can implement custom shutdown logic for remote systems | ||||||
|  | # that require password authentication or webhooks | ||||||
|  | # | ||||||
|  | # if [ "$NUPST_THRESHOLDS_EXCEEDED" = "true" ]; then | ||||||
|  | #   # Call a webhook with a secret password/token | ||||||
|  | #   curl -X POST "https://remote-server.local/shutdown?token=YOUR_SECRET_TOKEN" | ||||||
|  | # | ||||||
|  | #   # Or use SSH with password (requires sshpass) | ||||||
|  | #   # sshpass -p 'your-password' ssh user@remote-server 'sudo shutdown -h +5' | ||||||
|  | # fi | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # EXAMPLE: Conditional logic based on battery level | ||||||
|  | # ============================================================================== | ||||||
|  | # if [ "$BATTERY_CAPACITY" -lt 20 ]; then | ||||||
|  | #   echo "Battery critically low! Immediate action needed." >> "$LOG_FILE" | ||||||
|  | # elif [ "$BATTERY_CAPACITY" -lt 50 ]; then | ||||||
|  | #   echo "Battery low. Preparing for shutdown." >> "$LOG_FILE" | ||||||
|  | # else | ||||||
|  | #   echo "Battery acceptable. Monitoring." >> "$LOG_FILE" | ||||||
|  | # fi | ||||||
|  |  | ||||||
|  | # ============================================================================== | ||||||
|  | # EXAMPLE: Different actions for different trigger reasons | ||||||
|  | # ============================================================================== | ||||||
|  | # case "$NUPST_TRIGGER_REASON" in | ||||||
|  | #   powerStatusChange) | ||||||
|  | #     echo "Power status changed to: $POWER_STATUS" >> "$LOG_FILE" | ||||||
|  | #     # Send notification but don't take drastic action yet | ||||||
|  | #     ;; | ||||||
|  | #   thresholdViolation) | ||||||
|  | #     echo "Thresholds violated! Taking emergency action." >> "$LOG_FILE" | ||||||
|  | #     # Initiate graceful shutdowns, save data, etc. | ||||||
|  | #     ;; | ||||||
|  | # esac | ||||||
|  |  | ||||||
|  | # Exit with success | ||||||
|  | exit 0 | ||||||
							
								
								
									
										459
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										459
									
								
								install.sh
									
									
									
									
									
								
							| @@ -1,44 +1,69 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
|  |  | ||||||
| # NUPST Installer Script | # NUPST Installer Script (v5.0+) | ||||||
| # Downloads and installs NUPST globally on the system | # Downloads and installs pre-compiled NUPST binary from Gitea releases | ||||||
| # Can be used directly with curl: | # | ||||||
| # Without auto-installing dependencies: | # Usage: | ||||||
|  | #   Direct piped installation (recommended): | ||||||
| #     curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash | #     curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash | ||||||
| # With auto-installing dependencies: | # | ||||||
| #   curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y | #   With version specification: | ||||||
|  | #     curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0 | ||||||
| # | # | ||||||
| # Options: | # Options: | ||||||
| #   -y, --yes     Automatically answer yes to all prompts |  | ||||||
| #   -h, --help             Show this help message | #   -h, --help             Show this help message | ||||||
|  | #   --version VERSION      Install specific version (e.g., v4.0.0) | ||||||
|  | #   --install-dir DIR      Installation directory (default: /opt/nupst) | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # Default values | ||||||
|  | SHOW_HELP=0 | ||||||
|  | SPECIFIED_VERSION="" | ||||||
|  | INSTALL_DIR="/opt/nupst" | ||||||
|  | GITEA_BASE_URL="https://code.foss.global" | ||||||
|  | GITEA_REPO="serve.zone/nupst" | ||||||
|  |  | ||||||
| # Parse command line arguments | # Parse command line arguments | ||||||
| AUTO_YES=0 | while [[ $# -gt 0 ]]; do | ||||||
| SHOW_HELP=0 |   case $1 in | ||||||
|  |  | ||||||
| for arg in "$@"; do |  | ||||||
|   case $arg in |  | ||||||
|     -y|--yes) |  | ||||||
|       AUTO_YES=1 |  | ||||||
|       shift |  | ||||||
|       ;; |  | ||||||
|     -h|--help) |     -h|--help) | ||||||
|       SHOW_HELP=1 |       SHOW_HELP=1 | ||||||
|       shift |       shift | ||||||
|       ;; |       ;; | ||||||
|  |     --version) | ||||||
|  |       SPECIFIED_VERSION="$2" | ||||||
|  |       shift 2 | ||||||
|  |       ;; | ||||||
|  |     --install-dir) | ||||||
|  |       INSTALL_DIR="$2" | ||||||
|  |       shift 2 | ||||||
|  |       ;; | ||||||
|     *) |     *) | ||||||
|       # Unknown option |       echo "Unknown option: $1" | ||||||
|  |       echo "Use -h or --help for usage information" | ||||||
|  |       exit 1 | ||||||
|       ;; |       ;; | ||||||
|   esac |   esac | ||||||
| done | done | ||||||
|  |  | ||||||
| if [ $SHOW_HELP -eq 1 ]; then | if [ $SHOW_HELP -eq 1 ]; then | ||||||
|   echo "NUPST Installer Script" |   echo "NUPST Installer Script (v5.0+)" | ||||||
|  |   echo "Downloads and installs pre-compiled NUPST binary" | ||||||
|  |   echo "" | ||||||
|   echo "Usage: $0 [options]" |   echo "Usage: $0 [options]" | ||||||
|   echo "" |   echo "" | ||||||
|   echo "Options:" |   echo "Options:" | ||||||
|   echo "  -y, --yes     Automatically answer yes to all prompts" |  | ||||||
|   echo "  -h, --help             Show this help message" |   echo "  -h, --help             Show this help message" | ||||||
|  |   echo "  --version VERSION      Install specific version (e.g., v5.0.0)" | ||||||
|  |   echo "  --install-dir DIR      Installation directory (default: /opt/nupst)" | ||||||
|  |   echo "" | ||||||
|  |   echo "Examples:" | ||||||
|  |   echo "  # Install latest version" | ||||||
|  |   echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash" | ||||||
|  |   echo "" | ||||||
|  |   echo "  # Install specific version" | ||||||
|  |   echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- --version v5.0.0" | ||||||
|   exit 0 |   exit 0 | ||||||
| fi | fi | ||||||
|  |  | ||||||
| @@ -48,249 +73,201 @@ if [ "$EUID" -ne 0 ]; then | |||||||
|   exit 1 |   exit 1 | ||||||
| fi | fi | ||||||
|  |  | ||||||
| # Detect if script is being piped or run directly | # Helper function to detect OS and architecture | ||||||
| PIPED=0 | detect_platform() { | ||||||
| INTERACTIVE=1 |   local os=$(uname -s) | ||||||
| if [ ! -t 0 ]; then |   local arch=$(uname -m) | ||||||
|   # Being piped, need to clone the repo |  | ||||||
|   PIPED=1 |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Check if stdin is a terminal |   # Map OS | ||||||
| if [ ! -t 0 ] || [ ! -t 1 ]; then |   case "$os" in | ||||||
|   # Either stdin or stdout is not a terminal, check if -y was provided |     Linux) | ||||||
|   if [ $AUTO_YES -ne 1 ]; then |       os_name="linux" | ||||||
|     echo "Script detected it's running in a non-interactive environment without -y flag." |  | ||||||
|     echo "Attempting to find a controlling terminal for interactive prompts..." |  | ||||||
|     # Try to use a controlling terminal for user input |  | ||||||
|     if [ -t 1 ]; then |  | ||||||
|       # Stdout is a terminal, use it |  | ||||||
|       exec < /dev/tty 2>/dev/null || INTERACTIVE=0 |  | ||||||
|     else |  | ||||||
|       # Try to find controlling terminal |  | ||||||
|       exec < /dev/tty 2>/dev/null || INTERACTIVE=0 |  | ||||||
|     fi |  | ||||||
|      |  | ||||||
|     if [ $INTERACTIVE -eq 0 ]; then |  | ||||||
|       echo "ERROR: No controlling terminal available for interactive prompts." |  | ||||||
|       echo "For interactive installation (RECOMMENDED):" |  | ||||||
|       echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh" |  | ||||||
|       echo "  sudo bash nupst-install.sh" |  | ||||||
|       echo "" |  | ||||||
|       echo "For non-interactive installation with automatic dependency installation:" |  | ||||||
|       echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y" |  | ||||||
|       exit 1 |  | ||||||
|     else |  | ||||||
|       echo "Interactive terminal found, continuing with prompts..." |  | ||||||
|     fi |  | ||||||
|   fi |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Helper function to detect OS type |  | ||||||
| detect_os() { |  | ||||||
|   if [ -f /etc/os-release ]; then |  | ||||||
|     . /etc/os-release |  | ||||||
|     OS=$ID |  | ||||||
|   elif type lsb_release >/dev/null 2>&1; then |  | ||||||
|     OS=$(lsb_release -si | tr '[:upper:]' '[:lower:]') |  | ||||||
|   elif [ -f /etc/lsb-release ]; then |  | ||||||
|     . /etc/lsb-release |  | ||||||
|     OS=$DISTRIB_ID |  | ||||||
|   elif [ -f /etc/debian_version ]; then |  | ||||||
|     OS="debian" |  | ||||||
|   elif [ -f /etc/redhat-release ]; then |  | ||||||
|     if grep -q "CentOS" /etc/redhat-release; then |  | ||||||
|       OS="centos" |  | ||||||
|     elif grep -q "Fedora" /etc/redhat-release; then |  | ||||||
|       OS="fedora" |  | ||||||
|     else |  | ||||||
|       OS="rhel" |  | ||||||
|     fi |  | ||||||
|   else |  | ||||||
|     OS=$(uname -s) |  | ||||||
|   fi |  | ||||||
|   echo $OS |  | ||||||
| } |  | ||||||
|  |  | ||||||
| # Helper function to install git |  | ||||||
| install_git() { |  | ||||||
|   OS=$(detect_os) |  | ||||||
|   echo "Detected OS: $OS" |  | ||||||
|    |  | ||||||
|   case "$OS" in |  | ||||||
|     ubuntu|debian|pop|mint|elementary|kali|zorin) |  | ||||||
|       echo "Installing git using apt..." |  | ||||||
|       apt-get update && apt-get install -y git |  | ||||||
|       ;; |       ;; | ||||||
|     fedora|rhel|centos|almalinux|rocky) |     Darwin) | ||||||
|       echo "Installing git using dnf/yum..." |       os_name="macos" | ||||||
|       if command -v dnf &> /dev/null; then |  | ||||||
|         dnf install -y git |  | ||||||
|       else |  | ||||||
|         yum install -y git |  | ||||||
|       fi |  | ||||||
|       ;; |       ;; | ||||||
|     arch|manjaro|endeavouros|garuda) |     MINGW*|MSYS*|CYGWIN*) | ||||||
|       echo "Installing git using pacman..." |       os_name="windows" | ||||||
|       pacman -Sy --noconfirm git |  | ||||||
|       ;; |  | ||||||
|     opensuse*|suse|sles) |  | ||||||
|       echo "Installing git using zypper..." |  | ||||||
|       zypper install -y git |  | ||||||
|       ;; |  | ||||||
|     alpine) |  | ||||||
|       echo "Installing git using apk..." |  | ||||||
|       apk add git |  | ||||||
|       ;; |       ;; | ||||||
|     *) |     *) | ||||||
|       echo "Unsupported OS: $OS" |       echo "Error: Unsupported operating system: $os" | ||||||
|       echo "Please install git manually and run the installer again." |       echo "Supported: Linux, macOS, Windows" | ||||||
|       exit 1 |       exit 1 | ||||||
|       ;; |       ;; | ||||||
|   esac |   esac | ||||||
|  |  | ||||||
|   # Check if git was installed successfully |   # Map architecture | ||||||
|   if ! command -v git &> /dev/null; then |   case "$arch" in | ||||||
|     echo "Failed to install git. Please install git manually and run the installer again." |     x86_64|amd64) | ||||||
|  |       arch_name="x64" | ||||||
|  |       ;; | ||||||
|  |     aarch64|arm64) | ||||||
|  |       arch_name="arm64" | ||||||
|  |       ;; | ||||||
|  |     *) | ||||||
|  |       echo "Error: Unsupported architecture: $arch" | ||||||
|  |       echo "Supported: x86_64/amd64 (x64), aarch64/arm64 (arm64)" | ||||||
|       exit 1 |       exit 1 | ||||||
|   fi |       ;; | ||||||
|  |   esac | ||||||
|  |  | ||||||
|   echo "Git installed successfully." |   # Construct binary name | ||||||
|  |   if [ "$os_name" = "windows" ]; then | ||||||
|  |     echo "nupst-${os_name}-${arch_name}.exe" | ||||||
|  |   else | ||||||
|  |     echo "nupst-${os_name}-${arch_name}" | ||||||
|  |   fi | ||||||
| } | } | ||||||
|  |  | ||||||
| # Define installation directory | # Get latest release version from Gitea API | ||||||
| INSTALL_DIR="/opt/nupst" | get_latest_version() { | ||||||
| REPO_URL="https://code.foss.global/serve.zone/nupst.git" |   echo "Fetching latest release version from Gitea..." >&2 | ||||||
|  |  | ||||||
| # Check if git is installed - needed for both piped and direct execution |   local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest" | ||||||
| if ! command -v git &> /dev/null; then |   local response=$(curl -sSL "$api_url" 2>/dev/null) | ||||||
|   echo "Git is required but not installed." |  | ||||||
|  |  | ||||||
|   if [ $AUTO_YES -eq 1 ]; then |   if [ $? -ne 0 ] || [ -z "$response" ]; then | ||||||
|     echo "Auto-installing git (-y flag provided)..." |     echo "Error: Failed to fetch latest release information from Gitea API" >&2 | ||||||
|     install_git |     echo "URL: $api_url" >&2 | ||||||
|   elif [ $INTERACTIVE -eq 1 ]; then |  | ||||||
|     # If interactive and no -y flag, ask the user |  | ||||||
|     echo "Would you like to install git now? (y/N): " |  | ||||||
|     read -r install_git_prompt |  | ||||||
|      |  | ||||||
|     if [[ "$install_git_prompt" =~ ^[Yy]$ ]]; then |  | ||||||
|       install_git |  | ||||||
|     else |  | ||||||
|       echo "Git installation skipped. Please install git manually and run the installer again." |  | ||||||
|       echo "Alternatively, you can run the installer with -y flag to automatically install git:" |  | ||||||
|       echo "  sudo bash install.sh -y" |  | ||||||
|       exit 1 |  | ||||||
|     fi |  | ||||||
|   else |  | ||||||
|     # Non-interactive mode without -y flag |  | ||||||
|     echo "Error: Git is required but not installed." |  | ||||||
|     echo "In non-interactive mode, use -y flag to auto-install dependencies:" |  | ||||||
|     echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y" |  | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| if [ $PIPED -eq 1 ]; then |  | ||||||
|   echo "Installing NUPST from remote repository..." |  | ||||||
|    |  | ||||||
|   # Check if installation directory exists |  | ||||||
|   if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then |  | ||||||
|     echo "Existing installation found at $INSTALL_DIR. Updating..." |  | ||||||
|     cd "$INSTALL_DIR" |  | ||||||
|      |  | ||||||
|     # Try to update the repository |  | ||||||
|     git fetch origin |  | ||||||
|     git reset --hard origin/main |  | ||||||
|      |  | ||||||
|     if [ $? -ne 0 ]; then |  | ||||||
|       echo "Failed to update repository. Reinstalling..." |  | ||||||
|       cd / |  | ||||||
|       rm -rf "$INSTALL_DIR" |  | ||||||
|       mkdir -p "$INSTALL_DIR" |  | ||||||
|       git clone --depth 1 $REPO_URL "$INSTALL_DIR" |  | ||||||
|     else |  | ||||||
|       echo "Repository updated successfully." |  | ||||||
|     fi |  | ||||||
|   else |  | ||||||
|     # Fresh installation |  | ||||||
|     if [ -d "$INSTALL_DIR" ]; then |  | ||||||
|       echo "Removing previous installation at $INSTALL_DIR..." |  | ||||||
|       rm -rf "$INSTALL_DIR" |  | ||||||
|     fi |  | ||||||
|      |  | ||||||
|     # Create installation directory |  | ||||||
|     mkdir -p "$INSTALL_DIR" |  | ||||||
|      |  | ||||||
|     # Clone the repository |  | ||||||
|     echo "Cloning NUPST repository to $INSTALL_DIR..." |  | ||||||
|     git clone --depth 1 $REPO_URL "$INSTALL_DIR" |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   if [ $? -ne 0 ]; then |  | ||||||
|     echo "Failed to clone/update repository. Please check your internet connection." |  | ||||||
|     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 or downloaded script |  | ||||||
|   SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" |  | ||||||
|  |  | ||||||
|   # When running from a downloaded script in a different location  |   if [ -z "$version" ]; then | ||||||
|   # we need to clone the repository first |     echo "Error: Could not determine latest version from API response" >&2 | ||||||
|   if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then |  | ||||||
|     echo "Running installer from downloaded script outside repository." |  | ||||||
|     echo "Will clone the repository to $INSTALL_DIR..." |  | ||||||
|      |  | ||||||
|     # Create installation directory if needed |  | ||||||
|     if [ -d "$INSTALL_DIR" ]; then |  | ||||||
|       echo "Removing previous installation at $INSTALL_DIR..." |  | ||||||
|       rm -rf "$INSTALL_DIR" |  | ||||||
|     fi |  | ||||||
|      |  | ||||||
|     mkdir -p "$INSTALL_DIR" |  | ||||||
|      |  | ||||||
|     # Clone the repository |  | ||||||
|     echo "Cloning NUPST repository to $INSTALL_DIR..." |  | ||||||
|     git clone --depth 1 $REPO_URL "$INSTALL_DIR" |  | ||||||
|      |  | ||||||
|     if [ $? -ne 0 ]; then |  | ||||||
|       echo "Failed to clone repository. Please check your internet connection." |  | ||||||
|     exit 1 |     exit 1 | ||||||
|   fi |   fi | ||||||
|  |  | ||||||
|     # Update script directory to use the cloned repo |   echo "$version" | ||||||
|     SCRIPT_DIR="$INSTALL_DIR" | } | ||||||
|   fi |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Run setup script | # Main installation process | ||||||
| echo "Running setup script..." | echo "================================================" | ||||||
| if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then | echo "  NUPST Installation Script (v5.0+)" | ||||||
|   echo "ERROR: Setup script not found at $SCRIPT_DIR/setup.sh" | echo "================================================" | ||||||
|   echo "Current directory: $(pwd)" | echo "" | ||||||
|   echo "Script directory: $SCRIPT_DIR" |  | ||||||
|   ls -la "$SCRIPT_DIR" | # Detect platform | ||||||
|   exit 1 | BINARY_NAME=$(detect_platform) | ||||||
| fi | echo "Detected platform: $BINARY_NAME" | ||||||
|  | echo "" | ||||||
| bash "$SCRIPT_DIR/setup.sh" |  | ||||||
|  | # Determine version to install | ||||||
| # Install globally | if [ -n "$SPECIFIED_VERSION" ]; then | ||||||
| echo "Installing NUPST globally..." |   VERSION="$SPECIFIED_VERSION" | ||||||
| ln -sf "$SCRIPT_DIR/bin/nupst" /usr/local/bin/nupst |   echo "Installing specified version: $VERSION" | ||||||
|  | else | ||||||
| # Installation completed |   VERSION=$(get_latest_version) | ||||||
| if [ $PIPED -eq 1 ]; then |   echo "Installing latest version: $VERSION" | ||||||
|   echo "NUPST has been installed globally at $INSTALL_DIR" | fi | ||||||
| else | echo "" | ||||||
|   echo "NUPST has been installed globally." |  | ||||||
| fi | # Construct download URL | ||||||
|  | DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BINARY_NAME}" | ||||||
| echo "You can now run 'nupst' from anywhere." | echo "Download URL: $DOWNLOAD_URL" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Check if service is running and stop it | ||||||
|  | SERVICE_WAS_RUNNING=0 | ||||||
|  | if systemctl is-enabled --quiet nupst 2>/dev/null || systemctl is-active --quiet nupst 2>/dev/null; then | ||||||
|  |   SERVICE_WAS_RUNNING=1 | ||||||
|  |   if systemctl is-active --quiet nupst 2>/dev/null; then | ||||||
|  |     echo "Stopping NUPST service..." | ||||||
|  |     systemctl stop nupst | ||||||
|  |   fi | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Clean installation directory - ensure only binary exists | ||||||
|  | if [ -d "$INSTALL_DIR" ]; then | ||||||
|  |   echo "Cleaning installation directory: $INSTALL_DIR" | ||||||
|  |   rm -rf "$INSTALL_DIR" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Create fresh installation directory | ||||||
|  | echo "Creating installation directory: $INSTALL_DIR" | ||||||
|  | mkdir -p "$INSTALL_DIR" | ||||||
|  |  | ||||||
|  | # Download binary | ||||||
|  | echo "Downloading NUPST binary..." | ||||||
|  | TEMP_FILE="$INSTALL_DIR/nupst.download" | ||||||
|  | curl -sSL "$DOWNLOAD_URL" -o "$TEMP_FILE" | ||||||
|  |  | ||||||
|  | if [ $? -ne 0 ]; then | ||||||
|  |   echo "Error: Failed to download binary from $DOWNLOAD_URL" | ||||||
|  |   echo "" | ||||||
|  |   echo "Please check:" | ||||||
|  |   echo "  1. Your internet connection" | ||||||
|  |   echo "  2. The specified version exists: ${GITEA_BASE_URL}/${GITEA_REPO}/releases" | ||||||
|  |   echo "  3. The platform binary is available for this release" | ||||||
|  |   rm -f "$TEMP_FILE" | ||||||
|  |   exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # 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 | ||||||
|  | ln -sf "$BINARY_PATH" "$BIN_DIR/nupst" | ||||||
|  | echo "Symlink created: $BIN_DIR/nupst -> $BINARY_PATH" | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Restart service if it was running before update | ||||||
|  | if [ $SERVICE_WAS_RUNNING -eq 1 ]; then | ||||||
|  |   echo "Restarting NUPST service..." | ||||||
|  |   systemctl start nupst | ||||||
|  |   echo "Service restarted successfully." | ||||||
|  |   echo "" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  NUPST Installation Complete!" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | echo "Installation details:" | ||||||
|  | echo "  Binary location: $BINARY_PATH" | ||||||
|  | echo "  Symlink location: $BIN_DIR/nupst" | ||||||
|  | echo "  Version: $VERSION" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Check if configuration exists | ||||||
|  | if [ -f "/etc/nupst/config.json" ]; then | ||||||
|  |   echo "Configuration: /etc/nupst/config.json (preserved)" | ||||||
|  |   echo "" | ||||||
|  |   echo "Your existing configuration has been preserved." | ||||||
|  |   if [ $SERVICE_WAS_RUNNING -eq 1 ]; then | ||||||
|  |     echo "The service has been restarted with your current settings." | ||||||
|  |   else | ||||||
|  |     echo "Start the service with: sudo nupst service start" | ||||||
|  |   fi | ||||||
|  | else | ||||||
|  |   echo "Get started:" | ||||||
|  |   echo "  nupst --version" | ||||||
|  |   echo "  nupst help" | ||||||
|  |   echo "  nupst ups add       # Add a UPS device" | ||||||
|  |   echo "  nupst service enable # Enable systemd service" | ||||||
|  | fi | ||||||
| echo "" | echo "" | ||||||
| echo "To get started, try:" |  | ||||||
| echo "  nupst help" |  | ||||||
| echo "  nupst setup  # To configure your UPS connection" |  | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								mod.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								mod.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | #!/usr/bin/env -S deno run --allow-all | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * NUPST - UPS Shutdown Tool | ||||||
|  |  * | ||||||
|  |  * A command-line tool for monitoring SNMP-enabled UPS devices and | ||||||
|  |  * initiating system shutdown when power conditions are critical. | ||||||
|  |  * | ||||||
|  |  * Required Permissions: | ||||||
|  |  * - --allow-net: SNMP communication with UPS devices | ||||||
|  |  * - --allow-read: Read configuration files (/etc/nupst/config.json) | ||||||
|  |  * - --allow-write: Write configuration files | ||||||
|  |  * - --allow-run: Execute system commands (systemctl, shutdown, git, bash) | ||||||
|  |  * - --allow-sys: Access system information (hostname, OS details) | ||||||
|  |  * - --allow-env: Read environment variables | ||||||
|  |  * | ||||||
|  |  * @module | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import { NupstCli } from './ts/cli.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Main entry point for the NUPST application | ||||||
|  |  * Parses command-line arguments and executes the requested command | ||||||
|  |  */ | ||||||
|  | async function main(): Promise<void> { | ||||||
|  |   const cli = new NupstCli(); | ||||||
|  |  | ||||||
|  |   // Deno.args is already 0-indexed (unlike Node's process.argv which starts at index 2) | ||||||
|  |   // We need to prepend placeholder args to match the existing CLI parser expectations | ||||||
|  |   const args = ['deno', 'mod.ts', ...Deno.args]; | ||||||
|  |  | ||||||
|  |   await cli.parseAndExecute(args); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Execute main and handle errors | ||||||
|  | if (import.meta.main) { | ||||||
|  |   try { | ||||||
|  |     await main(); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|  |     Deno.exit(1); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1 +0,0 @@ | |||||||
| {} |  | ||||||
							
								
								
									
										61
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,61 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "@serve.zone/nupst", |  | ||||||
|   "version": "3.1.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": { |  | ||||||
|     "net-snmp": "3.20.0" |  | ||||||
|   }, |  | ||||||
|   "devDependencies": { |  | ||||||
|     "@git.zone/tsbuild": "^2.3.2", |  | ||||||
|     "@git.zone/tsrun": "^1.3.3", |  | ||||||
|     "@git.zone/tstest": "^1.0.96", |  | ||||||
|     "@push.rocks/qenv": "^6.1.0", |  | ||||||
|     "@push.rocks/tapbundle": "^5.6.0", |  | ||||||
|     "@types/node": "^20.11.0" |  | ||||||
|   }, |  | ||||||
|   "engines": { |  | ||||||
|     "node": ">=16.0.0" |  | ||||||
|   }, |  | ||||||
|   "pnpm": { |  | ||||||
|     "onlyBuiltDependencies": [ |  | ||||||
|       "esbuild", |  | ||||||
|       "mongodb-memory-server", |  | ||||||
|       "puppeteer" |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6" |  | ||||||
| } |  | ||||||
							
								
								
									
										10204
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10204
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										613
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										613
									
								
								readme.plan.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,613 @@ | |||||||
|  | # NUPST Migration Plan: Node.js → Deno v4.0.0 | ||||||
|  |  | ||||||
|  | **Migration Goal**: Convert NUPST from Node.js to Deno with single-executable distribution | ||||||
|  | **Version**: 3.1.2 → 4.0.0 (breaking changes) **Platforms**: Linux x64/ARM64, macOS x64/ARM64, | ||||||
|  | Windows x64 | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 0: Planning & Preparation | ||||||
|  |  | ||||||
|  | - [x] Research Deno compilation targets and npm: specifier support | ||||||
|  | - [x] Analyze current codebase structure and dependencies | ||||||
|  | - [x] Define CLI command structure simplification | ||||||
|  | - [x] Create detailed migration task list | ||||||
|  | - [ ] Create feature branch: `migration/deno-v4` | ||||||
|  | - [ ] Backup current working state with git tag: `v3.1.2-pre-deno-migration` | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 1: Dependency Migration (4-6 hours) | ||||||
|  |  | ||||||
|  | ### 1.1 Analyze Current Dependencies | ||||||
|  |  | ||||||
|  | - [ ] List all production dependencies from `package.json` | ||||||
|  |   - Current: `net-snmp@3.20.0` | ||||||
|  | - [ ] List all dev dependencies to be removed | ||||||
|  |   - `@git.zone/tsbuild`, `@git.zone/tsrun`, `@git.zone/tstest`, `@push.rocks/qenv`, | ||||||
|  |     `@push.rocks/tapbundle`, `@types/node` | ||||||
|  | - [ ] Identify Node.js built-in module usage | ||||||
|  |   - `child_process` (execSync) | ||||||
|  |   - `https` (for version checking) | ||||||
|  |   - `fs` (readFileSync, writeFileSync, existsSync, mkdirSync) | ||||||
|  |   - `path` (join, dirname, resolve) | ||||||
|  |  | ||||||
|  | ### 1.2 Create Deno Configuration | ||||||
|  |  | ||||||
|  | - [ ] Create `deno.json` with project configuration | ||||||
|  |   ```json | ||||||
|  |   { | ||||||
|  |     "name": "@serve.zone/nupst", | ||||||
|  |     "version": "4.0.0", | ||||||
|  |     "exports": "./mod.ts", | ||||||
|  |     "tasks": { | ||||||
|  |       "dev": "deno run --allow-all mod.ts", | ||||||
|  |       "compile": "deno task compile:all", | ||||||
|  |       "compile:all": "bash scripts/compile-all.sh", | ||||||
|  |       "test": "deno test --allow-all tests/", | ||||||
|  |       "check": "deno check mod.ts" | ||||||
|  |     }, | ||||||
|  |     "lint": { | ||||||
|  |       "rules": { | ||||||
|  |         "tags": ["recommended"] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "fmt": { | ||||||
|  |       "useTabs": false, | ||||||
|  |       "lineWidth": 100, | ||||||
|  |       "indentWidth": 2, | ||||||
|  |       "semiColons": true | ||||||
|  |     }, | ||||||
|  |     "compilerOptions": { | ||||||
|  |       "lib": ["deno.window"], | ||||||
|  |       "strict": true | ||||||
|  |     }, | ||||||
|  |     "imports": { | ||||||
|  |       "@std/cli": "jsr:@std/cli@^1.0.0", | ||||||
|  |       "@std/fmt": "jsr:@std/fmt@^1.0.0", | ||||||
|  |       "@std/path": "jsr:@std/path@^1.0.0" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   ``` | ||||||
|  |  | ||||||
|  | ### 1.3 Update Import Statements | ||||||
|  |  | ||||||
|  | - [ ] `ts/snmp/manager.ts`: Change `import * as snmp from 'net-snmp'` to | ||||||
|  |       `import * as snmp from "npm:net-snmp@3.20.0"` | ||||||
|  | - [ ] `ts/cli.ts`: Change `import { execSync } from 'child_process'` to | ||||||
|  |       `import { execSync } from "node:child_process"` | ||||||
|  | - [ ] `ts/nupst.ts`: Change `import * as https from 'https'` to | ||||||
|  |       `import * as https from "node:https"` | ||||||
|  | - [ ] Search for all `fs` imports and update to `node:fs` | ||||||
|  | - [ ] Search for all `path` imports and update to `node:path` | ||||||
|  | - [ ] Update all relative imports to use `.ts` extension instead of `.js` | ||||||
|  |   - Example: `'./nupst.js'` → `'./nupst.ts'` | ||||||
|  |  | ||||||
|  | ### 1.4 Test npm: Specifier Compatibility | ||||||
|  |  | ||||||
|  | - [ ] Create test file: `tests/snmp_compatibility_test.ts` | ||||||
|  | - [ ] Test SNMP v1 connection with npm:net-snmp | ||||||
|  | - [ ] Test SNMP v2c connection with npm:net-snmp | ||||||
|  | - [ ] Test SNMP v3 connection with npm:net-snmp | ||||||
|  | - [ ] Verify native addon loading works in compiled binary | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 2: Code Structure Refactoring (3-4 hours) | ||||||
|  |  | ||||||
|  | ### 2.1 Create Main Entry Point | ||||||
|  |  | ||||||
|  | - [ ] Create `mod.ts` as main Deno entry point: | ||||||
|  |   ```typescript | ||||||
|  |   #!/usr/bin/env -S deno run --allow-all | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * NUPST - UPS Shutdown Tool for Deno | ||||||
|  |    * | ||||||
|  |    * Required Permissions: | ||||||
|  |    * --allow-net: SNMP communication with UPS devices | ||||||
|  |    * --allow-read: Configuration file access (/etc/nupst/config.json) | ||||||
|  |    * --allow-write: Configuration file updates | ||||||
|  |    * --allow-run: System commands (systemctl, shutdown) | ||||||
|  |    * --allow-sys: System information (hostname, OS info) | ||||||
|  |    * --allow-env: Environment variables | ||||||
|  |    */ | ||||||
|  |  | ||||||
|  |   import { NupstCli } from './ts/cli.ts'; | ||||||
|  |  | ||||||
|  |   const cli = new NupstCli(); | ||||||
|  |   await cli.parseAndExecute(Deno.args); | ||||||
|  |   ``` | ||||||
|  |  | ||||||
|  | ### 2.2 Update All Import Extensions | ||||||
|  |  | ||||||
|  | Files to update (change .js → .ts in imports): | ||||||
|  |  | ||||||
|  | - [ ] `ts/index.ts` | ||||||
|  | - [ ] `ts/cli.ts` (imports from ./nupst.js, ./logger.js) | ||||||
|  | - [ ] `ts/nupst.ts` (imports from ./snmp/manager.js, ./daemon.js, etc.) | ||||||
|  | - [ ] `ts/daemon.ts` (imports from ./snmp/manager.js, ./logger.js, ./helpers/) | ||||||
|  | - [ ] `ts/systemd.ts` (imports from ./daemon.js, ./logger.js) | ||||||
|  | - [ ] `ts/cli/service-handler.ts` | ||||||
|  | - [ ] `ts/cli/group-handler.ts` | ||||||
|  | - [ ] `ts/cli/ups-handler.ts` | ||||||
|  | - [ ] `ts/snmp/index.ts` | ||||||
|  | - [ ] `ts/snmp/manager.ts` (imports from ./types.js, ./oid-sets.js) | ||||||
|  | - [ ] `ts/snmp/oid-sets.ts` (imports from ./types.js) | ||||||
|  | - [ ] `ts/helpers/index.ts` | ||||||
|  | - [ ] `ts/logger.ts` | ||||||
|  |  | ||||||
|  | ### 2.3 Update process.argv References | ||||||
|  |  | ||||||
|  | - [ ] `ts/cli.ts`: Replace `process.argv` with `Deno.args` (adjust indexing: process.argv[2] → | ||||||
|  |       Deno.args[0]) | ||||||
|  | - [ ] Update parseAndExecute method to work with Deno.args (0-indexed vs 2-indexed) | ||||||
|  |  | ||||||
|  | ### 2.4 Update File System Operations | ||||||
|  |  | ||||||
|  | - [ ] Search for `fs.readFileSync()` → Consider using `Deno.readTextFile()` or keep node:fs | ||||||
|  | - [ ] Search for `fs.writeFileSync()` → Consider using `Deno.writeTextFile()` or keep node:fs | ||||||
|  | - [ ] Search for `fs.existsSync()` → Keep node:fs or use Deno.stat | ||||||
|  | - [ ] Search for `fs.mkdirSync()` → Keep node:fs or use Deno.mkdir | ||||||
|  | - [ ] Decision: Keep node:fs for consistency or migrate to Deno APIs? | ||||||
|  |  | ||||||
|  | ### 2.5 Update Path Operations | ||||||
|  |  | ||||||
|  | - [ ] Verify all `path.join()`, `path.resolve()`, `path.dirname()` work with node:path | ||||||
|  | - [ ] Consider using `@std/path` from JSR for better Deno integration | ||||||
|  |  | ||||||
|  | ### 2.6 Handle __dirname and __filename | ||||||
|  |  | ||||||
|  | - [ ] Find all `__dirname` usage | ||||||
|  | - [ ] Replace with `import.meta.dirname` (Deno) or `dirname(fromFileUrl(import.meta.url))` | ||||||
|  | - [ ] Find all `__filename` usage | ||||||
|  | - [ ] Replace with `import.meta.filename` or `fromFileUrl(import.meta.url)` | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 3: CLI Command Simplification (3-4 hours) | ||||||
|  |  | ||||||
|  | ### 3.1 Design New Command Structure | ||||||
|  |  | ||||||
|  | Current → New mapping: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | OLD                          NEW | ||||||
|  | ===                          === | ||||||
|  | nupst enable                 → nupst service enable | ||||||
|  | nupst disable                → nupst service disable | ||||||
|  | nupst daemon-start           → nupst service start-daemon | ||||||
|  | nupst logs                   → nupst service logs | ||||||
|  | nupst stop                   → nupst service stop | ||||||
|  | nupst start                  → nupst service start | ||||||
|  | nupst status                 → nupst service status | ||||||
|  |  | ||||||
|  | nupst add                    → nupst ups add | ||||||
|  | nupst edit [id]              → nupst ups edit [id] | ||||||
|  | nupst delete <id>            → nupst ups remove <id> | ||||||
|  | nupst list                   → nupst ups list | ||||||
|  | nupst setup                  → nupst ups edit (removed alias) | ||||||
|  | nupst test                   → nupst ups test | ||||||
|  |  | ||||||
|  | nupst group list             → nupst group list | ||||||
|  | nupst group add              → nupst group add | ||||||
|  | nupst group edit <id>        → nupst group edit <id> | ||||||
|  | nupst group delete <id>      → nupst group remove <id> | ||||||
|  |  | ||||||
|  | nupst config                 → nupst config show | ||||||
|  | nupst update                 → nupst update | ||||||
|  | nupst uninstall              → nupst uninstall | ||||||
|  | nupst help                   → nupst help / nupst --help | ||||||
|  | (new)                        → nupst --version | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### 3.2 Update CLI Parser (ts/cli.ts) | ||||||
|  |  | ||||||
|  | - [ ] Refactor `parseAndExecute()` to handle new command structure | ||||||
|  | - [ ] Add `service` subcommand handler | ||||||
|  | - [ ] Add `ups` subcommand handler | ||||||
|  | - [ ] Keep `group` subcommand handler (already exists, just update delete→remove) | ||||||
|  | - [ ] Add `config` subcommand handler with `show` default | ||||||
|  | - [ ] Add `--version` flag handler | ||||||
|  | - [ ] Update `help` command to show new structure | ||||||
|  | - [ ] Add command aliases: `rm` → `remove`, `ls` → `list` | ||||||
|  | - [ ] Add `--json` flag for machine-readable output (future enhancement) | ||||||
|  |  | ||||||
|  | ### 3.3 Update Command Handlers | ||||||
|  |  | ||||||
|  | - [ ] `ts/cli/service-handler.ts`: Update method names if needed | ||||||
|  | - [ ] `ts/cli/ups-handler.ts`: Rename `delete()` → `remove()`, remove `setup` method | ||||||
|  | - [ ] `ts/cli/group-handler.ts`: Rename `delete()` → `remove()` | ||||||
|  |  | ||||||
|  | ### 3.4 Improve Help Messages | ||||||
|  |  | ||||||
|  | - [ ] Update `showHelp()` in ts/cli.ts with new command structure | ||||||
|  | - [ ] Update `showGroupHelp()` in ts/cli.ts | ||||||
|  | - [ ] Add `showServiceHelp()` method | ||||||
|  | - [ ] Add `showUpsHelp()` method | ||||||
|  | - [ ] Add `showConfigHelp()` method | ||||||
|  | - [ ] Include usage examples in help text | ||||||
|  |  | ||||||
|  | ### 3.5 Add Version Command | ||||||
|  |  | ||||||
|  | - [ ] Read version from deno.json | ||||||
|  | - [ ] Create `--version` handler in CLI | ||||||
|  | - [ ] Display version with build info | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 4: Compilation & Distribution (2-3 hours) | ||||||
|  |  | ||||||
|  | ### 4.1 Create Compilation Script | ||||||
|  |  | ||||||
|  | - [ ] Create directory: `scripts/` | ||||||
|  | - [ ] Create `scripts/compile-all.sh`: | ||||||
|  |   ```bash | ||||||
|  |   #!/bin/bash | ||||||
|  |   set -e | ||||||
|  |  | ||||||
|  |   VERSION=$(cat deno.json | jq -r '.version') | ||||||
|  |   BINARY_DIR="dist/binaries" | ||||||
|  |  | ||||||
|  |   echo "Compiling NUPST v${VERSION} for all platforms..." | ||||||
|  |   mkdir -p "$BINARY_DIR" | ||||||
|  |  | ||||||
|  |   # Linux x86_64 | ||||||
|  |   echo "→ Linux x86_64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-linux-x64" \ | ||||||
|  |     --target x86_64-unknown-linux-gnu mod.ts | ||||||
|  |  | ||||||
|  |   # Linux ARM64 | ||||||
|  |   echo "→ Linux ARM64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-linux-arm64" \ | ||||||
|  |     --target aarch64-unknown-linux-gnu mod.ts | ||||||
|  |  | ||||||
|  |   # macOS x86_64 | ||||||
|  |   echo "→ macOS x86_64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-macos-x64" \ | ||||||
|  |     --target x86_64-apple-darwin mod.ts | ||||||
|  |  | ||||||
|  |   # macOS ARM64 | ||||||
|  |   echo "→ macOS ARM64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-macos-arm64" \ | ||||||
|  |     --target aarch64-apple-darwin mod.ts | ||||||
|  |  | ||||||
|  |   # Windows x86_64 | ||||||
|  |   echo "→ Windows x86_64..." | ||||||
|  |   deno compile --allow-all --output "$BINARY_DIR/nupst-windows-x64.exe" \ | ||||||
|  |     --target x86_64-pc-windows-msvc mod.ts | ||||||
|  |  | ||||||
|  |   echo "" | ||||||
|  |   echo "✓ Compilation complete!" | ||||||
|  |   ls -lh "$BINARY_DIR/" | ||||||
|  |   ``` | ||||||
|  | - [ ] Make script executable: `chmod +x scripts/compile-all.sh` | ||||||
|  |  | ||||||
|  | ### 4.2 Test Local Compilation | ||||||
|  |  | ||||||
|  | - [ ] Run `deno task compile` to compile for all platforms | ||||||
|  | - [ ] Verify all 5 binaries are created | ||||||
|  | - [ ] Check binary sizes (should be reasonable, < 100MB each) | ||||||
|  | - [ ] Test local binary on current platform: `./dist/binaries/nupst-linux-x64 --version` | ||||||
|  |  | ||||||
|  | ### 4.3 Update Installation Scripts | ||||||
|  |  | ||||||
|  | - [ ] Update `install.sh`: | ||||||
|  |   - Remove Node.js download logic (lines dealing with vendor/node-*) | ||||||
|  |   - Add detection for binary download from GitHub releases | ||||||
|  |   - Simplify to download appropriate binary based on OS/arch | ||||||
|  |   - Place binary in `/opt/nupst/bin/nupst` | ||||||
|  |   - Create symlink: `/usr/local/bin/nupst → /opt/nupst/bin/nupst` | ||||||
|  |   - Update to v4.0.0 in script | ||||||
|  | - [ ] Simplify or remove `setup.sh` (no longer needed without Node.js) | ||||||
|  | - [ ] Update `bin/nupst` launcher: | ||||||
|  |   - Option A: Keep as simple wrapper | ||||||
|  |   - Option B: Remove and symlink directly to binary | ||||||
|  | - [ ] Update `uninstall.sh`: | ||||||
|  |   - Remove vendor directory cleanup | ||||||
|  |   - Update paths to new binary location | ||||||
|  |  | ||||||
|  | ### 4.4 Update Systemd Service | ||||||
|  |  | ||||||
|  | - [ ] Update systemd service file path in `ts/systemd.ts` | ||||||
|  | - [ ] Verify ExecStart points to correct binary location: `/opt/nupst/bin/nupst daemon-start` | ||||||
|  | - [ ] Remove Node.js environment variables if any | ||||||
|  | - [ ] Test service installation and startup | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 5: Testing & Validation (4-6 hours) | ||||||
|  |  | ||||||
|  | ### 5.1 Create Deno Test Suite | ||||||
|  |  | ||||||
|  | - [ ] Create `tests/` directory (or migrate from existing `test/`) | ||||||
|  | - [ ] Create `tests/snmp_test.ts`: Test SNMP manager functionality | ||||||
|  | - [ ] Create `tests/config_test.ts`: Test configuration loading/saving | ||||||
|  | - [ ] Create `tests/cli_test.ts`: Test CLI parsing and command routing | ||||||
|  | - [ ] Create `tests/daemon_test.ts`: Test daemon logic | ||||||
|  | - [ ] Remove dependency on @git.zone/tstest and @push.rocks/tapbundle | ||||||
|  | - [ ] Use Deno's built-in test runner (`Deno.test()`) | ||||||
|  |  | ||||||
|  | ### 5.2 Unit Tests | ||||||
|  |  | ||||||
|  | - [ ] Test SNMP connection with mock responses | ||||||
|  | - [ ] Test configuration validation | ||||||
|  | - [ ] Test UPS status parsing for different models | ||||||
|  | - [ ] Test group logic (redundant/non-redundant modes) | ||||||
|  | - [ ] Test threshold checking | ||||||
|  | - [ ] Test version comparison logic | ||||||
|  |  | ||||||
|  | ### 5.3 Integration Tests | ||||||
|  |  | ||||||
|  | - [ ] Test CLI command parsing for all commands | ||||||
|  | - [ ] Test config file creation and updates | ||||||
|  | - [ ] Test UPS add/edit/remove operations | ||||||
|  | - [ ] Test group add/edit/remove operations | ||||||
|  | - [ ] Mock systemd operations for testing | ||||||
|  |  | ||||||
|  | ### 5.4 Binary Testing | ||||||
|  |  | ||||||
|  | - [ ] Test compiled binary on Linux x64 | ||||||
|  | - [ ] Test compiled binary on Linux ARM64 (if available) | ||||||
|  | - [ ] Test compiled binary on macOS x64 (if available) | ||||||
|  | - [ ] Test compiled binary on macOS ARM64 (if available) | ||||||
|  | - [ ] Test compiled binary on Windows x64 (if available) | ||||||
|  | - [ ] Verify SNMP functionality works in compiled binary | ||||||
|  | - [ ] Verify config file operations work in compiled binary | ||||||
|  | - [ ] Test systemd integration with compiled binary | ||||||
|  |  | ||||||
|  | ### 5.5 Performance Testing | ||||||
|  |  | ||||||
|  | - [ ] Measure binary size for each platform | ||||||
|  | - [ ] Measure startup time: `time ./nupst-linux-x64 --version` | ||||||
|  | - [ ] Measure memory footprint during daemon operation | ||||||
|  | - [ ] Compare with Node.js version performance | ||||||
|  | - [ ] Document performance metrics | ||||||
|  |  | ||||||
|  | ### 5.6 Upgrade Path Testing | ||||||
|  |  | ||||||
|  | - [ ] Create test with v3.x config | ||||||
|  | - [ ] Verify v4.x can read existing config | ||||||
|  | - [ ] Test migration from old commands to new commands | ||||||
|  | - [ ] Verify systemd service upgrade path | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 6: Distribution Strategy (2-3 hours) | ||||||
|  |  | ||||||
|  | ### 6.1 GitHub Actions Workflow | ||||||
|  |  | ||||||
|  | - [ ] Create `.github/workflows/release.yml`: | ||||||
|  |   ```yaml | ||||||
|  |   name: Release | ||||||
|  |   on: | ||||||
|  |     push: | ||||||
|  |       tags: | ||||||
|  |         - 'v*' | ||||||
|  |   jobs: | ||||||
|  |     build: | ||||||
|  |       runs-on: ubuntu-latest | ||||||
|  |       steps: | ||||||
|  |         - uses: actions/checkout@v4 | ||||||
|  |         - uses: denoland/setup-deno@v1 | ||||||
|  |           with: | ||||||
|  |             deno-version: v1.x | ||||||
|  |         - name: Compile binaries | ||||||
|  |           run: deno task compile | ||||||
|  |         - name: Generate checksums | ||||||
|  |           run: | | ||||||
|  |             cd dist/binaries | ||||||
|  |             sha256sum * > SHA256SUMS | ||||||
|  |         - name: Create Release | ||||||
|  |           uses: softprops/action-gh-release@v1 | ||||||
|  |           with: | ||||||
|  |             files: dist/binaries/* | ||||||
|  |             generate_release_notes: true | ||||||
|  |   ``` | ||||||
|  |  | ||||||
|  | ### 6.2 Update package.json for npm | ||||||
|  |  | ||||||
|  | - [ ] Update version to 4.0.0 | ||||||
|  | - [ ] Update description to mention Deno | ||||||
|  | - [ ] Add postinstall script to symlink appropriate binary: | ||||||
|  |   ```json | ||||||
|  |   { | ||||||
|  |     "name": "@serve.zone/nupst", | ||||||
|  |     "version": "4.0.0", | ||||||
|  |     "description": "UPS Shutdown Tool - Deno-based single executable", | ||||||
|  |     "bin": { | ||||||
|  |       "nupst": "bin/nupst-npm-wrapper.js" | ||||||
|  |     }, | ||||||
|  |     "type": "module", | ||||||
|  |     "scripts": { | ||||||
|  |       "postinstall": "node bin/setup-npm-binary.js" | ||||||
|  |     }, | ||||||
|  |     "files": [ | ||||||
|  |       "dist/binaries/*", | ||||||
|  |       "bin/*" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  |   ``` | ||||||
|  | - [ ] Create `bin/setup-npm-binary.js` to symlink correct binary | ||||||
|  | - [ ] Create `bin/nupst-npm-wrapper.js` as entry point | ||||||
|  |  | ||||||
|  | ### 6.3 Verify Distribution Methods | ||||||
|  |  | ||||||
|  | - [ ] Test GitHub release download and installation | ||||||
|  | - [ ] Test npm install from tarball | ||||||
|  | - [ ] Test direct install.sh script | ||||||
|  | - [ ] Verify all methods create working installation | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 7: Documentation Updates (2-3 hours) | ||||||
|  |  | ||||||
|  | ### 7.1 Update README.md | ||||||
|  |  | ||||||
|  | - [ ] Remove Node.js requirements section | ||||||
|  | - [ ] Update features list (mention Deno, single executable) | ||||||
|  | - [ ] Update installation methods: | ||||||
|  |   - Method 1: Quick install script (updated) | ||||||
|  |   - Method 2: GitHub releases (new) | ||||||
|  |   - Method 3: npm (updated with notes) | ||||||
|  | - [ ] Update usage section with new command structure | ||||||
|  | - [ ] Add command mapping table (v3 → v4) | ||||||
|  | - [ ] Update platform support matrix (note: no Windows ARM) | ||||||
|  | - [ ] Update "System Changes" section (no vendor directory) | ||||||
|  | - [ ] Update security section (remove Node.js mentions) | ||||||
|  | - [ ] Update uninstallation instructions | ||||||
|  |  | ||||||
|  | ### 7.2 Create MIGRATION.md | ||||||
|  |  | ||||||
|  | - [ ] Create detailed migration guide from v3.x to v4.x | ||||||
|  | - [ ] List all breaking changes: | ||||||
|  |   1. CLI command structure reorganization | ||||||
|  |   2. No Node.js requirement | ||||||
|  |   3. Windows ARM not supported | ||||||
|  |   4. Installation path changes | ||||||
|  | - [ ] Provide command mapping table | ||||||
|  | - [ ] Explain config compatibility | ||||||
|  | - [ ] Document upgrade procedure | ||||||
|  | - [ ] Add rollback instructions | ||||||
|  |  | ||||||
|  | ### 7.3 Update CHANGELOG.md | ||||||
|  |  | ||||||
|  | - [ ] Add v4.0.0 section with all breaking changes | ||||||
|  | - [ ] List new features (Deno, single executable) | ||||||
|  | - [ ] List improvements (startup time, binary size) | ||||||
|  | - [ ] List removed features (Windows ARM, setup command alias) | ||||||
|  | - [ ] Migration guide reference | ||||||
|  |  | ||||||
|  | ### 7.4 Update Help Text | ||||||
|  |  | ||||||
|  | - [ ] Ensure all help commands show new structure | ||||||
|  | - [ ] Add examples for common operations | ||||||
|  | - [ ] Include migration notes in help output | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Phase 8: Cleanup & Finalization (1 hour) | ||||||
|  |  | ||||||
|  | ### 8.1 Remove Obsolete Files | ||||||
|  |  | ||||||
|  | - [ ] Delete `vendor/` directory (Node.js binaries) | ||||||
|  | - [ ] Delete `dist/` directory (old compiled JS) | ||||||
|  | - [ ] Delete `dist_ts/` directory (old compiled TS) | ||||||
|  | - [ ] Delete `node_modules/` directory | ||||||
|  | - [ ] Remove or update `tsconfig.json` (decide if needed for npm compatibility) | ||||||
|  | - [ ] Remove `setup.sh` if no longer needed | ||||||
|  | - [ ] Remove old test files in `test/` if migrated to `tests/` | ||||||
|  | - [ ] Delete `pnpm-lock.yaml` | ||||||
|  |  | ||||||
|  | ### 8.2 Update Git Configuration | ||||||
|  |  | ||||||
|  | - [ ] Update `.gitignore`: | ||||||
|  |   ``` | ||||||
|  |   # Deno | ||||||
|  |   .deno/ | ||||||
|  |   deno.lock | ||||||
|  |  | ||||||
|  |   # Compiled binaries | ||||||
|  |   dist/binaries/ | ||||||
|  |  | ||||||
|  |   # Old Node.js artifacts (to be removed) | ||||||
|  |   node_modules/ | ||||||
|  |   vendor/ | ||||||
|  |   dist/ | ||||||
|  |   dist_ts/ | ||||||
|  |   pnpm-lock.yaml | ||||||
|  |   ``` | ||||||
|  | - [ ] Add `deno.lock` to version control | ||||||
|  | - [ ] Create `.denoignore` if needed | ||||||
|  |  | ||||||
|  | ### 8.3 Final Validation | ||||||
|  |  | ||||||
|  | - [ ] Run `deno check mod.ts` - verify no type errors | ||||||
|  | - [ ] Run `deno lint` - verify code quality | ||||||
|  | - [ ] Run `deno fmt --check` - verify formatting | ||||||
|  | - [ ] Run `deno task test` - verify all tests pass | ||||||
|  | - [ ] Run `deno task compile` - verify all binaries compile | ||||||
|  | - [ ] Test each binary manually | ||||||
|  |  | ||||||
|  | ### 8.4 Prepare for Release | ||||||
|  |  | ||||||
|  | - [ ] Create git tag: `v4.0.0` | ||||||
|  | - [ ] Push to main branch | ||||||
|  | - [ ] Push tags to trigger release workflow | ||||||
|  | - [ ] Verify GitHub Actions workflow succeeds | ||||||
|  | - [ ] Verify binaries are attached to release | ||||||
|  | - [ ] Test installation from GitHub release | ||||||
|  | - [ ] Publish to npm: `npm publish` | ||||||
|  | - [ ] Test npm installation | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Rollback Strategy | ||||||
|  |  | ||||||
|  | If critical issues are discovered: | ||||||
|  |  | ||||||
|  | - [ ] Keep `v3.1.2` tag available for rollback | ||||||
|  | - [ ] Create `v3-stable` branch for continued v3 maintenance | ||||||
|  | - [ ] Update install.sh to offer v3/v4 choice | ||||||
|  | - [ ] Document known issues in GitHub Issues | ||||||
|  | - [ ] Provide downgrade instructions in docs | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Success Criteria Checklist | ||||||
|  |  | ||||||
|  | - [ ] ✅ All 5 platform binaries compile successfully | ||||||
|  | - [ ] ✅ Binary sizes are reasonable (< 100MB per platform) | ||||||
|  | - [ ] ✅ Startup time < 2 seconds | ||||||
|  | - [ ] ✅ SNMP v1/v2c/v3 functionality verified on real UPS device | ||||||
|  | - [ ] ✅ All CLI commands work with new structure | ||||||
|  | - [ ] ✅ Config file compatibility maintained | ||||||
|  | - [ ] ✅ Systemd integration works on Linux | ||||||
|  | - [ ] ✅ Installation scripts work on fresh systems | ||||||
|  | - [ ] ✅ npm package still installable and functional | ||||||
|  | - [ ] ✅ All tests pass | ||||||
|  | - [ ] ✅ Documentation is complete and accurate | ||||||
|  | - [ ] ✅ GitHub release created with binaries | ||||||
|  | - [ ] ✅ Migration guide tested by following it step-by-step | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Timeline | ||||||
|  |  | ||||||
|  | - **Phase 0**: 1 hour ✓ (in progress) | ||||||
|  | - **Phase 1**: 4-6 hours | ||||||
|  | - **Phase 2**: 3-4 hours | ||||||
|  | - **Phase 3**: 3-4 hours | ||||||
|  | - **Phase 4**: 2-3 hours | ||||||
|  | - **Phase 5**: 4-6 hours | ||||||
|  | - **Phase 6**: 2-3 hours | ||||||
|  | - **Phase 7**: 2-3 hours | ||||||
|  | - **Phase 8**: 1 hour | ||||||
|  |  | ||||||
|  | **Total Estimate**: 22-31 hours | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ## Notes & Decisions | ||||||
|  |  | ||||||
|  | ### Key Decisions Made: | ||||||
|  |  | ||||||
|  | 1. ✅ Use npm:net-snmp (no pure Deno SNMP library available) | ||||||
|  | 2. ✅ Major version bump to 4.0.0 (breaking changes) | ||||||
|  | 3. ✅ CLI reorganization with subcommands | ||||||
|  | 4. ✅ Keep npm publishing alongside binary distribution | ||||||
|  | 5. ✅ 5 platform targets (Windows ARM not supported by Deno yet) | ||||||
|  |  | ||||||
|  | ### Open Questions: | ||||||
|  |  | ||||||
|  | - [ ] Should we keep tsconfig.json for npm package compatibility? | ||||||
|  | - [ ] Should we fully migrate to Deno APIs (Deno.readFile) or keep node:fs? | ||||||
|  | - [ ] Should we remove the `bin/nupst` wrapper or keep it? | ||||||
|  | - [ ] Should setup.sh be completely removed or kept for dependencies? | ||||||
|  |  | ||||||
|  | ### Risk Areas: | ||||||
|  |  | ||||||
|  | - ⚠️ SNMP native addon compatibility in compiled binaries (HIGH PRIORITY TO TEST) | ||||||
|  | - ⚠️ Systemd integration with new binary structure | ||||||
|  | - ⚠️ Config migration from v3 to v4 | ||||||
|  | - ⚠️ npm package installation with embedded binaries | ||||||
							
								
								
									
										66
									
								
								scripts/compile-all.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										66
									
								
								scripts/compile-all.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # Get version from deno.json | ||||||
|  | VERSION=$(cat deno.json | grep -o '"version": *"[^"]*"' | cut -d'"' -f4) | ||||||
|  | BINARY_DIR="dist/binaries" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  NUPST Compilation Script" | ||||||
|  | echo "  Version: ${VERSION}" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | echo "Compiling for all supported platforms..." | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Clean up old binaries and create fresh directory | ||||||
|  | rm -rf "$BINARY_DIR" | ||||||
|  | mkdir -p "$BINARY_DIR" | ||||||
|  | echo "→ Cleaned old binaries from $BINARY_DIR" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Linux x86_64 | ||||||
|  | echo "→ Compiling for Linux x86_64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-x64" \ | ||||||
|  |   --target x86_64-unknown-linux-gnu mod.ts | ||||||
|  | echo "  ✓ Linux x86_64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Linux ARM64 | ||||||
|  | echo "→ Compiling for Linux ARM64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-linux-arm64" \ | ||||||
|  |   --target aarch64-unknown-linux-gnu mod.ts | ||||||
|  | echo "  ✓ Linux ARM64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # macOS x86_64 | ||||||
|  | echo "→ Compiling for macOS x86_64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-x64" \ | ||||||
|  |   --target x86_64-apple-darwin mod.ts | ||||||
|  | echo "  ✓ macOS x86_64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # macOS ARM64 | ||||||
|  | echo "→ Compiling for macOS ARM64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-macos-arm64" \ | ||||||
|  |   --target aarch64-apple-darwin mod.ts | ||||||
|  | echo "  ✓ macOS ARM64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Windows x86_64 | ||||||
|  | echo "→ Compiling for Windows x86_64..." | ||||||
|  | deno compile --allow-all --no-check --output "$BINARY_DIR/nupst-windows-x64.exe" \ | ||||||
|  |   --target x86_64-pc-windows-msvc mod.ts | ||||||
|  | echo "  ✓ Windows x86_64 complete" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  Compilation Summary" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | ls -lh "$BINARY_DIR/" | tail -n +2 | ||||||
|  | echo "" | ||||||
|  | echo "✓ All binaries compiled successfully!" | ||||||
|  | echo "" | ||||||
|  | echo "Binary location: $BINARY_DIR/" | ||||||
|  | echo "" | ||||||
							
								
								
									
										328
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										328
									
								
								setup.sh
									
									
									
									
									
								
							| @@ -1,328 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| # NUPST Setup Script |  | ||||||
| # Downloads the appropriate Node.js binary for the current platform |  | ||||||
| # and installs production dependencies |  | ||||||
|  |  | ||||||
| # Parse command line arguments |  | ||||||
| FORCE_UPDATE=0 |  | ||||||
|  |  | ||||||
| for arg in "$@"; do |  | ||||||
|   case $arg in |  | ||||||
|     --force|-f) |  | ||||||
|       FORCE_UPDATE=1 |  | ||||||
|       shift |  | ||||||
|       ;; |  | ||||||
|     *) |  | ||||||
|       # Unknown option |  | ||||||
|       ;; |  | ||||||
|   esac |  | ||||||
| done |  | ||||||
|  |  | ||||||
| # Find the directory where this script is located |  | ||||||
| SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" |  | ||||||
|  |  | ||||||
| # Create vendor directory if it doesn't exist |  | ||||||
| mkdir -p "$SCRIPT_DIR/vendor" |  | ||||||
|  |  | ||||||
| # Get the latest LTS Node.js version |  | ||||||
| echo "Determining latest LTS Node.js version..." |  | ||||||
| NODE_VERSIONS_JSON=$(curl -s https://nodejs.org/dist/index.json) |  | ||||||
| if [ $? -ne 0 ]; then |  | ||||||
|   echo "Warning: Could not fetch latest Node.js versions. Using fallback version." |  | ||||||
|   NODE_VERSION="20.11.1"  # Fallback to a recent LTS version |  | ||||||
| else |  | ||||||
|   # Extract the latest LTS version (those marked with lts field) |  | ||||||
|   NODE_VERSION=$(echo "$NODE_VERSIONS_JSON" | grep -o '"version":"v[0-9.]*".*"lts":[^,]*' | grep -v '"lts":false' | grep -o 'v[0-9.]*' | head -1 | cut -c 2-) |  | ||||||
|    |  | ||||||
|   if [ -z "$NODE_VERSION" ]; then |  | ||||||
|     echo "Warning: Could not determine latest LTS version. Using fallback version." |  | ||||||
|     NODE_VERSION="20.11.1"  # Fallback to a recent LTS version |  | ||||||
|   else |  | ||||||
|     echo "Latest Node.js LTS version: $NODE_VERSION" |  | ||||||
|   fi |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Detect architecture |  | ||||||
| ARCH=$(uname -m) |  | ||||||
| OS=$(uname -s) |  | ||||||
|  |  | ||||||
| # Map architecture and OS to Node.js download URL |  | ||||||
| NODE_URL="" |  | ||||||
| NODE_DIR="" |  | ||||||
| case "$OS" in |  | ||||||
|   Linux) |  | ||||||
|     case "$ARCH" in |  | ||||||
|       x86_64) |  | ||||||
|         NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" |  | ||||||
|         NODE_DIR="node-linux-x64" |  | ||||||
|         ;; |  | ||||||
|       aarch64|arm64) |  | ||||||
|         NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-arm64.tar.gz" |  | ||||||
|         NODE_DIR="node-linux-arm64" |  | ||||||
|         ;; |  | ||||||
|       *) |  | ||||||
|         echo "Unsupported architecture: $ARCH. Please install Node.js manually." |  | ||||||
|         exit 1 |  | ||||||
|         ;; |  | ||||||
|     esac |  | ||||||
|     ;; |  | ||||||
|   Darwin) |  | ||||||
|     case "$ARCH" in |  | ||||||
|       x86_64) |  | ||||||
|         NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-x64.tar.gz" |  | ||||||
|         NODE_DIR="node-darwin-x64" |  | ||||||
|         ;; |  | ||||||
|       arm64) |  | ||||||
|         NODE_URL="https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-darwin-arm64.tar.gz" |  | ||||||
|         NODE_DIR="node-darwin-arm64" |  | ||||||
|         ;; |  | ||||||
|       *) |  | ||||||
|         echo "Unsupported architecture: $ARCH. Please install Node.js manually." |  | ||||||
|         exit 1 |  | ||||||
|         ;; |  | ||||||
|     esac |  | ||||||
|     ;; |  | ||||||
|   *) |  | ||||||
|     echo "Unsupported operating system: $OS. Please install Node.js manually." |  | ||||||
|     exit 1 |  | ||||||
|     ;; |  | ||||||
| esac |  | ||||||
|  |  | ||||||
| # Check if we already have the Node.js binary |  | ||||||
| if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ] && [ $FORCE_UPDATE -eq 0 ]; then |  | ||||||
|   echo "Node.js binary already exists for $OS-$ARCH. Skipping download." |  | ||||||
|   echo "Use --force or -f to force update Node.js." |  | ||||||
| else |  | ||||||
|   echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..." |  | ||||||
|    |  | ||||||
|   # Download and extract Node.js |  | ||||||
|   TMP_FILE="$SCRIPT_DIR/vendor/node.tar.gz" |  | ||||||
|   curl -L "$NODE_URL" -o "$TMP_FILE" |  | ||||||
|    |  | ||||||
|   if [ $? -ne 0 ]; then |  | ||||||
|     echo "Error downloading Node.js. Please check your internet connection and try again." |  | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   # Create target directory |  | ||||||
|   mkdir -p "$SCRIPT_DIR/vendor/$NODE_DIR" |  | ||||||
|    |  | ||||||
|   # Extract Node.js |  | ||||||
|   tar -xzf "$TMP_FILE" -C "$SCRIPT_DIR/vendor" |  | ||||||
|    |  | ||||||
|   # Move extracted files to the target directory |  | ||||||
|   NODE_EXTRACT_DIR=$(find "$SCRIPT_DIR/vendor" -maxdepth 1 -name "node-v*" -type d | head -n 1) |  | ||||||
|   if [ -d "$NODE_EXTRACT_DIR" ]; then |  | ||||||
|     cp -R "$NODE_EXTRACT_DIR"/* "$SCRIPT_DIR/vendor/$NODE_DIR/" |  | ||||||
|     rm -rf "$NODE_EXTRACT_DIR" |  | ||||||
|   else |  | ||||||
|     echo "Error extracting Node.js. Please try again." |  | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   # Clean up |  | ||||||
|   rm "$TMP_FILE" |  | ||||||
|    |  | ||||||
|   echo "Node.js v$NODE_VERSION for $OS-$ARCH has been downloaded and extracted." |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Remove any existing dist_ts directory |  | ||||||
| if [ -d "$SCRIPT_DIR/dist_ts" ]; then |  | ||||||
|   echo "Removing existing dist_ts directory..." |  | ||||||
|   rm -rf "$SCRIPT_DIR/dist_ts" |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Download dist_ts from npm registry |  | ||||||
| echo "Downloading dist_ts from npm registry..." |  | ||||||
|  |  | ||||||
| # Create temp directory |  | ||||||
| TEMP_DIR=$(mktemp -d) |  | ||||||
|  |  | ||||||
| # Get version from package.json |  | ||||||
| if [ -f "$SCRIPT_DIR/package.json" ]; then |  | ||||||
|   echo "Reading version from package.json..." |  | ||||||
|   # Extract version using grep and cut |  | ||||||
|   VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json" | cut -d'"' -f4) |  | ||||||
|    |  | ||||||
|   if [ -z "$VERSION" ]; then |  | ||||||
|     echo "Error: Could not determine version from package.json." |  | ||||||
|     rm -rf "$TEMP_DIR" |  | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   echo "Package version is $VERSION. Downloading matching package tarball..." |  | ||||||
| else |  | ||||||
|   echo "Warning: package.json not found. Getting latest version from npm registry..." |  | ||||||
|   VERSION=$(curl -s https://registry.npmjs.org/@serve.zone/nupst | grep -o '"latest":"[^"]*"' | cut -d'"' -f4) |  | ||||||
|    |  | ||||||
|   if [ -z "$VERSION" ]; then |  | ||||||
|     echo "Error: Could not determine version from npm registry." |  | ||||||
|     rm -rf "$TEMP_DIR" |  | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   echo "Latest version is $VERSION. Using as fallback." |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # First try to download with the version from package.json |  | ||||||
| TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$VERSION.tgz" |  | ||||||
| TARBALL_PATH="$TEMP_DIR/nupst.tgz" |  | ||||||
|  |  | ||||||
| echo "Attempting to download version $VERSION from $TARBALL_URL..." |  | ||||||
| curl -sL "$TARBALL_URL" -o "$TARBALL_PATH" |  | ||||||
|  |  | ||||||
| # If download fails or file is empty, try to get the latest version from npm |  | ||||||
| if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then |  | ||||||
|   echo "Package version $VERSION not found on npm registry." |  | ||||||
|   echo "Fetching latest version information from npm registry..." |  | ||||||
|    |  | ||||||
|   # Get latest version from npm registry |  | ||||||
|   NPM_REGISTRY_INFO=$(curl -s https://registry.npmjs.org/@serve.zone/nupst) |  | ||||||
|    |  | ||||||
|   if [ $? -ne 0 ]; then |  | ||||||
|     echo "Error: Could not connect to npm registry." |  | ||||||
|     echo "Will attempt to build from source instead." |  | ||||||
|     rm -rf "$TEMP_DIR" |  | ||||||
|     mkdir -p "$SCRIPT_DIR/dist_ts" |  | ||||||
|     BUILD_FROM_SOURCE=1 |  | ||||||
|     return 0 |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   # Extract latest version |  | ||||||
|   LATEST_VERSION=$(echo "$NPM_REGISTRY_INFO" | grep -o '"latest":"[^"]*"' | cut -d'"' -f4) |  | ||||||
|    |  | ||||||
|   if [ -z "$LATEST_VERSION" ]; then |  | ||||||
|     echo "Error: Could not determine latest version from npm registry." |  | ||||||
|     echo "Will attempt to build from source instead." |  | ||||||
|     rm -rf "$TEMP_DIR" |  | ||||||
|     mkdir -p "$SCRIPT_DIR/dist_ts" |  | ||||||
|     BUILD_FROM_SOURCE=1 |  | ||||||
|     return 0 |  | ||||||
|   fi |  | ||||||
|    |  | ||||||
|   echo "Found latest version: $LATEST_VERSION. Downloading..." |  | ||||||
|    |  | ||||||
|   TARBALL_URL="https://registry.npmjs.org/@serve.zone/nupst/-/nupst-$LATEST_VERSION.tgz" |  | ||||||
|   TARBALL_PATH="$TEMP_DIR/nupst.tgz" |  | ||||||
|    |  | ||||||
|   curl -sL "$TARBALL_URL" -o "$TARBALL_PATH" |  | ||||||
|    |  | ||||||
|   if [ $? -ne 0 ] || [ ! -s "$TARBALL_PATH" ]; then |  | ||||||
|     echo "Error: Failed to download any package version from npm registry." |  | ||||||
|     echo "Installation cannot continue without the dist_ts directory." |  | ||||||
|     rm -rf "$TEMP_DIR" |  | ||||||
|     exit 1 |  | ||||||
|   fi |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Extract the tarball |  | ||||||
| mkdir -p "$TEMP_DIR/extract" |  | ||||||
| tar -xzf "$TARBALL_PATH" -C "$TEMP_DIR/extract" |  | ||||||
|  |  | ||||||
| # Copy dist_ts to the installation directory |  | ||||||
| if [ -d "$TEMP_DIR/extract/package/dist_ts" ]; then |  | ||||||
|   echo "Copying dist_ts directory to installation..." |  | ||||||
|   mkdir -p "$SCRIPT_DIR/dist_ts" |  | ||||||
|   cp -R "$TEMP_DIR/extract/package/dist_ts/"* "$SCRIPT_DIR/dist_ts/" |  | ||||||
| else |  | ||||||
|   echo "Error: dist_ts directory not found in the downloaded npm package." |  | ||||||
|   rm -rf "$TEMP_DIR" |  | ||||||
|   exit 1 |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Clean up |  | ||||||
| rm -rf "$TEMP_DIR" |  | ||||||
|  |  | ||||||
| echo "dist_ts directory successfully downloaded from npm registry." |  | ||||||
|  |  | ||||||
| # Make launcher script executable |  | ||||||
| chmod +x "$SCRIPT_DIR/bin/nupst" |  | ||||||
|  |  | ||||||
| # Set up Node.js binary path |  | ||||||
| NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin" |  | ||||||
| NODE_BIN="$NODE_BIN_DIR/node" |  | ||||||
| NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js" |  | ||||||
|  |  | ||||||
| # Ensure we have executable permissions |  | ||||||
| chmod +x "$NODE_BIN" |  | ||||||
|  |  | ||||||
| # Make sure the npm-cli.js exists |  | ||||||
| if [ ! -f "$NPM_CLI_JS" ]; then |  | ||||||
|   # Try to find npm-cli.js |  | ||||||
|   NPM_CLI_JS=$(find "$NODE_BIN_DIR/.." -name "npm-cli.js" | head -1) |  | ||||||
|    |  | ||||||
|   if [ -z "$NPM_CLI_JS" ]; then |  | ||||||
|     echo "Warning: Could not find npm-cli.js, npm commands may fail" |  | ||||||
|     # Set to a fallback value so code can continue |  | ||||||
|     NPM_CLI_JS="$NODE_BIN_DIR/npm" |  | ||||||
|   else |  | ||||||
|     echo "Found npm-cli.js at: $NPM_CLI_JS" |  | ||||||
|   fi |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Display which binaries we're using |  | ||||||
| echo "Using Node binary: $NODE_BIN" |  | ||||||
| echo "Using NPM CLI JS: $NPM_CLI_JS" |  | ||||||
|  |  | ||||||
| # Remove existing node_modules directory and package files |  | ||||||
| echo "Cleaning up existing installation..." |  | ||||||
| rm -rf "$SCRIPT_DIR/node_modules" |  | ||||||
| rm -f "$SCRIPT_DIR/package-lock.json" |  | ||||||
|  |  | ||||||
| # Back up existing package.json if it exists |  | ||||||
| if [ -f "$SCRIPT_DIR/package.json" ]; then |  | ||||||
|   echo "Backing up existing package.json..." |  | ||||||
|   cp "$SCRIPT_DIR/package.json" "$SCRIPT_DIR/package.json.bak" |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # Create a clean minimal package.json with ONLY net-snmp dependency |  | ||||||
| echo "Creating minimal package.json with only net-snmp dependency..." |  | ||||||
| VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json.bak" | head -1 | cut -d'"' -f4 || echo "2.6.3") |  | ||||||
| echo '{ |  | ||||||
|   "name": "@serve.zone/nupst", |  | ||||||
|   "version": "'$VERSION'", |  | ||||||
|   "description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices", |  | ||||||
|   "main": "dist_ts/index.js", |  | ||||||
|   "type": "module", |  | ||||||
|   "bin": { |  | ||||||
|     "nupst": "bin/nupst" |  | ||||||
|   }, |  | ||||||
|   "dependencies": { |  | ||||||
|     "net-snmp": "3.20.0" |  | ||||||
|   }, |  | ||||||
|   "engines": { |  | ||||||
|     "node": ">=16.0.0" |  | ||||||
|   }, |  | ||||||
|   "private": true |  | ||||||
| }' > "$SCRIPT_DIR/package.json" |  | ||||||
|  |  | ||||||
| # Install ONLY net-snmp |  | ||||||
| echo "Installing ONLY net-snmp dependency (+ 2 subdependencies)..." |  | ||||||
| echo "Node version: $("$NODE_BIN" --version)" |  | ||||||
| echo "Executing NPM directly with Node.js" |  | ||||||
|  |  | ||||||
| # Execute npm-cli.js directly with our Node.js binary |  | ||||||
| "$NODE_BIN" "$NPM_CLI_JS" --prefix "$SCRIPT_DIR" install --no-audit --no-fund |  | ||||||
|  |  | ||||||
| INSTALL_STATUS=$? |  | ||||||
| if [ $INSTALL_STATUS -ne 0 ]; then |  | ||||||
|   echo "Error: Failed to install net-snmp dependency. NUPST may not function correctly." |  | ||||||
|   echo "Restoring original package.json..." |  | ||||||
|   mv "$SCRIPT_DIR/package.json.bak" "$SCRIPT_DIR/package.json" |  | ||||||
|   exit 1 |  | ||||||
| else |  | ||||||
|   echo "net-snmp dependency installed successfully." |  | ||||||
|   # Show what's actually installed |  | ||||||
|   echo "Installed modules:" |  | ||||||
|   find "$SCRIPT_DIR/node_modules" -maxdepth 1 -type d | grep -v "^$SCRIPT_DIR/node_modules$" | sort |  | ||||||
|    |  | ||||||
|   # Remove backup if successful |  | ||||||
|   rm -f "$SCRIPT_DIR/package.json.bak" |  | ||||||
| fi |  | ||||||
|  |  | ||||||
| # No temporary files to clean up |  | ||||||
|  |  | ||||||
| echo "NUPST setup completed successfully." |  | ||||||
| echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst" |  | ||||||
| echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst" |  | ||||||
							
								
								
									
										168
									
								
								test/manualdocker/00-test-fresh-v4-install.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										168
									
								
								test/manualdocker/00-test-fresh-v4-install.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,168 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # | ||||||
|  | # Test fresh v4 installation from scratch | ||||||
|  | # Tests the most common user scenario: clean install using curl | bash | ||||||
|  | # | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | CONTAINER_NAME="nupst-test-fresh-v4" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  NUPST Fresh v4 Installation Test" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Check if container already exists | ||||||
|  | if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then | ||||||
|  |   echo "⚠️  Container ${CONTAINER_NAME} already exists" | ||||||
|  |   read -p "Remove and recreate? (y/N): " -n 1 -r | ||||||
|  |   echo | ||||||
|  |   if [[ $REPLY =~ ^[Yy]$ ]]; then | ||||||
|  |     echo "→ Stopping and removing existing container..." | ||||||
|  |     docker stop ${CONTAINER_NAME} 2>/dev/null || true | ||||||
|  |     docker rm ${CONTAINER_NAME} 2>/dev/null || true | ||||||
|  |   else | ||||||
|  |     echo "Exiting. Remove manually with: docker rm -f ${CONTAINER_NAME}" | ||||||
|  |     exit 1 | ||||||
|  |   fi | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | echo "→ Creating Docker container with systemd..." | ||||||
|  | docker run -d \ | ||||||
|  |   --name ${CONTAINER_NAME} \ | ||||||
|  |   --privileged \ | ||||||
|  |   --cgroupns=host \ | ||||||
|  |   -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ | ||||||
|  |   ubuntu:22.04 \ | ||||||
|  |   /bin/bash -c "apt-get update && apt-get install -y systemd systemd-sysv && exec /sbin/init" | ||||||
|  |  | ||||||
|  | echo "→ Waiting for systemd to initialize..." | ||||||
|  | sleep 10 | ||||||
|  |  | ||||||
|  | echo "→ Waiting for dpkg lock to be released..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do | ||||||
|  |     echo '  Waiting for dpkg lock...' | ||||||
|  |     sleep 2 | ||||||
|  |   done | ||||||
|  |   echo '  dpkg lock released' | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Installing prerequisites (curl)..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   apt-get update -qq | ||||||
|  |   apt-get install -y -qq curl | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "→ Installing NUPST v4 using curl | bash..." | ||||||
|  | echo "   Command: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "================================================" | ||||||
|  | echo "  Verifying Installation" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "→ Checking binary location..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   if [ -f /opt/nupst/nupst ]; then | ||||||
|  |     echo '  ✓ Binary exists at /opt/nupst/nupst' | ||||||
|  |     ls -lh /opt/nupst/nupst | ||||||
|  |   else | ||||||
|  |     echo '  ✗ Binary not found at /opt/nupst/nupst' | ||||||
|  |     exit 1 | ||||||
|  |   fi | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "→ Checking symlink..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   if [ -L /usr/local/bin/nupst ]; then | ||||||
|  |     echo '  ✓ Symlink exists at /usr/local/bin/nupst' | ||||||
|  |     ls -lh /usr/local/bin/nupst | ||||||
|  |   elif [ -L /usr/bin/nupst ]; then | ||||||
|  |     echo '  ✓ Symlink exists at /usr/bin/nupst' | ||||||
|  |     ls -lh /usr/bin/nupst | ||||||
|  |   else | ||||||
|  |     echo '  ✗ Symlink not found in /usr/local/bin or /usr/bin' | ||||||
|  |     exit 1 | ||||||
|  |   fi | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "→ Checking PATH integration..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   NUPST_PATH=\$(which nupst 2>/dev/null) | ||||||
|  |   if [ -n \"\$NUPST_PATH\" ]; then | ||||||
|  |     echo '  ✓ nupst found in PATH at: '\$NUPST_PATH | ||||||
|  |   else | ||||||
|  |     echo '  ✗ nupst not found in PATH' | ||||||
|  |     echo '  PATH contents:' | ||||||
|  |     echo \$PATH | ||||||
|  |     exit 1 | ||||||
|  |   fi | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "→ Testing nupst command execution..." | ||||||
|  | docker exec ${CONTAINER_NAME} nupst --version | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "→ Creating minimal config for service test..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   mkdir -p /etc/nupst | ||||||
|  |   cat > /etc/nupst/config.json << 'EOF' | ||||||
|  | { | ||||||
|  |   \"version\": \"4.0\", | ||||||
|  |   \"upsDevices\": [], | ||||||
|  |   \"groups\": [], | ||||||
|  |   \"checkInterval\": 30000 | ||||||
|  | } | ||||||
|  | EOF | ||||||
|  |   echo '  ✓ Minimal config created' | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "→ Testing service creation..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   echo '  Running: nupst service enable' | ||||||
|  |   nupst service enable | ||||||
|  |  | ||||||
|  |   if [ -f /etc/systemd/system/nupst.service ]; then | ||||||
|  |     echo '  ✓ Service file created successfully' | ||||||
|  |   else | ||||||
|  |     echo '  ✗ Service file creation failed' | ||||||
|  |     exit 1 | ||||||
|  |   fi | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "→ Checking if service is enabled..." | ||||||
|  | docker exec ${CONTAINER_NAME} systemctl is-enabled nupst | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "================================================" | ||||||
|  | echo "  ✓ Fresh v4 Installation Test Complete" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | echo "Installation verified successfully:" | ||||||
|  | echo "  • Binary installed to /opt/nupst/nupst" | ||||||
|  | echo "  • Symlink created for global access" | ||||||
|  | echo "  • nupst command available in PATH" | ||||||
|  | echo "  • Command executes correctly" | ||||||
|  | echo "  • Systemd service file created" | ||||||
|  | echo "" | ||||||
|  | echo "Useful commands:" | ||||||
|  | echo "  docker exec -it ${CONTAINER_NAME} bash" | ||||||
|  | echo "  docker exec ${CONTAINER_NAME} nupst --help" | ||||||
|  | echo "  docker exec ${CONTAINER_NAME} nupst service status" | ||||||
|  | echo "  docker stop ${CONTAINER_NAME}" | ||||||
|  | echo "  docker rm -f ${CONTAINER_NAME}" | ||||||
|  | echo "" | ||||||
							
								
								
									
										148
									
								
								test/manualdocker/01-setup-v3-container.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										148
									
								
								test/manualdocker/01-setup-v3-container.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,148 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # | ||||||
|  | # Setup Docker container with systemd and install NUPST v3 | ||||||
|  | # This creates a container from commit 806f81c6a057a2a5da586b96a231d391f12eb1bb (v3) | ||||||
|  | # | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | CONTAINER_NAME="nupst-test-v3" | ||||||
|  | V3_COMMIT="806f81c6a057a2a5da586b96a231d391f12eb1bb" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  NUPST v3 Test Container Setup" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Check if container already exists | ||||||
|  | if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then | ||||||
|  |   echo "⚠️  Container ${CONTAINER_NAME} already exists" | ||||||
|  |   read -p "Remove and recreate? (y/N): " -n 1 -r | ||||||
|  |   echo | ||||||
|  |   if [[ $REPLY =~ ^[Yy]$ ]]; then | ||||||
|  |     echo "→ Stopping and removing existing container..." | ||||||
|  |     docker stop ${CONTAINER_NAME} 2>/dev/null || true | ||||||
|  |     docker rm ${CONTAINER_NAME} 2>/dev/null || true | ||||||
|  |   else | ||||||
|  |     echo "Exiting. Remove manually with: docker rm -f ${CONTAINER_NAME}" | ||||||
|  |     exit 1 | ||||||
|  |   fi | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | echo "→ Creating Docker container (will install systemd)..." | ||||||
|  | docker run -d \ | ||||||
|  |   --name ${CONTAINER_NAME} \ | ||||||
|  |   --privileged \ | ||||||
|  |   --cgroupns=host \ | ||||||
|  |   -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ | ||||||
|  |   ubuntu:22.04 \ | ||||||
|  |   /bin/bash -c "apt-get update && apt-get install -y systemd systemd-sysv && exec /sbin/init" | ||||||
|  |  | ||||||
|  | echo "→ Waiting for systemd to initialize..." | ||||||
|  | sleep 10 | ||||||
|  |  | ||||||
|  | echo "→ Waiting for dpkg lock to be released..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do | ||||||
|  |     echo '  Waiting for dpkg lock...' | ||||||
|  |     sleep 2 | ||||||
|  |   done | ||||||
|  |   echo '  dpkg lock released' | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Installing prerequisites in container..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   apt-get update -qq | ||||||
|  |   apt-get install -y -qq git curl sudo jq | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Cloning NUPST v3 (commit ${V3_COMMIT})..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   cd /opt | ||||||
|  |   git clone https://code.foss.global/serve.zone/nupst.git | ||||||
|  |   cd nupst | ||||||
|  |   git checkout ${V3_COMMIT} | ||||||
|  |   echo 'Checked out commit:' | ||||||
|  |   git log -1 --oneline | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Running NUPST v3 installation directly (bypassing install.sh auto-update)..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   cd /opt/nupst | ||||||
|  |   # Run setup.sh directly to avoid install.sh trying to update to v4 | ||||||
|  |   bash setup.sh -y | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Creating NUPST configuration using real UPS data from .nogit/env.json..." | ||||||
|  |  | ||||||
|  | # Check if .nogit/env.json exists | ||||||
|  | if [ ! -f "../../.nogit/env.json" ]; then | ||||||
|  |   echo "❌ Error: .nogit/env.json not found" | ||||||
|  |   echo "This file contains test UPS credentials and is required for testing" | ||||||
|  |   exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Read UPS data from .nogit/env.json and create v3 config | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c "mkdir -p /etc/nupst" | ||||||
|  |  | ||||||
|  | # Generate config from .nogit/env.json using jq | ||||||
|  | cat ../../.nogit/env.json | jq -r ' | ||||||
|  | { | ||||||
|  |   "upsList": [ | ||||||
|  |     { | ||||||
|  |       "id": "test-ups-v1", | ||||||
|  |       "name": "Test UPS (SNMP v1)", | ||||||
|  |       "host": .testConfigV1.snmp.host, | ||||||
|  |       "port": .testConfigV1.snmp.port, | ||||||
|  |       "community": .testConfigV1.snmp.community, | ||||||
|  |       "version": (.testConfigV1.snmp.version | tostring), | ||||||
|  |       "batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0", | ||||||
|  |       "onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0", | ||||||
|  |       "shutdownCommand": "echo \"Shutdown triggered for test-ups-v1\"" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "id": "test-ups-v3", | ||||||
|  |       "name": "Test UPS (SNMP v3)", | ||||||
|  |       "host": .testConfigV3.snmp.host, | ||||||
|  |       "port": .testConfigV3.snmp.port, | ||||||
|  |       "version": (.testConfigV3.snmp.version | tostring), | ||||||
|  |       "securityLevel": .testConfigV3.snmp.securityLevel, | ||||||
|  |       "username": .testConfigV3.snmp.username, | ||||||
|  |       "authProtocol": .testConfigV3.snmp.authProtocol, | ||||||
|  |       "authKey": .testConfigV3.snmp.authKey, | ||||||
|  |       "batteryLowOID": "1.3.6.1.4.1.935.1.1.1.3.3.1.0", | ||||||
|  |       "onBatteryOID": "1.3.6.1.4.1.935.1.1.1.3.3.2.0", | ||||||
|  |       "shutdownCommand": "echo \"Shutdown triggered for test-ups-v3\"" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "groups": [] | ||||||
|  | }' | docker exec -i ${CONTAINER_NAME} tee /etc/nupst/config.json > /dev/null | ||||||
|  |  | ||||||
|  | echo "  ✓ Real UPS config created at /etc/nupst/config.json (from .nogit/env.json)" | ||||||
|  |  | ||||||
|  | echo "→ Enabling NUPST systemd service..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   nupst enable | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Starting NUPST service..." | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   nupst start | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "" | ||||||
|  | echo "================================================" | ||||||
|  | echo "  ✓ NUPST v3 Container Ready" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | echo "Container name: ${CONTAINER_NAME}" | ||||||
|  | echo "NUPST version: v3 (commit ${V3_COMMIT})" | ||||||
|  | echo "" | ||||||
|  | echo "Useful commands:" | ||||||
|  | echo "  docker exec -it ${CONTAINER_NAME} bash" | ||||||
|  | echo "  docker exec ${CONTAINER_NAME} systemctl status nupst" | ||||||
|  | echo "  docker exec ${CONTAINER_NAME} nupst --version" | ||||||
|  | echo "  docker stop ${CONTAINER_NAME}" | ||||||
|  | echo "  docker start ${CONTAINER_NAME}" | ||||||
|  | echo "  docker rm -f ${CONTAINER_NAME}" | ||||||
|  | echo "" | ||||||
							
								
								
									
										59
									
								
								test/manualdocker/02-test-v3-to-v4-migration.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										59
									
								
								test/manualdocker/02-test-v3-to-v4-migration.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # | ||||||
|  | # Test migration from v3 to v4 | ||||||
|  | # Run this after 01-setup-v3-container.sh | ||||||
|  | # | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | CONTAINER_NAME="nupst-test-v3" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  NUPST v3 → v4 Migration Test" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | # Check if container exists | ||||||
|  | if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then | ||||||
|  |   echo "❌ Container ${CONTAINER_NAME} is not running" | ||||||
|  |   echo "Run ./01-setup-v3-container.sh first" | ||||||
|  |   exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | echo "→ Checking current NUPST status..." | ||||||
|  | docker exec ${CONTAINER_NAME} systemctl status nupst --no-pager || true | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "→ Checking current version..." | ||||||
|  | docker exec ${CONTAINER_NAME} nupst --version | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "→ Stopping v3 service..." | ||||||
|  | docker exec ${CONTAINER_NAME} systemctl stop nupst | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "→ Running v4 installation from main branch (should auto-detect v3 and migrate)..." | ||||||
|  | echo "   Using: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash" | ||||||
|  | docker exec ${CONTAINER_NAME} bash -c " | ||||||
|  |   curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | bash -s -- -y | ||||||
|  | " | ||||||
|  |  | ||||||
|  | echo "→ Checking service status after migration..." | ||||||
|  | docker exec ${CONTAINER_NAME} systemctl status nupst --no-pager || true | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "→ Checking new version..." | ||||||
|  | docker exec ${CONTAINER_NAME} nupst --version | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "→ Testing service commands..." | ||||||
|  | docker exec ${CONTAINER_NAME} nupst service status || true | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  ✓ Migration Test Complete" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  | echo "Check logs with:" | ||||||
|  | echo "  docker exec ${CONTAINER_NAME} nupst service logs" | ||||||
|  | echo "" | ||||||
							
								
								
									
										28
									
								
								test/manualdocker/03-cleanup.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										28
									
								
								test/manualdocker/03-cleanup.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # | ||||||
|  | # Cleanup test container | ||||||
|  | # | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | CONTAINER_NAME="nupst-test-v3" | ||||||
|  |  | ||||||
|  | echo "================================================" | ||||||
|  | echo "  Cleanup NUPST Test Container" | ||||||
|  | echo "================================================" | ||||||
|  | echo "" | ||||||
|  |  | ||||||
|  | if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then | ||||||
|  |   echo "→ Stopping container..." | ||||||
|  |   docker stop ${CONTAINER_NAME} 2>/dev/null || true | ||||||
|  |  | ||||||
|  |   echo "→ Removing container..." | ||||||
|  |   docker rm ${CONTAINER_NAME} 2>/dev/null || true | ||||||
|  |  | ||||||
|  |   echo "" | ||||||
|  |   echo "✓ Container ${CONTAINER_NAME} removed" | ||||||
|  | else | ||||||
|  |   echo "Container ${CONTAINER_NAME} not found" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | echo "" | ||||||
							
								
								
									
										149
									
								
								test/manualdocker/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								test/manualdocker/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | |||||||
|  | # Manual Docker Testing Scripts | ||||||
|  |  | ||||||
|  | This directory contains scripts for manually testing NUPST installation and migration in Docker containers with systemd support. | ||||||
|  |  | ||||||
|  | ## Prerequisites | ||||||
|  |  | ||||||
|  | - Docker installed and running | ||||||
|  | - Privileged access (for systemd in container) | ||||||
|  | - Linux host (systemd container requirements) | ||||||
|  |  | ||||||
|  | ## Test Scripts | ||||||
|  |  | ||||||
|  | ### 1. `01-setup-v3-container.sh` | ||||||
|  |  | ||||||
|  | Creates a Docker container with systemd and installs NUPST v3. | ||||||
|  |  | ||||||
|  | **What it does:** | ||||||
|  | - Creates Ubuntu 22.04 container with systemd enabled | ||||||
|  | - Installs NUPST v3 from commit `806f81c6` (last v3 version) | ||||||
|  | - Enables and starts the systemd service | ||||||
|  | - Leaves container running for testing | ||||||
|  |  | ||||||
|  | **Usage:** | ||||||
|  | ```bash | ||||||
|  | chmod +x 01-setup-v3-container.sh | ||||||
|  | ./01-setup-v3-container.sh | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Container name:** `nupst-test-v3` | ||||||
|  |  | ||||||
|  | ### 2. `02-test-v3-to-v4-migration.sh` | ||||||
|  |  | ||||||
|  | Tests the migration from v3 to v4. | ||||||
|  |  | ||||||
|  | **What it does:** | ||||||
|  | - Checks current v3 installation | ||||||
|  | - Pulls v4 code from `migration/deno-v4` branch | ||||||
|  | - Runs install.sh (should auto-detect and migrate) | ||||||
|  | - Verifies service is running with v4 | ||||||
|  | - Tests basic commands | ||||||
|  |  | ||||||
|  | **Usage:** | ||||||
|  | ```bash | ||||||
|  | chmod +x 02-test-v3-to-v4-migration.sh | ||||||
|  | ./02-test-v3-to-v4-migration.sh | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | **Prerequisites:** Must run `01-setup-v3-container.sh` first | ||||||
|  |  | ||||||
|  | ### 3. `03-cleanup.sh` | ||||||
|  |  | ||||||
|  | Removes the test container. | ||||||
|  |  | ||||||
|  | **Usage:** | ||||||
|  | ```bash | ||||||
|  | chmod +x 03-cleanup.sh | ||||||
|  | ./03-cleanup.sh | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Manual Testing Workflow | ||||||
|  |  | ||||||
|  | ### Full Migration Test | ||||||
|  |  | ||||||
|  | 1. **Set up v3 environment:** | ||||||
|  |    ```bash | ||||||
|  |    ./01-setup-v3-container.sh | ||||||
|  |    ``` | ||||||
|  |  | ||||||
|  | 2. **Verify v3 is working:** | ||||||
|  |    ```bash | ||||||
|  |    docker exec nupst-test-v3 nupst --version | ||||||
|  |    docker exec nupst-test-v3 systemctl status nupst | ||||||
|  |    ``` | ||||||
|  |  | ||||||
|  | 3. **Test migration to v4:** | ||||||
|  |    ```bash | ||||||
|  |    ./02-test-v3-to-v4-migration.sh | ||||||
|  |    ``` | ||||||
|  |  | ||||||
|  | 4. **Manual verification:** | ||||||
|  |    ```bash | ||||||
|  |    # Enter container | ||||||
|  |    docker exec -it nupst-test-v3 bash | ||||||
|  |  | ||||||
|  |    # Inside container: | ||||||
|  |    nupst --version                    # Should show v4.0.0 | ||||||
|  |    nupst service status               # Should show running service | ||||||
|  |    cat /etc/nupst/config.json         # Config should be preserved | ||||||
|  |    systemctl status nupst             # Service should be active | ||||||
|  |    ``` | ||||||
|  |  | ||||||
|  | 5. **Cleanup:** | ||||||
|  |    ```bash | ||||||
|  |    ./03-cleanup.sh | ||||||
|  |    ``` | ||||||
|  |  | ||||||
|  | ## Useful Docker Commands | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Enter container shell | ||||||
|  | docker exec -it nupst-test-v3 bash | ||||||
|  |  | ||||||
|  | # Check service status | ||||||
|  | docker exec nupst-test-v3 systemctl status nupst | ||||||
|  |  | ||||||
|  | # View service logs | ||||||
|  | docker exec nupst-test-v3 journalctl -u nupst -n 50 | ||||||
|  |  | ||||||
|  | # Check NUPST version | ||||||
|  | docker exec nupst-test-v3 nupst --version | ||||||
|  |  | ||||||
|  | # Run NUPST commands | ||||||
|  | docker exec nupst-test-v3 nupst service status | ||||||
|  | docker exec nupst-test-v3 nupst ups list | ||||||
|  |  | ||||||
|  | # Stop container | ||||||
|  | docker stop nupst-test-v3 | ||||||
|  |  | ||||||
|  | # Start container | ||||||
|  | docker start nupst-test-v3 | ||||||
|  |  | ||||||
|  | # Remove container | ||||||
|  | docker rm -f nupst-test-v3 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Notes | ||||||
|  |  | ||||||
|  | - The container runs with `--privileged` flag for systemd support | ||||||
|  | - Container uses Ubuntu 22.04 as base image | ||||||
|  | - v3 installation is from commit `806f81c6a057a2a5da586b96a231d391f12eb1bb` | ||||||
|  | - v4 migration pulls from `migration/deno-v4` branch | ||||||
|  | - All scripts are designed to be idempotent where possible | ||||||
|  |  | ||||||
|  | ## Troubleshooting | ||||||
|  |  | ||||||
|  | ### Container won't start | ||||||
|  | - Ensure Docker daemon is running | ||||||
|  | - Check you have privileged access | ||||||
|  | - Try: `docker logs nupst-test-v3` | ||||||
|  |  | ||||||
|  | ### Systemd not working in container | ||||||
|  | - Requires Linux host (not macOS/Windows) | ||||||
|  | - Needs `--privileged` and cgroup volume mounts | ||||||
|  | - Check: `docker exec nupst-test-v3 systemctl --version` | ||||||
|  |  | ||||||
|  | ### Migration fails | ||||||
|  | - Check logs: `docker exec nupst-test-v3 journalctl -xe` | ||||||
|  | - Verify install.sh ran: `docker exec nupst-test-v3 ls -la /opt/nupst/` | ||||||
|  | - Check service: `docker exec nupst-test-v3 systemctl status nupst` | ||||||
| @@ -1,14 +1,14 @@ | |||||||
| import { tap, expect } from '@push.rocks/tapbundle'; | import { assert, assertEquals } from 'jsr:@std/assert@^1.0.0'; | ||||||
| import { Logger } from '../ts/logger.js'; | import { Logger } from '../ts/logger.ts'; | ||||||
|  |  | ||||||
| // Create a Logger instance for testing | // Create a Logger instance for testing | ||||||
| const logger = new Logger(); | const logger = new Logger(); | ||||||
|  |  | ||||||
| tap.test('should create a logger instance', async () => { | Deno.test('should create a logger instance', () => { | ||||||
|   expect(logger instanceof Logger).toBeTruthy(); |   assert(logger instanceof Logger); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should log messages with different log levels', async () => { | Deno.test('should log messages with different log levels', () => { | ||||||
|   // We're not testing console output directly, just ensuring no errors |   // We're not testing console output directly, just ensuring no errors | ||||||
|   logger.log('Regular log message'); |   logger.log('Regular log message'); | ||||||
|   logger.error('Error message'); |   logger.error('Error message'); | ||||||
| @@ -16,20 +16,20 @@ tap.test('should log messages with different log levels', async () => { | |||||||
|   logger.success('Success message'); |   logger.success('Success message'); | ||||||
|  |  | ||||||
|   // Just assert that the test runs without errors |   // Just assert that the test runs without errors | ||||||
|   expect(true).toBeTruthy(); |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should create a logbox with title, content, and end', async () => { | Deno.test('should create a logbox with title, content, and end', () => { | ||||||
|   // Just ensuring no errors occur |   // Just ensuring no errors occur | ||||||
|   logger.logBoxTitle('Test Box', 40); |   logger.logBoxTitle('Test Box', 40); | ||||||
|   logger.logBoxLine('This is a test line'); |   logger.logBoxLine('This is a test line'); | ||||||
|   logger.logBoxEnd(); |   logger.logBoxEnd(); | ||||||
|  |  | ||||||
|   // Just assert that the test runs without errors |   // Just assert that the test runs without errors | ||||||
|   expect(true).toBeTruthy(); |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should handle width persistence between logbox calls', async () => { | Deno.test('should handle width persistence between logbox calls', () => { | ||||||
|   logger.logBoxTitle('Width Test', 45); |   logger.logBoxTitle('Width Test', 45); | ||||||
|  |  | ||||||
|   // These should use the width from the title |   // These should use the width from the title | ||||||
| @@ -44,68 +44,68 @@ tap.test('should handle width persistence between logbox calls', async () => { | |||||||
|     logger.logBoxTitle('New Box', 30); |     logger.logBoxTitle('New Box', 30); | ||||||
|     logger.logBoxLine('New line'); |     logger.logBoxLine('New line'); | ||||||
|     logger.logBoxEnd(); |     logger.logBoxEnd(); | ||||||
|   } catch (error) { |   } catch (_error) { | ||||||
|     errorThrown = true; |     errorThrown = true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   expect(errorThrown).toBeFalsy(); |   assertEquals(errorThrown, false); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should use default width when no width is specified', async () => { | Deno.test('should use default width when no width is specified', () => { | ||||||
|   // This should automatically use the default width instead of throwing |   // This should automatically use the default width instead of throwing | ||||||
|   let errorThrown = false; |   let errorThrown = false; | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|     logger.logBoxLine('This should use default width'); |     logger.logBoxLine('This should use default width'); | ||||||
|     logger.logBoxEnd(); |     logger.logBoxEnd(); | ||||||
|   } catch (error) { |   } catch (_error) { | ||||||
|     errorThrown = true; |     errorThrown = true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Verify no error was thrown |   // Verify no error was thrown | ||||||
|   expect(errorThrown).toBeFalsy(); |   assertEquals(errorThrown, false); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should create a complete logbox in one call', async () => { | Deno.test('should create a complete logbox in one call', () => { | ||||||
|   // Just ensuring no errors occur |   // Just ensuring no errors occur | ||||||
|   logger.logBox('Complete Box', [ |   logger.logBox('Complete Box', [ | ||||||
|     'Line 1', |     'Line 1', | ||||||
|     'Line 2', |     'Line 2', | ||||||
|     'Line 3' |     'Line 3', | ||||||
|   ], 40); |   ], 40); | ||||||
|  |  | ||||||
|   // Just assert that the test runs without errors |   // Just assert that the test runs without errors | ||||||
|   expect(true).toBeTruthy(); |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should handle content that exceeds box width', async () => { | Deno.test('should handle content that exceeds box width', () => { | ||||||
|   // Just ensuring no errors occur when content is too long |   // Just ensuring no errors occur when content is too long | ||||||
|   logger.logBox('Truncation Test', [ |   logger.logBox('Truncation Test', [ | ||||||
|     'This line is way too long and should be truncated because it exceeds the available space' |     'This line is way too long and should be truncated because it exceeds the available space', | ||||||
|   ], 30); |   ], 30); | ||||||
|  |  | ||||||
|   // Just assert that the test runs without errors |   // Just assert that the test runs without errors | ||||||
|   expect(true).toBeTruthy(); |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should create dividers with custom characters', async () => { | Deno.test('should create dividers with custom characters', () => { | ||||||
|   // Just ensuring no errors occur |   // Just ensuring no errors occur | ||||||
|   logger.logDivider(30); |   logger.logDivider(30); | ||||||
|   logger.logDivider(20, '*'); |   logger.logDivider(20, '*'); | ||||||
|  |  | ||||||
|   // Just assert that the test runs without errors |   // Just assert that the test runs without errors | ||||||
|   expect(true).toBeTruthy(); |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should create divider with default width', async () => { | Deno.test('should create divider with default width', () => { | ||||||
|   // This should use the default width |   // This should use the default width | ||||||
|   logger.logDivider(undefined, '-'); |   logger.logDivider(undefined, '-'); | ||||||
|  |  | ||||||
|   // Just assert that the test runs without errors |   // Just assert that the test runs without errors | ||||||
|   expect(true).toBeTruthy(); |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('Logger Demo', async () => { | Deno.test('Logger Demo', () => { | ||||||
|   console.log('\n=== LOGGER DEMO ===\n'); |   console.log('\n=== LOGGER DEMO ===\n'); | ||||||
|  |  | ||||||
|   // Basic logging |   // Basic logging | ||||||
| @@ -126,13 +126,13 @@ tap.test('Logger Demo', async () => { | |||||||
|   logger.logBox('UPS Status', [ |   logger.logBox('UPS Status', [ | ||||||
|     'Power Status: onBattery', |     'Power Status: onBattery', | ||||||
|     'Battery Capacity: 75%', |     'Battery Capacity: 75%', | ||||||
|     'Runtime Remaining: 30 minutes' |     'Runtime Remaining: 30 minutes', | ||||||
|   ], 45); |   ], 45); | ||||||
|  |  | ||||||
|   // Logbox with content that's too long for the width |   // Logbox with content that's too long for the width | ||||||
|   logger.logBox('Truncation Example', [ |   logger.logBox('Truncation Example', [ | ||||||
|     'This line is short enough to fit within the box width', |     'This line is short enough to fit within the box width', | ||||||
|     'This line is way too long and will be truncated because it exceeds the available space for content within the logbox' |     'This line is way too long and will be truncated because it exceeds the available space for content within the logbox', | ||||||
|   ], 40); |   ], 40); | ||||||
|  |  | ||||||
|   // Demonstrating logbox width being remembered |   // Demonstrating logbox width being remembered | ||||||
| @@ -153,8 +153,5 @@ tap.test('Logger Demo', async () => { | |||||||
|   logger.logDivider(30, '*'); |   logger.logDivider(30, '*'); | ||||||
|   logger.logDivider(undefined, '='); |   logger.logDivider(undefined, '='); | ||||||
|  |  | ||||||
|   expect(true).toBeTruthy(); |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Export the default tap object |  | ||||||
| export default tap.start(); |  | ||||||
							
								
								
									
										233
									
								
								test/test.showcase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								test/test.showcase.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | |||||||
|  | /** | ||||||
|  |  * Showcase test for NUPST CLI outputs | ||||||
|  |  * Demonstrates all the beautiful colored output features | ||||||
|  |  * | ||||||
|  |  * Run with: deno run --allow-all test/showcase.ts | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import { logger, type ITableColumn } from '../ts/logger.ts'; | ||||||
|  | import { theme, symbols, getBatteryColor, formatPowerStatus } from '../ts/colors.ts'; | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  | console.log('═'.repeat(80)); | ||||||
|  | logger.highlight('NUPST CLI OUTPUT SHOWCASE'); | ||||||
|  | logger.dim('Demonstrating beautiful, colored terminal output'); | ||||||
|  | console.log('═'.repeat(80)); | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 1. Basic Logging Methods === | ||||||
|  | logger.logBoxTitle('Basic Logging Methods', 60, 'info'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.log('Normal log message (default color)'); | ||||||
|  | logger.success('Success message with ✓ symbol'); | ||||||
|  | logger.error('Error message with ✗ symbol'); | ||||||
|  | logger.warn('Warning message with ⚠ symbol'); | ||||||
|  | logger.info('Info message with ℹ symbol'); | ||||||
|  | logger.dim('Dim/secondary text for less important info'); | ||||||
|  | logger.highlight('Highlighted/bold text for emphasis'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 2. Colored Boxes === | ||||||
|  | logger.logBoxTitle('Colored Box Styles', 60); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine('Boxes can be styled with different colors:'); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | logger.logBox('Success Box (Green)', [ | ||||||
|  |   'Used for successful operations', | ||||||
|  |   'Installation complete, service started, etc.', | ||||||
|  | ], 60, 'success'); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | logger.logBox('Error Box (Red)', [ | ||||||
|  |   'Used for critical errors and failures', | ||||||
|  |   'Configuration errors, connection failures, etc.', | ||||||
|  | ], 60, 'error'); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | logger.logBox('Warning Box (Yellow)', [ | ||||||
|  |   'Used for warnings and deprecations', | ||||||
|  |   'Old command format, missing config, etc.', | ||||||
|  | ], 60, 'warning'); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | logger.logBox('Info Box (Cyan)', [ | ||||||
|  |   'Used for informational messages', | ||||||
|  |   'Version info, update available, etc.', | ||||||
|  | ], 60, 'info'); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 3. Status Symbols === | ||||||
|  | logger.logBoxTitle('Status Symbols', 60, 'info'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`${symbols.running}  Service Running`); | ||||||
|  | logger.logBoxLine(`${symbols.stopped}  Service Stopped`); | ||||||
|  | logger.logBoxLine(`${symbols.starting}  Service Starting`); | ||||||
|  | logger.logBoxLine(`${symbols.unknown}  Status Unknown`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`${symbols.success}  Operation Successful`); | ||||||
|  | logger.logBoxLine(`${symbols.error}  Operation Failed`); | ||||||
|  | logger.logBoxLine(`${symbols.warning}  Warning Condition`); | ||||||
|  | logger.logBoxLine(`${symbols.info}  Information`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 4. Battery Level Colors === | ||||||
|  | logger.logBoxTitle('Battery Level Color Coding', 60, 'info'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine('Battery levels are color-coded:'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`  ${getBatteryColor(85)('85%')} - Good (green, ≥60%)`); | ||||||
|  | logger.logBoxLine(`  ${getBatteryColor(45)('45%')} - Medium (yellow, 30-60%)`); | ||||||
|  | logger.logBoxLine(`  ${getBatteryColor(15)('15%')} - Critical (red, <30%)`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 5. Power Status Formatting === | ||||||
|  | logger.logBoxTitle('Power Status Formatting', 60, 'info'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`Status: ${formatPowerStatus('online')}`); | ||||||
|  | logger.logBoxLine(`Status: ${formatPowerStatus('onBattery')}`); | ||||||
|  | logger.logBoxLine(`Status: ${formatPowerStatus('unknown')}`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 6. Table Formatting === | ||||||
|  | const upsColumns: ITableColumn[] = [ | ||||||
|  |   { header: 'ID', key: 'id' }, | ||||||
|  |   { header: 'Name', key: 'name' }, | ||||||
|  |   { header: 'Host', key: 'host' }, | ||||||
|  |   { header: 'Status', key: 'status', color: (v) => { | ||||||
|  |     if (v.includes('Online')) return theme.success(v); | ||||||
|  |     if (v.includes('Battery')) return theme.warning(v); | ||||||
|  |     return theme.dim(v); | ||||||
|  |   }}, | ||||||
|  |   { header: 'Battery', key: 'battery', align: 'right', color: (v) => { | ||||||
|  |     const pct = parseInt(v); | ||||||
|  |     return getBatteryColor(pct)(v); | ||||||
|  |   }}, | ||||||
|  |   { header: 'Runtime', key: 'runtime', align: 'right' }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const upsData = [ | ||||||
|  |   { | ||||||
|  |     id: 'ups-1', | ||||||
|  |     name: 'Main UPS', | ||||||
|  |     host: '192.168.1.10', | ||||||
|  |     status: 'Online', | ||||||
|  |     battery: '95%', | ||||||
|  |     runtime: '45 min', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'ups-2', | ||||||
|  |     name: 'Backup UPS', | ||||||
|  |     host: '192.168.1.11', | ||||||
|  |     status: 'On Battery', | ||||||
|  |     battery: '42%', | ||||||
|  |     runtime: '12 min', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 'ups-3', | ||||||
|  |     name: 'Critical UPS', | ||||||
|  |     host: '192.168.1.12', | ||||||
|  |     status: 'On Battery', | ||||||
|  |     battery: '18%', | ||||||
|  |     runtime: '5 min', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | logger.logTable(upsColumns, upsData, 'UPS Devices'); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 7. Group Table === | ||||||
|  | const groupColumns: ITableColumn[] = [ | ||||||
|  |   { header: 'ID', key: 'id' }, | ||||||
|  |   { header: 'Name', key: 'name' }, | ||||||
|  |   { header: 'Mode', key: 'mode' }, | ||||||
|  |   { header: 'UPS Count', key: 'count', align: 'right' }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const groupData = [ | ||||||
|  |   { id: 'dc-1', name: 'Data Center 1', mode: 'redundant', count: '3' }, | ||||||
|  |   { id: 'office', name: 'Office Servers', mode: 'nonRedundant', count: '2' }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | logger.logTable(groupColumns, groupData, 'UPS Groups'); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 8. Service Status Example === | ||||||
|  | logger.logBoxTitle('Service Status', 70, 'success'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`Status:   ${symbols.running} ${theme.statusActive('Active (Running)')}`); | ||||||
|  | logger.logBoxLine(`Enabled:  ${symbols.success} ${theme.success('Yes')}`); | ||||||
|  | logger.logBoxLine(`Uptime:   2 days, 5 hours, 23 minutes`); | ||||||
|  | logger.logBoxLine(`PID:      ${theme.dim('12345')}`); | ||||||
|  | logger.logBoxLine(`Memory:   ${theme.dim('45.2 MB')}`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 9. Configuration Example === | ||||||
|  | logger.logBoxTitle('Configuration', 70); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`UPS Devices:      ${theme.highlight('3')}`); | ||||||
|  | logger.logBoxLine(`Groups:           ${theme.highlight('2')}`); | ||||||
|  | logger.logBoxLine(`Check Interval:   ${theme.dim('30 seconds')}`); | ||||||
|  | logger.logBoxLine(`Config File:      ${theme.path('/etc/nupst/config.json')}`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 10. Update Available Example === | ||||||
|  | logger.logBoxTitle('Update Available', 70, 'warning'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`Current Version:  ${theme.dim('4.0.1')}`); | ||||||
|  | logger.logBoxLine(`Latest Version:   ${theme.highlight('4.0.2')}`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`Run ${theme.command('sudo nupst update')} to update`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === 11. Error Example === | ||||||
|  | logger.logBoxTitle('Error Example', 70, 'error'); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`${symbols.error} Failed to connect to UPS at 192.168.1.10`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine('Possible causes:'); | ||||||
|  | logger.logBoxLine(`  ${theme.dim('• UPS is offline or unreachable')}`); | ||||||
|  | logger.logBoxLine(`  ${theme.dim('• Incorrect SNMP community string')}`); | ||||||
|  | logger.logBoxLine(`  ${theme.dim('• Firewall blocking port 161')}`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxLine(`Try: ${theme.command('nupst ups test --debug')}`); | ||||||
|  | logger.logBoxLine(''); | ||||||
|  | logger.logBoxEnd(); | ||||||
|  |  | ||||||
|  | console.log(''); | ||||||
|  |  | ||||||
|  | // === Final Summary === | ||||||
|  | console.log('═'.repeat(80)); | ||||||
|  | logger.success('CLI Output Showcase Complete!'); | ||||||
|  | logger.dim('All color and formatting features demonstrated'); | ||||||
|  | console.log('═'.repeat(80)); | ||||||
|  | console.log(''); | ||||||
							
								
								
									
										42
									
								
								test/test.ts
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								test/test.ts
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| import { tap, expect } from '@push.rocks/tapbundle'; | import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0'; | ||||||
| import { NupstSnmp } from '../ts/snmp/manager.js'; | import { NupstSnmp } from '../ts/snmp/manager.ts'; | ||||||
| import type { ISnmpConfig, IUpsStatus } from '../ts/snmp/types.js'; | import type { ISnmpConfig } from '../ts/snmp/types.ts'; | ||||||
|  |  | ||||||
| import * as qenv from '@push.rocks/qenv'; | import * as qenv from 'npm:@push.rocks/qenv@^6.0.0'; | ||||||
| const testQenv = new qenv.Qenv('./', '.nogit/'); | const testQenv = new qenv.Qenv('./', '.nogit/'); | ||||||
|  |  | ||||||
| // Create an SNMP instance with debug enabled | // Create an SNMP instance with debug enabled | ||||||
| @@ -12,17 +12,18 @@ const snmp = new NupstSnmp(true); | |||||||
| const testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1'); | const testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1'); | ||||||
| const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3'); | const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3'); | ||||||
|  |  | ||||||
| tap.test('should log config', async () => { | Deno.test('should log config', () => { | ||||||
|   console.log(testConfigV1); |   console.log(testConfigV1); | ||||||
|  |   assert(true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Test with real UPS using the configuration from .nogit/env.json | // Test with real UPS using the configuration from .nogit/env.json | ||||||
| tap.test('Real UPS test v1', async () => { | Deno.test('Real UPS test v1', async () => { | ||||||
|   try { |   try { | ||||||
|     console.log('Testing with real UPS configuration...'); |     console.log('Testing with real UPS configuration...'); | ||||||
|  |  | ||||||
|     // Extract the correct SNMP config from the test configuration |     // Extract the correct SNMP config from the test configuration | ||||||
|     const snmpConfig = testConfigV1.snmp; |     const snmpConfig = testConfigV1.snmp as ISnmpConfig; | ||||||
|     console.log('SNMP Config:'); |     console.log('SNMP Config:'); | ||||||
|     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); |     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); | ||||||
|     console.log(`  Version: SNMPv${snmpConfig.version}`); |     console.log(`  Version: SNMPv${snmpConfig.version}`); | ||||||
| @@ -31,7 +32,7 @@ tap.test('Real UPS test v1', 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 | ||||||
| @@ -43,10 +44,10 @@ tap.test('Real UPS test v1', 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 | ||||||
| @@ -54,12 +55,12 @@ tap.test('Real UPS test v1', async () => { | |||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('Real UPS test v3', async () => { | Deno.test('Real UPS test v3', async () => { | ||||||
|   try { |   try { | ||||||
|     console.log('Testing with real UPS configuration...'); |     console.log('Testing with real UPS configuration...'); | ||||||
|  |  | ||||||
|     // Extract the correct SNMP config from the test configuration |     // Extract the correct SNMP config from the test configuration | ||||||
|     const snmpConfig = testConfigV3.snmp; |     const snmpConfig = testConfigV3.snmp as ISnmpConfig; | ||||||
|     console.log('SNMP Config:'); |     console.log('SNMP Config:'); | ||||||
|     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); |     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); | ||||||
|     console.log(`  Version: SNMPv${snmpConfig.version}`); |     console.log(`  Version: SNMPv${snmpConfig.version}`); | ||||||
| @@ -68,7 +69,7 @@ tap.test('Real UPS test v3', 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 | ||||||
| @@ -80,16 +81,13 @@ tap.test('Real UPS test v3', 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 | ||||||
|     console.log('Skipping this test since the UPS might not be available'); |     console.log('Skipping this test since the UPS might not be available'); | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Export the default tap object |  | ||||||
| export default tap.start(); |  | ||||||
| @@ -1,8 +1,10 @@ | |||||||
| /** | /** | ||||||
|  * autocreated commitinfo by @push.rocks/commitinfo |  * commitinfo - reads version from deno.json | ||||||
|  */ |  */ | ||||||
|  | import denoConfig from '../deno.json' with { type: 'json' }; | ||||||
|  |  | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@serve.zone/nupst', |   name: denoConfig.name, | ||||||
|   version: '3.1.0', |   version: denoConfig.version, | ||||||
|   description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices' |   description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)', | ||||||
| } | }; | ||||||
|   | |||||||
							
								
								
									
										170
									
								
								ts/actions/base-action.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								ts/actions/base-action.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | |||||||
|  | /** | ||||||
|  |  * Base classes and interfaces for the NUPST action system | ||||||
|  |  * | ||||||
|  |  * Actions are triggered on: | ||||||
|  |  * 1. Power status changes (online ↔ onBattery) | ||||||
|  |  * 2. Threshold violations (battery/runtime cross below configured thresholds) | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | export type TPowerStatus = 'online' | 'onBattery' | 'unknown'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Context provided to actions when they execute | ||||||
|  |  * Contains all relevant UPS state and trigger information | ||||||
|  |  */ | ||||||
|  | export interface IActionContext { | ||||||
|  |   // UPS identification | ||||||
|  |   /** Unique ID of the UPS */ | ||||||
|  |   upsId: string; | ||||||
|  |   /** Human-readable name of the UPS */ | ||||||
|  |   upsName: string; | ||||||
|  |  | ||||||
|  |   // Current state | ||||||
|  |   /** Current power status */ | ||||||
|  |   powerStatus: TPowerStatus; | ||||||
|  |   /** Current battery capacity percentage (0-100) */ | ||||||
|  |   batteryCapacity: number; | ||||||
|  |   /** Estimated battery runtime in minutes */ | ||||||
|  |   batteryRuntime: number; | ||||||
|  |  | ||||||
|  |   // State tracking | ||||||
|  |   /** Previous power status before this trigger */ | ||||||
|  |   previousPowerStatus: TPowerStatus; | ||||||
|  |  | ||||||
|  |   // Metadata | ||||||
|  |   /** Timestamp when this action was triggered (milliseconds since epoch) */ | ||||||
|  |   timestamp: number; | ||||||
|  |   /** Reason this action was triggered */ | ||||||
|  |   triggerReason: 'powerStatusChange' | 'thresholdViolation'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Action trigger mode - determines when an action executes | ||||||
|  |  */ | ||||||
|  | export type TActionTriggerMode = | ||||||
|  |   | 'onlyPowerChanges' // Only on power status changes (online ↔ onBattery) | ||||||
|  |   | 'onlyThresholds' // Only when action's thresholds are exceeded | ||||||
|  |   | 'powerChangesAndThresholds' // On power changes OR threshold violations | ||||||
|  |   | 'anyChange'; // On every UPS poll/check (every ~30s) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Configuration for an action | ||||||
|  |  */ | ||||||
|  | export interface IActionConfig { | ||||||
|  |   /** Type of action to execute */ | ||||||
|  |   type: 'shutdown' | 'webhook' | 'script'; | ||||||
|  |  | ||||||
|  |   // Trigger configuration | ||||||
|  |   /** | ||||||
|  |    * When should this action be triggered? | ||||||
|  |    * - onlyPowerChanges: Only on power status changes | ||||||
|  |    * - onlyThresholds: Only when thresholds exceeded | ||||||
|  |    * - powerChangesAndThresholds: On both (default) | ||||||
|  |    * - anyChange: On every check | ||||||
|  |    */ | ||||||
|  |   triggerMode?: TActionTriggerMode; | ||||||
|  |  | ||||||
|  |   // Threshold configuration (applies to all action types) | ||||||
|  |   /** Threshold settings for this action */ | ||||||
|  |   thresholds?: { | ||||||
|  |     /** Battery percentage threshold (0-100) */ | ||||||
|  |     battery: number; | ||||||
|  |     /** Runtime threshold in minutes */ | ||||||
|  |     runtime: number; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // Shutdown action configuration | ||||||
|  |   /** Delay before shutdown in minutes (default: 5) */ | ||||||
|  |   shutdownDelay?: number; | ||||||
|  |   /** Only execute shutdown on threshold violation, not power status changes */ | ||||||
|  |   onlyOnThresholdViolation?: boolean; | ||||||
|  |  | ||||||
|  |   // Webhook action configuration | ||||||
|  |   /** URL to call for webhook */ | ||||||
|  |   webhookUrl?: string; | ||||||
|  |   /** HTTP method to use (default: POST) */ | ||||||
|  |   webhookMethod?: 'GET' | 'POST'; | ||||||
|  |   /** Timeout for webhook request in milliseconds (default: 10000) */ | ||||||
|  |   webhookTimeout?: number; | ||||||
|  |   /** Only execute webhook on threshold violation */ | ||||||
|  |   webhookOnlyOnThresholdViolation?: boolean; | ||||||
|  |  | ||||||
|  |   // Script action configuration | ||||||
|  |   /** Path to script relative to /etc/nupst (e.g., "myaction.sh") */ | ||||||
|  |   scriptPath?: string; | ||||||
|  |   /** Timeout for script execution in milliseconds (default: 60000) */ | ||||||
|  |   scriptTimeout?: number; | ||||||
|  |   /** Only execute script on threshold violation */ | ||||||
|  |   scriptOnlyOnThresholdViolation?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Abstract base class for all actions | ||||||
|  |  * Each action type must extend this class and implement execute() | ||||||
|  |  */ | ||||||
|  | export abstract class Action { | ||||||
|  |   /** Type identifier for this action */ | ||||||
|  |   abstract readonly type: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Create a new action with the given configuration | ||||||
|  |    * @param config Action configuration | ||||||
|  |    */ | ||||||
|  |   constructor(protected config: IActionConfig) {} | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute this action with the given context | ||||||
|  |    * @param context Current UPS state and trigger information | ||||||
|  |    */ | ||||||
|  |   abstract execute(context: IActionContext): Promise<void>; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Helper to check if this action should execute based on trigger mode | ||||||
|  |    * @param context Action context with current UPS state | ||||||
|  |    * @returns True if action should execute | ||||||
|  |    */ | ||||||
|  |   protected shouldExecute(context: IActionContext): boolean { | ||||||
|  |     const mode = this.config.triggerMode || 'powerChangesAndThresholds'; // Default | ||||||
|  |  | ||||||
|  |     switch (mode) { | ||||||
|  |       case 'onlyPowerChanges': | ||||||
|  |         // Only execute on power status changes | ||||||
|  |         return context.triggerReason === 'powerStatusChange'; | ||||||
|  |  | ||||||
|  |       case 'onlyThresholds': | ||||||
|  |         // Only execute when this action's thresholds are exceeded | ||||||
|  |         if (!this.config.thresholds) return false; // No thresholds = never execute | ||||||
|  |         return this.areThresholdsExceeded(context.batteryCapacity, context.batteryRuntime); | ||||||
|  |  | ||||||
|  |       case 'powerChangesAndThresholds': | ||||||
|  |         // Execute on power changes OR when thresholds exceeded | ||||||
|  |         if (context.triggerReason === 'powerStatusChange') return true; | ||||||
|  |         if (!this.config.thresholds) return false; | ||||||
|  |         return this.areThresholdsExceeded(context.batteryCapacity, context.batteryRuntime); | ||||||
|  |  | ||||||
|  |       case 'anyChange': | ||||||
|  |         // Execute on every trigger (power change or threshold check) | ||||||
|  |         return true; | ||||||
|  |  | ||||||
|  |       default: | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Check if current battery/runtime exceeds this action's thresholds | ||||||
|  |    * @param batteryCapacity Current battery percentage | ||||||
|  |    * @param batteryRuntime Current runtime in minutes | ||||||
|  |    * @returns True if thresholds are exceeded | ||||||
|  |    */ | ||||||
|  |   protected areThresholdsExceeded(batteryCapacity: number, batteryRuntime: number): boolean { | ||||||
|  |     if (!this.config.thresholds) { | ||||||
|  |       return false; // No thresholds configured | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       batteryCapacity < this.config.thresholds.battery || | ||||||
|  |       batteryRuntime < this.config.thresholds.runtime | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										91
									
								
								ts/actions/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								ts/actions/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | /** | ||||||
|  |  * Action system exports and ActionManager | ||||||
|  |  * | ||||||
|  |  * This module provides the central coordination for the action system. | ||||||
|  |  * The ActionManager is responsible for creating and executing actions. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  | import type { Action, IActionConfig, IActionContext } from './base-action.ts'; | ||||||
|  | import { ShutdownAction } from './shutdown-action.ts'; | ||||||
|  | import { WebhookAction } from './webhook-action.ts'; | ||||||
|  | import { ScriptAction } from './script-action.ts'; | ||||||
|  |  | ||||||
|  | // Re-export types for convenience | ||||||
|  | export type { IActionConfig, IActionContext, TPowerStatus } from './base-action.ts'; | ||||||
|  | export { Action } from './base-action.ts'; | ||||||
|  | export { ShutdownAction } from './shutdown-action.ts'; | ||||||
|  | export { WebhookAction } from './webhook-action.ts'; | ||||||
|  | export { ScriptAction } from './script-action.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ActionManager - Coordinates action creation and execution | ||||||
|  |  * | ||||||
|  |  * Provides factory methods for creating actions from configuration | ||||||
|  |  * and orchestrates action execution with error handling. | ||||||
|  |  */ | ||||||
|  | export class ActionManager { | ||||||
|  |   /** | ||||||
|  |    * Create an action instance from configuration | ||||||
|  |    * @param config Action configuration | ||||||
|  |    * @returns Instantiated action | ||||||
|  |    * @throws Error if action type is unknown | ||||||
|  |    */ | ||||||
|  |   static createAction(config: IActionConfig): Action { | ||||||
|  |     switch (config.type) { | ||||||
|  |       case 'shutdown': | ||||||
|  |         return new ShutdownAction(config); | ||||||
|  |       case 'webhook': | ||||||
|  |         return new WebhookAction(config); | ||||||
|  |       case 'script': | ||||||
|  |         return new ScriptAction(config); | ||||||
|  |       default: | ||||||
|  |         throw new Error(`Unknown action type: ${(config as IActionConfig).type}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute a sequence of actions with the given context | ||||||
|  |    * Each action runs sequentially, and failures are logged but don't stop the chain | ||||||
|  |    * @param actions Array of action configurations to execute | ||||||
|  |    * @param context Action context with UPS state | ||||||
|  |    */ | ||||||
|  |   static async executeActions( | ||||||
|  |     actions: IActionConfig[], | ||||||
|  |     context: IActionContext, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     if (!actions || actions.length === 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.log(''); | ||||||
|  |     logger.logBoxTitle(`Executing ${actions.length} Action(s)`, 60, 'info'); | ||||||
|  |     logger.logBoxLine(`Trigger: ${context.triggerReason}`); | ||||||
|  |     logger.logBoxLine(`UPS: ${context.upsName} (${context.upsId})`); | ||||||
|  |     logger.logBoxLine(`Power: ${context.powerStatus}`); | ||||||
|  |     logger.logBoxLine(`Battery: ${context.batteryCapacity}% / ${context.batteryRuntime} min`); | ||||||
|  |     logger.logBoxEnd(); | ||||||
|  |     logger.log(''); | ||||||
|  |  | ||||||
|  |     for (let i = 0; i < actions.length; i++) { | ||||||
|  |       const actionConfig = actions[i]; | ||||||
|  |       try { | ||||||
|  |         logger.info(`[${i + 1}/${actions.length}] ${actionConfig.type} action...`); | ||||||
|  |  | ||||||
|  |         const action = this.createAction(actionConfig); | ||||||
|  |         await action.execute(context); | ||||||
|  |       } catch (error) { | ||||||
|  |         logger.error( | ||||||
|  |           `Action ${actionConfig.type} failed: ${ | ||||||
|  |             error instanceof Error ? error.message : String(error) | ||||||
|  |           }`, | ||||||
|  |         ); | ||||||
|  |         // Continue with next action despite failure | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.log(''); | ||||||
|  |     logger.success('Action execution completed'); | ||||||
|  |     logger.log(''); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										167
									
								
								ts/actions/script-action.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								ts/actions/script-action.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | |||||||
|  | import * as path from 'node:path'; | ||||||
|  | import * as fs from 'node:fs'; | ||||||
|  | import process from 'node:process'; | ||||||
|  | import { exec } from 'node:child_process'; | ||||||
|  | import { promisify } from 'node:util'; | ||||||
|  | import { Action, type IActionConfig, type IActionContext } from './base-action.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | const execAsync = promisify(exec); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ScriptAction - Executes a custom shell script from /etc/nupst/ | ||||||
|  |  * | ||||||
|  |  * Runs user-provided scripts with UPS state passed as environment variables and arguments. | ||||||
|  |  * Scripts must be .sh files located in /etc/nupst/ for security. | ||||||
|  |  */ | ||||||
|  | export class ScriptAction extends Action { | ||||||
|  |   readonly type = 'script'; | ||||||
|  |  | ||||||
|  |   private static readonly SCRIPT_DIR = '/etc/nupst'; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute the script action | ||||||
|  |    * @param context Action context with UPS state | ||||||
|  |    */ | ||||||
|  |   async execute(context: IActionContext): Promise<void> { | ||||||
|  |     // Check if we should execute based on trigger mode | ||||||
|  |     if (!this.shouldExecute(context)) { | ||||||
|  |       logger.info(`Script action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!this.config.scriptPath) { | ||||||
|  |       logger.error('Script path not configured'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Validate and build script path | ||||||
|  |     const scriptPath = this.validateAndBuildScriptPath(this.config.scriptPath); | ||||||
|  |     if (!scriptPath) { | ||||||
|  |       logger.error(`Invalid script path: ${this.config.scriptPath}`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Check if script exists and is executable | ||||||
|  |     if (!fs.existsSync(scriptPath)) { | ||||||
|  |       logger.error(`Script not found: ${scriptPath}`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const timeout = this.config.scriptTimeout || 60000; // Default 60 seconds | ||||||
|  |  | ||||||
|  |     logger.info(`Executing script: ${scriptPath}`); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       await this.executeScript(scriptPath, context, timeout); | ||||||
|  |       logger.success('Script executed successfully'); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Script execution failed: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       // Don't throw - script failures shouldn't stop other actions | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Validate script path and build full path | ||||||
|  |    * Ensures security by preventing path traversal and limiting to /etc/nupst | ||||||
|  |    * @param scriptPath Relative script path from config | ||||||
|  |    * @returns Full validated path or null if invalid | ||||||
|  |    */ | ||||||
|  |   private validateAndBuildScriptPath(scriptPath: string): string | null { | ||||||
|  |     // Remove any leading/trailing whitespace | ||||||
|  |     scriptPath = scriptPath.trim(); | ||||||
|  |  | ||||||
|  |     // Reject paths with path traversal attempts | ||||||
|  |     if (scriptPath.includes('..') || scriptPath.includes('/') || scriptPath.includes('\\')) { | ||||||
|  |       logger.error('Script path must not contain directory separators or parent references'); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Require .sh extension | ||||||
|  |     if (!scriptPath.endsWith('.sh')) { | ||||||
|  |       logger.error('Script must have .sh extension'); | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Build full path | ||||||
|  |     return path.join(ScriptAction.SCRIPT_DIR, scriptPath); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute the script with UPS state as environment variables and arguments | ||||||
|  |    * @param scriptPath Full path to the script | ||||||
|  |    * @param context Action context | ||||||
|  |    * @param timeout Execution timeout in milliseconds | ||||||
|  |    */ | ||||||
|  |   private async executeScript( | ||||||
|  |     scriptPath: string, | ||||||
|  |     context: IActionContext, | ||||||
|  |     timeout: number, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     // Prepare environment variables | ||||||
|  |     const env = { | ||||||
|  |       ...process.env, | ||||||
|  |       NUPST_UPS_ID: context.upsId, | ||||||
|  |       NUPST_UPS_NAME: context.upsName, | ||||||
|  |       NUPST_POWER_STATUS: context.powerStatus, | ||||||
|  |       NUPST_BATTERY_CAPACITY: String(context.batteryCapacity), | ||||||
|  |       NUPST_BATTERY_RUNTIME: String(context.batteryRuntime), | ||||||
|  |       NUPST_TRIGGER_REASON: context.triggerReason, | ||||||
|  |       NUPST_TIMESTAMP: String(context.timestamp), | ||||||
|  |       // Include action's own thresholds if configured | ||||||
|  |       NUPST_BATTERY_THRESHOLD: this.config.thresholds ? String(this.config.thresholds.battery) : '', | ||||||
|  |       NUPST_RUNTIME_THRESHOLD: this.config.thresholds ? String(this.config.thresholds.runtime) : '', | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Build command with arguments | ||||||
|  |     // Arguments: powerStatus batteryCapacity batteryRuntime | ||||||
|  |     const args = [ | ||||||
|  |       context.powerStatus, | ||||||
|  |       String(context.batteryCapacity), | ||||||
|  |       String(context.batteryRuntime), | ||||||
|  |     ].join(' '); | ||||||
|  |  | ||||||
|  |     const command = `bash "${scriptPath}" ${args}`; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const { stdout, stderr } = await execAsync(command, { | ||||||
|  |         env, | ||||||
|  |         cwd: ScriptAction.SCRIPT_DIR, | ||||||
|  |         timeout, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Log output | ||||||
|  |       if (stdout) { | ||||||
|  |         logger.log('Script stdout:'); | ||||||
|  |         logger.dim(stdout.trim()); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (stderr) { | ||||||
|  |         logger.warn('Script stderr:'); | ||||||
|  |         logger.dim(stderr.trim()); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       // Check if it was a timeout | ||||||
|  |       if (error instanceof Error && 'killed' in error && error.killed) { | ||||||
|  |         throw new Error(`Script timed out after ${timeout}ms`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Include stdout/stderr in error if available | ||||||
|  |       if (error && typeof error === 'object' && 'stdout' in error && 'stderr' in error) { | ||||||
|  |         const execError = error as { stdout: string; stderr: string }; | ||||||
|  |         if (execError.stdout) { | ||||||
|  |           logger.log('Script stdout:'); | ||||||
|  |           logger.dim(execError.stdout.trim()); | ||||||
|  |         } | ||||||
|  |         if (execError.stderr) { | ||||||
|  |           logger.warn('Script stderr:'); | ||||||
|  |           logger.dim(execError.stderr.trim()); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       throw error; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										142
									
								
								ts/actions/shutdown-action.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								ts/actions/shutdown-action.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | |||||||
|  | import * as fs from 'node:fs'; | ||||||
|  | import { execFile } from 'node:child_process'; | ||||||
|  | import { promisify } from 'node:util'; | ||||||
|  | import { Action, type IActionConfig, type IActionContext } from './base-action.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | const execFileAsync = promisify(execFile); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * ShutdownAction - Initiates system shutdown | ||||||
|  |  * | ||||||
|  |  * This action triggers a system shutdown using the standard shutdown command. | ||||||
|  |  * It includes a configurable delay to allow VMs and services to gracefully terminate. | ||||||
|  |  */ | ||||||
|  | export class ShutdownAction extends Action { | ||||||
|  |   readonly type = 'shutdown'; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute the shutdown action | ||||||
|  |    * @param context Action context with UPS state | ||||||
|  |    */ | ||||||
|  |   async execute(context: IActionContext): Promise<void> { | ||||||
|  |     // Check if we should execute based on trigger mode and thresholds | ||||||
|  |     if (!this.shouldExecute(context)) { | ||||||
|  |       logger.info(`Shutdown action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const shutdownDelay = this.config.shutdownDelay || 5; // Default 5 minutes | ||||||
|  |  | ||||||
|  |     logger.log(''); | ||||||
|  |     logger.logBoxTitle('Initiating System Shutdown', 60, 'error'); | ||||||
|  |     logger.logBoxLine(`UPS: ${context.upsName} (${context.upsId})`); | ||||||
|  |     logger.logBoxLine(`Power Status: ${context.powerStatus}`); | ||||||
|  |     logger.logBoxLine(`Battery: ${context.batteryCapacity}%`); | ||||||
|  |     logger.logBoxLine(`Runtime: ${context.batteryRuntime} minutes`); | ||||||
|  |     logger.logBoxLine(`Trigger: ${context.triggerReason}`); | ||||||
|  |     logger.logBoxLine(`Shutdown delay: ${shutdownDelay} minutes`); | ||||||
|  |     logger.logBoxEnd(); | ||||||
|  |     logger.log(''); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       await this.executeShutdownCommand(shutdownDelay); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Shutdown command failed: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       // Try alternative methods | ||||||
|  |       await this.tryAlternativeShutdownMethods(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute the primary shutdown command | ||||||
|  |    * @param delayMinutes Minutes to delay before shutdown | ||||||
|  |    */ | ||||||
|  |   private async executeShutdownCommand(delayMinutes: number): Promise<void> { | ||||||
|  |     // Find shutdown command in common system paths | ||||||
|  |     const shutdownPaths = [ | ||||||
|  |       '/sbin/shutdown', | ||||||
|  |       '/usr/sbin/shutdown', | ||||||
|  |       '/bin/shutdown', | ||||||
|  |       '/usr/bin/shutdown', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     let shutdownCmd = ''; | ||||||
|  |     for (const path of shutdownPaths) { | ||||||
|  |       try { | ||||||
|  |         if (fs.existsSync(path)) { | ||||||
|  |           shutdownCmd = path; | ||||||
|  |           logger.log(`Found shutdown command at: ${shutdownCmd}`); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } catch (_e) { | ||||||
|  |         // Continue checking other paths | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (shutdownCmd) { | ||||||
|  |       // Execute shutdown command with delay to allow for VM graceful shutdown | ||||||
|  |       const message = `UPS battery critical, shutting down in ${delayMinutes} minutes`; | ||||||
|  |       logger.log(`Executing: ${shutdownCmd} -h +${delayMinutes} "${message}"`); | ||||||
|  |  | ||||||
|  |       const { stdout } = await execFileAsync(shutdownCmd, [ | ||||||
|  |         '-h', | ||||||
|  |         `+${delayMinutes}`, | ||||||
|  |         message, | ||||||
|  |       ]); | ||||||
|  |  | ||||||
|  |       logger.log(`Shutdown initiated: ${stdout}`); | ||||||
|  |       logger.log(`Allowing ${delayMinutes} minutes for VMs to shut down safely`); | ||||||
|  |     } else { | ||||||
|  |       throw new Error('Shutdown command not found in common paths'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Try alternative shutdown methods if primary command fails | ||||||
|  |    */ | ||||||
|  |   private async tryAlternativeShutdownMethods(): Promise<void> { | ||||||
|  |     logger.error('Trying alternative shutdown methods...'); | ||||||
|  |  | ||||||
|  |     const alternatives = [ | ||||||
|  |       { cmd: 'poweroff', args: ['--force'] }, | ||||||
|  |       { cmd: 'halt', args: ['-p'] }, | ||||||
|  |       { cmd: 'systemctl', args: ['poweroff'] }, | ||||||
|  |       { cmd: 'reboot', args: ['-p'] }, // Some systems allow reboot -p for power off | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     for (const alt of alternatives) { | ||||||
|  |       try { | ||||||
|  |         // First check if command exists in common system paths | ||||||
|  |         const paths = [ | ||||||
|  |           `/sbin/${alt.cmd}`, | ||||||
|  |           `/usr/sbin/${alt.cmd}`, | ||||||
|  |           `/bin/${alt.cmd}`, | ||||||
|  |           `/usr/bin/${alt.cmd}`, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         let cmdPath = ''; | ||||||
|  |         for (const path of paths) { | ||||||
|  |           if (fs.existsSync(path)) { | ||||||
|  |             cmdPath = path; | ||||||
|  |             break; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (cmdPath) { | ||||||
|  |           logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`); | ||||||
|  |           await execFileAsync(cmdPath, alt.args); | ||||||
|  |           logger.log(`Alternative method ${alt.cmd} succeeded`); | ||||||
|  |           return; // Exit if successful | ||||||
|  |         } | ||||||
|  |       } catch (_altError) { | ||||||
|  |         logger.error(`Alternative method ${alt.cmd} failed`); | ||||||
|  |         // Continue to next method | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.error('All shutdown methods failed'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										141
									
								
								ts/actions/webhook-action.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								ts/actions/webhook-action.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | |||||||
|  | import * as http from 'node:http'; | ||||||
|  | import * as https from 'node:https'; | ||||||
|  | import { URL } from 'node:url'; | ||||||
|  | import { Action, type IActionConfig, type IActionContext } from './base-action.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * WebhookAction - Calls an HTTP webhook with UPS state information | ||||||
|  |  * | ||||||
|  |  * Sends UPS status to a configured webhook URL via GET or POST. | ||||||
|  |  * This is useful for remote notifications and integrations with external systems. | ||||||
|  |  */ | ||||||
|  | export class WebhookAction extends Action { | ||||||
|  |   readonly type = 'webhook'; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute the webhook action | ||||||
|  |    * @param context Action context with UPS state | ||||||
|  |    */ | ||||||
|  |   async execute(context: IActionContext): Promise<void> { | ||||||
|  |     // Check if we should execute based on trigger mode | ||||||
|  |     if (!this.shouldExecute(context)) { | ||||||
|  |       logger.info(`Webhook action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!this.config.webhookUrl) { | ||||||
|  |       logger.error('Webhook URL not configured'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const method = this.config.webhookMethod || 'POST'; | ||||||
|  |     const timeout = this.config.webhookTimeout || 10000; | ||||||
|  |  | ||||||
|  |     logger.info(`Calling webhook: ${method} ${this.config.webhookUrl}`); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       await this.callWebhook(context, method, timeout); | ||||||
|  |       logger.success('Webhook call successful'); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Webhook call failed: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       // Don't throw - webhook failures shouldn't stop other actions | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Call the webhook with UPS state data | ||||||
|  |    * @param context Action context | ||||||
|  |    * @param method HTTP method (GET or POST) | ||||||
|  |    * @param timeout Request timeout in milliseconds | ||||||
|  |    */ | ||||||
|  |   private async callWebhook( | ||||||
|  |     context: IActionContext, | ||||||
|  |     method: 'GET' | 'POST', | ||||||
|  |     timeout: number, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     const payload: any = { | ||||||
|  |       upsId: context.upsId, | ||||||
|  |       upsName: context.upsName, | ||||||
|  |       powerStatus: context.powerStatus, | ||||||
|  |       batteryCapacity: context.batteryCapacity, | ||||||
|  |       batteryRuntime: context.batteryRuntime, | ||||||
|  |       triggerReason: context.triggerReason, | ||||||
|  |       timestamp: context.timestamp, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Include action's own thresholds if configured | ||||||
|  |     if (this.config.thresholds) { | ||||||
|  |       payload.thresholds = { | ||||||
|  |         battery: this.config.thresholds.battery, | ||||||
|  |         runtime: this.config.thresholds.runtime, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const url = new URL(this.config.webhookUrl!); | ||||||
|  |  | ||||||
|  |     if (method === 'GET') { | ||||||
|  |       // Append payload as query parameters for GET | ||||||
|  |       url.searchParams.append('upsId', payload.upsId); | ||||||
|  |       url.searchParams.append('upsName', payload.upsName); | ||||||
|  |       url.searchParams.append('powerStatus', payload.powerStatus); | ||||||
|  |       url.searchParams.append('batteryCapacity', String(payload.batteryCapacity)); | ||||||
|  |       url.searchParams.append('batteryRuntime', String(payload.batteryRuntime)); | ||||||
|  |        | ||||||
|  |       url.searchParams.append('triggerReason', payload.triggerReason); | ||||||
|  |       url.searchParams.append('timestamp', String(payload.timestamp)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       const protocol = url.protocol === 'https:' ? https : http; | ||||||
|  |  | ||||||
|  |       const options: http.RequestOptions = { | ||||||
|  |         method, | ||||||
|  |         headers: method === 'POST' | ||||||
|  |           ? { | ||||||
|  |             'Content-Type': 'application/json', | ||||||
|  |             'User-Agent': 'nupst', | ||||||
|  |           } | ||||||
|  |           : { | ||||||
|  |             'User-Agent': 'nupst', | ||||||
|  |           }, | ||||||
|  |         timeout, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const req = protocol.request(url, options, (res) => { | ||||||
|  |         let data = ''; | ||||||
|  |  | ||||||
|  |         res.on('data', (chunk) => { | ||||||
|  |           data += chunk; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         res.on('end', () => { | ||||||
|  |           if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { | ||||||
|  |             logger.dim(`Webhook response (${res.statusCode}): ${data.substring(0, 100)}`); | ||||||
|  |             resolve(); | ||||||
|  |           } else { | ||||||
|  |             reject(new Error(`Webhook returned status ${res.statusCode}`)); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       req.on('error', (error) => { | ||||||
|  |         reject(error); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       req.on('timeout', () => { | ||||||
|  |         req.destroy(); | ||||||
|  |         reject(new Error(`Webhook request timed out after ${timeout}ms`)); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Send POST data if applicable | ||||||
|  |       if (method === 'POST') { | ||||||
|  |         req.write(JSON.stringify(payload)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       req.end(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										661
									
								
								ts/cli.ts
									
									
									
									
									
								
							
							
						
						
									
										661
									
								
								ts/cli.ts
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| import { execSync } from 'child_process'; | import { execSync } from 'node:child_process'; | ||||||
| import { Nupst } from './nupst.js'; | import { Nupst } from './nupst.ts'; | ||||||
| import { logger } from './logger.js'; | import { logger, type ITableColumn } from './logger.ts'; | ||||||
|  | import { theme, symbols } from './colors.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for handling CLI commands |  * Class for handling CLI commands | ||||||
| @@ -21,7 +22,7 @@ export class NupstCli { | |||||||
|    * @param args Command line arguments (process.argv) |    * @param args Command line arguments (process.argv) | ||||||
|    */ |    */ | ||||||
|   public async parseAndExecute(args: string[]): Promise<void> { |   public async parseAndExecute(args: string[]): Promise<void> { | ||||||
|     // Extract debug flag from any position |     // Extract debug and version flags from any position | ||||||
|     const debugOptions = this.extractDebugOptions(args); |     const debugOptions = this.extractDebugOptions(args); | ||||||
|     if (debugOptions.debugMode) { |     if (debugOptions.debugMode) { | ||||||
|       logger.log('Debug mode enabled'); |       logger.log('Debug mode enabled'); | ||||||
| @@ -29,6 +30,12 @@ export class NupstCli { | |||||||
|       this.nupst.getSnmp().enableDebug(); |       this.nupst.getSnmp().enableDebug(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Check for version flag | ||||||
|  |     if (debugOptions.cleanedArgs.includes('--version') || debugOptions.cleanedArgs.includes('-v')) { | ||||||
|  |       this.showVersion(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Get the command (default to help if none provided) |     // Get the command (default to help if none provided) | ||||||
|     const command = debugOptions.cleanedArgs[2] || 'help'; |     const command = debugOptions.cleanedArgs[2] || 'help'; | ||||||
|     const commandArgs = debugOptions.cleanedArgs.slice(3); |     const commandArgs = debugOptions.cleanedArgs.slice(3); | ||||||
| @@ -56,11 +63,93 @@ export class NupstCli { | |||||||
|    * @param commandArgs Additional command arguments |    * @param commandArgs Additional command arguments | ||||||
|    * @param debugMode Whether debug mode is enabled |    * @param debugMode Whether debug mode is enabled | ||||||
|    */ |    */ | ||||||
|   private async executeCommand(command: string, commandArgs: string[], debugMode: boolean): Promise<void> { |   private async executeCommand( | ||||||
|  |     command: string, | ||||||
|  |     commandArgs: string[], | ||||||
|  |     debugMode: boolean, | ||||||
|  |   ): Promise<void> { | ||||||
|     // Get access to the handlers |     // Get access to the handlers | ||||||
|     const upsHandler = this.nupst.getUpsHandler(); |     const upsHandler = this.nupst.getUpsHandler(); | ||||||
|     const groupHandler = this.nupst.getGroupHandler(); |     const groupHandler = this.nupst.getGroupHandler(); | ||||||
|     const serviceHandler = this.nupst.getServiceHandler(); |     const serviceHandler = this.nupst.getServiceHandler(); | ||||||
|  |     const actionHandler = this.nupst.getActionHandler(); | ||||||
|  |  | ||||||
|  |     // Handle service subcommands | ||||||
|  |     if (command === 'service') { | ||||||
|  |       const subcommand = commandArgs[0] || 'status'; | ||||||
|  |  | ||||||
|  |       switch (subcommand) { | ||||||
|  |         case 'enable': | ||||||
|  |           await serviceHandler.enable(); | ||||||
|  |           break; | ||||||
|  |         case 'disable': | ||||||
|  |           await serviceHandler.disable(); | ||||||
|  |           break; | ||||||
|  |         case 'start': | ||||||
|  |           await serviceHandler.start(); | ||||||
|  |           break; | ||||||
|  |         case 'stop': | ||||||
|  |           await serviceHandler.stop(); | ||||||
|  |           break; | ||||||
|  |         case 'restart': | ||||||
|  |           await serviceHandler.stop(); | ||||||
|  |           await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2s | ||||||
|  |           await serviceHandler.start(); | ||||||
|  |           break; | ||||||
|  |         case 'status': | ||||||
|  |           await serviceHandler.status(); | ||||||
|  |           break; | ||||||
|  |         case 'logs': | ||||||
|  |           await serviceHandler.logs(); | ||||||
|  |           break; | ||||||
|  |         case 'start-daemon': | ||||||
|  |           await serviceHandler.daemonStart(debugMode); | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           this.showServiceHelp(); | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Handle UPS subcommands | ||||||
|  |     if (command === 'ups') { | ||||||
|  |       const subcommand = commandArgs[0] || 'list'; | ||||||
|  |       const subcommandArgs = commandArgs.slice(1); | ||||||
|  |  | ||||||
|  |       switch (subcommand) { | ||||||
|  |         case 'add': | ||||||
|  |           await upsHandler.add(); | ||||||
|  |           break; | ||||||
|  |         case 'edit': { | ||||||
|  |           const upsId = subcommandArgs[0]; | ||||||
|  |           await upsHandler.edit(upsId); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         case 'remove': | ||||||
|  |         case 'rm': { | ||||||
|  |           const upsIdToRemove = subcommandArgs[0]; | ||||||
|  |           if (!upsIdToRemove) { | ||||||
|  |             logger.error('UPS ID is required for remove command'); | ||||||
|  |             this.showUpsHelp(); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           await upsHandler.remove(upsIdToRemove); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         case 'list': | ||||||
|  |         case 'ls': // Alias | ||||||
|  |           await upsHandler.list(); | ||||||
|  |           break; | ||||||
|  |         case 'test': | ||||||
|  |           await upsHandler.test(debugMode); | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           this.showUpsHelp(); | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Handle group subcommands |     // Handle group subcommands | ||||||
|     if (command === 'group') { |     if (command === 'group') { | ||||||
| @@ -71,8 +160,7 @@ export class NupstCli { | |||||||
|         case 'add': |         case 'add': | ||||||
|           await groupHandler.add(); |           await groupHandler.add(); | ||||||
|           break; |           break; | ||||||
|            |         case 'edit': { | ||||||
|         case 'edit': |  | ||||||
|           const groupId = subcommandArgs[0]; |           const groupId = subcommandArgs[0]; | ||||||
|           if (!groupId) { |           if (!groupId) { | ||||||
|             logger.error('Group ID is required for edit command'); |             logger.error('Group ID is required for edit command'); | ||||||
| @@ -81,21 +169,22 @@ export class NupstCli { | |||||||
|           } |           } | ||||||
|           await groupHandler.edit(groupId); |           await groupHandler.edit(groupId); | ||||||
|           break; |           break; | ||||||
|            |         } | ||||||
|         case 'delete': |         case 'remove': | ||||||
|           const groupIdToDelete = subcommandArgs[0]; |         case 'rm': { | ||||||
|           if (!groupIdToDelete) { |           const groupIdToRemove = subcommandArgs[0]; | ||||||
|             logger.error('Group ID is required for delete command'); |           if (!groupIdToRemove) { | ||||||
|  |             logger.error('Group ID is required for remove command'); | ||||||
|             this.showGroupHelp(); |             this.showGroupHelp(); | ||||||
|             return; |             return; | ||||||
|           } |           } | ||||||
|           await groupHandler.delete(groupIdToDelete); |           await groupHandler.remove(groupIdToRemove); | ||||||
|           break; |           break; | ||||||
|            |         } | ||||||
|         case 'list': |         case 'list': | ||||||
|  |         case 'ls': // Alias | ||||||
|           await groupHandler.list(); |           await groupHandler.list(); | ||||||
|           break; |           break; | ||||||
|            |  | ||||||
|         default: |         default: | ||||||
|           this.showGroupHelp(); |           this.showGroupHelp(); | ||||||
|           break; |           break; | ||||||
| @@ -103,82 +192,69 @@ export class NupstCli { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Handle main commands |     // Handle action subcommands | ||||||
|     switch (command) { |     if (command === 'action') { | ||||||
|       case 'add': |       const subcommand = commandArgs[0] || 'list'; | ||||||
|         await upsHandler.add(); |       const subcommandArgs = commandArgs.slice(1); | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'edit': |       switch (subcommand) { | ||||||
|         const upsId = commandArgs[0]; |         case 'add': { | ||||||
|         await upsHandler.edit(upsId); |           const upsId = subcommandArgs[0]; | ||||||
|  |           await actionHandler.add(upsId); | ||||||
|           break; |           break; | ||||||
|  |         } | ||||||
|       case 'delete': |         case 'remove': | ||||||
|         const upsIdToDelete = commandArgs[0]; |         case 'rm': { | ||||||
|         if (!upsIdToDelete) { |           const upsId = subcommandArgs[0]; | ||||||
|           logger.error('UPS ID is required for delete command'); |           const actionIndex = subcommandArgs[1]; | ||||||
|           this.showHelp(); |           await actionHandler.remove(upsId, actionIndex); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         case 'list': | ||||||
|  |         case 'ls': { // Alias | ||||||
|  |           const upsId = subcommandArgs[0]; | ||||||
|  |           await actionHandler.list(upsId); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         default: | ||||||
|  |           this.showActionHelp(); | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|         await upsHandler.delete(upsIdToDelete); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'list': |     // Handle config subcommand | ||||||
|         await upsHandler.list(); |     if (command === 'config') { | ||||||
|         break; |       const subcommand = commandArgs[0] || 'show'; | ||||||
|  |  | ||||||
|       case 'setup':  |       switch (subcommand) { | ||||||
|         // Backward compatibility: setup is now an alias for edit with no specific UPS ID |         case 'show': | ||||||
|         await upsHandler.edit(undefined); |         case 'display': | ||||||
|  |           await this.showConfig(); | ||||||
|           break; |           break; | ||||||
|  |         default: | ||||||
|       case 'enable': |           await this.showConfig(); | ||||||
|         await serviceHandler.enable(); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'daemon-start': |  | ||||||
|         await serviceHandler.daemonStart(debugMode); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'logs': |  | ||||||
|         await serviceHandler.logs(); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'stop': |  | ||||||
|         await serviceHandler.stop(); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'start': |  | ||||||
|         await serviceHandler.start(); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'status': |  | ||||||
|         await serviceHandler.status(); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'disable': |  | ||||||
|         await serviceHandler.disable(); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'test': |  | ||||||
|         await upsHandler.test(debugMode); |  | ||||||
|           break; |           break; | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Handle top-level commands | ||||||
|  |     switch (command) { | ||||||
|       case 'update': |       case 'update': | ||||||
|         await serviceHandler.update(); |         await serviceHandler.update(); | ||||||
|         break; |         break; | ||||||
|  |  | ||||||
|       case 'uninstall': |       case 'uninstall': | ||||||
|         await serviceHandler.uninstall(); |         await serviceHandler.uninstall(); | ||||||
|         break; |         break; | ||||||
|  |  | ||||||
|       case 'config': |  | ||||||
|         await this.showConfig(); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case 'help': |       case 'help': | ||||||
|  |       case '--help': | ||||||
|  |       case '-h': | ||||||
|  |         this.showHelp(); | ||||||
|  |         break; | ||||||
|       default: |       default: | ||||||
|  |         logger.error(`Unknown command: ${command}`); | ||||||
|  |         logger.log(''); | ||||||
|         this.showHelp(); |         this.showHelp(); | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
| @@ -192,181 +268,320 @@ export class NupstCli { | |||||||
|       // Try to load configuration |       // Try to load configuration | ||||||
|       try { |       try { | ||||||
|         await this.nupst.getDaemon().loadConfig(); |         await this.nupst.getDaemon().loadConfig(); | ||||||
|       } catch (error) { |       } catch (_error) { | ||||||
|         const errorBoxWidth = 45; |         logger.logBox('Configuration Error', [ | ||||||
|         logger.logBoxTitle('Configuration Error', errorBoxWidth); |           'No configuration found.', | ||||||
|         logger.logBoxLine('No configuration found.'); |           "Please run 'nupst ups add' first to create a configuration.", | ||||||
|         logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); |         ], 50, 'error'); | ||||||
|         logger.logBoxEnd(); |  | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Get current configuration |       // Get current configuration | ||||||
|       const config = this.nupst.getDaemon().getConfig(); |       const config = this.nupst.getDaemon().getConfig(); | ||||||
|  |  | ||||||
|       const boxWidth = 50; |  | ||||||
|       logger.logBoxTitle('NUPST Configuration', boxWidth); |  | ||||||
|  |  | ||||||
|       // Check if multi-UPS config |       // Check if multi-UPS config | ||||||
|       if (config.upsDevices && Array.isArray(config.upsDevices)) { |       if (config.upsDevices && Array.isArray(config.upsDevices)) { | ||||||
|         // Multi-UPS configuration |         // === Multi-UPS Configuration === | ||||||
|         logger.logBoxLine(`UPS Devices: ${config.upsDevices.length}`); |  | ||||||
|         logger.logBoxLine(`Groups: ${config.groups ? config.groups.length : 0}`); |  | ||||||
|         logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`); |  | ||||||
|         logger.logBoxLine(''); |  | ||||||
|         logger.logBoxLine('Configuration File Location:'); |  | ||||||
|         logger.logBoxLine('  /etc/nupst/config.json'); |  | ||||||
|         logger.logBoxEnd(); |  | ||||||
|          |          | ||||||
|         // Show UPS devices |         // Overview Box | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.logBox('NUPST Configuration', [ | ||||||
|  |           `UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`, | ||||||
|  |           `Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`, | ||||||
|  |           `Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`, | ||||||
|  |           '', | ||||||
|  |           theme.dim('Configuration File:'), | ||||||
|  |           `  ${theme.path('/etc/nupst/config.json')}`, | ||||||
|  |         ], 60, 'info'); | ||||||
|  |  | ||||||
|  |         // UPS Devices Table | ||||||
|         if (config.upsDevices.length > 0) { |         if (config.upsDevices.length > 0) { | ||||||
|           logger.logBoxTitle('UPS Devices', boxWidth); |           const upsRows = config.upsDevices.map((ups) => ({ | ||||||
|           for (const ups of config.upsDevices) { |             name: ups.name, | ||||||
|             logger.logBoxLine(`${ups.name} (${ups.id}):`); |             id: theme.dim(ups.id), | ||||||
|             logger.logBoxLine(`  Host: ${ups.snmp.host}:${ups.snmp.port}`); |             host: `${ups.snmp.host}:${ups.snmp.port}`, | ||||||
|             logger.logBoxLine(`  Model: ${ups.snmp.upsModel}`); |             model: ups.snmp.upsModel || 'cyberpower', | ||||||
|             logger.logBoxLine(`  Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`); |             actions: `${(ups.actions || []).length} configured`, | ||||||
|             logger.logBoxLine(`  Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`); |             groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'), | ||||||
|             logger.logBoxLine(''); |           })); | ||||||
|           } |  | ||||||
|           logger.logBoxEnd(); |           const upsColumns: ITableColumn[] = [ | ||||||
|  |             { header: 'Name', key: 'name', align: 'left', color: theme.highlight }, | ||||||
|  |             { header: 'ID', key: 'id', align: 'left' }, | ||||||
|  |             { header: 'Host:Port', key: 'host', align: 'left', color: theme.info }, | ||||||
|  |             { header: 'Model', key: 'model', align: 'left' }, | ||||||
|  |             { header: 'Actions', key: 'actions', align: 'left' }, | ||||||
|  |             { header: 'Groups', key: 'groups', align: 'left' }, | ||||||
|  |           ]; | ||||||
|  |  | ||||||
|  |           logger.log(''); | ||||||
|  |           logger.info(`UPS Devices (${config.upsDevices.length}):`); | ||||||
|  |           logger.log(''); | ||||||
|  |           logger.logTable(upsColumns, upsRows); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Show groups |         // Groups Table | ||||||
|         if (config.groups && config.groups.length > 0) { |         if (config.groups && config.groups.length > 0) { | ||||||
|           logger.logBoxTitle('UPS Groups', boxWidth); |           const groupRows = config.groups.map((group) => { | ||||||
|           for (const group of config.groups) { |             const upsInGroup = config.upsDevices.filter((ups) => | ||||||
|             logger.logBoxLine(`${group.name} (${group.id}):`); |               ups.groups && ups.groups.includes(group.id) | ||||||
|             logger.logBoxLine(`  Mode: ${group.mode}`); |             ); | ||||||
|             if (group.description) { |             return { | ||||||
|               logger.logBoxLine(`  Description: ${group.description}`); |               name: group.name, | ||||||
|             } |               id: theme.dim(group.id), | ||||||
|  |               mode: group.mode, | ||||||
|  |               upsCount: String(upsInGroup.length), | ||||||
|  |               ups: upsInGroup.length > 0  | ||||||
|  |                 ? upsInGroup.map((ups) => ups.name).join(', ')  | ||||||
|  |                 : theme.dim('None'), | ||||||
|  |               description: group.description || theme.dim('—'), | ||||||
|  |             }; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|             // List UPS devices in this group |           const groupColumns: ITableColumn[] = [ | ||||||
|             const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(group.id)); |             { header: 'Name', key: 'name', align: 'left', color: theme.highlight }, | ||||||
|             logger.logBoxLine(`  UPS Devices: ${upsInGroup.length > 0 ?  |             { header: 'ID', key: 'id', align: 'left' }, | ||||||
|               upsInGroup.map(ups => ups.name).join(', ') : 'None'}`); |             { header: 'Mode', key: 'mode', align: 'left', color: theme.info }, | ||||||
|             logger.logBoxLine(''); |             { header: 'UPS', key: 'upsCount', align: 'right' }, | ||||||
|           } |             { header: 'UPS Devices', key: 'ups', align: 'left' }, | ||||||
|           logger.logBoxEnd(); |             { header: 'Description', key: 'description', align: 'left' }, | ||||||
|  |           ]; | ||||||
|  |  | ||||||
|  |           logger.log(''); | ||||||
|  |           logger.info(`UPS Groups (${config.groups.length}):`); | ||||||
|  |           logger.log(''); | ||||||
|  |           logger.logTable(groupColumns, groupRows); | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         // Legacy single UPS configuration |         // === Legacy Single UPS Configuration === | ||||||
|         // SNMP Settings |  | ||||||
|         logger.logBoxLine('SNMP Settings:'); |  | ||||||
|         logger.logBoxLine(`  Host: ${config.snmp.host}`); |  | ||||||
|         logger.logBoxLine(`  Port: ${config.snmp.port}`); |  | ||||||
|         logger.logBoxLine(`  Version: ${config.snmp.version}`); |  | ||||||
|         logger.logBoxLine(`  UPS Model: ${config.snmp.upsModel || 'cyberpower'}`); |  | ||||||
|          |          | ||||||
|         if (config.snmp.version === 1 || config.snmp.version === 2) { |         if (!config.snmp) { | ||||||
|           logger.logBoxLine(`  Community: ${config.snmp.community}`); |           logger.logBox('Configuration Error', [ | ||||||
|         } else if (config.snmp.version === 3) { |             'Error: Legacy configuration missing SNMP settings', | ||||||
|           logger.logBoxLine(`  Security Level: ${config.snmp.securityLevel}`); |           ], 60, 'error'); | ||||||
|           logger.logBoxLine(`  Username: ${config.snmp.username}`); |           return; | ||||||
|  |  | ||||||
|           // Show auth and privacy details based on security level |  | ||||||
|           if ( |  | ||||||
|             config.snmp.securityLevel === 'authNoPriv' || |  | ||||||
|             config.snmp.securityLevel === 'authPriv' |  | ||||||
|           ) { |  | ||||||
|             logger.logBoxLine(`  Auth Protocol: ${config.snmp.authProtocol || 'None'}`); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|           if (config.snmp.securityLevel === 'authPriv') { |         logger.log(''); | ||||||
|             logger.logBoxLine(`  Privacy Protocol: ${config.snmp.privProtocol || 'None'}`); |         logger.logBox('NUPST Configuration (Legacy)', [ | ||||||
|  |           theme.warning('Legacy single-UPS configuration format'), | ||||||
|  |           '', | ||||||
|  |           theme.dim('SNMP Settings:'), | ||||||
|  |           `  Host: ${theme.info(config.snmp.host)}`, | ||||||
|  |           `  Port: ${theme.info(String(config.snmp.port))}`, | ||||||
|  |           `  Version: ${config.snmp.version}`, | ||||||
|  |           `  UPS Model: ${config.snmp.upsModel || 'cyberpower'}`, | ||||||
|  |           ...(config.snmp.version === 1 || config.snmp.version === 2  | ||||||
|  |             ? [`  Community: ${config.snmp.community}`] | ||||||
|  |             : [] | ||||||
|  |           ), | ||||||
|  |           ...(config.snmp.version === 3  | ||||||
|  |             ? [ | ||||||
|  |                 `  Security Level: ${config.snmp.securityLevel}`, | ||||||
|  |                 `  Username: ${config.snmp.username}`, | ||||||
|  |                 ...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv' | ||||||
|  |                   ? [`  Auth Protocol: ${config.snmp.authProtocol || 'None'}`] | ||||||
|  |                   : [] | ||||||
|  |                 ), | ||||||
|  |                 ...(config.snmp.securityLevel === 'authPriv' | ||||||
|  |                   ? [`  Privacy Protocol: ${config.snmp.privProtocol || 'None'}`] | ||||||
|  |                   : [] | ||||||
|  |                 ), | ||||||
|  |                 `  Timeout: ${config.snmp.timeout / 1000} seconds`, | ||||||
|  |               ] | ||||||
|  |             : [] | ||||||
|  |           ), | ||||||
|  |           ...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs | ||||||
|  |             ? [ | ||||||
|  |                 theme.dim('Custom OIDs:'), | ||||||
|  |                 `  Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`, | ||||||
|  |                 `  Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`, | ||||||
|  |                 `  Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`, | ||||||
|  |               ] | ||||||
|  |             : [] | ||||||
|  |           ), | ||||||
|  |           '', | ||||||
|  |            | ||||||
|  |           `  Check Interval: ${config.checkInterval / 1000} seconds`, | ||||||
|  |           '', | ||||||
|  |           theme.dim('Configuration File:'), | ||||||
|  |           `  ${theme.path('/etc/nupst/config.json')}`, | ||||||
|  |           '', | ||||||
|  |           theme.warning('Note: Using legacy single-UPS configuration format.'), | ||||||
|  |           `Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`, | ||||||
|  |         ], 70, 'warning'); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|           // Show timeout value |       // Service Status | ||||||
|           logger.logBoxLine(`  Timeout: ${config.snmp.timeout / 1000} seconds`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Show OIDs if custom model is selected |  | ||||||
|         if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) { |  | ||||||
|           logger.logBoxLine('Custom OIDs:'); |  | ||||||
|           logger.logBoxLine(`  Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`); |  | ||||||
|           logger.logBoxLine( |  | ||||||
|             `  Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}` |  | ||||||
|           ); |  | ||||||
|           logger.logBoxLine(`  Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Thresholds |  | ||||||
|         logger.logBoxLine('Thresholds:'); |  | ||||||
|         logger.logBoxLine(`  Battery: ${config.thresholds.battery}%`); |  | ||||||
|         logger.logBoxLine(`  Runtime: ${config.thresholds.runtime} minutes`); |  | ||||||
|         logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`); |  | ||||||
|  |  | ||||||
|         // Configuration file location |  | ||||||
|         logger.logBoxLine(''); |  | ||||||
|         logger.logBoxLine('Configuration File Location:'); |  | ||||||
|         logger.logBoxLine('  /etc/nupst/config.json'); |  | ||||||
|         logger.logBoxLine(''); |  | ||||||
|         logger.logBoxLine('Note: Using legacy single-UPS configuration format.'); |  | ||||||
|         logger.logBoxLine('Consider using "nupst add" to migrate to multi-UPS format.'); |  | ||||||
|  |  | ||||||
|         logger.logBoxEnd(); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Show service status |  | ||||||
|       try { |       try { | ||||||
|         const isActive = |         const isActive = | ||||||
|           execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; |           execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; | ||||||
|         const isEnabled = |         const isEnabled = | ||||||
|           execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled'; |           execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled'; | ||||||
|  |  | ||||||
|         const statusBoxWidth = 45; |         logger.log(''); | ||||||
|         logger.logBoxTitle('Service Status', statusBoxWidth); |         logger.logBox('Service Status', [ | ||||||
|         logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`); |           `Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`, | ||||||
|         logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`); |           `Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`, | ||||||
|         logger.logBoxEnd(); |         ], 50, isActive ? 'success' : 'default'); | ||||||
|       } catch (error) { |         logger.log(''); | ||||||
|  |       } catch (_error) { | ||||||
|         // Ignore errors checking service status |         // Ignore errors checking service status | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Failed to display configuration: ${error.message}`); |       logger.error( | ||||||
|  |         `Failed to display configuration: ${ | ||||||
|  |           error instanceof Error ? error.message : String(error) | ||||||
|  |         }`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Display version information | ||||||
|  |    */ | ||||||
|  |   private showVersion(): void { | ||||||
|  |     const version = this.nupst.getVersion(); | ||||||
|  |     logger.log(`NUPST version ${version}`); | ||||||
|  |     logger.log('Network UPS Shutdown Tool (https://nupst.serve.zone)'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Display help message |    * Display help message | ||||||
|    */ |    */ | ||||||
|   private showHelp(): void { |   private showHelp(): void { | ||||||
|  |     console.log(''); | ||||||
|  |     logger.highlight('NUPST - UPS Shutdown Tool'); | ||||||
|  |     logger.dim('Deno-powered UPS monitoring and shutdown automation'); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // Usage section | ||||||
|  |     logger.log(theme.info('Usage:')); | ||||||
|  |     logger.log(`  ${theme.command('nupst')} ${theme.dim('<command> [options]')}`); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // Main commands section | ||||||
|  |     logger.log(theme.info('Commands:')); | ||||||
|  |     this.printCommand('service <subcommand>', 'Manage systemd service'); | ||||||
|  |     this.printCommand('ups <subcommand>', 'Manage UPS devices'); | ||||||
|  |     this.printCommand('group <subcommand>', 'Manage UPS groups'); | ||||||
|  |     this.printCommand('action <subcommand>', 'Manage UPS actions'); | ||||||
|  |     this.printCommand('config [show]', 'Display current configuration'); | ||||||
|  |     this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)')); | ||||||
|  |     this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)')); | ||||||
|  |     this.printCommand('help, --help, -h', 'Show this help message'); | ||||||
|  |     this.printCommand('--version, -v', 'Show version information'); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // Service subcommands | ||||||
|  |     logger.log(theme.info('Service Subcommands:')); | ||||||
|  |     this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)')); | ||||||
|  |     this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)')); | ||||||
|  |     this.printCommand('nupst service start', 'Start the systemd service'); | ||||||
|  |     this.printCommand('nupst service stop', 'Stop the systemd service'); | ||||||
|  |     this.printCommand('nupst service restart', 'Restart the systemd service'); | ||||||
|  |     this.printCommand('nupst service status', 'Show service and UPS status'); | ||||||
|  |     this.printCommand('nupst service logs', 'Show service logs in real-time'); | ||||||
|  |     this.printCommand('nupst service start-daemon', 'Start daemon process directly'); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // UPS subcommands | ||||||
|  |     logger.log(theme.info('UPS Subcommands:')); | ||||||
|  |     this.printCommand('nupst ups add', 'Add a new UPS device'); | ||||||
|  |     this.printCommand('nupst ups edit [id]', 'Edit a UPS device (default if no ID)'); | ||||||
|  |     this.printCommand('nupst ups remove <id>', 'Remove a UPS device by ID'); | ||||||
|  |     this.printCommand('nupst ups list (or ls)', 'List all configured UPS devices'); | ||||||
|  |     this.printCommand('nupst ups test', 'Test UPS connections'); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // Group subcommands | ||||||
|  |     logger.log(theme.info('Group Subcommands:')); | ||||||
|  |     this.printCommand('nupst group add', 'Add a new UPS group'); | ||||||
|  |     this.printCommand('nupst group edit <id>', 'Edit an existing UPS group'); | ||||||
|  |     this.printCommand('nupst group remove <id>', 'Remove a UPS group by ID'); | ||||||
|  |     this.printCommand('nupst group list (or ls)', 'List all UPS groups'); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // Action subcommands | ||||||
|  |     logger.log(theme.info('Action Subcommands:')); | ||||||
|  |     this.printCommand('nupst action add <target-id>', 'Add a new action to a UPS or group'); | ||||||
|  |     this.printCommand('nupst action remove <target-id> <index>', 'Remove an action by index'); | ||||||
|  |     this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)'); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // Options | ||||||
|  |     logger.log(theme.info('Options:')); | ||||||
|  |     this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging'); | ||||||
|  |     logger.dim('                     (Example: nupst ups test --debug)'); | ||||||
|  |     console.log(''); | ||||||
|  |  | ||||||
|  |     // Examples | ||||||
|  |     logger.log(theme.info('Examples:')); | ||||||
|  |     logger.dim('  nupst service enable     # Install and start the service'); | ||||||
|  |     logger.dim('  nupst ups add            # Add a new UPS interactively'); | ||||||
|  |     logger.dim('  nupst group list         # Show all configured groups'); | ||||||
|  |     logger.dim('  nupst config             # Display current configuration'); | ||||||
|  |     console.log(''); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Helper to print a command with description | ||||||
|  |    */ | ||||||
|  |   private printCommand(command: string, description: string, extra?: string): void { | ||||||
|  |     const paddedCommand = command.padEnd(30); | ||||||
|  |     logger.log(`  ${theme.command(paddedCommand)} ${description}${extra ? ' ' + extra : ''}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Display help message for service commands | ||||||
|  |    */ | ||||||
|  |   private showServiceHelp(): void { | ||||||
|     logger.log(` |     logger.log(` | ||||||
| NUPST - Node.js UPS Shutdown Tool | NUPST - Service Management Commands | ||||||
|  |  | ||||||
| Usage: | Usage: | ||||||
|   nupst enable         - Install and enable the systemd service (requires root) |   nupst service <subcommand> | ||||||
|   nupst disable        - Stop and uninstall the systemd service (requires root) |  | ||||||
|   nupst daemon-start   - Start the daemon process directly |  | ||||||
|   nupst logs           - Show logs of the systemd service |  | ||||||
|   nupst stop           - Stop the systemd service |  | ||||||
|   nupst start          - Start the systemd service |  | ||||||
|   nupst status         - Show status of the systemd service and UPS status |  | ||||||
|  |  | ||||||
| UPS Management: | Subcommands: | ||||||
|   nupst add            - Add a new UPS device |   enable                   - Install and enable the systemd service (requires root) | ||||||
|   nupst edit [id]      - Edit an existing UPS (default UPS if no ID provided) |   disable                  - Stop and disable the systemd service (requires root) | ||||||
|   nupst delete <id>    - Delete a UPS by ID |   start                    - Start the systemd service | ||||||
|   nupst list           - List all configured UPS devices |   stop                     - Stop the systemd service | ||||||
|   nupst setup          - Alias for 'nupst edit' (backward compatibility) |   restart                  - Restart the systemd service | ||||||
|  |   status                   - Show service status and UPS information | ||||||
|  |   logs                     - Show service logs in real-time | ||||||
|  |   start-daemon             - Start the daemon process directly (for testing) | ||||||
|  |  | ||||||
| Group Management: | Options: | ||||||
|   nupst group list     - List all UPS groups |   --debug, -d             - Enable debug mode for detailed logging | ||||||
|   nupst group add      - Add a new UPS group | `); | ||||||
|   nupst group edit <id> - Edit an existing UPS group |   } | ||||||
|   nupst group delete <id> - Delete a UPS group |  | ||||||
|  |  | ||||||
| System Commands: |   /** | ||||||
|   nupst test           - Test the current configuration by connecting to all UPS devices |    * Display help message for UPS commands | ||||||
|   nupst config         - Display the current configuration |    */ | ||||||
|   nupst update         - Update NUPST from repository and refresh systemd service (requires root) |   private showUpsHelp(): void { | ||||||
|   nupst uninstall      - Completely uninstall NUPST from the system (requires root) |     logger.log(` | ||||||
|   nupst help           - Show this help message | NUPST - UPS Management Commands | ||||||
|  |  | ||||||
|  | Usage: | ||||||
|  |   nupst ups <subcommand> [arguments] | ||||||
|  |  | ||||||
|  | Subcommands: | ||||||
|  |   add                      - Add a new UPS device interactively | ||||||
|  |   edit [id]                - Edit a UPS device (edits default if no ID provided) | ||||||
|  |   remove <id>              - Remove a UPS device by ID (alias: rm) | ||||||
|  |   list                     - List all configured UPS devices (alias: ls) | ||||||
|  |   test                     - Test connections to all configured UPS devices | ||||||
|  |  | ||||||
| Options: | Options: | ||||||
|   --debug, -d             - Enable debug mode for detailed SNMP logging |   --debug, -d             - Enable debug mode for detailed SNMP logging | ||||||
|                         (Example: nupst test --debug) |  | ||||||
|  | Examples: | ||||||
|  |   nupst ups add           - Add a new UPS device | ||||||
|  |   nupst ups edit ups-1    - Edit UPS with ID 'ups-1' | ||||||
|  |   nupst ups remove ups-1  - Remove UPS with ID 'ups-1' | ||||||
|  |   nupst ups test --debug  - Test all UPS connections with debug output | ||||||
| `); | `); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -378,13 +593,45 @@ Options: | |||||||
| NUPST - Group Management Commands | NUPST - Group Management Commands | ||||||
|  |  | ||||||
| Usage: | Usage: | ||||||
|   nupst group list           - List all UPS groups |   nupst group <subcommand> [arguments] | ||||||
|   nupst group add            - Add a new UPS group |  | ||||||
|   nupst group edit <id>      - Edit an existing UPS group | Subcommands: | ||||||
|   nupst group delete <id>    - Delete a UPS group |   add                      - Add a new UPS group interactively | ||||||
|  |   edit <id>                - Edit an existing UPS group | ||||||
|  |   remove <id>              - Remove a UPS group by ID (alias: rm) | ||||||
|  |   list                     - List all UPS groups (alias: ls) | ||||||
|  |  | ||||||
| Options: | Options: | ||||||
|   --debug, -d             - Enable debug mode for detailed logging |   --debug, -d             - Enable debug mode for detailed logging | ||||||
|  |  | ||||||
|  | Examples: | ||||||
|  |   nupst group add         - Create a new group | ||||||
|  |   nupst group edit dc-1   - Edit group with ID 'dc-1' | ||||||
|  |   nupst group remove dc-1 - Remove group with ID 'dc-1' | ||||||
|  | `); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private showActionHelp(): void { | ||||||
|  |     logger.log(` | ||||||
|  | NUPST - Action Management Commands | ||||||
|  |  | ||||||
|  | Usage: | ||||||
|  |   nupst action <subcommand> [arguments] | ||||||
|  |  | ||||||
|  | Subcommands: | ||||||
|  |   add <ups-id|group-id>                   - Add a new action to a UPS or group interactively | ||||||
|  |   remove <ups-id|group-id> <index>        - Remove an action by index (alias: rm) | ||||||
|  |   list [ups-id|group-id]                  - List all actions (optionally for specific target) (alias: ls) | ||||||
|  |  | ||||||
|  | Options: | ||||||
|  |   --debug, -d                             - Enable debug mode for detailed logging | ||||||
|  |  | ||||||
|  | Examples: | ||||||
|  |   nupst action list                       - List actions for all UPS devices and groups | ||||||
|  |   nupst action list default               - List actions for UPS or group with ID 'default' | ||||||
|  |   nupst action add default                - Add a new action to UPS or group 'default' | ||||||
|  |   nupst action remove default 0           - Remove action at index 0 from UPS or group 'default' | ||||||
|  |   nupst action add dc-rack-1              - Add a new action to group 'dc-rack-1' | ||||||
| `); | `); | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										357
									
								
								ts/cli/action-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										357
									
								
								ts/cli/action-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,357 @@ | |||||||
|  | import process from 'node:process'; | ||||||
|  | import { Nupst } from '../nupst.ts'; | ||||||
|  | import { logger, type ITableColumn } from '../logger.ts'; | ||||||
|  | import { theme, symbols } from '../colors.ts'; | ||||||
|  | import type { IActionConfig } from '../actions/base-action.ts'; | ||||||
|  | import type { IUpsConfig, IGroupConfig } from '../daemon.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class for handling action-related CLI commands | ||||||
|  |  * Provides interface for managing UPS actions | ||||||
|  |  */ | ||||||
|  | export class ActionHandler { | ||||||
|  |   private readonly nupst: Nupst; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Create a new action handler | ||||||
|  |    * @param nupst Reference to the main Nupst instance | ||||||
|  |    */ | ||||||
|  |   constructor(nupst: Nupst) { | ||||||
|  |     this.nupst = nupst; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Add a new action to a UPS or group | ||||||
|  |    */ | ||||||
|  |   public async add(targetId?: string): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       if (!targetId) { | ||||||
|  |         logger.error('Target ID is required'); | ||||||
|  |         logger.log( | ||||||
|  |           `  ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id|group-id>')}`, | ||||||
|  |         ); | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log(`  ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`); | ||||||
|  |         logger.log(`  ${theme.dim('List groups:')} ${theme.command('nupst group list')}`); | ||||||
|  |         logger.log(''); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const config = await this.nupst.getDaemon().loadConfig(); | ||||||
|  |  | ||||||
|  |       // Check if it's a UPS | ||||||
|  |       const ups = config.upsDevices.find((u) => u.id === targetId); | ||||||
|  |       // Check if it's a group | ||||||
|  |       const group = config.groups?.find((g) => g.id === targetId); | ||||||
|  |  | ||||||
|  |       if (!ups && !group) { | ||||||
|  |         logger.error(`UPS or Group with ID '${targetId}' not found`); | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log(`  ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); | ||||||
|  |         logger.log(`  ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`); | ||||||
|  |         logger.log(''); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const target = ups || group; | ||||||
|  |       const targetType = ups ? 'UPS' : 'Group'; | ||||||
|  |       const targetName = ups ? ups.name : group!.name; | ||||||
|  |  | ||||||
|  |       const readline = await import('node:readline'); | ||||||
|  |       const rl = readline.createInterface({ | ||||||
|  |         input: process.stdin, | ||||||
|  |         output: process.stdout, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       const prompt = (question: string): Promise<string> => { | ||||||
|  |         return new Promise((resolve) => { | ||||||
|  |           rl.question(question, (answer: string) => { | ||||||
|  |             resolve(answer); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`); | ||||||
|  |         logger.log(''); | ||||||
|  |  | ||||||
|  |         // Action type (currently only shutdown is supported) | ||||||
|  |         const type = 'shutdown'; | ||||||
|  |         logger.log(`  ${theme.dim('Action type:')} ${theme.highlight('shutdown')}`); | ||||||
|  |  | ||||||
|  |         // Battery threshold | ||||||
|  |         const batteryStr = await prompt( | ||||||
|  |           `  ${theme.dim('Battery threshold')} ${theme.dim('(%):')} `, | ||||||
|  |         ); | ||||||
|  |         const battery = parseInt(batteryStr, 10); | ||||||
|  |         if (isNaN(battery) || battery < 0 || battery > 100) { | ||||||
|  |           logger.error('Invalid battery threshold. Must be 0-100.'); | ||||||
|  |           process.exit(1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Runtime threshold | ||||||
|  |         const runtimeStr = await prompt( | ||||||
|  |           `  ${theme.dim('Runtime threshold')} ${theme.dim('(minutes):')} `, | ||||||
|  |         ); | ||||||
|  |         const runtime = parseInt(runtimeStr, 10); | ||||||
|  |         if (isNaN(runtime) || runtime < 0) { | ||||||
|  |           logger.error('Invalid runtime threshold. Must be >= 0.'); | ||||||
|  |           process.exit(1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Trigger mode | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log(`  ${theme.dim('Trigger mode:')}`); | ||||||
|  |         logger.log(`    ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`); | ||||||
|  |         logger.log( | ||||||
|  |           `    ${theme.dim('2)')} onlyThresholds - Trigger only when thresholds are violated`, | ||||||
|  |         ); | ||||||
|  |         logger.log( | ||||||
|  |           `    ${theme.dim('3)')} powerChangesAndThresholds - Trigger on power change AND thresholds`, | ||||||
|  |         ); | ||||||
|  |         logger.log(`    ${theme.dim('4)')} anyChange - Trigger on any status change`); | ||||||
|  |         const triggerChoice = await prompt(`  ${theme.dim('Choice')} ${theme.dim('[2]:')} `); | ||||||
|  |         const triggerModeMap: Record<string, string> = { | ||||||
|  |           '1': 'onlyPowerChanges', | ||||||
|  |           '2': 'onlyThresholds', | ||||||
|  |           '3': 'powerChangesAndThresholds', | ||||||
|  |           '4': 'anyChange', | ||||||
|  |           '': 'onlyThresholds', // Default | ||||||
|  |         }; | ||||||
|  |         const triggerMode = triggerModeMap[triggerChoice] || 'onlyThresholds'; | ||||||
|  |  | ||||||
|  |         // Shutdown delay | ||||||
|  |         const delayStr = await prompt( | ||||||
|  |           `  ${theme.dim('Shutdown delay')} ${theme.dim('(seconds) [5]:')} `, | ||||||
|  |         ); | ||||||
|  |         const shutdownDelay = delayStr ? parseInt(delayStr, 10) : 5; | ||||||
|  |         if (isNaN(shutdownDelay) || shutdownDelay < 0) { | ||||||
|  |           logger.error('Invalid shutdown delay. Must be >= 0.'); | ||||||
|  |           process.exit(1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Create the action | ||||||
|  |         const newAction: IActionConfig = { | ||||||
|  |           type, | ||||||
|  |           thresholds: { | ||||||
|  |             battery, | ||||||
|  |             runtime, | ||||||
|  |           }, | ||||||
|  |           triggerMode: triggerMode as IActionConfig['triggerMode'], | ||||||
|  |           shutdownDelay, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Add to target (UPS or group) | ||||||
|  |         if (!target!.actions) { | ||||||
|  |           target!.actions = []; | ||||||
|  |         } | ||||||
|  |         target!.actions.push(newAction); | ||||||
|  |  | ||||||
|  |         await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |  | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.success(`Action added to ${targetType} ${targetName}`); | ||||||
|  |         logger.log(`  ${theme.dim('Changes saved and will be applied automatically')}`); | ||||||
|  |         logger.log(''); | ||||||
|  |       } finally { | ||||||
|  |         rl.close(); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Failed to add action: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Remove an action from a UPS or group | ||||||
|  |    */ | ||||||
|  |   public async remove(targetId?: string, actionIndexStr?: string): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       if (!targetId || !actionIndexStr) { | ||||||
|  |         logger.error('Target ID and action index are required'); | ||||||
|  |         logger.log( | ||||||
|  |           `  ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id|group-id> <action-index>')}`, | ||||||
|  |         ); | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log(`  ${theme.dim('List actions:')} ${theme.command('nupst action list')}`); | ||||||
|  |         logger.log(''); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const actionIndex = parseInt(actionIndexStr, 10); | ||||||
|  |       if (isNaN(actionIndex) || actionIndex < 0) { | ||||||
|  |         logger.error('Invalid action index. Must be >= 0.'); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const config = await this.nupst.getDaemon().loadConfig(); | ||||||
|  |  | ||||||
|  |       // Check if it's a UPS | ||||||
|  |       const ups = config.upsDevices.find((u) => u.id === targetId); | ||||||
|  |       // Check if it's a group | ||||||
|  |       const group = config.groups?.find((g) => g.id === targetId); | ||||||
|  |  | ||||||
|  |       if (!ups && !group) { | ||||||
|  |         logger.error(`UPS or Group with ID '${targetId}' not found`); | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log(`  ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); | ||||||
|  |         logger.log(`  ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`); | ||||||
|  |         logger.log(''); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const target = ups || group; | ||||||
|  |       const targetType = ups ? 'UPS' : 'Group'; | ||||||
|  |       const targetName = ups ? ups.name : group!.name; | ||||||
|  |  | ||||||
|  |       if (!target!.actions || target!.actions.length === 0) { | ||||||
|  |         logger.error(`No actions configured for ${targetType} '${targetName}'`); | ||||||
|  |         logger.log(''); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (actionIndex >= target!.actions.length) { | ||||||
|  |         logger.error( | ||||||
|  |           `Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`, | ||||||
|  |         ); | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log( | ||||||
|  |           `  ${theme.dim('List actions:')} ${theme.command(`nupst action list ${targetId}`)}`, | ||||||
|  |         ); | ||||||
|  |         logger.log(''); | ||||||
|  |         process.exit(1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const removedAction = target!.actions[actionIndex]; | ||||||
|  |       target!.actions.splice(actionIndex, 1); | ||||||
|  |  | ||||||
|  |       await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |  | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.success(`Action removed from ${targetType} ${targetName}`); | ||||||
|  |       logger.log(`  ${theme.dim('Type:')} ${removedAction.type}`); | ||||||
|  |       if (removedAction.thresholds) { | ||||||
|  |         logger.log( | ||||||
|  |           `  ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       logger.log(`  ${theme.dim('Changes saved and will be applied automatically')}`); | ||||||
|  |       logger.log(''); | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Failed to remove action: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * List all actions for a specific UPS/group or all devices | ||||||
|  |    */ | ||||||
|  |   public async list(targetId?: string): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       const config = await this.nupst.getDaemon().loadConfig(); | ||||||
|  |  | ||||||
|  |       if (targetId) { | ||||||
|  |         // List actions for specific UPS or group | ||||||
|  |         const ups = config.upsDevices.find((u) => u.id === targetId); | ||||||
|  |         const group = config.groups?.find((g) => g.id === targetId); | ||||||
|  |  | ||||||
|  |         if (!ups && !group) { | ||||||
|  |           logger.error(`UPS or Group with ID '${targetId}' not found`); | ||||||
|  |           logger.log(''); | ||||||
|  |           logger.log(`  ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`); | ||||||
|  |           logger.log(`  ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`); | ||||||
|  |           logger.log(''); | ||||||
|  |           process.exit(1); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (ups) { | ||||||
|  |           this.displayTargetActions(ups, 'UPS'); | ||||||
|  |         } else { | ||||||
|  |           this.displayTargetActions(group!, 'Group'); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         // List actions for all UPS devices and groups | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.info('Actions for All UPS Devices and Groups'); | ||||||
|  |         logger.log(''); | ||||||
|  |  | ||||||
|  |         let hasAnyActions = false; | ||||||
|  |  | ||||||
|  |         // Display UPS actions | ||||||
|  |         for (const ups of config.upsDevices) { | ||||||
|  |           if (ups.actions && ups.actions.length > 0) { | ||||||
|  |             hasAnyActions = true; | ||||||
|  |             this.displayTargetActions(ups, 'UPS'); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Display Group actions | ||||||
|  |         for (const group of config.groups || []) { | ||||||
|  |           if (group.actions && group.actions.length > 0) { | ||||||
|  |             hasAnyActions = true; | ||||||
|  |             this.displayTargetActions(group, 'Group'); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!hasAnyActions) { | ||||||
|  |           logger.log(`  ${theme.dim('No actions configured')}`); | ||||||
|  |           logger.log(''); | ||||||
|  |           logger.log( | ||||||
|  |             `  ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`, | ||||||
|  |           ); | ||||||
|  |           logger.log(''); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       logger.error( | ||||||
|  |         `Failed to list actions: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|  |       process.exit(1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Display actions for a single UPS or Group | ||||||
|  |    */ | ||||||
|  |   private displayTargetActions( | ||||||
|  |     target: IUpsConfig | IGroupConfig, | ||||||
|  |     targetType: 'UPS' | 'Group', | ||||||
|  |   ): void { | ||||||
|  |     logger.log( | ||||||
|  |       `${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`, | ||||||
|  |     ); | ||||||
|  |     logger.log(''); | ||||||
|  |  | ||||||
|  |     if (!target.actions || target.actions.length === 0) { | ||||||
|  |       logger.log(`  ${theme.dim('No actions configured')}`); | ||||||
|  |       logger.log(''); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const columns: ITableColumn[] = [ | ||||||
|  |       { header: 'Index', key: 'index', align: 'right' }, | ||||||
|  |       { header: 'Type', key: 'type', align: 'left' }, | ||||||
|  |       { header: 'Battery', key: 'battery', align: 'right' }, | ||||||
|  |       { header: 'Runtime', key: 'runtime', align: 'right' }, | ||||||
|  |       { header: 'Trigger Mode', key: 'triggerMode', align: 'left' }, | ||||||
|  |       { header: 'Delay', key: 'delay', align: 'right' }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     const rows = target.actions.map((action, index) => ({ | ||||||
|  |       index: theme.dim(index.toString()), | ||||||
|  |       type: theme.highlight(action.type), | ||||||
|  |       battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'), | ||||||
|  |       runtime: action.thresholds ? `${action.thresholds.runtime}min` : theme.dim('N/A'), | ||||||
|  |       triggerMode: theme.dim(action.triggerMode || 'onlyThresholds'), | ||||||
|  |       delay: `${action.shutdownDelay || 5}s`, | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     logger.logTable(columns, rows); | ||||||
|  |     logger.log(''); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| import { Nupst } from '../nupst.js'; | import process from 'node:process'; | ||||||
| import { logger } from '../logger.js'; | import { Nupst } from '../nupst.ts'; | ||||||
| import * as helpers from '../helpers/index.js'; | import { logger, type ITableColumn } from '../logger.ts'; | ||||||
| import { type IGroupConfig } from '../daemon.js'; | import { theme } from '../colors.ts'; | ||||||
|  | import * as helpers from '../helpers/index.ts'; | ||||||
|  | import { type IGroupConfig } from '../daemon.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for handling group-related CLI commands |  * Class for handling group-related CLI commands | ||||||
| @@ -27,11 +29,10 @@ export class GroupHandler { | |||||||
|       try { |       try { | ||||||
|         await this.nupst.getDaemon().loadConfig(); |         await this.nupst.getDaemon().loadConfig(); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         const errorBoxWidth = 45; |         logger.logBox('Configuration Error', [ | ||||||
|         logger.logBoxTitle('Configuration Error', errorBoxWidth); |           'No configuration found.', | ||||||
|         logger.logBoxLine('No configuration found.'); |           "Please run 'nupst ups add' first to create a configuration.", | ||||||
|         logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); |         ], 50, 'error'); | ||||||
|         logger.logBoxEnd(); |  | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -40,45 +41,57 @@ export class GroupHandler { | |||||||
|  |  | ||||||
|       // Check if multi-UPS config |       // Check if multi-UPS config | ||||||
|       if (!config.groups || !Array.isArray(config.groups)) { |       if (!config.groups || !Array.isArray(config.groups)) { | ||||||
|         // Legacy or missing groups configuration |         logger.logBox('UPS Groups', [ | ||||||
|         const boxWidth = 45; |           'No groups configured.', | ||||||
|         logger.logBoxTitle('UPS Groups', boxWidth); |           '', | ||||||
|         logger.logBoxLine('No groups configured.'); |           `${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`, | ||||||
|         logger.logBoxLine('Use "nupst group add" to add a UPS group.'); |         ], 50, 'info'); | ||||||
|         logger.logBoxEnd(); |  | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Display group list |       // Display group list with modern table | ||||||
|       const boxWidth = 60; |  | ||||||
|       logger.logBoxTitle('UPS Groups', boxWidth); |  | ||||||
|        |  | ||||||
|       if (config.groups.length === 0) { |       if (config.groups.length === 0) { | ||||||
|         logger.logBoxLine('No UPS groups configured.'); |         logger.logBox('UPS Groups', [ | ||||||
|         logger.logBoxLine('Use "nupst group add" to add a UPS group.'); |           'No UPS groups configured.', | ||||||
|       } else { |           '', | ||||||
|         logger.logBoxLine(`Found ${config.groups.length} group(s)`); |           `${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`, | ||||||
|         logger.logBoxLine(''); |         ], 60, 'info'); | ||||||
|         logger.logBoxLine('ID          | Name                  | Mode         | UPS Devices'); |         return; | ||||||
|         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); |  | ||||||
|  |  | ||||||
|  |       // Prepare table data | ||||||
|  |       const rows = config.groups.map((group) => { | ||||||
|         // Count UPS devices in this group |         // Count UPS devices in this group | ||||||
|           const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id)); |         const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id)); | ||||||
|         const upsCount = upsInGroup.length; |         const upsCount = upsInGroup.length; | ||||||
|           const upsNames = upsInGroup.map(ups => ups.name).join(', '); |         const upsNames = upsInGroup.map((ups) => ups.name).join(', '); | ||||||
|  |  | ||||||
|           logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`); |         return { | ||||||
|         } |           id: group.id, | ||||||
|       } |           name: group.name || '', | ||||||
|  |           mode: group.mode || 'unknown', | ||||||
|  |           count: String(upsCount), | ||||||
|  |           devices: upsCount > 0 ? upsNames : theme.dim('None'), | ||||||
|  |         }; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|       logger.logBoxEnd(); |       const columns: ITableColumn[] = [ | ||||||
|  |         { header: 'ID', key: 'id', align: 'left', color: theme.highlight }, | ||||||
|  |         { header: 'Name', key: 'name', align: 'left' }, | ||||||
|  |         { header: 'Mode', key: 'mode', align: 'left', color: theme.info }, | ||||||
|  |         { header: 'UPS Count', key: 'count', align: 'right' }, | ||||||
|  |         { header: 'UPS Devices', key: 'devices', align: 'left' }, | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.info(`UPS Groups (${config.groups.length}):`); | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.logTable(columns, rows); | ||||||
|  |       logger.log(''); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Failed to list UPS groups: ${error.message}`); |       logger.error( | ||||||
|  |         `Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -88,7 +101,7 @@ export class GroupHandler { | |||||||
|   public async add(): Promise<void> { |   public async add(): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       // Import readline module for user input |       // Import readline module for user input | ||||||
|       const readline = await import('readline'); |       const readline = await import('node:readline'); | ||||||
|  |  | ||||||
|       const rl = readline.createInterface({ |       const rl = readline.createInterface({ | ||||||
|         input: process.stdin, |         input: process.stdin, | ||||||
| @@ -109,7 +122,9 @@ export class GroupHandler { | |||||||
|         try { |         try { | ||||||
|           await this.nupst.getDaemon().loadConfig(); |           await this.nupst.getDaemon().loadConfig(); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|           logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); |           logger.error( | ||||||
|  |             'No configuration found. Please run "nupst setup" first to create a configuration.', | ||||||
|  |           ); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -148,7 +163,7 @@ export class GroupHandler { | |||||||
|           id: groupId, |           id: groupId, | ||||||
|           name: name || `Group-${groupId}`, |           name: name || `Group-${groupId}`, | ||||||
|           mode, |           mode, | ||||||
|           description: description || undefined |           description: description || undefined, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         // Add the group to the configuration |         // Add the group to the configuration | ||||||
| @@ -170,7 +185,9 @@ export class GroupHandler { | |||||||
|  |  | ||||||
|         // Check if there are UPS devices to assign to this group |         // Check if there are UPS devices to assign to this group | ||||||
|         if (config.upsDevices.length > 0) { |         if (config.upsDevices.length > 0) { | ||||||
|           const assignUps = await prompt('Would you like to assign UPS devices to this group now? (y/N): '); |           const assignUps = await prompt( | ||||||
|  |             'Would you like to assign UPS devices to this group now? (y/N): ', | ||||||
|  |           ); | ||||||
|           if (assignUps.toLowerCase() === 'y') { |           if (assignUps.toLowerCase() === 'y') { | ||||||
|             await this.assignUpsToGroup(newGroup.id, config, prompt); |             await this.assignUpsToGroup(newGroup.id, config, prompt); | ||||||
|  |  | ||||||
| @@ -185,9 +202,10 @@ export class GroupHandler { | |||||||
|         logger.log('\nGroup setup complete!'); |         logger.log('\nGroup setup complete!'); | ||||||
|       } finally { |       } finally { | ||||||
|         rl.close(); |         rl.close(); | ||||||
|  |         process.stdin.destroy(); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Add group error: ${error.message}`); |       logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -198,7 +216,7 @@ export class GroupHandler { | |||||||
|   public async edit(groupId: string): Promise<void> { |   public async edit(groupId: string): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       // Import readline module for user input |       // Import readline module for user input | ||||||
|       const readline = await import('readline'); |       const readline = await import('node:readline'); | ||||||
|  |  | ||||||
|       const rl = readline.createInterface({ |       const rl = readline.createInterface({ | ||||||
|         input: process.stdin, |         input: process.stdin, | ||||||
| @@ -219,7 +237,9 @@ export class GroupHandler { | |||||||
|         try { |         try { | ||||||
|           await this.nupst.getDaemon().loadConfig(); |           await this.nupst.getDaemon().loadConfig(); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|           logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); |           logger.error( | ||||||
|  |             'No configuration found. Please run "nupst setup" first to create a configuration.', | ||||||
|  |           ); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -228,12 +248,14 @@ export class GroupHandler { | |||||||
|  |  | ||||||
|         // Check if groups are initialized |         // Check if groups are initialized | ||||||
|         if (!config.groups || !Array.isArray(config.groups)) { |         if (!config.groups || !Array.isArray(config.groups)) { | ||||||
|           logger.error('No groups configured. Please run "nupst group add" first to create a group.'); |           logger.error( | ||||||
|  |             'No groups configured. Please run "nupst group add" first to create a group.', | ||||||
|  |           ); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Find the group to edit |         // Find the group to edit | ||||||
|         const groupIndex = config.groups.findIndex(group => group.id === groupId); |         const groupIndex = config.groups.findIndex((group) => group.id === groupId); | ||||||
|         if (groupIndex === -1) { |         if (groupIndex === -1) { | ||||||
|           logger.error(`Group with ID "${groupId}" not found.`); |           logger.error(`Group with ID "${groupId}" not found.`); | ||||||
|           return; |           return; | ||||||
| @@ -282,7 +304,9 @@ export class GroupHandler { | |||||||
|         logger.logBoxEnd(); |         logger.logBoxEnd(); | ||||||
|  |  | ||||||
|         // Edit UPS assignments if requested |         // Edit UPS assignments if requested | ||||||
|         const editAssignments = await prompt('Would you like to edit UPS assignments for this group? (y/N): '); |         const editAssignments = await prompt( | ||||||
|  |           'Would you like to edit UPS assignments for this group? (y/N): ', | ||||||
|  |         ); | ||||||
|         if (editAssignments.toLowerCase() === 'y') { |         if (editAssignments.toLowerCase() === 'y') { | ||||||
|           await this.assignUpsToGroup(group.id, config, prompt); |           await this.assignUpsToGroup(group.id, config, prompt); | ||||||
|  |  | ||||||
| @@ -296,9 +320,10 @@ export class GroupHandler { | |||||||
|         logger.log('\nGroup edit complete!'); |         logger.log('\nGroup edit complete!'); | ||||||
|       } finally { |       } finally { | ||||||
|         rl.close(); |         rl.close(); | ||||||
|  |         process.stdin.destroy(); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Edit group error: ${error.message}`); |       logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -306,13 +331,15 @@ export class GroupHandler { | |||||||
|    * Delete an existing UPS group |    * Delete an existing UPS group | ||||||
|    * @param groupId ID of the group to delete |    * @param groupId ID of the group to delete | ||||||
|    */ |    */ | ||||||
|   public async delete(groupId: string): Promise<void> { |   public async remove(groupId: string): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       // Try to load configuration |       // Try to load configuration | ||||||
|       try { |       try { | ||||||
|         await this.nupst.getDaemon().loadConfig(); |         await this.nupst.getDaemon().loadConfig(); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         logger.error('No configuration found. Please run "nupst setup" first to create a configuration.'); |         logger.error( | ||||||
|  |           'No configuration found. Please run "nupst setup" first to create a configuration.', | ||||||
|  |         ); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -326,7 +353,7 @@ export class GroupHandler { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Find the group to delete |       // Find the group to delete | ||||||
|       const groupIndex = config.groups.findIndex(group => group.id === groupId); |       const groupIndex = config.groups.findIndex((group) => group.id === groupId); | ||||||
|       if (groupIndex === -1) { |       if (groupIndex === -1) { | ||||||
|         logger.error(`Group with ID "${groupId}" not found.`); |         logger.error(`Group with ID "${groupId}" not found.`); | ||||||
|         return; |         return; | ||||||
| @@ -335,19 +362,23 @@ export class GroupHandler { | |||||||
|       const groupToDelete = config.groups[groupIndex]; |       const groupToDelete = config.groups[groupIndex]; | ||||||
|  |  | ||||||
|       // Get confirmation before deleting |       // Get confirmation before deleting | ||||||
|       const readline = await import('readline'); |       const readline = await import('node:readline'); | ||||||
|       const rl = readline.createInterface({ |       const rl = readline.createInterface({ | ||||||
|         input: process.stdin, |         input: process.stdin, | ||||||
|         output: process.stdout, |         output: process.stdout, | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       const confirm = await new Promise<string>(resolve => { |       const confirm = await new Promise<string>((resolve) => { | ||||||
|         rl.question(`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => { |         rl.question( | ||||||
|  |           `Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, | ||||||
|  |           (answer) => { | ||||||
|             resolve(answer.toLowerCase()); |             resolve(answer.toLowerCase()); | ||||||
|         }); |           }, | ||||||
|  |         ); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       rl.close(); |       rl.close(); | ||||||
|  |       process.stdin.destroy(); | ||||||
|  |  | ||||||
|       if (confirm !== 'y' && confirm !== 'yes') { |       if (confirm !== 'y' && confirm !== 'yes') { | ||||||
|         logger.log('Deletion cancelled.'); |         logger.log('Deletion cancelled.'); | ||||||
| @@ -375,7 +406,9 @@ export class GroupHandler { | |||||||
|       // Check if service is running and restart it if needed |       // Check if service is running and restart it if needed | ||||||
|       this.nupst.getUpsHandler().restartServiceIfRunning(); |       this.nupst.getUpsHandler().restartServiceIfRunning(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Failed to delete group: ${error.message}`); |       logger.error( | ||||||
|  |         `Failed to delete group: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -388,7 +421,7 @@ export class GroupHandler { | |||||||
|   public async assignUpsToGroups( |   public async assignUpsToGroups( | ||||||
|     ups: any, |     ups: any, | ||||||
|     groups: any[], |     groups: any[], | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     // Initialize groups array if it doesn't exist |     // Initialize groups array if it doesn't exist | ||||||
|     if (!ups.groups) { |     if (!ups.groups) { | ||||||
| @@ -399,7 +432,7 @@ export class GroupHandler { | |||||||
|     logger.log('\nCurrent Group Assignments:'); |     logger.log('\nCurrent Group Assignments:'); | ||||||
|     if (ups.groups && ups.groups.length > 0) { |     if (ups.groups && ups.groups.length > 0) { | ||||||
|       for (const groupId of ups.groups) { |       for (const groupId of ups.groups) { | ||||||
|         const group = groups.find(g => g.id === groupId); |         const group = groups.find((g) => g.id === groupId); | ||||||
|         if (group) { |         if (group) { | ||||||
|           logger.log(`- ${group.name} (${group.id})`); |           logger.log(`- ${group.name} (${group.id})`); | ||||||
|         } else { |         } else { | ||||||
| @@ -420,11 +453,15 @@ export class GroupHandler { | |||||||
|     for (let i = 0; i < groups.length; i++) { |     for (let i = 0; i < groups.length; i++) { | ||||||
|       const group = groups[i]; |       const group = groups[i]; | ||||||
|       const assigned = ups.groups && ups.groups.includes(group.id); |       const assigned = ups.groups && ups.groups.includes(group.id); | ||||||
|       logger.log(`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`); |       logger.log( | ||||||
|  |         `${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Prompt for group selection |     // Prompt for group selection | ||||||
|     const selection = await prompt('\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): '); |     const selection = await prompt( | ||||||
|  |       '\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     if (selection.toLowerCase() === 'clear') { |     if (selection.toLowerCase() === 'clear') { | ||||||
|       // Clear all group assignments |       // Clear all group assignments | ||||||
| @@ -439,7 +476,7 @@ export class GroupHandler { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Process selections |     // Process selections | ||||||
|     const selections = selection.split(',').map(s => s.trim()); |     const selections = selection.split(',').map((s) => s.trim()); | ||||||
|  |  | ||||||
|     for (const sel of selections) { |     for (const sel of selections) { | ||||||
|       const index = parseInt(sel, 10) - 1; |       const index = parseInt(sel, 10) - 1; | ||||||
| @@ -478,14 +515,14 @@ export class GroupHandler { | |||||||
|   public async assignUpsToGroup( |   public async assignUpsToGroup( | ||||||
|     groupId: string, |     groupId: string, | ||||||
|     config: any, |     config: any, | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     if (!config.upsDevices || config.upsDevices.length === 0) { |     if (!config.upsDevices || config.upsDevices.length === 0) { | ||||||
|       logger.log('No UPS devices available. Use "nupst add" to add UPS devices.'); |       logger.log('No UPS devices available. Use "nupst add" to add UPS devices.'); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const group = config.groups.find(g => g.id === groupId); |     const group = config.groups.find((g: { id: string }) => g.id === groupId); | ||||||
|     if (!group) { |     if (!group) { | ||||||
|       logger.error(`Group with ID "${groupId}" not found.`); |       logger.error(`Group with ID "${groupId}" not found.`); | ||||||
|       return; |       return; | ||||||
| @@ -493,7 +530,9 @@ export class GroupHandler { | |||||||
|  |  | ||||||
|     // Show current assignments |     // Show current assignments | ||||||
|     logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`); |     logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`); | ||||||
|     const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(groupId)); |     const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) => | ||||||
|  |       ups.groups && ups.groups.includes(groupId) | ||||||
|  |     ); | ||||||
|     if (upsInGroup.length === 0) { |     if (upsInGroup.length === 0) { | ||||||
|       logger.log('- None'); |       logger.log('- None'); | ||||||
|     } else { |     } else { | ||||||
| @@ -511,7 +550,9 @@ export class GroupHandler { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Prompt for UPS selection |     // Prompt for UPS selection | ||||||
|     const selection = await prompt('\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): '); |     const selection = await prompt( | ||||||
|  |       '\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ', | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     if (selection.toLowerCase() === 'clear') { |     if (selection.toLowerCase() === 'clear') { | ||||||
|       // Clear all UPS from this group |       // Clear all UPS from this group | ||||||
| @@ -533,7 +574,7 @@ export class GroupHandler { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Process selections |     // Process selections | ||||||
|     const selections = selection.split(',').map(s => s.trim()); |     const selections = selection.split(',').map((s) => s.trim()); | ||||||
|  |  | ||||||
|     for (const sel of selections) { |     for (const sel of selections) { | ||||||
|       const index = parseInt(sel, 10) - 1; |       const index = parseInt(sel, 10) - 1; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { execSync } from 'child_process'; | import process from 'node:process'; | ||||||
| import { Nupst } from '../nupst.js'; | import { execSync } from 'node:child_process'; | ||||||
| import { logger } from '../logger.js'; | import { Nupst } from '../nupst.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for handling service-related CLI commands |  * Class for handling service-related CLI commands | ||||||
| @@ -128,85 +129,61 @@ export class ServiceHandler { | |||||||
|     try { |     try { | ||||||
|       // Check if running as root |       // Check if running as root | ||||||
|       this.checkRootAccess( |       this.checkRootAccess( | ||||||
|         'This command must be run as root to update NUPST and refresh the systemd service.' |         'This command must be run as root to update NUPST.', | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       const boxWidth = 45; |       console.log(''); | ||||||
|       logger.logBoxTitle('NUPST Update Process', boxWidth); |       logger.info('Checking for updates...'); | ||||||
|       logger.logBoxLine('Updating NUPST from repository...'); |  | ||||||
|  |  | ||||||
|       // Determine the installation directory (assuming it's either /opt/nupst or the current directory) |  | ||||||
|       const { existsSync } = await import('fs'); |  | ||||||
|       let installDir = '/opt/nupst'; |  | ||||||
|  |  | ||||||
|       if (!existsSync(installDir)) { |  | ||||||
|         // If not installed in /opt/nupst, use the current directory |  | ||||||
|         const { dirname } = await import('path'); |  | ||||||
|         installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable |  | ||||||
|         logger.logBoxLine(`Using local installation directory: ${installDir}`); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       try { |       try { | ||||||
|         // 1. Update the repository |         // Get current version | ||||||
|         logger.logBoxLine('Pulling latest changes from git repository...'); |         const currentVersion = this.nupst.getVersion(); | ||||||
|         execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, { |  | ||||||
|           stdio: 'pipe', |         // Fetch latest version from Gitea API | ||||||
|  |         const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest'; | ||||||
|  |         const response = execSync(`curl -sSL ${apiUrl}`).toString(); | ||||||
|  |         const release = JSON.parse(response); | ||||||
|  |         const latestVersion = release.tag_name; // e.g., "v4.0.7" | ||||||
|  |  | ||||||
|  |         // Normalize versions for comparison (ensure both have "v" prefix) | ||||||
|  |         const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`; | ||||||
|  |         const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`; | ||||||
|  |  | ||||||
|  |         logger.dim(`Current version: ${normalizedCurrent}`); | ||||||
|  |         logger.dim(`Latest version:  ${normalizedLatest}`); | ||||||
|  |         console.log(''); | ||||||
|  |  | ||||||
|  |         // Compare normalized versions | ||||||
|  |         if (normalizedCurrent === normalizedLatest) { | ||||||
|  |           logger.success('Already up to date!'); | ||||||
|  |           console.log(''); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         logger.info(`New version available: ${latestVersion}`); | ||||||
|  |         logger.dim('Downloading and installing...'); | ||||||
|  |         console.log(''); | ||||||
|  |  | ||||||
|  |         // Download and run the install script | ||||||
|  |         // This handles everything: download binary, stop service, replace, restart | ||||||
|  |         const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh'; | ||||||
|  |  | ||||||
|  |         execSync(`curl -sSL ${installUrl} | bash`, { | ||||||
|  |           stdio: 'inherit', // Show install script output to user | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         // 2. Run the install.sh script |         console.log(''); | ||||||
|         logger.logBoxLine('Running install.sh to update NUPST...'); |         logger.success(`Updated to ${latestVersion}`); | ||||||
|         execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); |         console.log(''); | ||||||
|  |  | ||||||
|         // 3. Run the setup.sh script with force flag to update Node.js and dependencies |  | ||||||
|         logger.logBoxLine('Running setup.sh to update Node.js and dependencies...'); |  | ||||||
|         execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' }); |  | ||||||
|  |  | ||||||
|         // 4. Refresh the systemd service |  | ||||||
|         logger.logBoxLine('Refreshing systemd service...'); |  | ||||||
|  |  | ||||||
|         // First check if service exists |  | ||||||
|         let serviceExists = false; |  | ||||||
|         try { |  | ||||||
|           const output = execSync('systemctl list-unit-files | grep nupst.service').toString(); |  | ||||||
|           serviceExists = output.includes('nupst.service'); |  | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|           // If grep fails (service not found), serviceExists remains false |         console.log(''); | ||||||
|           serviceExists = false; |         logger.error('Update failed'); | ||||||
|         } |         logger.dim(`${error instanceof Error ? error.message : String(error)}`); | ||||||
|  |         console.log(''); | ||||||
|         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.message}`); |  | ||||||
|         logger.logBoxEnd(); |  | ||||||
|         process.exit(1); |         process.exit(1); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Update failed: ${error.message}`); |       logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|       process.exit(1); |       process.exit(1); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -236,13 +213,15 @@ export class ServiceHandler { | |||||||
|         }); |         }); | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       console.log('\nNUPST Uninstaller'); |       logger.log(''); | ||||||
|       console.log('==============='); |       logger.highlight('NUPST Uninstaller'); | ||||||
|       console.log('This will completely remove NUPST from your system.\n'); |       logger.dim('==============='); | ||||||
|  |       logger.log('This will completely remove NUPST from your system.'); | ||||||
|  |       logger.log(''); | ||||||
|  |  | ||||||
|       // Ask about removing configuration |       // Ask about removing configuration | ||||||
|       const removeConfig = await prompt( |       const removeConfig = await prompt( | ||||||
|         'Do you want to remove the NUPST configuration files? (y/N): ' |         'Do you want to remove the NUPST configuration files? (y/N): ', | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       // Find the uninstall.sh script location |       // Find the uninstall.sh script location | ||||||
| @@ -274,17 +253,20 @@ export class ServiceHandler { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!uninstallScriptPath) { |         if (!uninstallScriptPath) { | ||||||
|           console.error('Could not locate uninstall.sh script. Aborting uninstall.'); |           logger.error('Could not locate uninstall.sh script. Aborting uninstall.'); | ||||||
|           rl.close(); |           rl.close(); | ||||||
|  |           process.stdin.destroy(); | ||||||
|           process.exit(1); |           process.exit(1); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Close readline before executing script |       // Close readline before executing script | ||||||
|       rl.close(); |       rl.close(); | ||||||
|  |       process.stdin.destroy(); | ||||||
|  |  | ||||||
|       // Execute uninstall.sh with the appropriate option |       // Execute uninstall.sh with the appropriate option | ||||||
|       console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`); |       logger.log(''); | ||||||
|  |       logger.log(`Running uninstaller from ${uninstallScriptPath}...`); | ||||||
|  |  | ||||||
|       // Pass the configuration removal option as an environment variable |       // Pass the configuration removal option as an environment variable | ||||||
|       const env = { |       const env = { | ||||||
| @@ -300,7 +282,7 @@ export class ServiceHandler { | |||||||
|         stdio: 'inherit', // Show output in the terminal |         stdio: 'inherit', // Show output in the terminal | ||||||
|       }); |       }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error(`Uninstall failed: ${error.message}`); |       logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|       process.exit(1); |       process.exit(1); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,7 +1,11 @@ | |||||||
| import { execSync } from 'child_process'; | import process from 'node:process'; | ||||||
| import { Nupst } from '../nupst.js'; | import { execSync } from 'node:child_process'; | ||||||
| import { logger } from '../logger.js'; | import { Nupst } from '../nupst.ts'; | ||||||
| import * as helpers from '../helpers/index.js'; | import { logger, type ITableColumn } from '../logger.ts'; | ||||||
|  | import { theme } from '../colors.ts'; | ||||||
|  | import * as helpers from '../helpers/index.ts'; | ||||||
|  | import type { TUpsModel } from '../snmp/types.ts'; | ||||||
|  | import type { INupstConfig } from '../daemon.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for handling UPS-related CLI commands |  * Class for handling UPS-related CLI commands | ||||||
| @@ -24,7 +28,7 @@ export class UpsHandler { | |||||||
|   public async add(): Promise<void> { |   public async add(): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       // Import readline module for user input |       // Import readline module for user input | ||||||
|       const readline = await import('readline'); |       const readline = await import('node:readline'); | ||||||
|  |  | ||||||
|       const rl = readline.createInterface({ |       const rl = readline.createInterface({ | ||||||
|         input: process.stdin, |         input: process.stdin, | ||||||
| @@ -44,9 +48,10 @@ export class UpsHandler { | |||||||
|         await this.runAddProcess(prompt); |         await this.runAddProcess(prompt); | ||||||
|       } finally { |       } finally { | ||||||
|         rl.close(); |         rl.close(); | ||||||
|  |         process.stdin.destroy(); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Add UPS error: ${error.message}`); |       logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -74,10 +79,10 @@ export class UpsHandler { | |||||||
|             id: 'default', |             id: 'default', | ||||||
|         name: 'Default UPS', |         name: 'Default UPS', | ||||||
|         snmp: config.snmp, |         snmp: config.snmp, | ||||||
|             thresholds: config.thresholds, |         groups: [], | ||||||
|             groups: [] |         actions: [], | ||||||
|           }], |           }], | ||||||
|           groups: [] |           groups: [], | ||||||
|         }; |         }; | ||||||
|         logger.log('Converting existing configuration to multi-UPS format.'); |         logger.log('Converting existing configuration to multi-UPS format.'); | ||||||
|       } |       } | ||||||
| @@ -86,7 +91,7 @@ export class UpsHandler { | |||||||
|       config = { |       config = { | ||||||
|         checkInterval: 30000, // Default check interval |         checkInterval: 30000, // Default check interval | ||||||
|         upsDevices: [], |         upsDevices: [], | ||||||
|         groups: [] |         groups: [], | ||||||
|       }; |       }; | ||||||
|       logger.log('No existing configuration found. Creating a new configuration.'); |       logger.log('No existing configuration found. Creating a new configuration.'); | ||||||
|     } |     } | ||||||
| @@ -105,21 +110,19 @@ export class UpsHandler { | |||||||
|         community: 'public', |         community: 'public', | ||||||
|         version: 1, |         version: 1, | ||||||
|         timeout: 5000, |         timeout: 5000, | ||||||
|         upsModel: 'cyberpower' |         upsModel: 'cyberpower' as TUpsModel, | ||||||
|       }, |       }, | ||||||
|       thresholds: { |       thresholds: { | ||||||
|         battery: 60, |         battery: 60, | ||||||
|         runtime: 20 |         runtime: 20, | ||||||
|       }, |       }, | ||||||
|       groups: [] |       groups: [], | ||||||
|  |       actions: [], | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     // Gather SNMP settings |     // Gather SNMP settings | ||||||
|     await this.gatherSnmpSettings(newUps.snmp, prompt); |     await this.gatherSnmpSettings(newUps.snmp, prompt); | ||||||
|  |  | ||||||
|     // Gather threshold settings |  | ||||||
|     await this.gatherThresholdSettings(newUps.thresholds, prompt); |  | ||||||
|  |  | ||||||
|     // Gather UPS model settings |     // Gather UPS model settings | ||||||
|     await this.gatherUpsModelSettings(newUps.snmp, prompt); |     await this.gatherUpsModelSettings(newUps.snmp, prompt); | ||||||
|  |  | ||||||
| @@ -131,11 +134,14 @@ export class UpsHandler { | |||||||
|       await groupHandler.assignUpsToGroups(newUps, config.groups, prompt); |       await groupHandler.assignUpsToGroups(newUps, config.groups, prompt); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | // Gather action settings | ||||||
|  |     await this.gatherActionSettings(newUps.actions, prompt); | ||||||
|  |  | ||||||
|     // Add the new UPS to the config |     // Add the new UPS to the config | ||||||
|     config.upsDevices.push(newUps); |     config.upsDevices.push(newUps); | ||||||
|  |  | ||||||
|     // Save the configuration |     // Save the configuration | ||||||
|     await this.nupst.getDaemon().saveConfig(config); |     await this.nupst.getDaemon().saveConfig(config as INupstConfig); | ||||||
|  |  | ||||||
|     this.displayUpsConfigSummary(newUps); |     this.displayUpsConfigSummary(newUps); | ||||||
|  |  | ||||||
| @@ -155,7 +161,7 @@ export class UpsHandler { | |||||||
|   public async edit(upsId?: string): Promise<void> { |   public async edit(upsId?: string): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       // Import readline module for user input |       // Import readline module for user input | ||||||
|       const readline = await import('readline'); |       const readline = await import('node:readline'); | ||||||
|  |  | ||||||
|       const rl = readline.createInterface({ |       const rl = readline.createInterface({ | ||||||
|         input: process.stdin, |         input: process.stdin, | ||||||
| @@ -175,9 +181,10 @@ export class UpsHandler { | |||||||
|         await this.runEditProcess(upsId, prompt); |         await this.runEditProcess(upsId, prompt); | ||||||
|       } finally { |       } finally { | ||||||
|         rl.close(); |         rl.close(); | ||||||
|  |         process.stdin.destroy(); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Edit UPS error: ${error.message}`); |       logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -186,7 +193,10 @@ export class UpsHandler { | |||||||
|    * @param upsId ID of the UPS to edit (undefined for default UPS) |    * @param upsId ID of the UPS to edit (undefined for default UPS) | ||||||
|    * @param prompt Function to prompt for user input |    * @param prompt Function to prompt for user input | ||||||
|    */ |    */ | ||||||
|   public async runEditProcess(upsId: string | undefined, prompt: (question: string) => Promise<string>): Promise<void> { |   public async runEditProcess( | ||||||
|  |     upsId: string | undefined, | ||||||
|  |     prompt: (question: string) => Promise<string>, | ||||||
|  |   ): Promise<void> { | ||||||
|     logger.log('\nNUPST Edit UPS'); |     logger.log('\nNUPST Edit UPS'); | ||||||
|     logger.log('=============\n'); |     logger.log('=============\n'); | ||||||
|  |  | ||||||
| @@ -212,12 +222,16 @@ export class UpsHandler { | |||||||
|     // Convert old format to new format if needed |     // Convert old format to new format if needed | ||||||
|     if (!config.upsDevices) { |     if (!config.upsDevices) { | ||||||
|       // Initialize with the current config as the first UPS |       // Initialize with the current config as the first UPS | ||||||
|  |       if (!config.snmp) { | ||||||
|  |         logger.error('Legacy configuration is missing required SNMP settings'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|       config.upsDevices = [{ |       config.upsDevices = [{ | ||||||
|         id: 'default', |         id: 'default', | ||||||
|         name: 'Default UPS', |         name: 'Default UPS', | ||||||
|         snmp: config.snmp, |         snmp: config.snmp, | ||||||
|         thresholds: config.thresholds, |         groups: [], | ||||||
|         groups: [] |         actions: [], | ||||||
|       }]; |       }]; | ||||||
|       config.groups = []; |       config.groups = []; | ||||||
|       logger.log('Converting existing configuration to multi-UPS format.'); |       logger.log('Converting existing configuration to multi-UPS format.'); | ||||||
| @@ -227,7 +241,7 @@ export class UpsHandler { | |||||||
|     let upsToEdit; |     let upsToEdit; | ||||||
|     if (upsId) { |     if (upsId) { | ||||||
|       // Find specific UPS by ID |       // Find specific UPS by ID | ||||||
|       upsToEdit = config.upsDevices.find(ups => ups.id === upsId); |       upsToEdit = config.upsDevices.find((ups) => ups.id === upsId); | ||||||
|       if (!upsToEdit) { |       if (!upsToEdit) { | ||||||
|         logger.error(`UPS with ID "${upsId}" not found.`); |         logger.error(`UPS with ID "${upsId}" not found.`); | ||||||
|         return; |         return; | ||||||
| @@ -252,9 +266,6 @@ export class UpsHandler { | |||||||
|     // Edit SNMP settings |     // Edit SNMP settings | ||||||
|     await this.gatherSnmpSettings(upsToEdit.snmp, prompt); |     await this.gatherSnmpSettings(upsToEdit.snmp, prompt); | ||||||
|  |  | ||||||
|     // Edit threshold settings |  | ||||||
|     await this.gatherThresholdSettings(upsToEdit.thresholds, prompt); |  | ||||||
|      |  | ||||||
|     // Edit UPS model settings |     // Edit UPS model settings | ||||||
|     await this.gatherUpsModelSettings(upsToEdit.snmp, prompt); |     await this.gatherUpsModelSettings(upsToEdit.snmp, prompt); | ||||||
|  |  | ||||||
| @@ -266,6 +277,14 @@ export class UpsHandler { | |||||||
|       await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt); |       await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Initialize actions array if not exists | ||||||
|  |     if (!upsToEdit.actions) { | ||||||
|  |       upsToEdit.actions = []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Edit action settings | ||||||
|  |     await this.gatherActionSettings(upsToEdit.actions, prompt); | ||||||
|  |  | ||||||
|     // Save the configuration |     // Save the configuration | ||||||
|     await this.nupst.getDaemon().saveConfig(config); |     await this.nupst.getDaemon().saveConfig(config); | ||||||
|  |  | ||||||
| @@ -284,7 +303,7 @@ export class UpsHandler { | |||||||
|    * Delete a UPS by ID |    * Delete a UPS by ID | ||||||
|    * @param upsId ID of the UPS to delete |    * @param upsId ID of the UPS to delete | ||||||
|    */ |    */ | ||||||
|   public async delete(upsId: string): Promise<void> { |   public async remove(upsId: string): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       // Try to load configuration |       // Try to load configuration | ||||||
|       try { |       try { | ||||||
| @@ -309,7 +328,7 @@ export class UpsHandler { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Find the UPS to delete |       // Find the UPS to delete | ||||||
|       const upsIndex = config.upsDevices.findIndex(ups => ups.id === upsId); |       const upsIndex = config.upsDevices.findIndex((ups) => ups.id === upsId); | ||||||
|       if (upsIndex === -1) { |       if (upsIndex === -1) { | ||||||
|         logger.error(`UPS with ID "${upsId}" not found.`); |         logger.error(`UPS with ID "${upsId}" not found.`); | ||||||
|         return; |         return; | ||||||
| @@ -318,19 +337,23 @@ export class UpsHandler { | |||||||
|       const upsToDelete = config.upsDevices[upsIndex]; |       const upsToDelete = config.upsDevices[upsIndex]; | ||||||
|  |  | ||||||
|       // Get confirmation before deleting |       // Get confirmation before deleting | ||||||
|       const readline = await import('readline'); |       const readline = await import('node:readline'); | ||||||
|       const rl = readline.createInterface({ |       const rl = readline.createInterface({ | ||||||
|         input: process.stdin, |         input: process.stdin, | ||||||
|         output: process.stdout, |         output: process.stdout, | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       const confirm = await new Promise<string>(resolve => { |       const confirm = await new Promise<string>((resolve) => { | ||||||
|         rl.question(`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `, answer => { |         rl.question( | ||||||
|  |           `Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `, | ||||||
|  |           (answer) => { | ||||||
|             resolve(answer.toLowerCase()); |             resolve(answer.toLowerCase()); | ||||||
|         }); |           }, | ||||||
|  |         ); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       rl.close(); |       rl.close(); | ||||||
|  |       process.stdin.destroy(); | ||||||
|  |  | ||||||
|       if (confirm !== 'y' && confirm !== 'yes') { |       if (confirm !== 'y' && confirm !== 'yes') { | ||||||
|         logger.log('Deletion cancelled.'); |         logger.log('Deletion cancelled.'); | ||||||
| @@ -348,7 +371,9 @@ export class UpsHandler { | |||||||
|       // Check if service is running and restart it if needed |       // Check if service is running and restart it if needed | ||||||
|       await this.restartServiceIfRunning(); |       await this.restartServiceIfRunning(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Failed to delete UPS: ${error.message}`); |       logger.error( | ||||||
|  |         `Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -361,11 +386,10 @@ export class UpsHandler { | |||||||
|       try { |       try { | ||||||
|         await this.nupst.getDaemon().loadConfig(); |         await this.nupst.getDaemon().loadConfig(); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         const errorBoxWidth = 45; |         logger.logBox('Configuration Error', [ | ||||||
|         logger.logBoxTitle('Configuration Error', errorBoxWidth); |           'No configuration found.', | ||||||
|         logger.logBoxLine('No configuration found.'); |           "Please run 'nupst ups add' first to create a configuration.", | ||||||
|         logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); |         ], 50, 'error'); | ||||||
|         logger.logBoxEnd(); |  | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -375,48 +399,60 @@ export class UpsHandler { | |||||||
|       // Check if multi-UPS config |       // Check if multi-UPS config | ||||||
|       if (!config.upsDevices || !Array.isArray(config.upsDevices)) { |       if (!config.upsDevices || !Array.isArray(config.upsDevices)) { | ||||||
|         // Legacy single UPS configuration |         // Legacy single UPS configuration | ||||||
|         const boxWidth = 45; |         logger.logBox('UPS Devices', [ | ||||||
|         logger.logBoxTitle('UPS Devices', boxWidth); |           'Legacy single-UPS configuration detected.', | ||||||
|         logger.logBoxLine('Legacy single-UPS configuration detected.'); |           '', | ||||||
|         logger.logBoxLine(''); |           ...(!config.snmp  | ||||||
|         logger.logBoxLine('Default UPS:'); |             ? ['Error: Configuration missing SNMP settings'] | ||||||
|         logger.logBoxLine(`  Host: ${config.snmp.host}:${config.snmp.port}`); |             : [ | ||||||
|         logger.logBoxLine(`  Model: ${config.snmp.upsModel || 'cyberpower'}`); |                 'Default UPS:', | ||||||
|         logger.logBoxLine(`  Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`); |                 `  Host: ${config.snmp.host}:${config.snmp.port}`, | ||||||
|         logger.logBoxLine(''); |                 `  Model: ${config.snmp.upsModel || 'cyberpower'}`, | ||||||
|         logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate'); |                 '', | ||||||
|         logger.logBoxLine('to the multi-UPS configuration format.'); |                 'Use "nupst ups add" to add more UPS devices and migrate', | ||||||
|         logger.logBoxEnd(); |                 'to the multi-UPS configuration format.', | ||||||
|  |               ] | ||||||
|  |           ), | ||||||
|  |         ], 60, 'warning'); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Display UPS list |       // Display UPS list with modern table | ||||||
|       const boxWidth = 60; |  | ||||||
|       logger.logBoxTitle('UPS Devices', boxWidth); |  | ||||||
|        |  | ||||||
|       if (config.upsDevices.length === 0) { |       if (config.upsDevices.length === 0) { | ||||||
|         logger.logBoxLine('No UPS devices configured.'); |         logger.logBox('UPS Devices', [ | ||||||
|         logger.logBoxLine('Use "nupst add" to add a UPS device.'); |           'No UPS devices configured.', | ||||||
|       } else { |           '', | ||||||
|         logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`); |           `${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`, | ||||||
|         logger.logBoxLine(''); |         ], 60, 'info'); | ||||||
|         logger.logBoxLine('ID          | Name                  | Host            | Mode         | Groups'); |         return; | ||||||
|         logger.logBoxLine('-----------+----------------------+----------------+--------------+----------------'); |  | ||||||
|          |  | ||||||
|         for (const ups of config.upsDevices) { |  | ||||||
|           const id = ups.id.padEnd(10, ' ').substring(0, 10); |  | ||||||
|           const name = (ups.name || '').padEnd(20, ' ').substring(0, 20); |  | ||||||
|           const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15); |  | ||||||
|           const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12); |  | ||||||
|           const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None'; |  | ||||||
|            |  | ||||||
|           logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`); |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       logger.logBoxEnd(); |       // Prepare table data | ||||||
|  |       const rows = config.upsDevices.map((ups) => ({ | ||||||
|  |         id: ups.id, | ||||||
|  |         name: ups.name || '', | ||||||
|  |         host: `${ups.snmp.host}:${ups.snmp.port}`, | ||||||
|  |         model: ups.snmp.upsModel || 'cyberpower', | ||||||
|  |         groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'), | ||||||
|  |       })); | ||||||
|  |  | ||||||
|  |       const columns: ITableColumn[] = [ | ||||||
|  |         { header: 'ID', key: 'id', align: 'left', color: theme.highlight }, | ||||||
|  |         { header: 'Name', key: 'name', align: 'left' }, | ||||||
|  |         { header: 'Host:Port', key: 'host', align: 'left', color: theme.info }, | ||||||
|  |         { header: 'Model', key: 'model', align: 'left' }, | ||||||
|  |         { header: 'Groups', key: 'groups', align: 'left' }, | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.info(`UPS Devices (${config.upsDevices.length}):`); | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.logTable(columns, rows); | ||||||
|  |       logger.log(''); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Failed to list UPS devices: ${error.message}`); |       logger.error( | ||||||
|  |         `Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -465,7 +501,7 @@ export class UpsHandler { | |||||||
|         await this.testConnection(config); |         await this.testConnection(config); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Test failed: ${error.message}`); |       logger.error(`Test failed: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -475,9 +511,8 @@ export class UpsHandler { | |||||||
|    */ |    */ | ||||||
|   private displayTestConfig(config: any): void { |   private displayTestConfig(config: any): void { | ||||||
|     // Check if this is a UPS device or full configuration |     // Check if this is a UPS device or full configuration | ||||||
|     const isUpsConfig = config.snmp && config.thresholds; |     const isUpsConfig = config.snmp; | ||||||
|     const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {}; |     const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {}; | ||||||
|     const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {}; |  | ||||||
|     const checkInterval = config.checkInterval || 30000; |     const checkInterval = config.checkInterval || 30000; | ||||||
|  |  | ||||||
|     // Get UPS name and ID if available |     // Get UPS name and ID if available | ||||||
| @@ -516,16 +551,16 @@ export class UpsHandler { | |||||||
|     if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) { |     if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) { | ||||||
|       logger.logBoxLine('Custom OIDs:'); |       logger.logBoxLine('Custom OIDs:'); | ||||||
|       logger.logBoxLine(`  Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`); |       logger.logBoxLine(`  Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`); | ||||||
|       logger.logBoxLine(`  Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`); |       logger.logBoxLine( | ||||||
|  |         `  Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`, | ||||||
|  |       ); | ||||||
|       logger.logBoxLine(`  Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`); |       logger.logBoxLine(`  Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`); | ||||||
|     } |     } | ||||||
|     logger.logBoxLine('Thresholds:'); |  | ||||||
|     logger.logBoxLine(`  Battery: ${thresholds.battery}%`); |  | ||||||
|     logger.logBoxLine(`  Runtime: ${thresholds.runtime} minutes`); |  | ||||||
|      |  | ||||||
|     // Show group assignments if this is a UPS config |     // Show group assignments if this is a UPS config | ||||||
|     if (config.groups && Array.isArray(config.groups)) { |     if (config.groups && Array.isArray(config.groups)) { | ||||||
|       logger.logBoxLine(`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`); |       logger.logBoxLine( | ||||||
|  |         `Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`); |     logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`); | ||||||
| @@ -544,7 +579,6 @@ export class UpsHandler { | |||||||
|     try { |     try { | ||||||
|       // Create a test config with a short timeout |       // Create a test config with a short timeout | ||||||
|       const snmpConfig = config.snmp ? config.snmp : config.snmp; |       const snmpConfig = config.snmp ? config.snmp : config.snmp; | ||||||
|       const thresholds = config.thresholds ? config.thresholds : config.thresholds; |  | ||||||
|  |  | ||||||
|       const testConfig = { |       const testConfig = { | ||||||
|         ...snmpConfig, |         ...snmpConfig, | ||||||
| @@ -561,14 +595,11 @@ export class UpsHandler { | |||||||
|       logger.logBoxLine(`  Runtime Remaining: ${status.batteryRuntime} minutes`); |       logger.logBoxLine(`  Runtime Remaining: ${status.batteryRuntime} minutes`); | ||||||
|       logger.logBoxEnd(); |       logger.logBoxEnd(); | ||||||
|  |  | ||||||
|       // Check status against thresholds if on battery |        | ||||||
|       if (status.powerStatus === 'onBattery') { |  | ||||||
|         this.analyzeThresholds(status, thresholds); |  | ||||||
|       } |  | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       const errorBoxWidth = 45; |       const errorBoxWidth = 45; | ||||||
|       logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth); |       logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth); | ||||||
|       logger.logBoxLine(`Error: ${error.message}`); |       logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|       logger.logBoxEnd(); |       logger.logBoxEnd(); | ||||||
|       logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS."); |       logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS."); | ||||||
|     } |     } | ||||||
| @@ -586,26 +617,26 @@ export class UpsHandler { | |||||||
|     if (status.batteryCapacity < thresholds.battery) { |     if (status.batteryCapacity < thresholds.battery) { | ||||||
|       logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold'); |       logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold'); | ||||||
|       logger.logBoxLine( |       logger.logBoxLine( | ||||||
|         `  Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%` |         `  Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`, | ||||||
|       ); |       ); | ||||||
|       logger.logBoxLine('  System would initiate shutdown'); |       logger.logBoxLine('  System would initiate shutdown'); | ||||||
|     } else { |     } else { | ||||||
|       logger.logBoxLine('✓ Battery capacity above threshold'); |       logger.logBoxLine('✓ Battery capacity above threshold'); | ||||||
|       logger.logBoxLine( |       logger.logBoxLine( | ||||||
|         `  Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%` |         `  Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (status.batteryRuntime < thresholds.runtime) { |     if (status.batteryRuntime < thresholds.runtime) { | ||||||
|       logger.logBoxLine('⚠️ WARNING: Runtime below threshold'); |       logger.logBoxLine('⚠️ WARNING: Runtime below threshold'); | ||||||
|       logger.logBoxLine( |       logger.logBoxLine( | ||||||
|         `  Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min` |         `  Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`, | ||||||
|       ); |       ); | ||||||
|       logger.logBoxLine('  System would initiate shutdown'); |       logger.logBoxLine('  System would initiate shutdown'); | ||||||
|     } else { |     } else { | ||||||
|       logger.logBoxLine('✓ Runtime above threshold'); |       logger.logBoxLine('✓ Runtime above threshold'); | ||||||
|       logger.logBoxLine( |       logger.logBoxLine( | ||||||
|         `  Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min` |         `  Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -619,7 +650,7 @@ export class UpsHandler { | |||||||
|    */ |    */ | ||||||
|   private async gatherSnmpSettings( |   private async gatherSnmpSettings( | ||||||
|     snmpConfig: any, |     snmpConfig: any, | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     // SNMP IP Address |     // SNMP IP Address | ||||||
|     const defaultHost = snmpConfig.host || '127.0.0.1'; |     const defaultHost = snmpConfig.host || '127.0.0.1'; | ||||||
| @@ -634,14 +665,14 @@ export class UpsHandler { | |||||||
|  |  | ||||||
|     // SNMP Version |     // SNMP Version | ||||||
|     const defaultVersion = snmpConfig.version || 1; |     const defaultVersion = snmpConfig.version || 1; | ||||||
|     console.log('\nSNMP Version:'); |     logger.log(''); | ||||||
|     console.log('  1) SNMPv1'); |     logger.info('SNMP Version:'); | ||||||
|     console.log('  2) SNMPv2c'); |     logger.dim('  1) SNMPv1'); | ||||||
|     console.log('  3) SNMPv3 (with security features)'); |     logger.dim('  2) SNMPv2c'); | ||||||
|  |     logger.dim('  3) SNMPv3 (with security features)'); | ||||||
|     const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `); |     const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `); | ||||||
|     const version = parseInt(versionInput, 10); |     const version = parseInt(versionInput, 10); | ||||||
|     snmpConfig.version = |     snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3) | ||||||
|       versionInput.trim() && (version === 1 || version === 2 || version === 3) |  | ||||||
|       ? version |       ? version | ||||||
|       : defaultVersion; |       : defaultVersion; | ||||||
|  |  | ||||||
| @@ -663,15 +694,17 @@ export class UpsHandler { | |||||||
|    */ |    */ | ||||||
|   private async gatherSnmpV3Settings( |   private async gatherSnmpV3Settings( | ||||||
|     snmpConfig: any, |     snmpConfig: any, | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     console.log('\nSNMPv3 Security Settings:'); |     logger.log(''); | ||||||
|  |     logger.info('SNMPv3 Security Settings:'); | ||||||
|  |  | ||||||
|     // Security Level |     // Security Level | ||||||
|     console.log('\nSecurity Level:'); |     logger.log(''); | ||||||
|     console.log('  1) noAuthNoPriv (No Authentication, No Privacy)'); |     logger.info('Security Level:'); | ||||||
|     console.log('  2) authNoPriv (Authentication, No Privacy)'); |     logger.dim('  1) noAuthNoPriv (No Authentication, No Privacy)'); | ||||||
|     console.log('  3) authPriv (Authentication and Privacy)'); |     logger.dim('  2) authNoPriv (Authentication, No Privacy)'); | ||||||
|  |     logger.dim('  3) authPriv (Authentication and Privacy)'); | ||||||
|     const defaultSecLevel = snmpConfig.securityLevel |     const defaultSecLevel = snmpConfig.securityLevel | ||||||
|       ? snmpConfig.securityLevel === 'noAuthNoPriv' |       ? snmpConfig.securityLevel === 'noAuthNoPriv' | ||||||
|         ? 1 |         ? 1 | ||||||
| @@ -720,8 +753,9 @@ export class UpsHandler { | |||||||
|  |  | ||||||
|       // Allow customizing the timeout value |       // Allow customizing the timeout value | ||||||
|       const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display |       const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display | ||||||
|       console.log( |       logger.log(''); | ||||||
|         '\nSNMPv3 operations with authentication and privacy may require longer timeouts.' |       logger.info( | ||||||
|  |         'SNMPv3 operations with authentication and privacy may require longer timeouts.', | ||||||
|       ); |       ); | ||||||
|       const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `); |       const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `); | ||||||
|       const timeout = parseInt(timeoutInput, 10); |       const timeout = parseInt(timeoutInput, 10); | ||||||
| @@ -738,15 +772,16 @@ export class UpsHandler { | |||||||
|    */ |    */ | ||||||
|   private async gatherAuthenticationSettings( |   private async gatherAuthenticationSettings( | ||||||
|     snmpConfig: any, |     snmpConfig: any, | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     // Authentication protocol |     // Authentication protocol | ||||||
|     console.log('\nAuthentication Protocol:'); |     logger.log(''); | ||||||
|     console.log('  1) MD5'); |     logger.info('Authentication Protocol:'); | ||||||
|     console.log('  2) SHA'); |     logger.dim('  1) MD5'); | ||||||
|  |     logger.dim('  2) SHA'); | ||||||
|     const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1; |     const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1; | ||||||
|     const authProtocolInput = await prompt( |     const authProtocolInput = await prompt( | ||||||
|       `Select Authentication Protocol [${defaultAuthProtocol}]: ` |       `Select Authentication Protocol [${defaultAuthProtocol}]: `, | ||||||
|     ); |     ); | ||||||
|     const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol; |     const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol; | ||||||
|     snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5'; |     snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5'; | ||||||
| @@ -764,12 +799,13 @@ export class UpsHandler { | |||||||
|    */ |    */ | ||||||
|   private async gatherPrivacySettings( |   private async gatherPrivacySettings( | ||||||
|     snmpConfig: any, |     snmpConfig: any, | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     // Privacy protocol |     // Privacy protocol | ||||||
|     console.log('\nPrivacy Protocol:'); |     logger.log(''); | ||||||
|     console.log('  1) DES'); |     logger.info('Privacy Protocol:'); | ||||||
|     console.log('  2) AES'); |     logger.dim('  1) DES'); | ||||||
|  |     logger.dim('  2) AES'); | ||||||
|     const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1; |     const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1; | ||||||
|     const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `); |     const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `); | ||||||
|     const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol; |     const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol; | ||||||
| @@ -781,40 +817,6 @@ export class UpsHandler { | |||||||
|     snmpConfig.privKey = privKey.trim() || defaultPrivKey; |     snmpConfig.privKey = privKey.trim() || defaultPrivKey; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Gather threshold settings |  | ||||||
|    * @param thresholds Thresholds configuration object to update |  | ||||||
|    * @param prompt Function to prompt for user input |  | ||||||
|    */ |  | ||||||
|   private async gatherThresholdSettings( |  | ||||||
|     thresholds: any, |  | ||||||
|     prompt: (question: string) => Promise<string> |  | ||||||
|   ): Promise<void> { |  | ||||||
|     console.log('\nShutdown Thresholds:'); |  | ||||||
|  |  | ||||||
|     // Battery threshold |  | ||||||
|     const defaultBatteryThreshold = thresholds.battery || 60; |  | ||||||
|     const batteryThresholdInput = await prompt( |  | ||||||
|       `Battery percentage threshold [${defaultBatteryThreshold}%]: ` |  | ||||||
|     ); |  | ||||||
|     const batteryThreshold = parseInt(batteryThresholdInput, 10); |  | ||||||
|     thresholds.battery = |  | ||||||
|       batteryThresholdInput.trim() && !isNaN(batteryThreshold) |  | ||||||
|         ? batteryThreshold |  | ||||||
|         : defaultBatteryThreshold; |  | ||||||
|  |  | ||||||
|     // Runtime threshold |  | ||||||
|     const defaultRuntimeThreshold = thresholds.runtime || 20; |  | ||||||
|     const runtimeThresholdInput = await prompt( |  | ||||||
|       `Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: ` |  | ||||||
|     ); |  | ||||||
|     const runtimeThreshold = parseInt(runtimeThresholdInput, 10); |  | ||||||
|     thresholds.runtime = |  | ||||||
|       runtimeThresholdInput.trim() && !isNaN(runtimeThreshold) |  | ||||||
|         ? runtimeThreshold |  | ||||||
|         : defaultRuntimeThreshold; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Gather UPS model settings |    * Gather UPS model settings | ||||||
|    * @param snmpConfig SNMP configuration object to update |    * @param snmpConfig SNMP configuration object to update | ||||||
| @@ -822,18 +824,18 @@ export class UpsHandler { | |||||||
|    */ |    */ | ||||||
|   private async gatherUpsModelSettings( |   private async gatherUpsModelSettings( | ||||||
|     snmpConfig: any, |     snmpConfig: any, | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     console.log('\nUPS Model Selection:'); |     logger.log(''); | ||||||
|     console.log('  1) CyberPower'); |     logger.info('UPS Model Selection:'); | ||||||
|     console.log('  2) APC'); |     logger.dim('  1) CyberPower'); | ||||||
|     console.log('  3) Eaton'); |     logger.dim('  2) APC'); | ||||||
|     console.log('  4) TrippLite'); |     logger.dim('  3) Eaton'); | ||||||
|     console.log('  5) Liebert/Vertiv'); |     logger.dim('  4) TrippLite'); | ||||||
|     console.log('  6) Custom (Advanced)'); |     logger.dim('  5) Liebert/Vertiv'); | ||||||
|  |     logger.dim('  6) Custom (Advanced)'); | ||||||
|  |  | ||||||
|     const defaultModelValue = |     const defaultModelValue = snmpConfig.upsModel === 'cyberpower' | ||||||
|       snmpConfig.upsModel === 'cyberpower' |  | ||||||
|       ? 1 |       ? 1 | ||||||
|       : snmpConfig.upsModel === 'apc' |       : snmpConfig.upsModel === 'apc' | ||||||
|       ? 2 |       ? 2 | ||||||
| @@ -862,8 +864,9 @@ export class UpsHandler { | |||||||
|       snmpConfig.upsModel = 'liebert'; |       snmpConfig.upsModel = 'liebert'; | ||||||
|     } else if (modelValue === 6) { |     } else if (modelValue === 6) { | ||||||
|       snmpConfig.upsModel = 'custom'; |       snmpConfig.upsModel = 'custom'; | ||||||
|       console.log('\nEnter custom OIDs for your UPS:'); |       logger.log(''); | ||||||
|       console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)'); |       logger.info('Enter custom OIDs for your UPS:'); | ||||||
|  |       logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)'); | ||||||
|  |  | ||||||
|       // Custom OIDs |       // Custom OIDs | ||||||
|       const powerStatusOID = await prompt('Power Status OID: '); |       const powerStatusOID = await prompt('Power Status OID: '); | ||||||
| @@ -879,6 +882,151 @@ export class UpsHandler { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Gather action configuration settings | ||||||
|  |    * @param actions Actions array to configure | ||||||
|  |    * @param prompt Function to prompt for user input | ||||||
|  |    */ | ||||||
|  |   private async gatherActionSettings( | ||||||
|  |     actions: any[], | ||||||
|  |     prompt: (question: string) => Promise<string>, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     logger.log(''); | ||||||
|  |     logger.info('Action Configuration (Optional):'); | ||||||
|  |     logger.dim('Actions are triggered on power status changes and threshold violations.'); | ||||||
|  |     logger.dim('Leave empty to use default shutdown behavior on threshold violations.'); | ||||||
|  |  | ||||||
|  |     const configureActions = await prompt('Configure custom actions? (y/N): '); | ||||||
|  |     if (configureActions.toLowerCase() !== 'y') { | ||||||
|  |       return; // Keep existing actions or use default | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Clear existing actions | ||||||
|  |     actions.length = 0; | ||||||
|  |  | ||||||
|  |     let addMore = true; | ||||||
|  |     while (addMore) { | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.info('Action Type:'); | ||||||
|  |       logger.dim('  1) Shutdown (system shutdown)'); | ||||||
|  |       logger.dim('  2) Webhook (HTTP notification)'); | ||||||
|  |       logger.dim('  3) Custom Script (run .sh file from /etc/nupst)'); | ||||||
|  |  | ||||||
|  |       const typeInput = await prompt('Select action type [1]: '); | ||||||
|  |       const typeValue = parseInt(typeInput, 10) || 1; | ||||||
|  |  | ||||||
|  |       const action: any = {}; | ||||||
|  |  | ||||||
|  |       if (typeValue === 1) { | ||||||
|  |         // Shutdown action | ||||||
|  |         action.type = 'shutdown'; | ||||||
|  |  | ||||||
|  |         const delayInput = await prompt('Shutdown delay in minutes [5]: '); | ||||||
|  |         const delay = parseInt(delayInput, 10); | ||||||
|  |         if (delayInput.trim() && !isNaN(delay)) { | ||||||
|  |           action.shutdownDelay = delay; | ||||||
|  |         } | ||||||
|  |       } else if (typeValue === 2) { | ||||||
|  |         // Webhook action | ||||||
|  |         action.type = 'webhook'; | ||||||
|  |  | ||||||
|  |         const url = await prompt('Webhook URL: '); | ||||||
|  |         if (!url.trim()) { | ||||||
|  |           logger.warn('Webhook URL required, skipping action'); | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         action.webhookUrl = url.trim(); | ||||||
|  |  | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.info('HTTP Method:'); | ||||||
|  |         logger.dim('  1) POST (JSON body)'); | ||||||
|  |         logger.dim('  2) GET (query parameters)'); | ||||||
|  |         const methodInput = await prompt('Select method [1]: '); | ||||||
|  |         action.webhookMethod = methodInput === '2' ? 'GET' : 'POST'; | ||||||
|  |  | ||||||
|  |         const timeoutInput = await prompt('Timeout in seconds [10]: '); | ||||||
|  |         const timeout = parseInt(timeoutInput, 10); | ||||||
|  |         if (timeoutInput.trim() && !isNaN(timeout)) { | ||||||
|  |           action.webhookTimeout = timeout * 1000; // Convert to ms | ||||||
|  |         } | ||||||
|  |       } else if (typeValue === 3) { | ||||||
|  |         // Script action | ||||||
|  |         action.type = 'script'; | ||||||
|  |  | ||||||
|  |         const scriptPath = await prompt('Script filename (in /etc/nupst/, must end with .sh): '); | ||||||
|  |         if (!scriptPath.trim() || !scriptPath.trim().endsWith('.sh')) { | ||||||
|  |           logger.warn('Script path must end with .sh, skipping action'); | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         action.scriptPath = scriptPath.trim(); | ||||||
|  |  | ||||||
|  |         const timeoutInput = await prompt('Script timeout in seconds [60]: '); | ||||||
|  |         const timeout = parseInt(timeoutInput, 10); | ||||||
|  |         if (timeoutInput.trim() && !isNaN(timeout)) { | ||||||
|  |           action.scriptTimeout = timeout * 1000; // Convert to ms | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         logger.warn('Invalid action type, skipping'); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Configure trigger mode (applies to all action types) | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.info('Trigger Mode:'); | ||||||
|  |       logger.dim('  1) Power changes + thresholds (default)'); | ||||||
|  |       logger.dim('  2) Only power status changes'); | ||||||
|  |       logger.dim('  3) Only threshold violations'); | ||||||
|  |       logger.dim('  4) Any change (every ~30s check)'); | ||||||
|  |       const triggerInput = await prompt('Select trigger mode [1]: '); | ||||||
|  |       const triggerValue = parseInt(triggerInput, 10) || 1; | ||||||
|  |        | ||||||
|  |       switch (triggerValue) { | ||||||
|  |         case 2: | ||||||
|  |           action.triggerMode = 'onlyPowerChanges'; | ||||||
|  |           break; | ||||||
|  |         case 3: | ||||||
|  |           action.triggerMode = 'onlyThresholds'; | ||||||
|  |           break; | ||||||
|  |         case 4: | ||||||
|  |           action.triggerMode = 'anyChange'; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           action.triggerMode = 'powerChangesAndThresholds'; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes | ||||||
|  |       if (action.triggerMode === 'onlyThresholds' || action.triggerMode === 'powerChangesAndThresholds') { | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.info('Action Thresholds:'); | ||||||
|  |         logger.dim('Action will trigger when battery or runtime falls below these values (while on battery)'); | ||||||
|  |          | ||||||
|  |         const batteryInput = await prompt('Battery threshold percentage [60]: '); | ||||||
|  |         const battery = parseInt(batteryInput, 10); | ||||||
|  |         const batteryThreshold = (batteryInput.trim() && !isNaN(battery)) ? battery : 60; | ||||||
|  |  | ||||||
|  |         const runtimeInput = await prompt('Runtime threshold in minutes [20]: '); | ||||||
|  |         const runtime = parseInt(runtimeInput, 10); | ||||||
|  |         const runtimeThreshold = (runtimeInput.trim() && !isNaN(runtime)) ? runtime : 20; | ||||||
|  |  | ||||||
|  |         action.thresholds = { | ||||||
|  |           battery: batteryThreshold, | ||||||
|  |           runtime: runtimeThreshold, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       actions.push(action); | ||||||
|  |       logger.success(`${action.type.charAt(0).toUpperCase() + action.type.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`); | ||||||
|  |  | ||||||
|  |       const more = await prompt('Add another action? (y/N): '); | ||||||
|  |       addMore = more.toLowerCase() === 'y'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (actions.length > 0) { | ||||||
|  |       logger.log(''); | ||||||
|  |       logger.success(`${actions.length} action(s) configured`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Display UPS configuration summary |    * Display UPS configuration summary | ||||||
|    * @param ups UPS configuration |    * @param ups UPS configuration | ||||||
| @@ -891,9 +1039,7 @@ export class UpsHandler { | |||||||
|     logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`); |     logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`); | ||||||
|     logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`); |     logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`); | ||||||
|     logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`); |     logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`); | ||||||
|     logger.logBoxLine( |      | ||||||
|       `Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime` |  | ||||||
|     ); |  | ||||||
|     if (ups.groups && ups.groups.length > 0) { |     if (ups.groups && ups.groups.length > 0) { | ||||||
|       logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`); |       logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`); | ||||||
|     } else { |     } else { | ||||||
| @@ -910,10 +1056,10 @@ export class UpsHandler { | |||||||
|    */ |    */ | ||||||
|   private async optionallyTestConnection( |   private async optionallyTestConnection( | ||||||
|     snmpConfig: any, |     snmpConfig: any, | ||||||
|     prompt: (question: string) => Promise<string> |     prompt: (question: string) => Promise<string>, | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     const testConnection = await prompt( |     const testConnection = await prompt( | ||||||
|       'Would you like to test the connection to your UPS? (y/N): ' |       'Would you like to test the connection to your UPS? (y/N): ', | ||||||
|     ); |     ); | ||||||
|     if (testConnection.toLowerCase() === 'y') { |     if (testConnection.toLowerCase() === 'y') { | ||||||
|       logger.log('\nTesting connection to UPS...'); |       logger.log('\nTesting connection to UPS...'); | ||||||
| @@ -937,7 +1083,7 @@ export class UpsHandler { | |||||||
|         const errorBoxWidth = 45; |         const errorBoxWidth = 45; | ||||||
|         logger.log(''); |         logger.log(''); | ||||||
|         logger.logBoxTitle('Connection Failed!', errorBoxWidth); |         logger.logBoxTitle('Connection Failed!', errorBoxWidth); | ||||||
|         logger.logBoxLine(`Error: ${error.message}`); |         logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`); | ||||||
|         logger.logBoxEnd(); |         logger.logBoxEnd(); | ||||||
|         logger.log('\nPlease check your settings and try again.'); |         logger.log('\nPlease check your settings and try again.'); | ||||||
|       } |       } | ||||||
| @@ -948,7 +1094,7 @@ export class UpsHandler { | |||||||
|    * Check if the systemd service is running and restart it if it is |    * Check if the systemd service is running and restart it if it is | ||||||
|    * This is useful after configuration changes |    * This is useful after configuration changes | ||||||
|    */ |    */ | ||||||
|   public async restartServiceIfRunning(): Promise<void> { |   public restartServiceIfRunning(): void { | ||||||
|     try { |     try { | ||||||
|       // Check if the service is active |       // Check if the service is active | ||||||
|       const isActive = |       const isActive = | ||||||
| @@ -972,7 +1118,9 @@ export class UpsHandler { | |||||||
|             logger.logBoxLine('  sudo systemctl restart nupst.service'); |             logger.logBoxLine('  sudo systemctl restart nupst.service'); | ||||||
|           } |           } | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|           logger.logBoxLine(`Error restarting service: ${error.message}`); |           logger.logBoxLine( | ||||||
|  |             `Error restarting service: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |           ); | ||||||
|           logger.logBoxLine('You may need to restart the service manually:'); |           logger.logBoxLine('You may need to restart the service manually:'); | ||||||
|           logger.logBoxLine('  sudo systemctl restart nupst.service'); |           logger.logBoxLine('  sudo systemctl restart nupst.service'); | ||||||
|         } |         } | ||||||
|   | |||||||
							
								
								
									
										88
									
								
								ts/colors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								ts/colors.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | /** | ||||||
|  |  * Color theme and styling utilities for NUPST CLI | ||||||
|  |  * Uses Deno standard library colors module | ||||||
|  |  */ | ||||||
|  | import * as colors from '@std/fmt/colors'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Color theme for consistent CLI styling | ||||||
|  |  */ | ||||||
|  | export const theme = { | ||||||
|  |   // Message types | ||||||
|  |   error: colors.red, | ||||||
|  |   warning: colors.yellow, | ||||||
|  |   success: colors.green, | ||||||
|  |   info: colors.cyan, | ||||||
|  |   dim: colors.dim, | ||||||
|  |   highlight: colors.bold, | ||||||
|  |  | ||||||
|  |   // Status indicators | ||||||
|  |   statusActive: (text: string) => colors.green(colors.bold(text)), | ||||||
|  |   statusInactive: (text: string) => colors.red(text), | ||||||
|  |   statusWarning: (text: string) => colors.yellow(text), | ||||||
|  |   statusUnknown: (text: string) => colors.dim(text), | ||||||
|  |  | ||||||
|  |   // Battery level colors | ||||||
|  |   batteryGood: colors.green, // > 60% | ||||||
|  |   batteryMedium: colors.yellow, // 30-60% | ||||||
|  |   batteryCritical: colors.red, // < 30% | ||||||
|  |  | ||||||
|  |   // Box borders | ||||||
|  |   borderSuccess: colors.green, | ||||||
|  |   borderError: colors.red, | ||||||
|  |   borderWarning: colors.yellow, | ||||||
|  |   borderInfo: colors.cyan, | ||||||
|  |   borderDefault: (text: string) => text, // No color | ||||||
|  |  | ||||||
|  |   // Command/code highlighting | ||||||
|  |   command: colors.cyan, | ||||||
|  |   code: colors.dim, | ||||||
|  |   path: colors.blue, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Status symbols with colors | ||||||
|  |  */ | ||||||
|  | export const symbols = { | ||||||
|  |   success: colors.green('✓'), | ||||||
|  |   error: colors.red('✗'), | ||||||
|  |   warning: colors.yellow('⚠'), | ||||||
|  |   info: colors.cyan('ℹ'), | ||||||
|  |   running: colors.green('●'), | ||||||
|  |   stopped: colors.red('○'), | ||||||
|  |   starting: colors.yellow('◐'), | ||||||
|  |   unknown: colors.dim('◯'), | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get color for battery level | ||||||
|  |  */ | ||||||
|  | export function getBatteryColor(percentage: number): (text: string) => string { | ||||||
|  |   if (percentage >= 60) return theme.batteryGood; | ||||||
|  |   if (percentage >= 30) return theme.batteryMedium; | ||||||
|  |   return theme.batteryCritical; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get color for runtime remaining | ||||||
|  |  */ | ||||||
|  | export function getRuntimeColor(minutes: number): (text: string) => string { | ||||||
|  |   if (minutes >= 20) return theme.batteryGood; | ||||||
|  |   if (minutes >= 10) return theme.batteryMedium; | ||||||
|  |   return theme.batteryCritical; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Format UPS power status with color | ||||||
|  |  */ | ||||||
|  | export function formatPowerStatus(status: 'online' | 'onBattery' | 'unknown'): string { | ||||||
|  |   switch (status) { | ||||||
|  |     case 'online': | ||||||
|  |       return theme.success('Online'); | ||||||
|  |     case 'onBattery': | ||||||
|  |       return theme.warning('On Battery'); | ||||||
|  |     case 'unknown': | ||||||
|  |     default: | ||||||
|  |       return theme.dim('Unknown'); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										736
									
								
								ts/daemon.ts
									
									
									
									
									
								
							
							
						
						
									
										736
									
								
								ts/daemon.ts
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1 +1 @@ | |||||||
| export * from './shortid.js'; | export * from './shortid.ts'; | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| #!/usr/bin/env node | #!/usr/bin/env node | ||||||
|  |  | ||||||
| import { NupstCli } from './cli.js'; | import { NupstCli } from './cli.ts'; | ||||||
| import { logger } from './logger.js'; | import { logger } from './logger.ts'; | ||||||
|  | import process from 'node:process'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Main entry point for NUPST |  * Main entry point for NUPST | ||||||
| @@ -13,7 +14,7 @@ async function main() { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Run the main function and handle any errors | // Run the main function and handle any errors | ||||||
| main().catch(error => { | main().catch((error) => { | ||||||
|   logger.error(`Error: ${error}`); |   logger.error(`Error: ${error}`); | ||||||
|   process.exit(1); |   process.exit(1); | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										220
									
								
								ts/logger.ts
									
									
									
									
									
								
							
							
						
						
									
										220
									
								
								ts/logger.ts
									
									
									
									
									
								
							| @@ -1,9 +1,38 @@ | |||||||
|  | import { theme, symbols } from './colors.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Table column alignment options | ||||||
|  |  */ | ||||||
|  | export type TColumnAlign = 'left' | 'right' | 'center'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Table column definition | ||||||
|  |  */ | ||||||
|  | export interface ITableColumn { | ||||||
|  |   /** Column header text */ | ||||||
|  |   header: string; | ||||||
|  |   /** Column key in data object */ | ||||||
|  |   key: string; | ||||||
|  |   /** Column alignment (default: left) */ | ||||||
|  |   align?: TColumnAlign; | ||||||
|  |   /** Column width (auto-calculated if not specified) */ | ||||||
|  |   width?: number; | ||||||
|  |   /** Color function to apply to cell values */ | ||||||
|  |   color?: (value: string) => string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Box style types with colors | ||||||
|  |  */ | ||||||
|  | export type TBoxStyle = 'default' | 'success' | 'error' | 'warning' | 'info'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * A simple logger class that provides consistent formatting for log messages |  * A simple logger class that provides consistent formatting for log messages | ||||||
|  * including support for logboxes with title, lines, and closing |  * including support for logboxes with title, lines, and closing | ||||||
|  */ |  */ | ||||||
| export class Logger { | export class Logger { | ||||||
|   private currentBoxWidth: number | null = null; |   private currentBoxWidth: number | null = null; | ||||||
|  |   private currentBoxStyle: TBoxStyle = 'default'; | ||||||
|   private static instance: Logger; |   private static instance: Logger; | ||||||
|  |  | ||||||
|   /** Default width to use when no width is specified */ |   /** Default width to use when no width is specified */ | ||||||
| @@ -36,36 +65,83 @@ export class Logger { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Log an error message |    * Log an error message (red with ✗ symbol) | ||||||
|    * @param message Error message to log |    * @param message Error message to log | ||||||
|    */ |    */ | ||||||
|   public error(message: string): void { |   public error(message: string): void { | ||||||
|     console.error(message); |     console.error(`${symbols.error} ${theme.error(message)}`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Log a warning message with a warning emoji |    * Log a warning message (yellow with ⚠ symbol) | ||||||
|    * @param message Warning message to log |    * @param message Warning message to log | ||||||
|    */ |    */ | ||||||
|   public warn(message: string): void { |   public warn(message: string): void { | ||||||
|     console.warn(`⚠️ ${message}`); |     console.warn(`${symbols.warning} ${theme.warning(message)}`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Log a success message with a checkmark |    * Log a success message (green with ✓ symbol) | ||||||
|    * @param message Success message to log |    * @param message Success message to log | ||||||
|    */ |    */ | ||||||
|   public success(message: string): void { |   public success(message: string): void { | ||||||
|     console.log(`✓ ${message}`); |     console.log(`${symbols.success} ${theme.success(message)}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log an info message (cyan with ℹ symbol) | ||||||
|  |    * @param message Info message to log | ||||||
|  |    */ | ||||||
|  |   public info(message: string): void { | ||||||
|  |     console.log(`${symbols.info} ${theme.info(message)}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a dim/secondary message | ||||||
|  |    * @param message Message to log in dim style | ||||||
|  |    */ | ||||||
|  |   public dim(message: string): void { | ||||||
|  |     console.log(theme.dim(message)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a highlighted/bold message | ||||||
|  |    * @param message Message to highlight | ||||||
|  |    */ | ||||||
|  |   public highlight(message: string): void { | ||||||
|  |     console.log(theme.highlight(message)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get color function for box based on style | ||||||
|  |    */ | ||||||
|  |   private getBoxColor(style: TBoxStyle): (text: string) => string { | ||||||
|  |     switch (style) { | ||||||
|  |       case 'success': | ||||||
|  |         return theme.borderSuccess; | ||||||
|  |       case 'error': | ||||||
|  |         return theme.borderError; | ||||||
|  |       case 'warning': | ||||||
|  |         return theme.borderWarning; | ||||||
|  |       case 'info': | ||||||
|  |         return theme.borderInfo; | ||||||
|  |       case 'default': | ||||||
|  |       default: | ||||||
|  |         return theme.borderDefault; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Log a logbox title and set the current box width |    * Log a logbox title and set the current box width | ||||||
|    * @param title Title of the logbox |    * @param title Title of the logbox | ||||||
|    * @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH |    * @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH | ||||||
|  |    * @param style Box style for coloring (default, success, error, warning, info) | ||||||
|    */ |    */ | ||||||
|   public logBoxTitle(title: string, width?: number): void { |   public logBoxTitle(title: string, width?: number, style?: TBoxStyle): void { | ||||||
|     this.currentBoxWidth = width || this.DEFAULT_WIDTH; |     this.currentBoxWidth = width || this.DEFAULT_WIDTH; | ||||||
|  |     this.currentBoxStyle = style || 'default'; | ||||||
|  |  | ||||||
|  |     const colorFn = this.getBoxColor(this.currentBoxStyle); | ||||||
|  |  | ||||||
|     // Create the title line with appropriate padding |     // Create the title line with appropriate padding | ||||||
|     const paddedTitle = ` ${title} `; |     const paddedTitle = ` ${title} `; | ||||||
| @@ -74,7 +150,7 @@ export class Logger { | |||||||
|     // Title line: ┌─ Title ───┐ |     // Title line: ┌─ Title ───┐ | ||||||
|     const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`; |     const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`; | ||||||
|  |  | ||||||
|     console.log(titleLine); |     console.log(colorFn(titleLine)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -89,17 +165,21 @@ export class Logger { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; |     const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; | ||||||
|  |     const colorFn = this.getBoxColor(this.currentBoxStyle); | ||||||
|  |  | ||||||
|     // Calculate the available space for content |     // Calculate the available space for content (use visible length) | ||||||
|     const availableSpace = boxWidth - 2; // Account for left and right borders |     const availableSpace = boxWidth - 2; // Account for left and right borders | ||||||
|  |     const visibleLen = this.visibleLength(content); | ||||||
|  |  | ||||||
|     if (content.length <= availableSpace - 1) { |     if (visibleLen <= availableSpace - 1) { | ||||||
|       // If content fits with at least one space for the right border stripe |       // If content fits with at least one space for the right border stripe | ||||||
|       const padding = availableSpace - content.length - 1; |       const padding = availableSpace - visibleLen - 1; | ||||||
|       console.log(`│ ${content}${' '.repeat(padding)}│`); |       const line = `│ ${content}${' '.repeat(padding)}│`; | ||||||
|  |       console.log(colorFn(line)); | ||||||
|     } else { |     } else { | ||||||
|       // Content is too long, let it flow out of boundaries. |       // Content is too long, let it flow out of boundaries. | ||||||
|       console.log(`│ ${content}`); |       const line = `│ ${content}`; | ||||||
|  |       console.log(colorFn(line)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -109,12 +189,15 @@ export class Logger { | |||||||
|    */ |    */ | ||||||
|   public logBoxEnd(width?: number): void { |   public logBoxEnd(width?: number): void { | ||||||
|     const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; |     const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; | ||||||
|  |     const colorFn = this.getBoxColor(this.currentBoxStyle); | ||||||
|  |  | ||||||
|     // Create the bottom border: └────────┘ |     // Create the bottom border: └────────┘ | ||||||
|     console.log(`└${'─'.repeat(boxWidth - 2)}┘`); |     const bottomLine = `└${'─'.repeat(boxWidth - 2)}┘`; | ||||||
|  |     console.log(colorFn(bottomLine)); | ||||||
|  |  | ||||||
|     // Reset the current box width |     // Reset the current box width and style | ||||||
|     this.currentBoxWidth = null; |     this.currentBoxWidth = null; | ||||||
|  |     this.currentBoxStyle = 'default'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -122,9 +205,10 @@ export class Logger { | |||||||
|    * @param title Title of the logbox |    * @param title Title of the logbox | ||||||
|    * @param lines Array of content lines |    * @param lines Array of content lines | ||||||
|    * @param width Width of the logbox, defaults to DEFAULT_WIDTH |    * @param width Width of the logbox, defaults to DEFAULT_WIDTH | ||||||
|  |    * @param style Box style for coloring | ||||||
|    */ |    */ | ||||||
|   public logBox(title: string, lines: string[], width?: number): void { |   public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void { | ||||||
|     this.logBoxTitle(title, width || this.DEFAULT_WIDTH); |     this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style); | ||||||
|  |  | ||||||
|     for (const line of lines) { |     for (const line of lines) { | ||||||
|       this.logBoxLine(line); |       this.logBoxLine(line); | ||||||
| @@ -141,6 +225,108 @@ export class Logger { | |||||||
|   public logDivider(width?: number, character: string = '─'): void { |   public logDivider(width?: number, character: string = '─'): void { | ||||||
|     console.log(character.repeat(width || this.DEFAULT_WIDTH)); |     console.log(character.repeat(width || this.DEFAULT_WIDTH)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Strip ANSI color codes from string for accurate length calculation | ||||||
|  |    */ | ||||||
|  |   private stripAnsi(text: string): string { | ||||||
|  |     // Remove ANSI escape codes | ||||||
|  |     return text.replace(/\x1b\[[0-9;]*m/g, ''); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get visible length of string (excluding ANSI codes) | ||||||
|  |    */ | ||||||
|  |   private visibleLength(text: string): number { | ||||||
|  |     return this.stripAnsi(text).length; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Align text within a column (handles ANSI color codes correctly) | ||||||
|  |    */ | ||||||
|  |   private alignText(text: string, width: number, align: TColumnAlign = 'left'): string { | ||||||
|  |     const visibleLen = this.visibleLength(text); | ||||||
|  |  | ||||||
|  |     if (visibleLen >= width) { | ||||||
|  |       // Text is too long, truncate the visible part | ||||||
|  |       const stripped = this.stripAnsi(text); | ||||||
|  |       return stripped.substring(0, width); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const padding = width - visibleLen; | ||||||
|  |  | ||||||
|  |     switch (align) { | ||||||
|  |       case 'right': | ||||||
|  |         return ' '.repeat(padding) + text; | ||||||
|  |       case 'center': { | ||||||
|  |         const leftPad = Math.floor(padding / 2); | ||||||
|  |         const rightPad = padding - leftPad; | ||||||
|  |         return ' '.repeat(leftPad) + text + ' '.repeat(rightPad); | ||||||
|  |       } | ||||||
|  |       case 'left': | ||||||
|  |       default: | ||||||
|  |         return text + ' '.repeat(padding); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Log a formatted table | ||||||
|  |    * @param columns Column definitions | ||||||
|  |    * @param rows Array of data objects | ||||||
|  |    * @param title Optional table title | ||||||
|  |    */ | ||||||
|  |   public logTable(columns: ITableColumn[], rows: Record<string, string>[], title?: string): void { | ||||||
|  |     if (rows.length === 0) { | ||||||
|  |       this.dim('No data to display'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Calculate column widths | ||||||
|  |     const columnWidths = columns.map((col) => { | ||||||
|  |       if (col.width) return col.width; | ||||||
|  |  | ||||||
|  |       // Auto-calculate width based on header and data (use visible length) | ||||||
|  |       let maxWidth = this.visibleLength(col.header); | ||||||
|  |       for (const row of rows) { | ||||||
|  |         const value = String(row[col.key] || ''); | ||||||
|  |         maxWidth = Math.max(maxWidth, this.visibleLength(value)); | ||||||
|  |       } | ||||||
|  |       return maxWidth; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Calculate total table width | ||||||
|  |     const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + (columns.length * 3) + 1; | ||||||
|  |  | ||||||
|  |     // Print title if provided | ||||||
|  |     if (title) { | ||||||
|  |       this.logBoxTitle(title, totalWidth); | ||||||
|  |     } else { | ||||||
|  |       // Print top border | ||||||
|  |       console.log('┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Print header row | ||||||
|  |     const headerCells = columns.map((col, i) => | ||||||
|  |       theme.highlight(this.alignText(col.header, columnWidths[i], col.align)) | ||||||
|  |     ); | ||||||
|  |     console.log('│ ' + headerCells.join(' │ ') + ' │'); | ||||||
|  |  | ||||||
|  |     // Print separator | ||||||
|  |     console.log('├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤'); | ||||||
|  |  | ||||||
|  |     // Print data rows | ||||||
|  |     for (const row of rows) { | ||||||
|  |       const cells = columns.map((col, i) => { | ||||||
|  |         const value = String(row[col.key] || ''); | ||||||
|  |         const aligned = this.alignText(value, columnWidths[i], col.align); | ||||||
|  |         return col.color ? col.color(aligned) : aligned; | ||||||
|  |       }); | ||||||
|  |       console.log('│ ' + cells.join(' │ ') + ' │'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Print bottom border | ||||||
|  |     console.log('└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘'); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| // Export a singleton instance for easy use | // Export a singleton instance for easy use | ||||||
|   | |||||||
							
								
								
									
										67
									
								
								ts/migrations/base-migration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								ts/migrations/base-migration.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | /** | ||||||
|  |  * Abstract base class for configuration migrations | ||||||
|  |  * | ||||||
|  |  * Each migration represents an upgrade from one config version to another. | ||||||
|  |  * Migrations run in order based on the `order` field, allowing users to jump | ||||||
|  |  * multiple versions (e.g., v1 → v4 runs migrations 2, 3, and 4). | ||||||
|  |  */ | ||||||
|  | /** | ||||||
|  |  * Abstract base class for configuration migrations | ||||||
|  |  * | ||||||
|  |  * Each migration represents an upgrade from one config version to another. | ||||||
|  |  * Migrations run in order based on the `toVersion` field, allowing users to jump | ||||||
|  |  * multiple versions (e.g., v1 → v4 runs migrations 2, 3, and 4). | ||||||
|  |  */ | ||||||
|  | export abstract class BaseMigration { | ||||||
|  |   /** | ||||||
|  |    * Source version this migration upgrades from | ||||||
|  |    * e.g., "1.x", "3.x" | ||||||
|  |    */ | ||||||
|  |   abstract readonly fromVersion: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Target version this migration upgrades to | ||||||
|  |    * e.g., "2.0", "4.0", "4.1" | ||||||
|  |    */ | ||||||
|  |   abstract readonly toVersion: string; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Check if this migration should run on the given config | ||||||
|  |    * | ||||||
|  |    * @param config - Raw configuration object to check (unknown schema for migrations) | ||||||
|  |    * @returns True if migration should run, false otherwise | ||||||
|  |    */ | ||||||
|  |   abstract shouldRun(config: Record<string, unknown>): Promise<boolean>; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Perform the migration on the given config | ||||||
|  |    * | ||||||
|  |    * @param config - Raw configuration object to migrate (unknown schema for migrations) | ||||||
|  |    * @returns Migrated configuration object | ||||||
|  |    */ | ||||||
|  |   abstract migrate(config: Record<string, unknown>): Promise<Record<string, unknown>>; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get human-readable name for this migration | ||||||
|  |    * | ||||||
|  |    * @returns Migration name | ||||||
|  |    */ | ||||||
|  |   getName(): string { | ||||||
|  |     return `Migration ${this.fromVersion} → ${this.toVersion}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Parse version string into a comparable number | ||||||
|  |    * Supports formats like "2.0", "4.1", etc. | ||||||
|  |    * Returns a number like 2.0, 4.1 for sorting | ||||||
|  |    * | ||||||
|  |    * @returns Parsed version number for ordering | ||||||
|  |    */ | ||||||
|  |   getVersionOrder(): number { | ||||||
|  |     const parsed = parseFloat(this.toVersion); | ||||||
|  |     if (isNaN(parsed)) { | ||||||
|  |       throw new Error(`Invalid version format: ${this.toVersion}`); | ||||||
|  |     } | ||||||
|  |     return parsed; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								ts/migrations/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ts/migrations/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | /** | ||||||
|  |  * Configuration migrations module | ||||||
|  |  * | ||||||
|  |  * Exports the migration system for upgrading configs between versions. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | export { BaseMigration } from './base-migration.ts'; | ||||||
|  | export { MigrationRunner } from './migration-runner.ts'; | ||||||
|  | export { MigrationV1ToV2 } from './migration-v1-to-v2.ts'; | ||||||
|  | export { MigrationV3ToV4 } from './migration-v3-to-v4.ts'; | ||||||
|  | export { MigrationV4_0ToV4_1 } from './migration-v4.0-to-v4.1.ts'; | ||||||
							
								
								
									
										75
									
								
								ts/migrations/migration-runner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								ts/migrations/migration-runner.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | import { BaseMigration } from './base-migration.ts'; | ||||||
|  | import { MigrationV1ToV2 } from './migration-v1-to-v2.ts'; | ||||||
|  | import { MigrationV3ToV4 } from './migration-v3-to-v4.ts'; | ||||||
|  | import { MigrationV4_0ToV4_1 } from './migration-v4.0-to-v4.1.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Migration runner | ||||||
|  |  * | ||||||
|  |  * Discovers all available migrations, sorts them by order, | ||||||
|  |  * and runs applicable migrations in sequence. | ||||||
|  |  */ | ||||||
|  | export class MigrationRunner { | ||||||
|  |   private migrations: BaseMigration[]; | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     // Register all migrations here | ||||||
|  |     this.migrations = [ | ||||||
|  |       new MigrationV1ToV2(), | ||||||
|  |       new MigrationV3ToV4(), | ||||||
|  |       new MigrationV4_0ToV4_1(), | ||||||
|  |       // Add future migrations here (v4.3, v4.4, etc.) | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     // Sort by version order to ensure they run in sequence | ||||||
|  |     this.migrations.sort((a, b) => a.getVersionOrder() - b.getVersionOrder()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Run all applicable migrations on the config | ||||||
|  |    * | ||||||
|  |    * @param config - Raw configuration object to migrate | ||||||
|  |    * @returns Migrated configuration and whether migrations ran | ||||||
|  |    */ | ||||||
|  |   async run( | ||||||
|  |     config: Record<string, unknown>, | ||||||
|  |   ): Promise<{ config: Record<string, unknown>; migrated: boolean }> { | ||||||
|  |     let currentConfig = config; | ||||||
|  |     let anyMigrationsRan = false; | ||||||
|  |  | ||||||
|  |     for (const migration of this.migrations) { | ||||||
|  |       const shouldRun = await migration.shouldRun(currentConfig); | ||||||
|  |  | ||||||
|  |       if (shouldRun) { | ||||||
|  |         // Only show "checking" message when we actually need to migrate | ||||||
|  |         if (!anyMigrationsRan) { | ||||||
|  |           logger.dim('Checking for required config migrations...'); | ||||||
|  |         } | ||||||
|  |         logger.info(`Running ${migration.getName()}...`); | ||||||
|  |         currentConfig = await migration.migrate(currentConfig); | ||||||
|  |         anyMigrationsRan = true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (anyMigrationsRan) { | ||||||
|  |       logger.success('Configuration migrations complete'); | ||||||
|  |     } else { | ||||||
|  |       logger.success('config format ok'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       config: currentConfig, | ||||||
|  |       migrated: anyMigrationsRan, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get all registered migrations | ||||||
|  |    * | ||||||
|  |    * @returns Array of all migrations sorted by order | ||||||
|  |    */ | ||||||
|  |   getMigrations(): BaseMigration[] { | ||||||
|  |     return [...this.migrations]; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								ts/migrations/migration-v1-to-v2.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								ts/migrations/migration-v1-to-v2.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | import { BaseMigration } from './base-migration.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Migration from v1 (single SNMP config) to v2 (upsDevices array) | ||||||
|  |  * | ||||||
|  |  * Detects old format: | ||||||
|  |  * { | ||||||
|  |  *   snmp: { ... }, | ||||||
|  |  *   thresholds: { ... }, | ||||||
|  |  *   checkInterval: 30000 | ||||||
|  |  * } | ||||||
|  |  * | ||||||
|  |  * Converts to: | ||||||
|  |  * { | ||||||
|  |  *   version: "2.0", | ||||||
|  |  *   upsDevices: [{ id: "default", name: "Default UPS", snmp: ..., thresholds: ... }], | ||||||
|  |  *   groups: [], | ||||||
|  |  *   checkInterval: 30000 | ||||||
|  |  * } | ||||||
|  |  */ | ||||||
|  | export class MigrationV1ToV2 extends BaseMigration { | ||||||
|  |   readonly fromVersion = '1.x'; | ||||||
|  |   readonly toVersion = '2.0'; | ||||||
|  |  | ||||||
|  |   async shouldRun(config: any): Promise<boolean> { | ||||||
|  |     // V1 format has snmp field directly at root, no upsDevices or upsList | ||||||
|  |     return !!config.snmp && !config.upsDevices && !config.upsList; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async migrate(config: any): Promise<any> { | ||||||
|  |     logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`); | ||||||
|  |  | ||||||
|  |     const migrated = { | ||||||
|  |       version: this.toVersion, | ||||||
|  |       upsDevices: [ | ||||||
|  |         { | ||||||
|  |           id: 'default', | ||||||
|  |           name: 'Default UPS', | ||||||
|  |           snmp: config.snmp, | ||||||
|  |           thresholds: config.thresholds || { | ||||||
|  |             battery: 60, | ||||||
|  |             runtime: 20, | ||||||
|  |           }, | ||||||
|  |           groups: [], | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |       groups: [], | ||||||
|  |       checkInterval: config.checkInterval || 30000, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     logger.success(`${this.getName()}: Migration complete`); | ||||||
|  |     return migrated; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										118
									
								
								ts/migrations/migration-v3-to-v4.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								ts/migrations/migration-v3-to-v4.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | import { BaseMigration } from './base-migration.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Migration from v3 (upsList) to v4 (upsDevices) | ||||||
|  |  * | ||||||
|  |  * Transforms v3 format with flat SNMP config: | ||||||
|  |  * { | ||||||
|  |  *   upsList: [ | ||||||
|  |  *     { | ||||||
|  |  *       id: "ups-1", | ||||||
|  |  *       name: "UPS 1", | ||||||
|  |  *       host: "192.168.1.1", | ||||||
|  |  *       port: 161, | ||||||
|  |  *       community: "public", | ||||||
|  |  *       version: "1"  // string | ||||||
|  |  *     } | ||||||
|  |  *   ] | ||||||
|  |  * } | ||||||
|  |  * | ||||||
|  |  * To v4 format with nested SNMP config: | ||||||
|  |  * { | ||||||
|  |  *   version: "4.0", | ||||||
|  |  *   upsDevices: [ | ||||||
|  |  *     { | ||||||
|  |  *       id: "ups-1", | ||||||
|  |  *       name: "UPS 1", | ||||||
|  |  *       snmp: { | ||||||
|  |  *         host: "192.168.1.1", | ||||||
|  |  *         port: 161, | ||||||
|  |  *         community: "public", | ||||||
|  |  *         version: 1,  // number | ||||||
|  |  *         timeout: 5000 | ||||||
|  |  *       }, | ||||||
|  |  *       thresholds: { battery: 60, runtime: 20 }, | ||||||
|  |  *       groups: [] | ||||||
|  |  *     } | ||||||
|  |  *   ] | ||||||
|  |  * } | ||||||
|  |  */ | ||||||
|  | export class MigrationV3ToV4 extends BaseMigration { | ||||||
|  |   readonly fromVersion = '3.x'; | ||||||
|  |   readonly toVersion = '4.0'; | ||||||
|  |  | ||||||
|  |   async shouldRun(config: any): Promise<boolean> { | ||||||
|  |     // V3 format has upsList OR has upsDevices with flat structure (host at top level) | ||||||
|  |     if (config.upsList && !config.upsDevices) { | ||||||
|  |       return true; // Classic v3 with upsList | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Check if upsDevices exists but has flat structure (v3 format) | ||||||
|  |     if (config.upsDevices && config.upsDevices.length > 0) { | ||||||
|  |       const firstDevice = config.upsDevices[0]; | ||||||
|  |       // V3 has host at top level, v4 has it nested in snmp object | ||||||
|  |       return !!firstDevice.host && !firstDevice.snmp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async migrate(config: any): Promise<any> { | ||||||
|  |     logger.info(`${this.getName()}: Migrating v3 config to v4 format...`); | ||||||
|  |     logger.dim(`  - Restructuring UPS devices (flat → nested snmp config)`); | ||||||
|  |  | ||||||
|  |     // Get devices from either upsList or upsDevices (for partially migrated configs) | ||||||
|  |     const sourceDevices = config.upsList || config.upsDevices; | ||||||
|  |  | ||||||
|  |     // Transform each UPS device from v3 flat structure to v4 nested structure | ||||||
|  |     const transformedDevices = sourceDevices.map((device: any) => { | ||||||
|  |       // Build SNMP config object | ||||||
|  |       const snmpConfig: any = { | ||||||
|  |         host: device.host, | ||||||
|  |         port: device.port || 161, | ||||||
|  |         version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version, | ||||||
|  |         timeout: device.timeout || 5000, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // Add SNMPv1/v2c fields | ||||||
|  |       if (device.community) { | ||||||
|  |         snmpConfig.community = device.community; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Add SNMPv3 fields | ||||||
|  |       if (device.securityLevel) snmpConfig.securityLevel = device.securityLevel; | ||||||
|  |       if (device.username) snmpConfig.username = device.username; | ||||||
|  |       if (device.authProtocol) snmpConfig.authProtocol = device.authProtocol; | ||||||
|  |       if (device.authKey) snmpConfig.authKey = device.authKey; | ||||||
|  |       if (device.privProtocol) snmpConfig.privProtocol = device.privProtocol; | ||||||
|  |       if (device.privKey) snmpConfig.privKey = device.privKey; | ||||||
|  |  | ||||||
|  |       // Add UPS model if present | ||||||
|  |       if (device.upsModel) snmpConfig.upsModel = device.upsModel; | ||||||
|  |       if (device.customOIDs) snmpConfig.customOIDs = device.customOIDs; | ||||||
|  |  | ||||||
|  |       // Return v4 format with nested structure | ||||||
|  |       return { | ||||||
|  |         id: device.id, | ||||||
|  |         name: device.name, | ||||||
|  |         snmp: snmpConfig, | ||||||
|  |         thresholds: device.thresholds || { | ||||||
|  |           battery: 60, | ||||||
|  |           runtime: 20, | ||||||
|  |         }, | ||||||
|  |         groups: device.groups || [], | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const migrated = { | ||||||
|  |       version: this.toVersion, | ||||||
|  |       upsDevices: transformedDevices, | ||||||
|  |       groups: config.groups || [], | ||||||
|  |       checkInterval: config.checkInterval || 30000, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`); | ||||||
|  |     return migrated; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										127
									
								
								ts/migrations/migration-v4.0-to-v4.1.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								ts/migrations/migration-v4.0-to-v4.1.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | |||||||
|  | import { BaseMigration } from './base-migration.ts'; | ||||||
|  | import { logger } from '../logger.ts'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Migration from v4.0 to v4.1 | ||||||
|  |  * | ||||||
|  |  * Major changes: | ||||||
|  |  * 1. Moves thresholds from UPS level to action level | ||||||
|  |  * 2. Creates default shutdown action for UPS devices that had thresholds | ||||||
|  |  * 3. Adds empty actions array to UPS devices without actions | ||||||
|  |  * 4. Adds empty actions array to groups | ||||||
|  |  * | ||||||
|  |  * Transforms v4.0 format (with UPS-level thresholds): | ||||||
|  |  * { | ||||||
|  |  *   version: "4.0", | ||||||
|  |  *   upsDevices: [ | ||||||
|  |  *     { | ||||||
|  |  *       id: "ups-1", | ||||||
|  |  *       name: "UPS 1", | ||||||
|  |  *       snmp: {...}, | ||||||
|  |  *       thresholds: { battery: 60, runtime: 20 },  // UPS-level | ||||||
|  |  *       groups: [] | ||||||
|  |  *     } | ||||||
|  |  *   ] | ||||||
|  |  * } | ||||||
|  |  * | ||||||
|  |  * To v4.1 format (with action-level thresholds): | ||||||
|  |  * { | ||||||
|  |  *   version: "4.1", | ||||||
|  |  *   upsDevices: [ | ||||||
|  |  *     { | ||||||
|  |  *       id: "ups-1", | ||||||
|  |  *       name: "UPS 1", | ||||||
|  |  *       snmp: {...}, | ||||||
|  |  *       groups: [], | ||||||
|  |  *       actions: [  // Thresholds moved here | ||||||
|  |  *         { | ||||||
|  |  *           type: "shutdown", | ||||||
|  |  *           thresholds: { battery: 60, runtime: 20 }, | ||||||
|  |  *           triggerMode: "onlyThresholds", | ||||||
|  |  *           shutdownDelay: 5 | ||||||
|  |  *         } | ||||||
|  |  *       ] | ||||||
|  |  *     } | ||||||
|  |  *   ] | ||||||
|  |  * } | ||||||
|  |  */ | ||||||
|  | export class MigrationV4_0ToV4_1 extends BaseMigration { | ||||||
|  |   readonly fromVersion = '4.0'; | ||||||
|  |   readonly toVersion = '4.1'; | ||||||
|  |  | ||||||
|  |   async shouldRun(config: Record<string, unknown>): Promise<boolean> { | ||||||
|  |     // Run if config is version 4.0 | ||||||
|  |     if (config.version === '4.0') { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Also run if config has upsDevices with thresholds at UPS level (v4.0 format) | ||||||
|  |     if (Array.isArray(config.upsDevices) && config.upsDevices.length > 0) { | ||||||
|  |       const firstDevice = config.upsDevices[0] as Record<string, unknown>; | ||||||
|  |       // v4.0 has thresholds at UPS level, v4.1 has them in actions | ||||||
|  |       return firstDevice.thresholds !== undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async migrate(config: Record<string, unknown>): Promise<Record<string, unknown>> { | ||||||
|  |     logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`); | ||||||
|  |     logger.dim(`  - Moving thresholds from UPS level to action level`); | ||||||
|  |     logger.dim(`  - Creating default shutdown actions from existing thresholds`); | ||||||
|  |  | ||||||
|  |     // Migrate UPS devices | ||||||
|  |     const devices = (config.upsDevices as Array<Record<string, unknown>>) || []; | ||||||
|  |     const migratedDevices = devices.map((device) => { | ||||||
|  |       const migrated: Record<string, unknown> = { | ||||||
|  |         id: device.id, | ||||||
|  |         name: device.name, | ||||||
|  |         snmp: device.snmp, | ||||||
|  |         groups: device.groups || [], | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // If device has thresholds at UPS level, convert to shutdown action | ||||||
|  |       const deviceThresholds = device.thresholds as { battery: number; runtime: number } | undefined; | ||||||
|  |       if (deviceThresholds) { | ||||||
|  |         migrated.actions = [ | ||||||
|  |           { | ||||||
|  |             type: 'shutdown', | ||||||
|  |             thresholds: { | ||||||
|  |               battery: deviceThresholds.battery, | ||||||
|  |               runtime: deviceThresholds.runtime, | ||||||
|  |             }, | ||||||
|  |             triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation) | ||||||
|  |             shutdownDelay: 5, // Default delay | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  |         logger.dim( | ||||||
|  |           `    → ${device.name}: Created shutdown action (battery: ${deviceThresholds.battery}%, runtime: ${deviceThresholds.runtime}min)`, | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         // No thresholds, just add empty actions array | ||||||
|  |         migrated.actions = device.actions || []; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return migrated; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Add actions to groups | ||||||
|  |     const groups = (config.groups as Array<Record<string, unknown>>) || []; | ||||||
|  |     const migratedGroups = groups.map((group) => ({ | ||||||
|  |       ...group, | ||||||
|  |       actions: group.actions || [], | ||||||
|  |     })); | ||||||
|  |  | ||||||
|  |     const result = { | ||||||
|  |       version: this.toVersion, | ||||||
|  |       upsDevices: migratedDevices, | ||||||
|  |       groups: migratedGroups, | ||||||
|  |       checkInterval: config.checkInterval || 30000, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     logger.success( | ||||||
|  |       `${this.getName()}: Migration complete (${migratedDevices.length} devices, ${migratedGroups.length} groups updated)`, | ||||||
|  |     ); | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								ts/nupst.ts
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								ts/nupst.ts
									
									
									
									
									
								
							| @@ -1,12 +1,13 @@ | |||||||
| import { NupstSnmp } from './snmp/manager.js'; | import { NupstSnmp } from './snmp/manager.ts'; | ||||||
| import { NupstDaemon } from './daemon.js'; | import { NupstDaemon } from './daemon.ts'; | ||||||
| import { NupstSystemd } from './systemd.js'; | import { NupstSystemd } from './systemd.ts'; | ||||||
| import { commitinfo } from './00_commitinfo_data.js'; | import { commitinfo } from './00_commitinfo_data.ts'; | ||||||
| import { logger } from './logger.js'; | import { logger } from './logger.ts'; | ||||||
| import { UpsHandler } from './cli/ups-handler.js'; | import { UpsHandler } from './cli/ups-handler.ts'; | ||||||
| import { GroupHandler } from './cli/group-handler.js'; | import { GroupHandler } from './cli/group-handler.ts'; | ||||||
| import { ServiceHandler } from './cli/service-handler.js'; | import { ServiceHandler } from './cli/service-handler.ts'; | ||||||
| import * as https from 'https'; | import { ActionHandler } from './cli/action-handler.ts'; | ||||||
|  | import * as https from 'node:https'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Main Nupst class that coordinates all components |  * Main Nupst class that coordinates all components | ||||||
| @@ -19,6 +20,7 @@ export class Nupst { | |||||||
|   private readonly upsHandler: UpsHandler; |   private readonly upsHandler: UpsHandler; | ||||||
|   private readonly groupHandler: GroupHandler; |   private readonly groupHandler: GroupHandler; | ||||||
|   private readonly serviceHandler: ServiceHandler; |   private readonly serviceHandler: ServiceHandler; | ||||||
|  |   private readonly actionHandler: ActionHandler; | ||||||
|   private updateAvailable: boolean = false; |   private updateAvailable: boolean = false; | ||||||
|   private latestVersion: string = ''; |   private latestVersion: string = ''; | ||||||
|  |  | ||||||
| @@ -36,6 +38,7 @@ export class Nupst { | |||||||
|     this.upsHandler = new UpsHandler(this); |     this.upsHandler = new UpsHandler(this); | ||||||
|     this.groupHandler = new GroupHandler(this); |     this.groupHandler = new GroupHandler(this); | ||||||
|     this.serviceHandler = new ServiceHandler(this); |     this.serviceHandler = new ServiceHandler(this); | ||||||
|  |     this.actionHandler = new ActionHandler(this); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -80,6 +83,13 @@ export class Nupst { | |||||||
|     return this.serviceHandler; |     return this.serviceHandler; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Get the Action handler for action management | ||||||
|  |    */ | ||||||
|  |   public getActionHandler(): ActionHandler { | ||||||
|  |     return this.actionHandler; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Get the current version of NUPST |    * Get the current version of NUPST | ||||||
|    * @returns The current version string |    * @returns The current version string | ||||||
| @@ -103,7 +113,9 @@ export class Nupst { | |||||||
|  |  | ||||||
|       return this.updateAvailable; |       return this.updateAvailable; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Error checking for updates: ${error.message}`); |       logger.error( | ||||||
|  |         `Error checking for updates: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -113,14 +125,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, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -128,7 +140,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', | ||||||
| @@ -136,8 +148,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) => { | ||||||
| @@ -176,8 +188,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; | ||||||
| @@ -208,7 +220,7 @@ export class Nupst { | |||||||
|       logger.logBoxLine('Checking for updates...'); |       logger.logBoxLine('Checking for updates...'); | ||||||
|  |  | ||||||
|       // We can't end the box yet since we're in an async operation |       // We can't end the box yet since we're in an async operation | ||||||
|       this.checkForUpdates().then(updateAvailable => { |       this.checkForUpdates().then((updateAvailable) => { | ||||||
|         if (updateAvailable) { |         if (updateAvailable) { | ||||||
|           logger.logBoxLine(`Update Available: ${this.latestVersion}`); |           logger.logBoxLine(`Update Available: ${this.latestVersion}`); | ||||||
|           logger.logBoxLine('Run "sudo nupst update" to update'); |           logger.logBoxLine('Run "sudo nupst update" to update'); | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| // Re-export all public types | // Re-export all public types | ||||||
| export type { IUpsStatus, IOidSet, TUpsModel, ISnmpConfig } from './types.js'; | export type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts'; | ||||||
|  |  | ||||||
| // Re-export the SNMP manager class | // Re-export the SNMP manager class | ||||||
| export { NupstSnmp } from './manager.js'; | export { NupstSnmp } from './manager.ts'; | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as snmp from 'net-snmp'; | import * as snmp from 'npm:net-snmp@3.20.0'; | ||||||
| import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js'; | import { Buffer } from 'node:buffer'; | ||||||
| import { UpsOidSets } from './oid-sets.js'; | import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts'; | ||||||
|  | import { UpsOidSets } from './oid-sets.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for SNMP communication with UPS devices |  * Class for SNMP communication with UPS devices | ||||||
| @@ -87,14 +88,16 @@ export class NupstSnmp { | |||||||
|    * @param retryCount Current retry count (unused in this implementation) |    * @param retryCount Current retry count (unused in this implementation) | ||||||
|    * @returns Promise resolving to the SNMP response value |    * @returns Promise resolving to the SNMP response value | ||||||
|    */ |    */ | ||||||
|   public async snmpGet( |   public snmpGet( | ||||||
|     oid: string, |     oid: string, | ||||||
|     config = this.DEFAULT_CONFIG, |     config = this.DEFAULT_CONFIG, | ||||||
|     retryCount = 0 |     retryCount = 0, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`); |         console.log( | ||||||
|  |           `Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`, | ||||||
|  |         ); | ||||||
|         console.log('Using community:', config.community); |         console.log('Using community:', config.community); | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -105,7 +108,7 @@ export class NupstSnmp { | |||||||
|         timeout: config.timeout, |         timeout: config.timeout, | ||||||
|         transport: 'udp4', |         transport: 'udp4', | ||||||
|         idBitsSize: 32, |         idBitsSize: 32, | ||||||
|         context: config.context || '' |         context: config.context || '', | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       // Set version based on config |       // Set version based on config | ||||||
| @@ -127,7 +130,7 @@ export class NupstSnmp { | |||||||
|  |  | ||||||
|         // Create the user object with required structure for net-snmp |         // Create the user object with required structure for net-snmp | ||||||
|         const user: any = { |         const user: any = { | ||||||
|           name: config.username || '' |           name: config.username || '', | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         // Set security level |         // Set security level | ||||||
| @@ -190,11 +193,13 @@ export class NupstSnmp { | |||||||
|         if (this.debug) { |         if (this.debug) { | ||||||
|           console.log('SNMPv3 user configuration:', { |           console.log('SNMPv3 user configuration:', { | ||||||
|             name: user.name, |             name: user.name, | ||||||
|             level: Object.keys(snmp.SecurityLevel).find(key => snmp.SecurityLevel[key] === user.level), |             level: Object.keys(snmp.SecurityLevel).find((key) => | ||||||
|  |               snmp.SecurityLevel[key] === user.level | ||||||
|  |             ), | ||||||
|             authProtocol: user.authProtocol ? 'Set' : 'Not Set', |             authProtocol: user.authProtocol ? 'Set' : 'Not Set', | ||||||
|             authKey: user.authKey ? 'Set' : 'Not Set', |             authKey: user.authKey ? 'Set' : 'Not Set', | ||||||
|             privProtocol: user.privProtocol ? 'Set' : 'Not Set', |             privProtocol: user.privProtocol ? 'Set' : 'Not Set', | ||||||
|             privKey: user.privKey ? 'Set' : 'Not Set' |             privKey: user.privKey ? 'Set' : 'Not Set', | ||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -229,9 +234,11 @@ export class NupstSnmp { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Check for SNMP errors in the response |         // Check for SNMP errors in the response | ||||||
|         if (varbinds[0].type === snmp.ObjectType.NoSuchObject || |         if ( | ||||||
|  |           varbinds[0].type === snmp.ObjectType.NoSuchObject || | ||||||
|           varbinds[0].type === snmp.ObjectType.NoSuchInstance || |           varbinds[0].type === snmp.ObjectType.NoSuchInstance || | ||||||
|             varbinds[0].type === snmp.ObjectType.EndOfMibView) { |           varbinds[0].type === snmp.ObjectType.EndOfMibView | ||||||
|  |         ) { | ||||||
|           if (this.debug) { |           if (this.debug) { | ||||||
|             console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]); |             console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]); | ||||||
|           } |           } | ||||||
| @@ -245,7 +252,7 @@ export class NupstSnmp { | |||||||
|         // Handle specific types that might need conversion |         // Handle specific types that might need conversion | ||||||
|         if (Buffer.isBuffer(value)) { |         if (Buffer.isBuffer(value)) { | ||||||
|           // If value is a Buffer, try to convert it to a string if it's printable ASCII |           // If value is a Buffer, try to convert it to a string if it's printable ASCII | ||||||
|           const isPrintableAscii = value.every(byte => byte >= 32 && byte <= 126); |           const isPrintableAscii = value.every((byte: number) => byte >= 32 && byte <= 126); | ||||||
|           if (isPrintableAscii) { |           if (isPrintableAscii) { | ||||||
|             value = value.toString(); |             value = value.toString(); | ||||||
|           } |           } | ||||||
| @@ -258,7 +265,7 @@ export class NupstSnmp { | |||||||
|           console.log('SNMP response:', { |           console.log('SNMP response:', { | ||||||
|             oid: varbinds[0].oid, |             oid: varbinds[0].oid, | ||||||
|             type: varbinds[0].type, |             type: varbinds[0].type, | ||||||
|             value: value |             value: value, | ||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -301,9 +308,21 @@ export class NupstSnmp { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Get all values with independent retry logic |       // Get all values with independent retry logic | ||||||
|       const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config); |       const powerStatusValue = await this.getSNMPValueWithRetry( | ||||||
|       const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0; |         this.activeOIDs.POWER_STATUS, | ||||||
|       const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0; |         'power status', | ||||||
|  |         config, | ||||||
|  |       ); | ||||||
|  |       const batteryCapacity = await this.getSNMPValueWithRetry( | ||||||
|  |         this.activeOIDs.BATTERY_CAPACITY, | ||||||
|  |         'battery capacity', | ||||||
|  |         config, | ||||||
|  |       ) || 0; | ||||||
|  |       const batteryRuntime = await this.getSNMPValueWithRetry( | ||||||
|  |         this.activeOIDs.BATTERY_RUNTIME, | ||||||
|  |         'battery runtime', | ||||||
|  |         config, | ||||||
|  |       ) || 0; | ||||||
|  |  | ||||||
|       // Determine power status - handle different values for different UPS models |       // Determine power status - handle different values for different UPS models | ||||||
|       const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue); |       const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue); | ||||||
| @@ -335,10 +354,15 @@ 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)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -352,7 +376,7 @@ export class NupstSnmp { | |||||||
|   private async getSNMPValueWithRetry( |   private async getSNMPValueWithRetry( | ||||||
|     oid: string, |     oid: string, | ||||||
|     description: string, |     description: string, | ||||||
|     config: ISnmpConfig |     config: ISnmpConfig, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|     if (oid === '') { |     if (oid === '') { | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
| @@ -373,7 +397,10 @@ export class NupstSnmp { | |||||||
|       return value; |       return value; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.error(`Error getting ${description}:`, error.message); |         console.error( | ||||||
|  |           `Error getting ${description}:`, | ||||||
|  |           error instanceof Error ? error.message : String(error), | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // If we're using SNMPv3, try with different security levels |       // If we're using SNMPv3, try with different security levels | ||||||
| @@ -404,7 +431,7 @@ export class NupstSnmp { | |||||||
|   private async tryFallbackSecurityLevels( |   private async tryFallbackSecurityLevels( | ||||||
|     oid: string, |     oid: string, | ||||||
|     description: string, |     description: string, | ||||||
|     config: ISnmpConfig |     config: ISnmpConfig, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|     if (this.debug) { |     if (this.debug) { | ||||||
|       console.log(`Retrying ${description} with fallback security level...`); |       console.log(`Retrying ${description} with fallback security level...`); | ||||||
| @@ -424,7 +451,10 @@ export class NupstSnmp { | |||||||
|         return value; |         return value; | ||||||
|       } catch (retryError) { |       } catch (retryError) { | ||||||
|         if (this.debug) { |         if (this.debug) { | ||||||
|           console.error(`Retry failed for ${description}:`, retryError.message); |           console.error( | ||||||
|  |             `Retry failed for ${description}:`, | ||||||
|  |             retryError instanceof Error ? retryError.message : String(retryError), | ||||||
|  |           ); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -443,7 +473,10 @@ export class NupstSnmp { | |||||||
|         return value; |         return value; | ||||||
|       } catch (retryError) { |       } catch (retryError) { | ||||||
|         if (this.debug) { |         if (this.debug) { | ||||||
|           console.error(`Retry failed for ${description}:`, retryError.message); |           console.error( | ||||||
|  |             `Retry failed for ${description}:`, | ||||||
|  |             retryError instanceof Error ? retryError.message : String(retryError), | ||||||
|  |           ); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -461,14 +494,16 @@ export class NupstSnmp { | |||||||
|   private async tryStandardOids( |   private async tryStandardOids( | ||||||
|     oid: string, |     oid: string, | ||||||
|     description: string, |     description: string, | ||||||
|     config: ISnmpConfig |     config: ISnmpConfig, | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|     try { |     try { | ||||||
|       // Try RFC 1628 standard UPS MIB OIDs |       // Try RFC 1628 standard UPS MIB OIDs | ||||||
|       const standardOIDs = UpsOidSets.getStandardOids(); |       const standardOIDs = UpsOidSets.getStandardOids(); | ||||||
|  |  | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`); |         console.log( | ||||||
|  |           `Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`, | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const standardValue = await this.snmpGet(standardOIDs[description], config); |       const standardValue = await this.snmpGet(standardOIDs[description], config); | ||||||
| @@ -478,7 +513,10 @@ export class NupstSnmp { | |||||||
|       return standardValue; |       return standardValue; | ||||||
|     } catch (stdError) { |     } catch (stdError) { | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.error(`Standard OID retry failed for ${description}:`, stdError.message); |         console.error( | ||||||
|  |           `Standard OID retry failed for ${description}:`, | ||||||
|  |           stdError instanceof Error ? stdError.message : String(stdError), | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -487,46 +525,36 @@ export class NupstSnmp { | |||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Determine power status based on UPS model and raw value |    * Determine power status based on UPS model and raw value | ||||||
|  |    * Uses the value mappings defined in the OID sets | ||||||
|    * @param upsModel UPS model |    * @param upsModel UPS model | ||||||
|    * @param powerStatusValue Raw power status value |    * @param powerStatusValue Raw power status value | ||||||
|    * @returns Standardized power status |    * @returns Standardized power status | ||||||
|    */ |    */ | ||||||
|   private determinePowerStatus( |   private determinePowerStatus( | ||||||
|     upsModel: TUpsModel | undefined, |     upsModel: TUpsModel | undefined, | ||||||
|     powerStatusValue: number |     powerStatusValue: number, | ||||||
|   ): 'online' | 'onBattery' | 'unknown' { |   ): 'online' | 'onBattery' | 'unknown' { | ||||||
|     if (upsModel === 'cyberpower') { |     // Get the OID set for this UPS model | ||||||
|       // CyberPower RMCARD205: upsBaseOutputStatus values |     if (upsModel && upsModel !== 'custom') { | ||||||
|       // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. |       const oidSet = UpsOidSets.getOidSet(upsModel); | ||||||
|       if (powerStatusValue === 2) { |  | ||||||
|  |       // Use the value mappings if available | ||||||
|  |       if (oidSet.POWER_STATUS_VALUES) { | ||||||
|  |         if (powerStatusValue === oidSet.POWER_STATUS_VALUES.online) { | ||||||
|           return 'online'; |           return 'online'; | ||||||
|       } else if (powerStatusValue === 3) { |         } else if (powerStatusValue === oidSet.POWER_STATUS_VALUES.onBattery) { | ||||||
|           return 'onBattery'; |           return 'onBattery'; | ||||||
|         } |         } | ||||||
|     } else if (upsModel === 'eaton') { |       } | ||||||
|       // Eaton UPS: xupsOutputSource values |     } | ||||||
|       // 3=normal/mains, 5=battery, etc. |  | ||||||
|  |     // Fallback for custom or undefined models (RFC 1628 standard) | ||||||
|  |     // upsOutputSource: 3=normal (mains), 5=battery | ||||||
|     if (powerStatusValue === 3) { |     if (powerStatusValue === 3) { | ||||||
|       return 'online'; |       return 'online'; | ||||||
|     } else if (powerStatusValue === 5) { |     } else if (powerStatusValue === 5) { | ||||||
|       return 'onBattery'; |       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'; |     return 'unknown'; | ||||||
|   } |   } | ||||||
| @@ -539,7 +567,7 @@ export class NupstSnmp { | |||||||
|    */ |    */ | ||||||
|   private processRuntimeValue( |   private processRuntimeValue( | ||||||
|     upsModel: TUpsModel | undefined, |     upsModel: TUpsModel | undefined, | ||||||
|     batteryRuntime: number |     batteryRuntime: number, | ||||||
|   ): number { |   ): number { | ||||||
|     if (this.debug) { |     if (this.debug) { | ||||||
|       console.log('Raw runtime value:', batteryRuntime); |       console.log('Raw runtime value:', batteryRuntime); | ||||||
| @@ -549,14 +577,18 @@ export class NupstSnmp { | |||||||
|       // CyberPower: TimeTicks is in 1/100 seconds, convert to minutes |       // CyberPower: TimeTicks is in 1/100 seconds, convert to minutes | ||||||
|       const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute |       const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`); |         console.log( | ||||||
|  |           `Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`, | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|       return minutes; |       return minutes; | ||||||
|     } else if (upsModel === 'eaton' && batteryRuntime > 0) { |     } else if (upsModel === 'eaton' && batteryRuntime > 0) { | ||||||
|       // Eaton: Runtime is in seconds, convert to minutes |       // Eaton: Runtime is in seconds, convert to minutes | ||||||
|       const minutes = Math.floor(batteryRuntime / 60); |       const minutes = Math.floor(batteryRuntime / 60); | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`); |         console.log( | ||||||
|  |           `Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`, | ||||||
|  |         ); | ||||||
|       } |       } | ||||||
|       return minutes; |       return minutes; | ||||||
|     } else if (batteryRuntime > 10000) { |     } else if (batteryRuntime > 10000) { | ||||||
|   | |||||||
| @@ -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 | ||||||
| @@ -11,37 +11,57 @@ export class UpsOidSets { | |||||||
|   private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = { |   private static readonly UPS_OID_SETS: Record<TUpsModel, IOidSet> = { | ||||||
|     // Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11) |     // Cyberpower OIDs for RMCARD205 (based on CyberPower_MIB_v2.11) | ||||||
|     cyberpower: { |     cyberpower: { | ||||||
|       POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus (2=online, 3=on battery) |       POWER_STATUS: '1.3.6.1.4.1.3808.1.1.1.4.1.1.0', // upsBaseOutputStatus | ||||||
|       BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage) |       BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage) | ||||||
|       BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks) |       BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks) | ||||||
|  |       POWER_STATUS_VALUES: { | ||||||
|  |         online: 2, // upsBaseOutputStatus: 2=onLine | ||||||
|  |         onBattery: 3, // upsBaseOutputStatus: 3=onBattery | ||||||
|  |       }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     // APC OIDs |     // APC OIDs | ||||||
|     apc: { |     apc: { | ||||||
|       POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery) |       POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // upsBasicOutputStatus | ||||||
|       BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage |       BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage | ||||||
|       BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes |       BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes | ||||||
|  |       POWER_STATUS_VALUES: { | ||||||
|  |         online: 2, // upsBasicOutputStatus: 2=onLine | ||||||
|  |         onBattery: 3, // upsBasicOutputStatus: 3=onBattery | ||||||
|  |       }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     // Eaton OIDs |     // Eaton OIDs | ||||||
|     eaton: { |     eaton: { | ||||||
|       POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery) |       POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource | ||||||
|       BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage) |       BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage) | ||||||
|       BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds) |       BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds) | ||||||
|  |       POWER_STATUS_VALUES: { | ||||||
|  |         online: 3, // xupsOutputSource: 3=normal (mains power) | ||||||
|  |         onBattery: 5, // xupsOutputSource: 5=battery | ||||||
|  |       }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     // TrippLite OIDs |     // TrippLite OIDs | ||||||
|     tripplite: { |     tripplite: { | ||||||
|       POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status |       POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // tlUpsOutputSource | ||||||
|       BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage |       BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage | ||||||
|       BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes |       BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes | ||||||
|  |       POWER_STATUS_VALUES: { | ||||||
|  |         online: 2, // tlUpsOutputSource: 2=normal (mains power) | ||||||
|  |         onBattery: 3, // tlUpsOutputSource: 3=onBattery | ||||||
|  |       }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     // Liebert/Vertiv OIDs |     // Liebert/Vertiv OIDs | ||||||
|     liebert: { |     liebert: { | ||||||
|       POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status |       POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // lgpPwrOutputSource | ||||||
|       BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage |       BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage | ||||||
|       BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes |       BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes | ||||||
|  |       POWER_STATUS_VALUES: { | ||||||
|  |         online: 2, // lgpPwrOutputSource: 2=normal (mains power) | ||||||
|  |         onBattery: 3, // lgpPwrOutputSource: 3=onBattery | ||||||
|  |       }, | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     // Custom OIDs (to be provided by the user) |     // Custom OIDs (to be provided by the user) | ||||||
| @@ -49,7 +69,7 @@ export class UpsOidSets { | |||||||
|       POWER_STATUS: '', |       POWER_STATUS: '', | ||||||
|       BATTERY_CAPACITY: '', |       BATTERY_CAPACITY: '', | ||||||
|       BATTERY_RUNTIME: '', |       BATTERY_RUNTIME: '', | ||||||
|     } |     }, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -69,7 +89,7 @@ export class UpsOidSets { | |||||||
|     return { |     return { | ||||||
|       'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource |       'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource | ||||||
|       'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining |       'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining | ||||||
|       'battery runtime': '1.3.6.1.2.1.33.1.2.3.0'    // upsEstimatedMinutesRemaining |       'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -2,6 +2,8 @@ | |||||||
|  * Type definitions for SNMP module |  * Type definitions for SNMP module | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | import { Buffer } from 'node:buffer'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * UPS status interface |  * UPS status interface | ||||||
|  */ |  */ | ||||||
| @@ -26,6 +28,13 @@ export interface IOidSet { | |||||||
|   BATTERY_CAPACITY: string; |   BATTERY_CAPACITY: string; | ||||||
|   /** OID for battery runtime */ |   /** OID for battery runtime */ | ||||||
|   BATTERY_RUNTIME: string; |   BATTERY_RUNTIME: string; | ||||||
|  |   /** Power status value mappings */ | ||||||
|  |   POWER_STATUS_VALUES?: { | ||||||
|  |     /** SNMP value that indicates UPS is online (on AC power) */ | ||||||
|  |     online: number; | ||||||
|  |     /** SNMP value that indicates UPS is on battery */ | ||||||
|  |     onBattery: number; | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
							
								
								
									
										351
									
								
								ts/systemd.ts
									
									
									
									
									
								
							
							
						
						
									
										351
									
								
								ts/systemd.ts
									
									
									
									
									
								
							| @@ -1,7 +1,10 @@ | |||||||
| import { promises as fs } from 'fs'; | import process from 'node:process'; | ||||||
| import { execSync } from 'child_process'; | import { promises as fs } from 'node:fs'; | ||||||
| import { NupstDaemon } from './daemon.js'; | import { execSync } from 'node:child_process'; | ||||||
| import { logger } from './logger.js'; | import { NupstDaemon, type IUpsConfig } from './daemon.ts'; | ||||||
|  | import { NupstSnmp } from './snmp/manager.ts'; | ||||||
|  | import { logger } from './logger.ts'; | ||||||
|  | import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for managing systemd service |  * Class for managing systemd service | ||||||
| @@ -14,17 +17,17 @@ export class NupstSystemd { | |||||||
|  |  | ||||||
|   /** Template for the systemd service file */ |   /** Template for the systemd service file */ | ||||||
|   private readonly serviceTemplate = `[Unit] |   private readonly serviceTemplate = `[Unit] | ||||||
| Description=Node.js UPS Shutdown Tool for Multiple UPS Devices | Description=NUPST - Deno-powered UPS Monitoring Tool | ||||||
| After=network.target | After=network.target | ||||||
|  |  | ||||||
| [Service] | [Service] | ||||||
| ExecStart=/opt/nupst/bin/nupst daemon-start | ExecStart=/usr/local/bin/nupst service start-daemon | ||||||
| Restart=always | Restart=always | ||||||
|  | RestartSec=10 | ||||||
| User=root | User=root | ||||||
| Group=root | Group=root | ||||||
| Environment=PATH=/usr/bin:/usr/local/bin | Environment=PATH=/usr/bin:/usr/local/bin | ||||||
| Environment=NODE_ENV=production | WorkingDirectory=/opt/nupst | ||||||
| WorkingDirectory=/tmp |  | ||||||
|  |  | ||||||
| [Install] | [Install] | ||||||
| WantedBy=multi-user.target | WantedBy=multi-user.target | ||||||
| @@ -48,11 +51,11 @@ WantedBy=multi-user.target | |||||||
|     try { |     try { | ||||||
|       await fs.access(configPath); |       await fs.access(configPath); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       const boxWidth = 50; |       logger.log(''); | ||||||
|       logger.logBoxTitle('Configuration Error', boxWidth); |       logger.error('No configuration found'); | ||||||
|       logger.logBoxLine(`No configuration file found at ${configPath}`); |       logger.log(`  ${theme.dim('Config file:')} ${configPath}`); | ||||||
|       logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration."); |       logger.log(`  ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`); | ||||||
|       logger.logBoxEnd(); |       logger.log(''); | ||||||
|       throw new Error('Configuration not found'); |       throw new Error('Configuration not found'); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -81,7 +84,7 @@ WantedBy=multi-user.target | |||||||
|       logger.logBoxLine('Service enabled to start on boot'); |       logger.logBoxLine('Service enabled to start on boot'); | ||||||
|       logger.logBoxEnd(); |       logger.logBoxEnd(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       if (error.message === 'Configuration not found') { |       if (error instanceof Error && error.message === 'Configuration not found') { | ||||||
|         // Just rethrow the error as the message has already been displayed |         // Just rethrow the error as the message has already been displayed | ||||||
|         throw error; |         throw error; | ||||||
|       } |       } | ||||||
| @@ -105,7 +108,7 @@ WantedBy=multi-user.target | |||||||
|       logger.logBoxLine('NUPST service started successfully'); |       logger.logBoxLine('NUPST service started successfully'); | ||||||
|       logger.logBoxEnd(); |       logger.logBoxEnd(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       if (error.message === 'Configuration not found') { |       if (error instanceof Error && error.message === 'Configuration not found') { | ||||||
|         // Exit with error code since configuration is required |         // Exit with error code since configuration is required | ||||||
|         process.exit(1); |         process.exit(1); | ||||||
|       } |       } | ||||||
| @@ -118,7 +121,7 @@ WantedBy=multi-user.target | |||||||
|    * Stop the systemd service |    * Stop the systemd service | ||||||
|    * @throws Error if stop fails |    * @throws Error if stop fails | ||||||
|    */ |    */ | ||||||
|   public async stop(): Promise<void> { |   public stop(): void { | ||||||
|     try { |     try { | ||||||
|       execSync('systemctl stop nupst.service'); |       execSync('systemctl stop nupst.service'); | ||||||
|       logger.success('NUPST service stopped'); |       logger.success('NUPST service stopped'); | ||||||
| @@ -132,21 +135,59 @@ WantedBy=multi-user.target | |||||||
|    * 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 |    * @param debugMode Whether to enable debug mode for SNMP | ||||||
|    */ |    */ | ||||||
|  |   /** | ||||||
|  |    * Display version information and update status | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   private async displayVersionInfo(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       const nupst = this.daemon.getNupstSnmp().getNupst(); | ||||||
|  |       const version = nupst.getVersion(); | ||||||
|  |        | ||||||
|  |       // Check for updates | ||||||
|  |       const updateAvailable = await nupst.checkForUpdates(); | ||||||
|  |        | ||||||
|  |       // Display version info | ||||||
|  |       if (updateAvailable) { | ||||||
|  |         const updateStatus = nupst.getUpdateStatus(); | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log( | ||||||
|  |           `${theme.dim('NUPST')} ${theme.dim('v' + version)}  ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`, | ||||||
|  |         ); | ||||||
|  |         logger.log(`  ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`); | ||||||
|  |       } else { | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log( | ||||||
|  |           `${theme.dim('NUPST')} ${theme.dim('v' + version)}  ${symbols.success} ${theme.success('Up to date')}`, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       // If version check fails, show at least the current version | ||||||
|  |       try { | ||||||
|  |         const nupst = this.daemon.getNupstSnmp().getNupst(); | ||||||
|  |         const version = nupst.getVersion(); | ||||||
|  |         logger.log(''); | ||||||
|  |         logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`); | ||||||
|  |       } catch (_innerError) { | ||||||
|  |         // Silently fail if we can't even get the version | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public async getStatus(debugMode: boolean = false): Promise<void> { |   public async getStatus(debugMode: boolean = false): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       // Enable debug mode if requested |       // Enable debug mode if requested | ||||||
|       if (debugMode) { |       if (debugMode) { | ||||||
|         const boxWidth = 45; |         console.log(''); | ||||||
|         logger.logBoxTitle('Debug Mode', boxWidth); |         logger.info('Debug Mode: SNMP debugging enabled'); | ||||||
|         logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown'); |         console.log(''); | ||||||
|         logger.logBoxEnd(); |  | ||||||
|         this.daemon.getNupstSnmp().enableDebug(); |         this.daemon.getNupstSnmp().enableDebug(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Display version information |       // Display version and update status first | ||||||
|       this.daemon.getNupstSnmp().getNupst().logVersionInfo(); |       await this.displayVersionInfo(); | ||||||
|  |  | ||||||
|       // Check if config exists first |       // Check if config exists | ||||||
|       try { |       try { | ||||||
|         await this.checkConfigExists(); |         await this.checkConfigExists(); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
| @@ -157,7 +198,9 @@ WantedBy=multi-user.target | |||||||
|       await this.displayServiceStatus(); |       await this.displayServiceStatus(); | ||||||
|       await this.displayAllUpsStatus(); |       await this.displayAllUpsStatus(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.error(`Failed to get status: ${error.message}`); |       logger.error( | ||||||
|  |         `Failed to get status: ${error instanceof Error ? error.message : String(error)}`, | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -165,21 +208,53 @@ WantedBy=multi-user.target | |||||||
|    * Display the systemd service status |    * Display the systemd service status | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private async displayServiceStatus(): Promise<void> { |   private displayServiceStatus(): void { | ||||||
|     try { |     try { | ||||||
|       const serviceStatus = execSync('systemctl status nupst.service').toString(); |       const serviceStatus = execSync('systemctl status nupst.service').toString(); | ||||||
|       const boxWidth = 45; |       const lines = serviceStatus.split('\n'); | ||||||
|       logger.logBoxTitle('Service Status', boxWidth); |  | ||||||
|       // Process each line of the status output |       // Parse key information from systemctl output | ||||||
|       serviceStatus.split('\n').forEach(line => { |       let isActive = false; | ||||||
|         logger.logBoxLine(line); |       let pid = ''; | ||||||
|       }); |       let memory = ''; | ||||||
|       logger.logBoxEnd(); |       let cpu = ''; | ||||||
|  |  | ||||||
|  |       for (const line of lines) { | ||||||
|  |         if (line.includes('Active:')) { | ||||||
|  |           isActive = line.includes('active (running)'); | ||||||
|  |         } else if (line.includes('Main PID:')) { | ||||||
|  |           const match = line.match(/Main PID:\s+(\d+)/); | ||||||
|  |           if (match) pid = match[1]; | ||||||
|  |         } else if (line.includes('Memory:')) { | ||||||
|  |           const match = line.match(/Memory:\s+([\d.]+[A-Z])/); | ||||||
|  |           if (match) memory = match[1]; | ||||||
|  |         } else if (line.includes('CPU:')) { | ||||||
|  |           const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/); | ||||||
|  |           if (match) cpu = match[1]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Display beautiful status | ||||||
|  |       logger.log(''); | ||||||
|  |       if (isActive) { | ||||||
|  |         logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`); | ||||||
|  |       } else { | ||||||
|  |         logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (pid || memory || cpu) { | ||||||
|  |         const details = []; | ||||||
|  |         if (pid) details.push(`PID: ${theme.dim(pid)}`); | ||||||
|  |         if (memory) details.push(`Memory: ${theme.dim(memory)}`); | ||||||
|  |         if (cpu) details.push(`CPU: ${theme.dim(cpu)}`); | ||||||
|  |         logger.log(`  ${details.join('  ')}`); | ||||||
|  |       } | ||||||
|  |       logger.log(''); | ||||||
|  |  | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       const boxWidth = 45; |       logger.log(''); | ||||||
|       logger.logBoxTitle('Service Status', boxWidth); |       logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`); | ||||||
|       logger.logBoxLine('Service is not running'); |       logger.log(''); | ||||||
|       logger.logBoxEnd(); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -196,31 +271,47 @@ WantedBy=multi-user.target | |||||||
|  |  | ||||||
|       // Check if we have the new multi-UPS config format |       // Check if we have the new multi-UPS config format | ||||||
|       if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) { |       if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) { | ||||||
|         logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`); |         logger.info(`UPS Devices (${config.upsDevices.length}):`); | ||||||
|  |  | ||||||
|         // Show status for each UPS |         // Show status for each UPS | ||||||
|         for (const ups of config.upsDevices) { |         for (const ups of config.upsDevices) { | ||||||
|           await this.displaySingleUpsStatus(ups, snmp); |           await this.displaySingleUpsStatus(ups, snmp); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // Display groups after UPS devices | ||||||
|  |         this.displayGroupsStatus(); | ||||||
|       } else if (config.snmp) { |       } else if (config.snmp) { | ||||||
|         // Legacy single UPS configuration |         // Legacy single UPS configuration (v1/v2 format) | ||||||
|         const legacyUps = { |         logger.info('UPS Devices (1):'); | ||||||
|  |         const legacyUps: IUpsConfig = { | ||||||
|           id: 'default', |           id: 'default', | ||||||
|           name: 'Default UPS', |           name: 'Default UPS', | ||||||
|           snmp: config.snmp, |           snmp: config.snmp, | ||||||
|  |           groups: [], | ||||||
|  |           actions: config.thresholds | ||||||
|  |             ? [ | ||||||
|  |                 { | ||||||
|  |                   type: 'shutdown', | ||||||
|                   thresholds: config.thresholds, |                   thresholds: config.thresholds, | ||||||
|           groups: [] |                   triggerMode: 'onlyThresholds', | ||||||
|  |                   shutdownDelay: 5, | ||||||
|  |                 }, | ||||||
|  |               ] | ||||||
|  |             : [], | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         await this.displaySingleUpsStatus(legacyUps, snmp); |         await this.displaySingleUpsStatus(legacyUps, snmp); | ||||||
|       } else { |       } else { | ||||||
|         logger.error('No UPS devices found in configuration'); |         logger.log(''); | ||||||
|  |         logger.warn('No UPS devices configured'); | ||||||
|  |         logger.log(`  ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`); | ||||||
|  |         logger.log(''); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       const boxWidth = 45; |       logger.log(''); | ||||||
|       logger.logBoxTitle('UPS Status', boxWidth); |       logger.error('Failed to retrieve UPS status'); | ||||||
|       logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`); |       logger.log(`  ${theme.dim(error instanceof Error ? error.message : String(error))}`); | ||||||
|       logger.logBoxEnd(); |       logger.log(''); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -229,54 +320,146 @@ WantedBy=multi-user.target | |||||||
|    * @param ups UPS configuration |    * @param ups UPS configuration | ||||||
|    * @param snmp SNMP manager |    * @param snmp SNMP manager | ||||||
|    */ |    */ | ||||||
|   private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> { |   private async displaySingleUpsStatus(ups: IUpsConfig, snmp: NupstSnmp): Promise<void> { | ||||||
|     const boxWidth = 45; |  | ||||||
|     logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth); |  | ||||||
|     logger.logBoxLine(`ID: ${ups.id}`); |  | ||||||
|     logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`); |  | ||||||
|     logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`); |  | ||||||
|      |  | ||||||
|     if (ups.groups && ups.groups.length > 0) { |  | ||||||
|       // Get group names if available |  | ||||||
|       const config = this.daemon.getConfig(); |  | ||||||
|       const groupNames = ups.groups.map(groupId => { |  | ||||||
|         const group = config.groups?.find(g => g.id === groupId); |  | ||||||
|         return group ? group.name : groupId; |  | ||||||
|       }); |  | ||||||
|       logger.logBoxLine(`Groups: ${groupNames.join(', ')}`); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     logger.logBoxEnd(); |  | ||||||
|      |  | ||||||
|     try { |     try { | ||||||
|       // Create a test config with a short timeout |       // Create a test config with a short timeout | ||||||
|       const testConfig = { |       const testConfig = { | ||||||
|         ...ups.snmp, |         ...ups.snmp, | ||||||
|         timeout: Math.min(ups.snmp.timeout, 10000) // Use at most 10 seconds for status check |         timeout: Math.min(ups.snmp.timeout, 10000), // Use at most 10 seconds for status check | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const status = await snmp.getUpsStatus(testConfig); |       const status = await snmp.getUpsStatus(testConfig); | ||||||
|  |  | ||||||
|       logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); |       // Determine status symbol based on power status | ||||||
|       logger.logBoxLine(`Power Status: ${status.powerStatus}`); |       let statusSymbol = symbols.unknown; | ||||||
|       logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`); |       if (status.powerStatus === 'online') { | ||||||
|       logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`); |         statusSymbol = symbols.running; | ||||||
|  |       } else if (status.powerStatus === 'onBattery') { | ||||||
|  |         statusSymbol = symbols.warning; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       // Show threshold status |       // Display UPS name and power status | ||||||
|       logger.logBoxLine(''); |       logger.log(`  ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`); | ||||||
|       logger.logBoxLine('Thresholds:'); |  | ||||||
|       logger.logBoxLine(`  Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${ |       // Display battery with color coding | ||||||
|         status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓' |       const batteryColor = getBatteryColor(status.batteryCapacity); | ||||||
|       }`); |  | ||||||
|       logger.logBoxLine(`  Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${ |       // Get threshold from actions (if any action has thresholds defined) | ||||||
|         status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓' |       const actionWithThresholds = ups.actions?.find((action) => action.thresholds); | ||||||
|       }`); |       const batteryThreshold = actionWithThresholds?.thresholds?.battery; | ||||||
|  |       const batterySymbol = batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold | ||||||
|  |         ? symbols.success | ||||||
|  |         : batteryThreshold !== undefined | ||||||
|  |         ? symbols.warning | ||||||
|  |         : ''; | ||||||
|  |  | ||||||
|  |       logger.log(`    Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol}  Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`); | ||||||
|  |  | ||||||
|  |       // Display host info | ||||||
|  |       logger.log(`    ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`); | ||||||
|  |  | ||||||
|  |       // Display groups if any | ||||||
|  |       if (ups.groups && ups.groups.length > 0) { | ||||||
|  |         const config = this.daemon.getConfig(); | ||||||
|  |         const groupNames = ups.groups.map((groupId: string) => { | ||||||
|  |           const group = config.groups?.find((g: { id: string }) => g.id === groupId); | ||||||
|  |           return group ? group.name : groupId; | ||||||
|  |         }); | ||||||
|  |         logger.log(`    ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Display actions if any | ||||||
|  |       if (ups.actions && ups.actions.length > 0) { | ||||||
|  |         for (const action of ups.actions) { | ||||||
|  |           let actionDesc = `${action.type}`; | ||||||
|  |           if (action.thresholds) { | ||||||
|  |             actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`; | ||||||
|  |             if (action.shutdownDelay) { | ||||||
|  |               actionDesc += `, delay=${action.shutdownDelay}s`; | ||||||
|  |             } | ||||||
|  |             actionDesc += ')'; | ||||||
|  |           } else { | ||||||
|  |             actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`; | ||||||
|  |             if (action.shutdownDelay) { | ||||||
|  |               actionDesc += `, delay=${action.shutdownDelay}s`; | ||||||
|  |             } | ||||||
|  |             actionDesc += ')'; | ||||||
|  |           } | ||||||
|  |           logger.log(`    ${theme.dim('Action:')} ${theme.info(actionDesc)}`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       logger.log(''); | ||||||
|  |  | ||||||
|       logger.logBoxEnd(); |  | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); |       // Display error for this UPS | ||||||
|       logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`); |       logger.log(`  ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`); | ||||||
|       logger.logBoxEnd(); |       logger.log(`    ${theme.dim(error instanceof Error ? error.message : String(error))}`); | ||||||
|  |       logger.log(`    ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`); | ||||||
|  |       logger.log(''); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Display status of all groups | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   private displayGroupsStatus(): void { | ||||||
|  |     const config = this.daemon.getConfig(); | ||||||
|  |  | ||||||
|  |     if (!config.groups || config.groups.length === 0) { | ||||||
|  |       return; // No groups to display | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     logger.log(''); | ||||||
|  |     logger.info(`Groups (${config.groups.length}):`); | ||||||
|  |  | ||||||
|  |     for (const group of config.groups) { | ||||||
|  |       // Display group name and mode | ||||||
|  |       const modeColor = group.mode === 'redundant' ? theme.success : theme.warning; | ||||||
|  |       logger.log( | ||||||
|  |         `  ${symbols.info} ${theme.highlight(group.name)} ${theme.dim(`(${modeColor(group.mode)})`)}`, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // Display description if present | ||||||
|  |       if (group.description) { | ||||||
|  |         logger.log(`    ${theme.dim(group.description)}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Display UPS devices in this group | ||||||
|  |       const upsInGroup = config.upsDevices.filter((ups) => | ||||||
|  |         ups.groups && ups.groups.includes(group.id) | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (upsInGroup.length > 0) { | ||||||
|  |         const upsNames = upsInGroup.map((ups) => ups.name).join(', '); | ||||||
|  |         logger.log(`    ${theme.dim(`UPS Devices (${upsInGroup.length}):`)} ${upsNames}`); | ||||||
|  |       } else { | ||||||
|  |         logger.log(`    ${theme.dim('UPS Devices: None')}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Display actions if any | ||||||
|  |       if (group.actions && group.actions.length > 0) { | ||||||
|  |         for (const action of group.actions) { | ||||||
|  |           let actionDesc = `${action.type}`; | ||||||
|  |           if (action.thresholds) { | ||||||
|  |             actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`; | ||||||
|  |             if (action.shutdownDelay) { | ||||||
|  |               actionDesc += `, delay=${action.shutdownDelay}s`; | ||||||
|  |             } | ||||||
|  |             actionDesc += ')'; | ||||||
|  |           } else { | ||||||
|  |             actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`; | ||||||
|  |             if (action.shutdownDelay) { | ||||||
|  |               actionDesc += `, delay=${action.shutdownDelay}s`; | ||||||
|  |             } | ||||||
|  |             actionDesc += ')'; | ||||||
|  |           } | ||||||
|  |           logger.log(`    ${theme.dim('Action:')} ${theme.info(actionDesc)}`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       logger.log(''); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -304,7 +487,7 @@ WantedBy=multi-user.target | |||||||
|    * Stop the service if it's running |    * Stop the service if it's running | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private async stopService(): Promise<void> { |   private stopService(): void { | ||||||
|     try { |     try { | ||||||
|       logger.log('Stopping NUPST service...'); |       logger.log('Stopping NUPST service...'); | ||||||
|       execSync('systemctl stop nupst.service'); |       execSync('systemctl stop nupst.service'); | ||||||
| @@ -318,7 +501,7 @@ WantedBy=multi-user.target | |||||||
|    * Disable the service |    * Disable the service | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private async disableService(): Promise<void> { |   private disableService(): void { | ||||||
|     try { |     try { | ||||||
|       logger.log('Disabling NUPST service...'); |       logger.log('Disabling NUPST service...'); | ||||||
|       execSync('systemctl disable nupst.service'); |       execSync('systemctl disable nupst.service'); | ||||||
|   | |||||||
| @@ -1,15 +0,0 @@ | |||||||
| { |  | ||||||
|   "compilerOptions": { |  | ||||||
|     "experimentalDecorators": true, |  | ||||||
|     "emitDecoratorMetadata": true, |  | ||||||
|     "useDefineForClassFields": false, |  | ||||||
|     "target": "ES2022", |  | ||||||
|     "module": "NodeNext", |  | ||||||
|     "moduleResolution": "NodeNext", |  | ||||||
|     "esModuleInterop": true, |  | ||||||
|     "verbatimModuleSyntax": true |  | ||||||
|   }, |  | ||||||
|   "exclude": [ |  | ||||||
|     "dist_*/**/*.d.ts" |  | ||||||
|   ] |  | ||||||
| } |  | ||||||
		Reference in New Issue
	
	Block a user