feat: add --beta images for DO marketplace images (#2593)

* feat: add --beta images for DO marketplace images

Gate pre-built DigitalOcean marketplace images behind --beta images.
When active, uses hardcoded marketplace slugs (e.g. openrouter-spawnclaude)
instead of fresh Ubuntu + cloud-init, skipping agent install entirely.

All 8 images verified working via e2e smoke test (2026-03-13).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: sort exports to satisfy biome organizeImports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmed Abushagur 2026-03-13 12:45:25 -07:00 committed by GitHub
parent 8d3f848907
commit 39622b68ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 45 additions and 17 deletions

View file

@ -126,6 +126,7 @@ spawn claude gcp --beta tarball
| Feature | Description | | Feature | Description |
|---------|-------------| |---------|-------------|
| `tarball` | Use pre-built tarball for agent install (faster, skips live install) | | `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 ### Without the CLI

View file

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

View file

@ -210,7 +210,7 @@ async function promptSetupOptions(agentName: string): Promise<Set<string> | unde
return new Set(realSteps); return new Set(realSteps);
} }
export { promptSpawnName, promptSetupOptions, getAndValidateCloudChoices, selectCloud }; export { getAndValidateCloudChoices, promptSetupOptions, promptSpawnName, selectCloud };
export async function cmdInteractive(): Promise<void> { export async function cmdInteractive(): Promise<void> {
p.intro(pc.inverse(` spawn v${VERSION} `)); p.intro(pc.inverse(` spawn v${VERSION} `));

View file

@ -871,7 +871,7 @@ export async function createServer(
tier?: CloudInitTier, tier?: CloudInitTier,
dropletSize?: string, dropletSize?: string,
region?: string, region?: string,
snapshotId?: string, imageOverride?: string,
): Promise<VMConnection> { ): Promise<VMConnection> {
const size = dropletSize || process.env.DO_DROPLET_SIZE || "s-2vcpu-2gb"; const size = dropletSize || process.env.DO_DROPLET_SIZE || "s-2vcpu-2gb";
const effectiveRegion = region || process.env.DO_REGION || "nyc3"; const effectiveRegion = region || process.env.DO_REGION || "nyc3";
@ -881,8 +881,13 @@ export async function createServer(
throw new Error("Invalid region"); throw new Error("Invalid region");
} }
const image = snapshotId ? Number(snapshotId) : "ubuntu-24-04-x64"; // imageOverride can be a numeric snapshot ID or a marketplace slug (e.g. "openrouter-spawnclaude")
const imageLabel = snapshotId ? `snapshot:${snapshotId}` : "ubuntu-24-04-x64"; const image: string | number = imageOverride
? /^\d+$/.test(imageOverride)
? Number(imageOverride)
: imageOverride
: "ubuntu-24-04-x64";
const imageLabel = imageOverride ?? "ubuntu-24-04-x64";
logStep( logStep(
`Creating DigitalOcean droplet '${name}' (size: ${size}, region: ${effectiveRegion}, image: ${imageLabel})...`, `Creating DigitalOcean droplet '${name}' (size: ${size}, region: ${effectiveRegion}, image: ${imageLabel})...`,
@ -905,8 +910,8 @@ export async function createServer(
monitoring: false, monitoring: false,
}; };
// Only include cloud-init userdata when NOT booting from a snapshot // Only include cloud-init userdata when NOT booting from a pre-built image
if (!snapshotId) { if (!imageOverride) {
dropletConfig.user_data = getCloudInitUserdata(tier); dropletConfig.user_data = getCloudInitUserdata(tier);
} }

View file

@ -6,13 +6,13 @@ import type { CloudOrchestrator } from "../shared/orchestrate";
import { runOrchestration } from "../shared/orchestrate"; import { runOrchestration } from "../shared/orchestrate";
import { getErrorMessage } from "../shared/type-guards.js"; import { getErrorMessage } from "../shared/type-guards.js";
import { logInfo } from "../shared/ui";
import { agents, resolveAgent } from "./agents"; import { agents, resolveAgent } from "./agents";
import { import {
checkAccountStatus, checkAccountStatus,
createServer as createDroplet, createServer as createDroplet,
ensureDoToken, ensureDoToken,
ensureSshKey, ensureSshKey,
findSpawnSnapshot,
getConnectionInfo, getConnectionInfo,
getServerName, getServerName,
interactiveSession, interactiveSession,
@ -25,6 +25,18 @@ import {
waitForSshOnly, waitForSshOnly,
} from "./digitalocean"; } from "./digitalocean";
/** DO marketplace image slugs — hardcoded from vendor portal (approved 2026-03-13) */
const MARKETPLACE_IMAGES: Record<string, string> = {
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() { async function main() {
const agentName = process.argv[2]; const agentName = process.argv[2];
if (!agentName) { if (!agentName) {
@ -37,7 +49,7 @@ async function main() {
let dropletSize = ""; let dropletSize = "";
let region = ""; let region = "";
let snapshotId: string | null = null; let marketplaceImage: string | undefined;
const cloud: CloudOrchestrator = { const cloud: CloudOrchestrator = {
cloudName: "digitalocean", cloudName: "digitalocean",
@ -60,16 +72,23 @@ async function main() {
region = await promptDoRegion(); region = await promptDoRegion();
}, },
async createServer(name: string) { async createServer(name: string) {
// Check for a pre-built snapshot before provisioning // Use pre-built marketplace image when --beta images is active
snapshotId = await findSpawnSnapshot(agentName); const betaFeatures = (process.env.SPAWN_BETA ?? "").split(",");
if (snapshotId) { if (betaFeatures.includes("images")) {
cloud.skipAgentInstall = true; 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, getServerName,
async waitForReady() { async waitForReady() {
if (snapshotId) { if (marketplaceImage) {
await waitForSshOnly(); await waitForSshOnly();
} else { } else {
await waitForCloudInit(); await waitForCloudInit();

View file

@ -121,6 +121,7 @@ function checkUnknownFlags(args: string[]): void {
console.error(` ${pc.cyan("--config <path>")} Load config from JSON file`); console.error(` ${pc.cyan("--config <path>")} Load config from JSON file`);
console.error(` ${pc.cyan("--steps <list>")} Comma-separated setup steps to enable`); console.error(` ${pc.cyan("--steps <list>")} 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 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("--help, -h")} Show help information`);
console.error(` ${pc.cyan("--version, -v")} Show version`); console.error(` ${pc.cyan("--version, -v")} Show version`);
console.error(); console.error();
@ -789,13 +790,15 @@ async function main(): Promise<void> {
// Extract all --beta <feature> flags (repeatable, opt-in to experimental features) // Extract all --beta <feature> flags (repeatable, opt-in to experimental features)
const VALID_BETA_FEATURES = new Set([ const VALID_BETA_FEATURES = new Set([
"tarball", "tarball",
"images",
]); ]);
const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn <agent> <cloud> --beta tarball"); const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn <agent> <cloud> --beta images");
for (const flag of betaFeatures) { for (const flag of betaFeatures) {
if (!VALID_BETA_FEATURES.has(flag)) { if (!VALID_BETA_FEATURES.has(flag)) {
console.error(pc.red(`Unknown beta feature: ${pc.bold(flag)}`)); console.error(pc.red(`Unknown beta feature: ${pc.bold(flag)}`));
console.error("\nAvailable beta features:"); console.error("\nAvailable beta features:");
console.error(` ${pc.cyan("tarball")} Use pre-built tarball for agent installation`); 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); process.exit(1);
} }
} }

View file

@ -319,4 +319,4 @@ export function _resetCacheForTesting(): void {
_staleCache = false; _staleCache = false;
} }
export { RAW_BASE, REPO, SPAWN_CDN, VERSION_URL, stripDangerousKeys }; export { RAW_BASE, REPO, SPAWN_CDN, stripDangerousKeys, VERSION_URL };