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