1use crate::config::Config;
2use crate::ops::IndexService;
3use agtrace_index::Database;
4use agtrace_providers::get_all_providers;
5use anyhow::Result;
6use chrono::{DateTime, Duration, Utc};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
11pub enum InitProgress {
12 ConfigPhase,
13 DatabasePhase,
14 ScanPhase,
15 SessionPhase,
16}
17
18#[derive(Debug, Clone)]
19pub struct ProviderInfo {
20 pub name: String,
21 pub default_log_path: String,
22}
23
24#[derive(Debug, Clone)]
25pub enum ConfigStatus {
26 DetectedAndSaved {
27 providers: HashMap<String, PathBuf>,
28 },
29 LoadedExisting {
30 config_path: PathBuf,
31 },
32 NoProvidersDetected {
33 available_providers: Vec<ProviderInfo>,
34 },
35}
36
37#[derive(Debug, Clone)]
38pub enum ScanOutcome {
39 Scanned,
40 Skipped { elapsed: Duration },
41}
42
43#[derive(Debug, Clone)]
44pub struct InitResult {
45 pub config_status: ConfigStatus,
46 pub db_path: PathBuf,
47 pub scan_outcome: ScanOutcome,
48 pub session_count: usize,
49 pub all_projects: bool,
50 pub scan_needed: bool,
51}
52
53pub struct InitConfig {
54 pub data_dir: PathBuf,
55 pub project_root: Option<PathBuf>,
56 pub all_projects: bool,
57 pub refresh: bool,
58}
59
60pub struct InitService;
61
62impl InitService {
63 pub fn run<F>(config: InitConfig, mut progress_fn: Option<F>) -> Result<InitResult>
64 where
65 F: FnMut(InitProgress),
66 {
67 let config_path = config.data_dir.join("config.toml");
68 let db_path = config.data_dir.join("agtrace.db");
69
70 if let Some(ref mut f) = progress_fn {
71 f(InitProgress::ConfigPhase);
72 }
73 let config_status = Self::step1_config(&config_path)?;
74
75 if let ConfigStatus::NoProvidersDetected { .. } = config_status {
76 return Ok(InitResult {
77 config_status,
78 db_path: db_path.clone(),
79 scan_outcome: ScanOutcome::Skipped {
80 elapsed: Duration::zero(),
81 },
82 session_count: 0,
83 all_projects: config.all_projects,
84 scan_needed: false,
85 });
86 }
87
88 if let Some(ref mut f) = progress_fn {
89 f(InitProgress::DatabasePhase);
90 }
91 let db = Self::step2_database(&db_path)?;
92
93 let current_project_root = config
94 .project_root
95 .as_ref()
96 .map(|p| p.display().to_string())
97 .unwrap_or_else(|| ".".to_string());
98 let current_project_hash = agtrace_types::project_hash_from_root(¤t_project_root);
99
100 if let Some(ref mut f) = progress_fn {
101 f(InitProgress::ScanPhase);
102 }
103 let (scan_outcome, scan_needed) =
104 Self::step3_scan(&db, ¤t_project_hash, config.refresh)?;
105
106 if scan_needed {
108 let loaded_config = Config::load_from(&config_path)?;
109 let providers: Vec<(agtrace_providers::ProviderAdapter, PathBuf)> = loaded_config
110 .providers
111 .iter()
112 .filter_map(|(name, cfg)| {
113 agtrace_providers::create_adapter(name)
114 .ok()
115 .map(|p| (p, cfg.log_root.clone()))
116 })
117 .collect();
118
119 let service = IndexService::new(&db, providers);
120 let scope = if config.all_projects {
121 agtrace_types::ProjectScope::All
122 } else {
123 agtrace_types::ProjectScope::Specific {
124 root: current_project_root.clone(),
125 }
126 };
127
128 service.run(scope, config.refresh, |_progress| {
129 })?;
131 }
132
133 if let Some(ref mut f) = progress_fn {
134 f(InitProgress::SessionPhase);
135 }
136 let session_count = Self::step4_sessions(&db, ¤t_project_hash, config.all_projects)?;
137
138 Ok(InitResult {
139 config_status,
140 db_path: db_path.clone(),
141 scan_outcome,
142 session_count,
143 all_projects: config.all_projects,
144 scan_needed,
145 })
146 }
147
148 fn step1_config(config_path: &Path) -> Result<ConfigStatus> {
149 if !config_path.exists() {
150 let detected = Config::detect_providers()?;
151
152 if detected.providers.is_empty() {
153 let available_providers = get_all_providers()
154 .iter()
155 .map(|p| ProviderInfo {
156 name: p.name.to_string(),
157 default_log_path: p.default_log_path.to_string(),
158 })
159 .collect();
160 return Ok(ConfigStatus::NoProvidersDetected {
161 available_providers,
162 });
163 }
164
165 let providers: HashMap<String, PathBuf> = detected
166 .providers
167 .iter()
168 .map(|(name, cfg)| (name.clone(), cfg.log_root.clone()))
169 .collect();
170
171 detected.save_to(&config_path.to_path_buf())?;
172
173 Ok(ConfigStatus::DetectedAndSaved { providers })
174 } else {
175 Config::load_from(&config_path.to_path_buf())?;
176 Ok(ConfigStatus::LoadedExisting {
177 config_path: config_path.to_path_buf(),
178 })
179 }
180 }
181
182 fn step2_database(db_path: &Path) -> Result<Database> {
183 Database::open(db_path)
184 }
185
186 fn step3_scan(
187 db: &Database,
188 project_hash: &agtrace_types::ProjectHash,
189 force_refresh: bool,
190 ) -> Result<(ScanOutcome, bool)> {
191 let should_scan = if force_refresh {
192 true
193 } else if let Ok(Some(project)) = db.get_project(project_hash.as_str()) {
194 if let Some(last_scanned) = &project.last_scanned_at {
195 if let Ok(last_time) = DateTime::parse_from_rfc3339(last_scanned) {
196 let elapsed = Utc::now().signed_duration_since(last_time.with_timezone(&Utc));
197 if elapsed < Duration::minutes(5) {
198 return Ok((ScanOutcome::Skipped { elapsed }, false));
199 } else {
200 true
201 }
202 } else {
203 true
204 }
205 } else {
206 true
207 }
208 } else {
209 true
210 };
211
212 if should_scan {
213 Ok((ScanOutcome::Scanned, true))
214 } else {
215 Ok((
216 ScanOutcome::Skipped {
217 elapsed: Duration::zero(),
218 },
219 false,
220 ))
221 }
222 }
223
224 fn step4_sessions(
225 db: &Database,
226 project_hash: &agtrace_types::ProjectHash,
227 all_projects: bool,
228 ) -> Result<usize> {
229 let effective_hash = if all_projects {
230 None
231 } else {
232 Some(project_hash)
233 };
234
235 let sessions = db.list_sessions(effective_hash, 10)?;
236 Ok(sessions.len())
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use tempfile::TempDir;
244
245 #[test]
246 fn test_init_service_basic_flow() -> Result<()> {
247 let temp_dir = TempDir::new()?;
248
249 let config = InitConfig {
250 data_dir: temp_dir.path().to_path_buf(),
251 project_root: None,
252 all_projects: false,
253 refresh: false,
254 };
255
256 let result = InitService::run(config, None::<fn(InitProgress)>)?;
257
258 matches!(
259 result.config_status,
260 ConfigStatus::NoProvidersDetected { .. }
261 );
262
263 Ok(())
264 }
265}