Skip to main content

rs_guard/
cli.rs

1//! CLI argument definitions using `clap` derive macros.
2
3use clap::Parser;
4use std::path::PathBuf;
5
6/// Command-line arguments for the rs-guard review tool.
7#[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    /// Path to system prompt markdown file.
13    #[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    /// LLM model identifier (default: provider-specific).
22    #[arg(
23        short,
24        long,
25        help = "LLM model identifier (default: provider-specific)"
26    )]
27    pub model: Option<String>,
28
29    /// Sampling temperature (0.0 - 2.0). Default: 0.1.
30    #[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    /// LLM provider to use. Default: deepseek.
39    #[arg(
40        long,
41        env = "RS_GUARD_PROVIDER",
42        help = "LLM provider to use [default: deepseek]"
43    )]
44    pub provider: Option<String>,
45
46    /// Path to configuration TOML file.
47    #[arg(
48        short,
49        long,
50        default_value = ".reviewer.toml",
51        help = "Path to configuration TOML file"
52    )]
53    pub config: PathBuf,
54
55    /// Maximum tokens for LLM completions.
56    #[arg(long, help = "Maximum tokens for LLM completions")]
57    pub max_tokens: Option<u32>,
58
59    /// Path to a pre-existing diff file to review instead of fetching from GitHub.
60    ///
61    /// When set, rs-guard reads the diff content from this file path
62    /// instead of calling the GitHub API. Useful in CI when the diff has
63    /// already been generated (e.g. by `git diff` or a prior workflow step).
64    /// If the file does not exist, an error is returned.
65    #[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    /// Bypass the response cache, forcing an LLM API call.
73    #[arg(long, help = "Bypass response cache and force LLM API call")]
74    pub no_cache: bool,
75
76    /// Run the full pipeline but do not submit reviews or block commits.
77    ///
78    /// Useful for testing configuration and prompt changes without affecting
79    /// the repository. Always exits with code 0.
80    #[arg(long, help = "Dry-run mode: review without submitting or blocking")]
81    pub dry_run: bool,
82}
83
84/// Validates that a temperature value is within the OpenAI-compatible range (0.0 - 2.0).
85fn 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}