Bunli
Guides

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 --help

For distribution, you need to build optimized binaries:

# Production build
bunli build

Build 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 ./dist

Platform Targets

Bunli supports building for multiple platforms:

  • darwin-arm64 - macOS Apple Silicon
  • darwin-x64 - macOS Intel
  • linux-arm64 - Linux ARM64
  • linux-x64 - Linux x64
  • windows-x64 - Windows x64

Cross-Platform Builds

Build for all platforms at once:

// bunli.config.ts
import { defineConfig } from 'bunli'

export default defineConfig({
  build: {
    targets: [
      'darwin-arm64',
      'darwin-x64', 
      'linux-arm64',
      'linux-x64',
      'windows-x64'
    ],
    compress: true // Create .tar.gz archives
  }
})
bunli build --targets all

This creates:

dist/
├── darwin-arm64/
│   └── cli
├── darwin-x64/
│   └── cli
├── linux-arm64/
│   └── cli
├── linux-x64/
│   └── cli
└── windows-x64/
    └── cli.exe

With compression enabled, also:

dist/
├── darwin-arm64.tar.gz
├── darwin-x64.tar.gz
├── linux-arm64.tar.gz
├── linux-x64.tar.gz
└── windows-x64.tar.gz

Standalone 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 --bytecode

Executable 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'
    ]
  }
})

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-cli

Distribution Methods

1. Direct Download

Provide pre-built binaries on your website or GitHub releases:

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v1
      
      - name: Install dependencies
        run: bun install
        
      - name: Build for all platforms
        run: bunli build --targets all
        
      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: dist/*
          generate_release_notes: true

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-cli

3. Homebrew (macOS/Linux)

Create a Homebrew formula:

# Formula/my-cli.rb
class MyCli < Formula
  desc "Description of my CLI"
  homepage "https://github.com/username/my-cli"
  version "1.0.0"
  
  if OS.mac? && Hardware::CPU.arm?
    url "https://github.com/username/my-cli/releases/download/v1.0.0/darwin-arm64.tar.gz"
    sha256 "abc123..."
  elsif OS.mac?
    url "https://github.com/username/my-cli/releases/download/v1.0.0/darwin-x64.tar.gz"
    sha256 "def456..."
  elsif OS.linux? && Hardware::CPU.arm?
    url "https://github.com/username/my-cli/releases/download/v1.0.0/linux-arm64.tar.gz"
    sha256 "ghi789..."
  else
    url "https://github.com/username/my-cli/releases/download/v1.0.0/linux-x64.tar.gz"
    sha256 "jkl012..."
  end
  
  def install
    bin.install "my-cli"
  end
  
  test do
    system "#{bin}/my-cli", "--version"
  end
end

4. 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 --help

5. 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/windows-x64.tar.gz",
      "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/linux-x64.tar.gz")
source_aarch64=("$url/releases/download/v$pkgver/linux-arm64.tar.gz")

package() {
  install -Dm755 my-cli "$pkgdir/usr/bin/my-cli"
}

Automated Releases

Using bunli release

Bunli provides a release command that automates the release process:

# Create a new release
bunli release

# Release with specific version
bunli release --version 2.0.0

# Release without npm publish
bunli release --no-npm

# Dry run
bunli release --dry-run

Release Configuration

// bunli.config.ts
export default defineConfig({
  release: {
    npm: true,              // Publish to npm
    github: true,           // Create GitHub release
    tagFormat: 'v${version}', // Git tag format
    conventionalCommits: true, // Use conventional commits
    
    // Pre-release script
    beforeRelease: async (version) => {
      // Update changelog, docs, etc.
    },
    
    // Post-release script
    afterRelease: async (version) => {
      // Notify users, update website, etc.
    }
  }
})

Release Workflow

  1. Version Bump

    # Automatic version based on commits
    bunli release
    
    # Manual version
    bunli release --version 2.0.0
  2. Build All Platforms

    bunli build --targets all
  3. Create Git Tag

    git tag v2.0.0
    git push origin v2.0.0
  4. GitHub Release

    • Upload built artifacts
    • Generate release notes
    • Publish release
  5. Publish to npm

    npm publish

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"

# 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

# Download URL
URL="https://github.com/$REPO/releases/download/v$VERSION/$OS-$ARCH.tar.gz"

# Download and install
echo "Downloading my-cli..."
curl -fsSL "$URL" | tar -xz -C /tmp
sudo mv /tmp/my-cli /usr/local/bin/
sudo chmod +x /usr/local/bin/my-cli

echo "my-cli installed successfully!"
my-cli --version

Users install with:

curl -fsSL https://example.com/install.sh | sh

Version 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 { version } from '../package.json'

const cli = createCLI({
  name: 'my-cli',
  version, // Automatically shows with --version
  description: 'My awesome CLI'
})

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

  1. Test Before Release: Run tests on all target platforms
  2. Include License: Add LICENSE file to distributions
  3. Document Installation: Provide clear installation instructions
  4. Version Everything: Tag releases and maintain changelog
  5. Automate Releases: Use CI/CD for consistent releases
  6. Sign Binaries: Code sign for security and trust
  7. Provide Checksums: Include SHA256 checksums for verification

Troubleshooting

Common Issues

Binary not executable:

chmod +x ./my-cli

Missing dependencies:

# Bundle all dependencies
bunli build --targets native --external none

Large binary size:

# Enable optimizations
bunli build --targets native --minify --bytecode

Platform-specific issues:

// Handle platform differences
if (process.platform === 'win32') {
  // Windows-specific code
}

Next Steps