devboy_core/agents/
gemini.rs1use std::path::Path;
14
15use super::fs_util::{count_subdirs, to_utc};
16use super::{AgentDetector, AgentSnapshot, InstallStatus};
17
18const ID: &str = "gemini";
19const DISPLAY_NAME: &str = "Gemini CLI";
20
21pub struct GeminiDetector;
22
23impl AgentDetector for GeminiDetector {
24 fn id(&self) -> &'static str {
25 ID
26 }
27 fn display_name(&self) -> &'static str {
28 DISPLAY_NAME
29 }
30
31 fn detect(&self, home: &Path) -> AgentSnapshot {
32 let gemini_dir = home.join(".gemini");
33 let history = gemini_dir.join("history");
34 let state_json = gemini_dir.join("state.json");
35 let settings = gemini_dir.join("settings.json");
36 let paths_checked = vec![
37 gemini_dir.clone(),
38 history.clone(),
39 state_json.clone(),
40 settings.clone(),
41 ];
42
43 let dir_present = gemini_dir.is_dir();
44 let binary_present = which::which("gemini").is_ok();
45
46 let status = if dir_present || binary_present {
47 InstallStatus::Yes
48 } else {
49 InstallStatus::No
50 };
51 if status == InstallStatus::No {
52 return AgentSnapshot {
53 id: ID,
54 display_name: DISPLAY_NAME,
55 status,
56 sessions: None,
57 last_used: None,
58 score: 0.0,
59 paths_checked,
60 };
61 }
62
63 let projects = count_subdirs(&history);
64 let last_used = [&state_json, &settings, &gemini_dir]
65 .iter()
66 .filter_map(|p| std::fs::metadata(p).ok())
67 .filter_map(|m| m.modified().ok())
68 .filter_map(to_utc)
69 .max();
70
71 AgentSnapshot {
72 id: ID,
73 display_name: DISPLAY_NAME,
74 status,
75 sessions: (projects > 0).then_some(projects),
76 last_used,
77 score: 0.0,
78 paths_checked,
79 }
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use std::fs;
87 use tempfile::tempdir;
88
89 #[test]
90 fn counts_history_project_dirs() {
91 let home = tempdir().unwrap();
92 let gemini = home.path().join(".gemini");
93 fs::create_dir_all(gemini.join("history/devboy-tools")).unwrap();
94 fs::write(gemini.join("history/devboy-tools/.project_root"), b"x").unwrap();
95 fs::create_dir_all(gemini.join("history/another")).unwrap();
96 fs::write(gemini.join("state.json"), b"{}").unwrap();
97 fs::write(gemini.join("settings.json"), b"{}").unwrap();
98
99 let snap = GeminiDetector.detect(home.path());
100 assert_eq!(snap.status, InstallStatus::Yes);
101 assert_eq!(snap.sessions, Some(2));
102 assert!(snap.last_used.is_some());
103 }
104
105 #[test]
106 fn no_gemini_dir_means_not_installed() {
107 let home = tempdir().unwrap();
108 let snap = GeminiDetector.detect(home.path());
109 if which::which("gemini").is_err() {
110 assert_eq!(snap.status, InstallStatus::No);
111 }
112 }
113
114 #[test]
115 fn gemini_dir_without_history_still_reports_install() {
116 let home = tempdir().unwrap();
117 fs::create_dir_all(home.path().join(".gemini")).unwrap();
118 let snap = GeminiDetector.detect(home.path());
119 assert_eq!(snap.status, InstallStatus::Yes);
120 assert!(snap.sessions.is_none());
121 }
122}