oxios_kernel/project/
manager.rs1use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12
13use anyhow::Result;
14use chrono::Utc;
15use parking_lot::RwLock;
16
17use oxios_memory::memory::sqlite::MemoryDatabase;
18
19use super::project_db;
20use super::{detect_project, DetectionResult, Project, ProjectId, ProjectSource};
21use crate::event_bus::{EventBus, KernelEvent};
22
23#[derive(thiserror::Error, Debug)]
25pub enum ProjectManagerError {
26 #[error("Project not found: {0}")]
28 NotFound(ProjectId),
29 #[error("Project name already exists: {0}")]
31 DuplicateName(String),
32 #[error("Invalid operation: {0}")]
34 Invalid(String),
35}
36
37pub struct ProjectManager {
42 projects: RwLock<HashMap<ProjectId, Project>>,
44 name_index: RwLock<HashMap<String, ProjectId>>,
46 db: Arc<MemoryDatabase>,
48 event_bus: Option<EventBus>,
50}
51
52impl ProjectManager {
53 pub fn new(db: Arc<MemoryDatabase>, event_bus: Option<EventBus>) -> Result<Self> {
55 let mut projects = HashMap::new();
56 let mut name_index = HashMap::new();
57
58 let rows = project_db::list_projects(&db.conn())?;
60 for project in rows {
61 name_index.insert(project.name.clone(), project.id);
62 projects.insert(project.id, project);
63 }
64
65 tracing::info!(count = projects.len(), "ProjectManager initialized");
66
67 Ok(Self {
68 projects: RwLock::new(projects),
69 name_index: RwLock::new(name_index),
70 db,
71 event_bus,
72 })
73 }
74
75 pub fn list_projects(&self) -> Vec<Project> {
77 self.projects.read().values().cloned().collect()
78 }
79
80 pub fn get_project(&self, id: ProjectId) -> Option<Project> {
82 self.projects.read().get(&id).cloned()
83 }
84
85 pub fn get_project_by_name(&self, name: &str) -> Option<Project> {
87 let name_index = self.name_index.read();
88 let id = name_index.get(name)?;
89 self.projects.read().get(id).cloned()
90 }
91
92 pub fn create_project(
94 &self,
95 name: String,
96 paths: Vec<PathBuf>,
97 tags: Vec<String>,
98 emoji: Option<String>,
99 description: Option<String>,
100 source: ProjectSource,
101 ) -> Result<Project> {
102 {
104 let name_index = self.name_index.read();
105 if name_index.contains_key(&name) {
106 return Err(ProjectManagerError::DuplicateName(name).into());
107 }
108 }
109
110 let mut project = Project::new(&name, source);
111 project.paths = paths;
112 project.tags = tags;
113 if let Some(emoji) = emoji {
114 project.emoji = emoji;
115 }
116 if let Some(description) = description {
117 project.description = description;
118 }
119
120 project_db::save_project(&self.db.conn(), &project)?;
122
123 {
125 let mut projects = self.projects.write();
126 let mut name_index = self.name_index.write();
127 name_index.insert(project.name.clone(), project.id);
128 projects.insert(project.id, project.clone());
129 }
130
131 if let Some(ref event_bus) = self.event_bus {
133 let _ = event_bus.publish(KernelEvent::ProjectCreated {
134 project_id: project.id,
135 name: project.name.clone(),
136 source: source.to_string(),
137 });
138 }
139
140 tracing::info!(name = %project.name, id = %project.id, "Project created");
141 Ok(project)
142 }
143
144 pub fn update_project(
146 &self,
147 id: ProjectId,
148 name: Option<String>,
149 paths: Option<Vec<PathBuf>>,
150 tags: Option<Vec<String>>,
151 emoji: Option<String>,
152 description: Option<String>,
153 ) -> Result<Project> {
154 let mut projects = self.projects.write();
155 let mut name_index = self.name_index.write();
156
157 let project = projects
158 .get_mut(&id)
159 .ok_or(ProjectManagerError::NotFound(id))?;
160
161 if let Some(ref new_name) = name {
163 if *new_name != project.name {
164 if name_index.contains_key(new_name) {
165 return Err(ProjectManagerError::DuplicateName(new_name.clone()).into());
166 }
167 name_index.remove(&project.name);
169 name_index.insert(new_name.clone(), id);
170 project.name = new_name.clone();
171 }
172 }
173
174 if let Some(paths) = paths {
175 project.paths = paths;
176 }
177 if let Some(tags) = tags {
178 project.tags = tags;
179 }
180 if let Some(emoji) = emoji {
181 project.emoji = emoji;
182 }
183 if let Some(description) = description {
184 project.description = description;
185 }
186
187 project.updated_at = Utc::now();
188
189 let project_clone = project.clone();
191 drop(projects);
192 drop(name_index);
193 project_db::save_project(&self.db.conn(), &project_clone)?;
194
195 tracing::info!(name = %project_clone.name, id = %id, "Project updated");
196 Ok(project_clone)
197 }
198
199 pub fn remove_project(&self, id: ProjectId) -> Result<()> {
201 {
202 let mut projects = self.projects.write();
203 let mut name_index = self.name_index.write();
204
205 let project = projects
206 .remove(&id)
207 .ok_or(ProjectManagerError::NotFound(id))?;
208 name_index.remove(&project.name);
209 }
210
211 project_db::delete_project(&self.db.conn(), &id.to_string())?;
213
214 tracing::info!(id = %id, "Project removed");
215 Ok(())
216 }
217
218 pub fn touch(&self, id: ProjectId) {
220 if let Some(project) = self.projects.write().get_mut(&id) {
221 project.touch();
222 let project_clone = project.clone();
223 drop(self.projects.write());
224 let _ = project_db::save_project(&self.db.conn(), &project_clone);
225 }
226 }
227
228 pub fn detect(&self, message: &str) -> DetectionResult {
232 let projects = self.list_projects();
233 detect_project(message, &projects)
234 }
235
236 pub fn link_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
238 {
239 let projects = self.projects.read();
240 if !projects.contains_key(&project_id) {
241 return Err(ProjectManagerError::NotFound(project_id).into());
242 }
243 }
244 project_db::link_project_memory(&self.db.conn(), &project_id.to_string(), memory_id)?;
245 Ok(())
246 }
247
248 pub fn unlink_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
250 project_db::unlink_project_memory(&self.db.conn(), &project_id.to_string(), memory_id)?;
251 Ok(())
252 }
253
254 pub fn get_project_memory_ids(&self, project_id: ProjectId) -> Result<Vec<String>> {
256 project_db::get_project_memory_ids(&self.db.conn(), &project_id.to_string())
257 }
258
259 pub fn save_project(&self, project: &Project) -> Result<()> {
264 project_db::save_project(&self.db.conn(), project)?;
265
266 let mut projects = self.projects.write();
268 let mut name_index = self.name_index.write();
269 name_index.insert(project.name.clone(), project.id);
270 projects.insert(project.id, project.clone());
271
272 Ok(())
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
284 fn test_project_manager_error_display() {
285 let id = ProjectId::new_v4();
286 let err = ProjectManagerError::NotFound(id);
287 assert!(err.to_string().contains("Project not found"));
288
289 let err = ProjectManagerError::DuplicateName("test".to_string());
290 assert!(err.to_string().contains("already exists"));
291 }
292}