Skip to main content

codex_wrapper/command/
review.rs

1use crate::Codex;
2use crate::command::CodexCommand;
3#[cfg(feature = "json")]
4use crate::error::Error;
5use crate::error::Result;
6use crate::exec::{self, CommandOutput};
7#[cfg(feature = "json")]
8use crate::types::JsonLineEvent;
9
10#[derive(Debug, Clone)]
11pub struct ReviewCommand {
12    prompt: Option<String>,
13    config_overrides: Vec<String>,
14    enabled_features: Vec<String>,
15    disabled_features: Vec<String>,
16    uncommitted: bool,
17    base: Option<String>,
18    commit: Option<String>,
19    model: Option<String>,
20    title: Option<String>,
21    strict_config: bool,
22    dangerously_bypass_hook_trust: bool,
23    full_auto: bool,
24    dangerously_bypass_approvals_and_sandbox: bool,
25    skip_git_repo_check: bool,
26    ephemeral: bool,
27    json: bool,
28    output_last_message: Option<String>,
29    retry_policy: Option<crate::retry::RetryPolicy>,
30}
31
32impl ReviewCommand {
33    #[must_use]
34    pub fn new() -> Self {
35        Self {
36            prompt: None,
37            config_overrides: Vec::new(),
38            enabled_features: Vec::new(),
39            disabled_features: Vec::new(),
40            uncommitted: false,
41            base: None,
42            commit: None,
43            model: None,
44            title: None,
45            strict_config: false,
46            dangerously_bypass_hook_trust: false,
47            full_auto: false,
48            dangerously_bypass_approvals_and_sandbox: false,
49            skip_git_repo_check: false,
50            ephemeral: false,
51            json: false,
52            output_last_message: None,
53            retry_policy: None,
54        }
55    }
56
57    #[must_use]
58    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
59        self.prompt = Some(prompt.into());
60        self
61    }
62
63    #[must_use]
64    pub fn config(mut self, key_value: impl Into<String>) -> Self {
65        self.config_overrides.push(key_value.into());
66        self
67    }
68
69    #[must_use]
70    pub fn enable(mut self, feature: impl Into<String>) -> Self {
71        self.enabled_features.push(feature.into());
72        self
73    }
74
75    #[must_use]
76    pub fn disable(mut self, feature: impl Into<String>) -> Self {
77        self.disabled_features.push(feature.into());
78        self
79    }
80
81    #[must_use]
82    pub fn uncommitted(mut self) -> Self {
83        self.uncommitted = true;
84        self
85    }
86
87    #[must_use]
88    pub fn base(mut self, branch: impl Into<String>) -> Self {
89        self.base = Some(branch.into());
90        self
91    }
92
93    #[must_use]
94    pub fn commit(mut self, sha: impl Into<String>) -> Self {
95        self.commit = Some(sha.into());
96        self
97    }
98
99    #[must_use]
100    pub fn model(mut self, model: impl Into<String>) -> Self {
101        self.model = Some(model.into());
102        self
103    }
104
105    #[must_use]
106    pub fn title(mut self, title: impl Into<String>) -> Self {
107        self.title = Some(title.into());
108        self
109    }
110
111    /// Error on unrecognized config keys (`--strict-config`).
112    #[must_use]
113    pub fn strict_config(mut self) -> Self {
114        self.strict_config = true;
115        self
116    }
117
118    /// Bypass the hook trust prompt (`--dangerously-bypass-hook-trust`).
119    ///
120    /// Allows configured hooks to run without confirmation. Use with caution.
121    #[must_use]
122    pub fn dangerously_bypass_hook_trust(mut self) -> Self {
123        self.dangerously_bypass_hook_trust = true;
124        self
125    }
126
127    #[must_use]
128    pub fn full_auto(mut self) -> Self {
129        self.full_auto = true;
130        self
131    }
132
133    #[must_use]
134    pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
135        self.dangerously_bypass_approvals_and_sandbox = true;
136        self
137    }
138
139    #[must_use]
140    pub fn skip_git_repo_check(mut self) -> Self {
141        self.skip_git_repo_check = true;
142        self
143    }
144
145    #[must_use]
146    pub fn ephemeral(mut self) -> Self {
147        self.ephemeral = true;
148        self
149    }
150
151    #[must_use]
152    pub fn json(mut self) -> Self {
153        self.json = true;
154        self
155    }
156
157    #[must_use]
158    pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
159        self.output_last_message = Some(path.into());
160        self
161    }
162
163    #[must_use]
164    pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
165        self.retry_policy = Some(policy);
166        self
167    }
168
169    #[cfg(feature = "json")]
170    pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
171        let mut args = self.args();
172        if !self.json {
173            args.push("--json".into());
174        }
175
176        let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
177        output
178            .stdout
179            .lines()
180            .filter(|line| line.trim_start().starts_with('{'))
181            .map(|line| {
182                serde_json::from_str(line).map_err(|source| Error::Json {
183                    message: format!("failed to parse JSONL event: {line}"),
184                    source,
185                })
186            })
187            .collect()
188    }
189}
190
191impl Default for ReviewCommand {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197impl CodexCommand for ReviewCommand {
198    type Output = CommandOutput;
199
200    fn args(&self) -> Vec<String> {
201        let mut args = vec!["exec".into(), "review".into()];
202        for value in &self.config_overrides {
203            args.push("-c".into());
204            args.push(value.clone());
205        }
206        for value in &self.enabled_features {
207            args.push("--enable".into());
208            args.push(value.clone());
209        }
210        for value in &self.disabled_features {
211            args.push("--disable".into());
212            args.push(value.clone());
213        }
214        if self.uncommitted {
215            args.push("--uncommitted".into());
216        }
217        if let Some(base) = &self.base {
218            args.push("--base".into());
219            args.push(base.clone());
220        }
221        if let Some(commit) = &self.commit {
222            args.push("--commit".into());
223            args.push(commit.clone());
224        }
225        if let Some(model) = &self.model {
226            args.push("--model".into());
227            args.push(model.clone());
228        }
229        if let Some(title) = &self.title {
230            args.push("--title".into());
231            args.push(title.clone());
232        }
233        if self.strict_config {
234            args.push("--strict-config".into());
235        }
236        if self.full_auto {
237            args.push("--full-auto".into());
238        }
239        if self.dangerously_bypass_approvals_and_sandbox {
240            args.push("--dangerously-bypass-approvals-and-sandbox".into());
241        }
242        if self.dangerously_bypass_hook_trust {
243            args.push("--dangerously-bypass-hook-trust".into());
244        }
245        if self.skip_git_repo_check {
246            args.push("--skip-git-repo-check".into());
247        }
248        if self.ephemeral {
249            args.push("--ephemeral".into());
250        }
251        if self.json {
252            args.push("--json".into());
253        }
254        if let Some(path) = &self.output_last_message {
255            args.push("--output-last-message".into());
256            args.push(path.clone());
257        }
258        if let Some(prompt) = &self.prompt {
259            args.push(prompt.clone());
260        }
261        args
262    }
263
264    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
265        exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn review_args() {
275        let args = ReviewCommand::new()
276            .uncommitted()
277            .model("gpt-5")
278            .json()
279            .prompt("focus on correctness")
280            .args();
281
282        assert_eq!(
283            args,
284            vec![
285                "exec",
286                "review",
287                "--uncommitted",
288                "--model",
289                "gpt-5",
290                "--json",
291                "focus on correctness",
292            ]
293        );
294    }
295
296    #[test]
297    fn review_new_flags() {
298        let args = ReviewCommand::new()
299            .uncommitted()
300            .strict_config()
301            .dangerously_bypass_hook_trust()
302            .args();
303
304        assert_eq!(
305            args,
306            vec![
307                "exec",
308                "review",
309                "--uncommitted",
310                "--strict-config",
311                "--dangerously-bypass-hook-trust",
312            ]
313        );
314    }
315}