Skip to main content

cc_audit/
cli.rs

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/// Badge output format for security badges
34#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum BadgeFormat {
37    /// shields.io URL only
38    Url,
39    /// Markdown badge with link
40    #[default]
41    Markdown,
42    /// HTML image tag
43    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    /// Scan .claude/agents/ subagent definitions
71    Subagent,
72    /// Scan marketplace.json plugin definitions
73    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    /// Paths to scan (files or directories)
104    #[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    /// Scan all installed AI coding clients (Claude, Cursor, Windsurf, VS Code)
108    #[arg(long, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "client"])]
109    pub all_clients: bool,
110
111    /// Scan a specific AI coding client
112    #[arg(long, value_enum, conflicts_with_all = ["remote", "remote_list", "awesome_claude_code", "all_clients"])]
113    pub client: Option<ClientType>,
114
115    /// Remote repository URL to scan (e.g., `https://github.com/user/repo`)
116    #[arg(long, value_name = "URL")]
117    pub remote: Option<String>,
118
119    /// Git ref (branch, tag, or commit) for remote scan
120    #[arg(long, default_value = "HEAD")]
121    pub git_ref: String,
122
123    /// GitHub token for authentication (or use GITHUB_TOKEN env var)
124    #[arg(long, env = "GITHUB_TOKEN", value_name = "TOKEN")]
125    pub remote_auth: Option<String>,
126
127    /// File containing list of repository URLs to scan (one per line)
128    #[arg(long, conflicts_with = "remote", value_name = "FILE")]
129    pub remote_list: Option<PathBuf>,
130
131    /// Scan all repositories from awesome-claude-code
132    #[arg(long, conflicts_with_all = ["remote", "remote_list"])]
133    pub awesome_claude_code: bool,
134
135    /// Maximum number of parallel repository clones
136    #[arg(long, default_value = "4")]
137    pub parallel_clones: usize,
138
139    /// Generate security badge
140    #[arg(long)]
141    pub badge: bool,
142
143    /// Badge output format (url, markdown, html)
144    #[arg(long, value_enum, default_value_t = BadgeFormat::Markdown)]
145    pub badge_format: BadgeFormat,
146
147    /// Show summary only (for batch scans)
148    #[arg(long)]
149    pub summary: bool,
150
151    /// Output format
152    #[arg(short, long, value_enum, default_value_t = OutputFormat::Terminal)]
153    pub format: OutputFormat,
154
155    /// Strict mode: show medium/low severity findings and treat warnings as errors
156    #[arg(short, long)]
157    pub strict: bool,
158
159    /// Warn-only mode: treat all findings as warnings (exit code 0)
160    #[arg(long)]
161    pub warn_only: bool,
162
163    /// Minimum severity level to include in output (critical, high, medium, low)
164    #[arg(long, value_enum)]
165    pub min_severity: Option<Severity>,
166
167    /// Minimum rule severity to treat as errors (error, warn)
168    #[arg(long, value_enum)]
169    pub min_rule_severity: Option<RuleSeverity>,
170
171    /// Scan type
172    #[arg(short = 't', long = "type", value_enum, default_value_t = ScanType::Skill)]
173    pub scan_type: ScanType,
174
175    /// Recursive scan
176    #[arg(short, long)]
177    pub recursive: bool,
178
179    /// CI mode: non-interactive output
180    #[arg(long)]
181    pub ci: bool,
182
183    /// Verbose output
184    #[arg(short, long)]
185    pub verbose: bool,
186
187    /// Include test directories (tests/, spec/, __tests__, etc.) in scan
188    #[arg(long)]
189    pub include_tests: bool,
190
191    /// Include node_modules directories in scan
192    #[arg(long)]
193    pub include_node_modules: bool,
194
195    /// Include vendor directories (vendor/, third_party/) in scan
196    #[arg(long)]
197    pub include_vendor: bool,
198
199    /// Minimum confidence level for findings to be reported
200    #[arg(long, value_enum, default_value_t = Confidence::Tentative)]
201    pub min_confidence: Confidence,
202
203    /// Skip comment lines when scanning (lines starting with #, //, --, etc.)
204    #[arg(long)]
205    pub skip_comments: bool,
206
207    /// Show fix hints in terminal output
208    #[arg(long)]
209    pub fix_hint: bool,
210
211    /// Watch mode: continuously monitor files for changes and re-scan
212    #[arg(short, long)]
213    pub watch: bool,
214
215    /// Install cc-audit pre-commit hook in the git repository
216    #[arg(long)]
217    pub init_hook: bool,
218
219    /// Remove cc-audit pre-commit hook from the git repository
220    #[arg(long)]
221    pub remove_hook: bool,
222
223    /// Path to a custom malware signatures database (JSON)
224    #[arg(long)]
225    pub malware_db: Option<PathBuf>,
226
227    /// Disable malware signature scanning
228    #[arg(long)]
229    pub no_malware_scan: bool,
230
231    /// Path to a custom CVE database (JSON)
232    #[arg(long)]
233    pub cve_db: Option<PathBuf>,
234
235    /// Disable CVE vulnerability scanning
236    #[arg(long)]
237    pub no_cve_scan: bool,
238
239    /// Path to a custom rules file (YAML format)
240    #[arg(long)]
241    pub custom_rules: Option<PathBuf>,
242
243    /// Create a baseline snapshot for drift detection (rug pull prevention)
244    #[arg(long)]
245    pub baseline: bool,
246
247    /// Check for drift against saved baseline
248    #[arg(long)]
249    pub check_drift: bool,
250
251    /// Generate a default configuration file template
252    #[arg(long)]
253    pub init: bool,
254
255    /// Output file path (for HTML/JSON output)
256    #[arg(short, long)]
257    pub output: Option<PathBuf>,
258
259    /// Save baseline to specified file
260    #[arg(long, value_name = "FILE")]
261    pub save_baseline: Option<PathBuf>,
262
263    /// Compare against baseline file (show only new findings)
264    #[arg(long, value_name = "FILE")]
265    pub baseline_file: Option<PathBuf>,
266
267    /// Compare two paths and show differences
268    #[arg(long, num_args = 2, value_names = ["PATH1", "PATH2"])]
269    pub compare: Option<Vec<PathBuf>>,
270
271    /// Auto-fix issues (where possible)
272    #[arg(long)]
273    pub fix: bool,
274
275    /// Preview auto-fix changes without applying them
276    #[arg(long)]
277    pub fix_dry_run: bool,
278
279    /// Run as MCP server
280    #[arg(long)]
281    pub mcp_server: bool,
282
283    /// Run as Claude Code Hook (reads from stdin, writes to stdout)
284    #[arg(long)]
285    pub hook_mode: bool,
286
287    /// Pin MCP tool configurations for rug-pull detection
288    #[arg(long)]
289    pub pin: bool,
290
291    /// Verify MCP tool pins against current configuration
292    #[arg(long)]
293    pub pin_verify: bool,
294
295    /// Update MCP tool pins with current configuration
296    #[arg(long)]
297    pub pin_update: bool,
298
299    /// Force overwrite existing pins
300    #[arg(long)]
301    pub pin_force: bool,
302
303    /// Skip pin verification during scan
304    #[arg(long)]
305    pub ignore_pin: bool,
306
307    /// Enable deep scan with deobfuscation
308    #[arg(long)]
309    pub deep_scan: bool,
310
311    /// Load settings from a named profile
312    #[arg(long, value_name = "NAME")]
313    pub profile: Option<String>,
314
315    /// Save current settings as a named profile
316    #[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        // Both options can be parsed, but logic will determine behavior
755        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}