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