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>,
71 #[serde(default)]
73 pub tags: Vec<String>,
74 #[serde(default = "default_emoji")]
76 pub emoji: String,
77 pub source: ProjectSource,
79 #[serde(default = "default_true")]
81 pub memory_visible: bool,
82 pub created_at: DateTime<Utc>,
84 pub updated_at: DateTime<Utc>,
86 pub last_active_at: DateTime<Utc>,
88
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub mount_ids: Vec<crate::mount::MountId>,
94 #[serde(default, skip_serializing_if = "String::is_empty")]
97 pub instructions: String,
98}
99
100fn default_emoji() -> String {
101 "📦".to_string()
102}
103
104fn default_true() -> bool {
105 true
106}
107
108impl Project {
109 pub fn new(name: impl Into<String>, source: ProjectSource) -> Self {
111 let now = Utc::now();
112 Self {
113 id: ProjectId::new_v4(),
114 name: name.into(),
115 description: String::new(),
116 paths: Vec::new(),
117 tags: Vec::new(),
118 emoji: default_emoji(),
119 source,
120 memory_visible: true,
121 created_at: now,
122 updated_at: now,
123 last_active_at: now,
124 mount_ids: Vec::new(),
125 instructions: String::new(),
126 }
127 }
128
129 pub fn from_path(path: &Path) -> Self {
133 let name = path
134 .file_name()
135 .and_then(|n| n.to_str())
136 .unwrap_or("unknown")
137 .to_string();
138 let mut project = Self::new(&name, ProjectSource::AutoDetected);
139 project.paths.push(path.to_path_buf());
140 project
141 }
142
143 pub fn touch(&mut self) {
145 self.last_active_at = Utc::now();
146 self.updated_at = Utc::now();
147 }
148
149 pub fn add_path(&mut self, path: PathBuf) {
151 if !self.paths.contains(&path) {
152 self.paths.push(path.clone());
153 self.updated_at = Utc::now();
154 }
155 }
156
157 pub fn remove_path(&mut self, path: &PathBuf) -> bool {
159 if let Some(pos) = self.paths.iter().position(|p| p == path) {
160 self.paths.remove(pos);
161 self.updated_at = Utc::now();
162 true
163 } else {
164 false
165 }
166 }
167
168 pub fn add_tag(&mut self, tag: impl Into<String>) {
170 let tag = tag.into();
171 if !self.tags.contains(&tag) {
172 self.tags.push(tag);
173 self.updated_at = Utc::now();
174 }
175 }
176
177 pub fn has_paths(&self) -> bool {
179 !self.paths.is_empty()
180 }
181
182 pub fn primary_path(&self) -> Option<&PathBuf> {
184 self.paths.first()
185 }
186
187 pub fn tag(&self) -> String {
189 format!("[{} {}]", self.emoji, self.name)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_project_new() {
199 let p = Project::new("oxios", ProjectSource::Manual);
200 assert_eq!(p.name, "oxios");
201 assert_eq!(p.source, ProjectSource::Manual);
202 assert!(p.paths.is_empty());
203 assert_eq!(p.emoji, "📦");
204 }
205
206 #[test]
207 fn test_project_from_path() {
208 let path = PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios");
209 let p = Project::from_path(&path);
210 assert_eq!(p.name, "oxios");
211 assert_eq!(p.source, ProjectSource::AutoDetected);
212 assert_eq!(p.paths, vec![path]);
213 }
214
215 #[test]
216 fn test_project_add_path() {
217 let mut p = Project::new("oxios", ProjectSource::Manual);
218 assert!(!p.has_paths());
219
220 p.add_path(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
221 assert!(p.has_paths());
222 assert_eq!(
223 p.primary_path(),
224 Some(&PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
225 );
226
227 p.add_path(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
229 assert_eq!(p.paths.len(), 1);
230 }
231
232 #[test]
233 fn test_project_tag() {
234 let mut p = Project::new("oxios", ProjectSource::Manual);
235 p.emoji = "🔧".to_string();
236 assert_eq!(p.tag(), "[🔧 oxios]");
237 }
238}