oxios_kernel/project/
manager.rs1use std::collections::HashMap;
10use std::sync::Arc;
11
12use anyhow::Result;
13use chrono::Utc;
14use parking_lot::RwLock;
15
16use oxios_memory::memory::sqlite::MemoryDatabase;
17
18use super::project_db;
19use super::{DetectionResult, Project, ProjectId, ProjectSource, detect_project};
20use crate::event_bus::{EventBus, KernelEvent};
21
22#[derive(thiserror::Error, Debug)]
24pub enum ProjectManagerError {
25 #[error("Project not found: {0}")]
27 NotFound(ProjectId),
28 #[error("Project name already exists: {0}")]
30 DuplicateName(String),
31 #[error("Invalid operation: {0}")]
33 Invalid(String),
34}
35
36pub struct ProjectManager {
41 projects: RwLock<HashMap<ProjectId, Project>>,
43 name_index: RwLock<HashMap<String, ProjectId>>,
45 db: Arc<MemoryDatabase>,
47 event_bus: Option<EventBus>,
49}
50
51impl ProjectManager {
52 pub fn new(db: Arc<MemoryDatabase>, event_bus: Option<EventBus>) -> Result<Self> {
54 project_db::ensure_project_schema(&db.conn())?;
58
59 let mut projects = HashMap::new();
60 let mut name_index = HashMap::new();
61
62 let rows = project_db::list_projects(&db.conn())?;
64 for project in rows {
65 name_index.insert(project.name.clone(), project.id);
66 projects.insert(project.id, project);
67 }
68
69 tracing::info!(count = projects.len(), "ProjectManager initialized");
70 Ok(Self {
71 projects: RwLock::new(projects),
72 name_index: RwLock::new(name_index),
73 db,
74 event_bus,
75 })
76 }
77
78 pub fn list_projects(&self) -> Vec<Project> {
80 self.projects.read().values().cloned().collect()
81 }
82
83 pub fn get_project(&self, id: ProjectId) -> Option<Project> {
85 self.projects.read().get(&id).cloned()
86 }
87
88 pub fn get_project_by_name(&self, name: &str) -> Option<Project> {
90 let name_index = self.name_index.read();
91 let id = name_index.get(name)?;
92 self.projects.read().get(id).cloned()
93 }
94
95 pub fn create_project(
97 &self,
98 name: String,
99 tags: Vec<String>,
100 emoji: Option<String>,
101 description: Option<String>,
102 source: ProjectSource,
103 ) -> Result<Project> {
104 {
106 let name_index = self.name_index.read();
107 if name_index.contains_key(&name) {
108 return Err(ProjectManagerError::DuplicateName(name).into());
109 }
110 }
111
112 let mut project = Project::new(&name, source);
113 project.tags = tags;
114 if let Some(emoji) = emoji {
115 project.emoji = emoji;
116 }
117 if let Some(description) = description {
118 project.description = description;
119 }
120
121 project_db::save_project(&self.db.conn(), &project)?;
123
124 {
126 let mut projects = self.projects.write();
127 let mut name_index = self.name_index.write();
128 name_index.insert(project.name.clone(), project.id);
129 projects.insert(project.id, project.clone());
130 }
131
132 if let Some(ref event_bus) = self.event_bus {
134 let _ = event_bus.publish(KernelEvent::ProjectCreated {
135 project_id: project.id,
136 name: project.name.clone(),
137 source: source.to_string(),
138 });
139 }
140
141 tracing::info!(name = %project.name, id = %project.id, "Project created");
142 Ok(project)
143 }
144
145 pub fn update_project(
147 &self,
148 id: ProjectId,
149 name: Option<String>,
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 && *new_name != project.name
164 {
165 if name_index.contains_key(new_name) {
166 return Err(ProjectManagerError::DuplicateName(new_name.clone()).into());
167 }
168 name_index.remove(&project.name);
170 name_index.insert(new_name.clone(), id);
171 project.name = new_name.clone();
172 }
173
174 if let Some(tags) = tags {
175 project.tags = tags;
176 }
177 if let Some(emoji) = emoji {
178 project.emoji = emoji;
179 }
180 if let Some(description) = description {
181 project.description = description;
182 }
183
184 project.updated_at = Utc::now();
185
186 let project_clone = project.clone();
188 drop(projects);
189 drop(name_index);
190 project_db::save_project(&self.db.conn(), &project_clone)?;
191
192 tracing::info!(name = %project_clone.name, id = %id, "Project updated");
193 Ok(project_clone)
194 }
195
196 pub fn remove_project(&self, id: ProjectId) -> Result<()> {
198 {
199 let mut projects = self.projects.write();
200 let mut name_index = self.name_index.write();
201
202 let project = projects
203 .remove(&id)
204 .ok_or(ProjectManagerError::NotFound(id))?;
205 name_index.remove(&project.name);
206 }
207
208 project_db::delete_project(&self.db.conn(), &id.to_string())?;
210
211 tracing::info!(id = %id, "Project removed");
212 Ok(())
213 }
214
215 pub fn touch(&self, id: ProjectId) {
217 if let Some(project) = self.projects.write().get_mut(&id) {
218 project.touch();
219 let project_clone = project.clone();
220 drop(self.projects.write());
221 let _ = project_db::save_project(&self.db.conn(), &project_clone);
222 }
223 }
224
225 pub fn detect(&self, message: &str) -> DetectionResult {
229 let projects = self.list_projects();
230 detect_project(message, &projects)
231 }
232
233 pub fn link_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
235 {
236 let projects = self.projects.read();
237 if !projects.contains_key(&project_id) {
238 return Err(ProjectManagerError::NotFound(project_id).into());
239 }
240 }
241 project_db::link_project_memory(&self.db.conn(), &project_id.to_string(), memory_id)?;
242 Ok(())
243 }
244
245 pub fn unlink_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
247 project_db::unlink_project_memory(&self.db.conn(), &project_id.to_string(), memory_id)?;
248 Ok(())
249 }
250
251 pub fn get_project_memory_ids(&self, project_id: ProjectId) -> Result<Vec<String>> {
253 project_db::get_project_memory_ids(&self.db.conn(), &project_id.to_string())
254 }
255
256 pub fn save_project(&self, project: &Project) -> Result<()> {
261 project_db::save_project(&self.db.conn(), project)?;
262
263 let mut projects = self.projects.write();
265 let mut name_index = self.name_index.write();
266 name_index.insert(project.name.clone(), project.id);
267 projects.insert(project.id, project.clone());
268
269 Ok(())
270 }
271
272 pub fn update_project_bundle(
274 &self,
275 id: ProjectId,
276 mount_ids: Option<Vec<crate::mount::MountId>>,
277 instructions: Option<String>,
278 ) -> Result<Project> {
279 let mut projects = self.projects.write();
280 let project = projects
281 .get_mut(&id)
282 .ok_or(ProjectManagerError::NotFound(id))?;
283
284 if let Some(mids) = mount_ids {
285 project.mount_ids = mids;
286 }
287 if let Some(instr) = instructions {
288 project.instructions = instr;
289 }
290 project.updated_at = Utc::now();
291
292 let project_clone = project.clone();
293 drop(projects);
294 project_db::save_project(&self.db.conn(), &project_clone)?;
295 Ok(project_clone)
296 }
297 pub fn clear_legacy_paths(&self, id: ProjectId) -> Result<()> {
302 let mut projects = self.projects.write();
303 let project = projects
304 .get_mut(&id)
305 .ok_or(ProjectManagerError::NotFound(id))?;
306 project.paths.clear();
307 project.updated_at = Utc::now();
308 let project_clone = project.clone();
309 drop(projects);
310 project_db::save_project(&self.db.conn(), &project_clone)?;
311 Ok(())
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
323 fn test_project_manager_error_display() {
324 let id = ProjectId::new_v4();
325 let err = ProjectManagerError::NotFound(id);
326 assert!(err.to_string().contains("Project not found"));
327
328 let err = ProjectManagerError::DuplicateName("test".to_string());
329 assert!(err.to_string().contains("already exists"));
330 }
331}