intent_engine/
global_projects.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9
10const GLOBAL_DIR: &str = ".intent-engine";
11const PROJECTS_FILE: &str = "projects.json";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ProjectEntry {
16 pub path: String,
18 pub last_accessed: DateTime<Utc>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub name: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct ProjectsRegistry {
28 pub projects: Vec<ProjectEntry>,
29}
30
31impl ProjectsRegistry {
32 pub fn registry_path() -> Option<PathBuf> {
34 dirs::home_dir().map(|h| h.join(GLOBAL_DIR).join(PROJECTS_FILE))
35 }
36
37 pub fn load() -> Self {
39 let Some(path) = Self::registry_path() else {
40 return Self::default();
41 };
42
43 if !path.exists() {
44 return Self::default();
45 }
46
47 match std::fs::read_to_string(&path) {
48 Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
49 Err(_) => Self::default(),
50 }
51 }
52
53 pub fn save(&self) -> std::io::Result<()> {
55 let Some(path) = Self::registry_path() else {
56 return Ok(());
57 };
58
59 if let Some(parent) = path.parent() {
61 std::fs::create_dir_all(parent)?;
62 }
63
64 let content = serde_json::to_string_pretty(self)?;
65 std::fs::write(&path, content)
66 }
67
68 pub fn register_project(&mut self, project_path: &Path) {
70 let path_str = project_path.to_string_lossy().to_string();
71 let now = Utc::now();
72
73 if let Some(entry) = self.projects.iter_mut().find(|p| p.path == path_str) {
75 entry.last_accessed = now;
76 } else {
77 let name = project_path
79 .file_name()
80 .and_then(|n| n.to_str())
81 .map(|s| s.to_string());
82
83 self.projects.push(ProjectEntry {
84 path: path_str,
85 last_accessed: now,
86 name,
87 });
88 }
89 }
90
91 pub fn remove_project(&mut self, project_path: &str) -> bool {
93 let initial_len = self.projects.len();
94 self.projects.retain(|p| p.path != project_path);
95 self.projects.len() < initial_len
96 }
97
98 pub fn get_projects(&self) -> Vec<&ProjectEntry> {
100 let mut projects: Vec<_> = self.projects.iter().collect();
101 projects.sort_by(|a, b| b.last_accessed.cmp(&a.last_accessed));
102 projects
103 }
104
105 pub fn validate_project(path: &str) -> bool {
107 let project_path = PathBuf::from(path);
108 let db_path = project_path.join(".intent-engine").join("project.db");
109 db_path.exists()
110 }
111}
112
113pub fn register_project(project_path: &Path) {
115 let mut registry = ProjectsRegistry::load();
116 registry.register_project(project_path);
117 if let Err(e) = registry.save() {
118 tracing::warn!(error = %e, "Failed to save global projects registry");
119 }
120}
121
122pub fn remove_project(project_path: &str) -> bool {
124 let mut registry = ProjectsRegistry::load();
125 let removed = registry.remove_project(project_path);
126 if removed {
127 if let Err(e) = registry.save() {
128 tracing::warn!(error = %e, "Failed to save global projects registry");
129 }
130 }
131 removed
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use tempfile::TempDir;
138
139 #[test]
140 fn test_project_entry_serialization() {
141 let entry = ProjectEntry {
142 path: "/test/project".to_string(),
143 last_accessed: Utc::now(),
144 name: Some("project".to_string()),
145 };
146
147 let json = serde_json::to_string(&entry).unwrap();
148 let parsed: ProjectEntry = serde_json::from_str(&json).unwrap();
149 assert_eq!(parsed.path, entry.path);
150 }
151
152 #[test]
153 fn test_registry_register_and_remove() {
154 let mut registry = ProjectsRegistry::default();
155
156 let temp = TempDir::new().unwrap();
158 registry.register_project(temp.path());
159 assert_eq!(registry.projects.len(), 1);
160
161 registry.register_project(temp.path());
163 assert_eq!(registry.projects.len(), 1);
164
165 let path_str = temp.path().to_string_lossy().to_string();
167 assert!(registry.remove_project(&path_str));
168 assert_eq!(registry.projects.len(), 0);
169 }
170
171 #[test]
172 fn test_registry_get_projects_sorted() {
173 let mut registry = ProjectsRegistry::default();
174
175 registry.projects.push(ProjectEntry {
177 path: "/old".to_string(),
178 last_accessed: Utc::now() - chrono::Duration::hours(2),
179 name: None,
180 });
181 registry.projects.push(ProjectEntry {
182 path: "/new".to_string(),
183 last_accessed: Utc::now(),
184 name: None,
185 });
186
187 let projects = registry.get_projects();
188 assert_eq!(projects[0].path, "/new");
189 assert_eq!(projects[1].path, "/old");
190 }
191}