Skip to main content

cc_audit/run/
client.rs

1//! Client detection and scan mode handling.
2
3use crate::{Cli, ClientType, detect_client, detect_installed_clients};
4use std::path::PathBuf;
5
6/// Scan mode based on CLI options.
7#[derive(Debug, Clone)]
8pub enum ScanMode {
9    /// Scan specific paths provided via CLI.
10    Paths(Vec<PathBuf>),
11    /// Scan all installed AI coding clients.
12    AllClients,
13    /// Scan a specific client.
14    SingleClient(ClientType),
15}
16
17impl ScanMode {
18    /// Determine scan mode from CLI options.
19    pub fn from_cli(cli: &Cli) -> Self {
20        if cli.all_clients {
21            ScanMode::AllClients
22        } else if let Some(client) = cli.client {
23            ScanMode::SingleClient(client)
24        } else {
25            ScanMode::Paths(cli.paths.clone())
26        }
27    }
28}
29
30/// Resolve paths to scan based on CLI options.
31pub fn resolve_scan_paths(cli: &Cli) -> Vec<PathBuf> {
32    let mode = ScanMode::from_cli(cli);
33
34    match mode {
35        ScanMode::Paths(paths) => {
36            if paths.is_empty() {
37                // Default to current directory
38                vec![PathBuf::from(".")]
39            } else {
40                paths
41            }
42        }
43        ScanMode::AllClients => {
44            let clients = detect_installed_clients();
45            if clients.is_empty() {
46                eprintln!("No AI coding clients detected on this system.");
47                return Vec::new();
48            }
49
50            let mut paths = Vec::new();
51            for client in &clients {
52                eprintln!(
53                    "Detected {}: {}",
54                    client.client_type.display_name(),
55                    client.home_dir.display()
56                );
57                paths.extend(client.all_configs());
58            }
59            paths
60        }
61        ScanMode::SingleClient(client_type) => match detect_client(client_type) {
62            Some(client) => {
63                eprintln!(
64                    "Scanning {}: {}",
65                    client.client_type.display_name(),
66                    client.home_dir.display()
67                );
68                client.all_configs()
69            }
70            None => {
71                eprintln!(
72                    "{} is not installed or has no configuration files.",
73                    client_type.display_name()
74                );
75                Vec::new()
76            }
77        },
78    }
79}
80
81/// Determine which AI client a file path belongs to.
82pub fn detect_client_for_path(path: &str) -> Option<String> {
83    for client_type in ClientType::all() {
84        if let Some(home) = client_type.home_dir() {
85            let home_str = home.display().to_string();
86            if path.starts_with(&home_str) {
87                return Some(client_type.display_name().to_string());
88            }
89        }
90    }
91    None
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_scan_mode_from_cli_paths() {
100        let cli = crate::Cli {
101            paths: vec![PathBuf::from("/test/path")],
102            all_clients: false,
103            client: None,
104            ..Default::default()
105        };
106        match ScanMode::from_cli(&cli) {
107            ScanMode::Paths(paths) => assert_eq!(paths, vec![PathBuf::from("/test/path")]),
108            _ => panic!("Expected ScanMode::Paths"),
109        }
110    }
111
112    #[test]
113    fn test_scan_mode_from_cli_all_clients() {
114        let cli = crate::Cli {
115            paths: vec![],
116            all_clients: true,
117            client: None,
118            ..Default::default()
119        };
120        assert!(matches!(ScanMode::from_cli(&cli), ScanMode::AllClients));
121    }
122
123    #[test]
124    fn test_scan_mode_from_cli_single_client() {
125        let cli = crate::Cli {
126            paths: vec![],
127            all_clients: false,
128            client: Some(ClientType::Claude),
129            ..Default::default()
130        };
131        match ScanMode::from_cli(&cli) {
132            ScanMode::SingleClient(client) => assert_eq!(client, ClientType::Claude),
133            _ => panic!("Expected ScanMode::SingleClient"),
134        }
135    }
136
137    #[test]
138    fn test_scan_mode_debug() {
139        let mode = ScanMode::Paths(vec![PathBuf::from("./test")]);
140        let debug_str = format!("{:?}", mode);
141        assert!(debug_str.contains("Paths"));
142
143        let mode2 = ScanMode::AllClients;
144        let debug_str2 = format!("{:?}", mode2);
145        assert!(debug_str2.contains("AllClients"));
146
147        let mode3 = ScanMode::SingleClient(ClientType::Claude);
148        let debug_str3 = format!("{:?}", mode3);
149        assert!(debug_str3.contains("SingleClient"));
150    }
151
152    #[test]
153    fn test_resolve_scan_paths_empty() {
154        let cli = crate::Cli {
155            paths: vec![],
156            all_clients: false,
157            client: None,
158            ..Default::default()
159        };
160        let paths = resolve_scan_paths(&cli);
161        assert_eq!(paths, vec![PathBuf::from(".")]);
162    }
163
164    #[test]
165    fn test_resolve_scan_paths_with_paths() {
166        let cli = crate::Cli {
167            paths: vec![PathBuf::from("/test/path1"), PathBuf::from("/test/path2")],
168            all_clients: false,
169            client: None,
170            ..Default::default()
171        };
172        let paths = resolve_scan_paths(&cli);
173        assert_eq!(
174            paths,
175            vec![PathBuf::from("/test/path1"), PathBuf::from("/test/path2")]
176        );
177    }
178
179    #[test]
180    fn test_detect_client_for_path_unknown() {
181        let result = detect_client_for_path("/some/random/path");
182        // This might return None or Some depending on whether any client home matches
183        // Just verify it doesn't panic
184        let _ = result;
185    }
186
187    #[test]
188    fn test_scan_mode_clone() {
189        let mode = ScanMode::AllClients;
190        let cloned = mode.clone();
191        assert!(matches!(cloned, ScanMode::AllClients));
192    }
193
194    #[test]
195    fn test_resolve_scan_paths_all_clients() {
196        let cli = crate::Cli {
197            paths: vec![],
198            all_clients: true,
199            client: None,
200            ..Default::default()
201        };
202        // This tests the AllClients code path
203        // Result depends on what clients are installed
204        let _paths = resolve_scan_paths(&cli);
205    }
206
207    #[test]
208    fn test_resolve_scan_paths_single_client_claude() {
209        let cli = crate::Cli {
210            paths: vec![],
211            all_clients: false,
212            client: Some(ClientType::Claude),
213            ..Default::default()
214        };
215        // This tests the SingleClient code path
216        // Result depends on whether Claude is installed
217        let _paths = resolve_scan_paths(&cli);
218    }
219
220    #[test]
221    fn test_resolve_scan_paths_single_client_cursor() {
222        let cli = crate::Cli {
223            paths: vec![],
224            all_clients: false,
225            client: Some(ClientType::Cursor),
226            ..Default::default()
227        };
228        let _paths = resolve_scan_paths(&cli);
229    }
230
231    #[test]
232    fn test_resolve_scan_paths_single_client_windsurf() {
233        let cli = crate::Cli {
234            paths: vec![],
235            all_clients: false,
236            client: Some(ClientType::Windsurf),
237            ..Default::default()
238        };
239        let _paths = resolve_scan_paths(&cli);
240    }
241
242    #[test]
243    fn test_resolve_scan_paths_single_client_vscode() {
244        let cli = crate::Cli {
245            paths: vec![],
246            all_clients: false,
247            client: Some(ClientType::Vscode),
248            ..Default::default()
249        };
250        let _paths = resolve_scan_paths(&cli);
251    }
252
253    #[test]
254    fn test_detect_client_for_path_all_clients() {
255        // Test each client type's detection
256        for client_type in ClientType::all() {
257            if let Some(home) = client_type.home_dir() {
258                let test_path = format!("{}/test/file.json", home.display());
259                let result = detect_client_for_path(&test_path);
260                // Should detect the client if home exists
261                assert!(result.is_some() || result.is_none());
262            }
263        }
264    }
265}