pub mod detection;
pub mod manager;
pub mod meta_detection;
pub mod mount_db;
pub mod path_promotion;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::SystemTime;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use detection::{DetectionResult, detect_mounts, extract_path, find_by_id, find_by_name};
pub use manager::{MountManager, MountManagerError};
pub use meta_detection::{detect_meta, snapshot_markers};
pub use path_promotion::{PathFrequency, PromotionConfig};
pub type MountId = Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MountSource {
#[default]
Manual,
AutoDetected,
AutoPromoted,
}
impl std::fmt::Display for MountSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MountSource::Manual => write!(f, "manual"),
MountSource::AutoDetected => write!(f, "auto_detected"),
MountSource::AutoPromoted => write!(f, "auto_promoted"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MountMeta {
#[serde(default)]
pub languages: Vec<String>,
#[serde(default)]
pub stack: Vec<String>,
#[serde(default)]
pub markers: Vec<String>,
#[serde(default)]
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Mount {
pub id: MountId,
pub name: String,
pub paths: Vec<PathBuf>,
#[serde(default)]
pub auto_description: String,
#[serde(default)]
pub auto_meta: MountMeta,
pub source: MountSource,
#[serde(default)]
pub last_marker_snapshot: HashMap<PathBuf, SystemTime>,
#[serde(default)]
pub enrichment_pending: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_enriched_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_active_at: DateTime<Utc>,
}
impl Mount {
pub fn new(name: impl Into<String>, source: MountSource) -> Self {
let now = Utc::now();
Self {
id: MountId::new_v4(),
name: name.into(),
paths: Vec::new(),
auto_description: String::new(),
auto_meta: MountMeta::default(),
source,
last_marker_snapshot: HashMap::new(),
enrichment_pending: false,
last_enriched_at: None,
created_at: now,
updated_at: now,
last_active_at: now,
}
}
pub fn from_name_and_path(name: impl Into<String>, path: PathBuf) -> Self {
let mut mount = Self::new(name, MountSource::Manual);
mount.paths.push(path);
mount
}
pub fn touch(&mut self) {
self.last_active_at = Utc::now();
}
pub fn has_paths(&self) -> bool {
!self.paths.is_empty()
}
pub fn primary_path(&self) -> Option<&PathBuf> {
self.paths.first()
}
pub fn summary_line(&self) -> String {
if !self.auto_meta.summary.is_empty() {
return self.auto_meta.summary.clone();
}
if !self.auto_description.is_empty() {
return self
.auto_description
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("")
.trim()
.to_string();
}
if !self.auto_meta.languages.is_empty() {
return self.auto_meta.languages.join(", ");
}
String::new()
}
pub fn tag(&self) -> String {
format!("[🔧 {}]", self.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mount_new() {
let m = Mount::new("oxios", MountSource::Manual);
assert_eq!(m.name, "oxios");
assert_eq!(m.source, MountSource::Manual);
assert!(m.paths.is_empty());
assert!(!m.enrichment_pending);
}
#[test]
fn test_mount_from_name_and_path() {
let m =
Mount::from_name_and_path("oxios", PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
assert_eq!(m.name, "oxios");
assert!(m.has_paths());
assert_eq!(
m.primary_path(),
Some(&PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
);
}
#[test]
fn test_mount_tag() {
let m = Mount::new("oxios", MountSource::Manual);
assert_eq!(m.tag(), "[🔧 oxios]");
}
#[test]
fn test_summary_line_prefers_meta_summary() {
let mut m = Mount::new("oxios", MountSource::Manual);
m.auto_description = "Detailed description.\nSecond line.".to_string();
m.auto_meta.summary = "Agent OS in Rust".to_string();
m.auto_meta.languages = vec!["rust".to_string()];
assert_eq!(m.summary_line(), "Agent OS in Rust");
}
#[test]
fn test_summary_line_falls_back_to_description() {
let mut m = Mount::new("oxios", MountSource::Manual);
m.auto_description = "First line.\nSecond.".to_string();
assert_eq!(m.summary_line(), "First line.");
}
#[test]
fn test_summary_line_falls_back_to_languages() {
let mut m = Mount::new("oxios", MountSource::Manual);
m.auto_meta.languages = vec!["rust".to_string(), "typescript".to_string()];
assert_eq!(m.summary_line(), "rust, typescript");
}
}