oxios_kernel/project/
mod.rs1pub mod conversation_buffer;
15pub mod detection;
16pub mod manager;
17pub mod project_db;
18
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use std::path::{Path, PathBuf};
22use uuid::Uuid;
23
24pub use conversation_buffer::{ConversationBuffer, ConversationTurn};
26pub use detection::{DetectionResult, detect_project, extract_path, find_by_id, find_by_name};
27
28pub use manager::{ProjectManager, ProjectManagerError};
29
30pub type ProjectId = Uuid;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ProjectSource {
37 Manual,
39 AutoDetected,
41}
42
43impl std::fmt::Display for ProjectSource {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 ProjectSource::Manual => write!(f, "manual"),
47 ProjectSource::AutoDetected => write!(f, "auto_detected"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Project {
58 pub id: ProjectId,
60 pub name: String,
62 pub description: String,
64 pub paths: Vec<PathBuf>,
67 #[serde(default)]
69 pub tags: Vec<String>,
70 #[serde(default = "default_emoji")]
72 pub emoji: String,
73 pub source: ProjectSource,
75 #[serde(default = "default_true")]
77 pub memory_visible: bool,
78 pub created_at: DateTime<Utc>,
80 pub updated_at: DateTime<Utc>,
82 pub last_active_at: DateTime<Utc>,
84
85 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub mount_ids: Vec<crate::mount::MountId>,
90 #[serde(default, skip_serializing_if = "String::is_empty")]
93 pub instructions: String,
94}
95
96fn default_emoji() -> String {
97 "📦".to_string()
98}
99
100fn default_true() -> bool {
101 true
102}
103
104impl Project {
105 pub fn new(name: impl Into<String>, source: ProjectSource) -> Self {
107 let now = Utc::now();
108 Self {
109 id: ProjectId::new_v4(),
110 name: name.into(),
111 description: String::new(),
112 paths: Vec::new(),
113 tags: Vec::new(),
114 emoji: default_emoji(),
115 source,
116 memory_visible: true,
117 created_at: now,
118 updated_at: now,
119 last_active_at: now,
120 mount_ids: Vec::new(),
121 instructions: String::new(),
122 }
123 }
124
125 pub fn from_path(path: &Path) -> Self {
129 let name = path
130 .file_name()
131 .and_then(|n| n.to_str())
132 .unwrap_or("unknown")
133 .to_string();
134 let mut project = Self::new(&name, ProjectSource::AutoDetected);
135 project.paths.push(path.to_path_buf());
136 project
137 }
138
139 pub fn touch(&mut self) {
141 self.last_active_at = Utc::now();
142 self.updated_at = Utc::now();
143 }
144
145 pub fn add_path(&mut self, path: PathBuf) {
147 if !self.paths.contains(&path) {
148 self.paths.push(path.clone());
149 self.updated_at = Utc::now();
150 }
151 }
152
153 pub fn remove_path(&mut self, path: &PathBuf) -> bool {
155 if let Some(pos) = self.paths.iter().position(|p| p == path) {
156 self.paths.remove(pos);
157 self.updated_at = Utc::now();
158 true
159 } else {
160 false
161 }
162 }
163
164 pub fn add_tag(&mut self, tag: impl Into<String>) {
166 let tag = tag.into();
167 if !self.tags.contains(&tag) {
168 self.tags.push(tag);
169 self.updated_at = Utc::now();
170 }
171 }
172
173 pub fn has_paths(&self) -> bool {
175 !self.paths.is_empty()
176 }
177
178 pub fn primary_path(&self) -> Option<&PathBuf> {
180 self.paths.first()
181 }
182
183 pub fn tag(&self) -> String {
185 format!("[{} {}]", self.emoji, self.name)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_project_new() {
195 let p = Project::new("oxios", ProjectSource::Manual);
196 assert_eq!(p.name, "oxios");
197 assert_eq!(p.source, ProjectSource::Manual);
198 assert!(p.paths.is_empty());
199 assert_eq!(p.emoji, "📦");
200 }
201
202 #[test]
203 fn test_project_from_path() {
204 let path = PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios");
205 let p = Project::from_path(&path);
206 assert_eq!(p.name, "oxios");
207 assert_eq!(p.source, ProjectSource::AutoDetected);
208 assert_eq!(p.paths, vec![path]);
209 }
210
211 #[test]
212 fn test_project_add_path() {
213 let mut p = Project::new("oxios", ProjectSource::Manual);
214 assert!(!p.has_paths());
215
216 p.add_path(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
217 assert!(p.has_paths());
218 assert_eq!(
219 p.primary_path(),
220 Some(&PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
221 );
222
223 p.add_path(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
225 assert_eq!(p.paths.len(), 1);
226 }
227
228 #[test]
229 fn test_project_tag() {
230 let mut p = Project::new("oxios", ProjectSource::Manual);
231 p.emoji = "🔧".to_string();
232 assert_eq!(p.tag(), "[🔧 oxios]");
233 }
234}