use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const REGISTRY_FILENAME: &str = "pr-cleanup.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OpenedPr {
pub pr: u64,
pub repo: String,
pub repo_root: PathBuf,
pub opened_at: DateTime<Utc>,
#[serde(default)]
pub cleaned_at: Option<DateTime<Utc>>,
}
impl OpenedPr {
pub fn pending(&self) -> bool {
self.cleaned_at.is_none()
}
}
#[derive(Debug, Clone)]
pub struct CleanupRegistry {
path: PathBuf,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct RegistryFile {
#[serde(default)]
entries: Vec<OpenedPr>,
}
impl CleanupRegistry {
pub fn at(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn under_root(root: impl AsRef<Path>) -> Self {
Self::at(root.as_ref().join(REGISTRY_FILENAME))
}
pub fn production() -> Self {
Self::under_root(crate::core::paths::FrameworkPaths::default().root)
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn entries(&self) -> Vec<OpenedPr> {
let Ok(raw) = std::fs::read_to_string(&self.path) else {
return Vec::new();
};
serde_json::from_str::<RegistryFile>(&raw)
.map(|f| f.entries)
.unwrap_or_default()
}
pub fn pending(&self) -> Vec<OpenedPr> {
self.entries()
.into_iter()
.filter(OpenedPr::pending)
.collect()
}
pub fn record_open(&self, entry: OpenedPr) -> anyhow::Result<()> {
let mut entries = self.entries();
entries.retain(|e| !(e.pr == entry.pr && e.repo == entry.repo));
entries.push(entry);
self.write(entries)
}
pub fn mark_cleaned(&self, repo: &str, pr: u64, at: DateTime<Utc>) -> anyhow::Result<()> {
let mut entries = self.entries();
let mut hit = false;
for e in &mut entries {
if e.pr == pr && e.repo == repo {
e.cleaned_at = Some(at);
hit = true;
}
}
if !hit {
return Ok(());
}
self.write(entries)
}
fn write(&self, entries: Vec<OpenedPr>) -> anyhow::Result<()> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let body = serde_json::to_string_pretty(&RegistryFile { entries })?;
crate::core::agent_manifest::atomic_write(&self.path, &body)
.map_err(|e| anyhow::anyhow!("cannot write {}: {e}", self.path.display()))?;
Ok(())
}
}