spawn/cli
A 68349fa5d7
fix: Replace instanceof Error checks with duck typing (#60)
Fixes #59

The instanceof operator can fail in bundled/minified code or when
errors cross execution realm boundaries, causing the error:
"instanceof called on an object with an invalid prototype property"

This commit replaces all instanceof Error checks with duck typing
(checking for object with 'message' property) which is more reliable
across different execution contexts.

Changes:
- index.ts: Updated handleError() and prompt file error handling
- commands.ts: Updated getErrorMessage() helper

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 01:21:37 -08:00
..
src fix: Replace instanceof Error checks with duck typing (#60) 2026-02-09 01:21:37 -08:00
.gitignore Drop nounset (set -u) flag — incompatible with env var checks (#27) 2026-02-07 16:22:04 -08:00
install.sh fix: Install to ~/.local/bin (in PATH) instead of ~/.bun/bin 2026-02-09 00:45:18 +00:00
package-lock.json test: add unit tests for CLI TypeScript implementation 2026-02-08 00:52:12 +00:00
package.json chore: Update dependencies to latest versions 2026-02-08 04:41:24 +00:00
README.md docs: add CLI architecture and usage documentation 2026-02-08 00:49:09 +00:00
spawn.sh refactor: Security fixes, complexity reduction, and UX improvements (#58) 2026-02-08 17:09:27 -08:00
tsconfig.json Drop nounset (set -u) flag — incompatible with env var checks (#27) 2026-02-07 16:22:04 -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, Aider, 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

Three-Tier Installation Strategy

The CLI uses a progressive fallback installation strategy to maximize compatibility:

┌─────────────────────────────────────────────────────────┐
│ Method 1: Bun (Preferred)                               │
│ - Fastest execution (native TypeScript runtime)         │
│ - Full TypeScript support with minimal overhead         │
│ - Falls back to compiled binary if global install fails │
└─────────────────────────────────────────────────────────┘
                         ↓ (if bun not found)
┌─────────────────────────────────────────────────────────┐
│ Method 2: npm                                           │
│ - Standard Node.js package manager                      │
│ - Transpiles TypeScript to JavaScript at install time   │
│ - Requires Node.js runtime                              │
└─────────────────────────────────────────────────────────┘
                         ↓ (if npm not found)
┌─────────────────────────────────────────────────────────┐
│ Method 3: Bash Fallback                                 │
│ - Pure bash implementation (spawn.sh)                   │
│ - Zero runtime dependencies except curl + jq/python3    │
│ - Functional subset of TypeScript CLI                   │
└─────────────────────────────────────────────────────────┘

Why this pattern?

  • Universal compatibility: Works on any system with bash and curl
  • Optimal performance: Uses the fastest available runtime (bun > node > bash)
  • Zero friction: No prerequisite installation required for basic usage
  • Graceful degradation: Each tier provides full functionality with varying performance characteristics

Directory Structure

cli/
├── src/
│   ├── index.ts        # Entry point (routes commands to handlers)
│   ├── commands.ts     # All command implementations
│   ├── manifest.ts     # Manifest fetching and caching logic
│   └── version.ts      # Version constant
├── install.sh          # Multi-tier installer script
├── spawn.sh            # Bash fallback CLI (full implementation)
├── package.json        # npm package metadata
└── 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
  • 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

Bash Fallback Implementation

The bash CLI (spawn.sh) is a standalone script that:

  • Implements the same commands as the TypeScript version
  • Uses jq or python3 for JSON parsing (auto-detects which is available)
  • Provides a numbered menu picker for interactive mode
  • Maintains local manifest cache with TTL
  • Supports all core commands: list, agents, clouds, run, improve, update

Why maintain both implementations?

  • Portability: Bash version works on minimal systems (CI containers, embedded Linux, etc.)
  • Bootstrap: Used by installer when bun/npm aren't available
  • Reference: Demonstrates that the protocol is runtime-agnostic

Installation

Quick Install

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

The installer will:

  1. Check for bun → install via bun install -g if found
  2. Check for npm → install via npm install -g if found
  3. Fallback → download spawn.sh to $HOME/.local/bin if neither found

Environment Variables

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

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 aider hetzner    # Launch Aider 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.

Improve Command

spawn improve [--loop]

Clone (or update) the spawn repository and run the improve.sh script, which uses Claude to autonomously add missing matrix entries or new agents/clouds.

Update CLI

spawn update
  • TypeScript version: Displays update instructions (re-run installer)
  • Bash version: Self-updates by downloading the latest spawn.sh

Version

spawn version

Display the current CLI version.

Development

Prerequisites

  • Bun 1.0+ (or Node.js 18+ with npm)
  • TypeScript 5.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

# Test TypeScript version
bun run dev list
bun run dev agents
bun run dev claude sprite

# Test bash version
bash spawn.sh list
bash spawn.sh agents
bash spawn.sh claude sprite

Code Organization

src/index.ts

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

src/commands.ts

  • All command implementations
  • Interactive picker UI
  • Script execution logic
  • Help text

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
  • Imported by both TypeScript and bash implementations

Adding a New Command

  1. Add command handler in src/commands.ts:

    export async function cmdMyCommand() {
      const manifest = await loadManifest();
      // ... implementation
    }
    
  2. Add routing in src/index.ts:

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

  4. (Optional) Add equivalent implementation to spawn.sh for bash fallback

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 Bash Fallback?

  • Universality: Bash is available on virtually all Unix-like systems
  • Zero dependencies: Only requires curl and jq/python3 (one of which is usually installed)
  • CI/CD friendly: Works in minimal Docker containers, GitHub Actions, etc.
  • Educational: Demonstrates the protocol can be implemented in any language

Why Bun → npm → Bash Tiering?

  • Performance gradient: Bun is fastest, npm is widely available, bash always works
  • User experience: Bun users get instant execution, others get working tool
  • Distribution: Can be installed via package manager or curl | bash
  • Maintenance: Single TypeScript codebase serves bun and npm, bash is separate but synchronized

Manifest Caching

Both implementations cache 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/lab/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 both TypeScript and bash versions:

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

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

  4. Run the installer locally to verify the three-tier strategy works:

    # Test with bun
    bash install.sh
    
    # Test without bun (rename temporarily)
    mv $(which bun) $(which bun).bak
    bash install.sh
    mv $(which bun).bak $(which bun)
    

Release Checklist

  1. Bump version in all three 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.