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

View file

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

View file

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

View file

@ -871,7 +871,7 @@ export async function createServer(
tier?: CloudInitTier,
dropletSize?: string,
region?: string,
snapshotId?: string,
imageOverride?: string,
): Promise<VMConnection> {
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);
}

View file

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

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("--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 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<void> {
// Extract all --beta <feature> flags (repeatable, opt-in to experimental features)
const VALID_BETA_FEATURES = new Set([
"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) {
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);
}
}

View file

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