OXYGENHUB

Coverage / src/cli/open.rs 38.0%

27 of 71 lines run, 5 of 22 functions · run #31 on main · run missed

1
//! `spectra open`: sign in to Radar (spec §8.3). The owner's CLI signs a
2
//! request for a one-time link with the owner's key; the link, valid for a
3
//! minute, opens a level-2 browser session.
4
5
use super::identity::{owner_home, recorded_owner};
6
use super::{ui, Ctx};
7
use anyhow::{bail, Context, Result};
8
use spectra_core::ledger::Ledger;
9
use spectra_mcp::client::Client;
10
use std::path::PathBuf;
11
12
/// The sign-in link, landing on `page` if one is named.
131×
fn with_page(url: String, page: Option<&str>) -> String {
141×
    match page.filter(|p| !p.is_empty() && *p != "live") {
150×
        Some(p) => format!("{url}&next={}", p.trim_start_matches('/')),
161×
        None => url,
17
    }
181×
}
19
201×
pub async fn open(ctx: &Ctx, owner_home_flag: Option<PathBuf>, launch: bool, page: Option<&str>) -> Result<i32> {
211×
    let socket = ctx.socket();
221×
    if !Client::alive(&socket).await {
230×
        bail!("Spectra is not serving {} (spectra serve)", ctx.data.display());
241×
    }
251×
    let home = owner_home(ctx, owner_home_flag);
261×
    let ledger = Ledger::open_read_only(&ctx.db()?).await?;
271×
    let owner = recorded_owner(&ledger, &home).await?;
281×
    let r = Client::new(&socket, Some(owner)).send("POST", "/radar/link", &[], Vec::new()).await?;
291×
    if r.status != 200 {
300×
        bail!("the daemon refused the sign-in: {}", String::from_utf8_lossy(&r.body));
311×
    }
321×
    let url = r.json()?.get("url").and_then(|u| u.as_str()).map(String::from).context("no link in the answer")?;
331×
    let url = with_page(url, page);
341×
    println!("{} {}", ui::done("Signed"), ui::subject("a one-time link to Radar, good for a minute"));
351×
    show(&url, launch);
361×
    Ok(0)
371×
}
38
391×
fn show(url: &str, launch: bool) {
401×
    println!("  {}", ui::id(url));
411×
    if launch
420×
        && std::process::Command::new("xdg-open")
430×
            .arg(url)
440×
            .stdout(std::process::Stdio::null())
450×
            .stderr(std::process::Stdio::null())
460×
            .spawn()
470×
            .is_ok()
480×
    {
490×
        println!("  {} {}", ui::note(), ui::dim("opened in your browser"));
501×
    }
511×
}
52
53
/// `spectra open` with Spectra on another machine (spec §8.6): serve Radar
54
/// through a loopback proxy to the pinned server, and sign in through it.
550×
pub async fn open_remote(ctx: &Ctx, owner_home_flag: Option<PathBuf>, launch: bool, port: u16, page: Option<&str>) -> Result<i32> {
560×
    let cfg = ctx.remote()?.context("no server configured")?;
570×
    let home = owner_home(ctx, owner_home_flag);
580×
    let listener = tokio::net::TcpListener::bind(("127.0.0.1", port))
590×
        .await
600×
        .with_context(|| format!("binding 127.0.0.1:{port} for Radar (another `spectra open` running? --port picks another)"))?;
610×
    let port = listener.local_addr()?.port();
620×
    let client = ctx.owner_client(&home).await?.context("no server configured")?;
630×
    let r = client.send("POST", "/radar/link", &[], Vec::new()).await?;
640×
    if r.status != 200 {
650×
        bail!("the server refused the sign-in: {}", String::from_utf8_lossy(&r.body));
660×
    }
670×
    let url = r.json()?.get("url").and_then(|u| u.as_str()).map(String::from).context("no link in the answer")?;
680×
    let local = with_page(spectra_mcp::proxy::local_link(&url, port), page);
690×
    println!("{} {}{}{}", ui::done("Serving"), ui::subject("Radar"), ui::sep(), ui::dim(format!("from {} over pinned TLS", cfg.server)));
700×
    println!("{}", ui::kv("radar", format!("http://127.0.0.1:{port}")));
710×
    println!("{} {}", ui::done("Signed"), ui::subject("a one-time link, good for a minute"));
720×
    show(&local, launch);
730×
    println!("  {} {}", ui::note(), ui::dim("Ctrl-C stops the proxy"));
74
    use std::io::Write;
750×
    std::io::stdout().flush().ok();
760×
    let app = spectra_mcp::proxy::router(cfg.endpoint()?, port);
770×
    axum::serve(listener, app)
780×
        .with_graceful_shutdown(async {
790×
            let _ = tokio::signal::ctrl_c().await;
800×
        })
810×
        .await?;
820×
    Ok(0)
830×
}