mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +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 |
|
||||
|---------|-------------|
|
||||
| `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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.17.10",
|
||||
"version": "0.17.11",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -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} `));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue