1use crate::Result;
2use crate::config::Config;
3use crate::ops::IndexService;
4use agtrace_index::Database;
5use agtrace_providers::get_all_providers;
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_core::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(current_project_hash.clone())
124 };
125
126 service.run(scope, config.refresh, |_progress| {
127 })?;
129 }
130
131 if let Some(ref mut f) = progress_fn {
132 f(InitProgress::SessionPhase);
133 }
134 let session_count = Self::step4_sessions(&db, ¤t_project_hash, config.all_projects)?;
135
136 Ok(InitResult {
137 config_status,
138 db_path: db_path.clone(),
139 scan_outcome,
140 session_count,
141 all_projects: config.all_projects,
142 scan_needed,
143 })
144 }
145
146 fn step1_config(config_path: &Path) -> Result<ConfigStatus> {
147 if !config_path.exists() {
148 let detected = Config::detect_providers()?;
149
150 if detected.providers.is_empty() {
151 let available_providers = get_all_providers()
152 .iter()
153 .map(|p| ProviderInfo {
154 name: p.name.to_string(),
155 default_log_path: p.default_log_path.to_string(),
156 })
157 .collect();
158 return Ok(ConfigStatus::NoProvidersDetected {
159 available_providers,
160 });
161 }
162
163 let providers: HashMap<String, PathBuf> = detected
164 .providers
165 .iter()
166 .map(|(name, cfg)| (name.clone(), cfg.log_root.clone()))
167 .collect();
168
169 detected.save_to(&config_path.to_path_buf())?;
170
171 Ok(ConfigStatus::DetectedAndSaved { providers })
172 } else {
173 Config::load_from(&config_path.to_path_buf())?;
174 Ok(ConfigStatus::LoadedExisting {
175 config_path: config_path.to_path_buf(),
176 })
177 }
178 }
179
180 fn step2_database(db_path: &Path) -> Result<Database> {
181 Ok(Database::open(db_path)?)
182 }
183
184 fn step3_scan(
185 db: &Database,
186 project_hash: &agtrace_types::ProjectHash,
187 force_refresh: bool,
188 ) -> Result<(ScanOutcome, bool)> {
189 let should_scan = if force_refresh {
190 true
191 } else if let Ok(Some(project)) = db.get_project(project_hash.as_str()) {
192 if let Some(last_scanned) = &project.last_scanned_at {
193 if let Ok(last_time) = DateTime::parse_from_rfc3339(last_scanned) {
194 let elapsed = Utc::now().signed_duration_since(last_time.with_timezone(&Utc));
195 if elapsed < Duration::minutes(5) {
196 return Ok((ScanOutcome::Skipped { elapsed }, false));
197 } else {
198 true
199 }
200 } else {
201 true
202 }
203 } else {
204 true
205 }
206 } else {
207 true
208 };
209
210 if should_scan {
211 Ok((ScanOutcome::Scanned, true))
212 } else {
213 Ok((
214 ScanOutcome::Skipped {
215 elapsed: Duration::zero(),
216 },
217 false,
218 ))
219 }
220 }
221
222 fn step4_sessions(
223 db: &Database,
224 project_hash: &agtrace_types::ProjectHash,
225 all_projects: bool,
226 ) -> Result<usize> {
227 let effective_hash = if all_projects {
228 None
229 } else {
230 Some(project_hash)
231 };
232
233 let sessions = db.list_sessions(effective_hash, Some(10))?;
234 Ok(sessions.len())
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use tempfile::TempDir;
242
243 #[test]
244 fn test_init_service_basic_flow() -> Result<()> {
245 let temp_dir = TempDir::new()?;
246
247 let config = InitConfig {
248 data_dir: temp_dir.path().to_path_buf(),
249 project_root: None,
250 all_projects: false,
251 refresh: false,
252 };
253
254 let result = InitService::run(config, None::<fn(InitProgress)>)?;
255
256 matches!(
257 result.config_status,
258 ConfigStatus::NoProvidersDetected { .. }
259 );
260
261 Ok(())
262 }
263}