//! Commands a program uses to set Spectra up and to talk to it (spec //! ยง18.1): `spectra status` says what is set up, `spectra probe` what a //! server is before trusting it, and `spectra bridge` signs for a program //! that cannot (the VS Code extension). use super::identity::owner_home; use super::{ui, Ctx}; use anyhow::{bail, Context, Result}; use serde_json::{json, Value}; use spectra_mcp::client::{host_port, Client, Endpoint}; use spectra_mcp::tls::Pinned; use std::path::PathBuf; /// `spectra status [--json]`: the identity, the server or local Spectra. pub async fn status(ctx: &Ctx, as_json: bool) -> Result { let home = owner_home(ctx, None); let identity = spectra_core::keys::KeyPair::load_home(&home).ok(); let remote = ctx.remote()?; let initialised = ctx.data.join("spectra.db").exists(); let serving = initialised && Client::alive(&ctx.socket()).await; let reachable = match &remote { Some(c) => Client::at(c.endpoint()?, None).send("GET", "/api/v1/health", &[], vec![]).await.is_ok_and(|r| r.status < 500), None => serving, }; let v = json!({ "version": env!("SPECTRA_VERSION_LONG"), "home": home.display().to_string(), "identity": identity.as_ref().map(|p| json!({ "name": p.identity.name, "key": p.key.to_hex() })), "server": remote.as_ref().map(|c| json!({ "url": c.server, "instance": c.instance })), "local": { "data": ctx.data.display().to_string(), "initialised": initialised, "serving": serving }, "reachable": reachable, }); if as_json { println!("{v}"); return Ok(0); } println!("{}", ui::kv("version", env!("SPECTRA_VERSION_LONG"))); match &identity { Some(p) => println!("{}", ui::kv("you", format!("{}{}{}", ui::author(&p.identity.name), ui::sep(), ui::hash(p.key.short(12))))), None => println!("{}", ui::kv("you", ui::warn("no identity yet: spectra identity create --name NAME"))), } match &remote { Some(c) => println!("{}", ui::kv("spectra", format!("{}{}", ui::path(&c.server), if reachable { "" } else { " (not answering)" }))), None if initialised => println!( "{}", ui::kv("spectra", format!("{}{}", ui::path(ctx.data.display()), if serving { " (serving)" } else { " (not serving)" })) ), None => println!("{}", ui::kv("spectra", ui::warn("none yet: spectra init here, or spectra connect https://HOST:7350"))), } Ok(0) } /// `spectra probe URL [--json]`: who a server says it is, checked against /// its certificate, without trusting it. pub async fn probe(url: &str, as_json: bool) -> Result { let addr = host_port(url)?; let seen = Pinned::new(None); let c = Client::at(Endpoint::Tls { addr: addr.clone(), config: seen.client_config()? }, None); let r = c.send("GET", "/api/v1/instance", &[], vec![]).await.with_context(|| format!("reaching https://{addr}"))?; if r.status != 200 { bail!("https://{addr} answered {} to /api/v1/instance: is it a Spectra?", r.status); } let answer = r.json()?; let (_, pin) = seen.seen().context("the server showed no certificate")?; let checked = spectra_mcp::instance::check(&answer, &pin)?; let mine = spectra_core::keys::KeyPair::load_home(&spectra_core::keys::default_home()).ok(); let waiting = answer.get("named_owner").and_then(Value::as_str).map(String::from); let v = json!({ "url": format!("https://{addr}"), "name": checked.name, "instance": checked.instance.hash().to_hex(), "owner": checked.owner, "waiting_for": waiting, "yours": mine.as_ref().is_some_and(|m| { let me = m.key.to_hex(); checked.owner.as_deref() == Some(me.as_str()) || waiting.as_deref() == Some(me.as_str()) || [checked.owner.as_deref(), waiting.as_deref()].into_iter().flatten() .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())) }), "tls_spki": hex::encode(pin), }); if as_json { println!("{v}"); } else { println!("{}", ui::kv("server", ui::path(v["url"].as_str().unwrap_or("")))); println!("{}", ui::kv("name", v["name"].as_str().unwrap_or(""))); println!("{}", ui::kv("instance", ui::hash(v["instance"].as_str().unwrap_or("")))); let whose = match (v["owner"].as_str(), v["waiting_for"].as_str()) { (Some(o), _) => format!("owned by {}", &o[..12.min(o.len())]), (None, Some(w)) => format!("waiting to be claimed by {}", &w[..12.min(w.len())]), _ => "no owner".into(), }; println!("{}", ui::kv("owner", whose)); } Ok(0) } /// `spectra bridge [--port 0]`: sign as the owner for the program that /// started it; prints `{ "url", "token" }` and ends with standard input. pub async fn bridge(ctx: &Ctx, port: u16, owner_home_flag: Option) -> Result { let home = owner_home(ctx, owner_home_flag); let client = ctx.owner_client(&home).await?.context("no Spectra to bridge to: spectra init here, or spectra connect https://HOST:7350")?; let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await.with_context(|| format!("binding 127.0.0.1:{port}"))?; let port = listener.local_addr()?.port(); let token = spectra_mcp::bridge::token(); println!("{}", json!({ "url": format!("http://127.0.0.1:{port}"), "token": token })); use std::io::Write; std::io::stdout().flush().ok(); let app = spectra_mcp::bridge::router(client, token, port); axum::serve(listener, app) .with_graceful_shutdown(async { // Ends when the program that started it closes our input, or on Ctrl-C. let stdin = async { let mut buf = [0u8; 256]; let mut input = tokio::io::stdin(); loop { match tokio::io::AsyncReadExt::read(&mut input, &mut buf).await { Ok(0) | Err(_) => return, Ok(_) => {} } } }; tokio::select! { _ = stdin => {} _ = tokio::signal::ctrl_c() => {} } }) .await?; Ok(0) }