feat: project manifest with load/save/upsert

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 03:49:43 +00:00
co-authored by Claude Sonnet 4.6
parent 08b7f94e4c
commit bd390f953d
+92
View File
@@ -0,0 +1,92 @@
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Project {
pub name: String,
pub warm_path: PathBuf,
pub hot_target_path: PathBuf,
pub last_build: Option<DateTime<Utc>>,
pub last_active: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Manifest {
pub projects: Vec<Project>,
}
impl Manifest {
pub fn load(path: &Path) -> Result<Self> {
if !path.exists() { return Ok(Self::default()); }
let s = std::fs::read_to_string(path)
.with_context(|| format!("reading manifest at {}", path.display()))?;
toml::from_str(&s).context("parsing manifest")
}
pub fn save(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let s = toml::to_string_pretty(self).context("serialising manifest")?;
std::fs::write(path, s).context("writing manifest")
}
pub fn get(&self, name: &str) -> Option<&Project> {
self.projects.iter().find(|p| p.name == name)
}
pub fn get_mut(&mut self, name: &str) -> Option<&mut Project> {
self.projects.iter_mut().find(|p| p.name == name)
}
pub fn upsert(&mut self, project: Project) {
if let Some(p) = self.get_mut(&project.name.clone()) {
*p = project;
} else {
self.projects.push(project);
}
}
pub fn default_path() -> PathBuf {
PathBuf::from("/etc/claw-store/projects.toml")
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_roundtrip_manifest() {
let mut m = Manifest::default();
m.projects.push(Project {
name: "kitchen-cash".into(),
warm_path: "/slab/projects/kitchen-cash".into(),
hot_target_path: "/hot/targets/kitchen-cash".into(),
last_build: None,
last_active: None,
});
let f = NamedTempFile::new().unwrap();
m.save(f.path()).unwrap();
let loaded = Manifest::load(f.path()).unwrap();
assert_eq!(loaded.projects.len(), 1);
assert_eq!(loaded.projects[0].name, "kitchen-cash");
}
#[test]
fn test_get_project() {
let mut m = Manifest::default();
m.projects.push(Project {
name: "zeroclaw".into(),
warm_path: "/slab/projects/zeroclaw".into(),
hot_target_path: "/hot/targets/zeroclaw".into(),
last_build: None,
last_active: None,
});
assert!(m.get("zeroclaw").is_some());
assert!(m.get("nonexistent").is_none());
}
}