Skip to main content

npm Binary Packaging Skill

npm Binary Packaging Skill

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Overview

This skill provides comprehensive patterns for packaging and publishing native binaries as npm packages. It covers persona-specific package creation (developer/user/contributor), 6-platform binary distribution, optionalDependencies pattern for platform detection, postinstall scripts for binary location, and automated version synchronization.

Quick Reference

Package Architecture

@myorg/myapp                     (Main package - meta-package)
├── @myorg/myapp-darwin-x64 (macOS Intel binary)
├── @myorg/myapp-darwin-arm64 (macOS Apple Silicon binary)
├── @myorg/myapp-linux-x64 (Linux x64 binary)
├── @myorg/myapp-linux-arm64 (Linux ARM64 binary)
├── @myorg/myapp-linux-x64-musl (Linux x64 musl binary)
└── @myorg/myapp-win32-x64 (Windows x64 binary)

Platform Detection Flow

┌─────────────────┐
│ npm install │
│ @myorg/myapp │
└────────┬────────┘


┌────────────────────────────────────┐
│ optionalDependencies installation │
│ (all 6 platforms attempted) │
└────────┬───────────────────────────┘


┌────────────────────────────────────┐
│ postinstall.js executes │
│ - Detects current platform │
│ - Locates binary in node_modules │
│ - Creates bin/ symlink/wrapper │
└────────┬───────────────────────────┘


┌────────────────────────────────────┐
│ Binary ready at bin/myapp │
└────────────────────────────────────┘

When to Use This Skill

✅ Use when:

  • Publishing Rust/Go/C++ binaries as npm packages
  • Creating multi-platform CLI tools for npm distribution
  • Building persona-specific packages (developer vs user vs contributor)
  • Implementing optionalDependencies pattern for platform binaries
  • Automating npm publish workflows for binary releases
  • Synchronizing versions across main + platform packages

❌ Don't use when:

  • Package is pure JavaScript/TypeScript (no native binaries)
  • Only targeting a single platform (simplified packaging sufficient)
  • Binaries are downloaded at runtime (different pattern needed)
  • Using npm only for dependency management (not distribution)

Core Capabilities

1. Multi-Platform Package Generation

Creates 7 npm packages (1 main + 6 platform-specific) with correct package.json structure:

Main Package (@myorg/myapp):

  • Meta-package that declares optionalDependencies
  • Contains postinstall.js for binary location
  • Provides bin entries for CLI execution
  • Version synchronization across all packages

Platform Packages (@myorg/myapp-{platform}):

  • Contains single native binary for target platform
  • Minimal package.json with binary file reference
  • Published separately to npm registry
  • Installed only on matching platform via optionalDependencies

2. optionalDependencies Pattern

Implements npm's optionalDependencies mechanism for platform-specific installation:

{
"optionalDependencies": {
"@myorg/myapp-darwin-x64": "1.0.0",
"@myorg/myapp-darwin-arm64": "1.0.0",
"@myorg/myapp-linux-x64": "1.0.0",
"@myorg/myapp-linux-arm64": "1.0.0",
"@myorg/myapp-linux-x64-musl": "1.0.0",
"@myorg/myapp-win32-x64": "1.0.0"
}
}

Benefits:

  • npm attempts to install all optionalDependencies
  • Failures (platform mismatch) don't fail installation
  • Correct platform package installed automatically
  • Fallback handling for unsupported platforms

3. Postinstall Binary Location

Implements postinstall.js script to locate and configure binary:

Responsibilities:

  • Detect current platform (process.platform, process.arch)
  • Map to package naming convention (darwin-x64, linux-arm64, etc.)
  • Search node_modules for platform package
  • Create bin/ directory with symlink (Unix) or wrapper (Windows)
  • Verify binary is executable
  • Handle missing platform gracefully with error message

4. Persona-Specific Packaging

Creates specialized packages for different user personas:

Developer Package (@myorg/myapp-dev):

  • Includes source code and build tools
  • Development dependencies (testing, linting)
  • Debugging symbols and source maps
  • Larger download size acceptable

User Package (@myorg/myapp):

  • Production-ready binary only
  • Minimal dependencies
  • Optimized for size and performance
  • Default package for end users

Contributor Package (@myorg/myapp-contrib):

  • Full source + binaries
  • Contribution guidelines
  • Development scripts and hooks
  • Documentation for contributors

5. Automated Version Synchronization

Ensures all packages (main + 6 platforms + personas) share the same version:

Synchronization Points:

  • Update version in all package.json files
  • Update optionalDependencies references
  • Update installation instructions with version
  • Generate changelog for release
  • Tag git repository with version

6. Checksum Verification

Implements SHA256 verification for downloaded binaries:

Verification Flow:

  1. Platform packages include SHA256SUMS file
  2. Postinstall script verifies binary checksum
  3. Installation fails if checksum mismatch
  4. Prevents corrupted or tampered binaries

Level 1: Essential Patterns

Basic Platform Package Structure

Directory Layout:

packages/
├── myapp/ # Main package
│ ├── package.json
│ ├── postinstall.js
│ ├── bin/
│ │ └── myapp # Created by postinstall
│ └── README.md

├── myapp-darwin-x64/ # macOS Intel
│ ├── package.json
│ ├── myapp # Native binary
│ └── SHA256SUMS

├── myapp-darwin-arm64/ # macOS Apple Silicon
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS

├── myapp-linux-x64/ # Linux x64
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS

├── myapp-linux-arm64/ # Linux ARM64
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS

├── myapp-linux-x64-musl/ # Linux musl
│ ├── package.json
│ ├── myapp
│ └── SHA256SUMS

└── myapp-win32-x64/ # Windows x64
├── package.json
├── myapp.exe
└── SHA256SUMS

Main Package (package.json)

{
"name": "@myorg/myapp",
"version": "1.0.0",
"description": "My awesome CLI tool",
"bin": {
"myapp": "bin/myapp"
},
"scripts": {
"postinstall": "node postinstall.js"
},
"optionalDependencies": {
"@myorg/myapp-darwin-x64": "1.0.0",
"@myorg/myapp-darwin-arm64": "1.0.0",
"@myorg/myapp-linux-x64": "1.0.0",
"@myorg/myapp-linux-arm64": "1.0.0",
"@myorg/myapp-linux-x64-musl": "1.0.0",
"@myorg/myapp-win32-x64": "1.0.0"
},
"files": [
"bin/",
"postinstall.js",
"README.md"
],
"engines": {
"node": ">=14.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/myorg/myapp.git"
},
"keywords": ["cli", "binary", "tool"],
"author": "Your Name",
"license": "MIT"
}

Platform Package (package.json)

{
"name": "@myorg/myapp-darwin-x64",
"version": "1.0.0",
"description": "macOS x64 binary for @myorg/myapp",
"os": ["darwin"],
"cpu": ["x64"],
"files": [
"myapp",
"SHA256SUMS"
],
"repository": {
"type": "git",
"url": "https://github.com/myorg/myapp.git"
},
"license": "MIT"
}

Basic Postinstall Script

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

// Platform detection
const PLATFORM_MAP = {
'darwin-x64': 'darwin-x64',
'darwin-arm64': 'darwin-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'win32-x64': 'win32-x64'
};

function detectPlatform() {
const platform = process.platform;
const arch = process.arch;
const key = `${platform}-${arch}`;

if (key === 'linux-x64') {
// Check for musl
const lddOutput = spawnSync('ldd', ['--version'], { encoding: 'utf8' });
if (lddOutput.stdout && lddOutput.stdout.includes('musl')) {
return 'linux-x64-musl';
}
}

return PLATFORM_MAP[key];
}

function findBinary(packageName) {
const nodeModulesPath = path.join(__dirname, 'node_modules', packageName);
const binaryName = process.platform === 'win32' ? 'myapp.exe' : 'myapp';
const binaryPath = path.join(nodeModulesPath, binaryName);

if (fs.existsSync(binaryPath)) {
return binaryPath;
}

return null;
}

function createBinWrapper(binaryPath) {
const binDir = path.join(__dirname, 'bin');

if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}

const targetPath = path.join(binDir, process.platform === 'win32' ? 'myapp.cmd' : 'myapp');

if (process.platform === 'win32') {
// Windows: Create .cmd wrapper
const wrapper = `@echo off\n"${binaryPath}" %*\n`;
fs.writeFileSync(targetPath, wrapper);
} else {
// Unix: Create symlink
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
fs.symlinkSync(binaryPath, targetPath);
fs.chmodSync(targetPath, 0o755);
}

console.log(`✓ Binary installed at ${targetPath}`);
}

function main() {
const platform = detectPlatform();

if (!platform) {
console.error(`✗ Unsupported platform: ${process.platform}-${process.arch}`);
process.exit(1);
}

const packageName = `@myorg/myapp-${platform}`;
const binaryPath = findBinary(packageName);

if (!binaryPath) {
console.error(`✗ Binary not found for ${packageName}`);
console.error(' This may happen if the platform package failed to install.');
process.exit(1);
}

createBinWrapper(binaryPath);
}

main();

Level 2: Production Patterns

Complete Package Generation Script

#!/usr/bin/env node
/**
* generate-npm-packages.js
*
* Generates all npm packages (main + 6 platforms) from built binaries.
*
* Usage:
* node scripts/generate-npm-packages.js --version 1.0.0 --binaries dist/
*/

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const PLATFORMS = [
{ platform: 'darwin-x64', os: ['darwin'], cpu: ['x64'], ext: '' },
{ platform: 'darwin-arm64', os: ['darwin'], cpu: ['arm64'], ext: '' },
{ platform: 'linux-x64', os: ['linux'], cpu: ['x64'], ext: '' },
{ platform: 'linux-arm64', os: ['linux'], cpu: ['arm64'], ext: '' },
{ platform: 'linux-x64-musl', os: ['linux'], cpu: ['x64'], ext: '' },
{ platform: 'win32-x64', os: ['win32'], cpu: ['x64'], ext: '.exe' }
];

class PackageGenerator {
constructor(options) {
this.version = options.version;
this.binariesDir = options.binariesDir;
this.outputDir = options.outputDir || 'packages';
this.packageName = options.packageName || 'myapp';
this.scope = options.scope || '@myorg';
}

generate() {
console.log(`Generating npm packages v${this.version}...`);

// Create output directory
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
}

// Generate platform packages
for (const platform of PLATFORMS) {
this.generatePlatformPackage(platform);
}

// Generate main package
this.generateMainPackage();

console.log('✓ All packages generated successfully');
}

generatePlatformPackage(platform) {
const packageDir = path.join(this.outputDir, `${this.packageName}-${platform.platform}`);
const binaryName = `${this.packageName}${platform.ext}`;
const sourceBinary = path.join(this.binariesDir, `${this.packageName}-${platform.platform}${platform.ext}`);

console.log(` Creating ${this.packageName}-${platform.platform}...`);

// Create package directory
if (!fs.existsSync(packageDir)) {
fs.mkdirSync(packageDir, { recursive: true });
}

// Copy binary
if (!fs.existsSync(sourceBinary)) {
throw new Error(`Binary not found: ${sourceBinary}`);
}
fs.copyFileSync(sourceBinary, path.join(packageDir, binaryName));

// Generate checksum
const checksum = this.generateChecksum(sourceBinary);
fs.writeFileSync(
path.join(packageDir, 'SHA256SUMS'),
`${checksum} ${binaryName}\n`
);

// Generate package.json
const packageJson = {
name: `${this.scope}/${this.packageName}-${platform.platform}`,
version: this.version,
description: `${platform.platform} binary for ${this.scope}/${this.packageName}`,
os: platform.os,
cpu: platform.cpu,
files: [binaryName, 'SHA256SUMS'],
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`
},
license: 'MIT'
};

fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);

// Create README
const readme = this.generatePlatformReadme(platform);
fs.writeFileSync(path.join(packageDir, 'README.md'), readme);

console.log(`${this.packageName}-${platform.platform}`);
}

generateMainPackage() {
const packageDir = path.join(this.outputDir, this.packageName);

console.log(` Creating ${this.packageName} (main package)...`);

// Create package directory
if (!fs.existsSync(packageDir)) {
fs.mkdirSync(packageDir, { recursive: true });
}

// Generate package.json
const optionalDependencies = {};
for (const platform of PLATFORMS) {
optionalDependencies[`${this.scope}/${this.packageName}-${platform.platform}`] = this.version;
}

const packageJson = {
name: `${this.scope}/${this.packageName}`,
version: this.version,
description: 'My awesome CLI tool',
bin: {
[this.packageName]: 'bin/' + this.packageName
},
scripts: {
postinstall: 'node postinstall.js'
},
optionalDependencies,
files: ['bin/', 'postinstall.js', 'README.md'],
engines: {
node: '>=14.0.0'
},
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`
},
keywords: ['cli', 'binary', 'tool'],
license: 'MIT'
};

fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);

// Copy postinstall script
const postinstallScript = this.generatePostinstallScript();
fs.writeFileSync(path.join(packageDir, 'postinstall.js'), postinstallScript);
fs.chmodSync(path.join(packageDir, 'postinstall.js'), 0o755);

// Create bin directory (empty, populated by postinstall)
const binDir = path.join(packageDir, 'bin');
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir);
}

// Create README
const readme = this.generateMainReadme();
fs.writeFileSync(path.join(packageDir, 'README.md'), readme);

console.log(`${this.packageName} (main)`);
}

generateChecksum(filePath) {
const output = execSync(`shasum -a 256 "${filePath}"`, { encoding: 'utf8' });
return output.split(' ')[0];
}

generatePostinstallScript() {
return `#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const PLATFORM_MAP = {
'darwin-x64': 'darwin-x64',
'darwin-arm64': 'darwin-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'win32-x64': 'win32-x64'
};

function detectPlatform() {
const platform = process.platform;
const arch = process.arch;
const key = \`\${platform}-\${arch}\`;

if (key === 'linux-x64') {
const lddOutput = spawnSync('ldd', ['--version'], { encoding: 'utf8' });
if (lddOutput.stdout && lddOutput.stdout.includes('musl')) {
return 'linux-x64-musl';
}
}

return PLATFORM_MAP[key];
}

function findBinary(packageName) {
const nodeModulesPath = path.join(__dirname, 'node_modules', packageName);
const binaryName = process.platform === 'win32' ? '${this.packageName}.exe' : '${this.packageName}';
const binaryPath = path.join(nodeModulesPath, binaryName);

if (fs.existsSync(binaryPath)) {
return binaryPath;
}

return null;
}

function verifyChecksum(binaryPath, packageDir) {
const sumsFile = path.join(packageDir, 'SHA256SUMS');
if (!fs.existsSync(sumsFile)) {
console.warn('⚠ No SHA256SUMS file found, skipping verification');
return true;
}

const expectedSum = fs.readFileSync(sumsFile, 'utf8').split(' ')[0];
const { spawnSync } = require('child_process');
const result = spawnSync('shasum', ['-a', '256', binaryPath], { encoding: 'utf8' });
const actualSum = result.stdout.split(' ')[0];

if (actualSum !== expectedSum) {
console.error(\`✗ Checksum mismatch for \${binaryPath}\`);
console.error(\` Expected: \${expectedSum}\`);
console.error(\` Actual: \${actualSum}\`);
return false;
}

return true;
}

function createBinWrapper(binaryPath) {
const binDir = path.join(__dirname, 'bin');

if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}

const targetPath = path.join(binDir, process.platform === 'win32' ? '${this.packageName}.cmd' : '${this.packageName}');

if (process.platform === 'win32') {
const wrapper = \`@echo off\\n"\${binaryPath}" %*\\n\`;
fs.writeFileSync(targetPath, wrapper);
} else {
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
fs.symlinkSync(binaryPath, targetPath);
fs.chmodSync(targetPath, 0o755);
}

console.log(\`✓ Binary installed at \${targetPath}\`);
}

function main() {
const platform = detectPlatform();

if (!platform) {
console.error(\`✗ Unsupported platform: \${process.platform}-\${process.arch}\`);
process.exit(1);
}

const packageName = \`${this.scope}/${this.packageName}-\${platform}\`;
const binaryPath = findBinary(packageName);

if (!binaryPath) {
console.error(\`✗ Binary not found for \${packageName}\`);
console.error(' This may happen if the platform package failed to install.');
process.exit(1);
}

const packageDir = path.dirname(binaryPath);
if (!verifyChecksum(binaryPath, packageDir)) {
process.exit(1);
}

createBinWrapper(binaryPath);
}

main();
`;
}

generatePlatformReadme(platform) {
return `# ${this.scope}/${this.packageName}-${platform.platform}

${platform.platform} binary for ${this.scope}/${this.packageName}.

**This is a platform-specific package.** You should install the main package instead:

\`\`\`bash
npm install ${this.scope}/${this.packageName}
\`\`\`

## Platform Details

- **OS:** ${platform.os.join(', ')}
- **CPU:** ${platform.cpu.join(', ')}
- **Version:** ${this.version}

## Binary Verification

This package includes SHA256 checksums for binary verification. The checksum is verified automatically during installation.

## License

MIT
`;
}

generateMainReadme() {
return `# ${this.scope}/${this.packageName}

My awesome CLI tool.

## Installation

\`\`\`bash
npm install ${this.scope}/${this.packageName}
\`\`\`

This will automatically install the correct binary for your platform.

## Supported Platforms

- macOS (Intel and Apple Silicon)
- Linux (x64 and ARM64, glibc and musl)
- Windows (x64)

## Usage

\`\`\`bash
${this.packageName} --help
\`\`\`

## Development

See [CONTRIBUTING.md](https://github.com/${this.scope.replace('@', '')}/${this.packageName}/blob/main/CONTRIBUTING.md) for development instructions.

## License

MIT
`;
}
}

// CLI
const args = process.argv.slice(2);
const options = {
version: args[args.indexOf('--version') + 1],
binariesDir: args[args.indexOf('--binaries') + 1],
outputDir: args[args.indexOf('--output') + 1],
packageName: args[args.indexOf('--name') + 1] || 'myapp',
scope: args[args.indexOf('--scope') + 1] || '@myorg'
};

if (!options.version || !options.binariesDir) {
console.error('Usage: node generate-npm-packages.js --version 1.0.0 --binaries dist/ [--output packages/] [--name myapp] [--scope @myorg]');
process.exit(1);
}

const generator = new PackageGenerator(options);
generator.generate();

GitHub Actions npm Publish Workflow

name: Publish npm Packages

on:
push:
tags: ['v*']

permissions:
contents: read
id-token: write # For npm provenance

env:
NODE_VERSION: 18

jobs:
# Stage 1: Download binaries from release workflow
download-binaries:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Get version
id: version
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

- name: Download release binaries
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p dist/
gh release download "v${{ steps.version.outputs.version }}" -p '*' -D dist/

- name: Upload binaries
uses: actions/upload-artifact@v4
with:
name: binaries
path: dist/
retention-days: 1

outputs:
version: ${{ steps.version.outputs.version }}

# Stage 2: Generate all npm packages
generate-packages:
needs: download-binaries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Download binaries
uses: actions/download-artifact@v4
with:
name: binaries
path: dist/

- name: Generate npm packages
run: |
node scripts/generate-npm-packages.js \
--version ${{ needs.download-binaries.outputs.version }} \
--binaries dist/ \
--output packages/ \
--name myapp \
--scope @myorg

- name: Upload packages
uses: actions/upload-artifact@v4
with:
name: npm-packages
path: packages/
retention-days: 1

# Stage 3: Publish platform packages
publish-platform-packages:
needs: generate-packages
runs-on: ubuntu-latest
strategy:
matrix:
platform:
- darwin-x64
- darwin-arm64
- linux-x64
- linux-arm64
- linux-x64-musl
- win32-x64
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'

- name: Download packages
uses: actions/download-artifact@v4
with:
name: npm-packages
path: packages/

- name: Publish @myorg/myapp-${{ matrix.platform }}
working-directory: packages/myapp-${{ matrix.platform }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm publish --provenance --access public

# Stage 4: Publish main package (after all platforms published)
publish-main-package:
needs: publish-platform-packages
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org'

- name: Download packages
uses: actions/download-artifact@v4
with:
name: npm-packages
path: packages/

- name: Publish @myorg/myapp
working-directory: packages/myapp
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm publish --provenance --access public

- name: Verify installation
run: |
npx @myorg/myapp@latest --version

Level 3: Advanced Patterns

Persona-Specific Packages

Create specialized packages for different user types:

// generate-persona-packages.js

class PersonaPackageGenerator extends PackageGenerator {
generateDeveloperPackage() {
const packageDir = path.join(this.outputDir, `${this.packageName}-dev`);

const packageJson = {
name: `${this.scope}/${this.packageName}-dev`,
version: this.version,
description: 'Developer package with source code and build tools',
main: 'src/index.js',
bin: {
[this.packageName]: 'bin/' + this.packageName,
[`${this.packageName}-dev`]: 'bin/dev'
},
scripts: {
build: 'cargo build --release',
test: 'cargo test',
lint: 'cargo clippy',
format: 'cargo fmt'
},
devDependencies: {
// Testing and development tools
},
dependencies: {
[`${this.scope}/${this.packageName}`]: this.version
},
files: ['src/', 'tests/', 'Cargo.toml', 'build.rs', 'bin/', 'README.md'],
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`,
directory: 'packages/dev'
},
license: 'MIT'
};

// Include source code, tests, build configuration
this.copyDirectory(path.join(this.sourceDir, 'src'), path.join(packageDir, 'src'));
this.copyDirectory(path.join(this.sourceDir, 'tests'), path.join(packageDir, 'tests'));
fs.copyFileSync(path.join(this.sourceDir, 'Cargo.toml'), path.join(packageDir, 'Cargo.toml'));

fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);
}

generateContributorPackage() {
const packageDir = path.join(this.outputDir, `${this.packageName}-contrib`);

const packageJson = {
name: `${this.scope}/${this.packageName}-contrib`,
version: this.version,
description: 'Contributor package with full source and development environment',
dependencies: {
[`${this.scope}/${this.packageName}-dev`]: this.version
},
scripts: {
setup: 'node scripts/setup-dev-env.js',
'check-commit': 'node scripts/check-commit-msg.js',
'pre-commit': 'cargo fmt && cargo clippy',
'prepare-pr': 'node scripts/prepare-pr.js'
},
files: [
'CONTRIBUTING.md',
'CODE_OF_CONDUCT.md',
'.github/',
'docs/',
'scripts/',
'examples/'
],
repository: {
type: 'git',
url: `https://github.com/${this.scope.replace('@', '')}/${this.packageName}.git`
},
license: 'MIT'
};

// Include contribution guidelines, examples, development scripts
this.generateContributorDocs(packageDir);

fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2) + '\n'
);
}
}

Version Synchronization Automation

// sync-versions.js

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

class VersionSynchronizer {
constructor(newVersion, packagesDir) {
this.newVersion = newVersion;
this.packagesDir = packagesDir;
}

sync() {
console.log(`Synchronizing all packages to version ${this.newVersion}...`);

// Update all package.json files
const packages = fs.readdirSync(this.packagesDir);

for (const pkg of packages) {
const packageJsonPath = path.join(this.packagesDir, pkg, 'package.json');

if (!fs.existsSync(packageJsonPath)) continue;

const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.version = this.newVersion;

// Update optionalDependencies versions in main package
if (packageJson.optionalDependencies) {
for (const dep in packageJson.optionalDependencies) {
if (dep.includes(this.packageName)) {
packageJson.optionalDependencies[dep] = this.newVersion;
}
}
}

fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + '\n'
);

console.log(`${pkg}`);
}

// Update installation docs
this.updateDocs();

// Commit changes
this.commitChanges();

console.log('✓ Version synchronization complete');
}

updateDocs() {
// Update version references in README files
const readmePaths = [
'README.md',
'docs/INSTALLATION.md',
'packages/myapp/README.md'
];

for (const readmePath of readmePaths) {
if (!fs.existsSync(readmePath)) continue;

let content = fs.readFileSync(readmePath, 'utf8');

// Replace version references
content = content.replace(
/@myorg\/myapp@[\d.]+/g,
`@myorg/myapp@${this.newVersion}`
);

fs.writeFileSync(readmePath, content);
}
}

commitChanges() {
execSync('git add packages/*/package.json README.md docs/', { stdio: 'inherit' });
execSync(`git commit -m "chore: Bump version to ${this.newVersion}"`, { stdio: 'inherit' });
execSync(`git tag v${this.newVersion}`, { stdio: 'inherit' });
}
}

// CLI
const newVersion = process.argv[2];
if (!newVersion) {
console.error('Usage: node sync-versions.js 1.0.0');
process.exit(1);
}

const synchronizer = new VersionSynchronizer(newVersion, 'packages');
synchronizer.sync();

Enhanced Postinstall with Fallback

#!/usr/bin/env node
/**
* Advanced postinstall script with:
* - Platform detection (including musl detection)
* - Checksum verification
* - Fallback download from CDN
* - Graceful error handling
*/

const fs = require('fs');
const path = require('path');
const https = require('https');
const { spawnSync } = require('child_process');
const crypto = require('crypto');

const PACKAGE_NAME = 'myapp';
const SCOPE = '@myorg';
const CDN_BASE = 'https://cdn.example.com/releases';

class BinaryInstaller {
constructor() {
this.platform = this.detectPlatform();
this.version = this.getVersion();
}

detectPlatform() {
const platform = process.platform;
const arch = process.arch;
const key = `${platform}-${arch}`;

// Check for musl on Linux
if (key === 'linux-x64') {
const lddOutput = spawnSync('ldd', ['--version'], { encoding: 'utf8' });
if (lddOutput.stdout && lddOutput.stdout.includes('musl')) {
return 'linux-x64-musl';
}
}

const PLATFORM_MAP = {
'darwin-x64': 'darwin-x64',
'darwin-arm64': 'darwin-arm64',
'linux-x64': 'linux-x64',
'linux-arm64': 'linux-arm64',
'win32-x64': 'win32-x64'
};

return PLATFORM_MAP[key];
}

getVersion() {
const packageJsonPath = path.join(__dirname, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
}

async install() {
console.log(`Installing ${PACKAGE_NAME} v${this.version} for ${this.platform}...`);

if (!this.platform) {
console.error(`✗ Unsupported platform: ${process.platform}-${process.arch}`);
console.error('Supported platforms: darwin-x64, darwin-arm64, linux-x64, linux-arm64, linux-x64-musl, win32-x64');
process.exit(1);
}

// Try to find binary in optional dependency
let binaryPath = this.findBinaryInNodeModules();

if (!binaryPath) {
console.warn('⚠ Platform package not found, attempting CDN download...');
binaryPath = await this.downloadFromCDN();
}

if (!binaryPath) {
console.error('✗ Failed to install binary');
process.exit(1);
}

// Verify checksum
if (!this.verifyChecksum(binaryPath)) {
console.error('✗ Checksum verification failed');
process.exit(1);
}

// Create bin wrapper
this.createBinWrapper(binaryPath);

console.log('✓ Installation complete');
}

findBinaryInNodeModules() {
const packageName = `${SCOPE}/${PACKAGE_NAME}-${this.platform}`;
const nodeModulesPath = path.join(__dirname, 'node_modules', packageName);
const binaryName = process.platform === 'win32' ? `${PACKAGE_NAME}.exe` : PACKAGE_NAME;
const binaryPath = path.join(nodeModulesPath, binaryName);

if (fs.existsSync(binaryPath)) {
console.log(`✓ Found binary in ${packageName}`);
return binaryPath;
}

return null;
}

async downloadFromCDN() {
const binaryName = process.platform === 'win32' ? `${PACKAGE_NAME}.exe` : PACKAGE_NAME;
const url = `${CDN_BASE}/v${this.version}/${PACKAGE_NAME}-${this.platform}${process.platform === 'win32' ? '.exe' : ''}`;
const downloadPath = path.join(__dirname, 'bin', binaryName);

console.log(`Downloading from ${url}...`);

try {
await this.downloadFile(url, downloadPath);
console.log('✓ Download complete');
return downloadPath;
} catch (error) {
console.error(`✗ Download failed: ${error.message}`);
return null;
}
}

downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const destDir = path.dirname(dest);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}

const file = fs.createWriteStream(dest);

https.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}`));
return;
}

response.pipe(file);

file.on('finish', () => {
file.close();
fs.chmodSync(dest, 0o755);
resolve();
});
}).on('error', (error) => {
fs.unlinkSync(dest);
reject(error);
});
});
}

verifyChecksum(binaryPath) {
const sumsFile = path.join(path.dirname(binaryPath), 'SHA256SUMS');

if (!fs.existsSync(sumsFile)) {
console.warn('⚠ No SHA256SUMS file, skipping verification');
return true;
}

const expectedSum = fs.readFileSync(sumsFile, 'utf8').split(' ')[0];
const fileBuffer = fs.readFileSync(binaryPath);
const actualSum = crypto.createHash('sha256').update(fileBuffer).digest('hex');

if (actualSum !== expectedSum) {
console.error(`Checksum mismatch:`);
console.error(` Expected: ${expectedSum}`);
console.error(` Actual: ${actualSum}`);
return false;
}

console.log('✓ Checksum verified');
return true;
}

createBinWrapper(binaryPath) {
const binDir = path.join(__dirname, 'bin');

if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, { recursive: true });
}

const targetPath = path.join(binDir, process.platform === 'win32' ? `${PACKAGE_NAME}.cmd` : PACKAGE_NAME);

if (process.platform === 'win32') {
const wrapper = `@echo off\n"${binaryPath}" %*\n`;
fs.writeFileSync(targetPath, wrapper);
} else {
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
fs.symlinkSync(binaryPath, targetPath);
fs.chmodSync(targetPath, 0o755);
}

console.log(`✓ Binary available at ${targetPath}`);
}
}

// Main
const installer = new BinaryInstaller();
installer.install().catch((error) => {
console.error('Installation failed:', error);
process.exit(1);
});

Integration Points

Works with:

  • binary-release-workflow skill - Generates binaries for npm packaging
  • code-signing skill - Signs binaries before npm publish
  • sha256-verification skill - Verifies binary integrity

Calls:

  • Bash tool for binary operations and npm publish
  • Write tool for package.json generation
  • Read tool for version synchronization

Success Criteria

npm binary packaging is complete when:

  • All 7 packages generated (1 main + 6 platforms)
  • Platform packages contain correct binaries with checksums
  • Main package has correct optionalDependencies
  • Postinstall script successfully locates and configures binary
  • All packages published to npm registry
  • Installation tested on all 6 platforms
  • npm install @myorg/myapp installs correct binary
  • Binary executable via npx or global install
  • Version synchronized across all packages

Common Issues

Platform package installation fails:

# Check optionalDependencies format
cat packages/myapp/package.json | jq .optionalDependencies

# Verify platform package exists on npm
npm view @myorg/myapp-darwin-x64

Postinstall script can't find binary:

# Check node_modules structure
ls -la node_modules/@myorg/

# Verify platform detection
node -e "console.log(process.platform, process.arch)"

Checksum verification fails:

# Regenerate checksums
shasum -a 256 myapp > SHA256SUMS

# Verify checksum file format
cat SHA256SUMS
# Should be: <hash> <filename>

npm publish fails with 403:

# Verify npm authentication
npm whoami

# Check package scope/organization access
npm access ls-packages @myorg

# Re-authenticate
npm login

Additional Resources

  • scripts/generate-npm-packages.js: Complete package generation
  • scripts/sync-versions.js: Version synchronization automation
  • .github/workflows/publish-npm.yml: npm publish automation
  • examples/: Installation verification scripts

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: npm-binary-packaging

Completed:
- [x] Main package generated (@myorg/myapp)
- [x] 6 platform packages created (darwin-x64, darwin-arm64, linux-x64, linux-arm64, linux-x64-musl, win32-x64)
- [x] optionalDependencies configured correctly
- [x] postinstall.js created with platform detection
- [x] SHA256 checksums generated for all binaries
- [x] All packages published to npm registry
- [x] Installation tested on all platforms

Outputs:
- packages/myapp/package.json (main)
- packages/myapp/postinstall.js
- packages/myapp-{platform}/package.json (×6)
- packages/myapp-{platform}/SHA256SUMS (×6)

Verification:
- npm view @myorg/myapp (published ✓)
- npm install @myorg/myapp (platform binary detected ✓)
- npx @myorg/myapp --version (executable ✓)

Completion Checklist

Before marking this skill as complete, verify:

  • All 7 package directories created (1 main + 6 platforms)
  • Main package.json has correct optionalDependencies entries
  • Each platform package has correct os/cpu restrictions
  • Binaries copied to platform packages with correct names
  • SHA256SUMS files generated and match binaries
  • postinstall.js detects platform correctly (including musl)
  • postinstall.js creates bin/ wrapper (symlink or .cmd)
  • Version synchronized across all packages
  • All 7 packages published to npm (main published last)
  • Test installation on at least 3 platforms
  • Binary executable after global install: npm install -g @myorg/myapp

Failure Indicators

This skill has FAILED if:

  • ❌ Platform package installation fails (optionalDependencies error)
  • ❌ postinstall.js cannot find platform binary in node_modules
  • ❌ Checksum verification fails (binary corrupted)
  • ❌ Binary not executable (permission issues, missing wrapper)
  • ❌ npm publish fails (authentication, package name conflict)
  • ❌ Version mismatch between main and platform packages
  • ❌ Wrong binary installed (platform detection logic error)
  • ❌ Musl vs glibc detection incorrect on Linux

When NOT to Use

Do NOT use this skill when:

  • Package is pure JavaScript/TypeScript (no native binaries needed)
  • Only targeting single platform (simplified packaging sufficient)
  • Binaries downloaded at runtime (different distribution pattern)
  • Using electron or other bundling approach (different packaging model)
  • Package not intended for npm (Docker, homebrew, apt, etc.)
  • Binary size >100MB (npm package size limits)

Use alternatives:

  • electron-builder - For Electron app packaging
  • pkg - For bundling Node.js apps as single binary
  • docker-packaging - For container-based distribution
  • github-releases - For direct binary downloads
  • cargo-publish - For Rust crates (native packaging)

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Including all binaries in main package600MB+ download for all platformsUse optionalDependencies pattern
No checksum verificationCorrupted binaries undetectedAlways include SHA256SUMS
Hardcoded binary pathsBreaks on different platformsUse platform detection in postinstall
Publishing main before platformsInstallation fails (404 on platform packages)Publish platforms first, main last
Forgetting musl detectionWrong binary on Alpine LinuxCheck ldd output for musl
No fallback downloadInstallation fails if platform package unavailableImplement CDN fallback in postinstall
Version driftPlatform packages outdatedUse sync-versions.js automation
No provenanceSupply chain security concernsUse npm publish --provenance

Principles

This skill embodies:

  • #1 Search Before Create - Uses established npm patterns (optionalDependencies)
  • #5 Eliminate Ambiguity - Platform detection logic handles all edge cases (musl, arch)
  • #6 Clear, Understandable, Explainable - README explains installation flow clearly
  • #8 No Assumptions - Postinstall validates binary exists before creating wrapper
  • Automation - Package generation, version sync, and publishing fully automated
  • Security - Checksums, provenance, and binary verification

Standard: CODITECT-STANDARD-AUTOMATION.md


Version: 1.1.0 | Updated: 2026-01-04 | Quality Standard Applied