use std::sync::OnceLock;
use car_registry::supervisor::{AgentSpec, StopSignal, Supervisor, SupervisorError};
fn handle_or_locked() -> Result<&'static Supervisor, SupervisorError> {
static SUP: OnceLock<Supervisor> = OnceLock::new();
if let Some(s) = SUP.get() {
return Ok(s);
}
let s = Supervisor::user_default()?;
let _ = SUP.set(s);
SUP.get().ok_or_else(|| {
SupervisorError::Other("supervisor singleton not set after init".to_string())
})
}
fn handle() -> Result<&'static Supervisor, String> {
handle_or_locked().map_err(|e| e.to_string())
}
pub async fn list() -> Result<String, String> {
match handle_or_locked() {
Ok(s) => serde_json::to_string(&s.list().await).map_err(|e| e.to_string()),
Err(SupervisorError::AlreadyRunning(_)) => {
let path = Supervisor::user_default_manifest_path().map_err(|e| e.to_string())?;
let agents = Supervisor::list_from_manifest(&path).map_err(|e| e.to_string())?;
serde_json::to_string(&agents).map_err(|e| e.to_string())
}
Err(e) => Err(e.to_string()),
}
}
pub async fn upsert(spec_json: &str) -> Result<String, String> {
let mut value: serde_json::Value =
serde_json::from_str(spec_json).map_err(|e| format!("invalid AgentSpec JSON: {e}"))?;
if let Some(name) = value
.get("interpreter")
.and_then(|v| v.as_str())
.map(str::to_string)
{
let resolved =
car_registry::supervisor::resolve_interpreter(&name).map_err(|e| e.to_string())?;
value["command"] = serde_json::Value::String(resolved.to_string_lossy().into_owned());
}
let spec: AgentSpec =
serde_json::from_value(value).map_err(|e| format!("invalid AgentSpec JSON: {e}"))?;
let s = handle()?;
let agent = s.upsert(spec).await.map_err(|e| e.to_string())?;
serde_json::to_string(&agent).map_err(|e| e.to_string())
}
pub async fn health() -> Result<String, String> {
match handle_or_locked() {
Ok(s) => serde_json::to_string(&s.health().await).map_err(|e| e.to_string()),
Err(SupervisorError::AlreadyRunning(_)) => {
let path = Supervisor::user_default_manifest_path().map_err(|e| e.to_string())?;
let health = Supervisor::health_from_manifest(&path).map_err(|e| e.to_string())?;
serde_json::to_string(&health).map_err(|e| e.to_string())
}
Err(e) => Err(e.to_string()),
}
}
pub async fn install(manifest_json: &str) -> Result<String, String> {
let manifest: car_registry::manifest::AgentManifest = serde_json::from_str(manifest_json)
.map_err(|e| format!("invalid AgentManifest JSON: {e}"))?;
let host = car_registry::install::HostCapabilities::daemon_default(env!("CARGO_PKG_VERSION"));
let s = handle()?;
let (report, managed) = s
.install_manifest(manifest, &host)
.await
.map_err(|e| e.to_string())?;
Ok(serde_json::json!({
"report": {
"missingOptional": report
.missing_optional
.iter()
.map(|(ns, feat)| serde_json::json!({ "namespace": ns, "feature": feat }))
.collect::<Vec<_>>(),
},
"agent": managed,
})
.to_string())
}
pub async fn remove(id: &str) -> Result<String, String> {
let s = handle()?;
let removed = s.remove(id).await.map_err(|e| e.to_string())?;
serde_json::to_string(&serde_json::json!({ "removed": removed })).map_err(|e| e.to_string())
}
pub async fn start(id: &str) -> Result<String, String> {
let s = handle()?;
let agent = s.start(id).await.map_err(|e| e.to_string())?;
serde_json::to_string(&agent).map_err(|e| e.to_string())
}
pub async fn stop(id: &str, signal: Option<&str>) -> Result<String, String> {
let s = handle()?;
let signal = signal
.map(|raw| {
serde_json::from_value::<StopSignal>(serde_json::Value::String(raw.to_string()))
.map_err(|e| format!("invalid signal `{raw}`: {e}"))
})
.transpose()?
.unwrap_or_default();
let agent = s.stop(id, signal).await.map_err(|e| e.to_string())?;
serde_json::to_string(&agent).map_err(|e| e.to_string())
}
pub async fn restart(id: &str) -> Result<String, String> {
let s = handle()?;
let agent = s.restart(id).await.map_err(|e| e.to_string())?;
serde_json::to_string(&agent).map_err(|e| e.to_string())
}
pub async fn tail_log(id: &str, n: Option<usize>) -> Result<String, String> {
let s = handle()?;
let lines = s
.tail_log(id, n.unwrap_or(100))
.await
.map_err(|e| e.to_string())?;
serde_json::to_string(&serde_json::json!({ "lines": lines })).map_err(|e| e.to_string())
}