1use std::path::{Path, PathBuf};
2use std::time::SystemTime;
3
4#[derive(Debug, Clone)]
6pub struct ModifiedSession {
7 pub path: PathBuf,
8 pub mtime: SystemTime,
9}
10
11fn encode_project_path(project_path: &str) -> String {
14 project_path.replace('/', "-")
15}
16
17pub fn find_modified_sessions(
25 claude_dir: &Path,
26 since: Option<SystemTime>,
27 project_paths: &[String],
28) -> Vec<ModifiedSession> {
29 let projects_dir = claude_dir.join("projects");
30 if !projects_dir.exists() {
31 return Vec::new();
32 }
33
34 let patterns: Vec<String> = if project_paths.is_empty() {
35 vec![format!("{}/**/*.jsonl", projects_dir.display())]
37 } else {
38 project_paths
40 .iter()
41 .map(|path| {
42 let encoded = encode_project_path(path);
43 format!("{}/{}*/**/*.jsonl", projects_dir.display(), encoded)
44 })
45 .collect()
46 };
47
48 let mut results = Vec::new();
49 for pattern in &patterns {
50 for entry in glob::glob(pattern).unwrap_or_else(|_| glob::glob("").unwrap()) {
51 if let Ok(path) = entry {
52 if let Ok(metadata) = std::fs::metadata(&path) {
53 if let Ok(mtime) = metadata.modified() {
54 let dominated = match since {
55 Some(since_time) => mtime > since_time,
56 None => true,
57 };
58 if dominated {
59 results.push(ModifiedSession { path, mtime });
60 }
61 }
62 }
63 }
64 }
65 }
66 results
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use std::fs;
73 use tempfile::TempDir;
74
75 #[test]
76 fn test_encode_project_path() {
77 assert_eq!(
78 encode_project_path("/Users/iman/repos/app"),
79 "-Users-iman-repos-app"
80 );
81 assert_eq!(encode_project_path("/tmp/test"), "-tmp-test");
82 }
83
84 #[test]
85 fn test_find_modified_sessions_detects_new_files() {
86 let dir = TempDir::new().unwrap();
87 let sessions_dir = dir.path().join("projects").join("-tmp-my-project");
88 fs::create_dir_all(&sessions_dir).unwrap();
89
90 let session_file = sessions_dir.join("session1.jsonl");
91 fs::write(&session_file, "{}\n").unwrap();
92
93 let modified = find_modified_sessions(
95 dir.path(),
96 None,
97 &["/tmp/my-project".to_string()],
98 );
99 assert_eq!(modified.len(), 1);
100 assert!(modified[0].path.ends_with("session1.jsonl"));
101 }
102
103 #[test]
104 fn test_find_modified_sessions_skips_unregistered() {
105 let dir = TempDir::new().unwrap();
106
107 let registered_dir = dir.path().join("projects").join("-tmp-registered");
109 fs::create_dir_all(®istered_dir).unwrap();
110 fs::write(registered_dir.join("session1.jsonl"), "{}\n").unwrap();
111
112 let unregistered_dir = dir.path().join("projects").join("-Users-iman-Downloads-stuff");
113 fs::create_dir_all(&unregistered_dir).unwrap();
114 fs::write(unregistered_dir.join("session2.jsonl"), "{}\n").unwrap();
115
116 let modified = find_modified_sessions(
118 dir.path(),
119 None,
120 &["/tmp/registered".to_string()],
121 );
122 assert_eq!(modified.len(), 1);
123 assert!(modified[0].path.to_str().unwrap().contains("registered"));
124 }
125
126 #[test]
127 fn test_find_modified_sessions_empty_paths_scans_all() {
128 let dir = TempDir::new().unwrap();
129 let sessions_dir = dir.path().join("projects").join("any-project");
130 fs::create_dir_all(&sessions_dir).unwrap();
131 fs::write(sessions_dir.join("session1.jsonl"), "{}\n").unwrap();
132
133 let modified = find_modified_sessions(dir.path(), None, &[]);
134 assert_eq!(modified.len(), 1);
135 }
136
137 #[test]
138 fn test_find_modified_sessions_skips_unchanged() {
139 let dir = TempDir::new().unwrap();
140 let sessions_dir = dir.path().join("projects").join("-tmp-project");
141 fs::create_dir_all(&sessions_dir).unwrap();
142
143 let session_file = sessions_dir.join("session1.jsonl");
144 fs::write(&session_file, "{}\n").unwrap();
145
146 let mtime = fs::metadata(&session_file).unwrap().modified().unwrap();
147
148 let modified = find_modified_sessions(
149 dir.path(),
150 Some(mtime),
151 &["/tmp/project".to_string()],
152 );
153 assert_eq!(modified.len(), 0);
154 }
155}