DEV Community

Wilson Xu
Wilson Xu

Posted on

Ship Your CLI Tool Everywhere: npm, Homebrew, Docker, and GitHub Releases

Ship Your CLI Tool Everywhere: npm, Homebrew, Docker, and GitHub Releases

You published your Node.js CLI on npm. That covers JavaScript developers. But what about the Python developer who needs your tool in CI? The DevOps engineer who uses Homebrew? The team that runs everything in Docker?

Distributing a CLI tool through multiple channels is easier than you think. This article shows how to package one Node.js CLI for four distribution methods — covering 95% of how developers install tools.

Channel 1: npm (You're Already Here)

npm install -g mytool
Enter fullscreen mode Exit fullscreen mode

npm is the default for JavaScript tools. If your package.json has a bin field, you're done:

{
  "name": "mytool",
  "bin": { "mytool": "./bin/mytool.js" },
  "files": ["bin", "lib"]
}
Enter fullscreen mode Exit fullscreen mode

npx for Zero-Install Usage

Users can run your tool without installing:

npx mytool scan .
Enter fullscreen mode Exit fullscreen mode

This downloads, runs, and cleans up. Perfect for one-off usage and CI pipelines.

Channel 2: GitHub Releases with Standalone Binaries

Not everyone has Node.js installed. Compile your CLI into a standalone binary using pkg or bun build:

Using bun build --compile

# Build standalone binaries
bun build ./bin/mytool.js --compile --target=bun-linux-x64 --outfile=mytool-linux-x64
bun build ./bin/mytool.js --compile --target=bun-darwin-arm64 --outfile=mytool-darwin-arm64
bun build ./bin/mytool.js --compile --target=bun-windows-x64 --outfile=mytool-windows-x64.exe
Enter fullscreen mode Exit fullscreen mode

Automate with GitHub Actions

# .github/workflows/release.yml
name: Release
on:
  push:
    tags: ['v*']

jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: bun-linux-x64
            artifact: mytool-linux-x64
          - os: macos-latest
            target: bun-darwin-arm64
            artifact: mytool-darwin-arm64
          - os: windows-latest
            target: bun-windows-x64
            artifact: mytool-windows-x64.exe
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install
      - run: bun build ./bin/mytool.js --compile --target=${{ matrix.target }} --outfile=${{ matrix.artifact }}
      - uses: softprops/action-gh-release@v2
        with:
          files: ${{ matrix.artifact }}
Enter fullscreen mode Exit fullscreen mode

Now users can download binaries directly:

# Linux/macOS
curl -L https://github.com/you/mytool/releases/latest/download/mytool-linux-x64 -o mytool
chmod +x mytool
./mytool scan .
Enter fullscreen mode Exit fullscreen mode

Channel 3: Homebrew

macOS developers expect Homebrew. Create a formula:

# Formula/mytool.rb
class Mytool < Formula
  desc "Your tool description"
  homepage "https://github.com/you/mytool"
  url "https://github.com/you/mytool/releases/download/v1.0.0/mytool-darwin-arm64"
  sha256 "abc123..."  # shasum -a 256 mytool-darwin-arm64
  license "MIT"

  def install
    bin.install "mytool-darwin-arm64" => "mytool"
  end

  test do
    assert_match "mytool", shell_output("#{bin}/mytool --version")
  end
end
Enter fullscreen mode Exit fullscreen mode

Self-Hosted Tap

Create a repo homebrew-tap with your formulas:

# Users install with:
brew tap you/tap
brew install mytool
Enter fullscreen mode Exit fullscreen mode

Auto-Update Formula on Release

# In your release workflow, add:
- name: Update Homebrew formula
  env:
    TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
  run: |
    SHA=$(shasum -a 256 mytool-darwin-arm64 | cut -d' ' -f1)
    VERSION=${GITHUB_REF#refs/tags/v}

    git clone https://x-access-token:${TAP_TOKEN}@github.com/you/homebrew-tap.git
    cd homebrew-tap
    sed -i '' "s|url \".*\"|url \"https://github.com/you/mytool/releases/download/v${VERSION}/mytool-darwin-arm64\"|" Formula/mytool.rb
    sed -i '' "s|sha256 \".*\"|sha256 \"${SHA}\"|" Formula/mytool.rb
    git commit -am "Update mytool to ${VERSION}"
    git push
Enter fullscreen mode Exit fullscreen mode

Channel 4: Docker

For CI pipelines and teams that containerize everything:

# Dockerfile
FROM node:20-alpine
RUN npm install -g mytool
ENTRYPOINT ["mytool"]
Enter fullscreen mode Exit fullscreen mode

Or with a standalone binary for a tiny image:

FROM debian:bookworm-slim
COPY mytool-linux-x64 /usr/local/bin/mytool
RUN chmod +x /usr/local/bin/mytool
ENTRYPOINT ["mytool"]
Enter fullscreen mode Exit fullscreen mode
# Build and push
docker build -t you/mytool .
docker push you/mytool

# Users run with:
docker run --rm -v $(pwd):/app you/mytool scan /app
Enter fullscreen mode Exit fullscreen mode

GitHub Container Registry

- name: Push to GHCR
  run: |
    echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
    docker build -t ghcr.io/${{ github.repository }}:latest .
    docker push ghcr.io/${{ github.repository }}:latest
Enter fullscreen mode Exit fullscreen mode

The Release Script

Tie everything together:

{
  "scripts": {
    "release": "npm version $1 && git push --follow-tags",
    "build:binaries": "bun build ./bin/mytool.js --compile --target=bun-linux-x64 --outfile=dist/mytool-linux-x64 && bun build ./bin/mytool.js --compile --target=bun-darwin-arm64 --outfile=dist/mytool-darwin-arm64",
    "build:docker": "docker build -t mytool ."
  }
}
Enter fullscreen mode Exit fullscreen mode

The workflow:

  1. npm version patch — bumps version, creates git tag
  2. git push --follow-tags — pushes code and tag
  3. GitHub Actions triggers on tag → builds binaries → creates release → updates Homebrew → pushes Docker image
  4. npm publishes via prepublishOnly script

One git push, four distribution channels updated automatically.

Which Channels Do You Need?

Channel Best For Setup Effort
npm JS developers, quick prototyping Already done
npx One-off usage, CI without install Free with npm
GitHub Releases Non-JS developers, air-gapped environments Medium
Homebrew macOS developers Low (with tap)
Docker CI pipelines, containerized teams Low

Start with npm. Add GitHub Releases when non-JS users ask for your tool. Add Homebrew when macOS users complain. Add Docker when DevOps teams need it in pipelines.

Conclusion

Distribution is a multiplier. The same tool, available through four channels, reaches four different audiences. The initial setup takes an afternoon. After that, every release automatically updates every channel with a single git push.


Wilson Xu distributes 11+ CLI tools across npm and other channels. Find them at npm and follow at dev.to/chengyixu.

Top comments (0)