278 lines
12 KiB
Markdown
278 lines
12 KiB
Markdown
# CLI Distribution
|
|
|
|
The Cline CLI (`cline`) is distributed as compiled binaries via npm. Users run `npm i -g cline` and get a working `cline` command without needing Bun, Zig, or any other runtime installed.
|
|
|
|
## Why Compiled Binaries?
|
|
|
|
The CLI depends on OpenTUI (`@opentui/core`), which uses `bun:ffi` to call into a native Zig binary for terminal rendering. This means:
|
|
|
|
- The CLI cannot run on Node.js (Node doesn't support `bun:ffi`)
|
|
- If shipped as a JS bundle (`dist/index.js`), users must have Bun installed
|
|
- Compiled binaries (`bun build --compile`) embed the Bun runtime, so users need nothing pre-installed
|
|
|
|
Bun's `--compile` flag produces a single self-contained executable that includes the Bun runtime, all JS/TS code, and native addons.
|
|
|
|
## What Gets Published
|
|
|
|
Publishing the CLI publishes 7 packages to npm:
|
|
|
|
| Package | Description |
|
|
|---|---|
|
|
| `@cline/cli-darwin-arm64` | macOS Apple Silicon binary |
|
|
| `@cline/cli-darwin-x64` | macOS Intel binary |
|
|
| `@cline/cli-linux-arm64` | Linux ARM binary |
|
|
| `@cline/cli-linux-x64` | Linux x64 binary |
|
|
| `@cline/cli-windows-x64` | Windows x64 binary |
|
|
| `@cline/cli-windows-arm64` | Windows ARM binary |
|
|
| `cline` | Wrapper package (pulls the right binary via `optionalDependencies`) |
|
|
|
|
Each platform package contains a compiled binary and a minimal `package.json` with `os` and `cpu` fields:
|
|
|
|
```json
|
|
{
|
|
"name": "@cline/cli-darwin-arm64",
|
|
"version": "0.1.0",
|
|
"os": ["darwin"],
|
|
"cpu": ["arm64"],
|
|
"bin": {
|
|
"cline": "bin/cline"
|
|
}
|
|
}
|
|
```
|
|
|
|
The `os` and `cpu` fields tell npm to skip this package on non-matching platforms. A macOS ARM user gets ~30-60MB, not ~200MB of binaries for every platform.
|
|
|
|
The `cline` wrapper package contains no binary -- just the resolver script, postinstall script, and `optionalDependencies` pointing to all platform packages:
|
|
|
|
```json
|
|
{
|
|
"name": "cline",
|
|
"version": "0.1.0",
|
|
"bin": {
|
|
"cline": "./bin/cline"
|
|
},
|
|
"scripts": {
|
|
"postinstall": "node ./postinstall.mjs || true"
|
|
},
|
|
"optionalDependencies": {
|
|
"@cline/cli-darwin-arm64": "0.1.0",
|
|
"@cline/cli-darwin-x64": "0.1.0",
|
|
"@cline/cli-linux-arm64": "0.1.0",
|
|
"@cline/cli-linux-x64": "0.1.0",
|
|
"@cline/cli-windows-x64": "0.1.0",
|
|
"@cline/cli-windows-arm64": "0.1.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
After installing, users run `cline`:
|
|
|
|
```bash
|
|
npm i -g cline
|
|
|
|
cline # interactive mode
|
|
cline "prompt" # single-prompt mode
|
|
cline auth # authenticate a provider
|
|
```
|
|
|
|
## How to Publish
|
|
|
|
Every release starts by preparing one release commit from the code you want to publish:
|
|
|
|
1. Draft user-facing release notes from the commits since the last `cli-vX.Y.Z` tag.
|
|
2. Choose the release version. Because this publishes over the existing `cline` package, the version must be greater than the current published `cline` version. The handoff release is `3.0.0`.
|
|
3. Update `apps/cli/package.json`.
|
|
4. Add the approved notes to `apps/cli/CHANGELOG.md`.
|
|
5. Run checks.
|
|
6. Commit the release changes.
|
|
|
|
Then publish that release commit with one of these paths.
|
|
|
|
### Publish From GitHub Actions
|
|
|
|
Use this path for normal releases.
|
|
|
|
```bash
|
|
git tag -a cli-vX.Y.Z -m "CLI vX.Y.Z"
|
|
git push origin refs/tags/cli-vX.Y.Z
|
|
gh workflow run cli-publish.yml -f publish_target=main -f git_tag=cli-vX.Y.Z -f confirm_publish=publish
|
|
```
|
|
|
|
This path requires the release commit to be on `main` and the matching `cli-vX.Y.Z` tag to exist before the workflow runs. The workflow checks out the tag, publishes to npm with the `latest` dist-tag, creates the GitHub release, and posts to Slack.
|
|
|
|
### Publish Locally
|
|
|
|
Use this path when publishing from an authenticated local machine.
|
|
|
|
Start from a clean checkout at the release commit:
|
|
|
|
```bash
|
|
gh auth status
|
|
npm whoami
|
|
git tag -a cli-vX.Y.Z -m "CLI vX.Y.Z"
|
|
git push origin refs/tags/cli-vX.Y.Z
|
|
bun release cli
|
|
gh release create cli-vX.Y.Z --verify-tag --title "CLI vX.Y.Z" --notes "Paste the approved release notes here."
|
|
```
|
|
|
|
The release helper checks the working tree, verifies the tag points at `HEAD` locally and on `origin`, runs tests, builds all platform packages, and publishes the platform packages plus the generated `cline` wrapper package to npm. The package version and tag must match.
|
|
|
|
By default, `bun release cli` publishes with the npm dist-tag `latest` (what users get with `npm i -g cline`). To publish under a different dist-tag like `next`, pass `--tag`:
|
|
|
|
```bash
|
|
bun release cli --tag next
|
|
```
|
|
|
|
## CI Workflow
|
|
|
|
The GitHub workflow at `.github/workflows/cli-publish.yml` automates publishing:
|
|
|
|
- Main releases are manual. Select `publish_target=main` and set `confirm_publish=publish`.
|
|
- Main releases require `git_tag=cli-vX.Y.Z`, check out that tag, verify it matches `apps/cli/package.json`, run tests, build all platform packages, publish to npm with the `latest` dist-tag using trusted publishing, create a GitHub release, and post to Slack.
|
|
- Nightly releases run on a schedule or manually with `publish_target=nightly`.
|
|
- Nightly releases publish `X.Y.Z-nightly.TIMESTAMP` to npm with the `nightly` dist-tag and skip if there were no commits in the last 24 hours unless forced.
|
|
|
|
CI publishing uses npm trusted publishing. Configure npm trusted publishers for the `cline` wrapper package and every platform package before relying on the workflow.
|
|
|
|
## How It Works Under the Hood
|
|
|
|
```
|
|
User runs: npm i -g cline
|
|
|
|
|
v
|
|
npm installs cline (wrapper package)
|
|
+ optionalDependencies (only the matching platform gets installed):
|
|
- @cline/cli-darwin-arm64
|
|
- @cline/cli-darwin-x64
|
|
- @cline/cli-linux-arm64
|
|
- @cline/cli-linux-x64
|
|
- @cline/cli-windows-x64
|
|
- @cline/cli-windows-arm64
|
|
|
|
|
v
|
|
postinstall script runs:
|
|
- Detects platform/arch
|
|
- Finds the installed platform package
|
|
- Creates a cached hard link for fast startup
|
|
|
|
|
v
|
|
User runs: cline
|
|
|
|
|
v
|
|
bin/cline (Node.js resolver) executes:
|
|
1. Check CLINE_BIN_PATH env var override
|
|
2. Check cached binary at bin/.cline
|
|
3. Walk up node_modules for the platform package
|
|
4. Execute the compiled binary
|
|
```
|
|
|
|
## File Layout
|
|
|
|
```
|
|
apps/cli/
|
|
bin/
|
|
cline # Node.js resolver script (npm entry point)
|
|
script/
|
|
build.ts # Cross-compile for all platforms
|
|
publish-npm.ts # npm publish orchestration
|
|
postinstall.mjs # Post-install binary caching
|
|
```
|
|
|
|
## Scripts Reference
|
|
|
|
From `apps/cli/`:
|
|
|
|
```bash
|
|
bun run build:platforms:single # build only current platform
|
|
bun run build:platforms # build all 6 platform binaries
|
|
bun run publish:npm:dry # preview generated npm package publishing
|
|
```
|
|
|
|
Direct `bun pm pack` and `bun pm pack --dry-run` from `apps/cli` are blocked because the source package is not the npm release package. Build platform packages first, then use `bun run publish:npm:dry` to preview the generated packages under `dist/`.
|
|
|
|
## Build Script (`script/build.ts`)
|
|
|
|
Cross-compiles the CLI for all target platforms:
|
|
|
|
1. When `--install-native-variants` is passed, pre-installs all platform variants of `@opentui/core` using `bun install --os="*" --cpu="*"` so Bun can resolve native FFI binaries for cross-compilation. Without this, Bun only has the host platform's native binary and cross-compiled builds fail.
|
|
2. Builds SDK packages (`bun run build:sdk`) and the CLI JS bundle (`bun -F @cline/cli build`)
|
|
3. For each target platform:
|
|
- Runs `bun build --compile --target bun-{os}-{arch}` to create a standalone executable
|
|
- Generates a `package.json` with `os` and `cpu` fields for npm platform filtering
|
|
- Runs a smoke test on the current platform's binary (`cline --version`)
|
|
- Copies the plugin sandbox bootstrap file if present
|
|
|
|
Flags:
|
|
- `--single` -- build only for the current platform (faster for local testing)
|
|
- `--install-native-variants` -- allow the script to download all OpenTUI native packages required for cross-platform builds
|
|
- `--skip-install` -- skip re-downloading platform-specific native packages if they're already installed
|
|
- `--skip-sdk-build` -- skip rebuilding SDK packages (if already built)
|
|
|
|
## Publish Script (`script/publish-npm.ts`)
|
|
|
|
Orchestrates publishing all packages to npm:
|
|
|
|
1. Reads built packages from `dist/`
|
|
2. Publishes all 6 platform packages in parallel (`@cline/cli-darwin-arm64`, etc.)
|
|
3. Generates a clean main package (`cline`) with:
|
|
- `bin.cline` pointing to the resolver script
|
|
- `postinstall` running the binary caching script
|
|
- `optionalDependencies` listing all platform packages
|
|
4. Publishes the generated `cline` wrapper package
|
|
|
|
Platform packages must be published before the generated `cline` wrapper package because npm validates that `optionalDependencies` exist.
|
|
|
|
The publish script generates a separate `package.json` for the published `cline` wrapper package. The development `package.json` (with `bin` pointing to `src/index.ts` for `bun link`) is never published directly.
|
|
|
|
## Binary Resolver (`bin/cline`)
|
|
|
|
A Node.js script that serves as the entry point when users run `cline`. It finds and executes the correct platform-specific binary.
|
|
|
|
The shebang is `#!/usr/bin/env node` because Node.js is guaranteed to be available wherever npm is. The resolver uses only CommonJS (`require`) and Node.js APIs -- no `bun:` imports or Bun-specific APIs. It then spawns the compiled binary which has Bun embedded.
|
|
|
|
Resolution chain:
|
|
1. `CLINE_BIN_PATH` env var (for development or custom deployments)
|
|
2. `bin/.cline` cached hard link (created by postinstall for fast startup)
|
|
3. Walk up `node_modules` from the script directory to find the platform package
|
|
|
|
## Postinstall (`script/postinstall.mjs`)
|
|
|
|
Runs after `npm install cline`. Creates a hard link from the platform binary to `bin/.cline` for fast startup on subsequent runs. Falls back to file copy if hard linking fails (NFS, cross-device, network-mounted filesystems).
|
|
|
|
The postinstall is defensive: it wraps everything in try/catch and always exits 0 (the `|| true` in the npm script). If postinstall fails, the resolver script has its own fallback logic to find the binary at runtime, so the cached binary is just an optimization.
|
|
|
|
On Windows, the postinstall is a no-op because npm handles `.cmd` shim generation from the `bin` field.
|
|
|
|
## Development vs Distribution
|
|
|
|
During development, `bin` in package.json points to `src/index.ts` for `bun link` to work. The publish script generates a separate package.json for the published package that points to the resolver script. The development package.json is never modified during publish.
|
|
|
|
| Mode | bin target | Runtime | Needs Bun? |
|
|
|---|---|---|---|
|
|
| `bun run dev` | src/index.ts | Bun (source) | Yes |
|
|
| `bun link` + `cline` | src/index.ts | Bun (source) | Yes |
|
|
| `npm i -g cline` | bin/cline resolver | Compiled binary | No |
|
|
|
|
## Gotchas
|
|
|
|
### Native addon cross-compilation
|
|
When building for a different platform (e.g., compiling for Linux on a Mac), Bun needs the target platform's native binaries for `@opentui/core`. The build script handles this by pre-downloading all platform variants with `bun install --os="*" --cpu="*"`.
|
|
|
|
### Version synchronization
|
|
All 7 packages (6 platform + 1 wrapper) must have the same version. The build script reads the version from `apps/cli/package.json`. The publish script verifies that the built package versions match each other and `apps/cli/package.json`.
|
|
|
|
### Package naming and scoping
|
|
Platform packages are published under the `@cline` scope. The generated wrapper package is published as `cline`, so npm trusted publishing must be configured for all 7 package names.
|
|
|
|
### postinstall reliability
|
|
The postinstall script runs in diverse environments (CI, Docker, restricted permissions, network-mounted filesystems where hard links fail). It always wraps operations in try/catch and exits 0. The resolver script is the ultimate fallback.
|
|
|
|
### Windows
|
|
Windows binaries are `.exe` files. The build script appends `.exe` to the output filename on Windows targets. The resolver handles this. npm on Windows generates `.cmd` shims for bin entries automatically.
|
|
|
|
### File permissions
|
|
Compiled binaries need to be executable (`chmod 755`). The build script sets this after copying. The postinstall also sets permissions on the cached binary. Some npm packaging steps can strip permissions, so both handle this defensively.
|
|
|
|
### Package size
|
|
Each compiled binary is ~30-60MB (Bun runtime + all bundled code + native addons). This is normal for compiled CLI tools. Users only download their platform's variant thanks to `optionalDependencies`.
|