mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
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:
parent
5612cda40b
commit
32522882c1
26 changed files with 42 additions and 920 deletions
24
README.md
24
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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.5.6",
|
||||
"version": "0.5.7",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
|
@ -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"
|
||||
27
ovh/cline.sh
27
ovh/cline.sh
|
|
@ -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"
|
||||
24
ovh/codex.sh
24
ovh/codex.sh
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
25
ovh/gptme.sh
25
ovh/gptme.sh
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"; }
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
22
test/e2e.sh
22
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue