codex_wrapper/command/
review.rs1use 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 #[must_use]
113 pub fn strict_config(mut self) -> Self {
114 self.strict_config = true;
115 self
116 }
117
118 #[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}