oxios_kernel/mount/
mod.rs1pub mod detection;
20pub mod manager;
21pub mod meta_detection;
22pub mod mount_db;
23pub mod path_promotion;
24
25use std::collections::HashMap;
26use std::path::PathBuf;
27use std::time::SystemTime;
28
29use chrono::{DateTime, Utc};
30use serde::{Deserialize, Serialize};
31use uuid::Uuid;
32
33pub use detection::{DetectionResult, detect_mounts, extract_path, find_by_id, find_by_name};
35pub use manager::{MountManager, MountManagerError};
36pub use meta_detection::{detect_meta, snapshot_markers};
37pub use path_promotion::{PathFrequency, PromotionConfig};
38
39pub type MountId = Uuid;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum MountSource {
46 #[default]
48 Manual,
49 AutoDetected,
51 AutoPromoted,
55}
56
57impl std::fmt::Display for MountSource {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 MountSource::Manual => write!(f, "manual"),
61 MountSource::AutoDetected => write!(f, "auto_detected"),
62 MountSource::AutoPromoted => write!(f, "auto_promoted"),
63 }
64 }
65}
66
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73pub struct MountMeta {
74 #[serde(default)]
76 pub languages: Vec<String>,
77 #[serde(default)]
79 pub stack: Vec<String>,
80 #[serde(default)]
82 pub markers: Vec<String>,
83 #[serde(default)]
85 pub summary: String,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Mount {
96 pub id: MountId,
98 pub name: String,
100 pub paths: Vec<PathBuf>,
103 #[serde(default)]
105 pub auto_description: String,
106 #[serde(default)]
108 pub auto_meta: MountMeta,
109 pub source: MountSource,
111
112 #[serde(default)]
116 pub last_marker_snapshot: HashMap<PathBuf, SystemTime>,
117 #[serde(default)]
119 pub enrichment_pending: bool,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub last_enriched_at: Option<DateTime<Utc>>,
123
124 pub created_at: DateTime<Utc>,
126 pub updated_at: DateTime<Utc>,
128 pub last_active_at: DateTime<Utc>,
130}
131
132impl Mount {
133 pub fn new(name: impl Into<String>, source: MountSource) -> Self {
135 let now = Utc::now();
136 Self {
137 id: MountId::new_v4(),
138 name: name.into(),
139 paths: Vec::new(),
140 auto_description: String::new(),
141 auto_meta: MountMeta::default(),
142 source,
143 last_marker_snapshot: HashMap::new(),
144 enrichment_pending: false,
145 last_enriched_at: None,
146 created_at: now,
147 updated_at: now,
148 last_active_at: now,
149 }
150 }
151
152 pub fn from_name_and_path(name: impl Into<String>, path: PathBuf) -> Self {
155 let mut mount = Self::new(name, MountSource::Manual);
156 mount.paths.push(path);
157 mount
158 }
159
160 pub fn touch(&mut self) {
162 self.last_active_at = Utc::now();
163 }
164
165 pub fn has_paths(&self) -> bool {
167 !self.paths.is_empty()
168 }
169
170 pub fn primary_path(&self) -> Option<&PathBuf> {
172 self.paths.first()
173 }
174
175 pub fn summary_line(&self) -> String {
178 if !self.auto_meta.summary.is_empty() {
179 return self.auto_meta.summary.clone();
180 }
181 if !self.auto_description.is_empty() {
182 return self
184 .auto_description
185 .lines()
186 .find(|l| !l.trim().is_empty())
187 .unwrap_or("")
188 .trim()
189 .to_string();
190 }
191 if !self.auto_meta.languages.is_empty() {
192 return self.auto_meta.languages.join(", ");
193 }
194 String::new()
195 }
196
197 pub fn tag(&self) -> String {
199 format!("[🔧 {}]", self.name)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_mount_new() {
209 let m = Mount::new("oxios", MountSource::Manual);
210 assert_eq!(m.name, "oxios");
211 assert_eq!(m.source, MountSource::Manual);
212 assert!(m.paths.is_empty());
213 assert!(!m.enrichment_pending);
214 }
215
216 #[test]
217 fn test_mount_from_name_and_path() {
218 let m =
219 Mount::from_name_and_path("oxios", PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
220 assert_eq!(m.name, "oxios");
221 assert!(m.has_paths());
222 assert_eq!(
223 m.primary_path(),
224 Some(&PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
225 );
226 }
227
228 #[test]
229 fn test_mount_tag() {
230 let m = Mount::new("oxios", MountSource::Manual);
231 assert_eq!(m.tag(), "[🔧 oxios]");
232 }
233
234 #[test]
235 fn test_summary_line_prefers_meta_summary() {
236 let mut m = Mount::new("oxios", MountSource::Manual);
237 m.auto_description = "Detailed description.\nSecond line.".to_string();
238 m.auto_meta.summary = "Agent OS in Rust".to_string();
239 m.auto_meta.languages = vec!["rust".to_string()];
240 assert_eq!(m.summary_line(), "Agent OS in Rust");
241 }
242
243 #[test]
244 fn test_summary_line_falls_back_to_description() {
245 let mut m = Mount::new("oxios", MountSource::Manual);
246 m.auto_description = "First line.\nSecond.".to_string();
247 assert_eq!(m.summary_line(), "First line.");
248 }
249
250 #[test]
251 fn test_summary_line_falls_back_to_languages() {
252 let mut m = Mount::new("oxios", MountSource::Manual);
253 m.auto_meta.languages = vec!["rust".to_string(), "typescript".to_string()];
254 assert_eq!(m.summary_line(), "rust, typescript");
255 }
256}