feat(coder-lite): add /login command and share login helper

71520c7de7ed · AtlantisPleb · · parent 26b698d44e7f

feat(coder-lite): add /login command and share login helper

Refactored the GitHub device-authorization flow into a shared
 helper so it can be triggered both at startup (press Enter)
and on demand with the new  command.  opens the browser,
polls the token endpoint, and reports success or failure in the
transcript.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified crates/coder-lite/src/commands.rs
  • modified crates/coder-lite/src/interactive.rs

Diff

2 files changed, +30 -5

crates/coder-lite/src/commands.rs modified +15 -1

@@ -32,6 +32,7 @@ pub const COMMANDS: &[(&str, &str)] = &[

32 32
        "write the transcript to ~/.openagents/exports as an ATIF document",
33 33
    ),
34 34
    ("help", "list these commands and the keys"),
35
    ("login", "log in with GitHub and store the token"),
35 36
    (
36 37
        "resume",
37 38
        "coding-agent sessions other tools left on this machine: /resume, /resume <number>",

@@ -65,7 +66,7 @@ pub fn names() -> Vec<&'static str> {

65 66
66 67
/// Whether `name` is one this module runs.
67 68
pub fn handles(name: &str) -> bool {
68
    matches!(name, "clear" | "diff" | "export" | "help" | "resume" | "run")
69
    matches!(name, "clear" | "diff" | "export" | "help" | "login" | "resume" | "run")
69 70
}
70 71
71 72
/// Run one `/` line. `line` still carries its leading slash.

@@ -86,6 +87,7 @@ pub fn run(ui: &mut CoderUi, line: &str, tx: &Sender<Control>, cwd: &Path) {

86 87
            ui.scroll_override = None;
87 88
        }
88 89
        "export" => crate::interactive::export(ui),
90
        "login" => spawn_login(ui, tx),
89 91
        "diff" => spawn_diff(ui, arguments, tx, cwd),
90 92
        "run" => spawn_run(ui, &rest, tx, cwd),
91 93
        "resume" => spawn_resume(ui, &arguments, tx, cwd),

@@ -101,6 +103,18 @@ fn output(ui: &mut CoderUi, text: &str) {

101 103
    ui.scroll_override = None;
102 104
}
103 105
106
fn spawn_login(ui: &mut CoderUi, tx: &Sender<Control>) {
107
    output(ui, "Opening GitHub login in your browser...");
108
    let tx = tx.clone();
109
    tokio::spawn(async move {
110
        let text = match crate::interactive::do_login().await {
111
            Ok(message) => message,
112
            Err(error) => format!("Login failed: {error}"),
113
        };
114
        let _ = tx.send(Control::Output(text));
115
    });
116
}
117
104 118
fn help() -> String {
105 119
    let mut lines = vec!["**Commands**".to_string(), String::new()];
106 120
    for (name, what) in COMMANDS {
crates/coder-lite/src/interactive.rs modified +15 -4

@@ -227,19 +227,30 @@ async fn ensure_authenticated() -> Result<(), Box<dyn std::error::Error>> {

227 227
    let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
228 228
    tokio::io::AsyncBufReadExt::read_line(&mut stdin, &mut line).await?;
229 229
230
    match do_login().await {
231
        Ok(message) => {
232
            println!("{message}");
233
            Ok(())
234
        }
235
        Err(error) => Err(error),
236
    }
237
}
238
239
/// Start the GitHub device-authorization flow against the current endpoint,
240
/// open the approval URL in the browser, poll for the token, and store it.
241
pub async fn do_login() -> Result<String, Box<dyn std::error::Error>> {
242
    let endpoint = openagents_cli::auth::resolve_endpoint(None, None)?;
230 243
    let client = DeviceClient::new(&endpoint.origin);
231 244
    let scopes: &[String] = &[];
232 245
    let auth = client.start(scopes).await?;
233 246
234
    println!("Opening {} in your browser...", auth.verification_uri_complete);
235 247
    open_browser(&auth.verification_uri_complete);
236
    println!("Waiting for approval...");
237 248
238 249
    let token = client.wait(&auth).await?;
250
    let store = CredentialStore::for_origin(&endpoint.origin);
239 251
    let _ = store.store(&token)?;
240
    println!("Authenticated.");
241 252
242
    Ok(())
253
    Ok(format!("Authenticated for {}.", endpoint.origin))
243 254
}
244 255
245 256
/// The columns the composer soft-wraps to: the frame's width less its border

This page updates live while a promote is in flight · changelog