1use crate::cli::{Cli, ScanType};
4use crate::client::{ClientType, DetectedClient, detect_client, detect_installed_clients};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone)]
9pub enum InputSource {
10 LocalPaths(Vec<PathBuf>),
12 RemoteUrl {
14 url: String,
15 git_ref: String,
16 auth_token: Option<String>,
17 },
18 RemoteList {
20 file: PathBuf,
21 git_ref: String,
22 auth_token: Option<String>,
23 },
24 AllClients,
26 SpecificClient(ClientType),
28 AwesomeClaudeCode,
30}
31
32impl InputSource {
33 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 pub fn is_local(&self) -> bool {
68 matches!(
69 self,
70 Self::LocalPaths(_) | Self::AllClients | Self::SpecificClient(_)
71 )
72 }
73
74 pub fn is_remote(&self) -> bool {
76 matches!(
77 self,
78 Self::RemoteUrl { .. } | Self::RemoteList { .. } | Self::AwesomeClaudeCode
79 )
80 }
81}
82
83pub struct SourceResolver;
85
86impl SourceResolver {
87 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 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 pub fn scan_type(cli: &Cli) -> ScanType {
156 cli.scan_type
157 }
158}
159
160#[derive(Debug, Clone)]
162pub enum ResolvedSource {
163 Local,
165 Client,
167 Remote {
169 urls: Vec<String>,
170 git_ref: String,
171 auth_token: Option<String>,
172 },
173 AwesomeClaudeCode,
175}
176
177#[derive(Debug, Clone)]
179pub struct ResolvedInput {
180 pub paths: Vec<PathBuf>,
182 pub source: ResolvedSource,
184 pub clients: Vec<DetectedClient>,
186}
187
188impl ResolvedInput {
189 pub fn has_paths(&self) -> bool {
191 !self.paths.is_empty()
192 }
193
194 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}