-
Notifications
You must be signed in to change notification settings - Fork 13
Description
Problem
The current distribution approach for hookdeck-cli relies exclusively on postinstall scripts via go-npm, which creates a poor developer experience with modern package managers, particularly pnpm v10+.
Impact on Users
When users install hookdeck-cli with pnpm v10+, they encounter this error:
$ pnpm install -D hookdeck-cli
# Install succeeds but shows warning about ignored build scripts
$ pnpm hookdeck
./node_modules/.bin/hookdeck: cannot execute binary fileRoot Cause: pnpm v10+ blocks lifecycle scripts by default for security reasons. The package ships with a Linux x86-64 binary as a placeholder, and the postinstall script is supposed to download the correct platform-specific binary. When the script is blocked, users are left with the wrong binary.
Current Workarounds (Poor UX)
Users must manually configure pnpm before installation:
Option 1: Create pnpm-workspace.yaml before installing:
onlyBuiltDependencies:
- hookdeck-cliOption 2: Add to package.json before installing:
{
"pnpm": {
"onlyBuiltDependencies": ["hookdeck-cli"]
}
}Option 3: Post-install approval (multi-step):
pnpm install -D hookdeck-cli # Fails
pnpm approve-builds # Interactive selection required
pnpm rebuild hookdeck-cli # Finally worksNone of these are acceptable for a good CLI installation experience.
Reproduction Steps
- Create new project with pnpm v10+
- Run
pnpm install -D hookdeck-cli - Try to run
pnpm hookdeck --version - Result:
cannot execute binary fileerror
Environment:
- pnpm v10.19.0
- macOS (darwin arm64)
- hookdeck-cli v1.1.0
Recommended Solution
Switch to the optionalDependencies pattern used by industry-standard tools like esbuild, swc, and Sentry CLI. This approach:
- ✅ Works with pnpm v10+ without configuration
- ✅ No postinstall scripts required
- ✅ Works with
--ignore-scriptsflag - ✅ Faster installs (only downloads ~1-2MB for one platform)
- ✅ Better offline support (binaries in npm registry)
- ✅ Can still use postinstall as optional fallback
Implementation Pattern
1. Create platform-specific packages:
Publish separate packages for each platform:
@hookdeck/cli-darwin-arm64
@hookdeck/cli-darwin-x64
@hookdeck/cli-linux-x64
@hookdeck/cli-linux-arm64
@hookdeck/cli-win32-x64
etc.
Each platform package contains:
{
"name": "@hookdeck/cli-darwin-arm64",
"version": "1.1.0",
"os": ["darwin"],
"cpu": ["arm64"],
"bin": {
"hookdeck": "./hookdeck"
},
"files": ["hookdeck"]
}2. Update main package to use optionalDependencies:
{
"name": "hookdeck-cli",
"version": "1.1.0",
"bin": {
"hookdeck": "./bin/hookdeck.js"
},
"optionalDependencies": {
"@hookdeck/cli-darwin-arm64": "1.1.0",
"@hookdeck/cli-darwin-x64": "1.1.0",
"@hookdeck/cli-linux-x64": "1.1.0",
"@hookdeck/cli-linux-arm64": "1.1.0",
"@hookdeck/cli-win32-x64": "1.1.0",
"@hookdeck/cli-win32-arm64": "1.1.0"
}
}3. Create JavaScript wrapper to detect and run correct binary:
#!/usr/bin/env node
// bin/hookdeck.js
const { execFileSync } = require('child_process');
const { existsSync } = require('fs');
const path = require('path');
function getPlatformBinary() {
const platform = process.platform;
const arch = process.arch;
const packageName = `@hookdeck/cli-${platform}-${arch}`;
try {
const binaryPath = require.resolve(`${packageName}/hookdeck`);
if (existsSync(binaryPath)) {
return binaryPath;
}
} catch (e) {
// Platform package not installed
}
// Fallback: could still use postinstall download as backup
const fallbackPath = path.join(__dirname, 'hookdeck');
if (existsSync(fallbackPath)) {
return fallbackPath;
}
console.error(`Unsupported platform: ${platform}-${arch}`);
console.error('Please report this issue at https://github.com/hookdeck/hookdeck-cli/issues');
process.exit(1);
}
try {
const binaryPath = getPlatformBinary();
execFileSync(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
} catch (error) {
process.exit(error.status || 1);
}4. (Optional) Keep postinstall as fallback:
The postinstall script can remain as a fallback mechanism for edge cases, but the CLI will work without it:
{
"scripts": {
"postinstall": "node scripts/install-fallback.js"
}
}How npm/pnpm Handle This
When a user runs pnpm install hookdeck-cli:
- npm/pnpm reads the
optionalDependencieslist - Automatically filters based on current platform's
osandcpu - Only installs the matching platform package (e.g.,
@hookdeck/cli-darwin-arm64) - The wrapper script detects and executes that binary
- No postinstall script needed - everything works out of the box
Migration Benefits
- Immediate: Users can install without any pnpm configuration
- Security: Works with
--ignore-scriptsflag - Performance: Only downloads 1-2MB instead of running download script
- Reliability: Binaries are part of npm registry (better caching, offline support)
- Compatibility: Works with all package managers (npm, yarn, pnpm, bun)
References
- How esbuild solved this exact problem
- Sentry Engineering: Publishing Binaries on npm
- pnpm v10 blocks lifecycle scripts by default
- npm RFC: Package Distributions
Additional Context
This issue affects all users on pnpm v10+ (released in 2024), which is becoming the default in many environments. The current workarounds require users to understand pnpm's security model and manually configure their workspace before installation - this is not acceptable for a CLI tool that should "just work."
The optionalDependencies pattern is now the industry standard for distributing native binaries via npm. Making this change would dramatically improve the developer experience and align hookdeck-cli with best practices used by major tools in the ecosystem.