Skip to content

Commit 836aeb4

Browse files
committed
feat: show a QR code during OAuth login
1 parent 7b3d4ce commit 836aeb4

18 files changed

Lines changed: 226 additions & 26 deletions

File tree

‎bun.lock‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/cli/src/commands/handlers/auth/login.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { intro, log, outro, select, spinner, text } from "@clack/prompts"
22
import { Effect, Option } from "effect"
33
import type { FormAnswer, IntegrationInfo, OpenCodeClient } from "@opencode/client"
4+
import { renderAuthQr } from "@opencode/tui/util/qr"
45
import { Commands } from "../../commands"
56
import { Runtime } from "../../../framework/runtime"
67
import { selectIntegration, type IntegrationChoice } from "../../../ui/integration-picker"
@@ -193,7 +194,17 @@ export const oauthLogin = Effect.fn("cli.auth.login.oauth")(function* (
193194
)
194195
progress.stop("Authorization started")
195196
log.info(attempt.instructions)
196-
log.info(attempt.url)
197+
const qr = renderAuthQr(attempt.url, {
198+
device: attempt.device,
199+
tty: process.stdout.isTTY === true,
200+
columns: process.stdout.columns,
201+
contrast: true,
202+
})
203+
if (!qr || !attempt.instructions.includes(attempt.url)) log.info(attempt.url)
204+
if (qr) {
205+
log.info("Mobile sign-in")
206+
process.stdout.write(`${qr}\n`)
207+
}
197208
if (process.stdin.isTTY && process.stdout.isTTY) yield* openUrl(attempt.url)
198209

199210
if (attempt.mode === "code") {

‎packages/cli/src/commands/handlers/pair.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { EOL } from "os"
22
import { Effect, Option, Schedule } from "effect"
33
import { Service } from "@opencode/client/effect/service"
44
import { OpenCode } from "@opencode/client/promise"
5-
import { renderUnicodeCompact } from "uqr"
5+
import { renderQr } from "@opencode/tui/util/qr"
66
import { Commands } from "../commands"
77
import { Runtime } from "../../framework/runtime"
88
import { RemoteTunnel } from "../../services/remote-tunnel"
@@ -37,7 +37,7 @@ export default Runtime.handler(
3737
? [
3838
"",
3939
// uqr separates rows with "\n" on every platform, so splitting on EOL ("\r\n" on Windows) indents only the first row.
40-
renderUnicodeCompact(JSON.stringify({ code: pairing.code, urls: remote }), {
40+
renderQr(JSON.stringify({ code: pairing.code, urls: remote }), {
4141
border: 2,
4242
})
4343
.split("\n")

‎packages/client/src/promise/generated/types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ export type IntegrationAttempt = {
674674
url: string
675675
instructions: string
676676
mode: "auto" | "code"
677+
device?: boolean
677678
time: { created: number; expires: number }
678679
}
679680

‎packages/core/src/integration.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export type OAuthAuthorization = {
6161
readonly url: string
6262
readonly instructions: string
6363
readonly expiresAt?: number
64+
readonly device?: boolean
6465
} & (
6566
| {
6667
readonly mode: "auto"
@@ -630,6 +631,7 @@ const layer = Layer.effect(
630631
url: authorization.url,
631632
instructions: authorization.instructions,
632633
mode: authorization.mode,
634+
device: authorization.device === true,
633635
time,
634636
})
635637
})

‎packages/core/src/plugin/provider/github-copilot.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const oauth = (app: App.Info) =>
149149

150150
return {
151151
mode: "auto" as const,
152+
device: true,
152153
url: device.verification_uri,
153154
instructions: `Enter code: ${device.user_code}`,
154155
callback: poll(interval),

‎packages/core/src/plugin/provider/openai.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ const headless = (app: App.Info) =>
189189
const interval = Math.max(Number.parseInt(device.interval) || 5, 1) * 1000
190190
return {
191191
mode: "auto" as const,
192+
device: true,
192193
url: `${issuer}/codex/device`,
193194
instructions: `Enter code: ${device.user_code}`,
194195
callback: Effect.gen(function* () {

‎packages/core/src/plugin/provider/opencode.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ function oauth(http: HttpClient.HttpClient) {
135135
})
136136
return {
137137
mode: "auto" as const,
138+
device: true,
138139
url: verification.href,
139140
instructions: `Enter code: ${device.user_code}`,
140141
callback: poll(http, server, device.device_code, Duration.seconds(device.interval)),

‎packages/core/src/plugin/provider/xai.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const device = (app: App.Info) =>
6161
const lifetime = positiveSeconds(value.expires_in, 0)
6262
return {
6363
mode: "auto" as const,
64+
device: true,
6465
url: value.verification_uri_complete ?? value.verification_uri,
6566
instructions: `Open ${value.verification_uri} on any device and enter code: ${value.user_code}`,
6667
...(lifetime ? { expiresAt: created + lifetime * 1000 } : {}),

‎packages/plugin/src/effect/integration.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type IntegrationOAuthAuthorization = {
5151
readonly url: string
5252
readonly instructions: string
5353
readonly expiresAt?: number
54+
readonly device?: boolean
5455
} & (
5556
| {
5657
readonly mode: "auto"

0 commit comments

Comments
 (0)