1use clap::Parser;
4use std::path::PathBuf;
5
6#[derive(Parser, Debug, Clone)]
8#[command(name = "rs-guard")]
9#[command(about = "AI-powered code review CLI for GitHub PRs")]
10#[command(version = env!("CARGO_PKG_VERSION"))]
11pub struct Args {
12 #[arg(
14 short,
15 long,
16 default_value = ".github/review-prompt.md",
17 help = "Path to system prompt markdown file"
18 )]
19 pub prompt_file: PathBuf,
20
21 #[arg(
23 short,
24 long,
25 help = "LLM model identifier (default: provider-specific)"
26 )]
27 pub model: Option<String>,
28
29 #[arg(
31 short,
32 long,
33 help = "Sampling temperature (0.0 - 2.0) [default: 0.1]",
34 value_parser = parse_temperature
35 )]
36 pub temperature: Option<f32>,
37
38 #[arg(
40 long,
41 env = "RS_GUARD_PROVIDER",
42 help = "LLM provider to use [default: deepseek]"
43 )]
44 pub provider: Option<String>,
45
46 #[arg(
48 short,
49 long,
50 default_value = ".reviewer.toml",
51 help = "Path to configuration TOML file"
52 )]
53 pub config: PathBuf,
54
55 #[arg(long, help = "Maximum tokens for LLM completions")]
57 pub max_tokens: Option<u32>,
58
59 #[arg(
66 long,
67 env = "RS_GUARD_DIFF_FILE",
68 help = "Path to a pre-existing diff file to review"
69 )]
70 pub diff_file: Option<String>,
71
72 #[arg(long, help = "Bypass response cache and force LLM API call")]
74 pub no_cache: bool,
75
76 #[arg(long, help = "Dry-run mode: review without submitting or blocking")]
81 pub dry_run: bool,
82}
83
84fn parse_temperature(s: &str) -> Result<f32, String> {
86 let v: f32 = s
87 .parse()
88 .map_err(|e| format!("Invalid temperature '{}': {}", s, e))?;
89 if !(0.0..=2.0).contains(&v) {
90 return Err(format!(
91 "Temperature must be between 0.0 and 2.0, got: {}",
92 v
93 ));
94 }
95 Ok(v)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_parse_temperature_valid() {
104 assert_eq!(parse_temperature("0.0").unwrap(), 0.0);
105 assert_eq!(parse_temperature("0.1").unwrap(), 0.1);
106 assert_eq!(parse_temperature("1.0").unwrap(), 1.0);
107 assert_eq!(parse_temperature("2.0").unwrap(), 2.0);
108 }
109
110 #[test]
111 fn test_parse_temperature_out_of_range() {
112 assert!(parse_temperature("-0.1").is_err());
113 assert!(parse_temperature("2.1").is_err());
114 assert!(parse_temperature("5.0").is_err());
115 }
116
117 #[test]
118 fn test_parse_temperature_invalid_string() {
119 assert!(parse_temperature("not-a-number").is_err());
120 assert!(parse_temperature("").is_err());
121 }
122
123 #[test]
124 fn test_dry_run_flag_parsing() {
125 let args = Args::parse_from(["rs-guard", "--dry-run"]);
126 assert!(args.dry_run);
127 }
128
129 #[test]
130 fn test_dry_run_flag_default_false() {
131 let args = Args::parse_from(["rs-guard"]);
132 assert!(!args.dry_run);
133 }
134}