1use crate::client::ClientType;
2use crate::rules::{Confidence, ParseEnumError, RuleSeverity, Severity};
3use clap::{Parser, ValueEnum};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum OutputFormat {
10 #[default]
11 Terminal,
12 Json,
13 Sarif,
14 Html,
15 Markdown,
16}
17
18impl std::str::FromStr for OutputFormat {
19 type Err = ParseEnumError;
20
21 fn from_str(s: &str) -> Result<Self, Self::Err> {
22 match s.to_lowercase().as_str() {
23 "terminal" | "term" => Ok(OutputFormat::Terminal),
24 "json" => Ok(OutputFormat::Json),
25 "sarif" => Ok(OutputFormat::Sarif),
26 "html" => Ok(OutputFormat::Html),
27 "markdown" | "md" => Ok(OutputFormat::Markdown),
28 _ => Err(ParseEnumError::invalid("OutputFormat", s)),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum BadgeFormat {
37 Url,
39 #[default]
41 Markdown,
42 Html,
44}
45
46impl std::str::FromStr for BadgeFormat {
47 type Err = ParseEnumError;
48
49 fn from_str(s: &str) -> Result<Self, Self::Err> {
50 match s.to_lowercase().as_str() {
51 "url" => Ok(BadgeFormat::Url),
52 "markdown" | "md" => Ok(BadgeFormat::Markdown),
53 "html" => Ok(BadgeFormat::Html),
54 _ => Err(ParseEnumError::invalid("BadgeFormat", s)),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "lowercase")]
61pub enum ScanType {
62 #[default]
63 Skill,
64 Hook,
65 Mcp,
66 Command,
67 Rules,
68 Docker,
69 Dependency,
70 Subagent,
72 Plugin,
74}
75
76impl std::str::FromStr for ScanType {
77 type Err = ParseEnumError;
78
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 match s.to_lowercase().as_str() {
81 "skill" => Ok(ScanType::Skill),
82 "hook" => Ok(ScanType::Hook),
83 "mcp" => Ok(ScanType::Mcp),
84 "command" | "cmd" => Ok(ScanType::Command),
85 "rules" => Ok(ScanType::Rules),
86 "docker" => Ok(ScanType::Docker),
87 "dependency" | "dep" | "deps" => Ok(ScanType::Dependency),
88 "subagent" | "agent" => Ok(ScanType::Subagent),
89 "plugin" => Ok(ScanType::Plugin),
90 _ => Err(ParseEnumError::invalid("ScanType", s)),
91 }
92 }
93}
94
95#[derive(Parser, Debug)]
96#[command(
97 name = "cc-audit",
98 version,
99 about = "Security auditor for Claude Code skills, hooks, and MCP servers",
100 long_about = "cc-audit scans Claude Code skills, hooks, and MCP servers for security vulnerabilities before installation."
101)]
102pub struct Cli {
103 #[arg(required_unless_present_any = ["remote", "remote_list", "awesome_claude_code", "init", "all_clients", "client", "hook_mode", "mcp_server"])]
105 pub paths: Vec<PathBuf>,
106
107 #[arg(long, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "client"])]
109 pub all_clients: bool,
110
111 #[arg(long, value_enum, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "all_clients"])]
113 pub client: Option<ClientType>,
114
115 #[arg(long, value_name = "URL")]
117 pub remote: Option<String>,
118
119 #[arg(long, default_value = "HEAD")]
121 pub git_ref: String,
122
123 #[arg(long, env = "GITHUB_TOKEN", value_name = "TOKEN")]
125 pub remote_auth: Option<String>,
126
127 #[arg(long, conflicts_with = "remote", value_name = "FILE")]
129 pub remote_list: Option<PathBuf>,
130
131 #[arg(long, conflicts_with_all = ["remote", "remote_list"])]
133 pub awesome_claude_code: bool,
134
135 #[arg(long, default_value = "4")]
137 pub parallel_clones: usize,
138
139 #[arg(long)]
141 pub badge: bool,
142
143 #[arg(long, value_enum, default_value_t = BadgeFormat::Markdown)]
145 pub badge_format: BadgeFormat,
146
147 #[arg(long)]
149 pub summary: bool,
150
151 #[arg(short, long, value_enum, default_value_t = OutputFormat::Terminal)]
153 pub format: OutputFormat,
154
155 #[arg(short, long)]
157 pub strict: bool,
158
159 #[arg(long)]
161 pub warn_only: bool,
162
163 #[arg(long, value_enum)]
165 pub min_severity: Option<Severity>,
166
167 #[arg(long, value_enum)]
169 pub min_rule_severity: Option<RuleSeverity>,
170
171 #[arg(short = 't', long = "type", value_enum, default_value_t = ScanType::Skill)]
173 pub scan_type: ScanType,
174
175 #[arg(short, long)]
177 pub recursive: bool,
178
179 #[arg(long)]
181 pub ci: bool,
182
183 #[arg(short, long)]
185 pub verbose: bool,
186
187 #[arg(long)]
189 pub include_tests: bool,
190
191 #[arg(long)]
193 pub include_node_modules: bool,
194
195 #[arg(long)]
197 pub include_vendor: bool,
198
199 #[arg(long, value_enum, default_value_t = Confidence::Tentative)]
201 pub min_confidence: Confidence,
202
203 #[arg(long)]
205 pub skip_comments: bool,
206
207 #[arg(long)]
209 pub fix_hint: bool,
210
211 #[arg(short, long)]
213 pub watch: bool,
214
215 #[arg(long)]
217 pub init_hook: bool,
218
219 #[arg(long)]
221 pub remove_hook: bool,
222
223 #[arg(long)]
225 pub malware_db: Option<PathBuf>,
226
227 #[arg(long)]
229 pub no_malware_scan: bool,
230
231 #[arg(long)]
233 pub cve_db: Option<PathBuf>,
234
235 #[arg(long)]
237 pub no_cve_scan: bool,
238
239 #[arg(long)]
241 pub custom_rules: Option<PathBuf>,
242
243 #[arg(long)]
245 pub baseline: bool,
246
247 #[arg(long)]
249 pub check_drift: bool,
250
251 #[arg(long)]
253 pub init: bool,
254
255 #[arg(short, long)]
257 pub output: Option<PathBuf>,
258
259 #[arg(long, value_name = "FILE")]
261 pub save_baseline: Option<PathBuf>,
262
263 #[arg(long, value_name = "FILE")]
265 pub baseline_file: Option<PathBuf>,
266
267 #[arg(long, num_args = 2, value_names = ["PATH1", "PATH2"])]
269 pub compare: Option<Vec<PathBuf>>,
270
271 #[arg(long)]
273 pub fix: bool,
274
275 #[arg(long)]
277 pub fix_dry_run: bool,
278
279 #[arg(long)]
281 pub mcp_server: bool,
282
283 #[arg(long)]
285 pub hook_mode: bool,
286
287 #[arg(long)]
289 pub pin: bool,
290
291 #[arg(long)]
293 pub pin_verify: bool,
294
295 #[arg(long)]
297 pub pin_update: bool,
298
299 #[arg(long)]
301 pub pin_force: bool,
302
303 #[arg(long)]
305 pub ignore_pin: bool,
306
307 #[arg(long)]
309 pub deep_scan: bool,
310
311 #[arg(long, value_name = "NAME")]
313 pub profile: Option<String>,
314
315 #[arg(long, value_name = "NAME")]
317 pub save_profile: Option<String>,
318}
319
320impl Default for Cli {
321 fn default() -> Self {
322 Self {
323 paths: Vec::new(),
324 all_clients: false,
325 client: None,
326 remote: None,
327 git_ref: "HEAD".to_string(),
328 remote_auth: None,
329 remote_list: None,
330 awesome_claude_code: false,
331 parallel_clones: 4,
332 badge: false,
333 badge_format: BadgeFormat::Markdown,
334 summary: false,
335 format: OutputFormat::Terminal,
336 strict: false,
337 warn_only: false,
338 min_severity: None,
339 min_rule_severity: None,
340 scan_type: ScanType::Skill,
341 recursive: false,
342 ci: false,
343 verbose: false,
344 include_tests: false,
345 include_node_modules: false,
346 include_vendor: false,
347 min_confidence: Confidence::Tentative,
348 skip_comments: false,
349 fix_hint: false,
350 watch: false,
351 init_hook: false,
352 remove_hook: false,
353 malware_db: None,
354 no_malware_scan: false,
355 cve_db: None,
356 no_cve_scan: false,
357 custom_rules: None,
358 baseline: false,
359 check_drift: false,
360 init: false,
361 output: None,
362 save_baseline: None,
363 baseline_file: None,
364 compare: None,
365 fix: false,
366 fix_dry_run: false,
367 mcp_server: false,
368 hook_mode: false,
369 pin: false,
370 pin_verify: false,
371 pin_update: false,
372 pin_force: false,
373 ignore_pin: false,
374 deep_scan: false,
375 profile: None,
376 save_profile: None,
377 }
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::rules::{Confidence, RuleSeverity, Severity};
385 use clap::CommandFactory;
386
387 #[test]
388 fn test_cli_valid() {
389 Cli::command().debug_assert();
390 }
391
392 #[test]
393 fn test_parse_basic_args() {
394 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
395 assert_eq!(cli.paths.len(), 1);
396 assert!(!cli.strict);
397 assert!(!cli.recursive);
398 }
399
400 #[test]
401 fn test_parse_multiple_paths() {
402 let cli = Cli::try_parse_from(["cc-audit", "./skill1/", "./skill2/"]).unwrap();
403 assert_eq!(cli.paths.len(), 2);
404 }
405
406 #[test]
407 fn test_parse_format_json() {
408 let cli = Cli::try_parse_from(["cc-audit", "--format", "json", "./skill/"]).unwrap();
409 assert!(matches!(cli.format, OutputFormat::Json));
410 }
411
412 #[test]
413 fn test_parse_strict_mode() {
414 let cli = Cli::try_parse_from(["cc-audit", "--strict", "./skill/"]).unwrap();
415 assert!(cli.strict);
416 }
417
418 #[test]
419 fn test_parse_recursive() {
420 let cli = Cli::try_parse_from(["cc-audit", "-r", "./skills/"]).unwrap();
421 assert!(cli.recursive);
422 }
423
424 #[test]
425 fn test_parse_format_sarif() {
426 let cli = Cli::try_parse_from(["cc-audit", "--format", "sarif", "./skill/"]).unwrap();
427 assert!(matches!(cli.format, OutputFormat::Sarif));
428 }
429
430 #[test]
431 fn test_parse_type_hook() {
432 let cli = Cli::try_parse_from(["cc-audit", "--type", "hook", "./settings.json"]).unwrap();
433 assert!(matches!(cli.scan_type, ScanType::Hook));
434 }
435
436 #[test]
437 fn test_parse_type_mcp() {
438 let cli = Cli::try_parse_from(["cc-audit", "--type", "mcp", "./mcp.json"]).unwrap();
439 assert!(matches!(cli.scan_type, ScanType::Mcp));
440 }
441
442 #[test]
443 fn test_parse_type_command() {
444 let cli = Cli::try_parse_from(["cc-audit", "--type", "command", "./"]).unwrap();
445 assert!(matches!(cli.scan_type, ScanType::Command));
446 }
447
448 #[test]
449 fn test_parse_type_rules() {
450 let cli = Cli::try_parse_from(["cc-audit", "--type", "rules", "./"]).unwrap();
451 assert!(matches!(cli.scan_type, ScanType::Rules));
452 }
453
454 #[test]
455 fn test_parse_type_docker() {
456 let cli = Cli::try_parse_from(["cc-audit", "--type", "docker", "./"]).unwrap();
457 assert!(matches!(cli.scan_type, ScanType::Docker));
458 }
459
460 #[test]
461 fn test_parse_type_dependency() {
462 let cli = Cli::try_parse_from(["cc-audit", "--type", "dependency", "./"]).unwrap();
463 assert!(matches!(cli.scan_type, ScanType::Dependency));
464 }
465
466 #[test]
467 fn test_parse_ci_mode() {
468 let cli = Cli::try_parse_from(["cc-audit", "--ci", "./skill/"]).unwrap();
469 assert!(cli.ci);
470 }
471
472 #[test]
473 fn test_parse_verbose() {
474 let cli = Cli::try_parse_from(["cc-audit", "-v", "./skill/"]).unwrap();
475 assert!(cli.verbose);
476 }
477
478 #[test]
479 fn test_parse_all_options() {
480 let cli = Cli::try_parse_from([
481 "cc-audit",
482 "--format",
483 "json",
484 "--strict",
485 "--type",
486 "hook",
487 "--recursive",
488 "--ci",
489 "--verbose",
490 "./path/",
491 ])
492 .unwrap();
493 assert!(matches!(cli.format, OutputFormat::Json));
494 assert!(cli.strict);
495 assert!(matches!(cli.scan_type, ScanType::Hook));
496 assert!(cli.recursive);
497 assert!(cli.ci);
498 assert!(cli.verbose);
499 }
500
501 #[test]
502 fn test_default_values() {
503 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
504 assert!(matches!(cli.format, OutputFormat::Terminal));
505 assert!(matches!(cli.scan_type, ScanType::Skill));
506 assert!(!cli.strict);
507 assert!(!cli.recursive);
508 assert!(!cli.ci);
509 assert!(!cli.verbose);
510 assert!(!cli.include_tests);
511 assert!(!cli.include_node_modules);
512 assert!(!cli.include_vendor);
513 assert!(matches!(cli.min_confidence, Confidence::Tentative));
514 }
515
516 #[test]
517 fn test_parse_include_tests() {
518 let cli = Cli::try_parse_from(["cc-audit", "--include-tests", "./skill/"]).unwrap();
519 assert!(cli.include_tests);
520 }
521
522 #[test]
523 fn test_parse_include_node_modules() {
524 let cli = Cli::try_parse_from(["cc-audit", "--include-node-modules", "./skill/"]).unwrap();
525 assert!(cli.include_node_modules);
526 }
527
528 #[test]
529 fn test_parse_include_vendor() {
530 let cli = Cli::try_parse_from(["cc-audit", "--include-vendor", "./skill/"]).unwrap();
531 assert!(cli.include_vendor);
532 }
533
534 #[test]
535 fn test_parse_all_include_options() {
536 let cli = Cli::try_parse_from([
537 "cc-audit",
538 "--include-tests",
539 "--include-node-modules",
540 "--include-vendor",
541 "./skill/",
542 ])
543 .unwrap();
544 assert!(cli.include_tests);
545 assert!(cli.include_node_modules);
546 assert!(cli.include_vendor);
547 }
548
549 #[test]
550 fn test_parse_min_confidence_tentative() {
551 let cli =
552 Cli::try_parse_from(["cc-audit", "--min-confidence", "tentative", "./skill/"]).unwrap();
553 assert!(matches!(cli.min_confidence, Confidence::Tentative));
554 }
555
556 #[test]
557 fn test_parse_min_confidence_firm() {
558 let cli =
559 Cli::try_parse_from(["cc-audit", "--min-confidence", "firm", "./skill/"]).unwrap();
560 assert!(matches!(cli.min_confidence, Confidence::Firm));
561 }
562
563 #[test]
564 fn test_parse_min_confidence_certain() {
565 let cli =
566 Cli::try_parse_from(["cc-audit", "--min-confidence", "certain", "./skill/"]).unwrap();
567 assert!(matches!(cli.min_confidence, Confidence::Certain));
568 }
569
570 #[test]
571 fn test_parse_skip_comments() {
572 let cli = Cli::try_parse_from(["cc-audit", "--skip-comments", "./skill/"]).unwrap();
573 assert!(cli.skip_comments);
574 }
575
576 #[test]
577 fn test_default_skip_comments_false() {
578 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
579 assert!(!cli.skip_comments);
580 }
581
582 #[test]
583 fn test_parse_fix_hint() {
584 let cli = Cli::try_parse_from(["cc-audit", "--fix-hint", "./skill/"]).unwrap();
585 assert!(cli.fix_hint);
586 }
587
588 #[test]
589 fn test_default_fix_hint_false() {
590 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
591 assert!(!cli.fix_hint);
592 }
593
594 #[test]
595 fn test_parse_watch() {
596 let cli = Cli::try_parse_from(["cc-audit", "--watch", "./skill/"]).unwrap();
597 assert!(cli.watch);
598 }
599
600 #[test]
601 fn test_parse_watch_short() {
602 let cli = Cli::try_parse_from(["cc-audit", "-w", "./skill/"]).unwrap();
603 assert!(cli.watch);
604 }
605
606 #[test]
607 fn test_default_watch_false() {
608 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
609 assert!(!cli.watch);
610 }
611
612 #[test]
613 fn test_parse_init_hook() {
614 let cli = Cli::try_parse_from(["cc-audit", "--init-hook", "./repo/"]).unwrap();
615 assert!(cli.init_hook);
616 }
617
618 #[test]
619 fn test_parse_remove_hook() {
620 let cli = Cli::try_parse_from(["cc-audit", "--remove-hook", "./repo/"]).unwrap();
621 assert!(cli.remove_hook);
622 }
623
624 #[test]
625 fn test_default_init_hook_false() {
626 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
627 assert!(!cli.init_hook);
628 }
629
630 #[test]
631 fn test_default_remove_hook_false() {
632 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
633 assert!(!cli.remove_hook);
634 }
635
636 #[test]
637 fn test_parse_malware_db() {
638 let cli =
639 Cli::try_parse_from(["cc-audit", "--malware-db", "./custom.json", "./skill/"]).unwrap();
640 assert!(cli.malware_db.is_some());
641 assert_eq!(cli.malware_db.unwrap().to_str().unwrap(), "./custom.json");
642 }
643
644 #[test]
645 fn test_default_malware_db_none() {
646 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
647 assert!(cli.malware_db.is_none());
648 }
649
650 #[test]
651 fn test_parse_no_malware_scan() {
652 let cli = Cli::try_parse_from(["cc-audit", "--no-malware-scan", "./skill/"]).unwrap();
653 assert!(cli.no_malware_scan);
654 }
655
656 #[test]
657 fn test_default_no_malware_scan_false() {
658 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
659 assert!(!cli.no_malware_scan);
660 }
661
662 #[test]
663 fn test_parse_custom_rules() {
664 let cli = Cli::try_parse_from(["cc-audit", "--custom-rules", "./rules.yaml", "./skill/"])
665 .unwrap();
666 assert!(cli.custom_rules.is_some());
667 assert_eq!(cli.custom_rules.unwrap().to_str().unwrap(), "./rules.yaml");
668 }
669
670 #[test]
671 fn test_default_custom_rules_none() {
672 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
673 assert!(cli.custom_rules.is_none());
674 }
675
676 #[test]
677 fn test_parse_init() {
678 let cli = Cli::try_parse_from(["cc-audit", "--init", "./"]).unwrap();
679 assert!(cli.init);
680 }
681
682 #[test]
683 fn test_default_init_false() {
684 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
685 assert!(!cli.init);
686 }
687
688 #[test]
689 fn test_parse_warn_only() {
690 let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "./skill/"]).unwrap();
691 assert!(cli.warn_only);
692 }
693
694 #[test]
695 fn test_default_warn_only_false() {
696 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
697 assert!(!cli.warn_only);
698 }
699
700 #[test]
701 fn test_parse_min_severity_critical() {
702 let cli =
703 Cli::try_parse_from(["cc-audit", "--min-severity", "critical", "./skill/"]).unwrap();
704 assert_eq!(cli.min_severity, Some(Severity::Critical));
705 }
706
707 #[test]
708 fn test_parse_min_severity_high() {
709 let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "high", "./skill/"]).unwrap();
710 assert_eq!(cli.min_severity, Some(Severity::High));
711 }
712
713 #[test]
714 fn test_parse_min_severity_medium() {
715 let cli =
716 Cli::try_parse_from(["cc-audit", "--min-severity", "medium", "./skill/"]).unwrap();
717 assert_eq!(cli.min_severity, Some(Severity::Medium));
718 }
719
720 #[test]
721 fn test_parse_min_severity_low() {
722 let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "low", "./skill/"]).unwrap();
723 assert_eq!(cli.min_severity, Some(Severity::Low));
724 }
725
726 #[test]
727 fn test_default_min_severity_none() {
728 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
729 assert!(cli.min_severity.is_none());
730 }
731
732 #[test]
733 fn test_parse_min_rule_severity_error() {
734 let cli =
735 Cli::try_parse_from(["cc-audit", "--min-rule-severity", "error", "./skill/"]).unwrap();
736 assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Error));
737 }
738
739 #[test]
740 fn test_parse_min_rule_severity_warn() {
741 let cli =
742 Cli::try_parse_from(["cc-audit", "--min-rule-severity", "warn", "./skill/"]).unwrap();
743 assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Warn));
744 }
745
746 #[test]
747 fn test_default_min_rule_severity_none() {
748 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
749 assert!(cli.min_rule_severity.is_none());
750 }
751
752 #[test]
753 fn test_warn_only_with_strict_conflict() {
754 let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "--strict", "./skill/"]).unwrap();
756 assert!(cli.warn_only);
757 assert!(cli.strict);
758 }
759
760 #[test]
761 fn test_parse_all_clients() {
762 let cli = Cli::try_parse_from(["cc-audit", "--all-clients"]).unwrap();
763 assert!(cli.all_clients);
764 assert!(cli.paths.is_empty());
765 }
766
767 #[test]
768 fn test_parse_client_claude() {
769 let cli = Cli::try_parse_from(["cc-audit", "--client", "claude"]).unwrap();
770 assert_eq!(cli.client, Some(ClientType::Claude));
771 assert!(cli.paths.is_empty());
772 }
773
774 #[test]
775 fn test_parse_client_cursor() {
776 let cli = Cli::try_parse_from(["cc-audit", "--client", "cursor"]).unwrap();
777 assert_eq!(cli.client, Some(ClientType::Cursor));
778 }
779
780 #[test]
781 fn test_parse_client_windsurf() {
782 let cli = Cli::try_parse_from(["cc-audit", "--client", "windsurf"]).unwrap();
783 assert_eq!(cli.client, Some(ClientType::Windsurf));
784 }
785
786 #[test]
787 fn test_parse_client_vscode() {
788 let cli = Cli::try_parse_from(["cc-audit", "--client", "vscode"]).unwrap();
789 assert_eq!(cli.client, Some(ClientType::Vscode));
790 }
791
792 #[test]
793 fn test_all_clients_conflicts_with_client() {
794 let result = Cli::try_parse_from(["cc-audit", "--all-clients", "--client", "claude"]);
795 assert!(result.is_err());
796 }
797
798 #[test]
799 fn test_all_clients_conflicts_with_remote() {
800 let result = Cli::try_parse_from([
801 "cc-audit",
802 "--all-clients",
803 "--remote",
804 "https://github.com/x/y",
805 ]);
806 assert!(result.is_err());
807 }
808
809 #[test]
810 fn test_default_client_none() {
811 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
812 assert!(cli.client.is_none());
813 assert!(!cli.all_clients);
814 }
815}