cli/pkg/release/publishers/templates/npm/install.js.tmpl
Snider 513a241c1b feat(release): add package manager publishers for S3.2
Add publishers for distributing CLI binaries to package managers:
- npm: binary wrapper pattern with postinstall download
- Homebrew: formula generation + tap auto-commit
- Scoop: JSON manifest + bucket auto-commit
- AUR: PKGBUILD + .SRCINFO + AUR push
- Chocolatey: NuSpec + install script + optional push

Each publisher supports:
- Dry-run mode for previewing changes
- Auto-commit to own repos (tap/bucket/AUR)
- Generate files for PRs to official repos via `official` config

Also includes Docker and LinuxKit build helpers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 00:32:04 +00:00

176 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Binary installer for {{.Package}}
* Downloads the correct binary for the current platform from GitHub releases.
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const { spawnSync } = require('child_process');
const crypto = require('crypto');
const PACKAGE_VERSION = '{{.Version}}';
const GITHUB_REPO = '{{.Repository}}';
const BINARY_NAME = '{{.BinaryName}}';
// Platform/arch mapping
const PLATFORM_MAP = {
darwin: 'darwin',
linux: 'linux',
win32: 'windows',
};
const ARCH_MAP = {
x64: 'amd64',
arm64: 'arm64',
};
function getPlatformInfo() {
const platform = PLATFORM_MAP[process.platform];
const arch = ARCH_MAP[process.arch];
if (!platform || !arch) {
console.error(`Unsupported platform: ${process.platform}/${process.arch}`);
process.exit(1);
}
return { platform, arch };
}
function getDownloadUrl(platform, arch) {
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
const name = `${BINARY_NAME}-${platform}-${arch}${ext}`;
return `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${name}`;
}
function getChecksumsUrl() {
return `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/checksums.txt`;
}
function download(url) {
return new Promise((resolve, reject) => {
const request = (url) => {
https.get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
// Follow redirect
request(res.headers.location);
return;
}
if (res.statusCode !== 200) {
reject(new Error(`Failed to download ${url}: HTTP ${res.statusCode}`));
return;
}
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => resolve(Buffer.concat(chunks)));
res.on('error', reject);
}).on('error', reject);
};
request(url);
});
}
async function fetchChecksums() {
try {
const data = await download(getChecksumsUrl());
const checksums = {};
data.toString().split('\n').forEach((line) => {
const parts = line.trim().split(/\s+/);
if (parts.length === 2) {
checksums[parts[1]] = parts[0];
}
});
return checksums;
} catch (err) {
console.warn('Warning: Could not fetch checksums, skipping verification');
return null;
}
}
function verifyChecksum(data, expectedHash) {
const actualHash = crypto.createHash('sha256').update(data).digest('hex');
return actualHash === expectedHash;
}
function extract(data, destDir, platform) {
const tempFile = path.join(destDir, platform === 'windows' ? 'temp.zip' : 'temp.tar.gz');
fs.writeFileSync(tempFile, data);
try {
if (platform === 'windows') {
// Use PowerShell to extract zip
const result = spawnSync('powershell', [
'-command',
`Expand-Archive -Path '${tempFile}' -DestinationPath '${destDir}' -Force`
], { stdio: 'ignore' });
if (result.status !== 0) {
throw new Error('Failed to extract zip');
}
} else {
const result = spawnSync('tar', ['-xzf', tempFile, '-C', destDir], { stdio: 'ignore' });
if (result.status !== 0) {
throw new Error('Failed to extract tar.gz');
}
}
} finally {
fs.unlinkSync(tempFile);
}
}
async function main() {
const { platform, arch } = getPlatformInfo();
const binDir = path.join(__dirname, 'bin');
const binaryPath = path.join(binDir, platform === 'windows' ? `${BINARY_NAME}.exe` : BINARY_NAME);
// Skip if binary already exists
if (fs.existsSync(binaryPath)) {
console.log(`${BINARY_NAME} binary already installed`);
return;
}
console.log(`Installing ${BINARY_NAME} v${PACKAGE_VERSION} for ${platform}/${arch}...`);
// Ensure bin directory exists
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}
// Fetch checksums
const checksums = await fetchChecksums();
// Download binary
const url = getDownloadUrl(platform, arch);
console.log(`Downloading from ${url}`);
const data = await download(url);
// Verify checksum if available
if (checksums) {
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
const filename = `${BINARY_NAME}-${platform}-${arch}${ext}`;
const expectedHash = checksums[filename];
if (expectedHash && !verifyChecksum(data, expectedHash)) {
console.error('Checksum verification failed!');
process.exit(1);
}
console.log('Checksum verified');
}
// Extract
extract(data, binDir, platform);
// Make executable on Unix
if (platform !== 'windows') {
fs.chmodSync(binaryPath, 0o755);
}
console.log(`${BINARY_NAME} installed successfully`);
}
main().catch((err) => {
console.error(`Installation failed: ${err.message}`);
process.exit(1);
});