mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
Fix OAuth server for macOS bash 3.x (#24)
Three issues broke the OAuth callback server on macOS: 1. echo -e doesn't work in bash 3.x — \r\n appears as literal text in the HTTP response, browser gets malformed headers. Fix: pre-write response with printf to a file before the subshell. 2. local variables inside ( ... ) & subshell — undefined behavior in bash 3.x since subshells aren't function scope. Fix: use plain variables in subshells. 3. ((elapsed++)) when elapsed=0 evaluates to falsy — set -e kills the script on the first iteration of the timeout loop. Fix: use elapsed=$((elapsed + 1)) instead. Also simplified nc_listen detection to only check for BusyBox (the -p flag check could misfire on macOS nc). Applied to all 10 lib/common.sh files. Co-authored-by: Sprite <noreply@sprite.dev> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13896ba52d
commit
6ac59e6bb3
14 changed files with 922 additions and 159 deletions
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -88,6 +88,9 @@ Research cloud providers with API-based provisioning. To add one:
|
|||
|
||||
```
|
||||
spawn/
|
||||
cli/
|
||||
spawn.sh # Main CLI binary (interactive picker, matrix viewer, launcher)
|
||||
install.sh # One-liner installer (downloads spawn.sh to ~/.local/bin)
|
||||
{cloud}/
|
||||
lib/common.sh # Cloud-specific shared functions
|
||||
{agent}.sh # One script per agent
|
||||
|
|
@ -97,6 +100,13 @@ spawn/
|
|||
README.md # User-facing docs
|
||||
```
|
||||
|
||||
## CLI (`cli/`)
|
||||
|
||||
The `spawn` CLI is a pure-bash binary that provides a unified entry point for the matrix.
|
||||
|
||||
- **`cli/spawn.sh`** — Main binary. Fetches manifest.json from GitHub (cached for 1hr at `~/.cache/spawn/`), parses it with `jq` or `python3` fallback, and provides: interactive picker (`spawn`), direct launch (`spawn <agent> <cloud>`), agent info (`spawn <agent>`), matrix table (`spawn list`), and self-update (`spawn update`). Installed to `~/.local/bin/spawn`.
|
||||
- **`cli/install.sh`** — One-liner installer. Downloads `spawn.sh` to `~/.local/bin/spawn`, sets executable bit, and prints PATH instructions if needed. Override install dir with `SPAWN_INSTALL_DIR` env var.
|
||||
|
||||
## Script Conventions
|
||||
|
||||
- `#!/bin/bash` + `set -e`
|
||||
|
|
|
|||
19
README.md
19
README.md
|
|
@ -2,6 +2,25 @@
|
|||
|
||||
One command to launch any AI coding agent on any cloud, pre-configured with [OpenRouter](https://openrouter.ai).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install the `spawn` CLI:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cli/install.sh | bash
|
||||
```
|
||||
|
||||
Then launch any agent interactively:
|
||||
|
||||
```bash
|
||||
spawn # Interactive picker
|
||||
spawn claude sprite # Launch Claude Code on Sprite
|
||||
spawn aider hetzner # Launch Aider on Hetzner Cloud
|
||||
spawn list # See the full matrix
|
||||
```
|
||||
|
||||
Or run directly without installing:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/{cloud}/{agent}.sh)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ safe_read() {
|
|||
|
||||
nc_listen() {
|
||||
local port=$1; shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else nc -l "$port" "$@"; fi
|
||||
}
|
||||
|
|
@ -62,23 +62,29 @@ try_oauth_flow() {
|
|||
local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}"
|
||||
local oauth_dir=$(mktemp -d) code_file="$oauth_dir/code"
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>'
|
||||
while true; do
|
||||
local response_file=$(mktemp); echo -e "$success_response" > "$response_file"
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?; rm -f "$response_file"
|
||||
if [[ $nc_status -ne 0 ]]; then break; fi
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"; break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!; sleep 1
|
||||
if ! kill -0 $server_pid 2>/dev/null; then log_warn "Failed to start OAuth server"; rm -rf "$oauth_dir"; return 1; fi
|
||||
log_warn "Opening browser to authenticate with OpenRouter..."; open_browser "$auth_url"
|
||||
local timeout=120 elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; elapsed=$((elapsed + 1)); done
|
||||
kill $server_pid 2>/dev/null || true; wait $server_pid 2>/dev/null || true
|
||||
if [[ ! -f "$code_file" ]]; then log_warn "OAuth timeout"; rm -rf "$oauth_dir"; return 1; fi
|
||||
local oauth_code=$(cat "$code_file"); rm -rf "$oauth_dir"
|
||||
|
|
@ -224,7 +230,7 @@ create_server() {
|
|||
return 0
|
||||
fi
|
||||
log_warn "Instance state: $state ($attempt/$max_attempts)"
|
||||
sleep 5; ((attempt++))
|
||||
sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Instance did not become running in time"; return 1
|
||||
}
|
||||
|
|
@ -236,7 +242,7 @@ verify_server_connectivity() {
|
|||
if ssh $SSH_OPTS -o ConnectTimeout=5 "ubuntu@$ip" "echo ok" >/dev/null 2>&1; then
|
||||
log_info "SSH connection established"; return 0
|
||||
fi
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; ((attempt++))
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Server failed to respond via SSH after $max_attempts attempts"; return 1
|
||||
}
|
||||
|
|
@ -248,7 +254,7 @@ wait_for_cloud_init() {
|
|||
if ssh $SSH_OPTS "ubuntu@$ip" "test -f /home/ubuntu/.cloud-init-complete" >/dev/null 2>&1; then
|
||||
log_info "Cloud-init completed"; return 0
|
||||
fi
|
||||
log_warn "Cloud-init in progress... ($attempt/$max_attempts)"; sleep 5; ((attempt++))
|
||||
log_warn "Cloud-init in progress... ($attempt/$max_attempts)"; sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Cloud-init did not complete after $max_attempts attempts"; return 1
|
||||
}
|
||||
|
|
|
|||
66
cli/install.sh
Executable file
66
cli/install.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/bin/bash
|
||||
# Installer for the spawn CLI
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cli/install.sh | bash
|
||||
#
|
||||
# Override install directory:
|
||||
# SPAWN_INSTALL_DIR=/usr/local/bin curl -fsSL ... | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SPAWN_REPO="OpenRouterTeam/spawn"
|
||||
SPAWN_RAW_BASE="https://raw.githubusercontent.com/$SPAWN_REPO/main"
|
||||
INSTALL_DIR="${SPAWN_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[spawn]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[spawn]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[spawn]${NC} $1"; }
|
||||
|
||||
# Check curl
|
||||
if ! command -v curl &>/dev/null; then
|
||||
log_error "curl is required but not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install directory
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Download spawn CLI
|
||||
log_info "Downloading spawn CLI..."
|
||||
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 to $INSTALL_DIR/spawn"
|
||||
|
||||
# Check if install dir is in PATH
|
||||
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
|
||||
log_warn "$INSTALL_DIR is not in your PATH"
|
||||
echo ""
|
||||
echo "Add it by running one of:"
|
||||
echo ""
|
||||
|
||||
# Detect shell and suggest appropriate config
|
||||
case "${SHELL:-/bin/bash}" in
|
||||
*/zsh)
|
||||
echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.zshrc && source ~/.zshrc"
|
||||
;;
|
||||
*/fish)
|
||||
echo " fish_add_path $INSTALL_DIR"
|
||||
;;
|
||||
*)
|
||||
echo " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.bashrc && source ~/.bashrc"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
else
|
||||
log_info "Run 'spawn' to get started"
|
||||
fi
|
||||
650
cli/spawn.sh
Executable file
650
cli/spawn.sh
Executable file
|
|
@ -0,0 +1,650 @@
|
|||
#!/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 -uo 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'
|
||||
BLUE='\033[0;34m'
|
||||
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; json.load(open('$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)
|
||||
if curl -fsSL "$SPAWN_RAW_BASE/manifest.json" -o "$tmp" 2>/dev/null; then
|
||||
if json_validate "$tmp"; then
|
||||
mv "$tmp" "$SPAWN_MANIFEST"
|
||||
return 0
|
||||
else
|
||||
rm -f "$tmp"
|
||||
log_warn "Downloaded manifest is invalid JSON"
|
||||
fi
|
||||
else
|
||||
rm -f "$tmp"
|
||||
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 fallback)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "No manifest available. Check your internet connection."
|
||||
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 "Run 'spawn agents' to see available agents." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$cloud_name" ]]; then
|
||||
log_error "Unknown cloud: $cloud"
|
||||
echo "Run 'spawn clouds' to see available clouds." >&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"
|
||||
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)
|
||||
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
|
||||
rm -f "$tmp"
|
||||
log_error "Could not determine remote version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$remote_version" == "$SPAWN_VERSION" ]]; then
|
||||
rm -f "$tmp"
|
||||
log_info "Already up to date (v${SPAWN_VERSION})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
chmod +x "$tmp"
|
||||
mv "$tmp" "$self"
|
||||
log_info "Updated: v${SPAWN_VERSION} → v${remote_version}"
|
||||
|
||||
# Invalidate manifest cache on update
|
||||
rm -f "$SPAWN_MANIFEST"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
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 "Run 'spawn help' for usage." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (( $# >= 2 )); then
|
||||
cmd_run "$agent" "$2"
|
||||
else
|
||||
cmd_agent_info "$agent"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -45,7 +45,7 @@ safe_read() {
|
|||
nc_listen() {
|
||||
local port=$1
|
||||
shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else
|
||||
nc -l "$port" "$@"
|
||||
|
|
@ -113,26 +113,21 @@ try_oauth_flow() {
|
|||
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}@keyframes fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}.sub{color:#ffffff66;font-size:.85rem;animation:fadein .5s ease .5s both}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>Redirecting back to terminal...</p><p class="sub">This tab will close automatically</p></div><script>setTimeout(function(){try{window.close()}catch(e){}setTimeout(function(){document.querySelector(".sub").textContent="You can safely close this tab"},500)},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}@keyframes fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}.sub{color:#ffffff66;font-size:.85rem;animation:fadein .5s ease .5s both}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>Redirecting back to terminal...</p><p class="sub">This tab will close automatically</p></div><script>setTimeout(function(){try{window.close()}catch(e){}setTimeout(function(){document.querySelector(".sub").textContent="You can safely close this tab"},500)},3000)</script></body></html>'
|
||||
|
||||
while true; do
|
||||
local response_file=$(mktemp)
|
||||
echo -e "$success_response" > "$response_file"
|
||||
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?
|
||||
rm -f "$response_file"
|
||||
|
||||
if [[ $nc_status -ne 0 ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
local code=$(echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p')
|
||||
echo "$code" > "$code_file"
|
||||
break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!
|
||||
|
|
@ -152,7 +147,7 @@ try_oauth_flow() {
|
|||
local elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do
|
||||
sleep 1
|
||||
((elapsed++))
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
|
||||
kill $server_pid 2>/dev/null || true
|
||||
|
|
@ -452,7 +447,7 @@ for net in data['droplet']['networks']['v4']:
|
|||
|
||||
log_warn "Droplet status: $status ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Droplet did not become active in time"
|
||||
|
|
@ -473,7 +468,7 @@ verify_server_connectivity() {
|
|||
fi
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Server failed to respond via SSH after $max_attempts attempts"
|
||||
|
|
@ -494,7 +489,7 @@ wait_for_cloud_init() {
|
|||
fi
|
||||
log_warn "Cloud-init in progress... ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Cloud-init did not complete after $max_attempts attempts"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ safe_read() {
|
|||
|
||||
nc_listen() {
|
||||
local port=$1; shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else nc -l "$port" "$@"; fi
|
||||
}
|
||||
|
|
@ -64,23 +64,29 @@ try_oauth_flow() {
|
|||
local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}"
|
||||
local oauth_dir=$(mktemp -d) code_file="$oauth_dir/code"
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>'
|
||||
while true; do
|
||||
local response_file=$(mktemp); echo -e "$success_response" > "$response_file"
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?; rm -f "$response_file"
|
||||
if [[ $nc_status -ne 0 ]]; then break; fi
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"; break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!; sleep 1
|
||||
if ! kill -0 $server_pid 2>/dev/null; then log_warn "Failed to start OAuth server"; rm -rf "$oauth_dir"; return 1; fi
|
||||
log_warn "Opening browser to authenticate with OpenRouter..."; open_browser "$auth_url"
|
||||
local timeout=120 elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; elapsed=$((elapsed + 1)); done
|
||||
kill $server_pid 2>/dev/null || true; wait $server_pid 2>/dev/null || true
|
||||
if [[ ! -f "$code_file" ]]; then log_warn "OAuth timeout"; rm -rf "$oauth_dir"; return 1; fi
|
||||
local oauth_code=$(cat "$code_file"); rm -rf "$oauth_dir"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ safe_read() {
|
|||
|
||||
nc_listen() {
|
||||
local port=$1; shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else nc -l "$port" "$@"; fi
|
||||
}
|
||||
|
|
@ -62,23 +62,29 @@ try_oauth_flow() {
|
|||
local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}"
|
||||
local oauth_dir=$(mktemp -d) code_file="$oauth_dir/code"
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>'
|
||||
while true; do
|
||||
local response_file=$(mktemp); echo -e "$success_response" > "$response_file"
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?; rm -f "$response_file"
|
||||
if [[ $nc_status -ne 0 ]]; then break; fi
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"; break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!; sleep 1
|
||||
if ! kill -0 $server_pid 2>/dev/null; then log_warn "Failed to start OAuth server"; rm -rf "$oauth_dir"; return 1; fi
|
||||
log_warn "Opening browser to authenticate with OpenRouter..."; open_browser "$auth_url"
|
||||
local timeout=120 elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; elapsed=$((elapsed + 1)); done
|
||||
kill $server_pid 2>/dev/null || true; wait $server_pid 2>/dev/null || true
|
||||
if [[ ! -f "$code_file" ]]; then log_warn "OAuth timeout"; rm -rf "$oauth_dir"; return 1; fi
|
||||
local oauth_code=$(cat "$code_file"); rm -rf "$oauth_dir"
|
||||
|
|
@ -222,7 +228,7 @@ verify_server_connectivity() {
|
|||
if ssh $SSH_OPTS -o ConnectTimeout=5 "${username}@$ip" "echo ok" >/dev/null 2>&1; then
|
||||
log_info "SSH connection established"; return 0
|
||||
fi
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; ((attempt++))
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Server failed to respond via SSH after $max_attempts attempts"; return 1
|
||||
}
|
||||
|
|
@ -235,7 +241,7 @@ wait_for_cloud_init() {
|
|||
if ssh $SSH_OPTS "${username}@$ip" "test -f /tmp/.cloud-init-complete" >/dev/null 2>&1; then
|
||||
log_info "Startup script completed"; return 0
|
||||
fi
|
||||
log_warn "Startup script in progress... ($attempt/$max_attempts)"; sleep 5; ((attempt++))
|
||||
log_warn "Startup script in progress... ($attempt/$max_attempts)"; sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Startup script did not complete after $max_attempts attempts"; return 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ nc_listen() {
|
|||
local port=$1
|
||||
shift
|
||||
# Detect if nc requires -p flag (busybox nc on Termux)
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else
|
||||
nc -l "$port" "$@"
|
||||
|
|
@ -120,29 +120,21 @@ try_oauth_flow() {
|
|||
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Use a simpler nc approach - pipe response while capturing request
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}@keyframes fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}.sub{color:#ffffff66;font-size:.85rem;animation:fadein .5s ease .5s both}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>Redirecting back to terminal...</p><p class="sub">This tab will close automatically</p></div><script>setTimeout(function(){try{window.close()}catch(e){}setTimeout(function(){document.querySelector(".sub").textContent="You can safely close this tab"},500)},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}@keyframes fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}.sub{color:#ffffff66;font-size:.85rem;animation:fadein .5s ease .5s both}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>Redirecting back to terminal...</p><p class="sub">This tab will close automatically</p></div><script>setTimeout(function(){try{window.close()}catch(e){}setTimeout(function(){document.querySelector(".sub").textContent="You can safely close this tab"},500)},3000)</script></body></html>'
|
||||
|
||||
while true; do
|
||||
# Listen and capture just the first line of the request, then respond
|
||||
local response_file=$(mktemp)
|
||||
echo -e "$success_response" > "$response_file"
|
||||
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?
|
||||
rm -f "$response_file"
|
||||
|
||||
# If nc failed, exit the loop
|
||||
if [[ $nc_status -ne 0 ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
local code=$(echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p')
|
||||
echo "$code" > "$code_file"
|
||||
break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!
|
||||
|
|
@ -166,7 +158,7 @@ try_oauth_flow() {
|
|||
local elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do
|
||||
sleep 1
|
||||
((elapsed++))
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
|
||||
# Kill the background server process
|
||||
|
|
@ -461,7 +453,7 @@ verify_server_connectivity() {
|
|||
fi
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Server failed to respond via SSH after $max_attempts attempts"
|
||||
|
|
@ -482,7 +474,7 @@ wait_for_cloud_init() {
|
|||
fi
|
||||
log_warn "Cloud-init in progress... ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Cloud-init did not complete after $max_attempts attempts"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ safe_read() {
|
|||
|
||||
nc_listen() {
|
||||
local port=$1; shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else nc -l "$port" "$@"; fi
|
||||
}
|
||||
|
|
@ -62,23 +62,29 @@ try_oauth_flow() {
|
|||
local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}"
|
||||
local oauth_dir=$(mktemp -d) code_file="$oauth_dir/code"
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>'
|
||||
while true; do
|
||||
local response_file=$(mktemp); echo -e "$success_response" > "$response_file"
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?; rm -f "$response_file"
|
||||
if [[ $nc_status -ne 0 ]]; then break; fi
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"; break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!; sleep 1
|
||||
if ! kill -0 $server_pid 2>/dev/null; then log_warn "Failed to start OAuth server"; rm -rf "$oauth_dir"; return 1; fi
|
||||
log_warn "Opening browser to authenticate with OpenRouter..."; open_browser "$auth_url"
|
||||
local timeout=120 elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; elapsed=$((elapsed + 1)); done
|
||||
kill $server_pid 2>/dev/null || true; wait $server_pid 2>/dev/null || true
|
||||
if [[ ! -f "$code_file" ]]; then log_warn "OAuth timeout"; rm -rf "$oauth_dir"; return 1; fi
|
||||
local oauth_code=$(cat "$code_file"); rm -rf "$oauth_dir"
|
||||
|
|
@ -245,7 +251,7 @@ print(d.get('error', {}).get('message', d.get('error', 'Unknown error')))
|
|||
return 0
|
||||
fi
|
||||
log_warn "Instance status: $status ($attempt/$max_attempts)"
|
||||
sleep 10; ((attempt++))
|
||||
sleep 10; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Instance did not become active in time"; return 1
|
||||
}
|
||||
|
|
@ -257,7 +263,7 @@ verify_server_connectivity() {
|
|||
if ssh $SSH_OPTS -o ConnectTimeout=5 "ubuntu@$ip" "echo ok" >/dev/null 2>&1; then
|
||||
log_info "SSH connection established"; return 0
|
||||
fi
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; ((attempt++))
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Server failed to respond via SSH after $max_attempts attempts"; return 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ safe_read() {
|
|||
|
||||
nc_listen() {
|
||||
local port=$1; shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else nc -l "$port" "$@"; fi
|
||||
}
|
||||
|
|
@ -61,23 +61,29 @@ try_oauth_flow() {
|
|||
local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}"
|
||||
local oauth_dir=$(mktemp -d) code_file="$oauth_dir/code"
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>'
|
||||
while true; do
|
||||
local response_file=$(mktemp); echo -e "$success_response" > "$response_file"
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?; rm -f "$response_file"
|
||||
if [[ $nc_status -ne 0 ]]; then break; fi
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"; break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!; sleep 1
|
||||
if ! kill -0 $server_pid 2>/dev/null; then log_warn "Failed to start OAuth server"; rm -rf "$oauth_dir"; return 1; fi
|
||||
log_warn "Opening browser to authenticate with OpenRouter..."; open_browser "$auth_url"
|
||||
local timeout=120 elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; elapsed=$((elapsed + 1)); done
|
||||
kill $server_pid 2>/dev/null || true; wait $server_pid 2>/dev/null || true
|
||||
if [[ ! -f "$code_file" ]]; then log_warn "OAuth timeout"; rm -rf "$oauth_dir"; return 1; fi
|
||||
local oauth_code=$(cat "$code_file"); rm -rf "$oauth_dir"
|
||||
|
|
@ -276,7 +282,7 @@ print('; '.join(e.get('reason','Unknown') for e in errs) if errs else 'Unknown e
|
|||
return 0
|
||||
fi
|
||||
log_warn "Linode status: $status ($attempt/$max_attempts)"
|
||||
sleep 5; ((attempt++))
|
||||
sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Linode did not become active in time"; return 1
|
||||
}
|
||||
|
|
@ -288,7 +294,7 @@ verify_server_connectivity() {
|
|||
if ssh $SSH_OPTS -o ConnectTimeout=5 "root@$ip" "echo ok" >/dev/null 2>&1; then
|
||||
log_info "SSH connection established"; return 0
|
||||
fi
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; ((attempt++))
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Server failed to respond via SSH after $max_attempts attempts"; return 1
|
||||
}
|
||||
|
|
@ -300,7 +306,7 @@ wait_for_cloud_init() {
|
|||
if ssh $SSH_OPTS "root@$ip" "test -f /root/.cloud-init-complete" >/dev/null 2>&1; then
|
||||
log_info "Cloud-init completed"; return 0
|
||||
fi
|
||||
log_warn "Cloud-init in progress... ($attempt/$max_attempts)"; sleep 5; ((attempt++))
|
||||
log_warn "Cloud-init in progress... ($attempt/$max_attempts)"; sleep 5; attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Cloud-init did not complete after $max_attempts attempts"; return 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ safe_read() {
|
|||
|
||||
nc_listen() {
|
||||
local port=$1; shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else nc -l "$port" "$@"; fi
|
||||
}
|
||||
|
|
@ -64,23 +64,29 @@ try_oauth_flow() {
|
|||
local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}"
|
||||
local oauth_dir=$(mktemp -d) code_file="$oauth_dir/code"
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa}p{color:#ffffffcc}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>'
|
||||
while true; do
|
||||
local response_file=$(mktemp); echo -e "$success_response" > "$response_file"
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?; rm -f "$response_file"
|
||||
if [[ $nc_status -ne 0 ]]; then break; fi
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"; break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!; sleep 1
|
||||
if ! kill -0 $server_pid 2>/dev/null; then log_warn "Failed to start OAuth server"; rm -rf "$oauth_dir"; return 1; fi
|
||||
log_warn "Opening browser to authenticate with OpenRouter..."; open_browser "$auth_url"
|
||||
local timeout=120 elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; elapsed=$((elapsed + 1)); done
|
||||
kill $server_pid 2>/dev/null || true; wait $server_pid 2>/dev/null || true
|
||||
if [[ ! -f "$code_file" ]]; then log_warn "OAuth timeout"; rm -rf "$oauth_dir"; return 1; fi
|
||||
local oauth_code=$(cat "$code_file"); rm -rf "$oauth_dir"
|
||||
|
|
|
|||
|
|
@ -149,14 +149,15 @@ EOF
|
|||
rm "$bash_temp"
|
||||
}
|
||||
|
||||
# Listen on a port with netcat (handles busybox/Termux nc requiring -p flag)
|
||||
# Listen on a port with netcat (handles macOS, Linux, busybox/Termux)
|
||||
nc_listen() {
|
||||
local port=$1
|
||||
shift
|
||||
# Detect if nc requires -p flag (busybox nc on Termux)
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
# BusyBox nc (Termux) requires -p flag
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else
|
||||
# macOS and Linux GNU nc both accept: nc -l <port>
|
||||
nc -l "$port" "$@"
|
||||
fi
|
||||
}
|
||||
|
|
@ -211,29 +212,21 @@ try_oauth_flow() {
|
|||
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Use a simpler nc approach - pipe response while capturing request
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}</style></head><body><div class="card"><h1>Authentication Successful!</h1><p>You can close this tab</p></div><script>setTimeout(function(){try{window.close()}catch(e){}},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}@keyframes fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}.sub{color:#ffffff66;font-size:.85rem;animation:fadein .5s ease .5s both}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>Redirecting back to terminal...</p><p class="sub">This tab will close automatically</p></div><script>setTimeout(function(){try{window.close()}catch(e){}setTimeout(function(){document.querySelector(".sub").textContent="You can safely close this tab"},500)},3000)</script></body></html>'
|
||||
|
||||
while true; do
|
||||
# Listen and capture just the first line of the request, then respond
|
||||
local response_file=$(mktemp)
|
||||
echo -e "$success_response" > "$response_file"
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?
|
||||
rm -f "$response_file"
|
||||
|
||||
# If nc failed, exit the loop
|
||||
if [[ $nc_status -ne 0 ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
local code=$(echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p')
|
||||
echo "$code" > "$code_file"
|
||||
break
|
||||
fi
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!
|
||||
|
|
@ -257,7 +250,7 @@ try_oauth_flow() {
|
|||
local elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do
|
||||
sleep 1
|
||||
((elapsed++))
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
|
||||
# Kill the background server process
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ safe_read() {
|
|||
|
||||
nc_listen() {
|
||||
local port=$1; shift
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then
|
||||
if nc --help 2>&1 | grep -q "BusyBox\|busybox"; then
|
||||
nc -l -p "$port" "$@"
|
||||
else
|
||||
nc -l "$port" "$@"
|
||||
|
|
@ -77,20 +77,22 @@ try_oauth_flow() {
|
|||
local oauth_dir=$(mktemp -d)
|
||||
local code_file="$oauth_dir/code"
|
||||
log_warn "Starting local OAuth server on port ${callback_port}..."
|
||||
|
||||
# Write the HTTP response to a file (using printf for macOS bash 3.x compat)
|
||||
local response_tpl="$oauth_dir/response.http"
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}@keyframes fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}.sub{color:#ffffff66;font-size:.85rem;animation:fadein .5s ease .5s both}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>Redirecting back to terminal...</p><p class="sub">This tab will close automatically</p></div><script>setTimeout(function(){try{window.close()}catch(e){}setTimeout(function(){document.querySelector(".sub").textContent="You can safely close this tab"},500)},3000)</script></body></html>' > "$response_tpl"
|
||||
|
||||
# Background listener
|
||||
(
|
||||
local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n<html><head><style>@keyframes checkmark{0%{transform:scale(0) rotate(-45deg);opacity:0}60%{transform:scale(1.2) rotate(-45deg);opacity:1}100%{transform:scale(1) rotate(-45deg);opacity:1}}@keyframes fadein{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#1a1a2e}.card{text-align:center;color:#fff}.check{width:80px;height:80px;border-radius:50%;background:#00d4aa22;display:flex;align-items:center;justify-content:center;margin:0 auto 24px}.check::after{content:"";display:block;width:28px;height:14px;border-left:4px solid #00d4aa;border-bottom:4px solid #00d4aa;animation:checkmark .5s ease forwards}h1{color:#00d4aa;margin:0 0 8px;font-size:1.6rem}p{margin:0 0 6px;color:#ffffffcc;font-size:1rem}.sub{color:#ffffff66;font-size:.85rem;animation:fadein .5s ease .5s both}</style></head><body><div class="card"><div class="check"></div><h1>Authentication Successful!</h1><p>Redirecting back to terminal...</p><p class="sub">This tab will close automatically</p></div><script>setTimeout(function(){try{window.close()}catch(e){}setTimeout(function(){document.querySelector(".sub").textContent="You can safely close this tab"},500)},3000)</script></body></html>'
|
||||
while true; do
|
||||
local response_file=$(mktemp)
|
||||
echo -e "$success_response" > "$response_file"
|
||||
local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1)
|
||||
local nc_status=$?
|
||||
rm -f "$response_file"
|
||||
if [[ $nc_status -ne 0 ]]; then break; fi
|
||||
if [[ "$request" == *"/callback?code="* ]]; then
|
||||
local code=$(echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p')
|
||||
echo "$code" > "$code_file"
|
||||
break
|
||||
fi
|
||||
request=$(nc_listen "$callback_port" < "$response_tpl" 2>/dev/null | head -1) || break
|
||||
|
||||
case "$request" in
|
||||
*"/callback?code="*)
|
||||
echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
) </dev/null &
|
||||
local server_pid=$!
|
||||
|
|
@ -103,7 +105,7 @@ try_oauth_flow() {
|
|||
log_warn "Opening browser to authenticate with OpenRouter..."
|
||||
open_browser "$auth_url"
|
||||
local timeout=120 elapsed=0
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done
|
||||
while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; elapsed=$((elapsed + 1)); done
|
||||
kill $server_pid 2>/dev/null || true
|
||||
wait $server_pid 2>/dev/null || true
|
||||
if [[ ! -f "$code_file" ]]; then
|
||||
|
|
@ -334,7 +336,7 @@ print(json.dumps(body))
|
|||
|
||||
log_warn "Instance status: $status/$power ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Instance did not become active in time"
|
||||
|
|
@ -353,7 +355,7 @@ verify_server_connectivity() {
|
|||
fi
|
||||
log_warn "Waiting for SSH... ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Server failed to respond via SSH after $max_attempts attempts"
|
||||
return 1
|
||||
|
|
@ -371,7 +373,7 @@ wait_for_cloud_init() {
|
|||
fi
|
||||
log_warn "Cloud-init in progress... ($attempt/$max_attempts)"
|
||||
sleep 5
|
||||
((attempt++))
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
log_error "Cloud-init did not complete after $max_attempts attempts"
|
||||
return 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue