From 30f19b7df6fd9ca76514ca864ec1b8f362179d54 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:30:48 -0800 Subject: [PATCH] refactor: Drop spawn.sh bash fallback, auto-install bun instead (#163) The 663-line bash CLI (spawn.sh) has drifted from the TypeScript CLI, missing --prompt, security validation, download fallback, and other features. Rather than maintaining two implementations, the installer now auto-installs bun (~5 seconds) when it's not present, ensuring every user gets the full-featured TypeScript CLI. - Remove cli/spawn.sh (663 lines) - Simplify install.sh: remove npm method, add bun auto-install - Extract build_and_install() helper to deduplicate build logic - Update cli/README.md and CLAUDE.md to reflect bun-only strategy Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- CLAUDE.md | 3 +- cli/README.md | 113 ++------- cli/install.sh | 68 ++--- cli/spawn.sh | 662 ------------------------------------------------- 4 files changed, 48 insertions(+), 798 deletions(-) delete mode 100755 cli/spawn.sh diff --git a/CLAUDE.md b/CLAUDE.md index a11a706d..76ea527e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,8 +103,7 @@ spawn/ src/commands.ts # All subcommands (interactive, list, run, etc.) src/version.ts # Version constant package.json # npm package (@openrouter/spawn) - install.sh # One-liner installer (bun → npm → bash fallback) - spawn.sh # Bash fallback CLI (no bun/node required) + install.sh # One-liner installer (bun → npm → auto-install bun) shared/ common.sh # Provider-agnostic shared utilities {cloud}/ diff --git a/cli/README.md b/cli/README.md index eaaa9364..854f4b50 100644 --- a/cli/README.md +++ b/cli/README.md @@ -12,38 +12,15 @@ The spawn CLI provides a unified interface to: ## Architecture -### Three-Tier Installation Strategy +### Installation Strategy -The CLI uses a progressive fallback installation strategy to maximize compatibility: +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). -``` -┌─────────────────────────────────────────────────────────┐ -│ 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 +**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 @@ -54,9 +31,8 @@ cli/ │ ├── 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 +├── install.sh # Installer (auto-installs bun if needed) +├── package.json # Package metadata and dependencies └── tsconfig.json # TypeScript configuration ``` @@ -73,21 +49,6 @@ The TypeScript CLI (`src/*.ts`) provides: - `@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 @@ -97,13 +58,13 @@ curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cli/insta ``` 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 +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` for fallback method) +- `SPAWN_INSTALL_DIR` — Override install directory (default: `$HOME/.local/bin`) ### Manual Installation (Development) @@ -198,8 +159,7 @@ Clone (or update) the spawn repository and run the `improve.sh` script, which us spawn update ``` -- **TypeScript version**: Displays update instructions (re-run installer) -- **Bash version**: Self-updates by downloading the latest `spawn.sh` +Displays update instructions (re-run installer). ### Version @@ -213,8 +173,7 @@ Display the current CLI version. ### Prerequisites -- Bun 1.0+ (or Node.js 18+ with npm) -- TypeScript 5.0+ +- Bun 1.0+ ### Running Locally @@ -227,15 +186,9 @@ bun run compile # Compile to standalone binary ### Testing ```bash -# 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 @@ -259,7 +212,6 @@ bash spawn.sh claude sprite **`src/version.ts`** - Single source of truth for version number -- Imported by both TypeScript and bash implementations ### Adding a New Command @@ -280,8 +232,6 @@ bash spawn.sh claude sprite 3. Update help text in `src/commands.ts` → `cmdHelp()` -4. (Optional) Add equivalent implementation to `spawn.sh` for bash fallback - ## Design Rationale ### Why TypeScript? @@ -291,23 +241,16 @@ bash spawn.sh claude sprite - **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? +### Why Auto-install Bun? -- **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 +- **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 -Both implementations cache the manifest locally to reduce network requests: +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) @@ -329,33 +272,25 @@ When you run `spawn `: ### Before Submitting Changes -1. Test both TypeScript and bash versions: +1. Test the CLI: ```bash bun run dev --help - bash spawn.sh --help ``` 2. Ensure version numbers are synchronized: - `src/version.ts` → `VERSION` - - `spawn.sh` → `SPAWN_VERSION` - `package.json` → `version` 3. Update this README if you add new commands or change behavior -4. Run the installer locally to verify the three-tier strategy works: +4. Run the installer locally to verify it works: ```bash - # 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) +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"` diff --git a/cli/install.sh b/cli/install.sh index 281cbabe..7475faa0 100755 --- a/cli/install.sh +++ b/cli/install.sh @@ -4,10 +4,9 @@ # Usage: # curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cli/install.sh | bash # -# This installs spawn via bun (preferred) or npm. If neither is available, -# it falls back to downloading the bundled JS file and creating a runner script. +# This installs spawn via bun. If bun is not available, it auto-installs it first. # -# Override install directory (for fallback method): +# Override install directory: # SPAWN_INSTALL_DIR=/usr/local/bin curl -fsSL ... | bash set -eo pipefail @@ -25,7 +24,6 @@ log_info() { echo -e "${GREEN}[spawn]${NC} $1"; } log_warn() { echo -e "${YELLOW}[spawn]${NC} $1"; } log_error() { echo -e "${RED}[spawn]${NC} $1"; } -# --- Helper: clone the cli directory --- # --- Helper: find the best install directory --- # Picks the first directory that exists AND is in PATH find_install_dir() { @@ -113,9 +111,8 @@ clone_cli() { fi } -# --- Method 1: bun (preferred) --- -if command -v bun &>/dev/null; then - log_info "Installing spawn via bun..." +# --- Helper: build and install the CLI using bun --- +build_and_install() { tmpdir=$(mktemp -d) trap 'rm -rf "${tmpdir}"' EXIT @@ -131,49 +128,30 @@ if command -v bun &>/dev/null; then chmod +x "${INSTALL_DIR}/spawn" log_info "Installed spawn to ${INSTALL_DIR}/spawn" ensure_in_path "${INSTALL_DIR}" - exit 0 -fi +} -# --- Method 2: npm/node --- -if command -v npm &>/dev/null && command -v node &>/dev/null; then - log_info "Installing spawn via npm..." - tmpdir=$(mktemp -d) - trap 'rm -rf "${tmpdir}"' EXIT +# --- Install bun if not present --- +if ! command -v bun &>/dev/null; then + log_info "bun not found. Installing bun..." + curl -fsSL https://bun.sh/install | bash - clone_cli "${tmpdir}" + # Source the updated PATH so bun is available immediately + export BUN_INSTALL="${HOME}/.bun" + export PATH="${BUN_INSTALL}/bin:${PATH}" - cd "${tmpdir}/cli" - npm install - - # Build cli.js with node shebang - log_info "Building CLI..." - npx -y esbuild src/index.ts --bundle --outfile=cli.js --platform=node --format=esm --banner:js='#!/usr/bin/env node' 2>/dev/null || { - log_error "Failed to build cli.js. Install bun instead (recommended):" + if ! command -v bun &>/dev/null; then + log_error "Failed to install bun automatically" + echo "" + echo "Please install bun manually:" echo " curl -fsSL https://bun.sh/install | bash" - echo " Then re-run: curl -fsSL ${SPAWN_RAW_BASE}/cli/install.sh | bash" + echo "" + echo "Then re-run:" + echo " curl -fsSL ${SPAWN_RAW_BASE}/cli/install.sh | bash" exit 1 - } + fi - INSTALL_DIR="$(find_install_dir)" - mkdir -p "${INSTALL_DIR}" - cp cli.js "${INSTALL_DIR}/spawn" - chmod +x "${INSTALL_DIR}/spawn" - log_info "Installed spawn to ${INSTALL_DIR}/spawn" - ensure_in_path "${INSTALL_DIR}" - exit 0 + log_info "bun installed successfully" fi -# --- Method 3: Direct download fallback (bash wrapper) --- -log_warn "Neither bun nor npm found. Installing bash fallback..." - -INSTALL_DIR="$(find_install_dir)" -mkdir -p "${INSTALL_DIR}" - -if ! curl -fsSL "${SPAWN_RAW_BASE}/cli/spawn.sh" -o "${INSTALL_DIR}/spawn"; then - log_error "Failed to download spawn CLI" - exit 1 -fi - -chmod +x "${INSTALL_DIR}/spawn" -log_info "Installed spawn (bash) to ${INSTALL_DIR}/spawn" -ensure_in_path "${INSTALL_DIR}" +log_info "Installing spawn via bun..." +build_and_install diff --git a/cli/spawn.sh b/cli/spawn.sh deleted file mode 100755 index 52631681..00000000 --- a/cli/spawn.sh +++ /dev/null @@ -1,662 +0,0 @@ -#!/bin/bash -# spawn — Dynamic entry point for the Spawn matrix -# -# Launch any AI coding agent on any cloud, pre-configured with OpenRouter. -# Fetches the manifest dynamically from GitHub so it's always up-to-date. -# -# Usage: -# spawn Interactive agent + cloud picker -# spawn Launch agent on cloud directly -# spawn Show available clouds for agent -# spawn list Full matrix table -# spawn agents List all agents with descriptions -# spawn clouds List all cloud providers -# spawn improve [--loop] Run improvement system -# spawn update Self-update from GitHub -# spawn version Show version -# spawn help Show this help - -set -eo pipefail - -# ── Constants ────────────────────────────────────────────────────────────────── - -SPAWN_VERSION="0.1.0" -SPAWN_REPO="OpenRouterTeam/spawn" -SPAWN_RAW_BASE="https://raw.githubusercontent.com/${SPAWN_REPO}/main" -SPAWN_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/spawn" -SPAWN_MANIFEST="${SPAWN_CACHE_DIR}/manifest.json" -SPAWN_CACHE_TTL=3600 # 1 hour in seconds - -# ── Colors & Logging ────────────────────────────────────────────────────────── - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' - -log_info() { echo -e "${GREEN}[spawn]${NC} $1" >&2; } -log_warn() { echo -e "${YELLOW}[spawn]${NC} $1" >&2; } -log_error() { echo -e "${RED}[spawn]${NC} $1" >&2; } - -# ── Dependency Checks ───────────────────────────────────────────────────────── - -HAS_JQ=false -HAS_PYTHON3=false - -check_deps() { - if ! command -v curl &>/dev/null; then - log_error "curl is required but not found" - exit 1 - fi - command -v jq &>/dev/null && HAS_JQ=true - command -v python3 &>/dev/null && HAS_PYTHON3=true - if ! ${HAS_JQ} && ! ${HAS_PYTHON3}; then - log_error "Either jq or python3 is required for JSON parsing" - exit 1 - fi -} - -# ── JSON Helpers ─────────────────────────────────────────────────────────────── - -# Each helper tries jq first, falls back to python3. - -json_validate() { - local file="$1" - if ${HAS_JQ}; then - jq empty "${file}" 2>/dev/null - elif ${HAS_PYTHON3}; then - python3 -c "import json, sys; json.load(open(sys.argv[1]))" "${file}" 2>/dev/null - fi -} - -# List agent keys (one per line) -manifest_agents() { - if ${HAS_JQ}; then - jq -r '.agents | keys[]' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -for k in m['agents']: - print(k) -" - fi -} - -# List cloud keys (one per line) -manifest_clouds() { - if ${HAS_JQ}; then - jq -r '.clouds | keys[]' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -for k in m['clouds']: - print(k) -" - fi -} - -# Get agent display name -manifest_agent_name() { - local agent="$1" - if ${HAS_JQ}; then - jq -r --arg a "${agent}" '.agents[${a}].name // empty' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -a = m['agents'].get('${agent}', {}) -print(a.get('name', '')) -" - fi -} - -# Get agent description -manifest_agent_desc() { - local agent="$1" - if ${HAS_JQ}; then - jq -r --arg a "${agent}" '.agents[${a}].description // empty' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -a = m['agents'].get('${agent}', {}) -print(a.get('description', '')) -" - fi -} - -# Get cloud display name -manifest_cloud_name() { - local cloud="$1" - if ${HAS_JQ}; then - jq -r --arg c "${cloud}" '.clouds[${c}].name // empty' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -c = m['clouds'].get('${cloud}', {}) -print(c.get('name', '')) -" - fi -} - -# Get cloud description -manifest_cloud_desc() { - local cloud="$1" - if ${HAS_JQ}; then - jq -r --arg c "${cloud}" '.clouds[${c}].description // empty' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -c = m['clouds'].get('${cloud}', {}) -print(c.get('description', '')) -" - fi -} - -# Get matrix status for cloud/agent -manifest_matrix_status() { - local cloud="$1" agent="$2" - if ${HAS_JQ}; then - jq -r --arg key "${cloud}/${agent}" '.matrix[${key}] // "missing"' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -print(m.get('matrix', {}).get('${cloud}/${agent}', 'missing')) -" - fi -} - -# Count implemented entries -manifest_count_implemented() { - if ${HAS_JQ}; then - jq '[.matrix | to_entries[] | select(.value == "implemented")] | length' "${SPAWN_MANIFEST}" - else - python3 -c " -import json -m = json.load(open('${SPAWN_MANIFEST}')) -print(sum(1 for v in m.get('matrix', {}).values() if v == 'implemented')) -" - fi -} - -# ── Manifest Cache ───────────────────────────────────────────────────────────── - -file_age_seconds() { - local file="$1" - local now - now=$(date +%s) - local mtime - # macOS uses -f, Linux uses -c - if stat -f %m "${file}" &>/dev/null; then - mtime=$(stat -f %m "${file}") - else - mtime=$(stat -c %Y "${file}" 2>/dev/null || echo 0) - fi - echo $(( now - mtime )) -} - -ensure_manifest() { - mkdir -p "${SPAWN_CACHE_DIR}" - - # Check if cache exists and is fresh - if [[ -f "${SPAWN_MANIFEST}" ]]; then - local age - age=$(file_age_seconds "${SPAWN_MANIFEST}") - if (( age < SPAWN_CACHE_TTL )); then - return 0 - fi - fi - - log_info "Fetching manifest..." - local tmp - tmp=$(mktemp) - trap 'rm -f "${tmp}"' EXIT - if curl -fsSL "${SPAWN_RAW_BASE}/manifest.json" -o "${tmp}" 2>/dev/null; then - if json_validate "${tmp}"; then - mv "${tmp}" "${SPAWN_MANIFEST}" - trap - EXIT # Clear trap after successful move - return 0 - else - log_warn "Downloaded manifest is invalid JSON" - fi - else - log_warn "Failed to fetch manifest" - fi - - # Offline fallback: use stale cache if available - if [[ -f "${SPAWN_MANIFEST}" ]]; then - log_warn "Using cached manifest (offline or network issue)" - return 0 - fi - - log_error "No manifest available and no cached version found" - echo "" >&2 - echo "Check your internet connection and try again." >&2 - echo "If the problem persists, file an issue at:" >&2 - echo " https://github.com/${SPAWN_REPO}/issues" >&2 - exit 1 -} - -# ── Interactive Picker ───────────────────────────────────────────────────────── - -picker() { - local prompt="$1" - shift - local -a items=("$@") - local count=${#items[@]} - - echo "" >&2 - echo -e "${BOLD}${prompt}${NC}" >&2 - echo "" >&2 - - local i - for (( i = 0; i < count; i++ )); do - printf " %s%2d)%s %s\n" "${GREEN}" $(( i + 1 )) "${NC}" "${items[${i}]}" >&2 - done - - echo "" >&2 - local choice - while true; do - printf " Enter number (1-%d): " "${count}" >&2 - read -r choice = 1 && choice <= count )); then - echo $(( choice - 1 )) - return 0 - fi - log_warn "Invalid choice. Enter a number between 1 and ${count}." - done -} - -# ── Commands ─────────────────────────────────────────────────────────────────── - -cmd_interactive() { - ensure_manifest - - # Build agent list with descriptions - local -a agent_keys=() - local -a agent_labels=() - while IFS= read -r key; do - agent_keys+=("${key}") - local name desc - name=$(manifest_agent_name "${key}") - desc=$(manifest_agent_desc "${key}") - agent_labels+=("${name} ${DIM}- ${desc}${NC}") - done < <(manifest_agents) - - if (( ${#agent_keys[@]} == 0 )); then - log_error "No agents found in manifest" - exit 1 - fi - - local agent_idx - agent_idx=$(picker "Select an agent:" "${agent_labels[@]}") - local agent="${agent_keys[${agent_idx}]}" - - # Build cloud list — only show implemented clouds for this agent - local -a cloud_keys=() - local -a cloud_labels=() - while IFS= read -r key; do - local status - status=$(manifest_matrix_status "${key}" "${agent}") - if [[ "${status}" == "implemented" ]]; then - cloud_keys+=("${key}") - local name desc - name=$(manifest_cloud_name "${key}") - desc=$(manifest_cloud_desc "${key}") - cloud_labels+=("${name} ${DIM}- ${desc}${NC}") - fi - done < <(manifest_clouds) - - if (( ${#cloud_keys[@]} == 0 )); then - local aname - aname=$(manifest_agent_name "${agent}") - log_error "No implemented clouds found for ${aname}" - exit 1 - fi - - local cloud_idx - cloud_idx=$(picker "Select a cloud provider:" "${cloud_labels[@]}") - local cloud="${cloud_keys[${cloud_idx}]}" - - cmd_run "${agent}" "${cloud}" -} - -cmd_run() { - local agent="$1" cloud="$2" - ensure_manifest - - local agent_name cloud_name status - agent_name=$(manifest_agent_name "${agent}") - cloud_name=$(manifest_cloud_name "${cloud}") - - if [[ -z "${agent_name}" ]]; then - log_error "Unknown agent: ${agent}" - echo "" >&2 - echo "Run 'spawn agents' to see all available agents." >&2 - echo "Run 'spawn help' for usage information." >&2 - exit 1 - fi - if [[ -z "${cloud_name}" ]]; then - log_error "Unknown cloud: ${cloud}" - echo "" >&2 - echo "Run 'spawn clouds' to see all available clouds." >&2 - echo "Run 'spawn help' for usage information." >&2 - exit 1 - fi - - status=$(manifest_matrix_status "${cloud}" "${agent}") - if [[ "${status}" != "implemented" ]]; then - log_error "${agent_name} on ${cloud_name} is not yet implemented" - echo "" >&2 - echo "Run 'spawn list' to see all available combinations." >&2 - echo "Run 'spawn ${agent}' to see which clouds support ${agent_name}." >&2 - exit 1 - fi - - log_info "Launching ${BOLD}${agent_name}${NC}${GREEN} on ${BOLD}${cloud_name}${NC}${GREEN}...${NC}" - local url="https://openrouter.ai/lab/spawn/${cloud}/${agent}.sh" - bash <(curl -fsSL "${url}") -} - -cmd_list() { - ensure_manifest - - if ${HAS_PYTHON3}; then - cmd_list_python - elif ${HAS_JQ}; then - cmd_list_jq - fi -} - -cmd_list_jq() { - jq -r ' - def pad(${n}): . + (" " * (${n} - length)) | .[:${n}]; - def status_icon: if . == "implemented" then " ✓" else " -" end; - - . as ${root} | - (${root}.agents | keys) as ${agents} | - (${root}.clouds | keys) as ${clouds} | - - (("" | pad(18)) + (${clouds} | map(. as ${c} | ${root}.clouds[${c}].name | pad(14)) | join(""))), - (${agents}[] as ${agent} | - (${root}.agents[${agent}].name | pad(18)) + - (${clouds} | map(. as ${cloud} | - (${root}.matrix["\(${cloud})/\(${agent})"] // "missing" | status_icon | pad(14)) - ) | join("")) - ), - "", - ([${root}.matrix | to_entries[] | select(.value == "implemented")] | length | tostring) + - "/" + - ((${agents} | length) * (${clouds} | length) | tostring) + - " implemented" - ' "${SPAWN_MANIFEST}" -} - -cmd_list_python() { - python3 -c " -import json - -m = json.load(open('${SPAWN_MANIFEST}')) -agents = list(m['agents'].keys()) -clouds = list(m['clouds'].keys()) -matrix = m.get('matrix', {}) - -G = '\033[0;32m' -R = '\033[0;31m' -D = '\033[2m' -B = '\033[1m' -NC = '\033[0m' - -# Header -header = f'{\"\":18s}' -for c in clouds: - header += f'{m[\"clouds\"][c][\"name\"]:14s}' -print(B + header + NC) - -# Rows -for a in agents: - row = f'{m[\"agents\"][a][\"name\"]:18s}' - for c in clouds: - key = f'{c}/{a}' - status = matrix.get(key, 'missing') - if status == 'implemented': - row += G + f'{\" ✓\":14s}' + NC - else: - row += D + f'{\" -\":14s}' + NC - print(row) - -# Summary -impl = sum(1 for v in matrix.values() if v == 'implemented') -total = len(agents) * len(clouds) -print(f'\n{impl}/{total} implemented') -" -} - -cmd_agents() { - ensure_manifest - - echo "" - echo -e "${BOLD}Agents${NC}" - echo "" - - while IFS= read -r key; do - local name desc - name=$(manifest_agent_name "${key}") - desc=$(manifest_agent_desc "${key}") - printf " ${GREEN}%-16s${NC} %s\n" "${name}" "${desc}" - done < <(manifest_agents) - echo "" -} - -cmd_clouds() { - ensure_manifest - - echo "" - echo -e "${BOLD}Cloud Providers${NC}" - echo "" - - while IFS= read -r key; do - local name desc - name=$(manifest_cloud_name "${key}") - desc=$(manifest_cloud_desc "${key}") - printf " ${GREEN}%-16s${NC} %s\n" "${name}" "${desc}" - done < <(manifest_clouds) - echo "" -} - -cmd_agent_info() { - local agent="$1" - ensure_manifest - - local agent_name agent_desc - agent_name=$(manifest_agent_name "${agent}") - agent_desc=$(manifest_agent_desc "${agent}") - - if [[ -z "${agent_name}" ]]; then - log_error "Unknown agent: ${agent}" - echo "Run 'spawn agents' to see available agents." >&2 - exit 1 - fi - - echo "" - echo -e "${BOLD}${agent_name}${NC} — ${agent_desc}" - echo "" - echo -e "${BOLD}Available clouds:${NC}" - echo "" - - local found=false - while IFS= read -r cloud; do - local status - status=$(manifest_matrix_status "${cloud}" "${agent}") - if [[ "${status}" == "implemented" ]]; then - local cloud_name - cloud_name=$(manifest_cloud_name "${cloud}") - printf " ${GREEN}%-16s${NC} spawn %s %s\n" "${cloud_name}" "${agent}" "${cloud}" - found=true - fi - done < <(manifest_clouds) - - if ! ${found}; then - echo " No implemented clouds yet." - fi - echo "" -} - -cmd_improve() { - shift # remove 'improve' from args - local repo_dir - - # Check if we're already in the spawn repo - if [[ -f "./improve.sh" && -f "./manifest.json" ]]; then - repo_dir="." - else - repo_dir="${SPAWN_CACHE_DIR}/repo" - if [[ -d "${repo_dir}/.git" ]]; then - log_info "Updating spawn repo..." - git -C "${repo_dir}" pull --ff-only 2>/dev/null || true - else - log_info "Cloning spawn repo..." - git clone "https://github.com/${SPAWN_REPO}.git" "${repo_dir}" - fi - fi - - (cd "${repo_dir}" && bash improve.sh "$@") -} - -cmd_update() { - local self - self=$(command -v spawn 2>/dev/null || echo "") - if [[ -z "${self}" ]]; then - # Try common install locations - if [[ -f "${HOME}/.local/bin/spawn" ]]; then - self="${HOME}/.local/bin/spawn" - else - log_error "Cannot find spawn binary for self-update" - exit 1 - fi - fi - - log_info "Checking for updates..." - local tmp - tmp=$(mktemp) - trap 'rm -f "${tmp}"' EXIT - if curl -fsSL "${SPAWN_RAW_BASE}/cli/spawn.sh" -o "${tmp}" 2>/dev/null; then - local remote_version - remote_version=$(grep '^SPAWN_VERSION=' "${tmp}" | head -1 | cut -d'"' -f2) - if [[ -z "${remote_version}" ]]; then - log_error "Could not determine remote version" - exit 1 - fi - - if [[ "${remote_version}" == "${SPAWN_VERSION}" ]]; then - trap - EXIT # Clear trap, file already cleaned - log_info "Already up to date (v${SPAWN_VERSION})" - return 0 - fi - - chmod +x "${tmp}" - mv "${tmp}" "${self}" - trap - EXIT # Clear trap after successful move - log_info "Updated: v${SPAWN_VERSION} → v${remote_version}" - - # Invalidate manifest cache on update - rm -f "${SPAWN_MANIFEST}" - else - log_error "Failed to download update" - exit 1 - fi -} - -cmd_help() { - echo -e " -${BOLD}spawn${NC} — Launch any AI coding agent on any cloud - -${BOLD}USAGE${NC} - spawn Interactive agent + cloud picker - spawn Launch agent on cloud directly - spawn Show available clouds for agent - spawn list Full matrix table - spawn agents List all agents with descriptions - spawn clouds List all cloud providers - spawn improve [--loop] Run improvement system (wraps improve.sh) - spawn update Self-update from GitHub - spawn version Show version - spawn help Show this help - -${BOLD}EXAMPLES${NC} - spawn Pick interactively - spawn claude sprite Launch Claude Code on Sprite - spawn aider hetzner Launch Aider on Hetzner Cloud - spawn claude Show which clouds support Claude Code - spawn list See the full agent x cloud matrix - -${BOLD}INSTALL${NC} - curl -fsSL ${SPAWN_RAW_BASE}/cli/install.sh | bash -" -} - -# ── Main ─────────────────────────────────────────────────────────────────────── - -main() { - check_deps - - if (( $# == 0 )); then - cmd_interactive - return - fi - - case "$1" in - help|--help|-h) - cmd_help - ;; - version|--version|-v|-V) - echo "spawn v${SPAWN_VERSION}" - ;; - list|ls) - cmd_list - ;; - agents) - cmd_agents - ;; - clouds) - cmd_clouds - ;; - improve) - cmd_improve "$@" - ;; - update) - cmd_update - ;; - *) - # Could be: spawn or spawn - local agent="$1" - ensure_manifest - - # Check if it's a valid agent - local agent_name - agent_name=$(manifest_agent_name "${agent}") - if [[ -z "${agent_name}" ]]; then - log_error "Unknown command or agent: ${agent}" - echo "" >&2 - echo "Run 'spawn agents' to see all available agents." >&2 - echo "Run 'spawn help' for usage information." >&2 - exit 1 - fi - - if (( $# >= 2 )); then - cmd_run "${agent}" "$2" - else - cmd_agent_info "${agent}" - fi - ;; - esac -} - -main "$@"