Skip to content

Commit f2863af

Browse files
authored
fix: keep login fallback prompt visible (#594)
1 parent 0abde86 commit f2863af

3 files changed

Lines changed: 58 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Fix device login to keep the URL and code visible when the browser cannot be opened.

apps/kimi-code/src/cli/sub/login-flow.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@ export async function runLoginFlow(): Promise<never> {
1717
uiMode: 'cli',
1818
});
1919
const controller = new AbortController();
20-
process.once('SIGINT', () => controller.abort());
20+
process.once('SIGINT', () => {
21+
controller.abort();
22+
});
2123
try {
2224
const result = await harness.auth.login(undefined, {
2325
signal: controller.signal,
2426
onDeviceCode: (data) => {
2527
const url = data.verificationUriComplete || data.verificationUri;
26-
// Best-effort: try to open the user's default browser at the
27-
// pre-baked URL (which already embeds the user code). Print the
28-
// URL + code as a fallback for headless boxes / when openUrl
29-
// silently fails (it `execFile`s `open`/`xdg-open`/`cmd start`
30-
// with no error handling — see `utils/open-url.ts`).
31-
openUrl(url);
28+
// Print the manual fallback before attempting to open the user's
29+
// browser so headless/browser-opener failures never hide the URL
30+
// and code needed to complete login.
3231
process.stderr.write(
3332
[
3433
'',
@@ -43,15 +42,20 @@ export async function runLoginFlow(): Promise<never> {
4342
.filter((line): line is string => line !== undefined)
4443
.join('\n'),
4544
);
45+
try {
46+
openUrl(url);
47+
} catch {
48+
// Best effort only: the manual fallback has already been printed.
49+
}
4650
},
4751
});
4852
process.stderr.write(`Logged in to ${result.providerName}.\n`);
4953
process.exit(0);
50-
} catch (err) {
54+
} catch (error) {
5155
if (controller.signal.aborted) {
5256
process.stderr.write('Login cancelled.\n');
5357
} else {
54-
const message = err instanceof Error ? err.message : String(err);
58+
const message = error instanceof Error ? error.message : String(error);
5559
process.stderr.write(`Login failed: ${message}\n`);
5660
}
5761
process.exit(1);

apps/kimi-code/test/cli/login.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,46 @@ describe('kimi login', () => {
122122
expect(exitSpy).toHaveBeenCalledWith(0);
123123
});
124124

125+
it('still prints device code prompt when opening the browser fails', async () => {
126+
vi.mocked(openUrl).mockImplementation(() => {
127+
throw new Error('no browser');
128+
});
129+
mockLogin.mockImplementation(
130+
async (
131+
_providerName: string | undefined,
132+
options: {
133+
onDeviceCode?: (data: {
134+
userCode: string;
135+
verificationUri: string;
136+
verificationUriComplete: string;
137+
expiresIn: number | null;
138+
}) => void | Promise<void>;
139+
},
140+
) => {
141+
await options.onDeviceCode?.({
142+
userCode: 'ABCD-EFGH',
143+
verificationUri: 'https://example.com/v',
144+
verificationUriComplete: 'https://example.com/v?code=ABCD-EFGH',
145+
expiresIn: 600,
146+
});
147+
return { providerName: 'kimi-code', ok: true };
148+
},
149+
);
150+
151+
const program = new Command('kimi').exitOverride();
152+
registerLoginCommand(program);
153+
154+
await expect(program.parseAsync(['node', 'kimi', 'login'])).rejects.toThrow(ExitCalled);
155+
156+
const writtenChunks = stderrSpy.mock.calls.map((call: unknown[]) => String(call[0]));
157+
expect(writtenChunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true);
158+
expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe(
159+
true,
160+
);
161+
expect(openUrl).toHaveBeenCalledWith('https://example.com/v?code=ABCD-EFGH');
162+
expect(exitSpy).toHaveBeenCalledWith(0);
163+
});
164+
125165
it('exits 1 when auth.login throws', async () => {
126166
mockLogin.mockRejectedValue(new Error('boom'));
127167

0 commit comments

Comments
 (0)