use super::*;
#[derive(Debug, Clone, Default)]
pub(crate) struct RegistryFacts {
pub(crate) session: String,
pub(crate) pid: Option<u32>,
pub(crate) proc_start: Option<String>,
pub(crate) pid_domain: Option<String>,
pub(crate) status: Option<String>,
pub(crate) socket_present: bool,
pub(crate) headless: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct PeerRow {
pub(crate) lane: String,
pub(crate) kind: ReceiverKind,
pub(crate) state: String,
pub(crate) session: String,
pub(crate) last_activity_utc: Option<String>,
}
pub(crate) fn all_registry_rows() -> Result<Vec<RegistryFacts>> {
let dir = crate::path::claude_home()?.join("sessions");
if !dir.is_dir() {
return Ok(Vec::new());
}
let mut out = Vec::new();
for entry in std::fs::read_dir(&dir)?.flatten() {
let p = entry.path();
if p.extension().and_then(|e| e.to_str()) != Some("json") {
continue; }
let Ok(raw) = std::fs::read_to_string(&p) else {
continue;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
continue;
};
let Some(session) = v.get("sessionId").and_then(serde_json::Value::as_str) else {
continue;
};
out.push(RegistryFacts {
session: session.to_string(),
pid: v
.get("pid")
.and_then(serde_json::Value::as_u64)
.and_then(|n| u32::try_from(n).ok()),
proc_start: str_of(&v, "procStart"),
pid_domain: str_of(&v, "pidDomain"),
status: str_of(&v, "status"),
socket_present: v
.get("messagingSocketPath")
.and_then(serde_json::Value::as_str)
.is_some_and(|s| !s.trim().is_empty()),
headless: v.get("entrypoint").and_then(serde_json::Value::as_str) == Some("sdk-cli"),
});
}
Ok(out)
}
fn str_of(v: &serde_json::Value, key: &str) -> Option<String> {
v.get(key)
.and_then(serde_json::Value::as_str)
.map(std::string::ToString::to_string)
}
pub(crate) fn registry_facts(session_id: &str) -> Result<Option<RegistryFacts>> {
Ok(all_registry_rows()?
.into_iter()
.find(|r| r.session == session_id))
}
pub(crate) fn live_lane_census() -> Result<Vec<PeerRow>> {
let mut out = Vec::new();
for row in all_registry_rows()? {
let Some(pid) = row.pid else { continue };
if !matches!(
probe_pid(pid, row.proc_start.as_deref(), row.pid_domain.as_deref()),
PidLiveness::Alive { .. }
) {
continue;
}
let Some(path) = locate_session(&row.session) else {
out.push(session_row(&row, None));
continue;
};
out.push(session_row(&row, Some(&path)));
for child in live_child_lanes(&path, None) {
out.push(PeerRow {
lane: child.lane,
kind: child.kind,
state: child.state,
session: row.session.clone(),
last_activity_utc: child.last_activity_utc,
});
}
}
Ok(out)
}
fn session_row(row: &RegistryFacts, path: Option<&Path>) -> PeerRow {
PeerRow {
lane: row.session.clone(),
kind: ReceiverKind::TopLevel,
state: row.status.clone().unwrap_or_else(|| "running".to_string()),
session: row.session.clone(),
last_activity_utc: path
.and_then(|p| tail_shape(p).ok())
.and_then(|t| t.last_ts_utc),
}
}
fn locate_session(session_id: &str) -> Option<PathBuf> {
let filename = format!("{session_id}.jsonl");
crate::path::all_project_dirs()
.ok()?
.into_iter()
.find_map(|pd| {
let candidate = pd.dir.join(&filename);
candidate.is_file().then_some(candidate)
})
}
pub(crate) fn run_peers(format: OutputFormat) -> Result<()> {
let rows = live_lane_census()?;
match format {
OutputFormat::Text => {
render_peers_text(&rows);
Ok(())
}
OutputFormat::Json => render_peers_json(&rows),
}
}