Skip to content

Commit 98fcb67

Browse files
betegonclaude
andcommitted
fix(init): handle Ctrl+C during experimental warning gracefully
Catch WizardCancelledError inside confirmExperimental() so that pressing Ctrl+C during the experimental confirm prompt exits cleanly (exitCode 0) instead of propagating an unhandled error. Adds a test for the Ctrl+C path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0f8480f commit 98fcb67

File tree

2 files changed

+39
-7
lines changed

2 files changed

+39
-7
lines changed

src/lib/init/wizard-runner.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,25 @@ function withTimeout<T>(
175175
});
176176
}
177177

178-
/** Returns `true` if the user confirmed, `false` if they declined. */
178+
/**
179+
* Returns `true` if the user confirmed, `false` if they declined or
180+
* cancelled (Ctrl+C). Catches WizardCancelledError so that cancellation
181+
* during the preamble exits cleanly instead of propagating unhandled.
182+
*/
179183
async function confirmExperimental(yes: boolean): Promise<boolean> {
180184
if (yes) {
181185
return true;
182186
}
183-
const proceed = await confirm({
184-
message:
185-
"EXPERIMENTAL: This feature is experimental and may modify your code. Continue?",
186-
});
187-
abortIfCancelled(proceed);
188-
return !!proceed;
187+
try {
188+
const proceed = await confirm({
189+
message:
190+
"EXPERIMENTAL: This feature is experimental and may modify your code. Continue?",
191+
});
192+
abortIfCancelled(proceed);
193+
return !!proceed;
194+
} catch {
195+
return false;
196+
}
189197
}
190198

191199
/**

test/lib/init/wizard-runner.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { MastraClient } from "@mastra/client-js";
2323
import * as banner from "../../../src/lib/banner.js";
2424
// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
2525
import * as auth from "../../../src/lib/db/auth.js";
26+
import { WizardCancelledError } from "../../../src/lib/init/clack-utils.js";
2627
// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
2728
import * as fmt from "../../../src/lib/init/formatters.js";
2829
// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
@@ -275,6 +276,29 @@ describe("runWizard", () => {
275276
expect(process.exitCode).toBe(0);
276277
expect(formatResultSpy).not.toHaveBeenCalled();
277278
});
279+
280+
test("exits cleanly when user cancels experimental warning with Ctrl+C", async () => {
281+
const origIsTTY = process.stdin.isTTY;
282+
Object.defineProperty(process.stdin, "isTTY", {
283+
value: true,
284+
configurable: true,
285+
});
286+
287+
// Simulate Ctrl+C: confirm rejects with WizardCancelledError
288+
// (abortIfCancelled throws this when clack returns the cancel symbol)
289+
confirmSpy.mockRejectedValue(new WizardCancelledError());
290+
291+
await runWizard(makeOptions({ yes: false }));
292+
293+
Object.defineProperty(process.stdin, "isTTY", {
294+
value: origIsTTY,
295+
configurable: true,
296+
});
297+
298+
expect(cancelSpy).toHaveBeenCalledWith("Setup cancelled.");
299+
expect(process.exitCode).toBe(0);
300+
expect(formatResultSpy).not.toHaveBeenCalled();
301+
});
278302
});
279303

280304
describe("connection error", () => {

0 commit comments

Comments
 (0)