agtrace_runtime/
init.rs

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(&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(current_project_hash.clone())
124            };
125
126            service.run(scope, config.refresh, |_progress| {
127                // Silently index during init - progress is shown by the handler
128            })?;
129        }
130
131        if let Some(ref mut f) = progress_fn {
132            f(InitProgress::SessionPhase);
133        }
134        let session_count = Self::step4_sessions(&db, &current_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}