diff --git a/packages/cli/src/commands/handlers/auth/login.ts b/packages/cli/src/commands/handlers/auth/login.ts
index 746f105346b..2a4859ff190 100644
--- a/packages/cli/src/commands/handlers/auth/login.ts
+++ b/packages/cli/src/commands/handlers/auth/login.ts
@@ -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"))
})
diff --git a/packages/cli/src/ui/prompt.ts b/packages/cli/src/ui/prompt.ts
index 04b2ed716a6..3c54c2202eb 100644
--- a/packages/cli/src/ui/prompt.ts
+++ b/packages/cli/src/ui/prompt.ts
@@ -22,6 +22,7 @@ export const openUrl = Effect.fn("cli.prompt.open-url")(function* (url: string)
export function handlePromptErrors(effect: Effect.Effect) {
return effect.pipe(
+ Effect.onInterrupt(() => Effect.sync(() => cancel("Cancelled"))),
Effect.catchIf(
(error) => error === cancelled,
() =>
diff --git a/packages/cli/test/auth.test.ts b/packages/cli/test/auth.test.ts
index 07d77efe54a..8f33f6a9a35 100644
--- a/packages/cli/test/auth.test.ts
+++ b/packages/cli/test/auth.test.ts
@@ -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 ")
})