Skip to main content

cc_audit/input/
source.rs

1//! Input source resolution.
2
3use crate::cli::{Cli, ScanType};
4use crate::client::{ClientType, DetectedClient, detect_client, detect_installed_clients};
5use std::path::PathBuf;
6
7/// The source of input for scanning.
8#[derive(Debug, Clone)]
9pub enum InputSource {
10    /// Local file or directory paths specified by user.
11    LocalPaths(Vec<PathBuf>),
12    /// Remote repository URL.
13    RemoteUrl {
14        url: String,
15        git_ref: String,
16        auth_token: Option<String>,
17    },
18    /// List of remote repository URLs from a file.
19    RemoteList {
20        file: PathBuf,
21        git_ref: String,
22        auth_token: Option<String>,
23    },
24    /// All installed AI coding clients.
25    AllClients,
26    /// A specific AI coding client.
27    SpecificClient(ClientType),
28    /// Awesome Claude Code repositories.
29    AwesomeClaudeCode,
30}
31
32impl InputSource {
33    /// Determine the input source from CLI arguments.
34    pub fn from_cli(cli: &Cli) -> Self {
35        if cli.all_clients {
36            return Self::AllClients;
37        }
38
39        if let Some(client) = cli.client {
40            return Self::SpecificClient(client);
41        }
42
43        if let Some(ref url) = cli.remote {
44            return Self::RemoteUrl {
45                url: url.clone(),
46                git_ref: cli.git_ref.clone(),
47                auth_token: cli.remote_auth.clone(),
48            };
49        }
50
51        if let Some(ref file) = cli.remote_list {
52            return Self::RemoteList {
53                file: file.clone(),
54                git_ref: cli.git_ref.clone(),
55                auth_token: cli.remote_auth.clone(),
56            };
57        }
58
59        if cli.awesome_claude_code {
60            return Self::AwesomeClaudeCode;
61        }
62
63        Self::LocalPaths(cli.paths.clone())
64    }
65
66    /// Check if this is a local source.
67    pub fn is_local(&self) -> bool {
68        matches!(
69            self,
70            Self::LocalPaths(_) | Self::AllClients | Self::SpecificClient(_)
71        )
72    }
73
74    /// Check if this is a remote source.
75    pub fn is_remote(&self) -> bool {
76        matches!(
77            self,
78            Self::RemoteUrl { .. } | Self::RemoteList { .. } | Self::AwesomeClaudeCode
79        )
80    }
81}
82
83/// Resolves input sources to concrete scan targets.
84pub struct SourceResolver;
85
86impl SourceResolver {
87    /// Resolve the input source to a list of paths to scan.
88    pub fn resolve(cli: &Cli) -> ResolvedInput {
89        let source = InputSource::from_cli(cli);
90
91        match source {
92            InputSource::LocalPaths(paths) => ResolvedInput {
93                paths,
94                source: ResolvedSource::Local,
95                clients: Vec::new(),
96            },
97            InputSource::AllClients => {
98                let clients = detect_installed_clients();
99                let paths: Vec<PathBuf> = clients.iter().flat_map(|c| c.all_configs()).collect();
100
101                ResolvedInput {
102                    paths,
103                    source: ResolvedSource::Client,
104                    clients,
105                }
106            }
107            InputSource::SpecificClient(client_type) => {
108                let clients: Vec<DetectedClient> = detect_client(client_type).into_iter().collect();
109                let paths: Vec<PathBuf> = clients.iter().flat_map(|c| c.all_configs()).collect();
110
111                ResolvedInput {
112                    paths,
113                    source: ResolvedSource::Client,
114                    clients,
115                }
116            }
117            InputSource::RemoteUrl {
118                url,
119                git_ref,
120                auth_token,
121            } => ResolvedInput {
122                paths: Vec::new(),
123                source: ResolvedSource::Remote {
124                    urls: vec![url],
125                    git_ref,
126                    auth_token,
127                },
128                clients: Vec::new(),
129            },
130            InputSource::RemoteList {
131                file,
132                git_ref,
133                auth_token,
134            } => {
135                // URLs will be loaded from file later
136                ResolvedInput {
137                    paths: Vec::new(),
138                    source: ResolvedSource::Remote {
139                        urls: vec![file.to_string_lossy().to_string()],
140                        git_ref,
141                        auth_token,
142                    },
143                    clients: Vec::new(),
144                }
145            }
146            InputSource::AwesomeClaudeCode => ResolvedInput {
147                paths: Vec::new(),
148                source: ResolvedSource::AwesomeClaudeCode,
149                clients: Vec::new(),
150            },
151        }
152    }
153
154    /// Get the scan type from CLI or infer from input.
155    pub fn scan_type(cli: &Cli) -> ScanType {
156        cli.scan_type
157    }
158}
159
160/// The source type after resolution.
161#[derive(Debug, Clone)]
162pub enum ResolvedSource {
163    /// Local file system paths.
164    Local,
165    /// Client configuration paths.
166    Client,
167    /// Remote repository URLs.
168    Remote {
169        urls: Vec<String>,
170        git_ref: String,
171        auth_token: Option<String>,
172    },
173    /// Awesome Claude Code repositories.
174    AwesomeClaudeCode,
175}
176
177/// Resolved input ready for scanning.
178#[derive(Debug, Clone)]
179pub struct ResolvedInput {
180    /// Paths to scan (for local sources).
181    pub paths: Vec<PathBuf>,
182    /// The resolved source type.
183    pub source: ResolvedSource,
184    /// Detected clients (if source is Client).
185    pub clients: Vec<DetectedClient>,
186}
187
188impl ResolvedInput {
189    /// Check if there are any paths to scan.
190    pub fn has_paths(&self) -> bool {
191        !self.paths.is_empty()
192    }
193
194    /// Check if this is a remote source requiring clone.
195    pub fn requires_clone(&self) -> bool {
196        matches!(
197            self.source,
198            ResolvedSource::Remote { .. } | ResolvedSource::AwesomeClaudeCode
199        )
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_input_source_from_local_paths() {
209        let cli = Cli {
210            paths: vec![PathBuf::from("./test")],
211            ..Default::default()
212        };
213        let source = InputSource::from_cli(&cli);
214        assert!(matches!(source, InputSource::LocalPaths(_)));
215        assert!(source.is_local());
216        assert!(!source.is_remote());
217    }
218
219    #[test]
220    fn test_input_source_all_clients() {
221        let cli = Cli {
222            all_clients: true,
223            ..Default::default()
224        };
225        let source = InputSource::from_cli(&cli);
226        assert!(matches!(source, InputSource::AllClients));
227        assert!(source.is_local());
228    }
229
230    #[test]
231    fn test_input_source_specific_client() {
232        let cli = Cli {
233            client: Some(ClientType::Claude),
234            ..Default::default()
235        };
236        let source = InputSource::from_cli(&cli);
237        assert!(matches!(
238            source,
239            InputSource::SpecificClient(ClientType::Claude)
240        ));
241        assert!(source.is_local());
242    }
243
244    #[test]
245    fn test_input_source_remote_url() {
246        let cli = Cli {
247            remote: Some("https://github.com/user/repo".to_string()),
248            git_ref: "main".to_string(),
249            ..Default::default()
250        };
251        let source = InputSource::from_cli(&cli);
252        assert!(matches!(source, InputSource::RemoteUrl { .. }));
253        assert!(source.is_remote());
254        assert!(!source.is_local());
255    }
256
257    #[test]
258    fn test_input_source_awesome_claude_code() {
259        let cli = Cli {
260            awesome_claude_code: true,
261            ..Default::default()
262        };
263        let source = InputSource::from_cli(&cli);
264        assert!(matches!(source, InputSource::AwesomeClaudeCode));
265        assert!(source.is_remote());
266    }
267
268    #[test]
269    fn test_resolved_input_has_paths() {
270        let input = ResolvedInput {
271            paths: vec![PathBuf::from("./test")],
272            source: ResolvedSource::Local,
273            clients: Vec::new(),
274        };
275        assert!(input.has_paths());
276        assert!(!input.requires_clone());
277
278        let empty = ResolvedInput {
279            paths: Vec::new(),
280            source: ResolvedSource::Local,
281            clients: Vec::new(),
282        };
283        assert!(!empty.has_paths());
284    }
285
286    #[test]
287    fn test_resolved_input_requires_clone() {
288        let remote = ResolvedInput {
289            paths: Vec::new(),
290            source: ResolvedSource::Remote {
291                urls: vec!["https://github.com/user/repo".to_string()],
292                git_ref: "main".to_string(),
293                auth_token: None,
294            },
295            clients: Vec::new(),
296        };
297        assert!(remote.requires_clone());
298
299        let awesome = ResolvedInput {
300            paths: Vec::new(),
301            source: ResolvedSource::AwesomeClaudeCode,
302            clients: Vec::new(),
303        };
304        assert!(awesome.requires_clone());
305    }
306
307    #[test]
308    fn test_input_source_remote_list() {
309        let cli = Cli {
310            remote_list: Some(PathBuf::from("repos.txt")),
311            git_ref: "main".to_string(),
312            remote_auth: Some("token123".to_string()),
313            ..Default::default()
314        };
315        let source = InputSource::from_cli(&cli);
316        match &source {
317            InputSource::RemoteList {
318                file,
319                git_ref,
320                auth_token,
321            } => {
322                assert_eq!(*file, PathBuf::from("repos.txt"));
323                assert_eq!(*git_ref, "main");
324                assert_eq!(*auth_token, Some("token123".to_string()));
325            }
326            _ => panic!("Expected RemoteList"),
327        }
328        assert!(source.is_remote());
329    }
330
331    #[test]
332    fn test_input_source_remote_url_with_auth() {
333        let cli = Cli {
334            remote: Some("https://github.com/user/repo".to_string()),
335            git_ref: "develop".to_string(),
336            remote_auth: Some("my_token".to_string()),
337            ..Default::default()
338        };
339        let source = InputSource::from_cli(&cli);
340        match &source {
341            InputSource::RemoteUrl {
342                url,
343                git_ref,
344                auth_token,
345            } => {
346                assert_eq!(url, "https://github.com/user/repo");
347                assert_eq!(git_ref, "develop");
348                assert_eq!(*auth_token, Some("my_token".to_string()));
349            }
350            _ => panic!("Expected RemoteUrl"),
351        }
352    }
353
354    #[test]
355    fn test_source_resolver_resolve_local() {
356        let cli = Cli {
357            paths: vec![PathBuf::from("./src")],
358            ..Default::default()
359        };
360        let resolved = SourceResolver::resolve(&cli);
361        assert!(matches!(resolved.source, ResolvedSource::Local));
362        assert_eq!(resolved.paths, vec![PathBuf::from("./src")]);
363        assert!(!resolved.requires_clone());
364    }
365
366    #[test]
367    fn test_source_resolver_resolve_remote() {
368        let cli = Cli {
369            remote: Some("https://github.com/user/repo".to_string()),
370            git_ref: "main".to_string(),
371            ..Default::default()
372        };
373        let resolved = SourceResolver::resolve(&cli);
374        assert!(matches!(resolved.source, ResolvedSource::Remote { .. }));
375        assert!(resolved.requires_clone());
376    }
377
378    #[test]
379    fn test_source_resolver_resolve_remote_list() {
380        let cli = Cli {
381            remote_list: Some(PathBuf::from("repos.txt")),
382            git_ref: "main".to_string(),
383            ..Default::default()
384        };
385        let resolved = SourceResolver::resolve(&cli);
386        assert!(matches!(resolved.source, ResolvedSource::Remote { .. }));
387    }
388
389    #[test]
390    fn test_source_resolver_resolve_awesome() {
391        let cli = Cli {
392            awesome_claude_code: true,
393            ..Default::default()
394        };
395        let resolved = SourceResolver::resolve(&cli);
396        assert!(matches!(resolved.source, ResolvedSource::AwesomeClaudeCode));
397        assert!(resolved.requires_clone());
398    }
399
400    #[test]
401    fn test_source_resolver_scan_type() {
402        let cli = Cli {
403            scan_type: ScanType::Mcp,
404            ..Default::default()
405        };
406        assert_eq!(SourceResolver::scan_type(&cli), ScanType::Mcp);
407    }
408
409    #[test]
410    fn test_resolved_source_debug() {
411        let local = ResolvedSource::Local;
412        let debug_str = format!("{:?}", local);
413        assert!(debug_str.contains("Local"));
414
415        let client = ResolvedSource::Client;
416        let debug_str = format!("{:?}", client);
417        assert!(debug_str.contains("Client"));
418
419        let awesome = ResolvedSource::AwesomeClaudeCode;
420        let debug_str = format!("{:?}", awesome);
421        assert!(debug_str.contains("AwesomeClaudeCode"));
422    }
423
424    #[test]
425    fn test_resolved_input_debug() {
426        let input = ResolvedInput {
427            paths: vec![PathBuf::from("./test")],
428            source: ResolvedSource::Local,
429            clients: Vec::new(),
430        };
431        let debug_str = format!("{:?}", input);
432        assert!(debug_str.contains("ResolvedInput"));
433    }
434
435    #[test]
436    fn test_input_source_debug() {
437        let source = InputSource::AllClients;
438        let debug_str = format!("{:?}", source);
439        assert!(debug_str.contains("AllClients"));
440    }
441
442    #[test]
443    fn test_resolved_input_client_not_requires_clone() {
444        let client = ResolvedInput {
445            paths: Vec::new(),
446            source: ResolvedSource::Client,
447            clients: Vec::new(),
448        };
449        assert!(!client.requires_clone());
450    }
451}