Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/package.json
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.76",
"version": "0.2.77",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
105 changes: 104 additions & 1 deletion cli/src/__tests__/script-failure-guidance.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test";
import { getScriptFailureGuidance, getStatusDescription, buildRetryCommand } from "../commands";
import { getScriptFailureGuidance, getSignalGuidance, getStatusDescription, buildRetryCommand } from "../commands";

/**
* Tests for getScriptFailureGuidance() in commands.ts.
Expand DownExpand Up@@ -459,6 +459,109 @@ describe("getScriptFailureGuidance", () => {
});
});

describe("getSignalGuidance", () => {
describe("SIGKILL", () => {
it("should mention OOM killer", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("SIGKILL");
expect(joined).toContain("Out of memory");
});

it("should suggest a larger instance size", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("larger instance size");
});

it("should suggest checking cloud provider dashboard", () => {
const lines = getSignalGuidance("SIGKILL");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGTERM", () => {
it("should mention process was terminated", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("terminated");
expect(joined).toContain("SIGTERM");
});

it("should mention server shutdown or billing", () => {
const lines = getSignalGuidance("SIGTERM");
const joined = lines.join("\n");
expect(joined).toContain("shutdown");
});
});

describe("SIGINT", () => {
it("should mention Ctrl+C", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("Ctrl+C");
});

it("should warn about orphaned servers", () => {
const lines = getSignalGuidance("SIGINT");
const joined = lines.join("\n");
expect(joined).toContain("cloud provider dashboard");
});
});

describe("SIGHUP", () => {
it("should mention terminal disconnection", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("terminal connection");
expect(joined).toContain("SIGHUP");
});

it("should suggest tmux/screen", () => {
const lines = getSignalGuidance("SIGHUP");
const joined = lines.join("\n");
expect(joined).toContain("tmux");
});
});

describe("unknown signal", () => {
it("should show the signal name for unknown signals", () => {
const lines = getSignalGuidance("SIGUSR1");
const joined = lines.join("\n");
expect(joined).toContain("SIGUSR1");
});

it("should always return a non-empty array", () => {
const lines = getSignalGuidance("SIGFOO");
expect(lines.length).toBeGreaterThan(0);
});
});

describe("return type", () => {
it("should always return string arrays", () => {
const signals = ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGUSR1"];
for (const sig of signals) {
const lines = getSignalGuidance(sig);
expect(Array.isArray(lines)).toBe(true);
for (const line of lines) {
expect(typeof line).toBe("string");
}
}
});

it("should produce different output for each handled signal", () => {
const sigkill = getSignalGuidance("SIGKILL").join("\n");
const sigterm = getSignalGuidance("SIGTERM").join("\n");
const sigint = getSignalGuidance("SIGINT").join("\n");
const sighup = getSignalGuidance("SIGHUP").join("\n");
expect(sigkill).not.toBe(sigterm);
expect(sigterm).not.toBe(sigint);
expect(sigint).not.toBe(sighup);
});
});
});

describe("buildRetryCommand", () => {
it("should return simple command without prompt", () => {
expect(buildRetryCommand("claude", "sprite")).toBe("spawn claude sprite");
Expand Down
57 changes: 53 additions & 4 deletions cli/src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -672,6 +672,45 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}

export function getSignalGuidance(signal: string): string[] {
switch (signal) {
case "SIGKILL":
return [
"Script was forcibly killed (SIGKILL). Common causes:",
" - Out of memory (OOM killer terminated the process)",
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
" - Check your cloud provider dashboard to stop or delete any unused servers",
];
case "SIGTERM":
return [
"Script was terminated (SIGTERM). Common causes:",
" - The process was stopped by the system or a supervisor",
" - Server shutdown or reboot in progress",
" - Cloud provider terminated the instance (spot/preemptible instance or billing issue)",
];
case "SIGINT":
return [
"Script was interrupted (Ctrl+C).",
"Note: If a server was already created, it may still be running.",
" Check your cloud provider dashboard to stop or delete any unused servers.",
];
case "SIGHUP":
return [
"Script lost its terminal connection (SIGHUP). Common causes:",
" - SSH session disconnected or timed out",
" - Terminal window was closed during execution",
" - Try using a more stable connection or a terminal multiplexer (tmux/screen)",
];
default:
return [
`Script was killed by signal ${signal}.`,
" - The process was terminated by the system or another process",
" - Check your cloud provider dashboard for any orphaned servers",
];
}
}

export function getScriptFailureGuidance(exitCode: number | null, cloud: string, authHint?: string): string[] {
switch (exitCode) {
case 130:
Expand DownExpand Up@@ -746,7 +785,13 @@ function reportScriptFailure(errMsg: string, cloud: string, agent: string, authH
const exitCodeMatch = errMsg.match(/exited with code (\d+)/);
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : null;

const lines = getScriptFailureGuidance(exitCode, cloud, authHint);
// Check for signal-killed messages (e.g. "killed by SIGKILL")
const signalMatch = errMsg.match(/killed by (SIG\w+)/);
const signal = signalMatch ? signalMatch[1] : null;

const lines = signal
? getSignalGuidance(signal)
: getScriptFailureGuidance(exitCode, cloud, authHint);
console.error("");
for (const line of lines) console.error(line);
console.error("");
Expand All@@ -766,7 +811,7 @@ export function isRetryableExitCode(errMsg: string): boolean {
}

function handleUserInterrupt(errMsg: string): void {
if (!errMsg.includes("interrupted by user")) return;
if (!errMsg.includes("interrupted by user") && !errMsg.includes("killed by SIGINT")) return;
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
Expand DownExpand Up@@ -841,13 +886,17 @@ function runBash(script: string, prompt?: string): Promise<void> {
stdio: "inherit",
env,
});
child.on("close", (code: number | null) => {
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (code === 0) resolve();
else {
else if (code !== null) {
const msg = code === 130
? "Script interrupted by user (Ctrl+C)"
: `Script exited with code ${code}`;
reject(new Error(msg));
} else {
// code is null when killed by a signal (SIGKILL, SIGTERM, etc.)
const sig = signal ?? "unknown signal";
reject(new Error(`Script was killed by ${sig}`));
}
});
child.on("error", reject);
Expand Down