Skip to content
Draft
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 docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ A `ctx.packages` operation (an install, or running a package through the manager

### CLI.PROMPT_CANCELLED

The user cancelled a prompt: EOF on stdin at a line-rendered prompt, a clack cancel (Ctrl-C at the prompt UI), an abort during a browserWait poll, or — via the service commands' `userCancelledError` — consent declined interactively. Settles with exit 3, the cancellation code, instead of 2. Meta: none.
The user cancelled a prompt: EOF on stdin at a line-rendered prompt, a clack cancel (Ctrl-C at the prompt UI), an abort during a browserWait poll, Ctrl-C at the `prisma auth login` paste prompt, or — via the service commands' `userCancelledError` — consent declined interactively. Settles with exit 3, the cancellation code, instead of 2. Meta: none.

### CLI.PROMPT_INVALID

Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,15 @@ async function readPastedCallbackUrl(
signal: options.signal,
});
} catch (error) {
// The browser callback won the race and aborted us. Stop prompting.
if ((error as { name?: string } | null)?.name === "AbortError") return null;
if ((error as { name?: string } | null)?.name === "AbortError") {
// Our signal aborted: the login is over.
if (options.signal.aborted) return null;
// readline's own abort is the user's Ctrl-C (raw mode, no SIGINT).
throw new CliStructuredError(
"CLI.PROMPT_CANCELLED",
"Sign-in was cancelled before it completed.",
);
}
throw error;
}

Expand Down
31 changes: 30 additions & 1 deletion packages/cli/tests/auth-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,28 @@ describe("auth login remote paste flow", () => {
expect(result.handleCallbackCalls).toBe(2);
});

it("reports Ctrl-C at the paste prompt as a cancelled prompt", async () => {
await expect(
runLogin({
ttyInput: true,
terminal: true,
openUrl: () => {},
pasteLines: [CTRL_C],
}),
).rejects.toMatchObject({ code: "CLI.PROMPT_CANCELLED" });
});

it("completes through the browser callback while the paste prompt is still waiting", async () => {
const result = await runLogin({
ttyInput: true,
openUrl: async (redirectUri) => {
await fetch(`${redirectUri}?code=code_123&state=state_123`);
},
});

expect(result.handleCallbackCalls).toBe(1);
});

it("surfaces a browser-launch failure when stdin is not a TTY", async () => {
await expect(
runLogin({
Expand Down Expand Up @@ -401,8 +423,12 @@ describe("auth login remote paste flow", () => {
const PASTE_CALLBACK_URL =
"http://localhost:9999/auth/callback?code=code_123&state=state_123";

const CTRL_C = "\x03";

async function runLogin(options: {
ttyInput: boolean;
/** readline only sees keypresses such as Ctrl-C when the output is a TTY. */
terminal?: boolean;
openUrl: (redirectUri: string) => Promise<unknown> | unknown;
pasteLines?: string[];
}): Promise<{ handleCallbackCalls: number; output: string }> {
Expand Down Expand Up @@ -458,6 +484,9 @@ async function runLogin(options: {
// line at once loses all but the first across re-prompts.
const pasteLines = [...(options.pasteLines ?? [])];
const output = new PassThrough();
if (options.terminal) {
(output as unknown as { isTTY: boolean }).isTTY = true;
}
const chunks: string[] = [];
output.on("data", (chunk) => {
const text = chunk.toString();
Expand All @@ -467,7 +496,7 @@ async function runLogin(options: {
pasteLines.length > 0
) {
const line = pasteLines.shift() as string;
queueMicrotask(() => input.write(`${line}\n`));
queueMicrotask(() => input.write(line === CTRL_C ? line : `${line}\n`));
}
});

Expand Down
Loading