1use crate::Codex;
2use crate::command::CodexCommand;
3use crate::error::{Error, Result};
4use crate::exec::{self, CommandOutput};
5use crate::types::{ApprovalPolicy, Color, JsonLineEvent, SandboxMode};
6
7#[derive(Debug, Clone)]
8pub struct ExecCommand {
9 prompt: Option<String>,
10 config_overrides: Vec<String>,
11 enabled_features: Vec<String>,
12 disabled_features: Vec<String>,
13 images: Vec<String>,
14 model: Option<String>,
15 oss: bool,
16 local_provider: Option<String>,
17 sandbox: Option<SandboxMode>,
18 approval_policy: Option<ApprovalPolicy>,
19 profile: Option<String>,
20 full_auto: bool,
21 dangerously_bypass_approvals_and_sandbox: bool,
22 cd: Option<String>,
23 skip_git_repo_check: bool,
24 add_dirs: Vec<String>,
25 ephemeral: bool,
26 output_schema: Option<String>,
27 color: Option<Color>,
28 progress_cursor: bool,
29 json: bool,
30 output_last_message: Option<String>,
31 retry_policy: Option<crate::retry::RetryPolicy>,
32}
33
34impl ExecCommand {
35 #[must_use]
36 pub fn new(prompt: impl Into<String>) -> Self {
37 Self {
38 prompt: Some(prompt.into()),
39 config_overrides: Vec::new(),
40 enabled_features: Vec::new(),
41 disabled_features: Vec::new(),
42 images: Vec::new(),
43 model: None,
44 oss: false,
45 local_provider: None,
46 sandbox: None,
47 approval_policy: None,
48 profile: None,
49 full_auto: false,
50 dangerously_bypass_approvals_and_sandbox: false,
51 cd: None,
52 skip_git_repo_check: false,
53 add_dirs: Vec::new(),
54 ephemeral: false,
55 output_schema: None,
56 color: None,
57 progress_cursor: false,
58 json: false,
59 output_last_message: None,
60 retry_policy: None,
61 }
62 }
63
64 #[must_use]
65 pub fn from_stdin() -> Self {
66 Self::new("-")
67 }
68
69 #[must_use]
70 pub fn config(mut self, key_value: impl Into<String>) -> Self {
71 self.config_overrides.push(key_value.into());
72 self
73 }
74
75 #[must_use]
76 pub fn enable(mut self, feature: impl Into<String>) -> Self {
77 self.enabled_features.push(feature.into());
78 self
79 }
80
81 #[must_use]
82 pub fn disable(mut self, feature: impl Into<String>) -> Self {
83 self.disabled_features.push(feature.into());
84 self
85 }
86
87 #[must_use]
88 pub fn image(mut self, path: impl Into<String>) -> Self {
89 self.images.push(path.into());
90 self
91 }
92
93 #[must_use]
94 pub fn model(mut self, model: impl Into<String>) -> Self {
95 self.model = Some(model.into());
96 self
97 }
98
99 #[must_use]
100 pub fn oss(mut self) -> Self {
101 self.oss = true;
102 self
103 }
104
105 #[must_use]
106 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
107 self.local_provider = Some(provider.into());
108 self
109 }
110
111 #[must_use]
112 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
113 self.sandbox = Some(sandbox);
114 self
115 }
116
117 #[must_use]
118 pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
119 self.approval_policy = Some(policy);
120 self
121 }
122
123 #[must_use]
124 pub fn profile(mut self, profile: impl Into<String>) -> Self {
125 self.profile = Some(profile.into());
126 self
127 }
128
129 #[must_use]
130 pub fn full_auto(mut self) -> Self {
131 self.full_auto = true;
132 self
133 }
134
135 #[must_use]
136 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
137 self.dangerously_bypass_approvals_and_sandbox = true;
138 self
139 }
140
141 #[must_use]
142 pub fn cd(mut self, dir: impl Into<String>) -> Self {
143 self.cd = Some(dir.into());
144 self
145 }
146
147 #[must_use]
148 pub fn skip_git_repo_check(mut self) -> Self {
149 self.skip_git_repo_check = true;
150 self
151 }
152
153 #[must_use]
154 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
155 self.add_dirs.push(dir.into());
156 self
157 }
158
159 #[must_use]
160 pub fn ephemeral(mut self) -> Self {
161 self.ephemeral = true;
162 self
163 }
164
165 #[must_use]
166 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
167 self.output_schema = Some(path.into());
168 self
169 }
170
171 #[must_use]
172 pub fn color(mut self, color: Color) -> Self {
173 self.color = Some(color);
174 self
175 }
176
177 #[must_use]
178 pub fn progress_cursor(mut self) -> Self {
179 self.progress_cursor = true;
180 self
181 }
182
183 #[must_use]
184 pub fn json(mut self) -> Self {
185 self.json = true;
186 self
187 }
188
189 #[must_use]
190 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
191 self.output_last_message = Some(path.into());
192 self
193 }
194
195 #[must_use]
196 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
197 self.retry_policy = Some(policy);
198 self
199 }
200
201 #[cfg(feature = "json")]
202 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
203 let mut args = self.args();
204 if !self.json {
205 args.push("--json".into());
206 }
207
208 let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
209 parse_json_lines(&output.stdout)
210 }
211}
212
213impl CodexCommand for ExecCommand {
214 type Output = CommandOutput;
215
216 fn args(&self) -> Vec<String> {
217 let mut args = vec!["exec".to_string()];
218
219 push_repeat(&mut args, "-c", &self.config_overrides);
220 push_repeat(&mut args, "--enable", &self.enabled_features);
221 push_repeat(&mut args, "--disable", &self.disabled_features);
222 push_repeat(&mut args, "--image", &self.images);
223
224 if let Some(model) = &self.model {
225 args.push("--model".into());
226 args.push(model.clone());
227 }
228 if self.oss {
229 args.push("--oss".into());
230 }
231 if let Some(local_provider) = &self.local_provider {
232 args.push("--local-provider".into());
233 args.push(local_provider.clone());
234 }
235 if let Some(sandbox) = self.sandbox {
236 args.push("--sandbox".into());
237 args.push(sandbox.as_arg().into());
238 }
239 if let Some(policy) = self.approval_policy {
240 args.push("--ask-for-approval".into());
241 args.push(policy.as_arg().into());
242 }
243 if let Some(profile) = &self.profile {
244 args.push("--profile".into());
245 args.push(profile.clone());
246 }
247 if self.full_auto {
248 args.push("--full-auto".into());
249 }
250 if self.dangerously_bypass_approvals_and_sandbox {
251 args.push("--dangerously-bypass-approvals-and-sandbox".into());
252 }
253 if let Some(cd) = &self.cd {
254 args.push("--cd".into());
255 args.push(cd.clone());
256 }
257 if self.skip_git_repo_check {
258 args.push("--skip-git-repo-check".into());
259 }
260 push_repeat(&mut args, "--add-dir", &self.add_dirs);
261 if self.ephemeral {
262 args.push("--ephemeral".into());
263 }
264 if let Some(output_schema) = &self.output_schema {
265 args.push("--output-schema".into());
266 args.push(output_schema.clone());
267 }
268 if let Some(color) = self.color {
269 args.push("--color".into());
270 args.push(color.as_arg().into());
271 }
272 if self.progress_cursor {
273 args.push("--progress-cursor".into());
274 }
275 if self.json {
276 args.push("--json".into());
277 }
278 if let Some(path) = &self.output_last_message {
279 args.push("--output-last-message".into());
280 args.push(path.clone());
281 }
282 if let Some(prompt) = &self.prompt {
283 args.push(prompt.clone());
284 }
285
286 args
287 }
288
289 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
290 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
291 }
292}
293
294#[derive(Debug, Clone)]
295pub struct ExecResumeCommand {
296 session_id: Option<String>,
297 prompt: Option<String>,
298 last: bool,
299 all: bool,
300 config_overrides: Vec<String>,
301 enabled_features: Vec<String>,
302 disabled_features: Vec<String>,
303 images: Vec<String>,
304 model: Option<String>,
305 full_auto: bool,
306 dangerously_bypass_approvals_and_sandbox: bool,
307 skip_git_repo_check: bool,
308 ephemeral: bool,
309 json: bool,
310 output_last_message: Option<String>,
311 retry_policy: Option<crate::retry::RetryPolicy>,
312}
313
314impl ExecResumeCommand {
315 #[must_use]
316 pub fn new() -> Self {
317 Self {
318 session_id: None,
319 prompt: None,
320 last: false,
321 all: false,
322 config_overrides: Vec::new(),
323 enabled_features: Vec::new(),
324 disabled_features: Vec::new(),
325 images: Vec::new(),
326 model: None,
327 full_auto: false,
328 dangerously_bypass_approvals_and_sandbox: false,
329 skip_git_repo_check: false,
330 ephemeral: false,
331 json: false,
332 output_last_message: None,
333 retry_policy: None,
334 }
335 }
336
337 #[must_use]
338 pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
339 self.session_id = Some(session_id.into());
340 self
341 }
342
343 #[must_use]
344 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
345 self.prompt = Some(prompt.into());
346 self
347 }
348
349 #[must_use]
350 pub fn last(mut self) -> Self {
351 self.last = true;
352 self
353 }
354
355 #[must_use]
356 pub fn all(mut self) -> Self {
357 self.all = true;
358 self
359 }
360
361 #[must_use]
362 pub fn model(mut self, model: impl Into<String>) -> Self {
363 self.model = Some(model.into());
364 self
365 }
366
367 #[must_use]
368 pub fn image(mut self, path: impl Into<String>) -> Self {
369 self.images.push(path.into());
370 self
371 }
372
373 #[must_use]
374 pub fn json(mut self) -> Self {
375 self.json = true;
376 self
377 }
378
379 #[must_use]
380 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
381 self.output_last_message = Some(path.into());
382 self
383 }
384
385 #[must_use]
386 pub fn config(mut self, key_value: impl Into<String>) -> Self {
387 self.config_overrides.push(key_value.into());
388 self
389 }
390
391 #[must_use]
392 pub fn enable(mut self, feature: impl Into<String>) -> Self {
393 self.enabled_features.push(feature.into());
394 self
395 }
396
397 #[must_use]
398 pub fn disable(mut self, feature: impl Into<String>) -> Self {
399 self.disabled_features.push(feature.into());
400 self
401 }
402
403 #[must_use]
404 pub fn full_auto(mut self) -> Self {
405 self.full_auto = true;
406 self
407 }
408
409 #[must_use]
410 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
411 self.dangerously_bypass_approvals_and_sandbox = true;
412 self
413 }
414
415 #[must_use]
416 pub fn skip_git_repo_check(mut self) -> Self {
417 self.skip_git_repo_check = true;
418 self
419 }
420
421 #[must_use]
422 pub fn ephemeral(mut self) -> Self {
423 self.ephemeral = true;
424 self
425 }
426
427 #[must_use]
428 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
429 self.retry_policy = Some(policy);
430 self
431 }
432}
433
434impl Default for ExecResumeCommand {
435 fn default() -> Self {
436 Self::new()
437 }
438}
439
440impl CodexCommand for ExecResumeCommand {
441 type Output = CommandOutput;
442
443 fn args(&self) -> Vec<String> {
444 let mut args = vec!["exec".into(), "resume".into()];
445 push_repeat(&mut args, "-c", &self.config_overrides);
446 push_repeat(&mut args, "--enable", &self.enabled_features);
447 push_repeat(&mut args, "--disable", &self.disabled_features);
448 if self.last {
449 args.push("--last".into());
450 }
451 if self.all {
452 args.push("--all".into());
453 }
454 push_repeat(&mut args, "--image", &self.images);
455 if let Some(model) = &self.model {
456 args.push("--model".into());
457 args.push(model.clone());
458 }
459 if self.full_auto {
460 args.push("--full-auto".into());
461 }
462 if self.dangerously_bypass_approvals_and_sandbox {
463 args.push("--dangerously-bypass-approvals-and-sandbox".into());
464 }
465 if self.skip_git_repo_check {
466 args.push("--skip-git-repo-check".into());
467 }
468 if self.ephemeral {
469 args.push("--ephemeral".into());
470 }
471 if self.json {
472 args.push("--json".into());
473 }
474 if let Some(path) = &self.output_last_message {
475 args.push("--output-last-message".into());
476 args.push(path.clone());
477 }
478 if let Some(session_id) = &self.session_id {
479 args.push(session_id.clone());
480 }
481 if let Some(prompt) = &self.prompt {
482 args.push(prompt.clone());
483 }
484 args
485 }
486
487 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
488 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
489 }
490}
491
492fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
493 for value in values {
494 args.push(flag.into());
495 args.push(value.clone());
496 }
497}
498
499#[cfg(feature = "json")]
500fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
501 stdout
502 .lines()
503 .filter(|line| line.trim_start().starts_with('{'))
504 .map(|line| {
505 serde_json::from_str(line).map_err(|source| Error::Json {
506 message: format!("failed to parse JSONL event: {line}"),
507 source,
508 })
509 })
510 .collect()
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 #[test]
518 fn exec_args() {
519 let args = ExecCommand::new("fix the test")
520 .model("gpt-5")
521 .sandbox(SandboxMode::WorkspaceWrite)
522 .approval_policy(ApprovalPolicy::OnRequest)
523 .skip_git_repo_check()
524 .ephemeral()
525 .json()
526 .args();
527
528 assert_eq!(
529 args,
530 vec![
531 "exec",
532 "--model",
533 "gpt-5",
534 "--sandbox",
535 "workspace-write",
536 "--ask-for-approval",
537 "on-request",
538 "--skip-git-repo-check",
539 "--ephemeral",
540 "--json",
541 "fix the test",
542 ]
543 );
544 }
545
546 #[test]
547 fn exec_resume_args() {
548 let args = ExecResumeCommand::new()
549 .last()
550 .model("gpt-5")
551 .json()
552 .prompt("continue")
553 .args();
554
555 assert_eq!(
556 args,
557 vec![
558 "exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
559 ]
560 );
561 }
562}