Skip to main content

difflore_cli/mcp_install/
snapshot.rs

1use std::collections::BTreeSet;
2
3use super::{
4    InstallState, McpClientStatus, McpStatusSnapshot, TargetStatus,
5    common::{canonical_record_snapshot, probe_runtime_mcp_server, resolve_difflore_binary},
6    diagnosis::diagnose_status_snapshot,
7    registry::{self, AGENTS},
8};
9
10// ── Status aggregation ──────────────────────────────────────────────────
11
12fn find_surface(agents: &[TargetStatus], name: &str) -> Option<TargetStatus> {
13    agents.iter().find(|agent| agent.name == name).cloned()
14}
15
16fn client_status(name: &'static str, surfaces: Vec<TargetStatus>) -> McpClientStatus {
17    let detected = surfaces.iter().any(|surface| surface.detected);
18    let detected_surfaces: Vec<&TargetStatus> =
19        surfaces.iter().filter(|surface| surface.detected).collect();
20    let installed = detected_surfaces
21        .iter()
22        .filter(|surface| matches!(surface.state, InstallState::Installed))
23        .count();
24    let conflicts = detected_surfaces
25        .iter()
26        .filter(|surface| matches!(surface.state, InstallState::Conflict))
27        .count();
28    let unknowns = detected_surfaces
29        .iter()
30        .filter(|surface| matches!(surface.state, InstallState::Unknown))
31        .count();
32    let missing = detected_surfaces
33        .iter()
34        .filter(|surface| matches!(surface.state, InstallState::NotInstalled))
35        .count();
36
37    let state = if conflicts > 0 || (installed > 0 && missing > 0) {
38        InstallState::Conflict
39    } else if unknowns > 0 {
40        InstallState::Unknown
41    } else if detected && installed == detected_surfaces.len() && installed > 0 {
42        InstallState::Installed
43    } else {
44        InstallState::NotInstalled
45    };
46
47    let detail = match state {
48        InstallState::Installed => Some(format!(
49            "{installed}/{} detected surface(s) installed",
50            detected_surfaces.len()
51        )),
52        InstallState::Conflict if installed > 0 && missing > 0 => Some(format!(
53            "partial install: {installed} installed, {missing} missing"
54        )),
55        InstallState::Conflict => Some(format!("{conflicts} conflicting surface(s)")),
56        InstallState::Unknown => Some(format!("{unknowns} unknown surface(s)")),
57        InstallState::NotInstalled if detected => {
58            Some("detected, but no DiffLore surface installed".into())
59        }
60        InstallState::NotInstalled => Some("not detected".into()),
61    };
62
63    McpClientStatus {
64        name,
65        detected,
66        state,
67        detail,
68        surfaces,
69    }
70}
71
72/// Roll the per-surface `agents` up into one [`McpClientStatus`] per display
73/// client. Both the client list and the surface→client mapping are derived
74/// from the `AGENTS` table (`spec.client`), so adding an agent row also adds it
75/// here automatically. Clients are emitted in first-seen `AGENTS` order, and
76/// each client's surfaces are gathered in row order.
77pub(super) fn collect_client_statuses_from_agents(agents: &[TargetStatus]) -> Vec<McpClientStatus> {
78    let mut clients: Vec<&'static str> = Vec::new();
79    let mut seen: BTreeSet<&'static str> = BTreeSet::new();
80    for spec in AGENTS {
81        if seen.insert(spec.client) {
82            clients.push(spec.client);
83        }
84    }
85    clients
86        .into_iter()
87        .map(|client| {
88            let surfaces: Vec<TargetStatus> = AGENTS
89                .iter()
90                .filter(|spec| spec.client == client)
91                .filter_map(|spec| find_surface(agents, spec.name))
92                .collect();
93            client_status(client, surfaces)
94        })
95        .collect()
96}
97
98/// Probe every surface in the `AGENTS` table. Row order is load-bearing —
99/// Claude Code → Claude Code hooks → Codex come first — and is encoded directly
100/// in the table, so the manual reshuffle that used to live here is gone.
101pub(super) fn collect_agent_statuses(bin: &str) -> Vec<TargetStatus> {
102    AGENTS
103        .iter()
104        .map(|spec| registry::detect(spec, bin))
105        .collect()
106}
107
108pub(super) fn installed_targets_from_agents(agents: &[TargetStatus]) -> Vec<&'static str> {
109    agents
110        .iter()
111        .filter(|o| matches!(o.state, InstallState::Installed))
112        .map(|o| o.name)
113        .collect()
114}
115
116pub fn collect_status_snapshot() -> McpStatusSnapshot {
117    let bin = resolve_difflore_binary().unwrap_or_else(|_| "difflore".to_owned());
118    let agents = collect_agent_statuses(&bin);
119    let installed_targets = installed_targets_from_agents(&agents);
120    let canonical_record = canonical_record_snapshot(&bin, &installed_targets);
121    let clients = collect_client_statuses_from_agents(&agents);
122    McpStatusSnapshot {
123        binary: bin,
124        canonical_record,
125        runtime_probe: None,
126        diagnosis: None,
127        clients,
128        agents,
129    }
130}
131
132pub fn collect_status_snapshot_with_runtime_probe() -> McpStatusSnapshot {
133    let mut snapshot = collect_status_snapshot();
134    snapshot.runtime_probe = Some(probe_runtime_mcp_server(&snapshot.binary));
135    snapshot.diagnosis = Some(diagnose_status_snapshot(&snapshot));
136    snapshot
137}