agtrace_runtime/
init.rs

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(&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 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                // Silently index during init - progress is shown by the handler
130            })?;
131        }
132
133        if let Some(ref mut f) = progress_fn {
134            f(InitProgress::SessionPhase);
135        }
136        let session_count = Self::step4_sessions(&db, &current_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}