use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail};
use openmoat_hosts::Host;
use serde::{Deserialize, Serialize};
use crate::home::{Home, write_private};
const VERSION: u32 = 1;
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Recorded {
pub version: u32,
pub dirs: BTreeMap<String, PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub claude_rules: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub backups: Vec<PathBuf>,
}
impl Recorded {
pub fn load(home: &Home) -> Result<Self> {
let path = home.hosts_path();
if !path.exists() {
return Ok(Self::default());
}
let text =
fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let recorded: Self = serde_json::from_str(&text)
.with_context(|| format!("parsing {}; run `moat init`", path.display()))?;
if recorded.version != VERSION {
bail!(
"{} has version {}; this build supports {VERSION}",
path.display(),
recorded.version
);
}
Ok(recorded)
}
pub fn save(&mut self, home: &Home) -> Result<()> {
self.version = VERSION;
let text = serde_json::to_string_pretty(self)? + "\n";
write_private(&home.hosts_path(), text.as_bytes())
}
pub fn owns(&self, path: &Path) -> bool {
self.backups.iter().any(|b| b == path)
}
pub fn dir(&self, host: Host) -> Option<PathBuf> {
self.dirs.get(host.id()).cloned()
}
pub fn skipped(&self, host: Host) -> bool {
!self.dirs.is_empty() && self.dir(host).is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_and_refuses_other_versions() {
let dir = tempfile::tempdir().unwrap();
let home = Home::at(dir.path().to_path_buf());
assert_eq!(Recorded::load(&home).unwrap(), Recorded::default());
let mut recorded = Recorded::default();
recorded
.dirs
.insert("codex".into(), PathBuf::from("/cfg/codex"));
recorded.save(&home).unwrap();
let loaded = Recorded::load(&home).unwrap();
assert_eq!(loaded.dir(Host::Codex), Some(PathBuf::from("/cfg/codex")));
assert_eq!(loaded.dir(Host::ClaudeCode), None);
assert!(!loaded.owns(Path::new("/cfg/claude/settings.json.moat-backup")));
fs::write(home.hosts_path(), r#"{"version":7,"dirs":{}}"#).unwrap();
assert!(Recorded::load(&home).is_err());
}
#[test]
fn skips_only_hosts_left_out_of_a_non_empty_record() {
let mut recorded = Recorded::default();
assert!(!recorded.skipped(Host::Codex));
recorded
.dirs
.insert("claude-code".into(), PathBuf::from("/cfg/claude"));
assert!(recorded.skipped(Host::Codex));
assert!(!recorded.skipped(Host::ClaudeCode));
}
}