OXYGENHUB

Coverage / src/cli/setup.rs 72.5%

79 of 109 lines run, 10 of 32 functions · run #33 on main · run missed

1
//! Commands a program uses to set Spectra up and to talk to it (spec
2
//! §18.1): `spectra status` says what is set up, `spectra probe` what a
3
//! server is before trusting it, and `spectra bridge` signs for a program
4
//! that cannot (the VS Code extension).
5
6
use super::identity::owner_home;
7
use super::{ui, Ctx};
8
use anyhow::{bail, Context, Result};
9
use serde_json::{json, Value};
10
use spectra_mcp::client::{host_port, Client, Endpoint};
11
use spectra_mcp::tls::Pinned;
12
use std::path::PathBuf;
13
14
/// `spectra status [--json]`: the identity, the server or local Spectra.
152×
pub async fn status(ctx: &Ctx, as_json: bool) -> Result<i32> {
162×
    let home = owner_home(ctx, None);
172×
    let identity = spectra_core::keys::KeyPair::load_home(&home).ok();
182×
    let remote = ctx.remote()?;
192×
    let initialised = ctx.data.join("spectra.db").exists();
202×
    let serving = initialised && Client::alive(&ctx.socket()).await;
212×
    let reachable = match &remote {
220×
        Some(c) => Client::at(c.endpoint()?, None).send("GET", "/api/v1/health", &[], vec![]).await.is_ok_and(|r| r.status < 500),
232×
        None => serving,
24
    };
252×
    let v = json!({
262×
        "version": env!("SPECTRA_VERSION_LONG"),
272×
        "home": home.display().to_string(),
282×
        "identity": identity.as_ref().map(|p| json!({ "name": p.identity.name, "key": p.key.to_hex() })),
292×
        "server": remote.as_ref().map(|c| json!({ "url": c.server, "instance": c.instance })),
302×
        "local": { "data": ctx.data.display().to_string(), "initialised": initialised, "serving": serving },
312×
        "reachable": reachable,
32
    });
332×
    if as_json {
342×
        println!("{v}");
352×
        return Ok(0);
360×
    }
370×
    println!("{}", ui::kv("version", env!("SPECTRA_VERSION_LONG")));
380×
    match &identity {
390×
        Some(p) => println!("{}", ui::kv("you", format!("{}{}{}", ui::author(&p.identity.name), ui::sep(), ui::hash(p.key.short(12))))),
400×
        None => println!("{}", ui::kv("you", ui::warn("no identity yet: spectra identity create --name NAME"))),
41
    }
420×
    match &remote {
430×
        Some(c) => println!("{}", ui::kv("spectra", format!("{}{}", ui::path(&c.server), if reachable { "" } else { " (not answering)" }))),
440×
        None if initialised => println!(
45
            "{}",
460×
            ui::kv("spectra", format!("{}{}", ui::path(ctx.data.display()), if serving { " (serving)" } else { " (not serving)" }))
47
        ),
480×
        None => println!("{}", ui::kv("spectra", ui::warn("none yet: spectra init here, or spectra connect https://HOST:7350"))),
49
    }
500×
    Ok(0)
512×
}
52
53
/// `spectra probe URL [--json]`: who a server says it is, checked against
54
/// its certificate, without trusting it.
551×
pub async fn probe(url: &str, as_json: bool) -> Result<i32> {
561×
    let addr = host_port(url)?;
571×
    let seen = Pinned::new(None);
581×
    let c = Client::at(Endpoint::Tls { addr: addr.clone(), config: seen.client_config()? }, None);
591×
    let r = c.send("GET", "/api/v1/instance", &[], vec![]).await.with_context(|| format!("reaching https://{addr}"))?;
601×
    if r.status != 200 {
610×
        bail!("https://{addr} answered {} to /api/v1/instance: is it a Spectra?", r.status);
621×
    }
631×
    let answer = r.json()?;
641×
    let (_, pin) = seen.seen().context("the server showed no certificate")?;
651×
    let checked = spectra_mcp::instance::check(&answer, &pin)?;
661×
    let mine = spectra_core::keys::KeyPair::load_home(&spectra_core::keys::default_home()).ok();
671×
    let waiting = answer.get("named_owner").and_then(Value::as_str).map(String::from);
681×
    let v = json!({
691×
        "url": format!("https://{addr}"),
701×
        "name": checked.name,
711×
        "instance": checked.instance.hash().to_hex(),
721×
        "owner": checked.owner,
731×
        "waiting_for": waiting,
741×
        "yours": mine.as_ref().is_some_and(|m| {
751×
            let me = m.key.to_hex();
761×
            checked.owner.as_deref() == Some(me.as_str()) || waiting.as_deref() == Some(me.as_str())
770×
                || [checked.owner.as_deref(), waiting.as_deref()].into_iter().flatten()
780×
                    .any(|k| spectra_core::identity::KeyId::from_hex(k).is_some_and(|k| spectra_core::keys::identity_in(&spectra_core::keys::default_home(), &k).is_some()))
791×
        }),
801×
        "tls_spki": hex::encode(pin),
81
    });
821×
    if as_json {
831×
        println!("{v}");
841×
    } else {
850×
        println!("{}", ui::kv("server", ui::path(v["url"].as_str().unwrap_or(""))));
860×
        println!("{}", ui::kv("name", v["name"].as_str().unwrap_or("")));
870×
        println!("{}", ui::kv("instance", ui::hash(v["instance"].as_str().unwrap_or(""))));
880×
        let whose = match (v["owner"].as_str(), v["waiting_for"].as_str()) {
890×
            (Some(o), _) => format!("owned by {}", &o[..12.min(o.len())]),
900×
            (None, Some(w)) => format!("waiting to be claimed by {}", &w[..12.min(w.len())]),
910×
            _ => "no owner".into(),
92
        };
930×
        println!("{}", ui::kv("owner", whose));
94
    }
951×
    Ok(0)
961×
}
97
98
/// `spectra bridge [--port 0]`: sign as the owner for the program that
99
/// started it; prints `{ "url", "token" }` and ends with standard input.
1001×
pub async fn bridge(ctx: &Ctx, port: u16, owner_home_flag: Option<PathBuf>) -> Result<i32> {
1011×
    let home = owner_home(ctx, owner_home_flag);
1021×
    let client =
1031×
        ctx.owner_client(&home).await?.context("no Spectra to bridge to: spectra init here, or spectra connect https://HOST:7350")?;
1041×
    let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await.with_context(|| format!("binding 127.0.0.1:{port}"))?;
1051×
    let port = listener.local_addr()?.port();
1061×
    let token = spectra_mcp::bridge::token();
1071×
    println!("{}", json!({ "url": format!("http://127.0.0.1:{port}"), "token": token }));
108
    use std::io::Write;
1091×
    std::io::stdout().flush().ok();
1101×
    let app = spectra_mcp::bridge::router(client, token, port);
1111×
    axum::serve(listener, app)
1121×
        .with_graceful_shutdown(async {
113
            // Ends when the program that started it closes our input, or on Ctrl-C.
1141×
            let stdin = async {
1151×
                let mut buf = [0u8; 256];
1161×
                let mut input = tokio::io::stdin();
117
                loop {
1181×
                    match tokio::io::AsyncReadExt::read(&mut input, &mut buf).await {
1191×
                        Ok(0) | Err(_) => return,
1200×
                        Ok(_) => {}
121
                    }
122
                }
1231×
            };
1241×
            tokio::select! {
1251×
                _ = stdin => {}
1261×
                _ = tokio::signal::ctrl_c() => {}
127
            }
1281×
        })
1291×
        .await?;
1301×
    Ok(0)
1311×
}