Initial implementation of vault-os
Complete implementation across all 13 phases: - vault-core: types, YAML frontmatter parsing, entity classification, filesystem ops, config, prompt composition, validation, search - vault-watch: filesystem watcher with daemon write filtering, event classification - vault-scheduler: cron engine, process executor, task runner with retry logic and concurrency limiting - vault-api: Axum REST API (15 route modules), WebSocket with broadcast, AI assistant proxy, validation, templates - Dashboard: React + TypeScript + Tailwind v4 with kanban, CodeMirror editor, dynamic view system, AI chat sidebar - Nix flake with dev shell and NixOS module - Graceful shutdown, inotify overflow recovery, tracing instrumentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
f820a72b04
123 changed files with 18288 additions and 0 deletions
167
crates/vault-core/src/filesystem.rs
Normal file
167
crates/vault-core/src/filesystem.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
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<T: DeserializeOwned + Serialize>(path: &Path) -> Result<VaultEntity<T>, 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<T: DeserializeOwned + Serialize>(entity: &VaultEntity<T>) -> 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<Vec<PathBuf>, 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<Vec<PathBuf>, 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<PathBuf>) -> 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::<String>()
|
||||
.split('-')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue