Skip to main content

codex_wrapper/command/
review.rs

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