Distribution
Package and distribute your CLI to users
Building Your CLI
Development vs Production
During development, you run your CLI directly:
# Development
bunli dev
./src/index.ts --helpFor distribution, you need to build optimized binaries:
# Production build
bunli buildBuild Options
# Build traditional JS (requires Bun runtime)
bunli build
# Build standalone executable for current platform
bunli build --targets native
# Build for specific platforms
bunli build --targets darwin-arm64,linux-x64
# Build for all platforms
bunli build --targets all
# Build with custom options
bunli build \
--targets darwin-arm64,linux-x64 \
--minify \
--sourcemap \
--outdir ./distPlatform Targets
Bunli supports building for multiple platforms:
darwin-arm64- macOS Apple Silicondarwin-x64- macOS Intellinux-arm64- Linux ARM64linux-x64- Linux x64windows-x64- Windows x64
Cross-Platform Builds
Build for all platforms at once:
// bunli.config.ts
import { defineConfig } from "@bunli/core";
export default defineConfig({
build: {
targets: ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "windows-x64"],
// Optional: create `dist/<target>.tar.gz` archives (for manual distribution).
// If you're using bunli-releaser, keep this disabled (it expects per-target output dirs).
compress: true,
},
});bunli build --targets allThis creates:
dist/
├── darwin-arm64/
│ └── cli
├── darwin-x64/
│ └── cli
├── linux-arm64/
│ └── cli
├── linux-x64/
│ └── cli
└── windows-x64/
└── cli.exeWith compression enabled, also:
dist/
├── darwin-arm64.tar.gz
├── darwin-x64.tar.gz
├── linux-arm64.tar.gz
├── linux-x64.tar.gz
└── windows-x64.tar.gzStandalone Executables
Creating Single-File Executables
Bunli uses Bun's --compile flag to create standalone executables that bundle the runtime:
# Compile for current platform
bunli build --targets native
# Compile for specific platforms
bunli build --targets darwin-arm64,linux-x64
# Compile with optimization
bunli build --targets native --minify --bytecodeExecutable Size Optimization
Reduce executable size:
// bunli.config.ts
export default defineConfig({
build: {
targets: ["native"], // or specific platforms
minify: true,
external: [
// Exclude large optional dependencies
"sharp",
"sqlite3",
"@prisma/client",
],
},
});Generated Files in Distribution
When using type generation, the generated .bunli/commands.gen.ts file is automatically included in your build:
// bunli.config.ts
export default defineConfig({
name: "my-cli",
version: "1.0.0",
build: {
entry: "./src/index.ts",
outdir: "./dist",
},
});The generated types are:
- Included in builds - Automatically bundled with your CLI
- Type-safe at runtime - Full TypeScript definitions available
- Optimized for production - Minified and tree-shaken
- Cross-platform compatible - Works on all target platforms
Generated type files are automatically included in your distribution builds. No additional configuration needed.
Code Signing (macOS)
Sign your macOS executables:
# Build the executable
bunli build --targets darwin-arm64
# Sign the executable
codesign --sign "Developer ID Application: Your Name" \
--options runtime \
--entitlements entitlements.plist \
dist/darwin-arm64/my-cli
# Verify signature
codesign --verify --verbose dist/darwin-arm64/my-cliDistribution Methods
1. GitHub Releases (Recommended)
For standalone binaries, checksums, and Homebrew updates, the recommended path is to use the
bunli-releaser GitHub Action.
Official starter template:
It will:
- Build cross-platform standalone binaries from a single Ubuntu runner
- Upload stable, per-target archives plus
checksums.txt(sha256) to the GitHub Release for the tag - Update a Homebrew tap formula (macOS/Linux only)
If you use bunli-releaser, keep build.compress disabled in bunli.config.ts. The action
expects per-target build output directories.
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and publish binaries
uses: AryaLabsHQ/bunli-releaser@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Optional Homebrew update:
# provide both brew-tap and brew-token to enable tap automation
# brew-tap: <owner>/<homebrew-tap>
# brew-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
# brew-pr: "true"
# Optional:
# workdir: "."
# targets: "all"
# artifact-name: "my-cli"
# existing-assets: "fail" # or "skip"Published assets use stable naming:
- Unix:
${name}-${version}-${os}-${arch}.tar.gz - Windows:
${name}-${version}-${os}-${arch}.zip checksums.txt(sha256)
Notes:
- v1 only supports tags in the format
vX.Y.Z(strict). - Existing release asset handling defaults to
existing-assets: "fail". - Use
existing-assets: "skip"to allow reruns without re-uploading already existing assets.
2. npm Distribution
Distribute via npm for easy installation:
// package.json
{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "./dist/index.js"
},
"files": ["dist/"],
"scripts": {
"prepublishOnly": "bunli build"
}
}# Publish to npm
npm publish
# Users install with
npm install -g my-cli
# or
bunx my-cli3. Homebrew (macOS/Linux)
If you're using bunli-releaser, it will generate/update your Homebrew formula for you.
This is the expected URL and filename shape (derived from the published GitHub Release assets):
# Formula/my-cli.rb
class MyCli < Formula
desc "Description of my CLI"
homepage "https://github.com/username/my-cli"
version "1.0.0"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/username/my-cli/releases/download/v1.0.0/my-cli-1.0.0-darwin-arm64.tar.gz"
sha256 "abc123..."
else
url "https://github.com/username/my-cli/releases/download/v1.0.0/my-cli-1.0.0-darwin-x64.tar.gz"
sha256 "def456..."
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/username/my-cli/releases/download/v1.0.0/my-cli-1.0.0-linux-arm64.tar.gz"
sha256 "ghi789..."
else
url "https://github.com/username/my-cli/releases/download/v1.0.0/my-cli-1.0.0-linux-x64.tar.gz"
sha256 "jkl012..."
end
end
def install
bin.install "my-cli"
end
test do
system "#{bin}/my-cli", "--version"
end
end4. Docker Distribution
Create a Docker image:
# Dockerfile
FROM oven/bun:1-alpine AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bunli build --targets native
FROM alpine:latest
RUN apk add --no-cache libstdc++
COPY --from=builder /app/dist/my-cli /usr/local/bin/my-cli
ENTRYPOINT ["my-cli"]# Build and push
docker build -t username/my-cli:latest .
docker push username/my-cli:latest
# Users run with
docker run username/my-cli --help5. Package Managers
Scoop (Windows)
// my-cli.json
{
"version": "1.0.0",
"description": "Description of my CLI",
"homepage": "https://github.com/username/my-cli",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/username/my-cli/releases/download/v1.0.0/my-cli-1.0.0-windows-x64.zip",
"hash": "sha256:abc123..."
}
},
"bin": "my-cli.exe"
}AUR (Arch Linux)
# PKGBUILD
pkgname=my-cli
pkgver=1.0.0
pkgrel=1
pkgdesc="Description of my CLI"
arch=('x86_64' 'aarch64')
url="https://github.com/username/my-cli"
license=('MIT')
source_x86_64=("$url/releases/download/v$pkgver/my-cli-$pkgver-linux-x64.tar.gz")
source_aarch64=("$url/releases/download/v$pkgver/my-cli-$pkgver-linux-arm64.tar.gz")
package() {
install -Dm755 my-cli "$pkgdir/usr/bin/my-cli"
}Automated Releases
Using bunli-releaser (Binaries + Homebrew)
If you want GitHub Release assets (stable per-target archives + checksums.txt) and Homebrew
automation, use bunli-releaser on tag pushes:
name: Release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: AryaLabsHQ/bunli-releaser@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Optional Homebrew update (macOS/Linux):
# brew-tap: <owner>/<homebrew-tap>
# brew-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
# brew-pr: "true"
# existing-assets: "fail" # or "skip"build.compress disabled when using bunli-releaser.Using bunli release (npm + tags)
Bunli also provides a bunli release command for automating version bumps, git tags, and npm
publishing. It can also publish binary packages to npm via release.binary
(optionalDependencies + generated shim). For GitHub release assets/checksums/Homebrew automation,
use bunli-releaser.
# Create a new release
bunli release
# Release with specific version
bunli release --version 2.0.0
# Release without npm publish
bunli release --npm=false
# Dry run
bunli release --dryWhen npm publishing is enabled, --dry runs npm publish --dry-run.
Release Configuration
// bunli.config.ts
export default defineConfig({
release: {
npm: true, // Publish to npm
github: true, // Create a GitHub Release entry (notes only; no assets)
tagFormat: "v{{version}}", // Git tag format used for both git + GitHub tags
binary: {
packageNameFormat: "{{name}}-{{platform}}",
shimPath: "bin/run.mjs",
},
},
});Release Workflow
At a high level, bunli release:
- Runs tests and your build script
- Updates
package.jsonversion - Commits, tags, and pushes
- Publishes to npm (unless disabled)
- Optionally creates a GitHub Release entry via
gh(no assets)
Installation Scripts
Universal Install Script
Create an install script for easy installation:
#!/bin/sh
# install.sh
set -e
VERSION="1.0.0"
REPO="username/my-cli"
NAME="my-cli"
# Detect OS and architecture
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$OS" in
darwin) OS="darwin" ;;
linux) OS="linux" ;;
*) echo "Unsupported OS: $OS"; exit 1 ;;
esac
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
# bunli-releaser asset naming:
# - Unix: ${NAME}-${VERSION}-${OS}-${ARCH}.tar.gz
# - Windows: ${NAME}-${VERSION}-${OS}-${ARCH}.zip
ASSET="${NAME}-${VERSION}-${OS}-${ARCH}.tar.gz"
URL="https://github.com/$REPO/releases/download/v$VERSION/$ASSET"
# Download and install
echo "Downloading $NAME..."
tmp="$(mktemp -d)"
curl -fsSL "$URL" | tar -xz -C "$tmp"
sudo mv "$tmp/$NAME" /usr/local/bin/
sudo chmod +x /usr/local/bin/"$NAME"
echo "my-cli installed successfully!"
"$NAME" --versionUsers install with:
curl -fsSL https://example.com/install.sh | shVersion Management
Semantic Versioning
Follow semantic versioning (semver):
- Major (1.0.0 → 2.0.0): Breaking changes
- Minor (1.0.0 → 1.1.0): New features
- Patch (1.0.0 → 1.0.1): Bug fixes
Version Display
// src/index.ts
import { createCLI } from "@bunli/core";
import pkg from "../package.json" with { type: "json" };
const cli = await createCLI({
name: "my-cli",
version: pkg.version, // Automatically shows with --version
description: "My awesome CLI",
});For compiled binaries, prefer embedding the version (like importing package.json as above)
rather than reading a runtime env var.
Update Notifications
Notify users of new versions using the npm registry API:
// src/utils/check-update.ts
export async function checkForUpdates(currentVersion: string, packageName: string) {
try {
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
const data = (await res.json()) as { version: string };
const latest = data.version;
if (latest !== currentVersion) {
console.log(`\n Update available: ${currentVersion} → ${latest}`);
console.log(` Run: npm update -g ${packageName}\n`);
}
} catch {
// Ignore update check failures
}
}Best Practices
- Test Before Release: Run tests on all target platforms
- Include License: Add LICENSE file to distributions
- Document Installation: Provide clear installation instructions
- Version Everything: Tag releases and maintain changelog
- Automate Releases: Use CI/CD for consistent releases
- Sign Binaries: Code sign for security and trust
- Provide Checksums: Include SHA256 checksums for verification
Troubleshooting
Common Issues
Binary not executable:
chmod +x ./my-cliMissing dependencies:
# Bundle all dependencies
bunli build --targets nativeLarge binary size:
# Enable optimizations
bunli build --targets native --minify --bytecodePlatform-specific issues:
// Handle platform differences
if (process.platform === "win32") {
// Windows-specific code
}Next Steps
- Testing - Test before distribution
- bunli CLI - CLI command reference
- Configuration - Build configuration options