Skip to main content

cc_audit/run/
client.rs

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