use crate::entity::VaultEntity; use crate::error::VaultError; use serde::{de::DeserializeOwned, Serialize}; use std::path::{Path, PathBuf}; /// Read and parse a vault entity from a markdown file. pub fn read_entity(path: &Path) -> Result, VaultError> { let content = std::fs::read_to_string(path).map_err(|e| VaultError::io(e, path))?; VaultEntity::from_content(path.to_path_buf(), &content) } /// Write a vault entity to disk. pub fn write_entity(entity: &VaultEntity) -> Result<(), VaultError> { let content = entity.to_string()?; if let Some(parent) = entity.path.parent() { std::fs::create_dir_all(parent).map_err(|e| VaultError::io(e, parent))?; } std::fs::write(&entity.path, content).map_err(|e| VaultError::io(e, &entity.path)) } /// Move a file from one path to another, creating parent dirs as needed. pub fn move_file(from: &Path, to: &Path) -> Result<(), VaultError> { if let Some(parent) = to.parent() { std::fs::create_dir_all(parent).map_err(|e| VaultError::io(e, parent))?; } std::fs::rename(from, to).map_err(|e| VaultError::io(e, from)) } /// Ensure the standard vault directory structure exists. pub fn ensure_vault_structure(vault_root: &Path) -> Result<(), VaultError> { let dirs = [ "agents", "skills/vault", "crons/active", "crons/paused", "crons/templates", "todos/harald/urgent", "todos/harald/open", "todos/harald/in-progress", "todos/harald/done", "todos/agent/queued", "todos/agent/running", "todos/agent/done", "todos/agent/failed", "knowledge", "views/pages", "views/widgets", "views/layouts", "views/custom", "views/notifications", ".vault/logs", ".vault/templates", ]; for dir in &dirs { let path = vault_root.join(dir); std::fs::create_dir_all(&path).map_err(|e| VaultError::io(e, &path))?; } Ok(()) } /// List all .md files in a directory (non-recursive). pub fn list_md_files(dir: &Path) -> Result, VaultError> { if !dir.exists() { return Ok(vec![]); } let mut files = Vec::new(); let entries = std::fs::read_dir(dir).map_err(|e| VaultError::io(e, dir))?; for entry in entries { let entry = entry.map_err(|e| VaultError::io(e, dir))?; let path = entry.path(); if path.is_file() && path.extension().is_some_and(|e| e == "md") { files.push(path); } } files.sort(); Ok(files) } /// List all .md files in a directory tree (recursive). pub fn list_md_files_recursive(dir: &Path) -> Result, VaultError> { if !dir.exists() { return Ok(vec![]); } let mut files = Vec::new(); walk_dir_recursive(dir, &mut files)?; files.sort(); Ok(files) } fn walk_dir_recursive(dir: &Path, files: &mut Vec) -> Result<(), VaultError> { let entries = std::fs::read_dir(dir).map_err(|e| VaultError::io(e, dir))?; for entry in entries { let entry = entry.map_err(|e| VaultError::io(e, dir))?; let path = entry.path(); if path.is_dir() { // Skip dotfiles/dirs if path .file_name() .is_some_and(|n| n.to_str().is_some_and(|s| s.starts_with('.'))) { continue; } walk_dir_recursive(&path, files)?; } else if path.is_file() && path.extension().is_some_and(|e| e == "md") { files.push(path); } } Ok(()) } /// Convert a string to a URL-safe slug. pub fn slugify(s: &str) -> String { s.to_lowercase() .chars() .map(|c| { if c.is_alphanumeric() { c } else { '-' } }) .collect::() .split('-') .filter(|s| !s.is_empty()) .collect::>() .join("-") } /// Create a timestamped slug: `YYYYMMDD-HHMMSS-slug` pub fn timestamped_slug(title: &str) -> String { let now = chrono::Utc::now(); format!("{}-{}", now.format("%Y%m%d-%H%M%S"), slugify(title)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_slugify() { assert_eq!(slugify("Hello World!"), "hello-world"); assert_eq!(slugify("Review PR #1234"), "review-pr-1234"); assert_eq!(slugify(" spaces everywhere "), "spaces-everywhere"); } #[test] fn test_timestamped_slug() { let slug = timestamped_slug("My Task"); assert!(slug.ends_with("-my-task")); assert!(slug.len() > 20); } #[test] fn test_ensure_vault_structure() { let tmp = std::env::temp_dir().join("vault-os-test-structure"); let _ = std::fs::remove_dir_all(&tmp); ensure_vault_structure(&tmp).unwrap(); assert!(tmp.join("agents").is_dir()); assert!(tmp.join("todos/harald/urgent").is_dir()); assert!(tmp.join("todos/agent/queued").is_dir()); assert!(tmp.join(".vault/logs").is_dir()); let _ = std::fs::remove_dir_all(&tmp); } }