Skip to content

Commit 0f48e4d

Browse files
AhmedTMMclaudela14-1
authored
feat: headless delete via spawn delete --name <name> --yes (#3015)
Agents running on spawned VMs couldn't delete child spawns because `spawn delete` requires an interactive terminal for the picker UI. Added --name and --yes flags: when both are provided in non-interactive mode, the server matching the name is deleted without prompts. This enables agents to manage their own child VMs programmatically. Updated all skill files to teach agents the headless delete syntax. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
1 parent 73bb52e commit 0f48e4d

11 files changed

Lines changed: 49 additions & 15 deletions

File tree

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openrouter/spawn",
3-
"version": "0.26.11",
3+
"version": "0.26.12",
44
"type": "module",
55
"bin": {
66
"spawn": "cli.js"

packages/cli/src/commands/delete.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,12 @@ export async function cascadeDelete(record: SpawnRecord, manifest: Manifest | nu
352352
return confirmAndDelete(record, manifest);
353353
}
354354

355-
export async function cmdDelete(agentFilter?: string, cloudFilter?: string): Promise<void> {
355+
export async function cmdDelete(
356+
agentFilter?: string,
357+
cloudFilter?: string,
358+
nameFilter?: string,
359+
forceYes?: boolean,
360+
): Promise<void> {
356361
const resolved = await resolveListFilters(agentFilter, cloudFilter);
357362
agentFilter = resolved.agentFilter;
358363
cloudFilter = resolved.cloudFilter;
@@ -368,6 +373,15 @@ export async function cmdDelete(agentFilter?: string, cloudFilter?: string): Pro
368373
const lower = cloudFilter.toLowerCase();
369374
filtered = filtered.filter((r) => r.cloud.toLowerCase() === lower);
370375
}
376+
if (nameFilter) {
377+
const lower = nameFilter.toLowerCase();
378+
filtered = filtered.filter(
379+
(r) =>
380+
(r.name ?? "").toLowerCase() === lower ||
381+
(r.connection?.server_name ?? "").toLowerCase() === lower ||
382+
r.id === nameFilter,
383+
);
384+
}
371385

372386
if (filtered.length === 0) {
373387
p.log.info("No active servers to delete.");
@@ -387,10 +401,22 @@ export async function cmdDelete(agentFilter?: string, cloudFilter?: string): Pro
387401
const manifestResult = await asyncTryCatchIf(isNetworkError, loadManifest);
388402
const manifest: Manifest | null = manifestResult.ok ? manifestResult.data : null;
389403

404+
// Non-interactive headless delete: --name + --yes skips the picker
390405
if (!isInteractiveTTY()) {
391-
p.log.error("spawn delete requires an interactive terminal.");
392-
p.log.info(`Use ${pc.cyan("spawn list")} to see your servers.`);
393-
process.exit(1);
406+
if (!forceYes) {
407+
p.log.error("spawn delete requires --yes in non-interactive mode.");
408+
p.log.info(`Usage: ${pc.cyan("spawn delete --name <name> --yes")}`);
409+
process.exit(1);
410+
}
411+
for (const record of filtered) {
412+
const label = record.connection?.server_name || record.name || record.id;
413+
await ensureDeleteCredentials(record);
414+
const ok = await execDeleteServer(record);
415+
if (ok) {
416+
p.log.success(`Server "${label}" deleted`);
417+
}
418+
}
419+
return;
394420
}
395421

396422
await activeServerPicker(filtered, manifest);

packages/cli/src/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,8 +613,16 @@ async function dispatchDeleteCommand(filteredArgs: string[]): Promise<void> {
613613
cmdHelp();
614614
return;
615615
}
616-
const { agentFilter, cloudFilter } = parseListFilters(filteredArgs.slice(1));
617-
await cmdDelete(agentFilter, cloudFilter);
616+
const args = filteredArgs.slice(1);
617+
const forceYes = args.includes("--yes") || args.includes("-y");
618+
let nameFilter: string | undefined;
619+
const nameIdx = args.indexOf("--name");
620+
if (nameIdx !== -1 && args[nameIdx + 1]) {
621+
nameFilter = args[nameIdx + 1];
622+
}
623+
const cleanArgs = args.filter((a) => a !== "--yes" && a !== "-y" && a !== "--name" && a !== nameFilter);
624+
const { agentFilter, cloudFilter } = parseListFilters(cleanArgs);
625+
await cmdDelete(agentFilter, cloudFilter, nameFilter, forceYes);
618626
}
619627

620628
/** Handle status/ps commands with --prune and --json flags */

packages/cli/src/shared/spawn-skill.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Returns JSON: \`{"status":"success","ip_address":"...","ssh_user":"root","server
4444
## Managing Children
4545
4646
- \`spawn list --json\` — see running children
47-
- \`spawn delete\` — tear down a child VM
47+
- \`spawn delete --name <name> --yes\` — tear down a child VM (headless)
4848
- \`spawn tree\` — see the full spawn tree
4949
5050
## Context

skills/claude/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_
2828
## Managing Children
2929

3030
- `spawn list --json` — see running children
31-
- `spawn delete` — tear down a child VM
31+
- `spawn delete --name <name> --yes` — tear down a child VM
3232
- `spawn tree` — see the full spawn tree
3333

3434
## Context

skills/codex/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_
2828
## Managing Children
2929

3030
- `spawn list --json` — see running children
31-
- `spawn delete` — tear down a child VM
31+
- `spawn delete --name <name> --yes` — tear down a child VM
3232
- `spawn tree` — see the full spawn tree
3333

3434
## Context

skills/junie/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_
2222
## Managing Children
2323

2424
- `spawn list --json` — see running children
25-
- `spawn delete` — tear down a child VM
25+
- `spawn delete --name <name> --yes` — tear down a child VM
2626
- `spawn tree` — see the full spawn tree
2727

2828
## Context

skills/kilocode/spawn.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_
2222
## Managing Children
2323

2424
- `spawn list --json` — see running children
25-
- `spawn delete` — tear down a child VM
25+
- `spawn delete --name <name> --yes` — tear down a child VM
2626
- `spawn tree` — see the full spawn tree
2727

2828
## Context

skills/openclaw/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_
2828
## Managing Children
2929

3030
- `spawn list --json` — see running children
31-
- `spawn delete` — tear down a child VM
31+
- `spawn delete --name <name> --yes` — tear down a child VM
3232
- `spawn tree` — see the full spawn tree
3333

3434
## Context

skills/opencode/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_
2222
## Managing Children
2323

2424
- `spawn list --json` — see running children
25-
- `spawn delete` — tear down a child VM
25+
- `spawn delete --name <name> --yes` — tear down a child VM
2626
- `spawn tree` — see the full spawn tree
2727

2828
## Context

0 commit comments

Comments
 (0)