use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SoulState {
pub bootstrap_seeded_at: Option<DateTime<Utc>>,
pub bootstrap_completed_at: Option<DateTime<Utc>>,
}
pub struct SoulStateStore {
path: PathBuf,
}
impl SoulStateStore {
pub fn new(workspace: impl AsRef<Path>) -> Self {
let path = workspace
.as_ref()
.join(".agent-diva")
.join("soul-state.json");
Self { path }
}
pub fn load(&self) -> std::io::Result<SoulState> {
if !self.path.exists() {
return Ok(SoulState::default());
}
let raw = std::fs::read_to_string(&self.path)?;
let state: SoulState = serde_json::from_str(&raw).unwrap_or_default();
Ok(state)
}
pub fn save(&self, state: &SoulState) -> std::io::Result<()> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(state)?;
std::fs::write(&self.path, content)
}
pub fn is_bootstrap_completed(&self) -> bool {
self.load()
.ok()
.and_then(|state| state.bootstrap_completed_at)
.is_some()
}
pub fn mark_bootstrap_seeded(&self) -> std::io::Result<()> {
let mut state = self.load().unwrap_or_default();
if state.bootstrap_seeded_at.is_none() {
state.bootstrap_seeded_at = Some(Utc::now());
self.save(&state)?;
}
Ok(())
}
pub fn mark_bootstrap_completed(&self) -> std::io::Result<()> {
let mut state = self.load().unwrap_or_default();
if state.bootstrap_seeded_at.is_none() {
state.bootstrap_seeded_at = Some(Utc::now());
}
if state.bootstrap_completed_at.is_none() {
state.bootstrap_completed_at = Some(Utc::now());
self.save(&state)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_missing_returns_default() {
let temp = tempfile::tempdir().unwrap();
let store = SoulStateStore::new(temp.path());
let state = store.load().unwrap();
assert!(state.bootstrap_seeded_at.is_none());
assert!(state.bootstrap_completed_at.is_none());
}
#[test]
fn test_mark_bootstrap_seeded_writes_once() {
let temp = tempfile::tempdir().unwrap();
let store = SoulStateStore::new(temp.path());
store.mark_bootstrap_seeded().unwrap();
let first = store.load().unwrap();
assert!(first.bootstrap_seeded_at.is_some());
store.mark_bootstrap_seeded().unwrap();
let second = store.load().unwrap();
assert_eq!(first.bootstrap_seeded_at, second.bootstrap_seeded_at);
}
#[test]
fn test_is_bootstrap_completed() {
let temp = tempfile::tempdir().unwrap();
let store = SoulStateStore::new(temp.path());
assert!(!store.is_bootstrap_completed());
let mut state = SoulState::default();
state.bootstrap_completed_at = Some(Utc::now());
store.save(&state).unwrap();
assert!(store.is_bootstrap_completed());
}
#[test]
fn test_mark_bootstrap_completed_sets_completed_at() {
let temp = tempfile::tempdir().unwrap();
let store = SoulStateStore::new(temp.path());
store.mark_bootstrap_completed().unwrap();
let state = store.load().unwrap();
assert!(state.bootstrap_seeded_at.is_some());
assert!(state.bootstrap_completed_at.is_some());
}
}