fix(cli): cancel auth attempts on ctrl+c (#51484)

This commit is contained in:
Filip
2026-09-26 13:59:17 +02:00
committed by GitHub
parent 00d179015f
commit cefb2968e2
3 changed files with 24 additions and 14 deletions
@@ -195,16 +195,14 @@ export const oauthLogin = Effect.fn("cli.auth.login.oauth")(function* (
return
}
const waiting = spinner()
waiting.start("Waiting for authorization...")
const status = yield* waitForOAuth(client, integration.id, attempt.attemptID).pipe(
Effect.tapCause(() => Effect.sync(() => waiting.stop("Authentication failed", 1))),
)
// Clack's spinner captures Ctrl+C and exits the process directly, which would skip the finalizer that
// cancels the attempt. Waits that can last minutes use plain log lines so Ctrl+C interrupts normally.
log.step("Waiting for authorization...")
const status = yield* waitForOAuth(client, integration.id, attempt.attemptID)
if (status.status === "complete") {
waiting.stop(`Connected to ${integration.name}`)
log.success(`Connected to ${integration.name}`)
return
}
waiting.stop("Authentication failed", 1)
if (status.status === "failed") yield* Effect.fail(new Error(status.message))
yield* Effect.fail(new Error("Authorization expired"))
})
@@ -231,14 +229,21 @@ const commandLogin = Effect.fn("cli.auth.login.command")(function* (
),
).pipe(Effect.ignore),
)
const status = yield* waitForCommand(client, integration.id, started.data.attemptID, (message) =>
progress.message(message.trim() || "Waiting for authentication command..."),
).pipe(Effect.tapCause(() => Effect.sync(() => progress.stop("Authentication failed", 1))))
progress.stop("Authentication command started")
// The status message accumulates the command's stderr; print each completed line once.
let printed = 0
log.step("Waiting for authentication command...")
const status = yield* waitForCommand(client, integration.id, started.data.attemptID, (message) => {
const end = message.lastIndexOf("\n") + 1
if (end <= printed) return
const output = message.slice(printed, end).trim()
printed = end
if (output) log.message(output)
})
if (status.status === "complete") {
progress.stop(`Connected to ${integration.name}`)
log.success(`Connected to ${integration.name}`)
return
}
progress.stop("Authentication failed", 1)
if (status.status === "failed") yield* Effect.fail(new Error(status.message))
yield* Effect.fail(new Error("Authentication expired"))
})
+1
View File
@@ -22,6 +22,7 @@ export const openUrl = Effect.fn("cli.prompt.open-url")(function* (url: string)
export function handlePromptErrors<A, E, R>(effect: Effect.Effect<A, E, R>) {
return effect.pipe(
Effect.onInterrupt(() => Effect.sync(() => cancel("Cancelled"))),
Effect.catchIf(
(error) => error === cancelled,
() =>
+6 -2
View File
@@ -216,7 +216,8 @@ describe("auth command", () => {
expect(requests).toContainEqual({ method: "DELETE", path: `${endpoint}/con_oauth` })
})
test("settles the OAuth spinner when status polling fails", async () => {
test("reports OAuth status polling failures and cancels the attempt", async () => {
let cancelled = false
using server = authServer((request, url) => {
if (url.pathname === "/api/integration") {
return Response.json(
@@ -245,6 +246,7 @@ describe("auth command", () => {
return new Response("Unavailable", { status: 500 })
}
if (url.pathname === "/api/integration/openai/connect/oauth/con_oauth" && request.method === "DELETE") {
cancelled = true
return new Response(null, { status: 204 })
}
return new Response("Not found", { status: 404 })
@@ -252,8 +254,10 @@ describe("auth command", () => {
const result = await cli(["auth", "login", "openai", "--server", server.url.toString()])
expect(result.exitCode).toBe(1)
expect(result.stdout).toContain("Authentication failed")
expect(result.stdout).toContain("Waiting for authorization...")
expect(result.stdout).toContain("UnexpectedStatus: 500")
expect(result.stdout).toContain("Failed")
expect(cancelled).toBe(true)
expect(result.stdout).not.toContain("\n at ")
})