diff --git a/README.md b/README.md index 8491afff..e5af110e 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ spawn claude gcp --beta tarball | Feature | Description | |---------|-------------| | `tarball` | Use pre-built tarball for agent install (faster, skips live install) | +| `images` | Use pre-built DigitalOcean marketplace images (faster boot, skips cloud-init) | ### Without the CLI diff --git a/packages/cli/package.json b/packages/cli/package.json index f28db9f5..b6eaf55c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.17.10", + "version": "0.17.11", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/commands/interactive.ts b/packages/cli/src/commands/interactive.ts index 47252600..6f65373e 100644 --- a/packages/cli/src/commands/interactive.ts +++ b/packages/cli/src/commands/interactive.ts @@ -210,7 +210,7 @@ async function promptSetupOptions(agentName: string): Promise | unde return new Set(realSteps); } -export { promptSpawnName, promptSetupOptions, getAndValidateCloudChoices, selectCloud }; +export { getAndValidateCloudChoices, promptSetupOptions, promptSpawnName, selectCloud }; export async function cmdInteractive(): Promise { p.intro(pc.inverse(` spawn v${VERSION} `)); diff --git a/packages/cli/src/digitalocean/digitalocean.ts b/packages/cli/src/digitalocean/digitalocean.ts index 5a979cff..3046c480 100644 --- a/packages/cli/src/digitalocean/digitalocean.ts +++ b/packages/cli/src/digitalocean/digitalocean.ts @@ -871,7 +871,7 @@ export async function createServer( tier?: CloudInitTier, dropletSize?: string, region?: string, - snapshotId?: string, + imageOverride?: string, ): Promise { const size = dropletSize || process.env.DO_DROPLET_SIZE || "s-2vcpu-2gb"; const effectiveRegion = region || process.env.DO_REGION || "nyc3"; @@ -881,8 +881,13 @@ export async function createServer( throw new Error("Invalid region"); } - const image = snapshotId ? Number(snapshotId) : "ubuntu-24-04-x64"; - const imageLabel = snapshotId ? `snapshot:${snapshotId}` : "ubuntu-24-04-x64"; + // imageOverride can be a numeric snapshot ID or a marketplace slug (e.g. "openrouter-spawnclaude") + const image: string | number = imageOverride + ? /^\d+$/.test(imageOverride) + ? Number(imageOverride) + : imageOverride + : "ubuntu-24-04-x64"; + const imageLabel = imageOverride ?? "ubuntu-24-04-x64"; logStep( `Creating DigitalOcean droplet '${name}' (size: ${size}, region: ${effectiveRegion}, image: ${imageLabel})...`, @@ -905,8 +910,8 @@ export async function createServer( monitoring: false, }; - // Only include cloud-init userdata when NOT booting from a snapshot - if (!snapshotId) { + // Only include cloud-init userdata when NOT booting from a pre-built image + if (!imageOverride) { dropletConfig.user_data = getCloudInitUserdata(tier); } diff --git a/packages/cli/src/digitalocean/main.ts b/packages/cli/src/digitalocean/main.ts index daa326a6..e0e45c35 100644 --- a/packages/cli/src/digitalocean/main.ts +++ b/packages/cli/src/digitalocean/main.ts @@ -6,13 +6,13 @@ import type { CloudOrchestrator } from "../shared/orchestrate"; import { runOrchestration } from "../shared/orchestrate"; import { getErrorMessage } from "../shared/type-guards.js"; +import { logInfo } from "../shared/ui"; import { agents, resolveAgent } from "./agents"; import { checkAccountStatus, createServer as createDroplet, ensureDoToken, ensureSshKey, - findSpawnSnapshot, getConnectionInfo, getServerName, interactiveSession, @@ -25,6 +25,18 @@ import { waitForSshOnly, } from "./digitalocean"; +/** DO marketplace image slugs — hardcoded from vendor portal (approved 2026-03-13) */ +const MARKETPLACE_IMAGES: Record = { + claude: "openrouter-spawnclaude", + codex: "openrouter-spawncodex", + openclaw: "openrouter-spawnopenclaw", + opencode: "openrouter-spawnopencode", + kilocode: "openrouter-spawnkilocode", + zeroclaw: "openrouter-spawnzeroclaw", + hermes: "openrouter-spawnhermes", + junie: "openrouter-spawnjunie", +}; + async function main() { const agentName = process.argv[2]; if (!agentName) { @@ -37,7 +49,7 @@ async function main() { let dropletSize = ""; let region = ""; - let snapshotId: string | null = null; + let marketplaceImage: string | undefined; const cloud: CloudOrchestrator = { cloudName: "digitalocean", @@ -60,16 +72,23 @@ async function main() { region = await promptDoRegion(); }, async createServer(name: string) { - // Check for a pre-built snapshot before provisioning - snapshotId = await findSpawnSnapshot(agentName); - if (snapshotId) { - cloud.skipAgentInstall = true; + // Use pre-built marketplace image when --beta images is active + const betaFeatures = (process.env.SPAWN_BETA ?? "").split(","); + if (betaFeatures.includes("images")) { + const slug = MARKETPLACE_IMAGES[agentName]; + if (slug) { + marketplaceImage = slug; + cloud.skipAgentInstall = true; + logInfo(`Using marketplace image: ${slug}`); + } else { + logInfo(`No marketplace image for ${agentName}, using fresh install`); + } } - return await createDroplet(name, agent.cloudInitTier, dropletSize, region, snapshotId ?? undefined); + return await createDroplet(name, agent.cloudInitTier, dropletSize, region, marketplaceImage); }, getServerName, async waitForReady() { - if (snapshotId) { + if (marketplaceImage) { await waitForSshOnly(); } else { await waitForCloudInit(); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5a1d58df..5eb2847e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -121,6 +121,7 @@ function checkUnknownFlags(args: string[]): void { console.error(` ${pc.cyan("--config ")} Load config from JSON file`); console.error(` ${pc.cyan("--steps ")} Comma-separated setup steps to enable`); console.error(` ${pc.cyan("--beta tarball")} Use pre-built tarball for agent install (repeatable)`); + console.error(` ${pc.cyan("--beta images")} Use pre-built DO marketplace images (faster boot)`); console.error(` ${pc.cyan("--help, -h")} Show help information`); console.error(` ${pc.cyan("--version, -v")} Show version`); console.error(); @@ -789,13 +790,15 @@ async function main(): Promise { // Extract all --beta flags (repeatable, opt-in to experimental features) const VALID_BETA_FEATURES = new Set([ "tarball", + "images", ]); - const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn --beta tarball"); + const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn --beta images"); for (const flag of betaFeatures) { if (!VALID_BETA_FEATURES.has(flag)) { console.error(pc.red(`Unknown beta feature: ${pc.bold(flag)}`)); console.error("\nAvailable beta features:"); console.error(` ${pc.cyan("tarball")} Use pre-built tarball for agent installation`); + console.error(` ${pc.cyan("images")} Use pre-built DO marketplace images (faster boot)`); process.exit(1); } } diff --git a/packages/cli/src/manifest.ts b/packages/cli/src/manifest.ts index d699b606..810cfb62 100644 --- a/packages/cli/src/manifest.ts +++ b/packages/cli/src/manifest.ts @@ -319,4 +319,4 @@ export function _resetCacheForTesting(): void { _staleCache = false; } -export { RAW_BASE, REPO, SPAWN_CDN, VERSION_URL, stripDangerousKeys }; +export { RAW_BASE, REPO, SPAWN_CDN, stripDangerousKeys, VERSION_URL };