Distribution
Package and distribute your CLI to users
Distribution Guide
Learn how to build, package, and distribute your Bunli CLI application to users across different platforms.
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"Keep 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:
// src/utils/check-update.ts
import { getLatestVersion } from '@bunli/utils'
export async function checkForUpdates(currentVersion: string) {
try {
const latest = await getLatestVersion('my-cli')
if (latest > currentVersion) {
console.log(`\n Update available: ${currentVersion} → ${latest}`)
console.log(' Run: npm update -g my-cli\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