feat: remove OVH cloud and make featured_cloud an array (#1474)

- Remove OVH as a cloud provider: delete ovh/ directory (lib + 11 agent
  scripts), remove from manifest.json clouds and all ovh/* matrix entries,
  update README matrix table, remove OVH destroy case in CLI commands,
  and clean up all test harness references (mock.sh, mock-curl-script.sh,
  record.sh, e2e.sh, cloud-lib-api-surface.test.ts, test-infra-sync.test.ts)

- Make featured_cloud an array (string[]) so agents can recommend multiple
  clouds; update manifest.ts type, all 10 manifest.json values, and the
  prioritizeCloudsByCredentials() comparison in commands.ts

- Sandbox OAuth in subprocess tests: add OPENROUTER_API_KEY=sk-or-test-fake
  to the default env in cli-entry-edge-cases.test.ts and
  cmdrun-resolution.test.ts so get_or_prompt_api_key() never triggers the
  real OAuth browser flow during test runs

- Fix upload-file-security.test.ts SSH cloud count (5→4) after OVH removal

- Bump CLI version 0.5.6 → 0.5.7

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
L 2026-02-19 14:06:27 -05:00 committed by GitHub
parent 5612cda40b
commit 32522882c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 42 additions and 920 deletions

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.5.6",
"version": "0.5.7",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -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: "",

View file

@ -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 <action>_<cloud>_<noun> patterns (create_ovh_instance)
// Also check for <action>_<cloud>_<noun> 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", () => {

View file

@ -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: "",

View file

@ -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",

View file

@ -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) {

View file

@ -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<string, string>; 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")}

View file

@ -17,7 +17,7 @@ export interface AgentDef {
interactive_prompts?: Record<string, { prompt: string; default: string }>;
dotenv?: { path: string; values: Record<string, string> };
notes?: string;
featured_cloud?: string;
featured_cloud?: string[];
}
export interface CloudDef {

View file

@ -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",

View file

@ -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)
```

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"; }

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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),
}