fix: show cleanup warning when script is interrupted by Ctrl+C (#723)

Previously, when a user hit Ctrl+C during script execution, the CLI
silently exited with code 130. This left users unaware that a server
may have already been created and could still be running, potentially
incurring charges.

Now shows a warning about orphaned resources before exiting.

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-12 15:00:30 -08:00 committed by GitHub
parent 8140c1b61b
commit 96d96b3ef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 17 additions and 4 deletions

View file

@ -11,7 +11,7 @@ import { loadManifest } from "../manifest";
*
* - Exit code 127: "A required command was not found" (missing bash/curl/ssh/jq)
* - Exit code 126: "A command was found but could not be executed" (permission denied)
* - Exit code 130: Silent exit for Ctrl+C (user interrupt)
* - Exit code 130: Cleanup warning + exit for Ctrl+C (user interrupt)
* - Generic failures: "Common causes" with credential/rate-limit/dependency hints
* - runBash: Sets SPAWN_PROMPT and SPAWN_MODE env vars when prompt is provided
* - runBash: Resolves successfully for exit code 0
@ -23,6 +23,7 @@ const mockManifest = createMockManifest();
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogWarn = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockSpinnerStart = mock(() => {});
@ -38,7 +39,7 @@ mock.module("@clack/prompts", () => ({
log: {
step: mockLogStep,
info: mockLogInfo,
warn: mock(() => {}),
warn: mockLogWarn,
error: mockLogError,
},
intro: mock(() => {}),
@ -59,6 +60,7 @@ describe("execScript bash execution error handling", () => {
beforeEach(async () => {
consoleMocks = createConsoleMocks();
mockLogError.mockClear();
mockLogWarn.mockClear();
mockLogInfo.mockClear();
mockLogStep.mockClear();
mockSpinnerStart.mockClear();
@ -174,7 +176,7 @@ describe("execScript bash execution error handling", () => {
// ── Exit code 130: Ctrl+C (user interrupt) ──────────────────────────────
describe("exit code 130 - user interrupt", () => {
it("should exit silently with code 130 for Ctrl+C", async () => {
it("should exit with code 130 and show cleanup warning for Ctrl+C", async () => {
mockFetchWithScript("exit 130");
await loadManifest(true);
@ -187,10 +189,17 @@ describe("execScript bash execution error handling", () => {
// Should exit with 130
expect(processExitSpy).toHaveBeenCalledWith(130);
// Should NOT show error messages for Ctrl+C
// Should NOT show error messages for Ctrl+C (no "Spawn script failed")
const clackErrors = mockLogError.mock.calls.map((c: any[]) => c.join(" "));
const errorMessages = clackErrors.filter((e: string) => e.includes("Spawn script failed"));
expect(errorMessages).toHaveLength(0);
// Should show warning about orphaned resources
const warnMessages = mockLogWarn.mock.calls.map((c: any[]) => c.join(" "));
const warnText = warnMessages.join("\n");
expect(warnText).toContain("interrupted");
expect(warnText).toContain("server");
expect(warnText).toContain("cloud provider dashboard");
});
});

View file

@ -648,6 +648,10 @@ async function execScript(cloud: string, agent: string, prompt?: string, authHin
} catch (err) {
const errMsg = getErrorMessage(err);
if (errMsg.includes("interrupted by user")) {
console.error();
p.log.warn("Script interrupted (Ctrl+C).");
p.log.warn("If a server was already created, it may still be running.");
p.log.warn(` Check your cloud provider dashboard to stop or delete any unused servers.`);
process.exit(130);
}
lastErr = errMsg;