From 5e81f25f0c8fab8abcfa8166b1e12594b0e216ae Mon Sep 17 00:00:00 2001 From: Alec Fong Date: Wed, 1 Jul 2026 23:57:21 -0700 Subject: [PATCH] fix(auth): always show login URL so headless login can complete On a machine with no display (e.g. an SSH'd server), login hung and timed out after 5 minutes. hasBrowser() returned true because xdg-open was installed, so browser.OpenURL was called; xdg-open printed "no DISPLAY environment variable specified" to stderr but exited 0, so OpenURL returned nil and the CLI printed "Waiting for login to complete in browser..." without ever showing the login URL. With no URL to visit, login could never complete. Follow Claude Code's pattern: always attempt to open the browser best-effort, but always print the login URL as a fallback so the user is never stranded. Discard the launcher's stderr (pkg/browser pipes it to ours) to suppress the confusing "no DISPLAY" noise. This removes the need for platform/display detection, so hasBrowser() is deleted. --- pkg/auth/auth.go | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 66344cee..21de0d85 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -4,9 +4,8 @@ import ( "bufio" "errors" "fmt" + "io" "os" - "os/exec" - "runtime" "strings" "github.com/brevdev/brev-cli/pkg/config" @@ -324,10 +323,20 @@ func (t Auth) LoginWithAPIKey(apiKey string, orgID string) error { return nil } -// showLoginURL displays the login link and CLI alternative for manual navigation. +func init() { + // pkg/browser pipes the launcher's stderr to ours, which leaks confusing + // noise like "xdg-open: no DISPLAY environment variable specified" on + // headless machines. We always print the login URL, so discard it. + browser.Stderr = io.Discard +} + +// showLoginURL prints the login link, framed as a fallback for when the +// browser doesn't open (e.g. headless machine, wrong default browser). func showLoginURL(url string) { urlType := color.New(color.FgCyan, color.Bold).SprintFunc() - fmt.Println("Login here: " + urlType(url)) + fmt.Println("Browser didn't open? Use the URL below to sign in:") + fmt.Println() + fmt.Println(urlType(url)) } func defaultAuthFunc(url, code string) { @@ -337,12 +346,9 @@ func defaultAuthFunc(url, code string) { fmt.Print("\n") } - if hasBrowser() { - if err := browser.OpenURL(url); err == nil { - fmt.Println("Waiting for login to complete in browser...") - return - } - } + // Best-effort: try to open the browser, but always show the URL below so + // the user is never stranded if it doesn't open. + _ = browser.OpenURL(url) showLoginURL(url) fmt.Println("\nWaiting for login to complete...") } @@ -352,21 +358,6 @@ func skipBrowserAuthFunc(url, _ string) { fmt.Println("\nWaiting for login to complete...") } -// hasBrowser reports whether a browser can be opened on the current platform. -func hasBrowser() bool { - if runtime.GOOS == "darwin" { - // macOS always has "open". - return true - } - // Linux: check for a known browser launcher. - for _, name := range []string{"xdg-open", "x-www-browser", "www-browser"} { - if _, err := exec.LookPath(name); err == nil { - return true - } - } - return false -} - func (t Auth) Login(skipBrowser bool) (*LoginTokens, error) { authFunc := defaultAuthFunc if skipBrowser {