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)]
211 pub strict_secrets: bool,
212
213 #[arg(long)]
215 pub fix_hint: bool,
216
217 #[arg(long)]
219 pub compact: bool,
220
221 #[arg(short, long)]
223 pub watch: bool,
224
225 #[arg(long)]
227 pub init_hook: bool,
228
229 #[arg(long)]
231 pub remove_hook: bool,
232
233 #[arg(long)]
235 pub malware_db: Option<PathBuf>,
236
237 #[arg(long)]
239 pub no_malware_scan: bool,
240
241 #[arg(long)]
243 pub cve_db: Option<PathBuf>,
244
245 #[arg(long)]
247 pub no_cve_scan: bool,
248
249 #[arg(long)]
251 pub custom_rules: Option<PathBuf>,
252
253 #[arg(long)]
255 pub baseline: bool,
256
257 #[arg(long)]
259 pub check_drift: bool,
260
261 #[arg(long)]
263 pub init: bool,
264
265 #[arg(short, long)]
267 pub output: Option<PathBuf>,
268
269 #[arg(long, value_name = "FILE")]
271 pub save_baseline: Option<PathBuf>,
272
273 #[arg(long, value_name = "FILE")]
275 pub baseline_file: Option<PathBuf>,
276
277 #[arg(long, num_args = 2, value_names = ["PATH1", "PATH2"])]
279 pub compare: Option<Vec<PathBuf>>,
280
281 #[arg(long)]
283 pub fix: bool,
284
285 #[arg(long)]
287 pub fix_dry_run: bool,
288
289 #[arg(long)]
291 pub mcp_server: bool,
292
293 #[arg(long)]
295 pub hook_mode: bool,
296
297 #[arg(long)]
299 pub pin: bool,
300
301 #[arg(long)]
303 pub pin_verify: bool,
304
305 #[arg(long)]
307 pub pin_update: bool,
308
309 #[arg(long)]
311 pub pin_force: bool,
312
313 #[arg(long)]
315 pub ignore_pin: bool,
316
317 #[arg(long)]
319 pub deep_scan: bool,
320
321 #[arg(long, value_name = "NAME")]
323 pub profile: Option<String>,
324
325 #[arg(long, value_name = "NAME")]
327 pub save_profile: Option<String>,
328
329 #[arg(long)]
331 pub report_fp: bool,
332
333 #[arg(long)]
335 pub report_fp_dry_run: bool,
336
337 #[arg(long, value_name = "URL")]
339 pub report_fp_endpoint: Option<String>,
340
341 #[arg(long)]
343 pub no_telemetry: bool,
344
345 #[arg(long)]
347 pub sbom: bool,
348
349 #[arg(long, value_name = "FORMAT")]
351 pub sbom_format: Option<String>,
352
353 #[arg(long)]
355 pub sbom_npm: bool,
356
357 #[arg(long)]
359 pub sbom_cargo: bool,
360
361 #[arg(long)]
363 pub proxy: bool,
364
365 #[arg(long, value_name = "PORT")]
367 pub proxy_port: Option<u16>,
368
369 #[arg(long, value_name = "HOST:PORT")]
371 pub proxy_target: Option<String>,
372
373 #[arg(long)]
375 pub proxy_tls: bool,
376
377 #[arg(long)]
379 pub proxy_block: bool,
380
381 #[arg(long, value_name = "FILE")]
383 pub proxy_log: Option<std::path::PathBuf>,
384}
385
386impl Default for Cli {
387 fn default() -> Self {
388 Self {
389 paths: Vec::new(),
390 all_clients: false,
391 client: None,
392 remote: None,
393 git_ref: "HEAD".to_string(),
394 remote_auth: None,
395 remote_list: None,
396 awesome_claude_code: false,
397 parallel_clones: 4,
398 badge: false,
399 badge_format: BadgeFormat::Markdown,
400 summary: false,
401 format: OutputFormat::Terminal,
402 strict: false,
403 warn_only: false,
404 min_severity: None,
405 min_rule_severity: None,
406 scan_type: ScanType::Skill,
407 recursive: false,
408 ci: false,
409 verbose: false,
410 include_tests: false,
411 include_node_modules: false,
412 include_vendor: false,
413 min_confidence: Confidence::Tentative,
414 skip_comments: false,
415 strict_secrets: false,
416 fix_hint: false,
417 compact: false,
418 watch: false,
419 init_hook: false,
420 remove_hook: false,
421 malware_db: None,
422 no_malware_scan: false,
423 cve_db: None,
424 no_cve_scan: false,
425 custom_rules: None,
426 baseline: false,
427 check_drift: false,
428 init: false,
429 output: None,
430 save_baseline: None,
431 baseline_file: None,
432 compare: None,
433 fix: false,
434 fix_dry_run: false,
435 mcp_server: false,
436 hook_mode: false,
437 pin: false,
438 pin_verify: false,
439 pin_update: false,
440 pin_force: false,
441 ignore_pin: false,
442 deep_scan: false,
443 profile: None,
444 save_profile: None,
445 report_fp: false,
446 report_fp_dry_run: false,
447 report_fp_endpoint: None,
448 no_telemetry: false,
449 sbom: false,
450 sbom_format: None,
451 sbom_npm: false,
452 sbom_cargo: false,
453 proxy: false,
454 proxy_port: None,
455 proxy_target: None,
456 proxy_tls: false,
457 proxy_block: false,
458 proxy_log: None,
459 }
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::rules::{Confidence, RuleSeverity, Severity};
467 use clap::CommandFactory;
468
469 #[test]
470 fn test_cli_valid() {
471 Cli::command().debug_assert();
472 }
473
474 #[test]
475 fn test_parse_basic_args() {
476 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
477 assert_eq!(cli.paths.len(), 1);
478 assert!(!cli.strict);
479 assert!(!cli.recursive);
480 }
481
482 #[test]
483 fn test_parse_multiple_paths() {
484 let cli = Cli::try_parse_from(["cc-audit", "./skill1/", "./skill2/"]).unwrap();
485 assert_eq!(cli.paths.len(), 2);
486 }
487
488 #[test]
489 fn test_parse_format_json() {
490 let cli = Cli::try_parse_from(["cc-audit", "--format", "json", "./skill/"]).unwrap();
491 assert!(matches!(cli.format, OutputFormat::Json));
492 }
493
494 #[test]
495 fn test_parse_strict_mode() {
496 let cli = Cli::try_parse_from(["cc-audit", "--strict", "./skill/"]).unwrap();
497 assert!(cli.strict);
498 }
499
500 #[test]
501 fn test_parse_recursive() {
502 let cli = Cli::try_parse_from(["cc-audit", "-r", "./skills/"]).unwrap();
503 assert!(cli.recursive);
504 }
505
506 #[test]
507 fn test_parse_format_sarif() {
508 let cli = Cli::try_parse_from(["cc-audit", "--format", "sarif", "./skill/"]).unwrap();
509 assert!(matches!(cli.format, OutputFormat::Sarif));
510 }
511
512 #[test]
513 fn test_parse_type_hook() {
514 let cli = Cli::try_parse_from(["cc-audit", "--type", "hook", "./settings.json"]).unwrap();
515 assert!(matches!(cli.scan_type, ScanType::Hook));
516 }
517
518 #[test]
519 fn test_parse_type_mcp() {
520 let cli = Cli::try_parse_from(["cc-audit", "--type", "mcp", "./mcp.json"]).unwrap();
521 assert!(matches!(cli.scan_type, ScanType::Mcp));
522 }
523
524 #[test]
525 fn test_parse_type_command() {
526 let cli = Cli::try_parse_from(["cc-audit", "--type", "command", "./"]).unwrap();
527 assert!(matches!(cli.scan_type, ScanType::Command));
528 }
529
530 #[test]
531 fn test_parse_type_rules() {
532 let cli = Cli::try_parse_from(["cc-audit", "--type", "rules", "./"]).unwrap();
533 assert!(matches!(cli.scan_type, ScanType::Rules));
534 }
535
536 #[test]
537 fn test_parse_type_docker() {
538 let cli = Cli::try_parse_from(["cc-audit", "--type", "docker", "./"]).unwrap();
539 assert!(matches!(cli.scan_type, ScanType::Docker));
540 }
541
542 #[test]
543 fn test_parse_type_dependency() {
544 let cli = Cli::try_parse_from(["cc-audit", "--type", "dependency", "./"]).unwrap();
545 assert!(matches!(cli.scan_type, ScanType::Dependency));
546 }
547
548 #[test]
549 fn test_parse_ci_mode() {
550 let cli = Cli::try_parse_from(["cc-audit", "--ci", "./skill/"]).unwrap();
551 assert!(cli.ci);
552 }
553
554 #[test]
555 fn test_parse_verbose() {
556 let cli = Cli::try_parse_from(["cc-audit", "-v", "./skill/"]).unwrap();
557 assert!(cli.verbose);
558 }
559
560 #[test]
561 fn test_parse_all_options() {
562 let cli = Cli::try_parse_from([
563 "cc-audit",
564 "--format",
565 "json",
566 "--strict",
567 "--type",
568 "hook",
569 "--recursive",
570 "--ci",
571 "--verbose",
572 "./path/",
573 ])
574 .unwrap();
575 assert!(matches!(cli.format, OutputFormat::Json));
576 assert!(cli.strict);
577 assert!(matches!(cli.scan_type, ScanType::Hook));
578 assert!(cli.recursive);
579 assert!(cli.ci);
580 assert!(cli.verbose);
581 }
582
583 #[test]
584 fn test_default_values() {
585 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
586 assert!(matches!(cli.format, OutputFormat::Terminal));
587 assert!(matches!(cli.scan_type, ScanType::Skill));
588 assert!(!cli.strict);
589 assert!(!cli.recursive);
590 assert!(!cli.ci);
591 assert!(!cli.verbose);
592 assert!(!cli.include_tests);
593 assert!(!cli.include_node_modules);
594 assert!(!cli.include_vendor);
595 assert!(matches!(cli.min_confidence, Confidence::Tentative));
596 }
597
598 #[test]
599 fn test_parse_include_tests() {
600 let cli = Cli::try_parse_from(["cc-audit", "--include-tests", "./skill/"]).unwrap();
601 assert!(cli.include_tests);
602 }
603
604 #[test]
605 fn test_parse_include_node_modules() {
606 let cli = Cli::try_parse_from(["cc-audit", "--include-node-modules", "./skill/"]).unwrap();
607 assert!(cli.include_node_modules);
608 }
609
610 #[test]
611 fn test_parse_include_vendor() {
612 let cli = Cli::try_parse_from(["cc-audit", "--include-vendor", "./skill/"]).unwrap();
613 assert!(cli.include_vendor);
614 }
615
616 #[test]
617 fn test_parse_all_include_options() {
618 let cli = Cli::try_parse_from([
619 "cc-audit",
620 "--include-tests",
621 "--include-node-modules",
622 "--include-vendor",
623 "./skill/",
624 ])
625 .unwrap();
626 assert!(cli.include_tests);
627 assert!(cli.include_node_modules);
628 assert!(cli.include_vendor);
629 }
630
631 #[test]
632 fn test_parse_min_confidence_tentative() {
633 let cli =
634 Cli::try_parse_from(["cc-audit", "--min-confidence", "tentative", "./skill/"]).unwrap();
635 assert!(matches!(cli.min_confidence, Confidence::Tentative));
636 }
637
638 #[test]
639 fn test_parse_min_confidence_firm() {
640 let cli =
641 Cli::try_parse_from(["cc-audit", "--min-confidence", "firm", "./skill/"]).unwrap();
642 assert!(matches!(cli.min_confidence, Confidence::Firm));
643 }
644
645 #[test]
646 fn test_parse_min_confidence_certain() {
647 let cli =
648 Cli::try_parse_from(["cc-audit", "--min-confidence", "certain", "./skill/"]).unwrap();
649 assert!(matches!(cli.min_confidence, Confidence::Certain));
650 }
651
652 #[test]
653 fn test_parse_skip_comments() {
654 let cli = Cli::try_parse_from(["cc-audit", "--skip-comments", "./skill/"]).unwrap();
655 assert!(cli.skip_comments);
656 }
657
658 #[test]
659 fn test_default_skip_comments_false() {
660 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
661 assert!(!cli.skip_comments);
662 }
663
664 #[test]
665 fn test_parse_fix_hint() {
666 let cli = Cli::try_parse_from(["cc-audit", "--fix-hint", "./skill/"]).unwrap();
667 assert!(cli.fix_hint);
668 }
669
670 #[test]
671 fn test_default_fix_hint_false() {
672 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
673 assert!(!cli.fix_hint);
674 }
675
676 #[test]
677 fn test_parse_watch() {
678 let cli = Cli::try_parse_from(["cc-audit", "--watch", "./skill/"]).unwrap();
679 assert!(cli.watch);
680 }
681
682 #[test]
683 fn test_parse_watch_short() {
684 let cli = Cli::try_parse_from(["cc-audit", "-w", "./skill/"]).unwrap();
685 assert!(cli.watch);
686 }
687
688 #[test]
689 fn test_default_watch_false() {
690 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
691 assert!(!cli.watch);
692 }
693
694 #[test]
695 fn test_parse_init_hook() {
696 let cli = Cli::try_parse_from(["cc-audit", "--init-hook", "./repo/"]).unwrap();
697 assert!(cli.init_hook);
698 }
699
700 #[test]
701 fn test_parse_remove_hook() {
702 let cli = Cli::try_parse_from(["cc-audit", "--remove-hook", "./repo/"]).unwrap();
703 assert!(cli.remove_hook);
704 }
705
706 #[test]
707 fn test_default_init_hook_false() {
708 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
709 assert!(!cli.init_hook);
710 }
711
712 #[test]
713 fn test_default_remove_hook_false() {
714 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
715 assert!(!cli.remove_hook);
716 }
717
718 #[test]
719 fn test_parse_malware_db() {
720 let cli =
721 Cli::try_parse_from(["cc-audit", "--malware-db", "./custom.json", "./skill/"]).unwrap();
722 assert!(cli.malware_db.is_some());
723 assert_eq!(cli.malware_db.unwrap().to_str().unwrap(), "./custom.json");
724 }
725
726 #[test]
727 fn test_default_malware_db_none() {
728 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
729 assert!(cli.malware_db.is_none());
730 }
731
732 #[test]
733 fn test_parse_no_malware_scan() {
734 let cli = Cli::try_parse_from(["cc-audit", "--no-malware-scan", "./skill/"]).unwrap();
735 assert!(cli.no_malware_scan);
736 }
737
738 #[test]
739 fn test_default_no_malware_scan_false() {
740 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
741 assert!(!cli.no_malware_scan);
742 }
743
744 #[test]
745 fn test_parse_custom_rules() {
746 let cli = Cli::try_parse_from(["cc-audit", "--custom-rules", "./rules.yaml", "./skill/"])
747 .unwrap();
748 assert!(cli.custom_rules.is_some());
749 assert_eq!(cli.custom_rules.unwrap().to_str().unwrap(), "./rules.yaml");
750 }
751
752 #[test]
753 fn test_default_custom_rules_none() {
754 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
755 assert!(cli.custom_rules.is_none());
756 }
757
758 #[test]
759 fn test_parse_init() {
760 let cli = Cli::try_parse_from(["cc-audit", "--init", "./"]).unwrap();
761 assert!(cli.init);
762 }
763
764 #[test]
765 fn test_default_init_false() {
766 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
767 assert!(!cli.init);
768 }
769
770 #[test]
771 fn test_parse_warn_only() {
772 let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "./skill/"]).unwrap();
773 assert!(cli.warn_only);
774 }
775
776 #[test]
777 fn test_default_warn_only_false() {
778 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
779 assert!(!cli.warn_only);
780 }
781
782 #[test]
783 fn test_parse_min_severity_critical() {
784 let cli =
785 Cli::try_parse_from(["cc-audit", "--min-severity", "critical", "./skill/"]).unwrap();
786 assert_eq!(cli.min_severity, Some(Severity::Critical));
787 }
788
789 #[test]
790 fn test_parse_min_severity_high() {
791 let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "high", "./skill/"]).unwrap();
792 assert_eq!(cli.min_severity, Some(Severity::High));
793 }
794
795 #[test]
796 fn test_parse_min_severity_medium() {
797 let cli =
798 Cli::try_parse_from(["cc-audit", "--min-severity", "medium", "./skill/"]).unwrap();
799 assert_eq!(cli.min_severity, Some(Severity::Medium));
800 }
801
802 #[test]
803 fn test_parse_min_severity_low() {
804 let cli = Cli::try_parse_from(["cc-audit", "--min-severity", "low", "./skill/"]).unwrap();
805 assert_eq!(cli.min_severity, Some(Severity::Low));
806 }
807
808 #[test]
809 fn test_default_min_severity_none() {
810 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
811 assert!(cli.min_severity.is_none());
812 }
813
814 #[test]
815 fn test_parse_min_rule_severity_error() {
816 let cli =
817 Cli::try_parse_from(["cc-audit", "--min-rule-severity", "error", "./skill/"]).unwrap();
818 assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Error));
819 }
820
821 #[test]
822 fn test_parse_min_rule_severity_warn() {
823 let cli =
824 Cli::try_parse_from(["cc-audit", "--min-rule-severity", "warn", "./skill/"]).unwrap();
825 assert_eq!(cli.min_rule_severity, Some(RuleSeverity::Warn));
826 }
827
828 #[test]
829 fn test_default_min_rule_severity_none() {
830 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
831 assert!(cli.min_rule_severity.is_none());
832 }
833
834 #[test]
835 fn test_warn_only_with_strict_conflict() {
836 let cli = Cli::try_parse_from(["cc-audit", "--warn-only", "--strict", "./skill/"]).unwrap();
838 assert!(cli.warn_only);
839 assert!(cli.strict);
840 }
841
842 #[test]
843 fn test_parse_all_clients() {
844 let cli = Cli::try_parse_from(["cc-audit", "--all-clients"]).unwrap();
845 assert!(cli.all_clients);
846 assert!(cli.paths.is_empty());
847 }
848
849 #[test]
850 fn test_parse_client_claude() {
851 let cli = Cli::try_parse_from(["cc-audit", "--client", "claude"]).unwrap();
852 assert_eq!(cli.client, Some(ClientType::Claude));
853 assert!(cli.paths.is_empty());
854 }
855
856 #[test]
857 fn test_parse_client_cursor() {
858 let cli = Cli::try_parse_from(["cc-audit", "--client", "cursor"]).unwrap();
859 assert_eq!(cli.client, Some(ClientType::Cursor));
860 }
861
862 #[test]
863 fn test_parse_client_windsurf() {
864 let cli = Cli::try_parse_from(["cc-audit", "--client", "windsurf"]).unwrap();
865 assert_eq!(cli.client, Some(ClientType::Windsurf));
866 }
867
868 #[test]
869 fn test_parse_client_vscode() {
870 let cli = Cli::try_parse_from(["cc-audit", "--client", "vscode"]).unwrap();
871 assert_eq!(cli.client, Some(ClientType::Vscode));
872 }
873
874 #[test]
875 fn test_all_clients_conflicts_with_client() {
876 let result = Cli::try_parse_from(["cc-audit", "--all-clients", "--client", "claude"]);
877 assert!(result.is_err());
878 }
879
880 #[test]
881 fn test_all_clients_conflicts_with_remote() {
882 let result = Cli::try_parse_from([
883 "cc-audit",
884 "--all-clients",
885 "--remote",
886 "https://github.com/x/y",
887 ]);
888 assert!(result.is_err());
889 }
890
891 #[test]
892 fn test_default_client_none() {
893 let cli = Cli::try_parse_from(["cc-audit", "./skill/"]).unwrap();
894 assert!(cli.client.is_none());
895 assert!(!cli.all_clients);
896 }
897}