devboy_core/agents/
registry.rs1use std::path::Path;
8
9use chrono::Utc;
10
11use super::{AgentDetector, AgentSnapshot};
12
13fn detectors() -> Vec<Box<dyn AgentDetector>> {
14 vec![
15 Box::new(super::claude::ClaudeDetector),
16 Box::new(super::copilot::CopilotDetector),
17 Box::new(super::codex::CodexDetector),
18 Box::new(super::kimi::KimiDetector),
19 Box::new(super::cursor::CursorDetector),
20 Box::new(super::gemini::GeminiDetector),
21 Box::new(super::antigravity::AntigravityDetector),
22 ]
23}
24
25pub fn detect_all() -> Vec<AgentSnapshot> {
28 match dirs::home_dir() {
29 Some(home) => detect_all_with_home(&home),
30 None => Vec::new(),
31 }
32}
33
34pub fn detect_all_with_home(home: &Path) -> Vec<AgentSnapshot> {
37 let now = Utc::now();
38 let mut out: Vec<AgentSnapshot> = detectors()
39 .into_iter()
40 .map(|d| {
41 let mut snap = d.detect(home);
42 snap.score = super::score::compute_score(snap.last_used, snap.sessions, now);
43 snap
44 })
45 .collect();
46 out.sort_by(|a, b| {
47 b.score
48 .partial_cmp(&a.score)
49 .unwrap_or(std::cmp::Ordering::Equal)
50 });
51 out
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use crate::agents::InstallStatus;
58 use std::fs;
59 use tempfile::tempdir;
60
61 #[test]
62 fn returns_one_snapshot_per_known_detector() {
63 let home = tempdir().unwrap();
64 let snaps = detect_all_with_home(home.path());
65 assert_eq!(snaps.len(), 7);
67 let ids: std::collections::HashSet<_> = snaps.iter().map(|s| s.id).collect();
68 for expected in [
69 "claude",
70 "copilot",
71 "codex",
72 "kimi",
73 "cursor",
74 "gemini",
75 "antigravity",
76 ] {
77 assert!(ids.contains(expected), "missing detector for {expected}");
78 }
79 }
80
81 #[test]
82 fn scores_are_clamped_in_unit_interval() {
83 let home = tempdir().unwrap();
84 let snaps = detect_all_with_home(home.path());
85 for s in &snaps {
86 assert!(
87 (0.0..=1.0).contains(&s.score),
88 "score out of range for {}: {}",
89 s.id,
90 s.score
91 );
92 }
93 }
94
95 #[test]
96 fn detect_all_runs_against_real_home_without_panicking() {
97 let snaps = detect_all();
101 assert!(snaps.is_empty() || snaps.len() == 7);
102 }
103
104 #[test]
105 fn snapshots_sorted_by_score_descending() {
106 let home = tempdir().unwrap();
109 let projects = home.path().join(".claude/projects/-Users-x-foo");
110 fs::create_dir_all(&projects).unwrap();
111 for i in 0..50 {
112 fs::write(projects.join(format!("session-{i}.jsonl")), b"{}\n").unwrap();
113 }
114
115 let snaps = detect_all_with_home(home.path());
116 for window in snaps.windows(2) {
117 assert!(
118 window[0].score >= window[1].score,
119 "not sorted: {} ({}) before {} ({})",
120 window[0].id,
121 window[0].score,
122 window[1].id,
123 window[1].score,
124 );
125 }
126 assert_eq!(snaps[0].id, "claude");
127 assert_eq!(snaps[0].status, InstallStatus::Yes);
128 assert_eq!(snaps[0].sessions, Some(50));
129 }
130}