From 67f97e6115a96517cc9f7d9aadb6c13ad38fc888 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 23 Oct 2025 23:31:53 +0000 Subject: [PATCH] feat(npm): Add npm package distribution support Added npm package wrapper to enable installation via npm while maintaining the Deno binary distribution model. New Files: - package.json: npm package configuration with binary wrapper - bin/spark-wrapper.js: Detects platform and executes correct binary - scripts/install-binary.js: Downloads appropriate binary on npm install - .npmignore: Excludes source files from npm package - npmextra.json: npm extra configuration Updated: - readme.md: Added npm installation instructions How It Works: 1. User runs: npm install -g @serve.zone/spark 2. Postinstall script (install-binary.js) downloads the correct pre-compiled binary for the user's platform from Gitea releases 3. Binary is cached in dist/binaries/ 4. Wrapper script (spark-wrapper.js) executes the binary when user runs 'spark' command Supported via npm: - Linux (x64, ARM64) - macOS (Intel, Apple Silicon) - Windows (x64) This maintains the benefits of Deno compilation (no runtime deps) while providing familiar npm-based installation for users who prefer it. --- .npmignore | 55 +++++++++ bin/spark-wrapper.js | 108 ++++++++++++++++++ npmextra.json | 1 + package.json | 69 ++++++++++++ readme.md | 8 ++ scripts/install-binary.js | 231 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 472 insertions(+) create mode 100644 .npmignore create mode 100755 bin/spark-wrapper.js create mode 100644 npmextra.json create mode 100644 package.json create mode 100755 scripts/install-binary.js diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..85659bf --- /dev/null +++ b/.npmignore @@ -0,0 +1,55 @@ +# Source code (not needed for binary distribution) +/ts/ +/test/ +mod.ts +*.ts +!*.d.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 +test.simple.ts + +# 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/spark-wrapper.js b/bin/spark-wrapper.js new file mode 100755 index 0000000..5804005 --- /dev/null +++ b/bin/spark-wrapper.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +/** + * SPARK 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 = `spark-${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/spark'); + console.error(' npm install -g @serve.zone/spark'); + 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 spark: ${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/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..74f42f0 --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "@serve.zone/spark", + "version": "1.2.2", + "description": "A comprehensive tool for maintaining and configuring servers, integrating with Docker and supporting advanced task scheduling, targeted at the Servezone infrastructure and used by @serve.zone/cloudly as a cluster node server system manager.", + "keywords": [ + "server management", + "devops", + "automation", + "docker", + "configuration management", + "daemon service", + "continuous integration", + "continuous deployment", + "deployment automation", + "service orchestration", + "deno", + "task scheduling", + "CLI", + "logging", + "server maintenance", + "serve.zone", + "cluster management", + "system manager", + "server configuration" + ], + "homepage": "https://code.foss.global/serve.zone/spark", + "bugs": { + "url": "https://code.foss.global/serve.zone/spark/issues" + }, + "repository": { + "type": "git", + "url": "git+https://code.foss.global/serve.zone/spark.git" + }, + "author": "Serve Zone", + "license": "MIT", + "type": "module", + "bin": { + "spark": "./bin/spark-wrapper.js" + }, + "scripts": { + "postinstall": "node scripts/install-binary.js", + "prepublishOnly": "echo 'Publishing SPARK binaries to npm...'", + "test": "echo 'Tests are run with Deno: deno task test'", + "build": "echo 'no build needed'" + }, + "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 807b2a5..6668892 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,14 @@ Install the latest version via our installation script: curl -sSL https://code.foss.global/serve.zone/spark/raw/branch/master/install.sh | sudo bash ``` +### npm Install + +Install via npm (automatically downloads the correct binary for your platform): + +```bash +npm install -g @serve.zone/spark +``` + ### Specific Version ```bash diff --git a/scripts/install-binary.js b/scripts/install-binary.js new file mode 100755 index 0000000..2479cfb --- /dev/null +++ b/scripts/install-binary.js @@ -0,0 +1,231 @@ +#!/usr/bin/env node + +/** + * SPARK npm postinstall script + * Downloads the appropriate binary for the current platform from Gitea 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/spark'; +const VERSION = process.env.npm_package_version || '1.2.2'; + +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 = `spark-${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(' SPARK - 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/spark/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/master/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/spark'); + 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/spark/raw/branch/master/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('✅ SPARK installation completed successfully!'); + console.log(''); + console.log('You can now use SPARK by running:'); + console.log(' spark --help'); + console.log(''); + console.log('For daemon setup, run:'); + console.log(' sudo spark installdaemon'); + 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