mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-30 21:09:29 +00:00
Security: - Fix command injection in modal/lib/common.sh (run_server, upload_file, interactive_session) - Fix command injection in fly/lib/common.sh (run_server, upload_file, interactive_session) - All container providers now use printf '%q' for proper shell escaping Complexity: - Extract _api_should_retry_on_error() helper in shared/common.sh (-19 lines) - Refactor scaleway_api and upcloud_api to use shared retry helper (-24 lines) - Extract _save_fly_token() helper in fly/lib/common.sh (-11 lines) - Extract validateAndGetAgent() in commands.ts, reducing cmdRun/cmdAgentInfo duplication - Refactor cmdList column width calculation to use calculateColumnWidth() UX: - Add actionable next steps to error messages in shared/common.sh - Improve CLI bash fallback error messages with guidance (spawn.sh) - Add OAuth progress indicator during browser authentication wait - Show invalid model ID value and link to openrouter.ai/models - Add troubleshooting steps for agent installation failures Tests: - Update test assertions in test/run.sh to match refactored patterns - All tests passing: 74 TypeScript + 75 bash = 149 total, 0 failures Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
662 lines
19 KiB
Bash
Executable file
662 lines
19 KiB
Bash
Executable file
#!/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 <agent> <cloud> Launch agent on cloud directly
|
|
# spawn <agent> 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 </dev/tty
|
|
if [[ "${choice}" =~ ^[0-9]+$ ]] && (( 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 <agent> <cloud> Launch agent on cloud directly
|
|
spawn <agent> 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 <agent> or spawn <agent> <cloud>
|
|
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 "$@"
|