From a21580702556765cfca98fbe0156263f839b9e91 Mon Sep 17 00:00:00 2001 From: Laurence Date: Wed, 3 Jun 2026 17:04:16 +0100 Subject: [PATCH 1/2] fix: release stdin after web login so org selection works Cancel the Enter-to-open-browser stdin reader when device auth completes so huh prompts can accept input on multi-org accounts. Co-authored-by: Cursor --- cmd/auth/login/login.go | 67 ++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/cmd/auth/login/login.go b/cmd/auth/login/login.go index ad3070e..80b7fbf 100644 --- a/cmd/auth/login/login.go +++ b/cmd/auth/login/login.go @@ -2,6 +2,7 @@ package login import ( "bufio" + "context" "errors" "fmt" "os" @@ -13,6 +14,8 @@ import ( "github.com/fosrl/cli/internal/config" "github.com/fosrl/cli/internal/logger" "github.com/fosrl/cli/internal/utils" + "github.com/mattn/go-isatty" + "github.com/muesli/cancelreader" "github.com/pkg/browser" "github.com/spf13/cobra" ) @@ -75,29 +78,58 @@ func loginWithWeb(hostname string) (string, error) { logger.Info("First copy your one-time code: %s", code) logger.Info("Press Enter to open %s in your browser...", baseLoginURL) - // Wait for Enter in a goroutine (non-blocking) and open browser when pressed - go func() { - reader := bufio.NewReader(os.Stdin) - _, err := reader.ReadString('\n') - if err == nil { - // User pressed Enter, open browser - if err := browser.OpenURL(loginURL); err != nil { - // Don't fail if browser can't be opened, just warn - logger.Warning("Failed to open browser automatically") - logger.Info("Please manually visit: %s", baseLoginURL) - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + enterCh := make(chan struct{}, 1) + if isatty.IsTerminal(os.Stdin.Fd()) { + stdin, err := cancelreader.NewReader(os.Stdin) + if err != nil { + logger.Info("Visit %s?code=%s to authorize this device", baseLoginURL, code) + } else { + go func() { + <-ctx.Done() + stdin.Cancel() + }() + go func() { + _, err := bufio.NewReader(stdin).ReadString('\n') + if err != nil { + return + } + select { + case enterCh <- struct{}{}: + case <-ctx.Done(): + } + }() } - }() + } else { + logger.Info("Visit %s?code=%s to authorize this device", baseLoginURL, code) + } + + browserOpened := false + openBrowserOnce := func() { + if browserOpened { + return + } + browserOpened = true + if err := browser.OpenURL(loginURL); err != nil { + logger.Warning("Failed to open browser automatically") + logger.Info("Please manually visit: %s?code=%s", baseLoginURL, code) + } + } // Poll for verification (starts immediately, doesn't wait for Enter) pollInterval := 1 * time.Second startTime := time.Now() maxPollDuration := 5 * time.Minute - var token string - for { - // print + select { + case <-enterCh: + openBrowserOnce() + default: + } + logger.Debug("Polling for device web auth verification...") // Check if code has expired if time.Now().After(expiresAt) { @@ -122,12 +154,11 @@ func loginWithWeb(hostname string) (string, error) { // Check verification status if pollResp.Verified { - token = pollResp.Token - if token == "" { + if pollResp.Token == "" { logger.Error("Verification succeeded but no token received") return "", fmt.Errorf("verification succeeded but no token received") } - return token, nil + return pollResp.Token, nil } // Check for expired or not found messages From d28fe46a6114dc467b45aa20156bdf7546070423 Mon Sep 17 00:00:00 2001 From: Laurence Date: Wed, 3 Jun 2026 17:10:05 +0100 Subject: [PATCH 2/2] refactor: reuse loginURL in web login info messages Build the device auth URL once and reference it in all user-facing logs. Co-authored-by: Cursor --- cmd/auth/login/login.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cmd/auth/login/login.go b/cmd/auth/login/login.go index 80b7fbf..95ad77b 100644 --- a/cmd/auth/login/login.go +++ b/cmd/auth/login/login.go @@ -69,14 +69,12 @@ func loginWithWeb(hostname string) (string, error) { // Calculate expiry time from relative seconds expiresAt := time.Now().Add(time.Duration(startResp.ExpiresInSeconds) * time.Second) - // Build the base login URL (without query parameter) for display - baseLoginURL := fmt.Sprintf("%s/auth/login/device", strings.TrimSuffix(hostname, "/")) - // Build the login URL with code as query parameter for browser - loginURL := fmt.Sprintf("%s?code=%s", baseLoginURL, code) + // Build the device login URL with the one-time code + loginURL := fmt.Sprintf("%s/auth/login/device?code=%s", strings.TrimSuffix(hostname, "/"), code) // Display code and instructions (similar to GH CLI format) logger.Info("First copy your one-time code: %s", code) - logger.Info("Press Enter to open %s in your browser...", baseLoginURL) + logger.Info("Press Enter to open %s in your browser...", loginURL) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -85,7 +83,7 @@ func loginWithWeb(hostname string) (string, error) { if isatty.IsTerminal(os.Stdin.Fd()) { stdin, err := cancelreader.NewReader(os.Stdin) if err != nil { - logger.Info("Visit %s?code=%s to authorize this device", baseLoginURL, code) + logger.Info("Visit %s to authorize this device", loginURL) } else { go func() { <-ctx.Done() @@ -103,7 +101,7 @@ func loginWithWeb(hostname string) (string, error) { }() } } else { - logger.Info("Visit %s?code=%s to authorize this device", baseLoginURL, code) + logger.Info("Visit %s to authorize this device", loginURL) } browserOpened := false @@ -114,7 +112,7 @@ func loginWithWeb(hostname string) (string, error) { browserOpened = true if err := browser.OpenURL(loginURL); err != nil { logger.Warning("Failed to open browser automatically") - logger.Info("Please manually visit: %s?code=%s", baseLoginURL, code) + logger.Info("Please manually visit: %s", loginURL) } }