spawn/packages/cli
Ahmed Abushagur 8072c084c2
feat: pre-built agent tarballs for fast install (#2232)
* feat: pre-built agent tarballs on GitHub Releases for fast install

Adds a nightly GitHub Actions workflow that builds and uploads agent
tarballs to rolling GitHub Releases. During provisioning, the CLI now
attempts to download and extract a tarball before falling back to live
install. Priority chain: snapshot > tarball > live install.

- New workflow: .github/workflows/agent-tarballs.yml
- New capture script: packer/scripts/capture-agent.sh
- New module: packages/cli/src/shared/agent-tarball.ts
- Orchestrate tries tarball first on non-local clouds
- Skip tarball when using DO snapshot (skipTarball flag)
- Tests for tarball install + orchestration integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use global.fetch mock pattern and address security review

- Use `global.fetch = mock(...)` instead of `spyOn(globalThis, "fetch")`
  to match codebase convention and fix CI mock interception
- Add URL validation regex to reject shell metacharacters (CRITICAL)
- Add agent name validation in workflow input (MEDIUM)
- Add `jq has()` check before executing install commands (CRITICAL)
- Use `tar -T` instead of unquoted word-splitting in capture-agent.sh (MEDIUM)
- Resolve merge conflicts with upstream/main (keep Docker fields, adapt
  to simplified DO flow, bump version to 0.15.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use globalThis.fetch for testability in CI

Bun's native fetch binding doesn't go through global.fetch property
lookup, so global.fetch = mock(...) doesn't intercept it. Using
globalThis.fetch explicitly ensures the mock interception works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing packer dependencies and harden install command safety

- Add packer/agents.json (agent tier + install command definitions)
- Add packer/scripts/tier-{minimal,node,bun,full}.sh (dependency scripts)
- Add basic command safety check rejecting suspicious patterns
- Document packer/agents.json as a trust boundary requiring PR review

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): fix npm prefix mismatch, add apt-get update, cleanup

- Add apt-get update -y before apt-get install in all tier scripts
- Add --prefix ~/.npm-global to npm install commands in agents.json
  so installed packages land where capture-agent.sh expects them
- Rename misleading MARKER_DIR → MARKER_FILE in capture-agent.sh
- Remove stale comment referencing packer snapshots in workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): detect empty agent installs in capture script

The "no files found" check was dead code — the marker file is always
created before filtering, so FILTERED_FILE always had at least one
entry. Now we count non-marker entries to catch cases where the agent
install silently fails and no actual files are on disk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): use bare fetch() for Bun mock compatibility in CI

In Bun, global.fetch = mock(...) overrides bare fetch() calls but NOT
globalThis.fetch() calls. Every other source file in the codebase uses
bare fetch() and their mocks work fine in CI. Switch to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): use dependency injection for fetch in tests

Bun's global.fetch mock doesn't reliably intercept bare fetch() calls
across all Bun versions in CI. Instead of fighting the runtime, accept
an optional fetchFn parameter (defaults to fetch) and pass mock fetch
directly in tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): bypass mock.module bleed in agent-tarball tests

orchestrate.test.ts uses mock.module("../shared/agent-tarball", ...)
which is process-global in Bun and bleeds into agent-tarball.test.ts.
Import via URL (import.meta.url resolution) to bypass the specifier-
based mock matching and get the real module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): eliminate mock.module bleed between test files

Bun's mock.module is process-global — orchestrate.test.ts mocking
agent-tarball poisoned agent-tarball.test.ts (the mock function
ignored the fetchFn parameter and always returned false).

Fix: make tryTarballInstall injectable via OrchestrationOptions.
orchestrate.test.ts passes the mock directly via options instead
of using mock.module. agent-tarball.test.ts imports the real module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tests): mock Bun.which in credential priority tests

Tests assumed no cloud CLIs were installed, but machines with hcloud/
doctl would get "CLI installed" hint overrides, failing the assertion.
Spy on Bun.which to return null so tests are environment-independent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: fix import ordering after rebase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* security: add curl domain allowlist and expand command blocklist

Addresses security review findings:
- Add domain allowlist for curl/wget targets (claude.ai, opencode.ai,
  raw.githubusercontent.com, registry.npmjs.org, crates.io, github.com)
- Expand suspicious command blocklist (python -c, perl -e, ruby -e, dd, /dev/)
- Document 4-layer security model in workflow comments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* security: add rm -rf to command blocklist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 04:49:39 -05:00
..
src feat: pre-built agent tarballs for fast install (#2232) 2026-03-06 04:49:39 -05:00
.gitignore feat!: remove Fly.io cloud provider support (#1979) 2026-02-27 00:06:32 -05:00
biome.json fix: resolve biome nested root configuration conflict (#1896) 2026-02-24 21:57:50 -08:00
build-clouds.ts feat!: remove Fly.io cloud provider support (#1979) 2026-02-27 00:06:32 -05:00
bun.lock feat: Bun workspace monorepo — packages/cli + packages/shared (#1853) 2026-02-23 22:07:05 -08:00
bunfig.toml feat: Bun workspace monorepo — packages/cli + packages/shared (#1853) 2026-02-23 22:07:05 -08:00
package-lock.json feat: Bun workspace monorepo — packages/cli + packages/shared (#1853) 2026-02-23 22:07:05 -08:00
package.json feat: pre-built agent tarballs for fast install (#2232) 2026-03-06 04:49:39 -05:00
README.md refactor: Remove dead code and stale references (#2104) 2026-03-02 08:32:02 -05:00
tsconfig.json feat: Bun workspace monorepo — packages/cli + packages/shared (#1853) 2026-02-23 22:07:05 -08:00

Spawn CLI

The spawn CLI is a command-line tool for launching AI coding agents on cloud providers, pre-configured with OpenRouter.

Overview

The spawn CLI provides a unified interface to:

  • Launch any supported AI agent (Claude Code, Codex, etc.) on any supported cloud provider
  • Interactively browse available agents and clouds
  • View the agent × cloud compatibility matrix
  • Self-update to the latest version

Architecture

Installation Strategy

The installer uses bun to build the TypeScript CLI into a standalone JavaScript file. If bun is not already installed, the installer auto-installs it first (~5 seconds).

Why bun?

  • Fast: Native TypeScript runtime, instant builds
  • Universal: Auto-installed if missing, works on any system with bash and curl
  • Zero friction: No prerequisite installation required
  • Single implementation: One codebase, always feature-complete

Directory Structure

cli/
├── src/
│   ├── index.ts        # Entry point (routes commands to handlers)
│   ├── commands/       # Per-command modules (interactive, list, run, etc.)
│   │   └── index.ts    # Barrel re-export
│   ├── commands.ts     # Compatibility shim → re-exports from commands/index.ts
│   ├── manifest.ts     # Manifest fetching and caching logic
│   ├── update-check.ts # Auto-update check (once per day)
│   └── __tests__/      # Test suite (Bun test runner)
├── ../sh/cli/install.sh # Installer (auto-installs bun if needed, lives in sh/cli/)
├── package.json        # Package metadata and dependencies
└── tsconfig.json       # TypeScript configuration

TypeScript Implementation

The TypeScript CLI (src/*.ts) provides:

  • Interactive mode: Terminal UI with prompts for selecting agents and clouds
  • Manifest caching: Local cache with TTL to minimize network requests
  • Auto-update check: Non-intrusive daily version check with notifications
  • Progress indicators: Spinners and colored output for better UX
  • Error handling: Structured error messages and exit codes

Key dependencies:

  • @clack/prompts — Interactive terminal prompts
  • picocolors — Terminal color support

Installation

Quick Install

curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sh/cli/install.sh | bash

The installer will:

  1. Install bun if not already present
  2. Clone the CLI source
  3. Build and install the spawn binary to ~/.local/bin

Environment Variables

  • SPAWN_INSTALL_DIR — Override install directory (default: $HOME/.local/bin)

Manual Installation (Development)

cd cli
bun install
bun link

Or build a standalone binary:

bun run compile  # Creates ./spawn executable

Usage

Interactive Mode

spawn

Launches an interactive picker to select an agent and cloud provider.

Direct Launch

spawn <agent> <cloud>

Examples:

spawn claude sprite    # Launch Claude Code on Sprite
spawn codex hetzner    # Launch Codex CLI on Hetzner Cloud

Agent Information

spawn <agent>

Show which cloud providers support the specified agent.

Example:

spawn claude
# Output:
# Claude Code — AI coding agent from Anthropic
#
# Available clouds:
#   Sprite          spawn claude sprite
#   Hetzner Cloud   spawn claude hetzner

List All Combinations

spawn list

Display the full agent × cloud compatibility matrix.

List Agents

spawn agents

Show all available agents with descriptions.

List Cloud Providers

spawn clouds

Show all available cloud providers with descriptions.

Update CLI

spawn update

Displays update instructions (re-run installer).

Auto-update check: The CLI automatically checks for updates once per day and displays a notification if a newer version is available. To disable this, set SPAWN_NO_UPDATE_CHECK=1.

Version

spawn version

Display the current CLI version.

Development

Prerequisites

  • Bun 1.0+

Running Locally

bun run dev             # Run TypeScript CLI directly
bun run build           # Build to cli.js
bun run compile         # Compile to standalone binary

Testing

bun run dev list
bun run dev agents
bun run dev claude sprite

Code Organization

src/index.ts

  • Command-line argument parsing
  • Routes to appropriate command handler
  • Minimal logic (just dispatching)

src/commands/

  • Per-command modules: interactive.ts, list.ts, run.ts, delete.ts, update.ts, etc.
  • shared.ts — helpers, entity resolution, fuzzy matching, credential hints
  • index.ts — barrel re-export for backward compat
  • commands.ts at root is a thin shim that re-exports from commands/index.ts

src/manifest.ts

  • Manifest fetching from GitHub
  • Local caching with TTL
  • Offline fallback to stale cache
  • Typed manifest structure

src/version.ts

  • Single source of truth for version number

Adding a New Command

  1. Add a new file src/commands/mycommand.ts:

    export async function cmdMyCommand() {
      const manifest = await loadManifest();
      // ... implementation
    }
    
  2. Re-export from src/commands/index.ts:

    export { cmdMyCommand } from "./mycommand.js";
    
  3. Add routing in src/index.ts:

    case "mycommand":
      await cmdMyCommand();
      break;
    
  4. Update help text in src/commands/help.tscmdHelp()

Design Rationale

Why TypeScript?

  • Type safety: Manifest structure is type-checked at compile time
  • Modern async/await: Clean, readable asynchronous code
  • Rich ecosystem: Access to high-quality CLI libraries (@clack/prompts, etc.)
  • Single codebase: Same code runs on bun, node, or as a compiled binary

Why Auto-install Bun?

  • Single implementation: No need to maintain a separate bash CLI
  • Feature parity: Every user gets the full TypeScript CLI with all features
  • Fast install: Bun installs in ~5 seconds via curl -fsSL https://bun.sh/install | bash
  • Simple maintenance: One codebase, one source of truth

Manifest Caching

The CLI caches the manifest locally to reduce network requests:

  • Cache location: $XDG_CACHE_HOME/spawn/manifest.json (or ~/.cache/spawn/manifest.json)
  • TTL: 1 hour (3600 seconds)
  • Offline fallback: If fetch fails, uses stale cache if available
  • Invalidation: spawn update clears the cache

Script Execution Flow

When you run spawn <agent> <cloud>:

  1. Load manifest: Fetch from GitHub or use cached version
  2. Validate combination: Check that matrix["<cloud>/<agent>"] is "implemented"
  3. Download script: Fetch https://openrouter.ai/labs/spawn/<cloud>/<agent>.sh
    • Fallback to GitHub raw URL if OpenRouter CDN fails
  4. Execute: Pipe script to bash -c with inherited stdio
  5. Interactive handoff: User interacts directly with the spawned agent

Contributing

Before Submitting Changes

  1. Test the CLI:

    bun run dev --help
    
  2. Ensure version numbers are synchronized:

    • src/version.tsVERSION
    • package.jsonversion
  3. Update this README if you add new commands or change behavior

  4. Run the installer locally to verify it works:

    bash install.sh
    

Release Checklist

  1. Bump version in both locations (see above)
  2. Update CHANGELOG (if exists)
  3. Test installer on clean system
  4. Tag release: git tag -a cli-vX.Y.Z -m "Release vX.Y.Z"
  5. Push tag: git push --tags

License

See repository root for license information.