diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 3417fc66c..6f131b0f7 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openrouter/spawn", - "version": "0.2.82", + "version": "1.0.45", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@openrouter/spawn", - "version": "0.2.82", + "version": "1.0.45", "dependencies": { "@clack/prompts": "^1.0.0", "picocolors": "^1.1.1" diff --git a/packages/cli/package.json b/packages/cli/package.json index 7ddfc6674..380aa333d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "1.0.44", + "version": "1.0.46", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/local/agents.ts b/packages/cli/src/local/agents.ts index f2e7ec7c1..6972207a5 100644 --- a/packages/cli/src/local/agents.ts +++ b/packages/cli/src/local/agents.ts @@ -3,8 +3,13 @@ import { createCloudAgents } from "../shared/agent-setup.js"; import { downloadFile, runLocal, uploadFile } from "./local.js"; -export const { agents, resolveAgent } = createCloudAgents({ - runServer: runLocal, - uploadFile: async (l: string, r: string) => uploadFile(l, r), - downloadFile: async (r: string, l: string) => downloadFile(r, l), -}); +export const { agents, resolveAgent } = createCloudAgents( + { + runServer: runLocal, + uploadFile: async (l: string, r: string) => uploadFile(l, r), + downloadFile: async (r: string, l: string) => downloadFile(r, l), + }, + { + isLocal: true, + }, +); diff --git a/packages/cli/src/shared/agent-setup.ts b/packages/cli/src/shared/agent-setup.ts index 50eb8d8da..0e82912d7 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -1195,7 +1195,12 @@ export async function setupSecurityScan(runner: CloudRunner): Promise { // ─── Default Agent Definitions ─────────────────────────────────────────────── -function createAgents(runner: CloudRunner): Record { +function createAgents( + runner: CloudRunner, + options?: { + isLocal?: boolean; + }, +): Record { return { claude: { name: "Claude Code", @@ -1443,8 +1448,8 @@ function createAgents(runner: CloudRunner): Record { `OPENROUTER_API_KEY=${apiKey}`, `CURSOR_API_KEY=${apiKey}`, ], - configure: () => setupCursorProxy(runner), - preLaunch: () => startCursorProxy(runner), + configure: () => setupCursorProxy(runner, options), + preLaunch: () => startCursorProxy(runner, options), launchCmd: () => 'source ~/.spawnrc 2>/dev/null; export PATH="$HOME/.local/bin:$PATH"; agent --endpoint https://api2.cursor.sh', promptCmd: (prompt) => @@ -1468,11 +1473,16 @@ function resolveAgent(agents: Record, name: string): AgentC * Factory that creates agents + resolveAgent for a given CloudRunner. * Replaces the identical 16-line boilerplate in each cloud's agents.ts. */ -export function createCloudAgents(runner: CloudRunner): { +export function createCloudAgents( + runner: CloudRunner, + options?: { + isLocal?: boolean; + }, +): { agents: Record; resolveAgent: (name: string) => AgentConfig; } { - const agentMap = createAgents(runner); + const agentMap = createAgents(runner, options); return { agents: agentMap, resolveAgent: (name: string) => resolveAgent(agentMap, name), diff --git a/packages/cli/src/shared/cursor-proxy.ts b/packages/cli/src/shared/cursor-proxy.ts index 4b0f1255c..4e75eb62e 100644 --- a/packages/cli/src/shared/cursor-proxy.ts +++ b/packages/cli/src/shared/cursor-proxy.ts @@ -272,18 +272,41 @@ const CURSOR_DOMAINS = [ // ── Deployment ────────────────────────────────────────────────────────────── /** - * Deploy the Cursor proxy infrastructure onto the remote VM. + * Deploy the Cursor proxy infrastructure onto a remote VM. * Installs Caddy, uploads proxy scripts, writes Caddyfile, configures /etc/hosts. + * + * SAFETY: This modifies /etc/hosts and installs system-level services. + * Must NOT run on the user's local machine — only on remote VMs/containers. */ -export async function setupCursorProxy(runner: CloudRunner): Promise { +export async function setupCursorProxy( + runner: CloudRunner, + options?: { + isLocal?: boolean; + }, +): Promise { + if (options?.isLocal) { + logWarn("Cursor proxy setup skipped (not supported on local cloud — would modify host system)"); + return; + } logStep("Deploying Cursor→OpenRouter proxy..."); // 1. Install Caddy if not present + // Detect OS and arch at runtime so this works on macOS (darwin/arm64|amd64) + // and Linux (linux/amd64|arm64). Write to ~/.local/bin which is user-writable + // on every platform -- /usr/local/bin is SIP-protected on macOS and requires + // root elsewhere. const installCaddy = [ + "set -e", 'if command -v caddy >/dev/null 2>&1; then echo "caddy already installed"; exit 0; fi', 'echo "Installing Caddy..."', - 'curl -sf "https://caddyserver.com/api/download?os=linux&arch=amd64" -o /usr/local/bin/caddy', - "chmod +x /usr/local/bin/caddy", + 'OS=$(uname -s | tr "[:upper:]" "[:lower:]")', + "ARCH=$(uname -m)", + '[ "$ARCH" = "x86_64" ] && ARCH="amd64"', + '[ "$ARCH" = "aarch64" ] && ARCH="arm64"', + 'mkdir -p "$HOME/.local/bin"', + 'curl -fsSL "https://caddyserver.com/api/download?os=${OS}&arch=${ARCH}" -o "$HOME/.local/bin/caddy"', + 'chmod +x "$HOME/.local/bin/caddy"', + 'export PATH="$HOME/.local/bin:$PATH"', "caddy version", ].join("\n"); @@ -322,18 +345,23 @@ export async function setupCursorProxy(runner: CloudRunner): Promise { logInfo("Proxy scripts deployed"); // 3. Configure /etc/hosts for domain spoofing + // Requires elevated privileges on all platforms (/etc/hosts is root-owned). + // Use a temp-file swap via sudo cp to avoid sed -i portability issues + // (macOS sed requires a backup suffix with -i; Linux does not). const hostsScript = [ - // Remove any existing cursor entries - 'sed -i "/cursor\\.sh/d" /etc/hosts 2>/dev/null || true', - // Add our entries - `echo "127.0.0.1 ${CURSOR_DOMAINS.join(" ")}" >> /etc/hosts`, + // Build a clean copy without existing cursor entries then append ours + 'grep -v "cursor.sh" /etc/hosts > /tmp/hosts_cursor_new 2>/dev/null || cp /etc/hosts /tmp/hosts_cursor_new', + `echo "127.0.0.1 ${CURSOR_DOMAINS.join(" ")}" >> /tmp/hosts_cursor_new`, + "sudo cp /tmp/hosts_cursor_new /etc/hosts", + "rm -f /tmp/hosts_cursor_new", ].join(" && "); await wrapSshCall(runner.runServer(hostsScript)); logInfo("Hosts spoofing configured"); // 4. Install Caddy's internal CA cert - const trustScript = "caddy trust 2>/dev/null || true"; + // Include ~/.local/bin in PATH since Caddy may have been installed there above. + const trustScript = 'export PATH="$HOME/.local/bin:$PATH" && caddy trust 2>/dev/null || true'; await wrapSshCall(runner.runServer(trustScript, 30)); logInfo("Caddy CA trusted"); @@ -353,9 +381,17 @@ CONF`, /** * Start the Cursor proxy services (Caddy + two Node.js backends). - * Uses systemd if available, falls back to setsid/nohup. + * Uses systemd if available, falls back to nohup (POSIX -- works on macOS and Linux). */ -export async function startCursorProxy(runner: CloudRunner): Promise { +export async function startCursorProxy( + runner: CloudRunner, + options?: { + isLocal?: boolean; + }, +): Promise { + if (options?.isLocal) { + return; + } logStep("Starting Cursor proxy services..."); // Find Node.js binary (cursor bundles its own) @@ -370,6 +406,8 @@ export async function startCursorProxy(runner: CloudRunner): Promise { const script = [ "source ~/.spawnrc 2>/dev/null", + // Ensure ~/.local/bin (where Caddy may be installed) is in PATH for this script. + 'export PATH="$HOME/.local/bin:$PATH"', nodeFind, // Start unary backend @@ -395,7 +433,8 @@ export async function startCursorProxy(runner: CloudRunner): Promise { " $_sudo systemctl daemon-reload", " $_sudo systemctl restart cursor-proxy-unary", " else", - " setsid $NODE ~/.cursor/proxy/unary.mjs < /dev/null &", + // setsid is Linux-only; nohup is POSIX and works on macOS and Linux. + " nohup $NODE ~/.cursor/proxy/unary.mjs < /dev/null > /tmp/cursor-proxy-unary.log 2>&1 &", " fi", "fi", @@ -423,7 +462,7 @@ export async function startCursorProxy(runner: CloudRunner): Promise { " $_sudo systemctl daemon-reload", " $_sudo systemctl restart cursor-proxy-bidi", " else", - " setsid $NODE ~/.cursor/proxy/bidi.mjs < /dev/null &", + " nohup $NODE ~/.cursor/proxy/bidi.mjs < /dev/null > /tmp/cursor-proxy-bidi.log 2>&1 &", " fi", "fi",