skilllite_executor/
plan.rs1use anyhow::{Context, Result};
7use serde_json::Value;
8use std::fs::OpenOptions;
9use std::io::{BufRead, BufReader, Write};
10use std::path::{Path, PathBuf};
11
12fn date_today() -> String {
13 chrono::Local::now().format("%Y-%m-%d").to_string()
14}
15
16pub fn plan_path_jsonl(plans_dir: &Path, session_key: &str, date: Option<&str>) -> PathBuf {
18 let date_str = date.map(|s| s.to_string()).unwrap_or_else(date_today);
19 plans_dir.join(format!("{}-{}.jsonl", session_key, date_str))
20}
21
22pub fn plan_path_legacy(plans_dir: &Path, session_key: &str, date: Option<&str>) -> PathBuf {
24 let date_str = date.map(|s| s.to_string()).unwrap_or_else(date_today);
25 plans_dir.join(format!("{}-{}.json", session_key, date_str))
26}
27
28pub fn append_plan(plans_dir: &Path, session_key: &str, plan_json: &Value) -> Result<()> {
30 let path = plan_path_jsonl(plans_dir, session_key, None);
31 if let Some(parent) = path.parent() {
32 std::fs::create_dir_all(parent)?;
33 }
34 let mut file = OpenOptions::new()
35 .create(true)
36 .append(true)
37 .open(&path)
38 .with_context(|| format!("Failed to open plan: {}", path.display()))?;
39 let line = serde_json::to_string(plan_json)?;
40 writeln!(file, "{}", line)?;
41 Ok(())
42}
43
44fn read_plan_entries(
46 plans_dir: &Path,
47 session_key: &str,
48 date: Option<&str>,
49) -> Result<Vec<Value>> {
50 let path = plan_path_jsonl(plans_dir, session_key, date);
51 if !path.exists() {
52 return Ok(Vec::new());
53 }
54 let file = std::fs::File::open(&path)
55 .with_context(|| format!("Failed to open plan: {}", path.display()))?;
56 let reader = BufReader::new(file);
57 let mut entries = Vec::new();
58 for line in reader.lines() {
59 let line = line?;
60 let line = line.trim();
61 if line.is_empty() {
62 continue;
63 }
64 let v: Value = serde_json::from_str(line)?;
65 entries.push(v);
66 }
67 Ok(entries)
68}
69
70pub fn read_latest_plan(
72 plans_dir: &Path,
73 session_key: &str,
74 date: Option<&str>,
75) -> Result<Option<Value>> {
76 let entries = read_plan_entries(plans_dir, session_key, date)?;
77 if let Some(last) = entries.last() {
78 return Ok(Some(last.clone()));
79 }
80 let legacy_path = plan_path_legacy(plans_dir, session_key, date);
82 if legacy_path.exists() {
83 let content = skilllite_fs::read_file(&legacy_path)?;
84 let plan: Value = serde_json::from_str(&content)?;
85 return Ok(Some(plan));
86 }
87 Ok(None)
88}
89
90pub fn list_plan_files(plans_dir: &Path, session_key: &str) -> Result<Vec<PathBuf>> {
92 if !plans_dir.exists() {
93 return Ok(Vec::new());
94 }
95 let mut files: Vec<PathBuf> = skilllite_fs::read_dir(plans_dir)
96 .with_context(|| format!("Failed to read plans dir: {}", plans_dir.display()))?
97 .into_iter()
98 .map(|(p, _)| p)
99 .filter(|p| p.is_file())
100 .filter(|p| {
101 p.extension().is_some_and(|e| e == "jsonl" || e == "json")
102 && p.file_stem()
103 .and_then(|s| s.to_str())
104 .is_some_and(|n| n.starts_with(session_key))
105 })
106 .collect();
107 files.sort_by(|a, b| {
108 std::fs::metadata(a)
109 .and_then(|m| m.modified())
110 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
111 .cmp(
112 &std::fs::metadata(b)
113 .and_then(|m| m.modified())
114 .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
115 )
116 });
117 Ok(files)
118}