use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::path::PathBuf;
use tracing::warn;
const INDEX_FILE: &str = "plans-index.json";
const VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanEntry {
pub name: String,
#[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(rename = "projectPath", skip_serializing_if = "Option::is_none")]
pub project_path: Option<String>,
pub created: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct IndexData {
version: u32,
entries: Vec<PlanEntry>,
}
impl Default for IndexData {
fn default() -> Self {
Self {
version: VERSION,
entries: Vec::new(),
}
}
}
pub struct PlanIndex {
plans_dir: PathBuf,
index_path: PathBuf,
}
impl PlanIndex {
pub fn new(plans_dir: impl Into<PathBuf>) -> Self {
let dir = plans_dir.into();
let index_path = dir.join(INDEX_FILE);
Self {
plans_dir: dir,
index_path,
}
}
fn read_index(&self) -> IndexData {
if !self.index_path.exists() {
return IndexData::default();
}
match std::fs::read_to_string(&self.index_path) {
Ok(content) => serde_json::from_str::<IndexData>(&content).unwrap_or_default(),
Err(_) => IndexData::default(),
}
}
fn write_index(&self, data: &IndexData) -> std::io::Result<()> {
std::fs::create_dir_all(&self.plans_dir)?;
let tmp_path = self.plans_dir.join(".plans-idx-tmp");
{
let mut f = std::fs::File::create(&tmp_path)?;
let json = serde_json::to_string_pretty(data).map_err(std::io::Error::other)?;
f.write_all(json.as_bytes())?;
f.write_all(b"\n")?;
f.sync_all()?;
}
std::fs::rename(&tmp_path, &self.index_path).inspect_err(|_| {
let _ = std::fs::remove_file(&tmp_path);
})
}
pub fn add_entry(&self, name: &str, session_id: Option<&str>, project_path: Option<&str>) {
let mut data = self.read_index();
data.entries.retain(|e| e.name != name);
data.entries.push(PlanEntry {
name: name.to_string(),
session_id: session_id.map(|s| s.to_string()),
project_path: project_path.map(|s| s.to_string()),
created: Utc::now().to_rfc3339(),
});
if let Err(e) = self.write_index(&data) {
warn!("Failed to write plan index: {}", e);
}
}
pub fn get_by_session(&self, session_id: &str) -> Option<PlanEntry> {
self.read_index()
.entries
.into_iter()
.find(|e| e.session_id.as_deref() == Some(session_id))
}
pub fn get_by_project(&self, project_path: &str) -> Vec<PlanEntry> {
self.read_index()
.entries
.into_iter()
.filter(|e| e.project_path.as_deref() == Some(project_path))
.collect()
}
pub fn remove_entry(&self, name: &str) {
let mut data = self.read_index();
data.entries.retain(|e| e.name != name);
if let Err(e) = self.write_index(&data) {
warn!("Failed to write plan index: {}", e);
}
}
pub fn list_all(&self) -> Vec<PlanEntry> {
self.read_index().entries
}
}
#[cfg(test)]
#[path = "plan_index_tests.rs"]
mod tests;