Skip to main content

codex_wrapper/command/
exec.rs

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}