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