1use crate::Codex;
2use crate::command::CodexCommand;
3#[cfg(feature = "json")]
4use crate::error::Error;
5use crate::error::Result;
6use crate::exec::{self, CommandOutput};
7use crate::types::{Color, SandboxMode};
8#[cfg(feature = "json")]
9use crate::types::{JsonLineEvent, QueryResult};
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 strict_config: bool,
46 dangerously_bypass_hook_trust: bool,
47 ignore_user_config: bool,
48 ignore_rules: bool,
49 profile: Option<String>,
50 full_auto: bool,
51 dangerously_bypass_approvals_and_sandbox: bool,
52 cd: Option<String>,
53 skip_git_repo_check: bool,
54 add_dirs: Vec<String>,
55 ephemeral: bool,
56 output_schema: Option<String>,
57 color: Option<Color>,
58 json: bool,
59 output_last_message: Option<String>,
60 retry_policy: Option<crate::retry::RetryPolicy>,
61}
62
63impl ExecCommand {
64 #[must_use]
66 pub fn new(prompt: impl Into<String>) -> Self {
67 Self {
68 prompt: Some(prompt.into()),
69 config_overrides: Vec::new(),
70 enabled_features: Vec::new(),
71 disabled_features: Vec::new(),
72 images: Vec::new(),
73 model: None,
74 oss: false,
75 local_provider: None,
76 sandbox: None,
77 strict_config: false,
78 dangerously_bypass_hook_trust: false,
79 ignore_user_config: false,
80 ignore_rules: false,
81 profile: None,
82 full_auto: false,
83 dangerously_bypass_approvals_and_sandbox: false,
84 cd: None,
85 skip_git_repo_check: false,
86 add_dirs: Vec::new(),
87 ephemeral: false,
88 output_schema: None,
89 color: None,
90 json: false,
91 output_last_message: None,
92 retry_policy: None,
93 }
94 }
95
96 #[must_use]
98 pub fn from_stdin() -> Self {
99 Self::new("-")
100 }
101
102 #[must_use]
106 pub fn config(mut self, key_value: impl Into<String>) -> Self {
107 self.config_overrides.push(key_value.into());
108 self
109 }
110
111 #[must_use]
115 pub fn enable(mut self, feature: impl Into<String>) -> Self {
116 self.enabled_features.push(feature.into());
117 self
118 }
119
120 #[must_use]
124 pub fn disable(mut self, feature: impl Into<String>) -> Self {
125 self.disabled_features.push(feature.into());
126 self
127 }
128
129 #[must_use]
133 pub fn image(mut self, path: impl Into<String>) -> Self {
134 self.images.push(path.into());
135 self
136 }
137
138 #[must_use]
142 pub fn model(mut self, model: impl Into<String>) -> Self {
143 let model = model.into();
144 assert!(!model.is_empty(), "model name must not be empty");
145 self.model = Some(model);
146 self
147 }
148
149 #[must_use]
151 pub fn oss(mut self) -> Self {
152 self.oss = true;
153 self
154 }
155
156 #[must_use]
158 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
159 self.local_provider = Some(provider.into());
160 self
161 }
162
163 #[must_use]
165 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
166 self.sandbox = Some(sandbox);
167 self
168 }
169
170 #[must_use]
172 pub fn strict_config(mut self) -> Self {
173 self.strict_config = true;
174 self
175 }
176
177 #[must_use]
181 pub fn dangerously_bypass_hook_trust(mut self) -> Self {
182 self.dangerously_bypass_hook_trust = true;
183 self
184 }
185
186 #[must_use]
188 pub fn ignore_user_config(mut self) -> Self {
189 self.ignore_user_config = true;
190 self
191 }
192
193 #[must_use]
195 pub fn ignore_rules(mut self) -> Self {
196 self.ignore_rules = true;
197 self
198 }
199
200 #[must_use]
202 pub fn profile(mut self, profile: impl Into<String>) -> Self {
203 self.profile = Some(profile.into());
204 self
205 }
206
207 #[must_use]
213 pub fn full_auto(mut self) -> Self {
214 self.full_auto = true;
215 self
216 }
217
218 #[must_use]
222 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
223 self.dangerously_bypass_approvals_and_sandbox = true;
224 self
225 }
226
227 #[must_use]
229 pub fn cd(mut self, dir: impl Into<String>) -> Self {
230 self.cd = Some(dir.into());
231 self
232 }
233
234 #[must_use]
236 pub fn skip_git_repo_check(mut self) -> Self {
237 self.skip_git_repo_check = true;
238 self
239 }
240
241 #[must_use]
245 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
246 self.add_dirs.push(dir.into());
247 self
248 }
249
250 #[must_use]
252 pub fn ephemeral(mut self) -> Self {
253 self.ephemeral = true;
254 self
255 }
256
257 #[must_use]
259 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
260 self.output_schema = Some(path.into());
261 self
262 }
263
264 #[must_use]
266 pub fn color(mut self, color: Color) -> Self {
267 self.color = Some(color);
268 self
269 }
270
271 #[must_use]
277 pub fn json(mut self) -> Self {
278 self.json = true;
279 self
280 }
281
282 #[must_use]
284 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
285 self.output_last_message = Some(path.into());
286 self
287 }
288
289 #[must_use]
293 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
294 self.retry_policy = Some(policy);
295 self
296 }
297
298 #[cfg(feature = "json")]
321 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
322 where
323 F: FnMut(JsonLineEvent),
324 {
325 crate::streaming::stream_exec(codex, self, handler).await
326 }
327
328 #[cfg(feature = "json")]
333 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
334 let mut args = self.args();
335 if !self.json {
336 args.push("--json".into());
337 }
338
339 let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
340 parse_json_lines(&output.stdout)
341 }
342
343 #[cfg(feature = "json")]
349 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
350 let events = self.execute_json_lines(codex).await?;
351 Ok(QueryResult::from_events(events))
352 }
353}
354
355impl CodexCommand for ExecCommand {
356 type Output = CommandOutput;
357
358 fn args(&self) -> Vec<String> {
359 let mut args = vec!["exec".to_string()];
360
361 push_repeat(&mut args, "-c", &self.config_overrides);
362 push_repeat(&mut args, "--enable", &self.enabled_features);
363 push_repeat(&mut args, "--disable", &self.disabled_features);
364 push_repeat(&mut args, "--image", &self.images);
365
366 if let Some(model) = &self.model {
367 args.push("--model".into());
368 args.push(model.clone());
369 }
370 if self.oss {
371 args.push("--oss".into());
372 }
373 if let Some(local_provider) = &self.local_provider {
374 args.push("--local-provider".into());
375 args.push(local_provider.clone());
376 }
377 if let Some(sandbox) = self.sandbox {
378 args.push("--sandbox".into());
379 args.push(sandbox.as_arg().into());
380 }
381 if self.strict_config {
382 args.push("--strict-config".into());
383 }
384 if let Some(profile) = &self.profile {
385 args.push("--profile".into());
386 args.push(profile.clone());
387 }
388 if self.full_auto {
389 args.push("--full-auto".into());
390 }
391 if self.dangerously_bypass_approvals_and_sandbox {
392 args.push("--dangerously-bypass-approvals-and-sandbox".into());
393 }
394 if self.dangerously_bypass_hook_trust {
395 args.push("--dangerously-bypass-hook-trust".into());
396 }
397 if let Some(cd) = &self.cd {
398 args.push("--cd".into());
399 args.push(cd.clone());
400 }
401 if self.skip_git_repo_check {
402 args.push("--skip-git-repo-check".into());
403 }
404 push_repeat(&mut args, "--add-dir", &self.add_dirs);
405 if self.ephemeral {
406 args.push("--ephemeral".into());
407 }
408 if self.ignore_user_config {
409 args.push("--ignore-user-config".into());
410 }
411 if self.ignore_rules {
412 args.push("--ignore-rules".into());
413 }
414 if let Some(output_schema) = &self.output_schema {
415 args.push("--output-schema".into());
416 args.push(output_schema.clone());
417 }
418 if let Some(color) = self.color {
419 args.push("--color".into());
420 args.push(color.as_arg().into());
421 }
422 if self.json {
423 args.push("--json".into());
424 }
425 if let Some(path) = &self.output_last_message {
426 args.push("--output-last-message".into());
427 args.push(path.clone());
428 }
429 if let Some(prompt) = &self.prompt {
430 args.push(prompt.clone());
431 }
432
433 args
434 }
435
436 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
437 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
438 }
439}
440
441#[derive(Debug, Clone)]
446pub struct ExecResumeCommand {
447 session_id: Option<String>,
448 prompt: Option<String>,
449 last: bool,
450 all: bool,
451 config_overrides: Vec<String>,
452 enabled_features: Vec<String>,
453 disabled_features: Vec<String>,
454 images: Vec<String>,
455 model: Option<String>,
456 strict_config: bool,
457 dangerously_bypass_hook_trust: bool,
458 full_auto: bool,
459 dangerously_bypass_approvals_and_sandbox: bool,
460 skip_git_repo_check: bool,
461 ephemeral: bool,
462 json: bool,
463 output_last_message: Option<String>,
464 retry_policy: Option<crate::retry::RetryPolicy>,
465}
466
467impl ExecResumeCommand {
468 #[must_use]
470 pub fn new() -> Self {
471 Self {
472 session_id: None,
473 prompt: None,
474 last: false,
475 all: false,
476 config_overrides: Vec::new(),
477 enabled_features: Vec::new(),
478 disabled_features: Vec::new(),
479 images: Vec::new(),
480 model: None,
481 strict_config: false,
482 dangerously_bypass_hook_trust: false,
483 full_auto: false,
484 dangerously_bypass_approvals_and_sandbox: false,
485 skip_git_repo_check: false,
486 ephemeral: false,
487 json: false,
488 output_last_message: None,
489 retry_policy: None,
490 }
491 }
492
493 #[must_use]
495 pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
496 self.session_id = Some(session_id.into());
497 self
498 }
499
500 #[must_use]
502 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
503 self.prompt = Some(prompt.into());
504 self
505 }
506
507 #[must_use]
509 pub fn last(mut self) -> Self {
510 self.last = true;
511 self
512 }
513
514 #[must_use]
516 pub fn all(mut self) -> Self {
517 self.all = true;
518 self
519 }
520
521 #[must_use]
525 pub fn model(mut self, model: impl Into<String>) -> Self {
526 let model = model.into();
527 assert!(!model.is_empty(), "model name must not be empty");
528 self.model = Some(model);
529 self
530 }
531
532 #[must_use]
536 pub fn image(mut self, path: impl Into<String>) -> Self {
537 self.images.push(path.into());
538 self
539 }
540
541 #[must_use]
543 pub fn json(mut self) -> Self {
544 self.json = true;
545 self
546 }
547
548 #[must_use]
550 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
551 self.output_last_message = Some(path.into());
552 self
553 }
554
555 #[must_use]
559 pub fn config(mut self, key_value: impl Into<String>) -> Self {
560 self.config_overrides.push(key_value.into());
561 self
562 }
563
564 #[must_use]
568 pub fn enable(mut self, feature: impl Into<String>) -> Self {
569 self.enabled_features.push(feature.into());
570 self
571 }
572
573 #[must_use]
577 pub fn disable(mut self, feature: impl Into<String>) -> Self {
578 self.disabled_features.push(feature.into());
579 self
580 }
581
582 #[must_use]
584 pub fn strict_config(mut self) -> Self {
585 self.strict_config = true;
586 self
587 }
588
589 #[must_use]
593 pub fn dangerously_bypass_hook_trust(mut self) -> Self {
594 self.dangerously_bypass_hook_trust = true;
595 self
596 }
597
598 #[must_use]
600 pub fn full_auto(mut self) -> Self {
601 self.full_auto = true;
602 self
603 }
604
605 #[must_use]
609 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
610 self.dangerously_bypass_approvals_and_sandbox = true;
611 self
612 }
613
614 #[must_use]
616 pub fn skip_git_repo_check(mut self) -> Self {
617 self.skip_git_repo_check = true;
618 self
619 }
620
621 #[must_use]
623 pub fn ephemeral(mut self) -> Self {
624 self.ephemeral = true;
625 self
626 }
627
628 #[must_use]
632 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
633 self.retry_policy = Some(policy);
634 self
635 }
636
637 #[cfg(feature = "json")]
642 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
643 let mut args = self.args();
644 if !self.json {
645 args.push("--json".into());
646 }
647
648 let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
649 parse_json_lines(&output.stdout)
650 }
651
652 #[cfg(feature = "json")]
657 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
658 let events = self.execute_json_lines(codex).await?;
659 Ok(QueryResult::from_events(events))
660 }
661
662 #[cfg(feature = "json")]
668 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
669 where
670 F: FnMut(JsonLineEvent),
671 {
672 crate::streaming::stream_exec_resume(codex, self, handler).await
673 }
674}
675
676impl Default for ExecResumeCommand {
677 fn default() -> Self {
678 Self::new()
679 }
680}
681
682impl CodexCommand for ExecResumeCommand {
683 type Output = CommandOutput;
684
685 fn args(&self) -> Vec<String> {
686 let mut args = vec!["exec".into(), "resume".into()];
687 push_repeat(&mut args, "-c", &self.config_overrides);
688 push_repeat(&mut args, "--enable", &self.enabled_features);
689 push_repeat(&mut args, "--disable", &self.disabled_features);
690 if self.last {
691 args.push("--last".into());
692 }
693 if self.all {
694 args.push("--all".into());
695 }
696 push_repeat(&mut args, "--image", &self.images);
697 if let Some(model) = &self.model {
698 args.push("--model".into());
699 args.push(model.clone());
700 }
701 if self.strict_config {
702 args.push("--strict-config".into());
703 }
704 if self.full_auto {
705 args.push("--full-auto".into());
706 }
707 if self.dangerously_bypass_approvals_and_sandbox {
708 args.push("--dangerously-bypass-approvals-and-sandbox".into());
709 }
710 if self.dangerously_bypass_hook_trust {
711 args.push("--dangerously-bypass-hook-trust".into());
712 }
713 if self.skip_git_repo_check {
714 args.push("--skip-git-repo-check".into());
715 }
716 if self.ephemeral {
717 args.push("--ephemeral".into());
718 }
719 if self.json {
720 args.push("--json".into());
721 }
722 if let Some(path) = &self.output_last_message {
723 args.push("--output-last-message".into());
724 args.push(path.clone());
725 }
726 if let Some(session_id) = &self.session_id {
727 args.push(session_id.clone());
728 }
729 if let Some(prompt) = &self.prompt {
730 args.push(prompt.clone());
731 }
732 args
733 }
734
735 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
736 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
737 }
738}
739
740fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
741 for value in values {
742 args.push(flag.into());
743 args.push(value.clone());
744 }
745}
746
747#[cfg(feature = "json")]
748fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
749 stdout
750 .lines()
751 .filter(|line| line.trim_start().starts_with('{'))
752 .map(|line| {
753 serde_json::from_str(line).map_err(|source| Error::Json {
754 message: format!("failed to parse JSONL event: {line}"),
755 source,
756 })
757 })
758 .collect()
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn exec_args() {
767 let args = ExecCommand::new("fix the test")
768 .model("gpt-5")
769 .sandbox(SandboxMode::WorkspaceWrite)
770 .strict_config()
771 .skip_git_repo_check()
772 .ephemeral()
773 .ignore_user_config()
774 .ignore_rules()
775 .json()
776 .args();
777
778 assert_eq!(
779 args,
780 vec![
781 "exec",
782 "--model",
783 "gpt-5",
784 "--sandbox",
785 "workspace-write",
786 "--strict-config",
787 "--skip-git-repo-check",
788 "--ephemeral",
789 "--ignore-user-config",
790 "--ignore-rules",
791 "--json",
792 "fix the test",
793 ]
794 );
795 }
796
797 #[test]
798 fn exec_args_hook_trust() {
799 let args = ExecCommand::new("go")
800 .dangerously_bypass_approvals_and_sandbox()
801 .dangerously_bypass_hook_trust()
802 .args();
803
804 assert_eq!(
805 args,
806 vec![
807 "exec",
808 "--dangerously-bypass-approvals-and-sandbox",
809 "--dangerously-bypass-hook-trust",
810 "go",
811 ]
812 );
813 }
814
815 #[test]
816 #[should_panic(expected = "model name must not be empty")]
817 fn exec_model_empty_panics() {
818 let _ = ExecCommand::new("prompt").model("");
819 }
820
821 #[test]
822 #[should_panic(expected = "model name must not be empty")]
823 fn exec_resume_model_empty_panics() {
824 let _ = ExecResumeCommand::new().model("");
825 }
826
827 #[test]
828 fn exec_resume_args() {
829 let args = ExecResumeCommand::new()
830 .last()
831 .model("gpt-5")
832 .json()
833 .prompt("continue")
834 .args();
835
836 assert_eq!(
837 args,
838 vec![
839 "exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
840 ]
841 );
842 }
843
844 #[test]
845 fn exec_resume_new_flags() {
846 let args = ExecResumeCommand::new()
847 .last()
848 .strict_config()
849 .dangerously_bypass_hook_trust()
850 .args();
851
852 assert_eq!(
853 args,
854 vec![
855 "exec",
856 "resume",
857 "--last",
858 "--strict-config",
859 "--dangerously-bypass-hook-trust",
860 ]
861 );
862 }
863}