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]
101 pub fn config(mut self, key_value: impl Into<String>) -> Self {
102 self.config_overrides.push(key_value.into());
103 self
104 }
105
106 #[must_use]
107 pub fn enable(mut self, feature: impl Into<String>) -> Self {
108 self.enabled_features.push(feature.into());
109 self
110 }
111
112 #[must_use]
113 pub fn disable(mut self, feature: impl Into<String>) -> Self {
114 self.disabled_features.push(feature.into());
115 self
116 }
117
118 #[must_use]
119 pub fn image(mut self, path: impl Into<String>) -> Self {
120 self.images.push(path.into());
121 self
122 }
123
124 #[must_use]
125 pub fn model(mut self, model: impl Into<String>) -> Self {
126 let model = model.into();
127 assert!(!model.is_empty(), "model name must not be empty");
128 self.model = Some(model);
129 self
130 }
131
132 #[must_use]
133 pub fn oss(mut self) -> Self {
134 self.oss = true;
135 self
136 }
137
138 #[must_use]
139 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
140 self.local_provider = Some(provider.into());
141 self
142 }
143
144 #[must_use]
145 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
146 self.sandbox = Some(sandbox);
147 self
148 }
149
150 #[must_use]
151 pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
152 self.approval_policy = Some(policy);
153 self
154 }
155
156 #[must_use]
157 pub fn profile(mut self, profile: impl Into<String>) -> Self {
158 self.profile = Some(profile.into());
159 self
160 }
161
162 #[must_use]
163 pub fn full_auto(mut self) -> Self {
164 self.full_auto = true;
165 self
166 }
167
168 #[must_use]
169 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
170 self.dangerously_bypass_approvals_and_sandbox = true;
171 self
172 }
173
174 #[must_use]
175 pub fn cd(mut self, dir: impl Into<String>) -> Self {
176 self.cd = Some(dir.into());
177 self
178 }
179
180 #[must_use]
181 pub fn skip_git_repo_check(mut self) -> Self {
182 self.skip_git_repo_check = true;
183 self
184 }
185
186 #[must_use]
187 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
188 self.add_dirs.push(dir.into());
189 self
190 }
191
192 #[must_use]
194 pub fn search(mut self) -> Self {
195 self.search = true;
196 self
197 }
198
199 #[must_use]
200 pub fn ephemeral(mut self) -> Self {
201 self.ephemeral = true;
202 self
203 }
204
205 #[must_use]
206 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
207 self.output_schema = Some(path.into());
208 self
209 }
210
211 #[must_use]
212 pub fn color(mut self, color: Color) -> Self {
213 self.color = Some(color);
214 self
215 }
216
217 #[must_use]
218 pub fn progress_cursor(mut self) -> Self {
219 self.progress_cursor = true;
220 self
221 }
222
223 #[must_use]
224 pub fn json(mut self) -> Self {
225 self.json = true;
226 self
227 }
228
229 #[must_use]
230 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
231 self.output_last_message = Some(path.into());
232 self
233 }
234
235 #[must_use]
236 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
237 self.retry_policy = Some(policy);
238 self
239 }
240
241 #[cfg(feature = "json")]
242 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
243 let mut args = self.args();
244 if !self.json {
245 args.push("--json".into());
246 }
247
248 let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
249 parse_json_lines(&output.stdout)
250 }
251}
252
253impl CodexCommand for ExecCommand {
254 type Output = CommandOutput;
255
256 fn args(&self) -> Vec<String> {
257 let mut args = vec!["exec".to_string()];
258
259 push_repeat(&mut args, "-c", &self.config_overrides);
260 push_repeat(&mut args, "--enable", &self.enabled_features);
261 push_repeat(&mut args, "--disable", &self.disabled_features);
262 push_repeat(&mut args, "--image", &self.images);
263
264 if let Some(model) = &self.model {
265 args.push("--model".into());
266 args.push(model.clone());
267 }
268 if self.oss {
269 args.push("--oss".into());
270 }
271 if let Some(local_provider) = &self.local_provider {
272 args.push("--local-provider".into());
273 args.push(local_provider.clone());
274 }
275 if let Some(sandbox) = self.sandbox {
276 args.push("--sandbox".into());
277 args.push(sandbox.as_arg().into());
278 }
279 if let Some(policy) = self.approval_policy {
280 args.push("--ask-for-approval".into());
281 args.push(policy.as_arg().into());
282 }
283 if let Some(profile) = &self.profile {
284 args.push("--profile".into());
285 args.push(profile.clone());
286 }
287 if self.full_auto {
288 args.push("--full-auto".into());
289 }
290 if self.dangerously_bypass_approvals_and_sandbox {
291 args.push("--dangerously-bypass-approvals-and-sandbox".into());
292 }
293 if let Some(cd) = &self.cd {
294 args.push("--cd".into());
295 args.push(cd.clone());
296 }
297 if self.skip_git_repo_check {
298 args.push("--skip-git-repo-check".into());
299 }
300 push_repeat(&mut args, "--add-dir", &self.add_dirs);
301 if self.search {
302 args.push("--search".into());
303 }
304 if self.ephemeral {
305 args.push("--ephemeral".into());
306 }
307 if let Some(output_schema) = &self.output_schema {
308 args.push("--output-schema".into());
309 args.push(output_schema.clone());
310 }
311 if let Some(color) = self.color {
312 args.push("--color".into());
313 args.push(color.as_arg().into());
314 }
315 if self.progress_cursor {
316 args.push("--progress-cursor".into());
317 }
318 if self.json {
319 args.push("--json".into());
320 }
321 if let Some(path) = &self.output_last_message {
322 args.push("--output-last-message".into());
323 args.push(path.clone());
324 }
325 if let Some(prompt) = &self.prompt {
326 args.push(prompt.clone());
327 }
328
329 args
330 }
331
332 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
333 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
334 }
335}
336
337#[derive(Debug, Clone)]
342pub struct ExecResumeCommand {
343 session_id: Option<String>,
344 prompt: Option<String>,
345 last: bool,
346 all: bool,
347 config_overrides: Vec<String>,
348 enabled_features: Vec<String>,
349 disabled_features: Vec<String>,
350 images: Vec<String>,
351 model: Option<String>,
352 full_auto: bool,
353 dangerously_bypass_approvals_and_sandbox: bool,
354 skip_git_repo_check: bool,
355 ephemeral: bool,
356 json: bool,
357 output_last_message: Option<String>,
358 retry_policy: Option<crate::retry::RetryPolicy>,
359}
360
361impl ExecResumeCommand {
362 #[must_use]
363 pub fn new() -> Self {
364 Self {
365 session_id: None,
366 prompt: None,
367 last: false,
368 all: false,
369 config_overrides: Vec::new(),
370 enabled_features: Vec::new(),
371 disabled_features: Vec::new(),
372 images: Vec::new(),
373 model: None,
374 full_auto: false,
375 dangerously_bypass_approvals_and_sandbox: false,
376 skip_git_repo_check: false,
377 ephemeral: false,
378 json: false,
379 output_last_message: None,
380 retry_policy: None,
381 }
382 }
383
384 #[must_use]
385 pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
386 self.session_id = Some(session_id.into());
387 self
388 }
389
390 #[must_use]
391 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
392 self.prompt = Some(prompt.into());
393 self
394 }
395
396 #[must_use]
397 pub fn last(mut self) -> Self {
398 self.last = true;
399 self
400 }
401
402 #[must_use]
403 pub fn all(mut self) -> Self {
404 self.all = true;
405 self
406 }
407
408 #[must_use]
409 pub fn model(mut self, model: impl Into<String>) -> Self {
410 let model = model.into();
411 assert!(!model.is_empty(), "model name must not be empty");
412 self.model = Some(model);
413 self
414 }
415
416 #[must_use]
417 pub fn image(mut self, path: impl Into<String>) -> Self {
418 self.images.push(path.into());
419 self
420 }
421
422 #[must_use]
423 pub fn json(mut self) -> Self {
424 self.json = true;
425 self
426 }
427
428 #[must_use]
429 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
430 self.output_last_message = Some(path.into());
431 self
432 }
433
434 #[must_use]
435 pub fn config(mut self, key_value: impl Into<String>) -> Self {
436 self.config_overrides.push(key_value.into());
437 self
438 }
439
440 #[must_use]
441 pub fn enable(mut self, feature: impl Into<String>) -> Self {
442 self.enabled_features.push(feature.into());
443 self
444 }
445
446 #[must_use]
447 pub fn disable(mut self, feature: impl Into<String>) -> Self {
448 self.disabled_features.push(feature.into());
449 self
450 }
451
452 #[must_use]
453 pub fn full_auto(mut self) -> Self {
454 self.full_auto = true;
455 self
456 }
457
458 #[must_use]
459 pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
460 self.dangerously_bypass_approvals_and_sandbox = true;
461 self
462 }
463
464 #[must_use]
465 pub fn skip_git_repo_check(mut self) -> Self {
466 self.skip_git_repo_check = true;
467 self
468 }
469
470 #[must_use]
471 pub fn ephemeral(mut self) -> Self {
472 self.ephemeral = true;
473 self
474 }
475
476 #[must_use]
477 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
478 self.retry_policy = Some(policy);
479 self
480 }
481}
482
483impl Default for ExecResumeCommand {
484 fn default() -> Self {
485 Self::new()
486 }
487}
488
489impl CodexCommand for ExecResumeCommand {
490 type Output = CommandOutput;
491
492 fn args(&self) -> Vec<String> {
493 let mut args = vec!["exec".into(), "resume".into()];
494 push_repeat(&mut args, "-c", &self.config_overrides);
495 push_repeat(&mut args, "--enable", &self.enabled_features);
496 push_repeat(&mut args, "--disable", &self.disabled_features);
497 if self.last {
498 args.push("--last".into());
499 }
500 if self.all {
501 args.push("--all".into());
502 }
503 push_repeat(&mut args, "--image", &self.images);
504 if let Some(model) = &self.model {
505 args.push("--model".into());
506 args.push(model.clone());
507 }
508 if self.full_auto {
509 args.push("--full-auto".into());
510 }
511 if self.dangerously_bypass_approvals_and_sandbox {
512 args.push("--dangerously-bypass-approvals-and-sandbox".into());
513 }
514 if self.skip_git_repo_check {
515 args.push("--skip-git-repo-check".into());
516 }
517 if self.ephemeral {
518 args.push("--ephemeral".into());
519 }
520 if self.json {
521 args.push("--json".into());
522 }
523 if let Some(path) = &self.output_last_message {
524 args.push("--output-last-message".into());
525 args.push(path.clone());
526 }
527 if let Some(session_id) = &self.session_id {
528 args.push(session_id.clone());
529 }
530 if let Some(prompt) = &self.prompt {
531 args.push(prompt.clone());
532 }
533 args
534 }
535
536 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
537 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
538 }
539}
540
541fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
542 for value in values {
543 args.push(flag.into());
544 args.push(value.clone());
545 }
546}
547
548#[cfg(feature = "json")]
549fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
550 stdout
551 .lines()
552 .filter(|line| line.trim_start().starts_with('{'))
553 .map(|line| {
554 serde_json::from_str(line).map_err(|source| Error::Json {
555 message: format!("failed to parse JSONL event: {line}"),
556 source,
557 })
558 })
559 .collect()
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn exec_args() {
568 let args = ExecCommand::new("fix the test")
569 .model("gpt-5")
570 .sandbox(SandboxMode::WorkspaceWrite)
571 .approval_policy(ApprovalPolicy::OnRequest)
572 .skip_git_repo_check()
573 .ephemeral()
574 .json()
575 .args();
576
577 assert_eq!(
578 args,
579 vec![
580 "exec",
581 "--model",
582 "gpt-5",
583 "--sandbox",
584 "workspace-write",
585 "--ask-for-approval",
586 "on-request",
587 "--skip-git-repo-check",
588 "--ephemeral",
589 "--json",
590 "fix the test",
591 ]
592 );
593 }
594
595 #[test]
596 #[should_panic(expected = "model name must not be empty")]
597 fn exec_model_empty_panics() {
598 let _ = ExecCommand::new("prompt").model("");
599 }
600
601 #[test]
602 #[should_panic(expected = "model name must not be empty")]
603 fn exec_resume_model_empty_panics() {
604 let _ = ExecResumeCommand::new().model("");
605 }
606
607 #[test]
608 fn exec_resume_args() {
609 let args = ExecResumeCommand::new()
610 .last()
611 .model("gpt-5")
612 .json()
613 .prompt("continue")
614 .args();
615
616 assert_eq!(
617 args,
618 vec![
619 "exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
620 ]
621 );
622 }
623}