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::{DetectionResult, Project, ProjectId, ProjectSource, detect_project};
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 project_db::ensure_project_schema(&db.conn())?;
59
60 let mut projects = HashMap::new();
61 let mut name_index = HashMap::new();
62
63 let rows = project_db::list_projects(&db.conn())?;
65 for project in rows {
66 name_index.insert(project.name.clone(), project.id);
67 projects.insert(project.id, project);
68 }
69
70 tracing::info!(count = projects.len(), "ProjectManager initialized");
71 Ok(Self {
72 projects: RwLock::new(projects),
73 name_index: RwLock::new(name_index),
74 db,
75 event_bus,
76 })
77 }
78
79 pub fn list_projects(&self) -> Vec<Project> {
81 self.projects.read().values().cloned().collect()
82 }
83
84 pub fn get_project(&self, id: ProjectId) -> Option<Project> {
86 self.projects.read().get(&id).cloned()
87 }
88
89 pub fn get_project_by_name(&self, name: &str) -> Option<Project> {
91 let name_index = self.name_index.read();
92 let id = name_index.get(name)?;
93 self.projects.read().get(id).cloned()
94 }
95
96 pub fn create_project(
98 &self,
99 name: String,
100 paths: Vec<PathBuf>,
101 tags: Vec<String>,
102 emoji: Option<String>,
103 description: Option<String>,
104 source: ProjectSource,
105 ) -> Result<Project> {
106 {
108 let name_index = self.name_index.read();
109 if name_index.contains_key(&name) {
110 return Err(ProjectManagerError::DuplicateName(name).into());
111 }
112 }
113
114 let mut project = Project::new(&name, source);
115 project.paths = paths;
116 project.tags = tags;
117 if let Some(emoji) = emoji {
118 project.emoji = emoji;
119 }
120 if let Some(description) = description {
121 project.description = description;
122 }
123
124 project_db::save_project(&self.db.conn(), &project)?;
126
127 {
129 let mut projects = self.projects.write();
130 let mut name_index = self.name_index.write();
131 name_index.insert(project.name.clone(), project.id);
132 projects.insert(project.id, project.clone());
133 }
134
135 if let Some(ref event_bus) = self.event_bus {
137 let _ = event_bus.publish(KernelEvent::ProjectCreated {
138 project_id: project.id,
139 name: project.name.clone(),
140 source: source.to_string(),
141 });
142 }
143
144 tracing::info!(name = %project.name, id = %project.id, "Project created");
145 Ok(project)
146 }
147
148 pub fn update_project(
150 &self,
151 id: ProjectId,
152 name: Option<String>,
153 paths: Option<Vec<PathBuf>>,
154 tags: Option<Vec<String>>,
155 emoji: Option<String>,
156 description: Option<String>,
157 ) -> Result<Project> {
158 let mut projects = self.projects.write();
159 let mut name_index = self.name_index.write();
160
161 let project = projects
162 .get_mut(&id)
163 .ok_or(ProjectManagerError::NotFound(id))?;
164
165 if let Some(ref new_name) = name
167 && *new_name != project.name
168 {
169 if name_index.contains_key(new_name) {
170 return Err(ProjectManagerError::DuplicateName(new_name.clone()).into());
171 }
172 name_index.remove(&project.name);
174 name_index.insert(new_name.clone(), id);
175 project.name = new_name.clone();
176 }
177
178 if let Some(paths) = paths {
179 project.paths = paths;
180 }
181 if let Some(tags) = tags {
182 project.tags = tags;
183 }
184 if let Some(emoji) = emoji {
185 project.emoji = emoji;
186 }
187 if let Some(description) = description {
188 project.description = description;
189 }
190
191 project.updated_at = Utc::now();
192
193 let project_clone = project.clone();
195 drop(projects);
196 drop(name_index);
197 project_db::save_project(&self.db.conn(), &project_clone)?;
198
199 tracing::info!(name = %project_clone.name, id = %id, "Project updated");
200 Ok(project_clone)
201 }
202
203 pub fn remove_project(&self, id: ProjectId) -> Result<()> {
205 {
206 let mut projects = self.projects.write();
207 let mut name_index = self.name_index.write();
208
209 let project = projects
210 .remove(&id)
211 .ok_or(ProjectManagerError::NotFound(id))?;
212 name_index.remove(&project.name);
213 }
214
215 project_db::delete_project(&self.db.conn(), &id.to_string())?;
217
218 tracing::info!(id = %id, "Project removed");
219 Ok(())
220 }
221
222 pub fn touch(&self, id: ProjectId) {
224 if let Some(project) = self.projects.write().get_mut(&id) {
225 project.touch();
226 let project_clone = project.clone();
227 drop(self.projects.write());
228 let _ = project_db::save_project(&self.db.conn(), &project_clone);
229 }
230 }
231
232 pub fn detect(&self, message: &str) -> DetectionResult {
236 let projects = self.list_projects();
237 detect_project(message, &projects)
238 }
239
240 pub fn link_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
242 {
243 let projects = self.projects.read();
244 if !projects.contains_key(&project_id) {
245 return Err(ProjectManagerError::NotFound(project_id).into());
246 }
247 }
248 project_db::link_project_memory(&self.db.conn(), &project_id.to_string(), memory_id)?;
249 Ok(())
250 }
251
252 pub fn unlink_memory(&self, project_id: ProjectId, memory_id: &str) -> Result<()> {
254 project_db::unlink_project_memory(&self.db.conn(), &project_id.to_string(), memory_id)?;
255 Ok(())
256 }
257
258 pub fn get_project_memory_ids(&self, project_id: ProjectId) -> Result<Vec<String>> {
260 project_db::get_project_memory_ids(&self.db.conn(), &project_id.to_string())
261 }
262
263 pub fn save_project(&self, project: &Project) -> Result<()> {
268 project_db::save_project(&self.db.conn(), project)?;
269
270 let mut projects = self.projects.write();
272 let mut name_index = self.name_index.write();
273 name_index.insert(project.name.clone(), project.id);
274 projects.insert(project.id, project.clone());
275
276 Ok(())
277 }
278
279 pub fn update_project_bundle(
281 &self,
282 id: ProjectId,
283 mount_ids: Option<Vec<crate::mount::MountId>>,
284 instructions: Option<String>,
285 ) -> Result<Project> {
286 let mut projects = self.projects.write();
287 let project = projects
288 .get_mut(&id)
289 .ok_or(ProjectManagerError::NotFound(id))?;
290
291 if let Some(mids) = mount_ids {
292 project.mount_ids = mids;
293 }
294 if let Some(instr) = instructions {
295 project.instructions = instr;
296 }
297 project.updated_at = Utc::now();
298
299 let project_clone = project.clone();
300 drop(projects);
301 project_db::save_project(&self.db.conn(), &project_clone)?;
302 Ok(project_clone)
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
314 fn test_project_manager_error_display() {
315 let id = ProjectId::new_v4();
316 let err = ProjectManagerError::NotFound(id);
317 assert!(err.to_string().contains("Project not found"));
318
319 let err = ProjectManagerError::DuplicateName("test".to_string());
320 assert!(err.to_string().contains("already exists"));
321 }
322}