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;
9use crate::types::{ApprovalPolicy, Color, SandboxMode};
10
11#[derive(Debug, Clone)]
35pub struct ExecCommand {
36 prompt: Option<String>,
37 config_overrides: Vec<String>,
38 enabled_features: Vec<String>,
39 disabled_features: Vec<String>,
40 images: Vec<String>,
41 model: Option<String>,
42 oss: bool,
43 local_provider: Option<String>,
44 sandbox: Option<SandboxMode>,
45 approval_policy: Option<ApprovalPolicy>,
46 profile: Option<String>,
47 full_auto: bool,
48 dangerously_bypass_approvals_and_sandbox: bool,
49 cd: Option<String>,
50 skip_git_repo_check: bool,
51 add_dirs: Vec<String>,
52 search: bool,
53 ephemeral: bool,
54 output_schema: Option<String>,
55 color: Option<Color>,
56 progress_cursor: bool,
57 json: bool,
58 output_last_message: Option<String>,
59 retry_policy: Option<crate::retry::RetryPolicy>,
60}
61
62impl ExecCommand {
63 #[must_use]
65 pub fn new(prompt: impl Into<String>) -> Self {
66 Self {
67 prompt: Some(prompt.into()),
68 config_overrides: Vec::new(),
69 enabled_features: Vec::new(),
70 disabled_features: Vec::new(),
71 images: Vec::new(),
72 model: None,
73 oss: false,
74 local_provider: None,
75 sandbox: None,
76 approval_policy: None,
77 profile: None,
78 full_auto: false,
79 dangerously_bypass_approvals_and_sandbox: false,
80 cd: None,
81 skip_git_repo_check: false,
82 add_dirs: Vec::new(),
83 search: false,
84 ephemeral: false,
85 output_schema: None,
86 color: None,
87 progress_cursor: false,
88 json: false,
89 output_last_message: None,
90 retry_policy: None,
91 }
92 }
93
94 #[must_use]
96 pub fn from_stdin() -> Self {
97 Self::new("-")
98 }
99
100 #[must_use]
104 pub fn config(mut self, key_value: impl Into<String>) -> Self {
105 self.config_overrides.push(key_value.into());
106 self
107 }
108
109 #[must_use]
113 pub fn enable(mut self, feature: impl Into<String>) -> Self {
114 self.enabled_features.push(feature.into());
115 self
116 }
117
118 #[must_use]
122 pub fn disable(mut self, feature: impl Into<String>) -> Self {
123 self.disabled_features.push(feature.into());
124 self
125 }
126
127 #[must_use]
131 pub fn image(mut self, path: impl Into<String>) -> Self {
132 self.images.push(path.into());
133 self
134 }
135
136 #[must_use]
140 pub fn model(mut self, model: impl Into<String>) -> Self {
141 let model = model.into();
142 assert!(!model.is_empty(), "model name must not be empty");
143 self.model = Some(model);
144 self
145 }
146
147 #[must_use]
149 pub fn oss(mut self) -> Self {
150 self.oss = true;
151 self
152 }
153
154 #[must_use]
156 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
157 self.local_provider = Some(provider.into());
158 self
159 }
160
161 #[must_use]
163 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
164 self.sandbox = Some(sandbox);
165 self
166 }
167
168 #[must_use]
170 pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
171 self.approval_policy = Some(policy);
172 self
173 }
174
175 #[must_use]
177 pub fn profile(mut self, profile: impl Into<String>) -> Self {
178 self.profile = Some(profile.into());
179 self
180 }
181
182 #[must_use]
184 pub fn full_auto(mut self) -> Self {
185 self.full_auto = true;
186 self
187 }
188
189 #[must_use]
193 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
194 self.dangerously_bypass_approvals_and_sandbox = true;
195 self
196 }
197
198 #[must_use]
200 pub fn cd(mut self, dir: impl Into<String>) -> Self {
201 self.cd = Some(dir.into());
202 self
203 }
204
205 #[must_use]
207 pub fn skip_git_repo_check(mut self) -> Self {
208 self.skip_git_repo_check = true;
209 self
210 }
211
212 #[must_use]
216 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
217 self.add_dirs.push(dir.into());
218 self
219 }
220
221 #[must_use]
223 pub fn search(mut self) -> Self {
224 self.search = true;
225 self
226 }
227
228 #[must_use]
230 pub fn ephemeral(mut self) -> Self {
231 self.ephemeral = true;
232 self
233 }
234
235 #[must_use]
237 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
238 self.output_schema = Some(path.into());
239 self
240 }
241
242 #[must_use]
244 pub fn color(mut self, color: Color) -> Self {
245 self.color = Some(color);
246 self
247 }
248
249 #[must_use]
251 pub fn progress_cursor(mut self) -> Self {
252 self.progress_cursor = true;
253 self
254 }
255
256 #[must_use]
262 pub fn json(mut self) -> Self {
263 self.json = true;
264 self
265 }
266
267 #[must_use]
269 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
270 self.output_last_message = Some(path.into());
271 self
272 }
273
274 #[must_use]
278 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
279 self.retry_policy = Some(policy);
280 self
281 }
282
283 #[cfg(feature = "json")]
306 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
307 where
308 F: FnMut(JsonLineEvent),
309 {
310 crate::streaming::stream_exec(codex, self, handler).await
311 }
312
313 #[cfg(feature = "json")]
318 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
319 let mut args = self.args();
320 if !self.json {
321 args.push("--json".into());
322 }
323
324 let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
325 parse_json_lines(&output.stdout)
326 }
327}
328
329impl CodexCommand for ExecCommand {
330 type Output = CommandOutput;
331
332 fn args(&self) -> Vec<String> {
333 let mut args = vec!["exec".to_string()];
334
335 push_repeat(&mut args, "-c", &self.config_overrides);
336 push_repeat(&mut args, "--enable", &self.enabled_features);
337 push_repeat(&mut args, "--disable", &self.disabled_features);
338 push_repeat(&mut args, "--image", &self.images);
339
340 if let Some(model) = &self.model {
341 args.push("--model".into());
342 args.push(model.clone());
343 }
344 if self.oss {
345 args.push("--oss".into());
346 }
347 if let Some(local_provider) = &self.local_provider {
348 args.push("--local-provider".into());
349 args.push(local_provider.clone());
350 }
351 if let Some(sandbox) = self.sandbox {
352 args.push("--sandbox".into());
353 args.push(sandbox.as_arg().into());
354 }
355 if let Some(policy) = self.approval_policy {
356 args.push("--ask-for-approval".into());
357 args.push(policy.as_arg().into());
358 }
359 if let Some(profile) = &self.profile {
360 args.push("--profile".into());
361 args.push(profile.clone());
362 }
363 if self.full_auto {
364 args.push("--full-auto".into());
365 }
366 if self.dangerously_bypass_approvals_and_sandbox {
367 args.push("--dangerously-bypass-approvals-and-sandbox".into());
368 }
369 if let Some(cd) = &self.cd {
370 args.push("--cd".into());
371 args.push(cd.clone());
372 }
373 if self.skip_git_repo_check {
374 args.push("--skip-git-repo-check".into());
375 }
376 push_repeat(&mut args, "--add-dir", &self.add_dirs);
377 if self.search {
378 args.push("--search".into());
379 }
380 if self.ephemeral {
381 args.push("--ephemeral".into());
382 }
383 if let Some(output_schema) = &self.output_schema {
384 args.push("--output-schema".into());
385 args.push(output_schema.clone());
386 }
387 if let Some(color) = self.color {
388 args.push("--color".into());
389 args.push(color.as_arg().into());
390 }
391 if self.progress_cursor {
392 args.push("--progress-cursor".into());
393 }
394 if self.json {
395 args.push("--json".into());
396 }
397 if let Some(path) = &self.output_last_message {
398 args.push("--output-last-message".into());
399 args.push(path.clone());
400 }
401 if let Some(prompt) = &self.prompt {
402 args.push(prompt.clone());
403 }
404
405 args
406 }
407
408 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
409 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
410 }
411}
412
413#[derive(Debug, Clone)]
418pub struct ExecResumeCommand {
419 session_id: Option<String>,
420 prompt: Option<String>,
421 last: bool,
422 all: bool,
423 config_overrides: Vec<String>,
424 enabled_features: Vec<String>,
425 disabled_features: Vec<String>,
426 images: Vec<String>,
427 model: Option<String>,
428 full_auto: bool,
429 dangerously_bypass_approvals_and_sandbox: bool,
430 skip_git_repo_check: bool,
431 ephemeral: bool,
432 json: bool,
433 output_last_message: Option<String>,
434 retry_policy: Option<crate::retry::RetryPolicy>,
435}
436
437impl ExecResumeCommand {
438 #[must_use]
440 pub fn new() -> Self {
441 Self {
442 session_id: None,
443 prompt: None,
444 last: false,
445 all: false,
446 config_overrides: Vec::new(),
447 enabled_features: Vec::new(),
448 disabled_features: Vec::new(),
449 images: Vec::new(),
450 model: None,
451 full_auto: false,
452 dangerously_bypass_approvals_and_sandbox: false,
453 skip_git_repo_check: false,
454 ephemeral: false,
455 json: false,
456 output_last_message: None,
457 retry_policy: None,
458 }
459 }
460
461 #[must_use]
463 pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
464 self.session_id = Some(session_id.into());
465 self
466 }
467
468 #[must_use]
470 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
471 self.prompt = Some(prompt.into());
472 self
473 }
474
475 #[must_use]
477 pub fn last(mut self) -> Self {
478 self.last = true;
479 self
480 }
481
482 #[must_use]
484 pub fn all(mut self) -> Self {
485 self.all = true;
486 self
487 }
488
489 #[must_use]
493 pub fn model(mut self, model: impl Into<String>) -> Self {
494 let model = model.into();
495 assert!(!model.is_empty(), "model name must not be empty");
496 self.model = Some(model);
497 self
498 }
499
500 #[must_use]
504 pub fn image(mut self, path: impl Into<String>) -> Self {
505 self.images.push(path.into());
506 self
507 }
508
509 #[must_use]
511 pub fn json(mut self) -> Self {
512 self.json = true;
513 self
514 }
515
516 #[must_use]
518 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
519 self.output_last_message = Some(path.into());
520 self
521 }
522
523 #[must_use]
527 pub fn config(mut self, key_value: impl Into<String>) -> Self {
528 self.config_overrides.push(key_value.into());
529 self
530 }
531
532 #[must_use]
536 pub fn enable(mut self, feature: impl Into<String>) -> Self {
537 self.enabled_features.push(feature.into());
538 self
539 }
540
541 #[must_use]
545 pub fn disable(mut self, feature: impl Into<String>) -> Self {
546 self.disabled_features.push(feature.into());
547 self
548 }
549
550 #[must_use]
552 pub fn full_auto(mut self) -> Self {
553 self.full_auto = true;
554 self
555 }
556
557 #[must_use]
561 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
562 self.dangerously_bypass_approvals_and_sandbox = true;
563 self
564 }
565
566 #[must_use]
568 pub fn skip_git_repo_check(mut self) -> Self {
569 self.skip_git_repo_check = true;
570 self
571 }
572
573 #[must_use]
575 pub fn ephemeral(mut self) -> Self {
576 self.ephemeral = true;
577 self
578 }
579
580 #[must_use]
584 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
585 self.retry_policy = Some(policy);
586 self
587 }
588
589 #[cfg(feature = "json")]
594 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
595 let mut args = self.args();
596 if !self.json {
597 args.push("--json".into());
598 }
599
600 let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
601 parse_json_lines(&output.stdout)
602 }
603
604 #[cfg(feature = "json")]
610 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
611 where
612 F: FnMut(JsonLineEvent),
613 {
614 crate::streaming::stream_exec_resume(codex, self, handler).await
615 }
616}
617
618impl Default for ExecResumeCommand {
619 fn default() -> Self {
620 Self::new()
621 }
622}
623
624impl CodexCommand for ExecResumeCommand {
625 type Output = CommandOutput;
626
627 fn args(&self) -> Vec<String> {
628 let mut args = vec!["exec".into(), "resume".into()];
629 push_repeat(&mut args, "-c", &self.config_overrides);
630 push_repeat(&mut args, "--enable", &self.enabled_features);
631 push_repeat(&mut args, "--disable", &self.disabled_features);
632 if self.last {
633 args.push("--last".into());
634 }
635 if self.all {
636 args.push("--all".into());
637 }
638 push_repeat(&mut args, "--image", &self.images);
639 if let Some(model) = &self.model {
640 args.push("--model".into());
641 args.push(model.clone());
642 }
643 if self.full_auto {
644 args.push("--full-auto".into());
645 }
646 if self.dangerously_bypass_approvals_and_sandbox {
647 args.push("--dangerously-bypass-approvals-and-sandbox".into());
648 }
649 if self.skip_git_repo_check {
650 args.push("--skip-git-repo-check".into());
651 }
652 if self.ephemeral {
653 args.push("--ephemeral".into());
654 }
655 if self.json {
656 args.push("--json".into());
657 }
658 if let Some(path) = &self.output_last_message {
659 args.push("--output-last-message".into());
660 args.push(path.clone());
661 }
662 if let Some(session_id) = &self.session_id {
663 args.push(session_id.clone());
664 }
665 if let Some(prompt) = &self.prompt {
666 args.push(prompt.clone());
667 }
668 args
669 }
670
671 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
672 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
673 }
674}
675
676fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
677 for value in values {
678 args.push(flag.into());
679 args.push(value.clone());
680 }
681}
682
683#[cfg(feature = "json")]
684fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
685 stdout
686 .lines()
687 .filter(|line| line.trim_start().starts_with('{'))
688 .map(|line| {
689 serde_json::from_str(line).map_err(|source| Error::Json {
690 message: format!("failed to parse JSONL event: {line}"),
691 source,
692 })
693 })
694 .collect()
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn exec_args() {
703 let args = ExecCommand::new("fix the test")
704 .model("gpt-5")
705 .sandbox(SandboxMode::WorkspaceWrite)
706 .approval_policy(ApprovalPolicy::OnRequest)
707 .skip_git_repo_check()
708 .ephemeral()
709 .json()
710 .args();
711
712 assert_eq!(
713 args,
714 vec![
715 "exec",
716 "--model",
717 "gpt-5",
718 "--sandbox",
719 "workspace-write",
720 "--ask-for-approval",
721 "on-request",
722 "--skip-git-repo-check",
723 "--ephemeral",
724 "--json",
725 "fix the test",
726 ]
727 );
728 }
729
730 #[test]
731 #[should_panic(expected = "model name must not be empty")]
732 fn exec_model_empty_panics() {
733 let _ = ExecCommand::new("prompt").model("");
734 }
735
736 #[test]
737 #[should_panic(expected = "model name must not be empty")]
738 fn exec_resume_model_empty_panics() {
739 let _ = ExecResumeCommand::new().model("");
740 }
741
742 #[test]
743 fn exec_resume_args() {
744 let args = ExecResumeCommand::new()
745 .last()
746 .model("gpt-5")
747 .json()
748 .prompt("continue")
749 .args();
750
751 assert_eq!(
752 args,
753 vec![
754 "exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
755 ]
756 );
757 }
758}