agtrace_runtime/client/
projects.rs

1use crate::ops::{IndexProgress, IndexService, ProjectInfo, ProjectService};
2use agtrace_index::Database;
3use agtrace_providers::{ProviderAdapter, ScanContext};
4use agtrace_types::{discover_project_root, project_hash_from_root};
5use anyhow::Result;
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex};
8
9#[derive(Debug, Clone)]
10pub struct ScanSummary {
11    pub total_sessions: usize,
12    pub scanned_files: usize,
13    pub skipped_files: usize,
14}
15
16pub struct ProjectOps {
17    db: Arc<Mutex<Database>>,
18    provider_configs: Arc<Vec<(String, PathBuf)>>,
19}
20
21impl ProjectOps {
22    pub fn new(db: Arc<Mutex<Database>>, provider_configs: Arc<Vec<(String, PathBuf)>>) -> Self {
23        Self {
24            db,
25            provider_configs,
26        }
27    }
28
29    pub fn list(&self) -> Result<Vec<ProjectInfo>> {
30        self.ensure_index_is_fresh()?;
31
32        let db = self.db.lock().unwrap();
33        let service = ProjectService::new(&db);
34        service.list_projects()
35    }
36
37    fn ensure_index_is_fresh(&self) -> Result<()> {
38        let db = self.db.lock().unwrap();
39
40        let providers: Vec<(ProviderAdapter, PathBuf)> = self
41            .provider_configs
42            .iter()
43            .filter_map(|(name, path)| {
44                agtrace_providers::create_adapter(name)
45                    .ok()
46                    .map(|p| (p, path.clone()))
47            })
48            .collect();
49
50        let service = IndexService::new(&db, providers);
51
52        // Scan all projects: project_root=None means no filtering
53        // project_hash is only used for reporting and as fallback
54        let project_hash = discover_project_root(None)
55            .ok()
56            .map(|root| project_hash_from_root(&root.display().to_string()))
57            .unwrap_or_else(|| "unknown".to_string());
58
59        let scan_context = ScanContext {
60            project_hash,
61            project_root: None,
62            provider_filter: None,
63        };
64
65        service.run(&scan_context, false, |_progress: IndexProgress| {})?;
66
67        Ok(())
68    }
69
70    pub fn scan<F>(
71        &self,
72        scan_context: &ScanContext,
73        force: bool,
74        mut on_progress: F,
75    ) -> Result<ScanSummary>
76    where
77        F: FnMut(IndexProgress),
78    {
79        let db = self.db.lock().unwrap();
80        let providers: Vec<(ProviderAdapter, PathBuf)> = self
81            .provider_configs
82            .iter()
83            .filter_map(|(name, path)| {
84                // Apply provider filter if specified
85                if let Some(ref filter) = scan_context.provider_filter
86                    && filter != "all"
87                    && name != filter
88                {
89                    return None;
90                }
91                agtrace_providers::create_adapter(name)
92                    .ok()
93                    .map(|p| (p, path.clone()))
94            })
95            .collect();
96        let service = IndexService::new(&db, providers);
97
98        let mut total_sessions = 0;
99        let mut scanned_files = 0;
100        let mut skipped_files = 0;
101
102        service.run(scan_context, force, |progress| {
103            if let IndexProgress::Completed {
104                total_sessions: ts,
105                scanned_files: sf,
106                skipped_files: skf,
107            } = progress
108            {
109                total_sessions = ts;
110                scanned_files = sf;
111                skipped_files = skf;
112            }
113            on_progress(progress);
114        })?;
115
116        Ok(ScanSummary {
117            total_sessions,
118            scanned_files,
119            skipped_files,
120        })
121    }
122}