agtrace_runtime/
init.rs

1use crate::config::Config;
2use crate::ops::IndexService;
3use agtrace_index::Database;
4use agtrace_providers::{ScanContext, 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(&current_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, &current_project_hash, config.refresh)?;
105
106        // Perform actual scan if needed
107        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 scan_context = ScanContext {
121                project_hash: current_project_hash.clone(),
122                project_root: if config.all_projects {
123                    None
124                } else {
125                    Some(current_project_root.clone())
126                },
127                provider_filter: None,
128            };
129
130            service.run(&scan_context, config.refresh, |_progress| {
131                // Silently index during init - progress is shown by the handler
132            })?;
133        }
134
135        if let Some(ref mut f) = progress_fn {
136            f(InitProgress::SessionPhase);
137        }
138        let session_count = Self::step4_sessions(&db, &current_project_hash, config.all_projects)?;
139
140        Ok(InitResult {
141            config_status,
142            db_path: db_path.clone(),
143            scan_outcome,
144            session_count,
145            all_projects: config.all_projects,
146            scan_needed,
147        })
148    }
149
150    fn step1_config(config_path: &Path) -> Result<ConfigStatus> {
151        if !config_path.exists() {
152            let detected = Config::detect_providers()?;
153
154            if detected.providers.is_empty() {
155                let available_providers = get_all_providers()
156                    .iter()
157                    .map(|p| ProviderInfo {
158                        name: p.name.to_string(),
159                        default_log_path: p.default_log_path.to_string(),
160                    })
161                    .collect();
162                return Ok(ConfigStatus::NoProvidersDetected {
163                    available_providers,
164                });
165            }
166
167            let providers: HashMap<String, PathBuf> = detected
168                .providers
169                .iter()
170                .map(|(name, cfg)| (name.clone(), cfg.log_root.clone()))
171                .collect();
172
173            detected.save_to(&config_path.to_path_buf())?;
174
175            Ok(ConfigStatus::DetectedAndSaved { providers })
176        } else {
177            Config::load_from(&config_path.to_path_buf())?;
178            Ok(ConfigStatus::LoadedExisting {
179                config_path: config_path.to_path_buf(),
180            })
181        }
182    }
183
184    fn step2_database(db_path: &Path) -> Result<Database> {
185        Database::open(db_path)
186    }
187
188    fn step3_scan(
189        db: &Database,
190        project_hash: &str,
191        force_refresh: bool,
192    ) -> Result<(ScanOutcome, bool)> {
193        let should_scan = if force_refresh {
194            true
195        } else if let Ok(Some(project)) = db.get_project(project_hash) {
196            if let Some(last_scanned) = &project.last_scanned_at {
197                if let Ok(last_time) = DateTime::parse_from_rfc3339(last_scanned) {
198                    let elapsed = Utc::now().signed_duration_since(last_time.with_timezone(&Utc));
199                    if elapsed < Duration::minutes(5) {
200                        return Ok((ScanOutcome::Skipped { elapsed }, false));
201                    } else {
202                        true
203                    }
204                } else {
205                    true
206                }
207            } else {
208                true
209            }
210        } else {
211            true
212        };
213
214        if should_scan {
215            Ok((ScanOutcome::Scanned, true))
216        } else {
217            Ok((
218                ScanOutcome::Skipped {
219                    elapsed: Duration::zero(),
220                },
221                false,
222            ))
223        }
224    }
225
226    fn step4_sessions(db: &Database, project_hash: &str, all_projects: bool) -> 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, 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}