Skip to main content

devboy_core/agents/
gemini.rs

1//! Gemini CLI detector.
2//!
3//! Gemini CLI keeps a tiny footprint — `~/.gemini/` ~50KB total. There is no
4//! per-session events file we can count meaningfully. Best heuristics:
5//! - install marker: `~/.gemini/` exists OR `gemini` in PATH.
6//! - sessions: count of subdirectories under `~/.gemini/history/` (one per
7//!   project the user touched with Gemini); upper bound, not per-session.
8//! - last_used: max of `state.json` / `settings.json` mtimes.
9//!
10//! Antigravity (Google's IDE-agent) shares `~/.gemini/antigravity/` and is a
11//! separate detector (`agents::antigravity`).
12
13use 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}