mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
feat(cli): add spawn name for each run (#1397)
Implements spawn name feature (#1372) to improve UX: - Add optional spawn name prompt in interactive mode - Pass spawn name via SPAWN_NAME env var to shell scripts - Shell scripts use spawn name as default for resource names - Store spawn name in history for future reference - Bump CLI version to 0.4.0 The spawn name is prompted before agent/cloud selection and automatically used as the default for platform-specific resource names (server name on Hetzner, sprite name on Sprite, etc.). Agent: ux-engineer Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
27e7f32da3
commit
7544dd0dcb
4 changed files with 44 additions and 8 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.3.3",
|
||||
"version": "0.4.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -447,10 +447,32 @@ async function selectCloud(manifest: Manifest, cloudList: string[], hintOverride
|
|||
return cloudChoice;
|
||||
}
|
||||
|
||||
// Prompt user to enter a spawn name for the instance
|
||||
async function promptSpawnName(): Promise<string | undefined> {
|
||||
const spawnName = await p.text({
|
||||
message: "Enter a name for this spawn (optional)",
|
||||
placeholder: "my-spawn",
|
||||
validate: (value) => {
|
||||
if (!value) return undefined; // Optional field
|
||||
// Validate name format (alphanumeric, hyphens, underscores)
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
||||
return "Name must contain only letters, numbers, hyphens, and underscores";
|
||||
}
|
||||
if (value.length > 63) {
|
||||
return "Name must be 63 characters or less";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
if (p.isCancel(spawnName)) handleCancel();
|
||||
return spawnName || undefined;
|
||||
}
|
||||
|
||||
export async function cmdInteractive(): Promise<void> {
|
||||
p.intro(pc.inverse(` spawn v${VERSION} `));
|
||||
|
||||
const manifest = await loadManifestWithSpinner();
|
||||
const spawnName = await promptSpawnName();
|
||||
const agentChoice = await selectAgent(manifest);
|
||||
|
||||
const { clouds, hintOverrides } = getAndValidateCloudChoices(manifest, agentChoice);
|
||||
|
|
@ -464,7 +486,7 @@ export async function cmdInteractive(): Promise<void> {
|
|||
p.log.info(`Next time, run directly: ${pc.cyan(`spawn ${agentChoice} ${cloudChoice}`)}`);
|
||||
p.outro("Handing off to spawn script...");
|
||||
|
||||
await execScript(cloudChoice, agentChoice, undefined, getAuthHint(manifest, cloudChoice), manifest.clouds[cloudChoice].url);
|
||||
await execScript(cloudChoice, agentChoice, undefined, getAuthHint(manifest, cloudChoice), manifest.clouds[cloudChoice].url, undefined, spawnName);
|
||||
}
|
||||
|
||||
/** Interactive cloud selection when agent is already known (e.g. `spawn claude`) */
|
||||
|
|
@ -472,6 +494,7 @@ export async function cmdAgentInteractive(agent: string, prompt?: string, dryRun
|
|||
p.intro(pc.inverse(` spawn v${VERSION} `));
|
||||
|
||||
const manifest = await loadManifestWithSpinner();
|
||||
const spawnName = await promptSpawnName();
|
||||
const resolvedAgent = resolveAgentKey(manifest, agent);
|
||||
|
||||
if (!resolvedAgent) {
|
||||
|
|
@ -495,7 +518,7 @@ export async function cmdAgentInteractive(agent: string, prompt?: string, dryRun
|
|||
p.log.info(`Next time, run directly: ${pc.cyan(`spawn ${resolvedAgent} ${cloudChoice}`)}`);
|
||||
p.outro("Handing off to spawn script...");
|
||||
|
||||
await execScript(cloudChoice, resolvedAgent, prompt, getAuthHint(manifest, cloudChoice), manifest.clouds[cloudChoice].url, dryRun);
|
||||
await execScript(cloudChoice, resolvedAgent, prompt, getAuthHint(manifest, cloudChoice), manifest.clouds[cloudChoice].url, dryRun, spawnName);
|
||||
}
|
||||
|
||||
// ── Run ────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -1057,10 +1080,10 @@ function handleUserInterrupt(errMsg: string, dashboardUrl?: string): void {
|
|||
process.exit(130);
|
||||
}
|
||||
|
||||
async function runWithRetries(script: string, prompt?: string, dashboardUrl?: string, debug?: boolean): Promise<string | undefined> {
|
||||
async function runWithRetries(script: string, prompt?: string, dashboardUrl?: string, debug?: boolean, spawnName?: string): Promise<string | undefined> {
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES + 1; attempt++) {
|
||||
try {
|
||||
await runBash(script, prompt, debug);
|
||||
await runBash(script, prompt, debug, spawnName);
|
||||
return undefined; // success
|
||||
} catch (err) {
|
||||
const errMsg = getErrorMessage(err);
|
||||
|
|
@ -1079,7 +1102,7 @@ async function runWithRetries(script: string, prompt?: string, dashboardUrl?: st
|
|||
return "Script failed after all retries";
|
||||
}
|
||||
|
||||
async function execScript(cloud: string, agent: string, prompt?: string, authHint?: string, dashboardUrl?: string, debug?: boolean): Promise<void> {
|
||||
async function execScript(cloud: string, agent: string, prompt?: string, authHint?: string, dashboardUrl?: string, debug?: boolean, spawnName?: string): Promise<void> {
|
||||
const url = `https://openrouter.ai/labs/spawn/${cloud}/${agent}.sh`;
|
||||
const ghUrl = `${RAW_BASE}/${cloud}/${agent}.sh`;
|
||||
|
||||
|
|
@ -1097,6 +1120,7 @@ async function execScript(cloud: string, agent: string, prompt?: string, authHin
|
|||
agent,
|
||||
cloud,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(spawnName ? { name: spawnName } : {}),
|
||||
...(prompt ? { prompt } : {}),
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
@ -1107,13 +1131,13 @@ async function execScript(cloud: string, agent: string, prompt?: string, authHin
|
|||
}
|
||||
}
|
||||
|
||||
const lastErr = await runWithRetries(scriptContent, prompt, dashboardUrl, debug);
|
||||
const lastErr = await runWithRetries(scriptContent, prompt, dashboardUrl, debug, spawnName);
|
||||
if (lastErr) {
|
||||
reportScriptFailure(lastErr, cloud, agent, authHint, prompt, dashboardUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function runBash(script: string, prompt?: string, debug?: boolean): Promise<void> {
|
||||
function runBash(script: string, prompt?: string, debug?: boolean, spawnName?: string): Promise<void> {
|
||||
// SECURITY: Validate script content before execution
|
||||
validateScriptContent(script);
|
||||
|
||||
|
|
@ -1126,6 +1150,9 @@ function runBash(script: string, prompt?: string, debug?: boolean): Promise<void
|
|||
if (debug) {
|
||||
env.SPAWN_DEBUG = "1";
|
||||
}
|
||||
if (spawnName) {
|
||||
env.SPAWN_NAME = spawnName;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = spawn("bash", ["-c", script], {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface SpawnRecord {
|
|||
agent: string;
|
||||
cloud: string;
|
||||
timestamp: string;
|
||||
name?: string;
|
||||
prompt?: string;
|
||||
connection?: VMConnection;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -453,12 +453,20 @@ get_resource_name() {
|
|||
local prompt_text="${2}"
|
||||
local resource_value="${!env_var_name}"
|
||||
|
||||
# First check platform-specific env var
|
||||
if [[ -n "${resource_value}" ]]; then
|
||||
log_info "Using ${prompt_text%:*} from environment: ${resource_value}"
|
||||
echo "${resource_value}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Then check for SPAWN_NAME (set by CLI)
|
||||
if [[ -n "${SPAWN_NAME:-}" ]]; then
|
||||
log_info "Using spawn name: ${SPAWN_NAME}"
|
||||
echo "${SPAWN_NAME}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local name
|
||||
name=$(safe_read "${prompt_text}")
|
||||
if [[ -z "${name}" ]]; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue