refactor: extract error guidance data structures into separate module (#1335)

Extracted EXIT_CODE_GUIDANCE and SIGNAL_GUIDANCE from commands.ts into a
new guidance-data.ts module. This reduces commands.ts complexity by 100+ lines,
making error handling logic more maintainable and focused.

Changes:
- New file: cli/src/guidance-data.ts (116 lines) with error/signal guidance data
- Refactored: commands.ts now 100 lines shorter, imports guidance data
- Improved: Exit code 1 handling to avoid circular dependency with credentialHints

The extracted module is a pure data file focused on error messages and guidance,
separate from the command execution logic.

Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-16 19:45:28 -08:00 committed by GitHub
parent 6be328c314
commit 2b87735e3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 127 additions and 111 deletions

View file

@ -19,6 +19,7 @@ import pkg from "../package.json" with { type: "json" };
const VERSION = pkg.version;
import { validateIdentifier, validateScriptContent, validatePrompt } from "./security.js";
import { saveSpawnRecord, filterHistory, clearHistory, markRecordDeleted, getActiveServers, type SpawnRecord, type VMConnection } from "./history.js";
import { buildDashboardHint, EXIT_CODE_GUIDANCE, SIGNAL_GUIDANCE, type ExitCodeEntry, type SignalEntry } from "./guidance-data.js";
// ── Helpers ────────────────────────────────────────────────────────────────────
@ -915,116 +916,6 @@ export function credentialHints(cloud: string, authHint?: string, verb = "Missin
return lines;
}
function buildDashboardHint(dashboardUrl?: string): string {
return dashboardUrl
? ` - Check your dashboard: ${pc.cyan(dashboardUrl)}`
: " - Check your cloud provider dashboard to stop or delete any unused servers";
}
interface SignalEntry {
header: string;
causes: string[];
includeDashboard: boolean;
}
interface ExitCodeEntry {
header: string;
lines: string[];
includeDashboard: boolean;
specialHandling?: (cloud: string, authHint?: string, dashboardUrl?: string) => string[];
}
const EXIT_CODE_GUIDANCE: Record<number, ExitCodeEntry> = {
130: {
header: "Script was interrupted (Ctrl+C).",
lines: ["Note: If a server was already created, it may still be running."],
includeDashboard: true,
},
137: {
header: "Script was killed (likely by the system due to timeout or out of memory).",
lines: [
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
],
includeDashboard: true,
},
255: {
header: "SSH connection failed. Common causes:",
lines: [
" - Server is still booting (wait a moment and retry)",
" - Firewall blocking SSH port 22",
" - Server was terminated before the session started",
],
includeDashboard: false,
},
127: {
header: "A required command was not found. Check that these are installed:",
lines: [" - bash, curl, ssh, jq"],
includeDashboard: false,
specialHandling: (cloud) => [` - Cloud-specific CLI tools (run ${pc.cyan(`spawn ${cloud}`)} for details)`],
},
126: {
header: "A command was found but could not be executed (permission denied).",
lines: [
" - A downloaded binary may lack execute permissions",
" - The script may require root/sudo access",
` - Report it if this persists: ${pc.cyan(`https://github.com/OpenRouterTeam/spawn/issues`)}`,
],
includeDashboard: false,
},
2: {
header: "Shell syntax or argument error. This is likely a bug in the script.",
lines: [` Report it at: ${pc.cyan(`https://github.com/OpenRouterTeam/spawn/issues`)}`],
includeDashboard: false,
},
1: {
header: "Common causes:",
lines: [],
includeDashboard: true,
specialHandling: (cloud, authHint) => [
...credentialHints(cloud, authHint),
" - Cloud provider API error (quota, rate limit, or region issue)",
" - Server provisioning failed (try again or pick a different region)",
],
},
};
const SIGNAL_GUIDANCE: Record<string, SignalEntry> = {
SIGKILL: {
header: "Script was forcibly killed (SIGKILL). Common causes:",
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",
],
includeDashboard: true,
},
SIGTERM: {
header: "Script was terminated (SIGTERM). Common causes:",
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)",
],
includeDashboard: true,
},
SIGINT: {
header: "Script was interrupted (Ctrl+C).",
causes: [
"Note: If a server was already created, it may still be running.",
],
includeDashboard: true,
},
SIGHUP: {
header: "Script lost its terminal connection (SIGHUP). Common causes:",
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)",
],
includeDashboard: false,
},
};
export function getSignalGuidance(signal: string, dashboardUrl?: string): string[] {
const entry = SIGNAL_GUIDANCE[signal];
@ -1062,7 +953,16 @@ export function getScriptFailureGuidance(exitCode: number | null, cloud: string,
// Apply special handling if defined for this exit code
if (entry.specialHandling) {
lines.push(...entry.specialHandling(cloud, authHint, dashboardUrl));
// Exit code 1 special case: needs credentialHints
if (exitCode === 1) {
lines.push(
...credentialHints(cloud, authHint),
" - Cloud provider API error (quota, rate limit, or region issue)",
" - Server provisioning failed (try again or pick a different region)"
);
} else {
lines.push(...entry.specialHandling(cloud, authHint, dashboardUrl));
}
}
if (entry.includeDashboard) {

116
cli/src/guidance-data.ts Normal file
View file

@ -0,0 +1,116 @@
/**
* Guidance data structures for error and signal reporting.
* Extracted from commands.ts to improve maintainability and reduce cognitive complexity.
*/
import pc from "picocolors";
export interface SignalEntry {
header: string;
causes: string[];
includeDashboard: boolean;
}
export interface ExitCodeEntry {
header: string;
lines: string[];
includeDashboard: boolean;
specialHandling?: (cloud: string, authHint?: string, dashboardUrl?: string) => string[];
}
export function buildDashboardHint(dashboardUrl?: string): string {
return dashboardUrl
? ` - Check your dashboard: ${pc.cyan(dashboardUrl)}`
: " - Check your cloud provider dashboard to stop or delete any unused servers";
}
// Note: Exit code 1 uses specialHandling because it needs credentialHints from commands.ts to avoid circular deps
export const EXIT_CODE_GUIDANCE: Record<number, ExitCodeEntry> = {
130: {
header: "Script was interrupted (Ctrl+C).",
lines: ["Note: If a server was already created, it may still be running."],
includeDashboard: true,
},
137: {
header: "Script was killed (likely by the system due to timeout or out of memory).",
lines: [
" - The server may not have enough RAM for this agent",
" - Try a larger instance size or a different cloud provider",
],
includeDashboard: true,
},
255: {
header: "SSH connection failed. Common causes:",
lines: [
" - Server is still booting (wait a moment and retry)",
" - Firewall blocking SSH port 22",
" - Server was terminated before the session started",
],
includeDashboard: false,
},
127: {
header: "A required command was not found. Check that these are installed:",
lines: [" - bash, curl, ssh, jq"],
includeDashboard: false,
specialHandling: (cloud) => [` - Cloud-specific CLI tools (run ${pc.cyan(`spawn ${cloud}`)} for details)`],
},
126: {
header: "A command was found but could not be executed (permission denied).",
lines: [
" - A downloaded binary may lack execute permissions",
" - The script may require root/sudo access",
` - Report it if this persists: ${pc.cyan(`https://github.com/OpenRouterTeam/spawn/issues`)}`,
],
includeDashboard: false,
},
2: {
header: "Shell syntax or argument error. This is likely a bug in the script.",
lines: [` Report it at: ${pc.cyan(`https://github.com/OpenRouterTeam/spawn/issues`)}`],
includeDashboard: false,
},
1: {
header: "Common causes:",
lines: [],
includeDashboard: true,
// specialHandling is set in getScriptFailureGuidance in commands.ts
// to avoid circular dependency with credentialHints
specialHandling: () => [],
},
};
export const SIGNAL_GUIDANCE: Record<string, SignalEntry> = {
SIGKILL: {
header: "Script was forcibly killed (SIGKILL). Common causes:",
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",
],
includeDashboard: true,
},
SIGTERM: {
header: "Script was terminated (SIGTERM). Common causes:",
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)",
],
includeDashboard: true,
},
SIGINT: {
header: "Script was interrupted (Ctrl+C).",
causes: [
"Note: If a server was already created, it may still be running.",
],
includeDashboard: true,
},
SIGHUP: {
header: "Script lost its terminal connection (SIGHUP). Common causes:",
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)",
],
includeDashboard: false,
},
};