fix: keep login fallback prompt visible (#594)

This commit is contained in:
liruifengv 2026-06-09 20:06:07 +08:00 committed by GitHub
parent 0abde8662a
commit f2863af267
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 9 deletions

View file

@ -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.

View file

@ -17,18 +17,17 @@ export async function runLoginFlow(): Promise<never> {
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<never> {
.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);

View file

@ -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<void>;
},
) => {
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'));