mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-01 21:30:21 +00:00
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:
parent
8d3f848907
commit
39622b68ab
7 changed files with 45 additions and 17 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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} `));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue