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(¤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 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 })?;
133 }
134
135 if let Some(ref mut f) = progress_fn {
136 f(InitProgress::SessionPhase);
137 }
138 let session_count = Self::step4_sessions(&db, ¤t_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}