Skip to content

Commit f3eccd8

Browse files
committed
fix(auth): always print sign-in URL, fire-and-forget browser open
`codebase auth login` would hang after "opening your browser…" on Macs where `open` silently no-ops and on SSH boxes where xdg-open isn't viable. The flow gated the URL print behind isHeadlessSession() and awaited the open command, so a hung or absent browser deadlocked the prompt. Always print the URL through onManualUrl regardless of session shape, and don't await the browser-open — auto-open stays as best-effort gravy on top of the printed URL. For SSH sessions the CLI also prints the matching `ssh -L` forward command so a remote login just works.
1 parent d3bb8f9 commit f3eccd8

4 files changed

Lines changed: 38 additions & 17 deletions

File tree

‎package-lock.json‎

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

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebase-cli",
3-
"version": "2.0.0-pre.54",
3+
"version": "2.0.0-pre.55",
44
"description": "Codebase CLI — a TypeScript coding agent on the pi-mono runtime. OAuth-aware, any LLM provider, single install.",
55
"keywords": [
66
"ai",

‎src/auth/cli.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,30 @@ async function loginCmd(
113113
out: (m: string) => void,
114114
err: (m: string) => void,
115115
): Promise<number> {
116-
out("opening your browser to sign in…");
117-
out("(if it doesn't open, the URL will be printed)");
118116
try {
119-
const creds = await runOAuthLogin(config);
117+
const creds = await runOAuthLogin(config, {
118+
onManualUrl: (url) => {
119+
out("Open this URL to sign in:");
120+
out("");
121+
out(` ${url}`);
122+
out("");
123+
// SSH callers need to reach the localhost callback on this box.
124+
// Print the port-forward command preemptively so a remote login
125+
// just works — they paste both commands and are done.
126+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) {
127+
const port = new URL(url).searchParams.get("redirect_uri")?.match(/:(\d+)\//)?.[1];
128+
if (port) {
129+
out("Detected SSH session. From your laptop, forward the callback port first:");
130+
out("");
131+
out(` ssh -L ${port}:127.0.0.1:${port} <user>@<host>`);
132+
out("");
133+
}
134+
}
135+
out("Waiting for sign-in…");
136+
},
137+
});
120138
store.save(creds);
139+
out("");
121140
out(`signed in${creds.email ? ` as ${creds.email}` : ""}.`);
122141
return 0;
123142
} catch (e) {

‎src/auth/flow.ts‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -386,17 +386,19 @@ export async function runOAuthLogin(
386386

387387
const callbackPromise = awaitCallback(server, state, config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
388388

389-
if (isHeadlessSession()) {
390-
opts.onManualUrl?.(authUrl, "headless session detected — open the URL manually in your browser");
391-
} else {
392-
try {
393-
await openBrowserFn(authUrl);
394-
} catch (err) {
395-
// DON'T tear down the server: the user can still complete
396-
// sign-in by opening the URL themselves. Surface the URL via
397-
// onManualUrl and continue waiting for the callback.
398-
opts.onManualUrl?.(authUrl, `auto-open failed: ${err instanceof Error ? err.message : String(err)}`);
399-
}
389+
// Always surface the URL to the caller so it's visible even when
390+
// auto-open succeeds. Browser-open auto-detection is unreliable
391+
// (xdg-open hangs on some boxes, `open` on macOS silently no-ops in
392+
// some shells, SSH forwarding makes localhost-callbacks tricky), so
393+
// the printed URL is the primary UX — auto-open is best-effort gravy.
394+
const reason = isHeadlessSession() ? "headless session — open the URL manually" : "open this URL in your browser";
395+
opts.onManualUrl?.(authUrl, reason);
396+
397+
// Fire-and-forget the browser open. Don't await — xdg-open / `open`
398+
// can hang indefinitely on some configurations, and we already gave
399+
// the user the URL above.
400+
if (!isHeadlessSession()) {
401+
openBrowserFn(authUrl).catch(() => undefined);
400402
}
401403

402404
const { code } = await callbackPromise;

0 commit comments

Comments
 (0)