diff --git a/README.md b/README.md index 1c605e2d..0b0a48c5 100644 --- a/README.md +++ b/README.md @@ -160,18 +160,18 @@ If an agent fails to install or launch on a cloud: ## Matrix -| | [Local Machine](local/) | [Hetzner Cloud](hetzner/) | [OVHcloud](ovh/) | [Fly.io](fly/) | [AWS Lightsail](aws/) | [Daytona](daytona/) | [DigitalOcean](digitalocean/) | [GCP Compute Engine](gcp/) | [Sprite](sprite/) | -|---|---|---|---|---|---|---|---|---|---| -| [**Claude Code**](https://claude.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**OpenClaw**](https://github.com/OpenRouterTeam/openclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**NanoClaw**](https://github.com/gavrielc/nanoclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Codex CLI**](https://github.com/openai/codex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Cline**](https://github.com/cline/cline) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**gptme**](https://github.com/gptme/gptme) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**OpenCode**](https://github.com/opencode-ai/opencode) | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Plandex**](https://github.com/plandex-ai/plandex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Kilo Code**](https://github.com/Kilo-Org/kilocode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Continue**](https://github.com/continuedev/continue) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| | [Local Machine](local/) | [Hetzner Cloud](hetzner/) | [Fly.io](fly/) | [AWS Lightsail](aws/) | [Daytona](daytona/) | [DigitalOcean](digitalocean/) | [GCP Compute Engine](gcp/) | [Sprite](sprite/) | +|---|---|---|---|---|---|---|---|---| +| [**Claude Code**](https://claude.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**OpenClaw**](https://github.com/OpenRouterTeam/openclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**NanoClaw**](https://github.com/gavrielc/nanoclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Codex CLI**](https://github.com/openai/codex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Cline**](https://github.com/cline/cline) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**gptme**](https://github.com/gptme/gptme) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**OpenCode**](https://github.com/opencode-ai/opencode) | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Plandex**](https://github.com/plandex-ai/plandex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Kilo Code**](https://github.com/Kilo-Org/kilocode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Continue**](https://github.com/continuedev/continue) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ### How it works diff --git a/cli/package.json b/cli/package.json index e502899d..e47aa51d 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.5.6", + "version": "0.5.7", "type": "module", "bin": { "spawn": "cli.js" diff --git a/cli/src/__tests__/cli-entry-edge-cases.test.ts b/cli/src/__tests__/cli-entry-edge-cases.test.ts index 123177d1..167935bd 100644 --- a/cli/src/__tests__/cli-entry-edge-cases.test.ts +++ b/cli/src/__tests__/cli-entry-edge-cases.test.ts @@ -37,6 +37,9 @@ function runCli( HOME: process.env.HOME, SHELL: process.env.SHELL, TERM: process.env.TERM || "xterm", + // Prevent OAuth browser from opening during tests — if OPENROUTER_API_KEY + // is set, get_or_prompt_api_key() skips the entire OAuth flow. + OPENROUTER_API_KEY: "sk-or-test-fake", ...env, SPAWN_NO_UPDATE_CHECK: "1", NODE_ENV: "", diff --git a/cli/src/__tests__/cloud-lib-api-surface.test.ts b/cli/src/__tests__/cloud-lib-api-surface.test.ts index 8476344d..afa4660a 100644 --- a/cli/src/__tests__/cloud-lib-api-surface.test.ts +++ b/cli/src/__tests__/cloud-lib-api-surface.test.ts @@ -74,12 +74,12 @@ function isSandboxOrContainer(cloud: string): boolean { } /** Check if a function name matches a pattern, allowing cloud-prefixed variants. - * e.g. hasFunctionOrVariant(fns, "run_server", "ovh") matches "run_server" or "run_ovh" */ + * e.g. hasFunctionOrVariant(fns, "run_server", "sprite") matches "run_server" or "run_sprite" */ function hasFunctionOrVariant(functions: string[], baseName: string, cloud: string): boolean { if (functions.includes(baseName)) return true; - // Check for cloud-prefixed variant (e.g. run_ovh, upload_file_sprite) + // Check for cloud-prefixed variant (e.g. run_sprite, upload_file_sprite) const prefix = baseName.replace(/_server$/, "").replace(/_file$/, ""); - const variant1 = `${prefix}_${cloud}`; // run_ovh, upload_file_ovh + const variant1 = `${prefix}_${cloud}`; // run_sprite, upload_file_sprite const variant2 = `${baseName}_${cloud}`; // upload_file_sprite return functions.includes(variant1) || functions.includes(variant2); } @@ -161,11 +161,11 @@ describe("Cloud lib/common.sh API surface contracts", () => { for (const fn of SSH_REQUIRED_FUNCTIONS) { it(`${cloud}/lib/common.sh defines ${fn}() or cloud-prefixed variant`, () => { - // Some clouds (OVH, Sprite) use cloud-prefixed function names - // e.g. run_ovh instead of run_server, create_ovh_instance instead of create_server + // Some clouds use cloud-prefixed function names + // e.g. run_sprite instead of run_server, create_sprite_instance instead of create_server const hasStandard = functions.includes(fn); const hasVariant = hasFunctionOrVariant(functions, fn, cloud); - // Also check for __ patterns (create_ovh_instance) + // Also check for __ patterns (create_sprite_instance) const hasExtendedVariant = functions.some((f) => { const prefix = fn.split("_")[0]; // "create", "run", "upload", etc. return f.startsWith(`${prefix}_${cloud}`); @@ -526,25 +526,6 @@ describe("Cloud lib/common.sh API surface contracts", () => { } }); - // ── OVH special case (uses function prefixing) ───────────────────── - - describe("OVH cloud special API pattern", () => { - const content = readCloudLib("ovh"); - if (!content) return; - const functions = extractFunctionNames(content); - - it("OVH lib defines signature-based auth functions", () => { - // OVH uses a custom auth pattern with signatures - const hasSigAuth = functions.some( - (fn) => - fn.includes("sign") || - fn.includes("ovh_api") || - fn.includes("_signature") - ); - expect(hasSigAuth).toBe(true); - }); - }); - // ── Sprite special case (CLI-based, no standard SSH) ─────────────── describe("Sprite cloud special CLI pattern", () => { diff --git a/cli/src/__tests__/cmdrun-resolution.test.ts b/cli/src/__tests__/cmdrun-resolution.test.ts index 9b8c0860..c9057978 100644 --- a/cli/src/__tests__/cmdrun-resolution.test.ts +++ b/cli/src/__tests__/cmdrun-resolution.test.ts @@ -34,6 +34,9 @@ function runCli( HOME: process.env.HOME, SHELL: process.env.SHELL, TERM: process.env.TERM || "xterm", + // Prevent OAuth browser from opening during tests — if OPENROUTER_API_KEY + // is set, get_or_prompt_api_key() skips the entire OAuth flow. + OPENROUTER_API_KEY: "sk-or-test-fake", ...env, SPAWN_NO_UPDATE_CHECK: "1", NODE_ENV: "", diff --git a/cli/src/__tests__/test-infra-sync.test.ts b/cli/src/__tests__/test-infra-sync.test.ts index ef69ac6b..9c0f8f84 100644 --- a/cli/src/__tests__/test-infra-sync.test.ts +++ b/cli/src/__tests__/test-infra-sync.test.ts @@ -120,7 +120,6 @@ function getCloudsInStripApiBase(): string[] { "console.kamatera.com": "kamatera", "api.latitude.sh": "latitude", "infrahub-api.nexgencloud.com": "hyperstack", - "eu.api.ovh.com": "ovh", "cloudapi.atlantic.net": "atlanticnet", "invapi.hostkey.com": "hostkey", "cloudsigma.com": "cloudsigma", diff --git a/cli/src/__tests__/upload-file-security.test.ts b/cli/src/__tests__/upload-file-security.test.ts index a7a696d4..0505d68a 100644 --- a/cli/src/__tests__/upload-file-security.test.ts +++ b/cli/src/__tests__/upload-file-security.test.ts @@ -210,7 +210,7 @@ describe("upload_file() Security Patterns", () => { .filter(([, info]) => info.type === "ssh"); it("should have multiple SSH-based clouds", () => { - expect(sshClouds.length).toBeGreaterThanOrEqual(5); + expect(sshClouds.length).toBeGreaterThanOrEqual(4); }); for (const [cloud, info] of sshClouds) { diff --git a/cli/src/commands.ts b/cli/src/commands.ts index 72a39441..864b4c5a 100644 --- a/cli/src/commands.ts +++ b/cli/src/commands.ts @@ -353,7 +353,7 @@ export function hasCloudCli(cloud: string): boolean { export function prioritizeCloudsByCredentials( clouds: string[], manifest: Manifest, - featuredCloud?: string + featuredCloud?: string[] ): { sortedClouds: string[]; hintOverrides: Record; credCount: number; cliCount: number } { const withCreds: string[] = []; const featured: string[] = []; @@ -362,7 +362,7 @@ export function prioritizeCloudsByCredentials( for (const c of clouds) { if (hasCloudCredentials(manifest.clouds[c].auth)) { withCreds.push(c); - } else if (featuredCloud && c === featuredCloud) { + } else if (featuredCloud && featuredCloud.includes(c)) { featured.push(c); } else if (hasCloudCli(c)) { withCli.push(c); @@ -1817,8 +1817,6 @@ function buildDeleteScript(cloud: string, connection: VMConnection): string { case "aws": return `${sourceLib}\nensure_aws_cli\ndestroy_server "${id}"`; - case "ovh": - return `${sourceLib}\nensure_ovh_authenticated\ndestroy_ovh_instance "${id}"`; case "daytona": return `${sourceLib}\nensure_daytona_cli\nensure_daytona_token\ndestroy_server "${id}"`; case "sprite": @@ -2568,7 +2566,7 @@ function getHelpExamplesSection(): string { spawn claude sprite --prompt "Fix all linter errors" ${pc.dim("# Execute Claude with prompt and exit")} spawn codex sprite -p "Add tests" ${pc.dim("# Short form of --prompt")} - spawn openclaw ovh -f instructions.txt + spawn openclaw fly -f instructions.txt ${pc.dim("# Read prompt from file (short for --prompt-file)")} spawn gptme gcp --dry-run ${pc.dim("# Preview without provisioning")} spawn claude hetzner --headless ${pc.dim("# Provision, print connection info, exit")} diff --git a/cli/src/manifest.ts b/cli/src/manifest.ts index bbaf99b6..928ff0a7 100644 --- a/cli/src/manifest.ts +++ b/cli/src/manifest.ts @@ -17,7 +17,7 @@ export interface AgentDef { interactive_prompts?: Record; dotenv?: { path: string; values: Record }; notes?: string; - featured_cloud?: string; + featured_cloud?: string[]; } export interface CloudDef { diff --git a/manifest.json b/manifest.json index c298877b..98ecab90 100644 --- a/manifest.json +++ b/manifest.json @@ -27,7 +27,7 @@ "bypassPermissionsModeAccepted": true } }, - "featured_cloud": "sprite" + "featured_cloud": ["sprite"] }, "openclaw": { "name": "OpenClaw", @@ -47,7 +47,7 @@ "default": "openrouter/auto" } }, - "featured_cloud": "fly" + "featured_cloud": ["fly"] }, "nanoclaw": { "name": "NanoClaw", @@ -70,7 +70,7 @@ } }, "notes": "Requires WhatsApp QR code scan for authentication", - "featured_cloud": "fly" + "featured_cloud": ["fly"] }, "codex": { "name": "Codex CLI", @@ -84,7 +84,7 @@ "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, "notes": "Works with OpenRouter via OPENAI_BASE_URL override pointing to openrouter.ai/api/v1", - "featured_cloud": "fly" + "featured_cloud": ["fly"] }, "cline": { "name": "Cline", @@ -98,7 +98,7 @@ "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, "notes": "Works with OpenRouter via OPENAI_BASE_URL override", - "featured_cloud": "fly" + "featured_cloud": ["fly"] }, "gptme": { "name": "gptme", @@ -116,7 +116,7 @@ } }, "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY and -m openrouter/... flag", - "featured_cloud": "daytona" + "featured_cloud": ["daytona"] }, "opencode": { "name": "OpenCode", @@ -128,7 +128,7 @@ "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY env var. Go-based TUI using Bubble Tea.", - "featured_cloud": "daytona" + "featured_cloud": ["daytona"] }, "plandex": { "name": "Plandex", @@ -140,7 +140,7 @@ "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY env var. Go-based CLI with sandbox and version control for AI changes.", - "featured_cloud": "daytona" + "featured_cloud": ["daytona"] }, "kilocode": { "name": "Kilo Code", @@ -154,7 +154,7 @@ "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, "notes": "Natively supports OpenRouter as a provider via KILO_PROVIDER_TYPE=openrouter. CLI installable via npm as @kilocode/cli, invocable as 'kilocode' or 'kilo'.", - "featured_cloud": "fly" + "featured_cloud": ["fly"] }, "continue": { "name": "Continue", @@ -179,7 +179,7 @@ } }, "notes": "Natively supports OpenRouter via config.json. CLI supports TUI mode (interactive) and headless mode (-p flag). 31K+ GitHub stars.", - "featured_cloud": "fly" + "featured_cloud": ["fly"] } }, "clouds": { @@ -209,22 +209,6 @@ "image": "ubuntu-24.04" } }, - "ovh": { - "name": "OVHcloud", - "description": "OVHcloud Public Cloud instances via REST API", - "url": "https://www.ovhcloud.com/", - "type": "api", - "auth": "OVH_APPLICATION_KEY + OVH_APPLICATION_SECRET + OVH_CONSUMER_KEY + OVH_PROJECT_ID", - "provision_method": "POST /cloud/project/{projectId}/instance with signature auth", - "exec_method": "ssh ubuntu@IP", - "interactive_method": "ssh -t ubuntu@IP", - "defaults": { - "flavor": "d2-2", - "region": "GRA7", - "image": "Ubuntu 24.04" - }, - "notes": "Major European cloud provider. Uses signature-based auth (Application Key + Secret + Consumer Key). Requires OVH_PROJECT_ID for Public Cloud. Create credentials at https://api.ovh.com/createToken/" - }, "fly": { "name": "Fly.io", "description": "Fly.io Machines via REST API and flyctl CLI", @@ -373,14 +357,6 @@ "gcp/plandex": "implemented", "fly/plandex": "implemented", "daytona/plandex": "implemented", - "ovh/claude": "implemented", - "ovh/codex": "implemented", - "ovh/openclaw": "implemented", - "ovh/nanoclaw": "implemented", - "ovh/cline": "implemented", - "ovh/gptme": "implemented", - "ovh/opencode": "implemented", - "ovh/plandex": "implemented", "sprite/kilocode": "implemented", "hetzner/kilocode": "implemented", "digitalocean/kilocode": "implemented", @@ -388,7 +364,6 @@ "gcp/kilocode": "implemented", "fly/kilocode": "implemented", "daytona/kilocode": "implemented", - "ovh/kilocode": "implemented", "sprite/continue": "implemented", "hetzner/continue": "implemented", "digitalocean/continue": "implemented", @@ -396,7 +371,6 @@ "gcp/continue": "implemented", "fly/continue": "implemented", "daytona/continue": "implemented", - "ovh/continue": "implemented", "local/claude": "implemented", "local/openclaw": "implemented", "local/nanoclaw": "implemented", diff --git a/ovh/README.md b/ovh/README.md deleted file mode 100644 index b85ad71e..00000000 --- a/ovh/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# OVHcloud - -OVHcloud Public Cloud instances via REST API. [OVHcloud](https://www.ovhcloud.com/) - -## Setup - -OVHcloud uses signature-based API authentication. You need: - -1. **Application Key** and **Application Secret** - Create at [https://api.ovh.com/createToken/](https://api.ovh.com/createToken/) -2. **Consumer Key** - Generated during token creation -3. **Project ID** - Find at [OVH Manager](https://www.ovh.com/manager/public-cloud/) (select project -> Project ID) - -Required API permissions: -- `GET /cloud/project/*` -- `POST /cloud/project/*` -- `DELETE /cloud/project/*` -- `GET /me` - -## Environment Variables - -| Variable | Description | -|----------|-------------| -| `OVH_APPLICATION_KEY` | OVH Application Key | -| `OVH_APPLICATION_SECRET` | OVH Application Secret | -| `OVH_CONSUMER_KEY` | OVH Consumer Key | -| `OVH_PROJECT_ID` | OVH Public Cloud Project ID | -| `OVH_SERVER_NAME` | Instance name (optional, prompted if not set) | -| `OVH_FLAVOR` | Instance flavor (default: `d2-2`) | -| `OVH_REGION` | Region (default: `GRA7`) | -| `OVH_SSH_USER` | SSH user (default: `ubuntu`) | - -## Agents - -#### Claude Code - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/claude.sh) -``` - -#### OpenClaw - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/openclaw.sh) -``` - -#### NanoClaw - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/nanoclaw.sh) -``` - -#### Cline - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/cline.sh) -``` - -#### Codex CLI - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/codex.sh) -``` - -#### OpenCode - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/opencode.sh) -``` - -#### Plandex - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/plandex.sh) -``` - -#### gptme - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/gptme.sh) -``` - -## Non-Interactive Mode - -```bash -OVH_SERVER_NAME=dev-mk1 \ -OVH_APPLICATION_KEY=your-app-key \ -OVH_APPLICATION_SECRET=your-app-secret \ -OVH_CONSUMER_KEY=your-consumer-key \ -OVH_PROJECT_ID=your-project-id \ -OPENROUTER_API_KEY=sk-or-v1-xxxxx \ - bash <(curl -fsSL https://openrouter.ai/labs/spawn/ovh/claude.sh) -``` diff --git a/ovh/claude.sh b/ovh/claude.sh deleted file mode 100755 index 9db970fc..00000000 --- a/ovh/claude.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "Claude Code on OVHcloud" -echo "" - -agent_pre_provision() { prompt_github_auth; } -agent_install() { install_claude_code cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ - "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_API_KEY=" \ - "CLAUDE_CODE_SKIP_ONBOARDING=1" \ - "CLAUDE_CODE_ENABLE_TELEMETRY=0" -} -agent_configure() { setup_claude_code_config "${OPENROUTER_API_KEY}" cloud_upload cloud_run; } -agent_launch_cmd() { echo 'source ~/.spawnrc 2>/dev/null; export PATH=$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH; claude'; } - -spawn_agent "Claude Code" diff --git a/ovh/cline.sh b/ovh/cline.sh deleted file mode 100755 index 09a1543a..00000000 --- a/ovh/cline.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "Cline on OVHcloud" -echo "" - -agent_install() { install_agent "Cline" "npm install -g cline" cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" -} -agent_configure() { - log_step "Authenticating Cline with OpenRouter..." - cloud_run "source ~/.zshrc && cline auth -p openrouter -k \"\${OPENROUTER_API_KEY}\"" -} -agent_launch_cmd() { echo 'source ~/.zshrc && cline'; } - -spawn_agent "Cline" diff --git a/ovh/codex.sh b/ovh/codex.sh deleted file mode 100755 index 5ac3b3f6..00000000 --- a/ovh/codex.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "Codex CLI on OVHcloud" -echo "" - -agent_install() { install_agent "Codex CLI" "npm install -g @openai/codex" cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" -} -agent_launch_cmd() { echo 'source ~/.zshrc && codex'; } - -spawn_agent "Codex CLI" diff --git a/ovh/continue.sh b/ovh/continue.sh deleted file mode 100755 index 74a42fbb..00000000 --- a/ovh/continue.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "Continue on OVHcloud" -echo "" - -agent_install() { install_agent "Continue CLI" "npm install -g @continuedev/cli" cloud_run; } -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"; } -agent_configure() { setup_continue_config "${OPENROUTER_API_KEY}" cloud_upload cloud_run; } -agent_launch_cmd() { echo 'source ~/.zshrc && cn'; } - -spawn_agent "Continue" diff --git a/ovh/gptme.sh b/ovh/gptme.sh deleted file mode 100755 index b8decd20..00000000 --- a/ovh/gptme.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then - source "$SCRIPT_DIR/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "gptme on OVHcloud" -echo "" - -AGENT_MODEL_PROMPT=1 -AGENT_MODEL_DEFAULT="openrouter/auto" - -agent_install() { - install_agent "gptme" "command -v uv >/dev/null || curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH=\"\$HOME/.local/bin:\$PATH\" && uv tool install gptme" cloud_run - verify_agent "gptme" "export PATH=\"\$HOME/.local/bin:\$PATH\" && command -v gptme" "uv tool install gptme" cloud_run -} -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=$OPENROUTER_API_KEY"; } -agent_launch_cmd() { printf 'source ~/.zshrc && gptme -m %s' "${MODEL_ID}"; } - -spawn_agent "gptme" diff --git a/ovh/kilocode.sh b/ovh/kilocode.sh deleted file mode 100755 index 51f66551..00000000 --- a/ovh/kilocode.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "Kilo Code on OVHcloud" -echo "" - -agent_install() { install_agent "Kilo Code" "npm install -g @kilocode/cli" cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "KILO_PROVIDER_TYPE=openrouter" \ - "KILO_OPEN_ROUTER_API_KEY=${OPENROUTER_API_KEY}" -} -agent_launch_cmd() { echo 'source ~/.zshrc && kilocode'; } - -spawn_agent "Kilo Code" diff --git a/ovh/lib/common.sh b/ovh/lib/common.sh deleted file mode 100644 index 6f57c6ee..00000000 --- a/ovh/lib/common.sh +++ /dev/null @@ -1,438 +0,0 @@ -#!/bin/bash -set -eo pipefail -# Common bash functions for OVHcloud spawn scripts - -# ============================================================ -# Provider-agnostic functions -# ============================================================ - -# Source shared provider-agnostic functions (local or remote fallback) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/../../shared/common.sh" ]]; then - source "$SCRIPT_DIR/../../shared/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)" -fi - -# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh - -# ============================================================ -# OVHcloud specific functions -# ============================================================ - -readonly OVH_API_BASE="https://eu.api.ovh.com/1.0" -SPAWN_DASHBOARD_URL="https://www.ovhcloud.com/manager/" - -# OVH API requires signature-based authentication. -# Headers: X-Ovh-Application, X-Ovh-Consumer, X-Ovh-Timestamp, X-Ovh-Signature -# Signature = "$1$" + SHA1(APP_SECRET + "+" + CONSUMER_KEY + "+" + METHOD + "+" + FULL_URL + "+" + BODY + "+" + TIMESTAMP) - -# Get OVH server timestamp (for clock sync) -_ovh_get_timestamp() { - curl -s "${OVH_API_BASE}/auth/time" 2>/dev/null || date +%s -} - -# Compute OVH API signature -# Usage: _ovh_sign METHOD FULL_URL BODY TIMESTAMP -_ovh_sign() { - local method="$1" - local url="$2" - local body="$3" - local timestamp="$4" - - local sig_data="${OVH_APPLICATION_SECRET}+${OVH_CONSUMER_KEY}+${method}+${url}+${body}+${timestamp}" - local hash - hash=$(printf '%s' "${sig_data}" | openssl dgst -sha1 2>/dev/null | awk '{print $NF}') - printf '$1$%s' "${hash}" -} - -# Centralized curl wrapper for OVH API with signature auth -# Usage: ovh_api_call METHOD ENDPOINT [BODY] -ovh_api_call() { - local method="$1" - local endpoint="$2" - local body="${3:-}" - - local full_url="${OVH_API_BASE}${endpoint}" - local timestamp - timestamp=$(_ovh_get_timestamp) - - local signature - signature=$(_ovh_sign "${method}" "${full_url}" "${body}" "${timestamp}") - - local args=( - -s - -X "${method}" - -H "X-Ovh-Application: ${OVH_APPLICATION_KEY}" - -H "X-Ovh-Consumer: ${OVH_CONSUMER_KEY}" - -H "X-Ovh-Timestamp: ${timestamp}" - -H "X-Ovh-Signature: ${signature}" - -H "Content-Type: application/json" - ) - - if [[ -n "${body}" ]]; then - args+=(-d "${body}") - fi - - local response - response=$(curl "${args[@]}" "${full_url}" 2>&1) - echo "${response}" -} - -# Test OVH API credentials -_test_ovh_credentials() { - local response - response=$(ovh_api_call GET "/me") - if echo "$response" | grep -q '"message"'; then - return 1 - fi - return 0 -} - -# Ensure OVH credentials are available (env vars -> config file -> prompt+save) -ensure_ovh_authenticated() { - ensure_multi_credentials "OVHcloud" "$HOME/.config/spawn/ovh.json" \ - "https://api.ovh.com/createToken/" _test_ovh_credentials \ - "OVH_APPLICATION_KEY:application_key:Application Key" \ - "OVH_APPLICATION_SECRET:application_secret:Application Secret" \ - "OVH_CONSUMER_KEY:consumer_key:Consumer Key" \ - "OVH_PROJECT_ID:project_id:Project ID" -} - -# Check if SSH key is registered with OVH -ovh_check_ssh_key() { - check_ssh_key_by_fingerprint ovh_api_call "/cloud/project/${OVH_PROJECT_ID}/sshkey" "$1" -} - -# Register SSH key with OVH -ovh_register_ssh_key() { - local key_name="$1" - local pub_path="$2" - local pub_key - pub_key=$(cat "$pub_path") - - local body - body=$(echo "$pub_key" | python3 -c " -import json, sys -pub_key = sys.stdin.read().strip() -body = { - 'name': sys.argv[1], - 'publicKey': pub_key -} -print(json.dumps(body)) -" "$key_name") - - local response - response=$(ovh_api_call POST "/cloud/project/${OVH_PROJECT_ID}/sshkey" "$body") - - if echo "$response" | grep -q '"message"'; then - local error_msg - error_msg=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('message','Unknown error'))" 2>/dev/null || echo "$response") - log_error "Failed to register SSH key: $error_msg" - return 1 - fi - - return 0 -} - -# Ensure SSH key exists locally and is registered with OVH -ensure_ssh_key() { - ensure_ssh_key_with_provider ovh_check_ssh_key ovh_register_ssh_key "OVHcloud" -} - -# Get server name from env var or prompt -get_server_name() { - get_validated_server_name "OVH_SERVER_NAME" "Enter server name: " -} - -# Find OVH image ID for Ubuntu 24.04 -_ovh_find_image_id() { - local region="$1" - local images_response - images_response=$(ovh_api_call GET "/cloud/project/${OVH_PROJECT_ID}/image?region=${region}&osType=linux") - - python3 -c " -import json, sys -images = json.loads(sys.stdin.read()) -for img in images: - name = img.get('name', '') - if 'Ubuntu 24.04' in name or 'ubuntu-24.04' in name.lower(): - print(img['id']) - sys.exit(0) -# Fallback: any Ubuntu image -for img in images: - name = img.get('name', '') - if 'Ubuntu' in name or 'ubuntu' in name: - print(img['id']) - sys.exit(0) -print('') -" <<< "${images_response}" -} - -# Find OVH flavor ID -_ovh_find_flavor_id() { - local region="$1" - local flavor_name="$2" - local flavors_response - flavors_response=$(ovh_api_call GET "/cloud/project/${OVH_PROJECT_ID}/flavor?region=${region}") - - python3 -c " -import json, sys -flavors = json.loads(sys.stdin.read()) -target = sys.argv[1] -for f in flavors: - if f.get('name', '') == target: - print(f['id']) - sys.exit(0) -print('') -" "$flavor_name" <<< "${flavors_response}" -} - -# Get SSH key ID from OVH -_ovh_get_ssh_key_id() { - local fingerprint="$1" - local keys_response - keys_response=$(ovh_api_call GET "/cloud/project/${OVH_PROJECT_ID}/sshkey") - - python3 -c " -import json, sys -keys = json.loads(sys.stdin.read()) -fp = sys.argv[1] -for k in keys: - if fp in k.get('fingerprint', '') or fp in k.get('publicKey', ''): - print(k['id']) - sys.exit(0) -# Fallback: return first key -if keys: - print(keys[0]['id']) -" "$fingerprint" <<< "${keys_response}" -} - -# Resolve image ID, flavor ID, and SSH key ID for OVH instance creation -# Outputs three lines: image_id, flavor_id, ssh_key_id -# Usage: _ovh_resolve_resources REGION FLAVOR_NAME -_ovh_resolve_resources() { - local region="$1" - local flavor_name="$2" - - local image_id - image_id=$(_ovh_find_image_id "${region}") - if [[ -z "${image_id}" ]]; then - log_error "Failed to find Ubuntu 24.04 image in region ${region}" - log_error "Try a different OVH_REGION (e.g., GRA11, SBG5, BHS5, WAW1)" - return 1 - fi - log_info "Found image: ${image_id}" - - local flavor_id - flavor_id=$(_ovh_find_flavor_id "${region}" "${flavor_name}") - if [[ -z "${flavor_id}" ]]; then - log_error "Failed to find flavor '${flavor_name}' in region ${region}" - log_error "Try a different OVH_FLAVOR (e.g., d2-2, d2-4, s1-2) or OVH_REGION" - return 1 - fi - log_info "Found flavor: ${flavor_id}" - - local pub_path="${HOME}/.ssh/id_ed25519.pub" - local fingerprint - fingerprint=$(get_ssh_fingerprint "${pub_path}") - local ssh_key_id - ssh_key_id=$(_ovh_get_ssh_key_id "${fingerprint}") - - printf '%s\n%s\n%s\n' "${image_id}" "${flavor_id}" "${ssh_key_id}" -} - -# Build JSON request body for OVH instance creation -# Usage: _ovh_build_instance_body NAME FLAVOR_ID IMAGE_ID REGION SSH_KEY_ID -_ovh_build_instance_body() { - local name="$1" flavor_id="$2" image_id="$3" region="$4" ssh_key_id="$5" - python3 -c " -import json, sys -name, flavor_id, image_id, region, ssh_key_id = sys.argv[1:6] -body = { - 'name': name, - 'flavorId': flavor_id, - 'imageId': image_id, - 'region': region, - 'monthlyBilling': False -} -if ssh_key_id: - body['sshKeyId'] = ssh_key_id -print(json.dumps(body)) -" "$name" "$flavor_id" "$image_id" "$region" "$ssh_key_id" -} - -# Create an OVH Public Cloud instance -create_ovh_instance() { - local name="$1" - local flavor="${OVH_FLAVOR:-d2-4}" - local region="${OVH_REGION:-GRA7}" - - # Validate env var inputs to prevent injection into Python code - validate_resource_name "$flavor" || { log_error "Invalid OVH_FLAVOR"; return 1; } - validate_region_name "$region" || { log_error "Invalid OVH_REGION"; return 1; } - - log_step "Creating OVHcloud instance '$name' (flavor: $flavor, region: $region)..." - - # Resolve image, flavor, and SSH key IDs - local resources - resources=$(_ovh_resolve_resources "${region}" "${flavor}") || return 1 - local image_id flavor_id ssh_key_id - { read -r image_id; read -r flavor_id; read -r ssh_key_id; } <<< "${resources}" - - local body - body=$(_ovh_build_instance_body "$name" "$flavor_id" "$image_id" "$region" "$ssh_key_id") - - local response - response=$(ovh_api_call POST "/cloud/project/${OVH_PROJECT_ID}/instance" "$body") - - # Check for errors - if echo "$response" | grep -q '"message"'; then - log_error "Failed to create OVHcloud instance" - local error_msg - error_msg=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('message','Unknown error'))" 2>/dev/null || echo "$response") - log_error "API Error: $error_msg" - log_error "" - log_error "Common issues:" - log_error " - Insufficient account balance or payment method required" - log_error " - Flavor/region unavailable (try different OVH_FLAVOR or OVH_REGION)" - log_error " - Project quota reached" - log_error "" - log_error "Check your account at: https://www.ovh.com/manager/public-cloud/" - return 1 - fi - - # Extract instance ID - OVH_INSTANCE_ID=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['id'])" 2>/dev/null) - if [[ -z "$OVH_INSTANCE_ID" ]]; then - log_error "Failed to extract instance ID from API response" - log_error "Response: $response" - return 1 - fi - export OVH_INSTANCE_ID - - log_info "Instance created: ID=$OVH_INSTANCE_ID" -} - -# Wait for OVH instance to be ACTIVE and get IP -# OVH IP extraction: prefer public IPv4, fallback to first IPv4 -wait_for_ovh_instance() { - local instance_id="$1" - local max_attempts="${2:-60}" - - generic_wait_for_instance ovh_api_call \ - "/cloud/project/${OVH_PROJECT_ID}/instance/${instance_id}" \ - "ACTIVE" \ - "d.get('status','')" \ - "next((a['ip'] for a in d.get('ipAddresses',[]) if a.get('version',0)==4 and a.get('type','')=='public'), next((a['ip'] for a in d.get('ipAddresses',[]) if a.get('version',0)==4), ''))" \ - OVH_SERVER_IP "OVHcloud instance" "${max_attempts}" -} - -# Destroy an OVH instance -destroy_ovh_instance() { - local instance_id="$1" - - log_step "Destroying OVHcloud instance $instance_id..." - local response - response=$(ovh_api_call DELETE "/cloud/project/${OVH_PROJECT_ID}/instance/${instance_id}") - - if echo "$response" | grep -q '"message"'; then - log_error "Failed to destroy instance $instance_id" - log_error "API Error: $(extract_api_error_message "$response" "$response")" - log_error "" - log_error "The instance may still be running and incurring charges." - log_error "Delete it manually at: https://www.ovhcloud.com/manager/" - return 1 - fi - - log_info "Instance $instance_id destroyed" -} - -# Standardized destroy_server wrapper (for compatibility with cross-cloud scripts) -destroy_server() { - destroy_ovh_instance "$@" -} - -# OVH uses configurable SSH user (ubuntu for newer images, root for older) -SSH_USER="${OVH_SSH_USER:-ubuntu}" - -# SSH operations — delegates to shared helpers -verify_server_connectivity() { ssh_verify_connectivity "$@"; } -run_ovh() { ssh_run_server "$@"; } -upload_file_ovh() { ssh_upload_file "$@"; } -interactive_session() { ssh_interactive_session "$@"; } - -# Install base dependencies on the server (since OVH doesn't use cloud-init by default) -install_base_deps() { - local ip="$1" - - log_step "Installing base dependencies..." - - # Use sudo if not root - local sudo_prefix="" - if [[ "${SSH_USER}" != "root" ]]; then - sudo_prefix="sudo " - fi - - run_ovh "$ip" "${sudo_prefix}apt-get update -qq && ${sudo_prefix}apt-get install -y -qq curl unzip git zsh build-essential python3 python3-pip nodejs npm > /dev/null 2>&1" - run_ovh "$ip" "${sudo_prefix}npm install -g n && ${sudo_prefix}n 22 && ${sudo_prefix}ln -sf /usr/local/bin/node /usr/bin/node && ${sudo_prefix}ln -sf /usr/local/bin/npm /usr/bin/npm && ${sudo_prefix}ln -sf /usr/local/bin/npx /usr/bin/npx" - - # Install Bun - run_ovh "$ip" "curl -fsSL https://bun.sh/install | bash" - - # Install Claude Code - run_ovh "$ip" "curl -fsSL https://claude.ai/install.sh | bash" - - # Configure npm global prefix so non-root user can npm install -g without sudo - run_ovh "$ip" "mkdir -p ~/.npm-global/bin && npm config set prefix ~/.npm-global" - - # Configure PATH - run_ovh "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.npm-global/bin:\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.bashrc" - run_ovh "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.npm-global/bin:\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.zshrc" - - log_info "Base dependencies installed" -} - -# List all OVH instances -list_instances() { - local response - response=$(ovh_api_call GET "/cloud/project/${OVH_PROJECT_ID}/instance") - - python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -if not data: - print('No instances found') - sys.exit(0) -print(f\"{'NAME':<25} {'ID':<40} {'STATUS':<12} {'IP':<16} {'FLAVOR':<10}\") -print('-' * 103) -for s in data: - name = s['name'] - sid = s['id'][:36] - status = s['status'] - ip = 'N/A' - for addr in s.get('ipAddresses', []): - if addr.get('version', 0) == 4: - ip = addr['ip'] - break - flavor = s.get('flavor', {}).get('name', 'N/A') if isinstance(s.get('flavor'), dict) else 'N/A' - print(f'{name:<25} {sid:<40} {status:<12} {ip:<16} {flavor:<10}') -" <<< "$response" -} - -# ============================================================ -# Cloud adapter interface -# ============================================================ - -cloud_authenticate() { ensure_ovh_authenticated; ensure_ssh_key; } -cloud_provision() { local name="$1"; OVH_SERVER_NAME="${name}"; export OVH_SERVER_NAME; create_ovh_instance "${name}"; } -cloud_wait_ready() { - wait_for_ovh_instance "${OVH_INSTANCE_ID}" - save_vm_connection "${OVH_SERVER_IP}" "${OVH_SSH_USER:-ubuntu}" "${OVH_INSTANCE_ID}" "${OVH_SERVER_NAME:-}" "ovh" - verify_server_connectivity "${OVH_SERVER_IP}" - install_base_deps "${OVH_SERVER_IP}" -} -cloud_run() { run_ovh "${OVH_SERVER_IP}" "$1"; } -cloud_upload() { upload_file_ovh "${OVH_SERVER_IP}" "$1" "$2"; } -cloud_interactive() { interactive_session "${OVH_SERVER_IP}" "$1"; } -cloud_label() { echo "OVHcloud instance"; } diff --git a/ovh/nanoclaw.sh b/ovh/nanoclaw.sh deleted file mode 100755 index 565a68bf..00000000 --- a/ovh/nanoclaw.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "NanoClaw on OVHcloud" -echo "" - -agent_install() { - log_step "Installing Docker (required by NanoClaw on Linux)..." - cloud_run "command -v docker >/dev/null || (curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker \$(whoami))" - log_step "Installing tsx..." - cloud_run "source ~/.bashrc && bun install -g tsx" - log_step "Cloning and building nanoclaw..." - cloud_run "git clone https://github.com/gavrielc/nanoclaw.git ~/nanoclaw && cd ~/nanoclaw && npm install && npm run build" - log_info "NanoClaw installed" -} -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_BASE_URL=https://openrouter.ai/api" -} -agent_configure() { - local dotenv_content - dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") - upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" -} -agent_launch_cmd() { echo 'cd ~/nanoclaw && source ~/.zshrc && npm run dev'; } - -spawn_agent "NanoClaw" diff --git a/ovh/openclaw.sh b/ovh/openclaw.sh deleted file mode 100755 index 9d9b7006..00000000 --- a/ovh/openclaw.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "OpenClaw on OVHcloud" -echo "" - -AGENT_MODEL_PROMPT=1 -AGENT_MODEL_DEFAULT="openrouter/auto" - -agent_install() { install_agent "openclaw" "source ~/.bashrc && bun install -g openclaw" cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_BASE_URL=https://openrouter.ai/api" -} -agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } -agent_pre_launch() { - start_openclaw_gateway cloud_run - wait_for_openclaw_gateway cloud_run -} -agent_launch_cmd() { echo 'source ~/.zshrc && openclaw tui'; } - -spawn_agent "OpenClaw" diff --git a/ovh/opencode.sh b/ovh/opencode.sh deleted file mode 100755 index 39deaed8..00000000 --- a/ovh/opencode.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "OpenCode on OVHcloud" -echo "" - -agent_install() { install_agent "OpenCode" "$(opencode_install_cmd)" cloud_run; } -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"; } -agent_launch_cmd() { echo 'source ~/.zshrc && opencode'; } - -spawn_agent "OpenCode" diff --git a/ovh/plandex.sh b/ovh/plandex.sh deleted file mode 100755 index 06ae8529..00000000 --- a/ovh/plandex.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=ovh/lib/common.sh -if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then - source "${SCRIPT_DIR}/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/ovh/lib/common.sh)" -fi - -log_info "Plandex on OVHcloud" -echo "" - -agent_install() { - install_agent "Plandex" "curl -sL https://plandex.ai/install.sh | bash" cloud_run - verify_agent "Plandex" "command -v plandex && plandex version" "curl -sL https://plandex.ai/install.sh | bash" cloud_run -} -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"; } -agent_launch_cmd() { echo 'source ~/.zshrc && plandex'; } - -spawn_agent "Plandex" diff --git a/test/e2e.sh b/test/e2e.sh index 83d405bc..54d1b7cd 100644 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -61,7 +61,6 @@ _get_name_env_var() { digitalocean) echo "DO_DROPLET_NAME" ;; aws) echo "LIGHTSAIL_SERVER_NAME" ;; daytona) echo "DAYTONA_SANDBOX_NAME" ;; - ovh) echo "OVH_SERVER_NAME" ;; gcp) echo "GCP_INSTANCE_NAME" ;; sprite) echo "SPRITE_NAME" ;; @@ -76,7 +75,6 @@ _get_token_env_var() { hetzner) echo "HCLOUD_TOKEN" ;; digitalocean) echo "DO_API_TOKEN" ;; daytona) echo "DAYTONA_API_KEY" ;; - ovh) echo "OVH_APP_KEY" ;; *) echo "" ;; esac } @@ -378,20 +376,6 @@ for d in data.get('droplets', []): source "${REPO_ROOT}/daytona/lib/common.sh" 2>/dev/null || return 0 destroy_server "$server_name" 2>/dev/null || true ;; - ovh) - source "${REPO_ROOT}/ovh/lib/common.sh" 2>/dev/null || return 0 - # OVH needs instance ID — list and find by name - local instances_json iid - instances_json=$(ovh_api GET "/cloud/project/${OVH_PROJECT_ID:-}/instance" 2>/dev/null) || return 0 - iid=$(printf '%s' "$instances_json" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -for i in (data if isinstance(data, list) else []): - if i.get('name') == '${server_name}': - print(i['id']); break -" 2>/dev/null) || return 0 - [[ -n "$iid" ]] && destroy_server "$iid" 2>/dev/null || true - ;; esac } @@ -420,10 +404,6 @@ _setup_noninteractive_env() { export GCP_ZONE="${GCP_ZONE:-us-central1-a}" export GCP_MACHINE_TYPE="${GCP_MACHINE_TYPE:-e2-micro}" ;; - ovh) - export OVH_REGION="${OVH_REGION:-GRA7}" - export OVH_FLAVOR="${OVH_FLAVOR:-d2-2}" - ;; esac } @@ -1034,7 +1014,7 @@ main() { E2E_RESULTS_DIR=$(mktemp -d "${TMPDIR:-/tmp}/e2e-results-XXXXXX") # Testable clouds (excludes local, sprite which don't provision real servers the same way) - local testable_clouds="fly hetzner digitalocean ovh aws daytona gcp" + local testable_clouds="fly hetzner digitalocean aws daytona gcp" # --- Credential collection (interactive) --- # Load tokens from config files and prompt for any missing ones diff --git a/test/mock-curl-script.sh b/test/mock-curl-script.sh index b6edf8c7..fafa173b 100644 --- a/test/mock-curl-script.sh +++ b/test/mock-curl-script.sh @@ -86,7 +86,6 @@ _strip_api_base() { case "$URL" in https://api.hetzner.cloud/v1*) ENDPOINT="${URL#https://api.hetzner.cloud/v1}" ;; https://api.digitalocean.com/v2*) ENDPOINT="${URL#https://api.digitalocean.com/v2}" ;; - *eu.api.ovh.com*) ENDPOINT=$(echo "$URL" | sed 's|https://eu.api.ovh.com/1.0||') ;; https://api.machines.dev/v1*) ENDPOINT="${URL#https://api.machines.dev/v1}" ;; esac EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||') @@ -110,7 +109,6 @@ _validate_body() { case "${MOCK_CLOUD}" in hetzner) case "$EP_CLEAN" in /servers) _check_fields "name server_type image location" ;; esac ;; digitalocean) case "$EP_CLEAN" in /droplets) _check_fields "name region size image" ;; esac ;; - ovh) case "$EP_CLEAN" in */create) _check_fields "name" ;; esac ;; fly) case "$EP_CLEAN" in */machines) _check_fields "name region config" ;; esac ;; esac } diff --git a/test/mock.sh b/test/mock.sh index 55b2e070..7b574711 100644 --- a/test/mock.sh +++ b/test/mock.sh @@ -340,8 +340,6 @@ _strip_api_base() { endpoint="${url#https://api.hetzner.cloud/v1}" ;; https://api.digitalocean.com/v2*) endpoint="${url#https://api.digitalocean.com/v2}" ;; - *eu.api.ovh.com*) - endpoint=$(echo "$url" | sed 's|https://eu.api.ovh.com/1.0||') ;; https://api.machines.dev/v1*) endpoint="${url#https://api.machines.dev/v1}" ;; esac @@ -357,7 +355,6 @@ _get_required_fields() { case "${cloud}:${endpoint}" in hetzner:/servers) echo "name server_type image location" ;; digitalocean:/droplets) echo "name region size image" ;; - ovh:*/create) echo "name" ;; fly:*/machines) echo "name region config" ;; esac } diff --git a/test/record.sh b/test/record.sh index 8797d8d9..95f25d22 100644 --- a/test/record.sh +++ b/test/record.sh @@ -37,7 +37,7 @@ ERRORS=0 PROMPT_FOR_CREDS=true # All clouds with REST APIs that we can record from -ALL_RECORDABLE_CLOUDS="hetzner digitalocean ovh fly" +ALL_RECORDABLE_CLOUDS="hetzner digitalocean fly" # --- Endpoint registry --- # Declare endpoints as string literal for each cloud @@ -58,12 +58,6 @@ regions:/regions " -_ENDPOINTS_ovh=" -flavors:/cloud/project/\${OVH_PROJECT_ID:-MISSING}/flavor -images:/cloud/project/\${OVH_PROJECT_ID:-MISSING}/image -ssh_keys:/cloud/project/\${OVH_PROJECT_ID:-MISSING}/sshkey -" - _ENDPOINTS_fly=" apps:/apps?org_slug=personal " @@ -82,13 +76,6 @@ get_endpoints() { _get_multi_cred_spec() { local cloud="$1" case "$cloud" in - ovh) - printf '%s\n' \ - "application_key:OVH_APPLICATION_KEY" \ - "application_secret:OVH_APPLICATION_SECRET" \ - "consumer_key:OVH_CONSUMER_KEY" \ - "project_id:OVH_PROJECT_ID" - ;; esac } @@ -173,7 +160,6 @@ get_auth_env_var() { case "$cloud" in hetzner) printf "HCLOUD_TOKEN" ;; digitalocean) printf "DO_API_TOKEN" ;; - ovh) printf "OVH_APPLICATION_KEY" ;; fly) printf "FLY_API_TOKEN" ;; esac } @@ -328,7 +314,6 @@ call_api() { case "$cloud" in hetzner) hetzner_api GET "$endpoint" ;; digitalocean) do_api GET "$endpoint" ;; - ovh) ovh_api_call GET "$endpoint" ;; fly) curl -fsSL -H "Authorization: ${FLY_API_TOKEN}" "https://api.machines.dev/v1${endpoint}" ;; esac } @@ -353,7 +338,6 @@ success_keys = {'servers','images','ssh_keys','flavors','sizes','regions','count error_checks = { 'hetzner': lambda d: d.get('error') and isinstance(d.get('error'), dict), 'digitalocean': lambda d: 'id' in d and isinstance(d.get('id'), str) and 'message' in d, - 'ovh': lambda d: 'message' in d and len(d) <= 3 and not any(k in d for k in success_keys), 'fly': lambda d: 'error' in d and isinstance(d.get('error'), str), }