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::{detect_project, extract_path, find_by_id, find_by_name, DetectionResult};
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
86fn default_emoji() -> String {
87 "📦".to_string()
88}
89
90fn default_true() -> bool {
91 true
92}
93
94impl Project {
95 pub fn new(name: impl Into<String>, source: ProjectSource) -> Self {
97 let now = Utc::now();
98 Self {
99 id: ProjectId::new_v4(),
100 name: name.into(),
101 description: String::new(),
102 paths: Vec::new(),
103 tags: Vec::new(),
104 emoji: default_emoji(),
105 source,
106 memory_visible: true,
107 created_at: now,
108 updated_at: now,
109 last_active_at: now,
110 }
111 }
112
113 pub fn from_path(path: &Path) -> Self {
117 let name = path
118 .file_name()
119 .and_then(|n| n.to_str())
120 .unwrap_or("unknown")
121 .to_string();
122 let mut project = Self::new(&name, ProjectSource::AutoDetected);
123 project.paths.push(path.to_path_buf());
124 project
125 }
126
127 pub fn touch(&mut self) {
129 self.last_active_at = Utc::now();
130 self.updated_at = Utc::now();
131 }
132
133 pub fn add_path(&mut self, path: PathBuf) {
135 if !self.paths.contains(&path) {
136 self.paths.push(path.clone());
137 self.updated_at = Utc::now();
138 }
139 }
140
141 pub fn remove_path(&mut self, path: &PathBuf) -> bool {
143 if let Some(pos) = self.paths.iter().position(|p| p == path) {
144 self.paths.remove(pos);
145 self.updated_at = Utc::now();
146 true
147 } else {
148 false
149 }
150 }
151
152 pub fn add_tag(&mut self, tag: impl Into<String>) {
154 let tag = tag.into();
155 if !self.tags.contains(&tag) {
156 self.tags.push(tag);
157 self.updated_at = Utc::now();
158 }
159 }
160
161 pub fn has_paths(&self) -> bool {
163 !self.paths.is_empty()
164 }
165
166 pub fn primary_path(&self) -> Option<&PathBuf> {
168 self.paths.first()
169 }
170
171 pub fn tag(&self) -> String {
173 format!("[{} {}]", self.emoji, self.name)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_project_new() {
183 let p = Project::new("oxios", ProjectSource::Manual);
184 assert_eq!(p.name, "oxios");
185 assert_eq!(p.source, ProjectSource::Manual);
186 assert!(p.paths.is_empty());
187 assert_eq!(p.emoji, "📦");
188 }
189
190 #[test]
191 fn test_project_from_path() {
192 let path = PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios");
193 let p = Project::from_path(&path);
194 assert_eq!(p.name, "oxios");
195 assert_eq!(p.source, ProjectSource::AutoDetected);
196 assert_eq!(p.paths, vec![path]);
197 }
198
199 #[test]
200 fn test_project_add_path() {
201 let mut p = Project::new("oxios", ProjectSource::Manual);
202 assert!(!p.has_paths());
203
204 p.add_path(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
205 assert!(p.has_paths());
206 assert_eq!(
207 p.primary_path(),
208 Some(&PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
209 );
210
211 p.add_path(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
213 assert_eq!(p.paths.len(), 1);
214 }
215
216 #[test]
217 fn test_project_tag() {
218 let mut p = Project::new("oxios", ProjectSource::Manual);
219 p.emoji = "🔧".to_string();
220 assert_eq!(p.tag(), "[🔧 oxios]");
221 }
222}