| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 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 | |
|
| 15 | 2× | pub async fn status(ctx: &Ctx, as_json: bool) -> Result<i32> { |
| 16 | 2× | let home = owner_home(ctx, None); |
| 17 | 2× | let identity = spectra_core::keys::KeyPair::load_home(&home).ok(); |
| 18 | 2× | let remote = ctx.remote()?; |
| 19 | 2× | let initialised = ctx.data.join("spectra.db").exists(); |
| 20 | 2× | let serving = initialised && Client::alive(&ctx.socket()).await; |
| 21 | 2× | let reachable = match &remote { |
| 22 | 0× | Some(c) => Client::at(c.endpoint()?, None).send("GET", "/api/v1/health", &[], vec![]).await.is_ok_and(|r| r.status < 500), |
| 23 | 2× | None => serving, |
| 24 | | }; |
| 25 | 2× | let v = json!({ |
| 26 | 2× | "version": env!("SPECTRA_VERSION_LONG"), |
| 27 | 2× | "home": home.display().to_string(), |
| 28 | 2× | "identity": identity.as_ref().map(|p| json!({ "name": p.identity.name, "key": p.key.to_hex() })), |
| 29 | 2× | "server": remote.as_ref().map(|c| json!({ "url": c.server, "instance": c.instance })), |
| 30 | 2× | "local": { "data": ctx.data.display().to_string(), "initialised": initialised, "serving": serving }, |
| 31 | 2× | "reachable": reachable, |
| 32 | | }); |
| 33 | 2× | if as_json { |
| 34 | 2× | println!("{v}"); |
| 35 | 2× | return Ok(0); |
| 36 | 0× | } |
| 37 | 0× | println!("{}", ui::kv("version", env!("SPECTRA_VERSION_LONG"))); |
| 38 | 0× | match &identity { |
| 39 | 0× | Some(p) => println!("{}", ui::kv("you", format!("{}{}{}", ui::author(&p.identity.name), ui::sep(), ui::hash(p.key.short(12))))), |
| 40 | 0× | None => println!("{}", ui::kv("you", ui::warn("no identity yet: spectra identity create --name NAME"))), |
| 41 | | } |
| 42 | 0× | match &remote { |
| 43 | 0× | Some(c) => println!("{}", ui::kv("spectra", format!("{}{}", ui::path(&c.server), if reachable { "" } else { " (not answering)" }))), |
| 44 | 0× | None if initialised => println!( |
| 45 | | "{}", |
| 46 | 0× | ui::kv("spectra", format!("{}{}", ui::path(ctx.data.display()), if serving { " (serving)" } else { " (not serving)" })) |
| 47 | | ), |
| 48 | 0× | None => println!("{}", ui::kv("spectra", ui::warn("none yet: spectra init here, or spectra connect https://HOST:7350"))), |
| 49 | | } |
| 50 | 0× | Ok(0) |
| 51 | 2× | } |
| 52 | |
|
| 53 | |
|
| 54 | |
|
| 55 | 1× | pub async fn probe(url: &str, as_json: bool) -> Result<i32> { |
| 56 | 1× | let addr = host_port(url)?; |
| 57 | 1× | let seen = Pinned::new(None); |
| 58 | 1× | let c = Client::at(Endpoint::Tls { addr: addr.clone(), config: seen.client_config()? }, None); |
| 59 | 1× | let r = c.send("GET", "/api/v1/instance", &[], vec![]).await.with_context(|| format!("reaching https://{addr}"))?; |
| 60 | 1× | if r.status != 200 { |
| 61 | 0× | bail!("https://{addr} answered {} to /api/v1/instance: is it a Spectra?", r.status); |
| 62 | 1× | } |
| 63 | 1× | let answer = r.json()?; |
| 64 | 1× | let (_, pin) = seen.seen().context("the server showed no certificate")?; |
| 65 | 1× | let checked = spectra_mcp::instance::check(&answer, &pin)?; |
| 66 | 1× | let mine = spectra_core::keys::KeyPair::load_home(&spectra_core::keys::default_home()).ok(); |
| 67 | 1× | let waiting = answer.get("named_owner").and_then(Value::as_str).map(String::from); |
| 68 | 1× | let v = json!({ |
| 69 | 1× | "url": format!("https://{addr}"), |
| 70 | 1× | "name": checked.name, |
| 71 | 1× | "instance": checked.instance.hash().to_hex(), |
| 72 | 1× | "owner": checked.owner, |
| 73 | 1× | "waiting_for": waiting, |
| 74 | 1× | "yours": mine.as_ref().is_some_and(|m| { |
| 75 | 1× | let me = m.key.to_hex(); |
| 76 | 1× | checked.owner.as_deref() == Some(me.as_str()) || waiting.as_deref() == Some(me.as_str()) |
| 77 | 0× | || [checked.owner.as_deref(), waiting.as_deref()].into_iter().flatten() |
| 78 | 0× | .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())) |
| 79 | 1× | }), |
| 80 | 1× | "tls_spki": hex::encode(pin), |
| 81 | | }); |
| 82 | 1× | if as_json { |
| 83 | 1× | println!("{v}"); |
| 84 | 1× | } else { |
| 85 | 0× | println!("{}", ui::kv("server", ui::path(v["url"].as_str().unwrap_or("")))); |
| 86 | 0× | println!("{}", ui::kv("name", v["name"].as_str().unwrap_or(""))); |
| 87 | 0× | println!("{}", ui::kv("instance", ui::hash(v["instance"].as_str().unwrap_or("")))); |
| 88 | 0× | let whose = match (v["owner"].as_str(), v["waiting_for"].as_str()) { |
| 89 | 0× | (Some(o), _) => format!("owned by {}", &o[..12.min(o.len())]), |
| 90 | 0× | (None, Some(w)) => format!("waiting to be claimed by {}", &w[..12.min(w.len())]), |
| 91 | 0× | _ => "no owner".into(), |
| 92 | | }; |
| 93 | 0× | println!("{}", ui::kv("owner", whose)); |
| 94 | | } |
| 95 | 1× | Ok(0) |
| 96 | 1× | } |
| 97 | |
|
| 98 | |
|
| 99 | |
|
| 100 | 1× | pub async fn bridge(ctx: &Ctx, port: u16, owner_home_flag: Option<PathBuf>) -> Result<i32> { |
| 101 | 1× | let home = owner_home(ctx, owner_home_flag); |
| 102 | 1× | let client = |
| 103 | 1× | ctx.owner_client(&home).await?.context("no Spectra to bridge to: spectra init here, or spectra connect https://HOST:7350")?; |
| 104 | 1× | let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await.with_context(|| format!("binding 127.0.0.1:{port}"))?; |
| 105 | 1× | let port = listener.local_addr()?.port(); |
| 106 | 1× | let token = spectra_mcp::bridge::token(); |
| 107 | 1× | println!("{}", json!({ "url": format!("http://127.0.0.1:{port}"), "token": token })); |
| 108 | | use std::io::Write; |
| 109 | 1× | std::io::stdout().flush().ok(); |
| 110 | 1× | let app = spectra_mcp::bridge::router(client, token, port); |
| 111 | 1× | axum::serve(listener, app) |
| 112 | 1× | .with_graceful_shutdown(async { |
| 113 | | |
| 114 | 1× | let stdin = async { |
| 115 | 1× | let mut buf = [0u8; 256]; |
| 116 | 1× | let mut input = tokio::io::stdin(); |
| 117 | | loop { |
| 118 | 1× | match tokio::io::AsyncReadExt::read(&mut input, &mut buf).await { |
| 119 | 1× | Ok(0) | Err(_) => return, |
| 120 | 0× | Ok(_) => {} |
| 121 | | } |
| 122 | | } |
| 123 | 1× | }; |
| 124 | 1× | tokio::select! { |
| 125 | 1× | _ = stdin => {} |
| 126 | 1× | _ = tokio::signal::ctrl_c() => {} |
| 127 | | } |
| 128 | 1× | }) |
| 129 | 1× | .await?; |
| 130 | 1× | Ok(0) |
| 131 | 1× | } |