memstead_cli/commands/
login.rs1use clap::Parser;
9use serde_json::json;
10
11use crate::CliError;
12use crate::auth::{credentials, device_flow};
13use crate::output::{ExitKind, print_json, print_markdown};
14use crate::registry;
15use crate::setup::CliContext;
16
17#[derive(Parser, Debug)]
18pub struct Args {
19 #[arg(long, value_name = "URL")]
21 pub registry: Option<String>,
22}
23
24pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
25 let base = registry::registry_base(args.registry.as_deref());
26 let host = registry::registry_host(&base);
27 let client = registry::build_http()?;
28
29 let outcome = device_flow::run(
30 &client,
31 device_flow::MEMSTEAD_GITHUB_CLIENT_ID,
32 device_flow::MEMSTEAD_GITHUB_SCOPE,
33 |url| {
34 let _ = device_flow::open_browser(url);
35 },
36 )
37 .map_err(|e| {
38 CliError::new(
39 ExitKind::Generic,
40 "LOGIN_FAILED",
41 format!("login failed: {e}"),
42 )
43 })?;
44
45 let user_login = fetch_login(&client, &outcome.access_token).unwrap_or_default();
49
50 let entry = credentials::Entry::new(
51 outcome.access_token.clone(),
52 user_login.clone(),
53 outcome.scopes.clone(),
54 );
55 credentials::save_for(&host, entry)?;
56
57 if ctx.json {
58 print_json(&json!({
59 "ok": true,
60 "registry": host,
61 "user_login": user_login,
62 "scopes": outcome.scopes,
63 }))?;
64 } else {
65 let who = if user_login.is_empty() {
66 "authorized".to_string()
67 } else {
68 format!("logged in as {user_login}")
69 };
70 print_markdown(&format!("# {who}\n\n- Registry: {host}"));
71 }
72
73 Ok(())
74}
75
76fn fetch_login(client: &reqwest::blocking::Client, token: &str) -> anyhow::Result<String> {
80 let base = std::env::var("MEMSTEAD_GITHUB_API_BASE")
81 .unwrap_or_else(|_| "https://api.github.com".to_string());
82 let url = format!("{}/user", base.trim_end_matches('/'));
83 let resp = client
84 .get(url)
85 .bearer_auth(token)
86 .header("accept", "application/vnd.github+json")
87 .send()?;
88 if !resp.status().is_success() {
89 anyhow::bail!("GitHub /user returned {}", resp.status());
90 }
91 #[derive(serde::Deserialize)]
92 struct User {
93 login: String,
94 }
95 let user: User = resp.json()?;
96 Ok(user.login)
97}