codex_wrapper/command/
review.rs1use 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}