CLI Distribution
Package and distribute CLI tools via npm/npx, standalone binaries with pkg, and Homebrew taps
You are an expert in packaging and distributing command-line tools through npm, standalone executables, and Homebrew. ## Key Points - Add a `engines.node` field specifying your minimum Node.js version, and test against it in CI — users on older Node versions get a clear error at install time instead of cryptic runtime failures. - For `npx` usage, keep install time under 5 seconds by minimizing dependencies — users expect `npx` tools to feel instant, and a 200MB dependency tree defeats the purpose. - **Forgetting the shebang line** — without `#!/usr/bin/env node` at the top of the bin entry, Linux and macOS will try to execute the file as a shell script, producing baffling syntax errors. - Forgetting the shebang line (`#!/usr/bin/env node`) in the bin entry point — without it, Linux/macOS will try to execute the file as a shell script and fail with syntax errors. ## Quick Example ```js #!/usr/bin/env node // bin/cli.js import '../dist/index.js'; ``` ```bash chmod +x bin/cli.js npm publish ```
skilldb get cli-development-skills/CLI DistributionFull skill: 260 linesPackaging & Distributing CLI Tools — CLI Development
You are an expert in packaging and distributing command-line tools through npm, standalone executables, and Homebrew.
Overview
Distributing a CLI tool involves making it easy for users to install and run. The three main channels are: npm (for Node.js users, supports npx for zero-install execution), standalone binaries via pkg or similar bundlers (for users without Node.js), and Homebrew (for macOS/Linux users who prefer brew install).
Setup & Configuration
npm distribution
Configure package.json for CLI publishing:
{
"name": "my-cli",
"version": "1.0.0",
"description": "A CLI tool for doing things",
"type": "module",
"bin": {
"my-cli": "./bin/cli.js"
},
"files": [
"bin/",
"dist/",
"LICENSE"
],
"engines": {
"node": ">=18"
},
"keywords": ["cli", "tool"],
"repository": {
"type": "git",
"url": "https://github.com/user/my-cli"
}
}
Create the bin entry point:
#!/usr/bin/env node
// bin/cli.js
import '../dist/index.js';
Make it executable and publish:
chmod +x bin/cli.js
npm publish
Users can then:
# Install globally
npm install -g my-cli
my-cli --help
# Or run without installing
npx my-cli --help
Core Patterns
Standalone binaries with pkg
npm install --save-dev @yao-pkg/pkg
Configure package.json:
{
"pkg": {
"targets": ["node18-linux-x64", "node18-macos-x64", "node18-win-x64"],
"outputPath": "release",
"assets": ["templates/**/*", "config.default.json"]
},
"scripts": {
"build:binaries": "pkg . --compress GZip"
}
}
Build and verify:
npm run build:binaries
# Produces:
# release/my-cli-linux
# release/my-cli-macos
# release/my-cli-win.exe
Standalone binaries with esbuild + sea (Node.js Single Executable)
# Bundle your CLI to a single file
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/cli.cjs --format=cjs
# Create SEA config
cat > sea-config.json << 'EOF'
{
"main": "dist/cli.cjs",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true
}
EOF
# Generate the blob and inject into node binary
node --experimental-sea-config sea-config.json
cp $(which node) my-cli
npx postject my-cli NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
Homebrew tap distribution
Create a GitHub repository named homebrew-tap, then add a formula:
# Formula/my-cli.rb
class MyCli < Formula
desc "A CLI tool for doing things"
homepage "https://github.com/user/my-cli"
url "https://github.com/user/my-cli/releases/download/v1.0.0/my-cli-macos-x64.tar.gz"
sha256 "abc123..."
license "MIT"
def install
bin.install "my-cli"
end
test do
assert_match "my-cli v1.0.0", shell_output("#{bin}/my-cli --version")
end
end
Users install with:
brew tap user/tap
brew install my-cli
GitHub Release automation
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: node18-linux-x64
artifact: my-cli-linux
- os: macos-latest
target: node18-macos-x64
artifact: my-cli-macos
- os: windows-latest
target: node18-win-x64
artifact: my-cli-win.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci && npm run build
- run: npx @yao-pkg/pkg . --target ${{ matrix.target }} --output ${{ matrix.artifact }}
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- uses: softprops/action-gh-release@v2
with:
files: |
my-cli-linux/my-cli-linux
my-cli-macos/my-cli-macos
my-cli-win.exe/my-cli-win.exe
Prepublish checklist script
{
"scripts": {
"prepublishOnly": "npm run build && npm run test && npm run lint",
"prepack": "rm -rf dist && npm run build"
}
}
Version management and changelogs
# Bump version, create git tag, update changelog
npx changeset
npx changeset version
npx changeset publish
# Or simpler with npm
npm version patch -m "Release %s"
npm publish
git push --follow-tags
Best Practices
- Always include a
filesfield inpackage.jsonto whitelist published files — without it, npm publishes everything not in.gitignore, which may include tests, source maps, and config files that bloat the package. - Add a
engines.nodefield specifying your minimum Node.js version, and test against it in CI — users on older Node versions get a clear error at install time instead of cryptic runtime failures. - For
npxusage, keep install time under 5 seconds by minimizing dependencies — users expectnpxtools to feel instant, and a 200MB dependency tree defeats the purpose.
Core Philosophy
Distribution is the last mile of CLI development, and it determines whether your tool is adopted or abandoned. A CLI that takes 30 seconds to install via npx will lose users to a competitor that takes 3 seconds. Minimize dependencies, whitelist published files, and test the install experience on a clean machine — not your development environment where everything is already cached.
Meet users where they are. Node.js developers expect npm install -g or npx. System administrators expect Homebrew taps or standalone binaries. DevOps engineers expect Docker images. Offering multiple distribution channels is not redundant — it is respectful of different workflows and environments. Start with npm, add standalone binaries for non-Node users, and add Homebrew if your audience includes macOS developers.
Automate everything about the release process. Version bumping, changelog generation, binary compilation, store publishing, and tag creation should all happen in CI when you push a version tag. Manual releases are error-prone, inconsistent, and demoralizing — especially when you need to publish to multiple channels simultaneously.
Anti-Patterns
-
Publishing without a
fileswhitelist — omitting thefilesfield inpackage.jsonpublishes tests, source maps, development configs, and other junk that bloats the package and may leak sensitive information. -
Not testing the install experience — running
npm packand installing the resulting tarball in a clean directory catches missing files, broken shebangs, and import errors that development mode hides. -
Bundling unnecessary dependencies for
npxusage — a 200MB dependency tree defeats the purpose of zero-install execution; audit dependencies and remove or replace heavy ones for tools intended to run vianpx. -
Hardcoding platform-specific paths in the binary — using
/usr/local/orC:\Program Files\instead of portable path resolution breaks the CLI on unexpected platforms and containerized environments. -
Forgetting the shebang line — without
#!/usr/bin/env nodeat the top of the bin entry, Linux and macOS will try to execute the file as a shell script, producing baffling syntax errors.
Common Pitfalls
- Forgetting the shebang line (
#!/usr/bin/env node) in the bin entry point — without it, Linux/macOS will try to execute the file as a shell script and fail with syntax errors. - Publishing with
"type": "module"but using.jsextensions in thebinfield without ensuring the file is valid ESM — on older npm versions or edge cases this can fail; using an explicit.mjsextension for the bin entry is safer.
Install this skill directly: skilldb add cli-development-skills
Related Skills
Chalk Picocolors
Style terminal output with chalk and picocolors for colored, formatted CLI text
Clack Prompts
Build beautiful interactive CLI prompts using @clack/prompts with minimal boilerplate
CLI Testing
Test CLI applications using subprocess execution, mock filesystems, and snapshot testing
Commander Js
Build structured CLI applications with Commander.js including commands, options, and argument parsing
Ink React CLI
Build rich interactive terminal UIs using Ink, a React renderer for the command line
Oclif Framework
Build production-grade, extensible CLI tools using the oclif framework with TypeScript