login: align browser-flow output with the device flow · Entire

login: align browser-flow output with the device flow

9bf3b25→main·

toothbrush·1mo ago·3 files·+37 added/-17 removed

Make the default loopback flow read like --device: show "Login URL:",
pause on "Press Enter to open in browser...", then print "Waiting for
sign-in... " with no newline so persistLogin's "Login complete." lands
on the same line. waitForEnter now short-circuits under test
(interactive.UnderTest) like openBrowser, so forcing interactive mode in
tests no longer blocks on a real /dev/tty read.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

eec99cd78cbaView transcript

Changes

3

332 unmodified lines

333
334
335
336
337
336
337
338
339
340
341
342
341
342
343
344
345
1 unmodified line

347
348
349
350
351
352
353
350
351
352
353
354

332 unmodified lines

return "", ""
// waitForBrowserPrompt reads login stdout until it finds the
// "Opening <url> in your browser to sign in..." line and returns the URL.
// waitForBrowserPrompt reads login stdout until it finds the "Login URL:"
// line (the browser flow mirrors the device flow's prompt) and returns the
// URL. The browser flow has no "Device code:" line, so this scans for the
// URL alone rather than reusing waitForLoginPrompt.
func waitForBrowserPrompt(t *testing.T, stdout *bufio.Reader) string {

t.Helper()

const prefix = "Opening "
    const suffix = " in your browser to sign in..."
    deadline := time.Now().Add(10 * time.Second)
    for time.Now().Before(deadline) {
        line, err := stdout.ReadString('\n')
        if err != nil {
            t.Fatalf("read login output: %v", err)
        }
        line = strings.TrimSpace(line)
        if after, ok := strings.CutPrefix(line, prefix); ok {
            if authURL, ok := strings.CutSuffix(after, suffix); ok {
                return authURL
            }
        }
        if after, ok := strings.CutPrefix(line, "Login URL:"); ok {
            return strings.TrimSpace(after)
        }
    }
}

Mcmd/entire/cli/integration_test/login_test.go+6/-8

128 unmodified lines

129
130
131
132
133
134
135
132
133
134
3 unmodified lines

138
139
140
141
142
143
144
145
146
147
3 unmodified lines

151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
158
159
172
173
162
174
175
176
177
164 unmodified lines

342
343
344
345
346
347
348
349
350
351
352
353
354

128 unmodified lines

return persistLogin(outW, errW, client.BaseURL(), token, refreshToken)

// runBrowserLogin runs the loopback authorization-code flow: open the
// authorization URL in the user's browser, wait for the redirect back to
// the local listener, then exchange the code for tokens. Shares the token
// validation + persistence tail with runLogin via persistLogin.
// shouldUseBrowserLogin reports whether `entire login` should use the
// loopback authorization-code (browser) flow. The browser flow is the
// default but needs a local browser + reachable 127.0.0.1, so it's only
3 unmodified lines

return !useDevice && canPrompt

// runBrowserLogin runs the loopback authorization-code flow: open the
// authorization URL in the user's browser, wait for the redirect back to
// the local listener, then exchange the code for tokens. Shares the token
// validation + persistence tail with runLogin via persistLogin.
func runBrowserLogin(ctx context.Context, outW, errW io.Writer, client browserAuthClient, openURL browserOpenFunc) error {
    flow, err := client.StartBrowserAuth(ctx)
    if err != nil {
3 unmodified lines
    }

// covers the error paths before Wait runs.
    defer func() { _ = flow.Close() }()

// Mirror the device flow's interactive shape: show the URL, pause on
    // Enter before opening the browser, then wait on the same line so
    // persistLogin's "Login complete." reads "Waiting for sign-in...
    // Login complete." runBrowserLogin is only reached interactively (see
    // shouldUseBrowserLogin), so the Enter prompt is unconditional here.
    authURL := flow.AuthorizationURL()
    fmt.Fprintf(outW, "Login URL:   %s\n\n", authURL)
    fmt.Fprintf(outW, "Press Enter to open in browser...")

// Read from /dev/tty so we get a real keypress and don't consume piped stdin.
    if err := waitForEnter(ctx); err != nil {
        return fmt.Errorf("wait for input: %w", err)
    }

fmt.Fprintln(outW)
    if err := openURL(ctx, authURL); err != nil {
        fmt.Fprintf(errW, "Warning: failed to open browser: %v\n", err)
        fmt.Fprintf(outW, "Open this URL in your browser to sign in: %s\n", authURL)
    } else {
        fmt.Fprintf(outW, "Opening %s in your browser to sign in...\n", authURL)
    }

fmt.Fprintln(outW, "Waiting for sign-in to complete...")
    fmt.Fprint(outW, "Waiting for sign-in... ")

code, err := flow.Wait(ctx)
    if err != nil {
164 unmodified lines

// If /dev/tty cannot be opened (e.g. on Windows), it returns immediately.
// Returns ctx.Err() if the context is cancelled before the user presses Enter.
func waitForEnter(ctx context.Context) error {
    // Under test (in-process go test, or a child with ENTIRE_TEST_TTY set)
    // don't block on a real /dev/tty read — tests that force interactive
    // mode still need this prompt to return. Mirrors openBrowser's guard.
    if interactive.UnderTest() {
        return nil
    }

ty, err := os.Open("/dev/tty")
    if err != nil {
        return nil //nolint:nilerr // tty unavailable (e.g. Windows) — skip prompt silently