fix(auth): continue login when browser launch fails#38
Conversation
|
Claude finished @cloud9c's task in 1m 24s —— View job Code Review —
|
| println!("Opening browser for authentication..."); | ||
| println!("Auth URL: {}", auth_url); | ||
| open::that(&auth_url)?; | ||
| if open_auth_url(&auth_url).is_err() { | ||
| eprintln!("Couldn't open a browser automatically."); | ||
| eprintln!("Open this URL to continue: {}", auth_url); | ||
| } |
There was a problem hiding this comment.
Optional: two things worth considering here.
- The underlying error is fully discarded. The original failure detail (e.g.
Launcher "gio" "open" ... failed with ExitStatus(...)) can be useful when diagnosing why a browser won't open in unusual environments. Consider logging it to stderr at a lower priority so the friendly message stays primary but the cause is still recoverable:
| println!("Opening browser for authentication..."); | |
| println!("Auth URL: {}", auth_url); | |
| open::that(&auth_url)?; | |
| if open_auth_url(&auth_url).is_err() { | |
| eprintln!("Couldn't open a browser automatically."); | |
| eprintln!("Open this URL to continue: {}", auth_url); | |
| } | |
| println!("Opening browser for authentication..."); | |
| if let Err(err) = open_auth_url(&auth_url) { | |
| eprintln!("Couldn't open a browser automatically ({err})."); | |
| eprintln!("Open this URL to continue: {}", auth_url); | |
| } |
- The
Auth URL: {}line (printed unconditionally just above) and theOpen this URL to continue: {}fallback now print the same URL twice on failure. Dropping the unconditionalAuth URL:print — as in the suggestion above — removes the redundancy while keeping the URL visible in the success path via the browser itself. Adjust to taste if the always-onAuth URL:line is intentional.
f0f0321 to
4ba0e9d
Compare
|
Claude finished @cloud9c's task in 1m 13s —— View job Code Review —
|
| println!("Auth URL: {}", auth_url); | ||
| open::that(&auth_url)?; | ||
| if open::that(&auth_url).is_err() { | ||
| eprintln!("Couldn't open a browser automatically."); | ||
| eprintln!("Open this URL to continue: {}", auth_url); | ||
| } |
There was a problem hiding this comment.
Optional (non-blocking) — the fix is correct as-is. Two small refinements:
- The launch error is discarded by
.is_err(). The original detail (e.g.Launcher "gio" "open" ... ExitStatus(...)) is useful when diagnosing odd environments. Capturing it keeps the friendly message primary while preserving the cause. - The URL prints twice on the failure path —
Auth URL:(line 144, unconditional) andOpen this URL to continue:(line 147). Folding the unconditional print into the success/failure branches removes the redundancy.
| println!("Auth URL: {}", auth_url); | |
| open::that(&auth_url)?; | |
| if open::that(&auth_url).is_err() { | |
| eprintln!("Couldn't open a browser automatically."); | |
| eprintln!("Open this URL to continue: {}", auth_url); | |
| } | |
| println!("Opening browser for authentication..."); | |
| if let Err(err) = open::that(&auth_url) { | |
| eprintln!("Couldn't open a browser automatically ({err})."); | |
| eprintln!("Open this URL to continue: {}", auth_url); | |
| } |
4ba0e9d to
cc4f3ae
Compare
|
Claude finished @cloud9c's task in 1m 13s —— View job Code Review —
|
| println!("Opening browser for authentication..."); | ||
| println!("Auth URL: {}", auth_url); | ||
| open::that(&auth_url)?; | ||
| if open::that(&auth_url).is_err() { |
There was a problem hiding this comment.
Optional (non-blocking): .is_err() discards the underlying launch error (e.g. Launcher "gio" "open" ... ExitStatus(...)), which is useful for diagnosing odd headless/SSH environments. Capturing it keeps the friendly message primary while preserving the cause:
| if open::that(&auth_url).is_err() { | |
| if let Err(err) = open::that(&auth_url) { | |
| eprintln!("Couldn't open a browser automatically ({err})."); | |
| eprintln!("Open this URL to continue: {}", auth_url); | |
| } |
cc4f3ae to
183ee5f
Compare
|
Claude finished @cloud9c's task in 1m 4s —— View job Code Review —
|
183ee5f to
3ecc377
Compare
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Claude finished @cloud9c's task in 1m 11s —— View job Code Review —
|
| } | ||
|
|
||
| let server = Server::http(format!("127.0.0.1:{}", port)) |
There was a problem hiding this comment.
This diff also removed println!("Waiting for authorization...") that previously sat just before the for request in server.incoming_requests() loop. After this change the CLI prints nothing while it blocks on the local callback server — the user is left staring at a frozen prompt with no indication anything is happening.
Notably, the PR description's own After example still lists Waiting for authorization... as expected output, so this removal looks unintentional. Suggest restoring it after the server binds:
| } | |
| let server = Server::http(format!("127.0.0.1:{}", port)) | |
| } | |
| let server = Server::http(format!("127.0.0.1:{}", port)) | |
| .map_err(|e| anyhow::anyhow!("Failed to start server: {}", e))?; | |
| println!("Waiting for authorization..."); |
3ecc377 to
949e562
Compare
|
Claude finished @cloud9c's task in 56s —— View job Code Review —
|
949e562 to
084a43b
Compare
|
Claude finished @cloud9c's task in 1m 19s —— View job Code Review —
|
| if open::that(&auth_url).is_err() { | ||
| eprintln!("Couldn't open a browser automatically."); | ||
| eprintln!("Open this URL to continue: {}", auth_url); | ||
| } |
There was a problem hiding this comment.
.is_err() discards the underlying launch error, so the current code prints:
Couldn't open a browser automatically.
Open this URL to continue: https://...
But the PR's own After example shows the error cause on its own indented line:
Couldn't open a browser automatically.
Launcher "xdg-open" "https://..." failed with ExitStatus(unix_wait_status(512))
Open this URL to continue: https://...
The indented cause line is missing because .is_err() drops the error. Surfacing it aids diagnosing odd headless/SSH environments and matches the PR description:
| if open::that(&auth_url).is_err() { | |
| eprintln!("Couldn't open a browser automatically."); | |
| eprintln!("Open this URL to continue: {}", auth_url); | |
| } | |
| if let Err(error) = open::that(&auth_url) { | |
| eprintln!("Couldn't open a browser automatically."); | |
| eprintln!(" {}", error); | |
| eprintln!("Open this URL to continue: {}", auth_url); | |
| } |
|
Claude finished @cloud9c's task in 2m 38s —— View job Code Review —
|
ba6793a to
084a43b
Compare
|
Claude finished @cloud9c's task in 59s —— View job Code Review —
|
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Claude finished @cloud9c's task in 2m 45s —— View job Code Review —
|
|
|
||
| config::check_api_response(resp).await?; | ||
| let resp = config::check_api_response(resp).await?; | ||
| let stat: Stat = resp.json().await?; |
There was a problem hiding this comment.
Behavior change on the default (non-JSON) path. Previously handle_stat_update only called check_api_response(resp).await?; now it unconditionally parses the body as a Stat (let stat: Stat = resp.json().await?) before the json_output branch. This runs even when --json is not passed, so if the update endpoint returns an empty body or a shape that isn't a Stat, the command will now fail where it previously succeeded.
If the parse is only needed for JSON output, gate it:
config::check_api_response(resp).await?;
if json_output {
let stat: Stat = resp.json().await?;
println!("{}", serde_json::to_string_pretty(&stat)?);
return Ok(());
}
println!("✓ Updated stat {}", stat_id);(Note: check_api_response consumes/returns resp, so you'd parse from the returned value as the diff already does — just move it inside the if.) handle_stat_create already had the parse, so it's unaffected; this is specific to update.
| if is_browser_login_unavailable() { | ||
| anyhow::bail!( | ||
| "Browser login requires a local desktop session. In headless, SSH, CI, or cloud-agent environments, set WAVEDASH_TOKEN or pipe a token into `wavedash auth login --token-stdin`." | ||
| ); | ||
| } |
There was a problem hiding this comment.
Worth noting the interaction with the src/auth.rs fallback this PR adds. is_browser_login_unavailable() hard-bails before login_with_browser() runs in CI / WAVEDASH_HEADLESS / SSH / Linux-without-DISPLAY. So the new "print a manual URL and keep the callback server running" fallback in auth.rs is only reachable when the heuristic says a browser is available but open::that still fails (e.g. a Linux desktop with DISPLAY set but a broken gio/xdg-open — the exact case in the PR description). That's a reasonable split, but two consequences:
- In a true headless SSH session the user never sees the manual-URL fallback — they get the bail message instead. That's arguably better UX, just flagging that the PR title ("continue login when browser launch fails") describes the narrower path.
- The fallback path can't be exercised by simply setting
CI=1in a test, since the bail fires first.
No change required — just confirming the layering is intentional.
Summary
wavedash auth loginno longer aborts when the OS browser launcher fails in headless/SSH environments. The CLI now prints a manual URL and keeps the local callback server running so auth can finish from another browser.Before:
After browser-launch failure:
After the user completes the browser flow, login continues normally:
Verification
rustfmt --check src/auth.rsdoppler run --project cli --config dev -- cargo test