diff --git a/CLAUDE.md b/CLAUDE.md index abf69944..c3ee6023 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,15 +44,14 @@ Look at `manifest.json` → `matrix` for any `"missing"` entry. To implement it: We are currently shipping with **9 curated clouds** (sorted by price): 1. **local** — free (no provisioning) -2. **oracle** — free tier (Always Free ARM instances) -3. **hetzner** — ~€3.29/mo (CX22) -4. **ovh** — ~€3.50/mo (d2-2) -5. **fly** — free tier (3 shared-cpu VMs) -6. **aws** — $3.50/mo (nano) -7. **daytona** — pay-per-second sandboxes -8. **digitalocean** — $4/mo (Basic droplet) -9. **gcp** — $7.11/mo (e2-micro) -10. **sprite** — Fly.io managed VMs +2. **hetzner** — ~€3.29/mo (CX22) +3. **ovh** — ~€3.50/mo (d2-2) +4. **fly** — free tier (3 shared-cpu VMs) +5. **aws** — $3.50/mo (nano) +6. **daytona** — pay-per-second sandboxes +7. **digitalocean** — $4/mo (Basic droplet) +8. **gcp** — $7.11/mo (e2-micro) +9. **sprite** — Fly.io managed VMs **Do NOT add clouds speculatively.** Every cloud must be manually tested and verified end-to-end before shipping. Adding a cloud that can't be tested is worse than not having it. diff --git a/README.md b/README.md index 294bf0db..1fbd22e6 100644 --- a/README.md +++ b/README.md @@ -155,23 +155,23 @@ If an agent fails to install or launch on a cloud: ## Matrix -| | [Local Machine](local/) | [Oracle Cloud Infrastructure](oracle/) | [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) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Aider**](https://github.com/paul-gauthier/aider) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Goose**](https://github.com/block/goose) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Codex CLI**](https://github.com/openai/codex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Open Interpreter**](https://github.com/OpenInterpreter/open-interpreter) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Amazon Q CLI**](https://aws.amazon.com/q/developer/) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**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/) | [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) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Aider**](https://github.com/paul-gauthier/aider) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Goose**](https://github.com/block/goose) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Codex CLI**](https://github.com/openai/codex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Open Interpreter**](https://github.com/OpenInterpreter/open-interpreter) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Amazon Q CLI**](https://aws.amazon.com/q/developer/) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Cline**](https://github.com/cline/cline) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**gptme**](https://github.com/gptme/gptme) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**OpenCode**](https://github.com/opencode-ai/opencode) | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Plandex**](https://github.com/plandex-ai/plandex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Kilo Code**](https://github.com/Kilo-Org/kilocode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Continue**](https://github.com/continuedev/continue) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ### How it works diff --git a/cli/package.json b/cli/package.json index 7315adf8..8ac0b2aa 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.5.0", + "version": "0.5.1", "type": "module", "bin": { "spawn": "cli.js" diff --git a/cli/src/__tests__/oracle-provider-patterns.test.ts b/cli/src/__tests__/oracle-provider-patterns.test.ts deleted file mode 100644 index e2fb6616..00000000 --- a/cli/src/__tests__/oracle-provider-patterns.test.ts +++ /dev/null @@ -1,870 +0,0 @@ -import { describe, it, expect } from "bun:test"; -import { readFileSync, existsSync } from "fs"; -import { join, resolve } from "path"; -import type { Manifest } from "../manifest"; - -/** - * Pattern tests for the Oracle Cloud Infrastructure (OCI) provider. - * - * Oracle is a CLI-based cloud provider with: - * - OCI CLI (oci compute) for instance management - * - Complex VCN networking setup (VCN -> Internet Gateway -> Route -> Subnet) - * - Cloud-init userdata for instance bootstrapping - * - Flex shape support with configurable OCPUs and memory - * - SSH-based exec (ubuntu@IP) - * - OCI_COMPARTMENT_ID-based resource scoping - * - * These tests validate: - * 1. lib/common.sh defines the correct provider-specific API surface - * 2. VCN networking setup is decomposed into focused helpers - * 3. Agent scripts follow the correct provisioning flow - * 4. Security conventions are enforced (no echo -e, no set -u) - * 5. SSH delegation patterns are used correctly - * 6. OpenRouter env var injection uses SSH-based helpers - * 7. Manifest consistency - * - * Agent: test-engineer - */ - -const REPO_ROOT = resolve(import.meta.dir, "../../.."); -const manifestPath = join(REPO_ROOT, "manifest.json"); -const manifest: Manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); - -// -- Helpers ------------------------------------------------------------------ - -function readScript(filePath: string): string { - return readFileSync(filePath, "utf-8"); -} - -function getCodeLines(content: string): string[] { - return content - .split("\n") - .filter((line) => line.trim() !== "" && !line.trimStart().startsWith("#")); -} - -function extractFunctions(content: string): string[] { - const matches = content.match(/^[_a-z][a-z0-9_]*\(\)/gm); - return matches ? matches.map((m) => m.replace("()", "")) : []; -} - -/** Collect implemented entries for Oracle */ -function getImplementedEntries() { - return Object.entries(manifest.matrix) - .filter(([key, status]) => key.startsWith("oracle/") && status === "implemented") - .map(([key]) => { - const agent = key.split("/")[1]; - return { key, agent, path: join(REPO_ROOT, key + ".sh") }; - }) - .filter(({ path }) => existsSync(path)); -} - -const oracleLibPath = join(REPO_ROOT, "oracle", "lib", "common.sh"); -const oracleLib = existsSync(oracleLibPath) ? readScript(oracleLibPath) : ""; -const oracleFunctions = extractFunctions(oracleLib); -const oracleEntries = getImplementedEntries(); - -// ============================================================================= -// lib/common.sh API surface -// ============================================================================= - -describe("Oracle lib/common.sh API surface", () => { - it("should exist", () => { - expect(existsSync(oracleLibPath)).toBe(true); - }); - - it("should source shared/common.sh with fallback pattern", () => { - expect(oracleLib).toContain("shared/common.sh"); - expect(oracleLib).toContain("raw.githubusercontent.com"); - expect(oracleLib).toContain("curl"); - }); - - it("should use set -eo pipefail", () => { - expect(oracleLib).toContain("set -eo pipefail"); - }); - - // Required SSH-based cloud functions - const requiredFunctions = [ - "create_server", - "destroy_server", - "verify_server_connectivity", - "run_server", - "upload_file", - "interactive_session", - "get_server_name", - "ensure_ssh_key", - "ensure_oci_cli", - "wait_for_cloud_init", - "list_servers", - ]; - - for (const fn of requiredFunctions) { - it(`should define ${fn}()`, () => { - expect(oracleFunctions).toContain(fn); - }); - } - - // Oracle-specific internal helpers - const internalHelpers = [ - "_get_ubuntu_image_id", - "_get_availability_domain", - "_create_vcn", - "_create_internet_gateway", - "_add_default_route", - "_add_ssh_security_rules", - "_setup_vcn_networking", - "_create_subnet", - "_get_subnet_id", - "_get_instance_public_ip", - "_encode_userdata_b64", - "_launch_oci_instance", - ]; - - for (const fn of internalHelpers) { - it(`should define internal helper ${fn}()`, () => { - expect(oracleFunctions).toContain(fn); - }); - } -}); - -// ============================================================================= -// OCI CLI dependency -// ============================================================================= - -describe("Oracle OCI CLI dependency management", () => { - it("should check for oci CLI with command -v", () => { - expect(oracleLib).toContain("command -v oci"); - }); - - it("should show pip install instructions if OCI CLI is missing", () => { - expect(oracleLib).toContain("pip install oci-cli"); - }); - - it("should show official installer URL as alternative", () => { - expect(oracleLib).toContain("oracle/oci-cli"); - }); - - it("should check for ~/.oci/config existence", () => { - expect(oracleLib).toContain(".oci/config"); - }); - - it("should show oci setup config guidance when config is missing", () => { - expect(oracleLib).toContain("oci setup config"); - }); - - it("should show what credentials are needed in error messages", () => { - expect(oracleLib).toContain("Tenancy OCID"); - expect(oracleLib).toContain("User OCID"); - expect(oracleLib).toContain("Compartment OCID"); - expect(oracleLib).toContain("Region"); - }); - - it("should use OCI_COMPARTMENT_ID for compartment scoping", () => { - expect(oracleLib).toContain("OCI_COMPARTMENT_ID"); - }); - - it("should attempt auto-detection of compartment when env var is not set", () => { - expect(oracleLib).toContain("oci iam compartment list"); - }); - - it("should export OCI_COMPARTMENT_ID after detection", () => { - expect(oracleLib).toContain('export OCI_COMPARTMENT_ID'); - }); -}); - -// ============================================================================= -// VCN networking setup (decomposed helpers) -// ============================================================================= - -describe("Oracle VCN networking setup", () => { - it("should create VCN with 10.0.0.0/16 CIDR", () => { - expect(oracleLib).toContain("10.0.0.0/16"); - }); - - it("should use oci network vcn create for VCN creation", () => { - expect(oracleLib).toContain("oci network vcn create"); - }); - - it("should name VCN as spawn-vcn", () => { - expect(oracleLib).toContain("spawn-vcn"); - }); - - it("should create internet gateway named spawn-igw", () => { - expect(oracleLib).toContain("spawn-igw"); - expect(oracleLib).toContain("oci network internet-gateway create"); - }); - - it("should add default route 0.0.0.0/0 via internet gateway", () => { - expect(oracleLib).toContain("0.0.0.0/0"); - expect(oracleLib).toContain("oci network route-table update"); - }); - - it("should add SSH security rules for port 22", () => { - expect(oracleLib).toContain("oci network security-list update"); - expect(oracleLib).toContain('"min":22'); - expect(oracleLib).toContain('"max":22'); - }); - - it("should create subnet with 10.0.1.0/24 CIDR", () => { - expect(oracleLib).toContain("10.0.1.0/24"); - expect(oracleLib).toContain("oci network subnet create"); - }); - - it("should name subnet as spawn-subnet", () => { - expect(oracleLib).toContain("spawn-subnet"); - }); - - it("should reuse existing public subnet when available", () => { - expect(oracleLib).toContain("oci network subnet list"); - expect(oracleLib).toContain("prohibit-public-ip-on-vnic"); - }); - - it("should compose _setup_vcn_networking from _create_internet_gateway and helpers", () => { - const lines = oracleLib.split("\n"); - let inSetup = false; - let callsIgw = false; - let callsRoute = false; - let callsSecurity = false; - for (const line of lines) { - if (line.match(/^_setup_vcn_networking\(\)/)) inSetup = true; - if (inSetup && line.includes("_create_internet_gateway")) callsIgw = true; - if (inSetup && line.includes("_add_default_route")) callsRoute = true; - if (inSetup && line.includes("_add_ssh_security_rules")) callsSecurity = true; - if (inSetup && line.match(/^}/)) break; - } - expect(callsIgw).toBe(true); - expect(callsRoute).toBe(true); - expect(callsSecurity).toBe(true); - }); - - it("should compose _get_subnet_id from _create_vcn and helpers", () => { - const lines = oracleLib.split("\n"); - let inGetSubnet = false; - let callsCreateVcn = false; - let callsSetupNet = false; - let callsCreateSubnet = false; - for (const line of lines) { - if (line.match(/^_get_subnet_id\(\)/)) inGetSubnet = true; - if (inGetSubnet && line.includes("_create_vcn")) callsCreateVcn = true; - if (inGetSubnet && line.includes("_setup_vcn_networking")) callsSetupNet = true; - if (inGetSubnet && line.includes("_create_subnet")) callsCreateSubnet = true; - if (inGetSubnet && line.match(/^}/)) break; - } - expect(callsCreateVcn).toBe(true); - expect(callsSetupNet).toBe(true); - expect(callsCreateSubnet).toBe(true); - }); - - it("should show helpful error messages when VCN creation fails", () => { - expect(oracleLib).toContain("VCN limit exceeded"); - expect(oracleLib).toContain("IAM policies"); - }); - - it("should show helpful error messages when subnet creation fails", () => { - expect(oracleLib).toContain("Subnet limit exceeded"); - }); - - it("should wait for AVAILABLE state on VCN resources", () => { - expect(oracleLib).toContain("--wait-for-state AVAILABLE"); - }); -}); - -// ============================================================================= -// Instance creation -// ============================================================================= - -describe("Oracle instance creation", () => { - it("should use oci compute instance launch", () => { - expect(oracleLib).toContain("oci compute instance launch"); - }); - - it("should default to VM.Standard.E2.1.Micro shape", () => { - expect(oracleLib).toContain("VM.Standard.E2.1.Micro"); - }); - - it("should allow OCI_SHAPE env var override", () => { - expect(oracleLib).toContain("OCI_SHAPE"); - }); - - it("should handle flex shapes with configurable OCPUs and memory", () => { - expect(oracleLib).toContain(".Flex"); - expect(oracleLib).toContain("OCI_OCPUS"); - expect(oracleLib).toContain("OCI_MEMORY_GB"); - expect(oracleLib).toContain("memoryInGBs"); - }); - - it("should search for Ubuntu 24.04 image", () => { - expect(oracleLib).toContain("Canonical Ubuntu"); - expect(oracleLib).toContain("24.04"); - }); - - it("should fall back to image search without shape filter", () => { - // _get_ubuntu_image_id tries with shape first, then without - const lines = oracleLib.split("\n"); - let inGetImage = false; - let imageListCalls = 0; - for (const line of lines) { - if (line.match(/^_get_ubuntu_image_id\(\)/)) inGetImage = true; - if (inGetImage && line.includes("oci compute image list")) imageListCalls++; - if (inGetImage && line.match(/^}/)) break; - } - expect(imageListCalls).toBeGreaterThanOrEqual(2); - }); - - it("should assign public IP to instance", () => { - expect(oracleLib).toContain("--assign-public-ip true"); - }); - - it("should pass SSH authorized key from ~/.ssh/id_ed25519.pub", () => { - expect(oracleLib).toContain("--ssh-authorized-keys-file"); - expect(oracleLib).toContain("id_ed25519.pub"); - }); - - it("should pass cloud-init userdata via --user-data", () => { - expect(oracleLib).toContain("--user-data"); - }); - - it("should wait for RUNNING state", () => { - expect(oracleLib).toContain("--wait-for-state RUNNING"); - }); - - it("should export OCI_INSTANCE_ID and OCI_SERVER_IP after creation", () => { - expect(oracleLib).toContain("export OCI_INSTANCE_ID"); - expect(oracleLib).toContain("export OCI_SERVER_IP"); - }); - - it("should export OCI_INSTANCE_NAME_ACTUAL after creation", () => { - expect(oracleLib).toContain("export OCI_INSTANCE_NAME_ACTUAL"); - }); - - it("should capture stderr for error reporting on launch failure", () => { - expect(oracleLib).toContain("mktemp"); - expect(oracleLib).toContain("track_temp_file"); - }); - - it("should show helpful error messages on instance launch failure", () => { - expect(oracleLib).toContain("Service limit (quota) exceeded"); - expect(oracleLib).toContain("Shape not available"); - expect(oracleLib).toContain("Out of host capacity"); - }); -}); - -// ============================================================================= -// Instance IP retrieval -// ============================================================================= - -describe("Oracle instance IP retrieval", () => { - it("should get VNIC attachment for instance", () => { - expect(oracleLib).toContain("oci compute vnic-attachment list"); - }); - - it("should get public IP from VNIC", () => { - expect(oracleLib).toContain("oci network vnic get"); - expect(oracleLib).toContain("public-ip"); - }); - - it("should show error when VNIC or IP cannot be found", () => { - expect(oracleLib).toContain("Could not get VNIC for instance"); - expect(oracleLib).toContain("Could not get public IP for instance"); - }); -}); - -// ============================================================================= -// Cloud-init userdata -// ============================================================================= - -describe("Oracle cloud-init userdata", () => { - it("should install essential packages (curl, git, zsh, python3)", () => { - expect(oracleLib).toContain("apt-get install"); - expect(oracleLib).toContain("curl"); - expect(oracleLib).toContain("git"); - expect(oracleLib).toContain("zsh"); - expect(oracleLib).toContain("python3"); - }); - - it("should install Bun", () => { - expect(oracleLib).toContain("bun.sh/install"); - }); - - it("should install Claude Code", () => { - expect(oracleLib).toContain("claude.ai/install.sh"); - }); - - it("should write .cloud-init-complete marker", () => { - expect(oracleLib).toContain(".cloud-init-complete"); - }); - - it("should run installations as ubuntu user", () => { - expect(oracleLib).toContain("su - ubuntu"); - }); - - it("should encode userdata as base64 (macOS and Linux compatible)", () => { - expect(oracleLib).toContain("base64 -w0"); - expect(oracleLib).toContain("base64"); - }); -}); - -// ============================================================================= -// SSH delegation pattern -// ============================================================================= - -describe("Oracle SSH delegation pattern", () => { - it("should set SSH_USER to ubuntu", () => { - expect(oracleLib).toContain('SSH_USER="ubuntu"'); - }); - - it("should delegate verify_server_connectivity to ssh_verify_connectivity", () => { - expect(oracleLib).toContain("ssh_verify_connectivity"); - }); - - it("should delegate run_server to ssh_run_server", () => { - expect(oracleLib).toContain("ssh_run_server"); - }); - - it("should delegate upload_file to ssh_upload_file", () => { - expect(oracleLib).toContain("ssh_upload_file"); - }); - - it("should delegate interactive_session to ssh_interactive_session", () => { - expect(oracleLib).toContain("ssh_interactive_session"); - }); - - it("should use generic_ssh_wait in wait_for_cloud_init", () => { - expect(oracleLib).toContain("generic_ssh_wait"); - }); - - it("should check for .cloud-init-complete marker in wait_for_cloud_init", () => { - const lines = oracleLib.split("\n"); - let inWait = false; - let checksMarker = false; - for (const line of lines) { - if (line.match(/^wait_for_cloud_init\(\)/)) inWait = true; - if (inWait && line.includes(".cloud-init-complete")) checksMarker = true; - if (inWait && line.match(/^}/)) break; - } - expect(checksMarker).toBe(true); - }); -}); - -// ============================================================================= -// Server destruction -// ============================================================================= - -describe("Oracle server destruction", () => { - it("should use oci compute instance terminate", () => { - expect(oracleLib).toContain("oci compute instance terminate"); - }); - - it("should not preserve boot volume on termination", () => { - expect(oracleLib).toContain("--preserve-boot-volume false"); - }); - - it("should accept instance_id parameter or fall back to OCI_INSTANCE_ID", () => { - const lines = oracleLib.split("\n"); - let inDestroy = false; - let usesParam = false; - let usesEnvFallback = false; - for (const line of lines) { - if (line.match(/^destroy_server\(\)/)) inDestroy = true; - if (inDestroy && line.includes("${1:-")) usesParam = true; - if (inDestroy && line.includes("OCI_INSTANCE_ID")) usesEnvFallback = true; - if (inDestroy && line.match(/^}/)) break; - } - expect(usesParam).toBe(true); - expect(usesEnvFallback).toBe(true); - }); -}); - -// ============================================================================= -// list_servers -// ============================================================================= - -describe("Oracle list_servers", () => { - it("should use oci compute instance list", () => { - expect(oracleLib).toContain("oci compute instance list"); - }); - - it("should filter out TERMINATED instances", () => { - expect(oracleLib).toContain("TERMINATED"); - }); - - it("should display as table", () => { - expect(oracleLib).toContain("--output table"); - }); - - it("should show instance name, state, shape, and creation time", () => { - expect(oracleLib).toContain("display-name"); - expect(oracleLib).toContain("lifecycle-state"); - expect(oracleLib).toContain("shape"); - expect(oracleLib).toContain("time-created"); - }); -}); - -// ============================================================================= -// Availability domain -// ============================================================================= - -describe("Oracle availability domain handling", () => { - it("should use oci iam availability-domain list", () => { - expect(oracleLib).toContain("oci iam availability-domain list"); - }); - - it("should use first available domain by default", () => { - expect(oracleLib).toContain("data[0].name"); - }); - - it("should show error when no availability domains found", () => { - expect(oracleLib).toContain("Could not list availability domains"); - }); -}); - -// ============================================================================= -// Security conventions -// ============================================================================= - -describe("Oracle security conventions", () => { - it("should NOT contain echo -e (macOS compatibility)", () => { - const codeLines = getCodeLines(oracleLib); - const hasEchoE = codeLines.some((l) => /\becho\s+-e\b/.test(l)); - expect(hasEchoE).toBe(false); - }); - - it("should NOT use set -u (nounset)", () => { - const codeLines = getCodeLines(oracleLib); - const hasSetU = codeLines.some( - (l) => /\bset\s+.*-[a-z]*u/.test(l) || /\bset\s+-o\s+nounset\b/.test(l) - ); - expect(hasSetU).toBe(false); - }); - - it("should use ${VAR:-} pattern for optional env var checks", () => { - // Key optional env vars should use :- pattern - expect(oracleLib).toContain("OCI_COMPARTMENT_ID:-"); - expect(oracleLib).toContain("OCI_SUBNET_ID:-"); - }); - - it("should use get_resource_name for server name input (sanitization)", () => { - expect(oracleLib).toContain("get_resource_name"); - }); - - it("should use generate_ssh_key_if_missing for SSH key creation", () => { - expect(oracleLib).toContain("generate_ssh_key_if_missing"); - }); - - it("should define configurable INSTANCE_STATUS_POLL_DELAY", () => { - expect(oracleLib).toContain("INSTANCE_STATUS_POLL_DELAY"); - }); -}); - -// ============================================================================= -// create_server decomposition -// ============================================================================= - -describe("Oracle create_server decomposition", () => { - it("should delegate image lookup to _get_ubuntu_image_id", () => { - const lines = oracleLib.split("\n"); - let inCreate = false; - let delegatesImage = false; - for (const line of lines) { - if (line.match(/^create_server\(\)/)) inCreate = true; - if (inCreate && line.includes("_get_ubuntu_image_id")) delegatesImage = true; - if (inCreate && line.match(/^}/)) break; - } - expect(delegatesImage).toBe(true); - }); - - it("should delegate AD lookup to _get_availability_domain", () => { - const lines = oracleLib.split("\n"); - let inCreate = false; - let delegatesAD = false; - for (const line of lines) { - if (line.match(/^create_server\(\)/)) inCreate = true; - if (inCreate && line.includes("_get_availability_domain")) delegatesAD = true; - if (inCreate && line.match(/^}/)) break; - } - expect(delegatesAD).toBe(true); - }); - - it("should delegate subnet lookup to _get_subnet_id", () => { - const lines = oracleLib.split("\n"); - let inCreate = false; - let delegatesSubnet = false; - for (const line of lines) { - if (line.match(/^create_server\(\)/)) inCreate = true; - if (inCreate && line.includes("_get_subnet_id")) delegatesSubnet = true; - if (inCreate && line.match(/^}/)) break; - } - expect(delegatesSubnet).toBe(true); - }); - - it("should delegate userdata encoding to _encode_userdata_b64", () => { - const lines = oracleLib.split("\n"); - let inCreate = false; - let delegatesUserdata = false; - for (const line of lines) { - if (line.match(/^create_server\(\)/)) inCreate = true; - if (inCreate && line.includes("_encode_userdata_b64")) delegatesUserdata = true; - if (inCreate && line.match(/^}/)) break; - } - expect(delegatesUserdata).toBe(true); - }); - - it("should delegate instance launch to _launch_oci_instance", () => { - const lines = oracleLib.split("\n"); - let inCreate = false; - let delegatesLaunch = false; - for (const line of lines) { - if (line.match(/^create_server\(\)/)) inCreate = true; - if (inCreate && line.includes("_launch_oci_instance")) delegatesLaunch = true; - if (inCreate && line.match(/^}/)) break; - } - expect(delegatesLaunch).toBe(true); - }); - - it("should delegate IP retrieval to _get_instance_public_ip", () => { - const lines = oracleLib.split("\n"); - let inCreate = false; - let delegatesIP = false; - for (const line of lines) { - if (line.match(/^create_server\(\)/)) inCreate = true; - if (inCreate && line.includes("_get_instance_public_ip")) delegatesIP = true; - if (inCreate && line.match(/^}/)) break; - } - expect(delegatesIP).toBe(true); - }); -}); - -// ============================================================================= -// Agent script patterns -// ============================================================================= - -describe("Oracle agent script patterns", () => { - it("should have at least 10 implemented agent scripts", () => { - expect(oracleEntries.length).toBeGreaterThanOrEqual(10); - }); - - for (const { key, agent, path } of oracleEntries) { - const content = readScript(path); - const codeLines = getCodeLines(content); - - describe(`${key}.sh`, () => { - it("should source oracle/lib/common.sh with fallback", () => { - expect(content).toContain("oracle/lib/common.sh"); - expect(content).toContain("raw.githubusercontent.com"); - }); - - it("should use set -eo pipefail", () => { - expect(content).toContain("set -eo pipefail"); - }); - - it("should call ensure_oci_cli", () => { - expect(codeLines.some((l) => l.includes("ensure_oci_cli"))).toBe(true); - }); - - it("should call ensure_ssh_key", () => { - expect(codeLines.some((l) => l.includes("ensure_ssh_key"))).toBe(true); - }); - - it("should call get_server_name and create_server", () => { - expect(codeLines.some((l) => l.includes("get_server_name"))).toBe(true); - expect(codeLines.some((l) => l.includes("create_server"))).toBe(true); - }); - - it("should call verify_server_connectivity with OCI_SERVER_IP", () => { - expect(codeLines.some((l) => l.includes("verify_server_connectivity"))).toBe(true); - expect(codeLines.some((l) => l.includes("OCI_SERVER_IP"))).toBe(true); - }); - - it("should call wait_for_cloud_init with OCI_SERVER_IP", () => { - expect(codeLines.some((l) => l.includes("wait_for_cloud_init"))).toBe(true); - const waitLines = codeLines.filter((l) => l.includes("wait_for_cloud_init")); - expect(waitLines.some((l) => l.includes("OCI_SERVER_IP"))).toBe(true); - }); - - it("should reference OPENROUTER_API_KEY", () => { - expect(codeLines.some((l) => l.includes("OPENROUTER_API_KEY"))).toBe(true); - }); - - it("should handle OPENROUTER_API_KEY from env or OAuth", () => { - expect(content).toContain("OPENROUTER_API_KEY:-"); - expect(content).toContain("get_openrouter_api_key_oauth"); - }); - - it("should use inject_env_vars_ssh for env var injection (SSH-based)", () => { - expect(codeLines.some((l) => l.includes("inject_env_vars_ssh"))).toBe(true); - }); - - it("should NOT use inject_env_vars_local (Oracle is SSH-based)", () => { - expect(codeLines.some((l) => l.includes("inject_env_vars_local"))).toBe(false); - }); - - it("should pass OCI_SERVER_IP to inject_env_vars_ssh", () => { - const injectLines = codeLines.filter((l) => l.includes("inject_env_vars_ssh")); - expect(injectLines.some((l) => l.includes("OCI_SERVER_IP"))).toBe(true); - }); - - it("should call interactive_session with OCI_SERVER_IP", () => { - expect(codeLines.some((l) => l.includes("interactive_session"))).toBe(true); - const sessionLines = codeLines.filter((l) => l.includes("interactive_session")); - expect(sessionLines.some((l) => l.includes("OCI_SERVER_IP"))).toBe(true); - }); - - it("should pass IP to run_server calls", () => { - const runServerLines = codeLines.filter((l) => l.includes("run_server")); - for (const line of runServerLines) { - expect(line).toContain("OCI_SERVER_IP"); - } - }); - - it("should NOT contain echo -e (macOS compat)", () => { - const hasEchoE = codeLines.some((l) => /\becho\s+-e\b/.test(l)); - expect(hasEchoE).toBe(false); - }); - - it("should NOT use set -u", () => { - const hasSetU = codeLines.some( - (l) => /\bset\s+.*-[a-z]*u/.test(l) || /\bset\s+-o\s+nounset\b/.test(l) - ); - expect(hasSetU).toBe(false); - }); - }); - } -}); - -// ============================================================================= -// Agent-specific behavior -// ============================================================================= - -describe("Oracle claude.sh agent-specific patterns", () => { - const claudePath = join(REPO_ROOT, "oracle", "claude.sh"); - const claudeExists = existsSync(claudePath); - const claudeContent = claudeExists ? readScript(claudePath) : ""; - - it("should exist", () => { - expect(claudeExists).toBe(true); - }); - - it("should install Claude Code if not present", () => { - expect(claudeContent).toContain("install_claude_code"); - }); - - it("should set ANTHROPIC_BASE_URL for OpenRouter", () => { - expect(claudeContent).toContain("ANTHROPIC_BASE_URL=https://openrouter.ai/api"); - }); - - it("should set CLAUDE_CODE_SKIP_ONBOARDING=1", () => { - expect(claudeContent).toContain("CLAUDE_CODE_SKIP_ONBOARDING=1"); - }); - - it("should set CLAUDE_CODE_ENABLE_TELEMETRY=0", () => { - expect(claudeContent).toContain("CLAUDE_CODE_ENABLE_TELEMETRY=0"); - }); - - it("should call setup_claude_code_config", () => { - expect(claudeContent).toContain("setup_claude_code_config"); - }); - - it("should launch claude in interactive session", () => { - const codeLines = getCodeLines(claudeContent); - const sessionLines = codeLines.filter((l) => l.includes("interactive_session")); - expect(sessionLines.some((l) => l.includes("claude"))).toBe(true); - }); -}); - -describe("Oracle aider.sh agent-specific patterns", () => { - const aiderPath = join(REPO_ROOT, "oracle", "aider.sh"); - const aiderExists = existsSync(aiderPath); - const aiderContent = aiderExists ? readScript(aiderPath) : ""; - - it("should exist", () => { - expect(aiderExists).toBe(true); - }); - - it("should install aider via pip", () => { - expect(aiderContent).toContain("pip install aider-chat"); - }); - - it("should call get_model_id_interactive for model selection", () => { - expect(aiderContent).toContain("get_model_id_interactive"); - }); - - it("should launch aider with openrouter model prefix", () => { - expect(aiderContent).toContain("openrouter/"); - }); -}); - -describe("Oracle cline.sh agent-specific patterns", () => { - const clinePath = join(REPO_ROOT, "oracle", "cline.sh"); - const clineExists = existsSync(clinePath); - const clineContent = clineExists ? readScript(clinePath) : ""; - - it("should exist", () => { - expect(clineExists).toBe(true); - }); - - it("should install cline via npm", () => { - expect(clineContent).toContain("npm install -g cline"); - }); - - it("should set OPENAI_API_KEY and OPENAI_BASE_URL for OpenRouter", () => { - expect(clineContent).toContain("OPENAI_API_KEY="); - expect(clineContent).toContain("OPENAI_BASE_URL=https://openrouter.ai/api/v1"); - }); -}); - -// ============================================================================= -// Manifest consistency -// ============================================================================= - -describe("Manifest consistency for Oracle", () => { - it("oracle should be in manifest.clouds", () => { - expect(manifest.clouds["oracle"]).toBeDefined(); - }); - - it("oracle should have type 'cli'", () => { - expect(manifest.clouds["oracle"]?.type).toBe("cli"); - }); - - it("oracle should use SSH exec method", () => { - expect(manifest.clouds["oracle"]?.exec_method).toContain("ssh"); - }); - - it("oracle should use SSH interactive method", () => { - expect(manifest.clouds["oracle"]?.interactive_method).toContain("ssh"); - }); - - it("oracle should have defaults for shape and image", () => { - const cloud = manifest.clouds["oracle"]; - expect(cloud?.defaults).toBeDefined(); - if (cloud?.defaults) { - expect(cloud.defaults.shape).toBe("VM.Standard.E2.1.Micro"); - expect(cloud.defaults.image).toBe("Ubuntu 24.04"); - } - }); - - it("oracle matrix entries should all be 'implemented' or 'missing'", () => { - const entries = Object.entries(manifest.matrix).filter(([key]) => - key.startsWith("oracle/") - ); - expect(entries.length).toBeGreaterThan(0); - for (const [, status] of entries) { - expect(["implemented", "missing"]).toContain(status); - } - }); - - it("every oracle/implemented entry should have a .sh file on disk", () => { - const impl = Object.entries(manifest.matrix).filter( - ([key, status]) => key.startsWith("oracle/") && status === "implemented" - ); - for (const [key] of impl) { - const scriptPath = join(REPO_ROOT, key + ".sh"); - expect(existsSync(scriptPath)).toBe(true); - } - }); - - it("should have at least 10 implemented matrix entries", () => { - const impl = Object.entries(manifest.matrix).filter( - ([key, status]) => key.startsWith("oracle/") && status === "implemented" - ); - expect(impl.length).toBeGreaterThanOrEqual(10); - }); -}); diff --git a/cli/src/commands.ts b/cli/src/commands.ts index 86a21408..9ae6c910 100644 --- a/cli/src/commands.ts +++ b/cli/src/commands.ts @@ -334,7 +334,7 @@ function validateImplementation(manifest: Manifest, cloud: string, agent: string const CLOUD_CLI_MAP: Record = { gcp: "gcloud", aws: "aws", - oracle: "oci", + fly: "flyctl", sprite: "sprite", hetzner: "hcloud", @@ -349,17 +349,21 @@ export function hasCloudCli(cloud: string): boolean { } /** Sort clouds by credential/CLI availability and build hint overrides for the picker. - * Three tiers: credentials set > CLI installed > neither. */ + * Four tiers: credentials set > featured cloud > CLI installed > neither. */ export function prioritizeCloudsByCredentials( clouds: string[], - manifest: Manifest + manifest: Manifest, + featuredCloud?: string ): { sortedClouds: string[]; hintOverrides: Record; credCount: number; cliCount: number } { const withCreds: string[] = []; + const featured: string[] = []; const withCli: string[] = []; const rest: string[] = []; for (const c of clouds) { if (hasCloudCredentials(manifest.clouds[c].auth)) { withCreds.push(c); + } else if (featuredCloud && c === featuredCloud) { + featured.push(c); } else if (hasCloudCli(c)) { withCli.push(c); } else { @@ -371,11 +375,14 @@ export function prioritizeCloudsByCredentials( for (const c of withCreds) { hintOverrides[c] = `credentials detected -- ${manifest.clouds[c].description}`; } + for (const c of featured) { + hintOverrides[c] = `recommended -- ${manifest.clouds[c].description}`; + } for (const c of withCli) { hintOverrides[c] = `CLI installed -- ${manifest.clouds[c].description}`; } - return { sortedClouds: [...withCreds, ...withCli, ...rest], hintOverrides, credCount: withCreds.length, cliCount: withCli.length }; + return { sortedClouds: [...withCreds, ...featured, ...withCli, ...rest], hintOverrides, credCount: withCreds.length, cliCount: withCli.length }; } /** Build hint overrides for the agent picker showing cloud count and credential readiness */ @@ -425,7 +432,8 @@ function getAndValidateCloudChoices( process.exit(1); } - const { sortedClouds, hintOverrides, credCount, cliCount } = prioritizeCloudsByCredentials(clouds, manifest); + const featuredCloud = manifest.agents[agent]?.featured_cloud; + const { sortedClouds, hintOverrides, credCount, cliCount } = prioritizeCloudsByCredentials(clouds, manifest, featuredCloud); if (credCount > 0) { p.log.info(`${credCount} cloud${credCount > 1 ? "s" : ""} with credentials detected (shown first)`); } @@ -1784,8 +1792,7 @@ function buildDeleteScript(cloud: string, connection: VMConnection): string { } case "aws": return `${sourceLib}\nensure_aws_cli\ndestroy_server "${id}"`; - case "oracle": - return `${sourceLib}\nensure_oci_cli\ndestroy_server "${id}"`; + case "ovh": return `${sourceLib}\nensure_ovh_authenticated\ndestroy_ovh_instance "${id}"`; case "daytona": diff --git a/cli/src/manifest.ts b/cli/src/manifest.ts index 35b72a43..bbaf99b6 100644 --- a/cli/src/manifest.ts +++ b/cli/src/manifest.ts @@ -17,6 +17,7 @@ export interface AgentDef { interactive_prompts?: Record; dotenv?: { path: string; values: Record }; notes?: string; + featured_cloud?: string; } export interface CloudDef { diff --git a/manifest.json b/manifest.json index d621be89..d1613e6d 100644 --- a/manifest.json +++ b/manifest.json @@ -26,7 +26,8 @@ "hasCompletedOnboarding": true, "bypassPermissionsModeAccepted": true } - } + }, + "featured_cloud": "sprite" }, "openclaw": { "name": "OpenClaw", @@ -45,7 +46,8 @@ "prompt": "Enter model ID", "default": "openrouter/auto" } - } + }, + "featured_cloud": "fly" }, "nanoclaw": { "name": "NanoClaw", @@ -67,7 +69,8 @@ "ANTHROPIC_API_KEY": "${OPENROUTER_API_KEY}" } }, - "notes": "Requires WhatsApp QR code scan for authentication" + "notes": "Requires WhatsApp QR code scan for authentication", + "featured_cloud": "fly" }, "aider": { "name": "Aider", @@ -84,7 +87,8 @@ "default": "openrouter/auto" } }, - "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY and --model openrouter/... flag" + "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY and --model openrouter/... flag", + "featured_cloud": "gcp" }, "goose": { "name": "Goose", @@ -96,7 +100,8 @@ "GOOSE_PROVIDER": "openrouter", "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, - "notes": "Model-agnostic agent by Block (formerly Square), supports OpenRouter as a provider" + "notes": "Model-agnostic agent by Block (formerly Square), supports OpenRouter as a provider", + "featured_cloud": "fly" }, "codex": { "name": "Codex CLI", @@ -109,7 +114,8 @@ "OPENAI_BASE_URL": "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, - "notes": "Works with OpenRouter via OPENAI_BASE_URL override pointing to openrouter.ai/api/v1" + "notes": "Works with OpenRouter via OPENAI_BASE_URL override pointing to openrouter.ai/api/v1", + "featured_cloud": "fly" }, "interpreter": { "name": "Open Interpreter", @@ -122,7 +128,8 @@ "OPENAI_BASE_URL": "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, - "notes": "Works with OpenRouter via OPENAI_BASE_URL override" + "notes": "Works with OpenRouter via OPENAI_BASE_URL override", + "featured_cloud": "gcp" }, "gemini": { "name": "Gemini CLI", @@ -136,7 +143,8 @@ "OPENAI_BASE_URL": "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, - "notes": "Works with OpenRouter via OPENAI_BASE_URL override and GEMINI_API_KEY" + "notes": "Works with OpenRouter via OPENAI_BASE_URL override and GEMINI_API_KEY", + "featured_cloud": "gcp" }, "amazonq": { "name": "Amazon Q CLI", @@ -149,7 +157,8 @@ "OPENAI_BASE_URL": "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, - "notes": "Works with OpenRouter via OPENAI_BASE_URL override" + "notes": "Works with OpenRouter via OPENAI_BASE_URL override", + "featured_cloud": "fly" }, "cline": { "name": "Cline", @@ -162,7 +171,8 @@ "OPENAI_BASE_URL": "https://openrouter.ai/api/v1", "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, - "notes": "Works with OpenRouter via OPENAI_BASE_URL override" + "notes": "Works with OpenRouter via OPENAI_BASE_URL override", + "featured_cloud": "fly" }, "gptme": { "name": "gptme", @@ -179,7 +189,8 @@ "default": "openrouter/auto" } }, - "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY and -m openrouter/... flag" + "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY and -m openrouter/... flag", + "featured_cloud": "daytona" }, "opencode": { "name": "OpenCode", @@ -190,7 +201,8 @@ "env": { "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" }, - "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY env var. Go-based TUI using Bubble Tea." + "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY env var. Go-based TUI using Bubble Tea.", + "featured_cloud": "daytona" }, "plandex": { "name": "Plandex", @@ -201,7 +213,8 @@ "env": { "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." + "notes": "Natively supports OpenRouter via OPENROUTER_API_KEY env var. Go-based CLI with sandbox and version control for AI changes.", + "featured_cloud": "daytona" }, "kilocode": { "name": "Kilo Code", @@ -214,7 +227,8 @@ "KILO_OPEN_ROUTER_API_KEY": "${OPENROUTER_API_KEY}", "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'." + "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" }, "continue": { "name": "Continue", @@ -238,7 +252,8 @@ ] } }, - "notes": "Natively supports OpenRouter via config.json. CLI supports TUI mode (interactive) and headless mode (-p flag). 31K+ GitHub stars." + "notes": "Natively supports OpenRouter via config.json. CLI supports TUI mode (interactive) and headless mode (-p flag). 31K+ GitHub stars.", + "featured_cloud": "fly" } }, "clouds": { @@ -253,21 +268,6 @@ "interactive_method": "exec", "notes": "No cloud provisioning needed. Installs agents and injects OpenRouter credentials locally. Useful for local development and testing." }, - "oracle": { - "name": "Oracle Cloud Infrastructure", - "description": "Oracle Cloud compute instances via OCI CLI", - "url": "https://cloud.oracle.com/", - "type": "cli", - "auth": "oci setup config (OCI CLI config with API keys)", - "provision_method": "oci compute instance launch with --user-data", - "exec_method": "ssh ubuntu@IP", - "interactive_method": "ssh -t ubuntu@IP", - "defaults": { - "shape": "VM.Standard.E2.1.Micro", - "image": "Ubuntu 24.04" - }, - "notes": "Has a generous Always Free tier (VM.Standard.E2.1.Micro, VM.Standard.A1.Flex). Uses 'ubuntu' user for SSH. Requires OCI CLI installed (pip install oci-cli) and configured. Set OCI_COMPARTMENT_ID for the target compartment." - }, "hetzner": { "name": "Hetzner Cloud", "description": "Hetzner Cloud servers via REST API", @@ -503,20 +503,6 @@ "fly/kilocode": "implemented", "daytona/kilocode": "implemented", "ovh/kilocode": "implemented", - "oracle/claude": "implemented", - "oracle/aider": "implemented", - "oracle/goose": "implemented", - "oracle/openclaw": "implemented", - "oracle/nanoclaw": "implemented", - "oracle/codex": "implemented", - "oracle/interpreter": "implemented", - "oracle/gemini": "implemented", - "oracle/amazonq": "implemented", - "oracle/cline": "implemented", - "oracle/gptme": "implemented", - "oracle/opencode": "implemented", - "oracle/plandex": "implemented", - "oracle/kilocode": "implemented", "sprite/continue": "implemented", "hetzner/continue": "implemented", "digitalocean/continue": "implemented", @@ -525,7 +511,6 @@ "fly/continue": "implemented", "daytona/continue": "implemented", "ovh/continue": "implemented", - "oracle/continue": "implemented", "local/claude": "implemented", "local/openclaw": "implemented", "local/nanoclaw": "implemented", diff --git a/oracle/README.md b/oracle/README.md deleted file mode 100644 index a6ed10cb..00000000 --- a/oracle/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Oracle Cloud Infrastructure - -Oracle Cloud compute instances via OCI CLI. [Oracle Cloud](https://cloud.oracle.com/) - -> Has a generous Always Free tier. Uses 'ubuntu' user for SSH. Requires OCI CLI installed and configured. - -## Prerequisites - -1. Install OCI CLI: `pip install oci-cli` -2. Configure: `oci setup config` -3. Set compartment: `export OCI_COMPARTMENT_ID=ocid1.compartment.oc1.....` - -## Agents - -#### Claude Code - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/oracle/claude.sh) -``` - -#### Aider - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/oracle/aider.sh) -``` - -#### Goose - -```bash -bash <(curl -fsSL https://openrouter.ai/labs/spawn/oracle/goose.sh) -``` - -## Non-Interactive Mode - -```bash -OCI_COMPARTMENT_ID=ocid1.compartment.oc1..... \ -OCI_INSTANCE_NAME=dev-mk1 \ -OPENROUTER_API_KEY=sk-or-v1-xxxxx \ - bash <(curl -fsSL https://openrouter.ai/labs/spawn/oracle/claude.sh) -``` - -## Environment Variables - -| Variable | Description | Default | -|---|---|---| -| `OCI_COMPARTMENT_ID` | OCI compartment OCID | Auto-detected | -| `OCI_INSTANCE_NAME` | Instance display name | Prompted | -| `OCI_SHAPE` | Compute shape | `VM.Standard.E2.1.Micro` | -| `OCI_SUBNET_ID` | Subnet OCID | Auto-created | -| `OCI_OCPUS` | OCPUs for flex shapes | `1` | -| `OCI_MEMORY_GB` | Memory (GB) for flex shapes | `4` | -| `OPENROUTER_API_KEY` | OpenRouter API key | OAuth or prompted | diff --git a/oracle/aider.sh b/oracle/aider.sh deleted file mode 100755 index f723f951..00000000 --- a/oracle/aider.sh +++ /dev/null @@ -1,26 +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=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Aider on Oracle Cloud Infrastructure" -echo "" - -AGENT_MODEL_PROMPT=1 -AGENT_MODEL_DEFAULT="openrouter/auto" - -agent_install() { - install_agent "Aider" "python3 -m pip install pipx && pipx install aider-chat" cloud_run - verify_agent "Aider" "command -v aider && aider --version" "pipx install aider-chat" cloud_run -} -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"; } -agent_launch_cmd() { printf 'source ~/.zshrc && aider --model openrouter/%s' "${MODEL_ID}"; } - -spawn_agent "Aider" diff --git a/oracle/amazonq.sh b/oracle/amazonq.sh deleted file mode 100755 index d3a1600c..00000000 --- a/oracle/amazonq.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Amazon Q on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "Amazon Q CLI" "curl -fsSL https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q-cli-install.sh | bash" 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 && q chat'; } - -spawn_agent "Amazon Q" diff --git a/oracle/claude.sh b/oracle/claude.sh deleted file mode 100755 index d5daa3ec..00000000 --- a/oracle/claude.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Claude Code on Oracle Cloud Infrastructure" -echo "" - -agent_pre_provision() { prompt_github_auth; } -agent_install() { install_claude_code cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ - "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ - "ANTHROPIC_API_KEY=" \ - "CLAUDE_CODE_SKIP_ONBOARDING=1" \ - "CLAUDE_CODE_ENABLE_TELEMETRY=0" -} -agent_configure() { setup_claude_code_config "${OPENROUTER_API_KEY}" cloud_upload cloud_run; } -agent_launch_cmd() { echo 'source ~/.spawnrc 2>/dev/null; export PATH=$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH; claude'; } - -spawn_agent "Claude Code" diff --git a/oracle/cline.sh b/oracle/cline.sh deleted file mode 100755 index fad61e6b..00000000 --- a/oracle/cline.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Cline on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "Cline" "npm install -g cline" 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 && cline'; } - -spawn_agent "Cline" diff --git a/oracle/codex.sh b/oracle/codex.sh deleted file mode 100755 index 7eff7cdb..00000000 --- a/oracle/codex.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Codex CLI on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "Codex CLI" "npm install -g @openai/codex" cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" -} -agent_launch_cmd() { echo 'source ~/.zshrc && codex'; } - -spawn_agent "Codex CLI" diff --git a/oracle/continue.sh b/oracle/continue.sh deleted file mode 100755 index fa09db14..00000000 --- a/oracle/continue.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Continue on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "Continue CLI" "npm install -g @continuedev/cli" cloud_run; } -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"; } -agent_configure() { setup_continue_config "${OPENROUTER_API_KEY}" cloud_upload cloud_run; } -agent_launch_cmd() { echo 'source ~/.zshrc && cn'; } - -spawn_agent "Continue" diff --git a/oracle/gemini.sh b/oracle/gemini.sh deleted file mode 100755 index 3453ff9b..00000000 --- a/oracle/gemini.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Gemini CLI on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "Gemini CLI" "npm install -g @google/gemini-cli" cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "GEMINI_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 && gemini'; } - -spawn_agent "Gemini CLI" diff --git a/oracle/goose.sh b/oracle/goose.sh deleted file mode 100755 index 8e934887..00000000 --- a/oracle/goose.sh +++ /dev/null @@ -1,27 +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=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Goose on Oracle Cloud Infrastructure" -echo "" - -agent_install() { - install_agent "Goose" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash" cloud_run - verify_agent "Goose" "command -v goose && goose --version" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash" cloud_run -} -agent_env_vars() { - generate_env_config \ - "GOOSE_PROVIDER=openrouter" \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" -} -agent_launch_cmd() { echo 'source ~/.zshrc && goose'; } - -spawn_agent "Goose" diff --git a/oracle/gptme.sh b/oracle/gptme.sh deleted file mode 100755 index 6ad947b6..00000000 --- a/oracle/gptme.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then - source "$SCRIPT_DIR/lib/common.sh" -else - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/oracle/lib/common.sh)" -fi - -log_info "gptme on Oracle Cloud Infrastructure" -echo "" - -AGENT_MODEL_PROMPT=1 -AGENT_MODEL_DEFAULT="openrouter/auto" - -agent_install() { - install_agent "gptme" "pip install gptme 2>/dev/null || pip3 install gptme" cloud_run - verify_agent "gptme" "command -v gptme && gptme --version" "pip install gptme" cloud_run -} -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=$OPENROUTER_API_KEY"; } -agent_launch_cmd() { printf 'source ~/.zshrc && gptme -m openrouter/%s' "${MODEL_ID}"; } - -spawn_agent "gptme" diff --git a/oracle/interpreter.sh b/oracle/interpreter.sh deleted file mode 100755 index 6888d6aa..00000000 --- a/oracle/interpreter.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Open Interpreter on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "Open Interpreter" "pip install open-interpreter 2>/dev/null || pip3 install open-interpreter" 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 && interpreter'; } - -spawn_agent "Open Interpreter" diff --git a/oracle/kilocode.sh b/oracle/kilocode.sh deleted file mode 100755 index b684c6e7..00000000 --- a/oracle/kilocode.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2154 -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Kilo Code on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "Kilo Code" "npm install -g @kilocode/cli" cloud_run; } -agent_env_vars() { - generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "KILO_PROVIDER_TYPE=openrouter" \ - "KILO_OPEN_ROUTER_API_KEY=${OPENROUTER_API_KEY}" -} -agent_launch_cmd() { echo 'source ~/.zshrc && kilocode'; } - -spawn_agent "Kilo Code" diff --git a/oracle/lib/common.sh b/oracle/lib/common.sh deleted file mode 100644 index a9f14f63..00000000 --- a/oracle/lib/common.sh +++ /dev/null @@ -1,481 +0,0 @@ -#!/bin/bash -# Common bash functions for Oracle Cloud Infrastructure (OCI) spawn scripts -# Uses OCI CLI (oci) — requires oci-cli installed and configured - -# Bash safety flags -set -eo pipefail - -# ============================================================ -# 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 - -# ============================================================ -# OCI specific functions -# ============================================================ - -SPAWN_DASHBOARD_URL="https://cloud.oracle.com/compute/instances" -# SSH_OPTS is defined in shared/common.sh - -# Configurable timeout/delay constants -INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-5} - -ensure_oci_cli() { - if ! command -v oci &>/dev/null; then - log_error "OCI CLI is required but not installed." - log_error "" - log_error "Install with: pip install oci-cli" - log_error "Or: bash -c \"\$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)\"" - log_error "" - log_error "After installing, run: oci setup config" - return 1 - fi - - # Verify config exists - if [[ ! -f "${HOME}/.oci/config" ]]; then - log_error "OCI CLI not configured. Run: oci setup config" - log_error "" - log_error "You will need:" - log_error " - Tenancy OCID (from OCI Console > Administration > Tenancy Details)" - log_error " - User OCID (from OCI Console > Profile > User Settings)" - log_error " - Compartment OCID (from OCI Console > Identity > Compartments)" - log_error " - Region (e.g., us-ashburn-1)" - log_error " - API signing key pair (generated during setup)" - return 1 - fi - - # Get compartment ID - local compartment="${OCI_COMPARTMENT_ID:-}" - if [[ -z "${compartment}" ]]; then - # Try to get tenancy OCID as default compartment (root compartment) - compartment=$(oci iam compartment list --compartment-id-in-subtree true --all \ - --query 'data[0]."compartment-id"' --raw-output 2>/dev/null || true) - if [[ -z "${compartment}" ]]; then - log_error "OCI_COMPARTMENT_ID not set and could not detect compartment." - log_error "" - log_error "Set it with: export OCI_COMPARTMENT_ID=ocid1.compartment.oc1....." - log_error "Find it in: OCI Console > Identity > Compartments" - return 1 - fi - fi - export OCI_COMPARTMENT_ID="${compartment}" - log_info "Using OCI compartment: ${compartment}" -} - -ensure_ssh_key() { - local key_path="${HOME}/.ssh/id_ed25519" - - # Generate key if needed - generate_ssh_key_if_missing "${key_path}" - - # OCI handles SSH keys via instance metadata during create - log_info "SSH key ready" -} - -get_server_name() { - get_resource_name "OCI_INSTANCE_NAME" "Enter OCI instance name: " -} - -get_cloud_init_userdata() { - cat << 'CLOUD_INIT_EOF' -#!/bin/bash -apt-get update -y -apt-get install -y curl unzip git zsh python3 -# Install Bun -su - ubuntu -c 'curl -fsSL https://bun.sh/install | bash' || true -# Install Claude Code -su - ubuntu -c 'curl -fsSL https://claude.ai/install.sh | bash' || true -# Configure PATH -echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.bashrc -echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.zshrc -touch /home/ubuntu/.cloud-init-complete -chown ubuntu:ubuntu /home/ubuntu/.cloud-init-complete -CLOUD_INIT_EOF -} - -_get_ubuntu_image_id() { - local compartment="${OCI_COMPARTMENT_ID}" - local shape="${1:-VM.Standard.E2.1.Micro}" - - # Determine OS for the shape - ARM shapes need aarch64 - local os_match="Canonical Ubuntu" - local os_version="24.04" - - local image_id - image_id=$(oci compute image list \ - --compartment-id "${compartment}" \ - --operating-system "${os_match}" \ - --operating-system-version "${os_version}" \ - --shape "${shape}" \ - --sort-by TIMECREATED \ - --sort-order DESC \ - --limit 1 \ - --query 'data[0].id' \ - --raw-output 2>/dev/null || true) - - if [[ -z "${image_id}" || "${image_id}" == "null" ]]; then - # Fallback: try without shape filter - image_id=$(oci compute image list \ - --compartment-id "${compartment}" \ - --operating-system "${os_match}" \ - --operating-system-version "${os_version}" \ - --sort-by TIMECREATED \ - --sort-order DESC \ - --limit 1 \ - --query 'data[0].id' \ - --raw-output 2>/dev/null || true) - fi - - if [[ -z "${image_id}" || "${image_id}" == "null" ]]; then - log_error "Could not find Ubuntu 24.04 image for shape ${shape}" - log_error "Check available images: oci compute image list --compartment-id ${compartment} --all" - return 1 - fi - - echo "${image_id}" -} - -_get_availability_domain() { - local compartment="${OCI_COMPARTMENT_ID}" - - local ad - ad=$(oci iam availability-domain list \ - --compartment-id "${compartment}" \ - --query 'data[0].name' \ - --raw-output 2>/dev/null || true) - - if [[ -z "${ad}" || "${ad}" == "null" ]]; then - log_error "Could not list availability domains" - return 1 - fi - - echo "${ad}" -} - -_create_vcn() { - local compartment="${1}" - - local vcn_id - vcn_id=$(oci network vcn create \ - --compartment-id "${compartment}" \ - --cidr-blocks '["10.0.0.0/16"]' \ - --display-name "spawn-vcn" \ - --dns-label "spawnvcn" \ - --wait-for-state AVAILABLE \ - --query 'data.id' \ - --raw-output 2>/dev/null) - - if [[ -z "${vcn_id}" || "${vcn_id}" == "null" ]]; then - log_error "Failed to create VCN (Virtual Cloud Network)" - log_warn "Common issues:" - log_warn " - VCN limit exceeded in this compartment (check OCI service limits)" - log_warn " - Compartment permissions insufficient (check IAM policies)" - return 1 - fi - - echo "${vcn_id}" -} - -_create_internet_gateway() { - local compartment="${1}" - local vcn_id="${2}" - - oci network internet-gateway create \ - --compartment-id "${compartment}" \ - --vcn-id "${vcn_id}" \ - --display-name "spawn-igw" \ - --is-enabled true \ - --wait-for-state AVAILABLE \ - --query 'data.id' \ - --raw-output 2>/dev/null -} - -_add_default_route() { - local compartment="${1}" - local vcn_id="${2}" - local igw_id="${3}" - - local rt_id - rt_id=$(oci network route-table list \ - --compartment-id "${compartment}" \ - --vcn-id "${vcn_id}" \ - --query 'data[0].id' \ - --raw-output 2>/dev/null) - - if [[ -n "${rt_id}" && "${rt_id}" != "null" ]]; then - oci network route-table update \ - --rt-id "${rt_id}" \ - --route-rules "[{\"destination\":\"0.0.0.0/0\",\"networkEntityId\":\"${igw_id}\",\"destinationType\":\"CIDR_BLOCK\"}]" \ - --force \ - --wait-for-state AVAILABLE >/dev/null 2>&1 || true - fi -} - -_add_ssh_security_rules() { - local compartment="${1}" - local vcn_id="${2}" - - local sl_id - sl_id=$(oci network security-list list \ - --compartment-id "${compartment}" \ - --vcn-id "${vcn_id}" \ - --query 'data[0].id' \ - --raw-output 2>/dev/null) - - if [[ -n "${sl_id}" && "${sl_id}" != "null" ]]; then - oci network security-list update \ - --security-list-id "${sl_id}" \ - --ingress-security-rules '[{"source":"0.0.0.0/0","protocol":"6","tcpOptions":{"destinationPortRange":{"min":22,"max":22}},"isStateless":false}]' \ - --egress-security-rules '[{"destination":"0.0.0.0/0","protocol":"all","isStateless":false}]' \ - --force \ - --wait-for-state AVAILABLE >/dev/null 2>&1 || true - fi -} - -_setup_vcn_networking() { - local compartment="${1}" - local vcn_id="${2}" - - local igw_id - igw_id=$(_create_internet_gateway "${compartment}" "${vcn_id}") - - if [[ -n "${igw_id}" && "${igw_id}" != "null" ]]; then - _add_default_route "${compartment}" "${vcn_id}" "${igw_id}" - fi - - _add_ssh_security_rules "${compartment}" "${vcn_id}" -} - -_create_subnet() { - local compartment="${1}" - local vcn_id="${2}" - - local ad - ad=$(_get_availability_domain) - - local subnet_id - subnet_id=$(oci network subnet create \ - --compartment-id "${compartment}" \ - --vcn-id "${vcn_id}" \ - --cidr-block "10.0.1.0/24" \ - --display-name "spawn-subnet" \ - --availability-domain "${ad}" \ - --dns-label "spawnsubnet" \ - --wait-for-state AVAILABLE \ - --query 'data.id' \ - --raw-output 2>/dev/null) - - if [[ -z "${subnet_id}" || "${subnet_id}" == "null" ]]; then - log_error "Failed to create subnet in VCN" - log_warn "Common issues:" - log_warn " - Subnet limit exceeded in this VCN" - log_warn " - Compartment permissions insufficient (check IAM policies)" - return 1 - fi - - echo "${subnet_id}" -} - -_get_subnet_id() { - local compartment="${OCI_COMPARTMENT_ID}" - - # Try to find an existing public subnet - local subnet_id - subnet_id=$(oci network subnet list \ - --compartment-id "${compartment}" \ - --query 'data[?("prohibit-public-ip-on-vnic"==`false`)].id | [0]' \ - --raw-output 2>/dev/null || true) - - if [[ -n "${subnet_id}" && "${subnet_id}" != "null" ]]; then - echo "${subnet_id}" - return 0 - fi - - # No public subnet found - create VCN with networking and subnet - log_step "No public subnet found. Creating VCN and subnet..." - - local vcn_id - vcn_id=$(_create_vcn "${compartment}") || return 1 - _setup_vcn_networking "${compartment}" "${vcn_id}" - _create_subnet "${compartment}" "${vcn_id}" -} - -_get_instance_public_ip() { - local instance_id="${1}" - - local vnic_id - vnic_id=$(oci compute vnic-attachment list \ - --compartment-id "${OCI_COMPARTMENT_ID}" \ - --instance-id "${instance_id}" \ - --query 'data[0]."vnic-id"' \ - --raw-output 2>/dev/null) - - if [[ -z "${vnic_id}" || "${vnic_id}" == "null" ]]; then - log_error "Could not get VNIC for instance" - return 1 - fi - - local ip - ip=$(oci network vnic get \ - --vnic-id "${vnic_id}" \ - --query 'data."public-ip"' \ - --raw-output 2>/dev/null || true) - - if [[ -z "${ip}" || "${ip}" == "null" ]]; then - log_error "Could not get public IP for instance" - return 1 - fi - - echo "${ip}" -} - -# Encode cloud-init userdata as base64 (macOS and Linux compatible) -_encode_userdata_b64() { - get_cloud_init_userdata | base64 -w0 2>/dev/null || get_cloud_init_userdata | base64 -} - -# Launch an OCI compute instance and return its OCID on stdout -# Usage: instance_id=$(_launch_oci_instance NAME SHAPE IMAGE_ID AD SUBNET_ID USERDATA_B64) -_launch_oci_instance() { - local name="${1}" shape="${2}" image_id="${3}" ad="${4}" subnet_id="${5}" userdata_b64="${6}" - - # Build shape config for flex shapes - local shape_config_args=() - if [[ "${shape}" == *".Flex" || "${shape}" == *".Flex."* ]]; then - local ocpus="${OCI_OCPUS:-1}" - local memory="${OCI_MEMORY_GB:-4}" - shape_config_args=(--shape-config "{\"ocpus\": ${ocpus}, \"memoryInGBs\": ${memory}}") - fi - - local instance_id - local oci_err - oci_err=$(mktemp) - track_temp_file "${oci_err}" - - instance_id=$(oci compute instance launch \ - --compartment-id "${OCI_COMPARTMENT_ID}" \ - --availability-domain "${ad}" \ - --shape "${shape}" \ - "${shape_config_args[@]}" \ - --image-id "${image_id}" \ - --subnet-id "${subnet_id}" \ - --display-name "${name}" \ - --assign-public-ip true \ - --ssh-authorized-keys-file "${HOME}/.ssh/id_ed25519.pub" \ - --user-data "${userdata_b64}" \ - --wait-for-state RUNNING \ - --query 'data.id' \ - --raw-output 2>"${oci_err}") || true - - if [[ -z "${instance_id}" || "${instance_id}" == "null" ]]; then - log_error "Failed to create OCI instance" - local err_output - err_output=$(cat "${oci_err}" 2>/dev/null) - if [[ -n "${err_output}" ]]; then - log_error "OCI error: ${err_output}" - fi - log_warn "Common issues:" - log_warn " - Service limit (quota) exceeded for this shape in the availability domain" - log_warn " - Compartment permissions insufficient (check IAM policies)" - log_warn " - Shape not available in this region (try different OCI_SHAPE)" - log_warn " - Out of host capacity (try a different availability domain)" - return 1 - fi - - echo "${instance_id}" -} - -create_server() { - local name="${1}" - local shape="${OCI_SHAPE:-VM.Standard.A1.Flex}" - - log_step "Creating OCI instance '${name}' (shape: ${shape})..." - - local image_id - image_id=$(_get_ubuntu_image_id "${shape}") || return 1 - - local ad - ad=$(_get_availability_domain) || return 1 - - local subnet_id="${OCI_SUBNET_ID:-}" - if [[ -z "${subnet_id}" ]]; then - subnet_id=$(_get_subnet_id) || return 1 - fi - - local userdata_b64 - userdata_b64=$(_encode_userdata_b64) - - local instance_id - instance_id=$(_launch_oci_instance "${name}" "${shape}" "${image_id}" "${ad}" "${subnet_id}" "${userdata_b64}") || return 1 - - export OCI_INSTANCE_ID="${instance_id}" - export OCI_INSTANCE_NAME_ACTUAL="${name}" - - local server_ip - server_ip=$(_get_instance_public_ip "${instance_id}") || return 1 - - export OCI_SERVER_IP="${server_ip}" - log_info "Instance created: IP=${OCI_SERVER_IP}" - - save_vm_connection "${OCI_SERVER_IP}" "ubuntu" "${OCI_INSTANCE_ID}" "$name" "oracle" -} - -# OCI Ubuntu images use 'ubuntu' user -SSH_USER="ubuntu" - -# SSH operations — delegates to shared helpers -verify_server_connectivity() { ssh_verify_connectivity "$@"; } -run_server() { ssh_run_server "$@"; } -upload_file() { ssh_upload_file "$@"; } -interactive_session() { ssh_interactive_session "$@"; } - -wait_for_cloud_init() { - local ip="${1}" - local max_attempts=${2:-60} - - # First ensure SSH connectivity - ssh_verify_connectivity "${ip}" 30 5 || return 1 - - # Then wait for cloud-init completion marker - generic_ssh_wait "ubuntu" "${ip}" "${SSH_OPTS}" "test -f /home/ubuntu/.cloud-init-complete" "cloud-init" "${max_attempts}" 5 -} - -destroy_server() { - local instance_id="${1:-${OCI_INSTANCE_ID:-}}" - if [[ -z "${instance_id}" ]]; then - log_error "No instance ID provided. Usage: destroy_server INSTANCE_OCID" - return 1 - fi - log_step "Terminating OCI instance ${instance_id}..." - oci compute instance terminate \ - --instance-id "${instance_id}" \ - --preserve-boot-volume false \ - --force >/dev/null 2>&1 - log_info "Instance terminated" -} - -list_servers() { - oci compute instance list \ - --compartment-id "${OCI_COMPARTMENT_ID}" \ - --query 'data[?("lifecycle-state"!=`TERMINATED`)].{"Name":"display-name","State":"lifecycle-state","Shape":"shape","Created":"time-created"}' \ - --output table 2>/dev/null -} - -# ============================================================ -# Cloud adapter interface -# ============================================================ - -cloud_authenticate() { ensure_oci_cli; ensure_ssh_key; } -cloud_provision() { create_server "$1"; } -cloud_wait_ready() { verify_server_connectivity "${OCI_SERVER_IP}"; wait_for_cloud_init "${OCI_SERVER_IP}" 60; } -cloud_run() { run_server "${OCI_SERVER_IP}" "$1"; } -cloud_upload() { upload_file "${OCI_SERVER_IP}" "$1" "$2"; } -cloud_interactive() { interactive_session "${OCI_SERVER_IP}" "$1"; } -cloud_label() { echo "OCI instance"; } diff --git a/oracle/nanoclaw.sh b/oracle/nanoclaw.sh deleted file mode 100755 index 4c1ee89d..00000000 --- a/oracle/nanoclaw.sh +++ /dev/null @@ -1,40 +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=oracle/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/oracle/lib/common.sh)" -fi - -log_info "NanoClaw on Oracle Cloud Infrastructure" -echo "" - -agent_install() { - 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() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - trap 'rm -f "${dotenv_temp}"' EXIT - chmod 600 "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" -} -agent_launch_cmd() { echo 'cd ~/nanoclaw && source ~/.zshrc && npm run dev'; } - -spawn_agent "NanoClaw" diff --git a/oracle/openclaw.sh b/oracle/openclaw.sh deleted file mode 100755 index 80f55884..00000000 --- a/oracle/openclaw.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "OpenClaw on Oracle Cloud Infrastructure" -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() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" - wait_for_openclaw_gateway cloud_run -} -agent_launch_cmd() { echo 'source ~/.zshrc && openclaw tui'; } - -spawn_agent "OpenClaw" diff --git a/oracle/opencode.sh b/oracle/opencode.sh deleted file mode 100755 index f234e23d..00000000 --- a/oracle/opencode.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "OpenCode on Oracle Cloud Infrastructure" -echo "" - -agent_install() { install_agent "OpenCode" "$(opencode_install_cmd)" cloud_run; } -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"; } -agent_launch_cmd() { echo 'source ~/.zshrc && opencode'; } - -spawn_agent "OpenCode" diff --git a/oracle/plandex.sh b/oracle/plandex.sh deleted file mode 100755 index 7265828f..00000000 --- a/oracle/plandex.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# Source common functions - try local file first, fall back to remote -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" -# shellcheck source=oracle/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/oracle/lib/common.sh)" -fi - -log_info "Plandex on Oracle Cloud Infrastructure" -echo "" - -agent_install() { - install_agent "Plandex" "curl -sL https://plandex.ai/install.sh | bash" cloud_run - verify_agent "Plandex" "command -v plandex && plandex version" "curl -sL https://plandex.ai/install.sh | bash" cloud_run -} -agent_env_vars() { generate_env_config "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"; } -agent_launch_cmd() { echo 'source ~/.zshrc && plandex'; } - -spawn_agent "Plandex" diff --git a/test/e2e.sh b/test/e2e.sh index 11c7e010..83d405bc 100644 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -63,7 +63,7 @@ _get_name_env_var() { daytona) echo "DAYTONA_SANDBOX_NAME" ;; ovh) echo "OVH_SERVER_NAME" ;; gcp) echo "GCP_INSTANCE_NAME" ;; - oracle) echo "OCI_INSTANCE_NAME" ;; + sprite) echo "SPRITE_NAME" ;; *) echo "" ;; esac @@ -121,7 +121,7 @@ _collect_credentials() { local token_var token_var=$(_get_token_env_var "$cloud") - # CLI-auth clouds (aws, gcp, oracle, sprite) — no token to collect + # CLI-auth clouds (aws, gcp, sprite) — no token to collect [[ -z "$token_var" ]] && continue # Already in env? @@ -198,7 +198,7 @@ _cloud_has_credentials() { case "$cloud" in aws) command -v aws &>/dev/null && aws sts get-caller-identity &>/dev/null 2>&1; return $? ;; gcp) command -v gcloud &>/dev/null && gcloud auth print-access-token &>/dev/null 2>&1; return $? ;; - oracle) command -v oci &>/dev/null && oci iam region list --output table &>/dev/null 2>&1; return $? ;; + sprite) command -v sprite &>/dev/null; return $? ;; local) return 0 ;; esac @@ -392,20 +392,6 @@ for i in (data if isinstance(data, list) else []): " 2>/dev/null) || return 0 [[ -n "$iid" ]] && destroy_server "$iid" 2>/dev/null || true ;; - oracle) - source "${REPO_ROOT}/oracle/lib/common.sh" 2>/dev/null || return 0 - # Oracle needs OCID — list and find by name - local instances_json ocid - instances_json=$(oci compute instance list --compartment-id "${OCI_COMPARTMENT_ID:-}" --display-name "$server_name" --lifecycle-state RUNNING 2>/dev/null) || return 0 - ocid=$(printf '%s' "$instances_json" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -items = data.get('data', []) -if items: - print(items[0]['id']) -" 2>/dev/null) || return 0 - [[ -n "$ocid" ]] && destroy_server "$ocid" 2>/dev/null || true - ;; esac } @@ -1048,7 +1034,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 oracle" + local testable_clouds="fly hetzner digitalocean ovh aws daytona gcp" # --- Credential collection (interactive) --- # Load tokens from config files and prompt for any missing ones diff --git a/test/record.sh b/test/record.sh index 82dd6f65..8797d8d9 100644 --- a/test/record.sh +++ b/test/record.sh @@ -873,7 +873,7 @@ list_clouds() { total_count=$(echo "$ALL_RECORDABLE_CLOUDS" | wc -w | tr -d ' ') printf '%b\n' " ${ready_count}/${total_count} clouds have credentials set" printf '\n' - printf " CLI-based clouds (not recordable): sprite, gcp, daytona, aws, oracle, local\n" + printf " CLI-based clouds (not recordable): sprite, gcp, daytona, aws, local\n" } # --- Main ---