devboy_core/agents/
cursor.rs1use std::path::{Path, PathBuf};
16
17use super::fs_util::{count_subdirs, to_utc};
18use super::{AgentDetector, AgentSnapshot, InstallStatus};
19
20const ID: &str = "cursor";
21const DISPLAY_NAME: &str = "Cursor";
22
23pub struct CursorDetector;
24
25impl AgentDetector for CursorDetector {
26 fn id(&self) -> &'static str {
27 ID
28 }
29 fn display_name(&self) -> &'static str {
30 DISPLAY_NAME
31 }
32
33 fn detect(&self, home: &Path) -> AgentSnapshot {
34 let cursor_dir = cursor_data_dir(home);
35 let workspace_storage = cursor_dir.join("User/workspaceStorage");
36 let paths_checked = vec![cursor_dir.clone(), workspace_storage.clone()];
37
38 let dir_present = cursor_dir.is_dir();
39 let binary_present = which::which("cursor").is_ok();
41
42 let status = if dir_present || binary_present {
43 InstallStatus::Yes
44 } else {
45 InstallStatus::No
46 };
47 if status == InstallStatus::No {
48 return empty(paths_checked);
49 }
50
51 let count = count_subdirs(&workspace_storage);
52 let last_used = max_state_vscdb_mtime(&workspace_storage);
53
54 AgentSnapshot {
55 id: ID,
56 display_name: DISPLAY_NAME,
57 status,
58 sessions: (count > 0).then_some(count),
59 last_used,
60 score: 0.0,
61 paths_checked,
62 }
63 }
64}
65
66fn cursor_data_dir(home: &Path) -> PathBuf {
67 if cfg!(target_os = "macos") {
68 home.join("Library/Application Support/Cursor")
69 } else if cfg!(target_os = "windows") {
70 std::env::var_os("APPDATA")
71 .map(PathBuf::from)
72 .unwrap_or_else(|| home.join("AppData/Roaming"))
73 .join("Cursor")
74 } else {
75 home.join(".config/Cursor")
77 }
78}
79
80fn max_state_vscdb_mtime(workspace_storage: &Path) -> Option<chrono::DateTime<chrono::Utc>> {
81 let Ok(workspaces) = std::fs::read_dir(workspace_storage) else {
82 return None;
83 };
84 let mut best: Option<chrono::DateTime<chrono::Utc>> = None;
85 for ws in workspaces.flatten() {
86 let p = ws.path().join("state.vscdb");
87 if let Ok(meta) = std::fs::metadata(&p)
88 && let Ok(modified) = meta.modified()
89 && let Some(t) = to_utc(modified)
90 {
91 best = Some(best.map_or(t, |b| b.max(t)));
92 }
93 }
94 best
95}
96
97fn empty(paths_checked: Vec<PathBuf>) -> AgentSnapshot {
98 AgentSnapshot {
99 id: ID,
100 display_name: DISPLAY_NAME,
101 status: InstallStatus::No,
102 sessions: None,
103 last_used: None,
104 score: 0.0,
105 paths_checked,
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use tempfile::tempdir;
113
114 #[test]
115 fn no_cursor_dir_means_not_installed() {
116 let home = tempdir().unwrap();
117 let snap = CursorDetector.detect(home.path());
118 if which::which("cursor").is_err() {
121 assert_eq!(snap.status, InstallStatus::No);
122 assert!(snap.sessions.is_none());
123 }
124 }
125
126 #[cfg(any(target_os = "macos", target_os = "linux"))]
134 fn assert_counts_workspaces(rel_storage: &str, ids: &[&str]) {
135 use std::fs;
136 let home = tempdir().unwrap();
137 let storage = home.path().join(rel_storage);
138 for ws in ids {
139 let dir = storage.join(ws);
140 fs::create_dir_all(&dir).unwrap();
141 fs::write(dir.join("state.vscdb"), b"").unwrap();
142 }
143 let snap = CursorDetector.detect(home.path());
144 assert_eq!(snap.status, InstallStatus::Yes);
145 assert_eq!(snap.sessions, Some(ids.len() as u64));
146 }
147
148 #[test]
149 #[cfg(target_os = "macos")]
150 fn counts_workspace_subdirs_macos() {
151 assert_counts_workspaces(
152 "Library/Application Support/Cursor/User/workspaceStorage",
153 &["aaaa", "bbbb", "cccc"],
154 );
155 }
156
157 #[test]
158 #[cfg(target_os = "linux")]
159 fn counts_workspace_subdirs_linux() {
160 assert_counts_workspaces(".config/Cursor/User/workspaceStorage", &["aaaa", "bbbb"]);
161 }
162}