From f2863af267b2e7d5ff5b99ff80c95c379a5b0272 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Tue, 9 Jun 2026 20:06:07 +0800 Subject: [PATCH] fix: keep login fallback prompt visible (#594) --- .changeset/login-browser-fallback.md | 5 +++ apps/kimi-code/src/cli/sub/login-flow.ts | 22 +++++++------ apps/kimi-code/test/cli/login.test.ts | 40 ++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 .changeset/login-browser-fallback.md diff --git a/.changeset/login-browser-fallback.md b/.changeset/login-browser-fallback.md new file mode 100644 index 000000000..cc27499f4 --- /dev/null +++ b/.changeset/login-browser-fallback.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix device login to keep the URL and code visible when the browser cannot be opened. diff --git a/apps/kimi-code/src/cli/sub/login-flow.ts b/apps/kimi-code/src/cli/sub/login-flow.ts index 3c1a123d4..005dc1729 100644 --- a/apps/kimi-code/src/cli/sub/login-flow.ts +++ b/apps/kimi-code/src/cli/sub/login-flow.ts @@ -17,18 +17,17 @@ export async function runLoginFlow(): Promise { uiMode: 'cli', }); const controller = new AbortController(); - process.once('SIGINT', () => controller.abort()); + process.once('SIGINT', () => { + controller.abort(); + }); try { const result = await harness.auth.login(undefined, { signal: controller.signal, onDeviceCode: (data) => { const url = data.verificationUriComplete || data.verificationUri; - // Best-effort: try to open the user's default browser at the - // pre-baked URL (which already embeds the user code). Print the - // URL + code as a fallback for headless boxes / when openUrl - // silently fails (it `execFile`s `open`/`xdg-open`/`cmd start` - // with no error handling — see `utils/open-url.ts`). - openUrl(url); + // Print the manual fallback before attempting to open the user's + // browser so headless/browser-opener failures never hide the URL + // and code needed to complete login. process.stderr.write( [ '', @@ -43,15 +42,20 @@ export async function runLoginFlow(): Promise { .filter((line): line is string => line !== undefined) .join('\n'), ); + try { + openUrl(url); + } catch { + // Best effort only: the manual fallback has already been printed. + } }, }); process.stderr.write(`Logged in to ${result.providerName}.\n`); process.exit(0); - } catch (err) { + } catch (error) { if (controller.signal.aborted) { process.stderr.write('Login cancelled.\n'); } else { - const message = err instanceof Error ? err.message : String(err); + const message = error instanceof Error ? error.message : String(error); process.stderr.write(`Login failed: ${message}\n`); } process.exit(1); diff --git a/apps/kimi-code/test/cli/login.test.ts b/apps/kimi-code/test/cli/login.test.ts index 6a8a12b46..6644c7f21 100644 --- a/apps/kimi-code/test/cli/login.test.ts +++ b/apps/kimi-code/test/cli/login.test.ts @@ -122,6 +122,46 @@ describe('kimi login', () => { expect(exitSpy).toHaveBeenCalledWith(0); }); + it('still prints device code prompt when opening the browser fails', async () => { + vi.mocked(openUrl).mockImplementation(() => { + throw new Error('no browser'); + }); + mockLogin.mockImplementation( + async ( + _providerName: string | undefined, + options: { + onDeviceCode?: (data: { + userCode: string; + verificationUri: string; + verificationUriComplete: string; + expiresIn: number | null; + }) => void | Promise; + }, + ) => { + await options.onDeviceCode?.({ + userCode: 'ABCD-EFGH', + verificationUri: 'https://example.com/v', + verificationUriComplete: 'https://example.com/v?code=ABCD-EFGH', + expiresIn: 600, + }); + return { providerName: 'kimi-code', ok: true }; + }, + ); + + const program = new Command('kimi').exitOverride(); + registerLoginCommand(program); + + await expect(program.parseAsync(['node', 'kimi', 'login'])).rejects.toThrow(ExitCalled); + + const writtenChunks = stderrSpy.mock.calls.map((call: unknown[]) => String(call[0])); + expect(writtenChunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true); + expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe( + true, + ); + expect(openUrl).toHaveBeenCalledWith('https://example.com/v?code=ABCD-EFGH'); + expect(exitSpy).toHaveBeenCalledWith(0); + }); + it('exits 1 when auth.login throws', async () => { mockLogin.mockRejectedValue(new Error('boom'));