diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..abd220b --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,183 @@ +name: Publish to npm + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 5.0.6)' + required: true + type: string + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v4 + + # Setup Deno + - name: Setup Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + # Setup Node.js for npm publishing + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org/' + + # Compile binaries for all platforms + - name: Compile binaries + run: | + echo "Compiling binaries for all platforms..." + deno task compile + echo "" + echo "Binary sizes:" + ls -lh dist/binaries/ + + # Update version in package.json if triggered manually + - name: Update version in package.json + if: github.event_name == 'workflow_dispatch' + run: | + VERSION=${{ github.event.inputs.version }} + echo "Updating package.json to version ${VERSION}" + npm version ${VERSION} --no-git-tag-version + + # Extract version from tag if triggered by tag push + - name: Extract version from tag + if: startsWith(github.ref, 'refs/tags/') + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION=${VERSION}" >> $GITHUB_ENV + echo "Extracted version: ${VERSION}" + + # Ensure versions are synchronized + - name: Sync versions + run: | + if [ -n "${VERSION}" ]; then + echo "Syncing version ${VERSION} across files..." + + # Update deno.json + sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" deno.json + + # Update package.json + npm version ${VERSION} --no-git-tag-version --allow-same-version + + echo "Updated versions:" + echo "deno.json: $(grep '"version"' deno.json)" + echo "package.json: $(grep '"version"' package.json | head -1)" + fi + + # Generate SHA256 checksums for binaries + - name: Generate checksums + run: | + cd dist/binaries + sha256sum * > SHA256SUMS + echo "Checksums generated:" + cat SHA256SUMS + cd ../.. + + # Create npm package + - name: Create npm package + run: | + echo "Creating npm package..." + npm pack + echo "" + echo "Package created:" + ls -lh *.tgz + + # Test package installation locally + - name: Test local installation + run: | + echo "Testing local package installation..." + PACKAGE_FILE=$(ls *.tgz) + npm install -g ${PACKAGE_FILE} + + echo "" + echo "Testing nupst command:" + nupst --version || echo "Note: Binary execution may fail in CI environment" + + echo "" + echo "Checking installed files:" + npm ls -g @serve.zone/nupst + + # Publish to npm (only on tag push or manual trigger) + - name: Publish to npm + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + echo "Publishing to npm registry..." + npm publish --access public + + echo "" + echo "✅ Successfully published @serve.zone/nupst to npm!" + echo "" + echo "Package info:" + npm view @serve.zone/nupst + + # Create GitHub Release (only on tag push) + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: | + dist/binaries/nupst-* + dist/binaries/SHA256SUMS + *.tgz + generate_release_notes: true + body: | + ## NUPST ${{ env.VERSION }} + + ### Installation + + #### Via npm (recommended) + ```bash + npm install -g @serve.zone/nupst + ``` + + #### Direct download + Download the appropriate binary for your platform from the assets below. + + ### Platform Support + - Linux x64 / ARM64 + - macOS x64 / ARM64 (Apple Silicon) + - Windows x64 + + ### Checksums + SHA256 checksums are available in `SHA256SUMS` file. + + # Verify the published package + verify: + needs: build-and-publish + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Wait for npm propagation + run: sleep 30 + + - name: Verify npm package + run: | + echo "Verifying published package..." + npm view @serve.zone/nupst + + echo "" + echo "Testing installation from npm:" + npm install -g @serve.zone/nupst + + echo "" + echo "Package installed successfully!" + which nupst || echo "Binary location check skipped" \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..4b0f1eb --- /dev/null +++ b/.npmignore @@ -0,0 +1,54 @@ +# Source code (not needed for binary distribution) +/ts/ +/test/ +mod.ts +*.ts + +# Development files +.git/ +.gitea/ +.claude/ +.serena/ +.nogit/ +.github/ +deno.json +deno.lock +tsconfig.json + +# Scripts not needed for npm +/scripts/compile-all.sh +install.sh +uninstall.sh +example-action.sh + +# Documentation files not needed for npm package +readme.plan.md +readme.hints.md +npm-publish-instructions.md +docs/ + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Keep only the install-binary.js in scripts/ +/scripts/* +!/scripts/install-binary.js + +# Exclude all dist directory (binaries will be downloaded during install) +/dist/ + +# Logs and temporary files +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Other +node_modules/ +.env +.env.* \ No newline at end of file diff --git a/bin/nupst-wrapper.js b/bin/nupst-wrapper.js new file mode 100644 index 0000000..3f3353b --- /dev/null +++ b/bin/nupst-wrapper.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +/** + * NUPST npm wrapper + * This script executes the appropriate pre-compiled binary based on the current platform + */ + +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import { existsSync } from 'fs'; +import { platform, arch } from 'os'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Get the binary name for the current platform + */ +function getBinaryName() { + const plat = platform(); + const architecture = arch(); + + // Map Node's platform/arch to our binary naming + const platformMap = { + 'darwin': 'macos', + 'linux': 'linux', + 'win32': 'windows' + }; + + const archMap = { + 'x64': 'x64', + 'arm64': 'arm64' + }; + + const mappedPlatform = platformMap[plat]; + const mappedArch = archMap[architecture]; + + if (!mappedPlatform || !mappedArch) { + console.error(`Error: Unsupported platform/architecture: ${plat}/${architecture}`); + console.error('Supported platforms: Linux, macOS, Windows'); + console.error('Supported architectures: x64, arm64'); + process.exit(1); + } + + // Construct binary name + let binaryName = `nupst-${mappedPlatform}-${mappedArch}`; + if (plat === 'win32') { + binaryName += '.exe'; + } + + return binaryName; +} + +/** + * Execute the binary + */ +function executeBinary() { + const binaryName = getBinaryName(); + const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName); + + // Check if binary exists + if (!existsSync(binaryPath)) { + console.error(`Error: Binary not found at ${binaryPath}`); + console.error('This might happen if:'); + console.error('1. The postinstall script failed to run'); + console.error('2. The platform is not supported'); + console.error('3. The package was not installed correctly'); + console.error(''); + console.error('Try reinstalling the package:'); + console.error(' npm uninstall -g @serve.zone/nupst'); + console.error(' npm install -g @serve.zone/nupst'); + process.exit(1); + } + + // Spawn the binary with all arguments passed through + const child = spawn(binaryPath, process.argv.slice(2), { + stdio: 'inherit', + shell: false + }); + + // Handle child process events + child.on('error', (err) => { + console.error(`Error executing nupst: ${err.message}`); + process.exit(1); + }); + + child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + } else { + process.exit(code || 0); + } + }); + + // Forward signals to child process + const signals = ['SIGINT', 'SIGTERM', 'SIGHUP']; + signals.forEach(signal => { + process.on(signal, () => { + if (!child.killed) { + child.kill(signal); + } + }); + }); +} + +// Execute +executeBinary(); \ No newline at end of file diff --git a/changelog.md b/changelog.md index f859b9d..923a2c3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-10-22 - 5.1.0 - feat(packaging) +Add npm packaging and installer: wrapper, postinstall downloader, publish workflow, and packaging files + +- Add package.json (v5.0.5) and npm packaging metadata to publish @serve.zone/nupst +- Include a small Node.js wrapper (bin/nupst-wrapper.js) to execute platform-specific precompiled binaries +- Add postinstall script (scripts/install-binary.js) that downloads the correct binary for the current platform and sets executable permissions +- Add GitHub Actions workflow (.github/workflows/npm-publish.yml) to build binaries, pack and publish to npm, and create releases +- Add .npmignore to keep source, tests and dev files out of npm package; keep only runtime installer and wrapper +- Move example action script into docs (docs/example-action.sh) and remove the top-level example-action.sh +- Include generated npm package artifact (serve.zone-nupst-5.0.5.tgz) and npmextra.json + ## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime **MAJOR RELEASE: NUPST v4.0 is a complete rewrite powered by Deno** diff --git a/example-action.sh b/docs/example-action.sh similarity index 100% rename from example-action.sh rename to docs/example-action.sh diff --git a/npmextra.json b/npmextra.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/npmextra.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d2ed159 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "@serve.zone/nupst", + "version": "5.0.5", + "description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies", + "keywords": [ + "ups", + "snmp", + "power", + "shutdown", + "monitoring", + "cyberpower", + "apc", + "eaton", + "tripplite", + "liebert", + "vertiv", + "battery", + "backup" + ], + "homepage": "https://code.foss.global/serve.zone/nupst", + "bugs": { + "url": "https://code.foss.global/serve.zone/nupst/issues" + }, + "repository": { + "type": "git", + "url": "git+https://code.foss.global/serve.zone/nupst.git" + }, + "author": "Serve Zone", + "license": "MIT", + "type": "module", + "bin": { + "nupst": "./bin/nupst-wrapper.js" + }, + "scripts": { + "postinstall": "node scripts/install-binary.js", + "prepublishOnly": "echo 'Publishing NUPST binaries to npm...'", + "test": "echo 'Tests are run with Deno: deno task test'" + }, + "files": [ + "bin/", + "scripts/install-binary.js", + "readme.md", + "license", + "changelog.md" + ], + "engines": { + "node": ">=14.0.0" + }, + "os": [ + "darwin", + "linux", + "win32" + ], + "cpu": [ + "x64", + "arm64" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} \ No newline at end of file diff --git a/readme.md b/readme.md index 4302e93..07aa8f5 100644 --- a/readme.md +++ b/readme.md @@ -52,7 +52,24 @@ nupst service status ## 📥 Installation -### Automated Installer (Recommended) +### Via npm (NEW! - Recommended) + +Install NUPST globally using npm: + +```bash +npm install -g @serve.zone/nupst +``` + +**Benefits:** +- Automatic platform detection and binary download +- Downloads only the binary for your platform (~400-500MB) +- Easy updates via `npm update -g @serve.zone/nupst` +- Version management with npm +- Works with Node.js >=14 + +**Note:** The installation will download the appropriate binary from GitHub releases during the postinstall step. + +### Automated Installer Script The installer script handles everything automatically: diff --git a/scripts/install-binary.js b/scripts/install-binary.js new file mode 100644 index 0000000..f495923 --- /dev/null +++ b/scripts/install-binary.js @@ -0,0 +1,231 @@ +#!/usr/bin/env node + +/** + * NUPST npm postinstall script + * Downloads the appropriate binary for the current platform from GitHub releases + */ + +import { platform, arch } from 'os'; +import { existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import https from 'https'; +import { pipeline } from 'stream'; +import { promisify } from 'util'; +import { createWriteStream } from 'fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const streamPipeline = promisify(pipeline); + +// Configuration +const REPO_BASE = 'https://code.foss.global/serve.zone/nupst'; +const VERSION = process.env.npm_package_version || '5.0.5'; + +function getBinaryInfo() { + const plat = platform(); + const architecture = arch(); + + const platformMap = { + 'darwin': 'macos', + 'linux': 'linux', + 'win32': 'windows' + }; + + const archMap = { + 'x64': 'x64', + 'arm64': 'arm64' + }; + + const mappedPlatform = platformMap[plat]; + const mappedArch = archMap[architecture]; + + if (!mappedPlatform || !mappedArch) { + return { supported: false, platform: plat, arch: architecture }; + } + + let binaryName = `nupst-${mappedPlatform}-${mappedArch}`; + if (plat === 'win32') { + binaryName += '.exe'; + } + + return { + supported: true, + platform: mappedPlatform, + arch: mappedArch, + binaryName, + originalPlatform: plat + }; +} + +function downloadFile(url, destination) { + return new Promise((resolve, reject) => { + console.log(`Downloading from: ${url}`); + + // Follow redirects + const download = (url, redirectCount = 0) => { + if (redirectCount > 5) { + reject(new Error('Too many redirects')); + return; + } + + https.get(url, (response) => { + if (response.statusCode === 301 || response.statusCode === 302) { + console.log(`Following redirect to: ${response.headers.location}`); + download(response.headers.location, redirectCount + 1); + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`)); + return; + } + + const totalSize = parseInt(response.headers['content-length'], 10); + let downloadedSize = 0; + let lastProgress = 0; + + response.on('data', (chunk) => { + downloadedSize += chunk.length; + const progress = Math.round((downloadedSize / totalSize) * 100); + + // Only log every 10% to reduce noise + if (progress >= lastProgress + 10) { + console.log(`Download progress: ${progress}%`); + lastProgress = progress; + } + }); + + const file = createWriteStream(destination); + + pipeline(response, file, (err) => { + if (err) { + reject(err); + } else { + console.log('Download complete!'); + resolve(); + } + }); + }).on('error', reject); + }; + + download(url); + }); +} + +async function main() { + console.log('==========================================='); + console.log(' NUPST - Binary Installation'); + console.log('==========================================='); + console.log(''); + + const binaryInfo = getBinaryInfo(); + + if (!binaryInfo.supported) { + console.error(`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`); + console.error(''); + console.error('Supported platforms:'); + console.error(' • Linux (x64, arm64)'); + console.error(' • macOS (x64, arm64)'); + console.error(' • Windows (x64)'); + console.error(''); + console.error('If you believe your platform should be supported, please file an issue:'); + console.error(' https://code.foss.global/serve.zone/nupst/issues'); + process.exit(1); + } + + console.log(`Platform: ${binaryInfo.platform} (${binaryInfo.originalPlatform})`); + console.log(`Architecture: ${binaryInfo.arch}`); + console.log(`Binary: ${binaryInfo.binaryName}`); + console.log(`Version: ${VERSION}`); + console.log(''); + + // Create dist/binaries directory if it doesn't exist + const binariesDir = join(__dirname, '..', 'dist', 'binaries'); + if (!existsSync(binariesDir)) { + console.log('Creating binaries directory...'); + mkdirSync(binariesDir, { recursive: true }); + } + + const binaryPath = join(binariesDir, binaryInfo.binaryName); + + // Check if binary already exists and skip download + if (existsSync(binaryPath)) { + console.log('✓ Binary already exists, skipping download'); + } else { + // Construct download URL + // Try release URL first, fall back to raw branch if needed + const releaseUrl = `${REPO_BASE}/releases/download/v${VERSION}/${binaryInfo.binaryName}`; + const fallbackUrl = `${REPO_BASE}/raw/branch/main/dist/binaries/${binaryInfo.binaryName}`; + + console.log('Downloading platform-specific binary...'); + console.log('This may take a moment depending on your connection speed.'); + console.log(''); + + try { + // Try downloading from release + await downloadFile(releaseUrl, binaryPath); + } catch (err) { + console.log(`Release download failed: ${err.message}`); + console.log('Trying fallback URL...'); + + try { + // Try fallback URL + await downloadFile(fallbackUrl, binaryPath); + } catch (fallbackErr) { + console.error(`❌ Error: Failed to download binary`); + console.error(` Primary URL: ${releaseUrl}`); + console.error(` Fallback URL: ${fallbackUrl}`); + console.error(''); + console.error('This might be because:'); + console.error('1. The release has not been created yet'); + console.error('2. Network connectivity issues'); + console.error('3. The version specified does not exist'); + console.error(''); + console.error('You can try:'); + console.error('1. Installing from source: https://code.foss.global/serve.zone/nupst'); + console.error('2. Downloading the binary manually from the releases page'); + console.error('3. Using the install script: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash'); + + // Clean up partial download + if (existsSync(binaryPath)) { + unlinkSync(binaryPath); + } + + process.exit(1); + } + } + + console.log(`✓ Binary downloaded successfully`); + } + + // On Unix-like systems, ensure the binary is executable + if (binaryInfo.originalPlatform !== 'win32') { + try { + console.log('Setting executable permissions...'); + chmodSync(binaryPath, 0o755); + console.log('✓ Binary permissions updated'); + } catch (err) { + console.error(`⚠️ Warning: Could not set executable permissions: ${err.message}`); + console.error(' You may need to manually run:'); + console.error(` chmod +x ${binaryPath}`); + } + } + + console.log(''); + console.log('✅ NUPST installation completed successfully!'); + console.log(''); + console.log('You can now use NUPST by running:'); + console.log(' nupst --help'); + console.log(''); + console.log('For initial setup, run:'); + console.log(' sudo nupst ups add'); + console.log(''); + console.log('==========================================='); +} + +// Run the installation +main().catch(err => { + console.error(`❌ Installation failed: ${err.message}`); + process.exit(1); +}); \ No newline at end of file diff --git a/serve.zone-nupst-5.0.5.tgz b/serve.zone-nupst-5.0.5.tgz new file mode 100644 index 0000000..2ec8a2a Binary files /dev/null and b/serve.zone-nupst-5.0.5.tgz differ diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0b85046..0bb98f4 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -1,10 +1,8 @@ /** - * commitinfo - reads version from deno.json + * autocreated commitinfo by @push.rocks/commitinfo */ -import denoConfig from '../deno.json' with { type: 'json' }; - export const commitinfo = { - name: denoConfig.name, - version: denoConfig.version, - description: 'Network UPS Shutdown Tool (https://nupst.serve.zone)', -}; + name: '@serve.zone/nupst', + version: '5.1.0', + description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies' +}