Skip to main content

codex_wrapper/command/
exec.rs

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/// Run Codex non-interactively (`codex exec <prompt>`).
12///
13/// This is the primary command for programmatic use. It supports the full
14/// range of exec flags: model selection, sandbox policy, images, config
15/// overrides, feature flags, JSON output, and more.
16///
17/// # Example
18///
19/// ```no_run
20/// use codex_wrapper::{Codex, CodexCommand, ExecCommand, SandboxMode};
21///
22/// # async fn example() -> codex_wrapper::Result<()> {
23/// let codex = Codex::builder().build()?;
24/// let output = ExecCommand::new("fix the failing test")
25///     .model("o3")
26///     .sandbox(SandboxMode::WorkspaceWrite)
27///     .ephemeral()
28///     .execute(&codex)
29///     .await?;
30/// println!("{}", output.stdout);
31/// # Ok(())
32/// # }
33/// ```
34#[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    /// Create a new exec command with the given prompt.
65    #[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    /// Read the prompt from stdin (`-`).
97    #[must_use]
98    pub fn from_stdin() -> Self {
99        Self::new("-")
100    }
101
102    /// Override a config key (`-c key=value`).
103    ///
104    /// May be called multiple times to set several keys.
105    #[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    /// Enable an optional feature flag (`--enable <feature>`).
112    ///
113    /// May be called multiple times.
114    #[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    /// Disable an optional feature flag (`--disable <feature>`).
121    ///
122    /// May be called multiple times.
123    #[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    /// Attach an image to the prompt (`--image <path>`).
130    ///
131    /// May be called multiple times to attach several images.
132    #[must_use]
133    pub fn image(mut self, path: impl Into<String>) -> Self {
134        self.images.push(path.into());
135        self
136    }
137
138    /// Set the model to use (`--model <model>`).
139    ///
140    /// Panics if `model` is an empty string.
141    #[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    /// Use the OSS model tier (`--oss`).
150    #[must_use]
151    pub fn oss(mut self) -> Self {
152        self.oss = true;
153        self
154    }
155
156    /// Use a local model provider (`--local-provider <provider>`).
157    #[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    /// Set the sandbox policy (`--sandbox <mode>`).
164    #[must_use]
165    pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
166        self.sandbox = Some(sandbox);
167        self
168    }
169
170    /// Error on unrecognized config keys (`--strict-config`).
171    #[must_use]
172    pub fn strict_config(mut self) -> Self {
173        self.strict_config = true;
174        self
175    }
176
177    /// Bypass the hook trust prompt (`--dangerously-bypass-hook-trust`).
178    ///
179    /// Allows configured hooks to run without confirmation. Use with caution.
180    #[must_use]
181    pub fn dangerously_bypass_hook_trust(mut self) -> Self {
182        self.dangerously_bypass_hook_trust = true;
183        self
184    }
185
186    /// Ignore the user-level config file (`--ignore-user-config`).
187    #[must_use]
188    pub fn ignore_user_config(mut self) -> Self {
189        self.ignore_user_config = true;
190        self
191    }
192
193    /// Ignore project rules files (`--ignore-rules`).
194    #[must_use]
195    pub fn ignore_rules(mut self) -> Self {
196        self.ignore_rules = true;
197        self
198    }
199
200    /// Select a named configuration profile (`--profile <name>`).
201    #[must_use]
202    pub fn profile(mut self, profile: impl Into<String>) -> Self {
203        self.profile = Some(profile.into());
204        self
205    }
206
207    /// Run in full-auto mode — no approval prompts (`--full-auto`).
208    ///
209    /// As of `codex-cli` 0.145.0 this flag is accepted but hidden from
210    /// `codex exec --help`. It still functions but is undocumented and may be
211    /// removed in a future CLI release.
212    #[must_use]
213    pub fn full_auto(mut self) -> Self {
214        self.full_auto = true;
215        self
216    }
217
218    /// Bypass all approval prompts and sandbox restrictions.
219    ///
220    /// Passes `--dangerously-bypass-approvals-and-sandbox`. Use with caution.
221    #[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    /// Change the working directory before running (`--cd <dir>`).
228    #[must_use]
229    pub fn cd(mut self, dir: impl Into<String>) -> Self {
230        self.cd = Some(dir.into());
231        self
232    }
233
234    /// Skip the git repository check (`--skip-git-repo-check`).
235    #[must_use]
236    pub fn skip_git_repo_check(mut self) -> Self {
237        self.skip_git_repo_check = true;
238        self
239    }
240
241    /// Add an extra directory to the context (`--add-dir <dir>`).
242    ///
243    /// May be called multiple times.
244    #[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    /// Run in ephemeral mode — no session is persisted (`--ephemeral`).
251    #[must_use]
252    pub fn ephemeral(mut self) -> Self {
253        self.ephemeral = true;
254        self
255    }
256
257    /// Require output to conform to a JSON schema (`--output-schema <path>`).
258    #[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    /// Control terminal color output (`--color <mode>`).
265    #[must_use]
266    pub fn color(mut self, color: Color) -> Self {
267        self.color = Some(color);
268        self
269    }
270
271    /// Emit JSON Lines output (`--json`).
272    ///
273    /// When set, stdout will contain one JSON object per line. Use
274    /// [`execute_json_lines`](ExecCommand::execute_json_lines) to parse the
275    /// events automatically (requires the `json` feature).
276    #[must_use]
277    pub fn json(mut self) -> Self {
278        self.json = true;
279        self
280    }
281
282    /// Write the last assistant message to a file (`--output-last-message <path>`).
283    #[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    /// Override the retry policy for this command.
290    ///
291    /// Takes precedence over the client-level policy set on [`Codex`].
292    #[must_use]
293    pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
294        self.retry_policy = Some(policy);
295        self
296    }
297
298    /// Stream JSONL events from the command, invoking `handler` for each
299    /// parsed [`JsonLineEvent`] as it arrives.
300    ///
301    /// Automatically appends `--json` if not already set. Requires the `json`
302    /// feature.
303    ///
304    /// # Example
305    ///
306    /// ```no_run
307    /// use codex_wrapper::{Codex, ExecCommand, JsonLineEvent};
308    ///
309    /// # async fn example() -> codex_wrapper::Result<()> {
310    /// let codex = Codex::builder().build()?;
311    /// ExecCommand::new("what is 2+2?")
312    ///     .ephemeral()
313    ///     .stream(&codex, |event: JsonLineEvent| {
314    ///         println!("{}: {:?}", event.event_type, event.extra);
315    ///     })
316    ///     .await?;
317    /// # Ok(())
318    /// # }
319    /// ```
320    #[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    /// Execute the command and parse the output as JSON Lines events.
329    ///
330    /// Automatically appends `--json` if not already set. Requires the `json`
331    /// feature.
332    #[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    /// Execute the command and return a typed [`QueryResult`].
344    ///
345    /// Assembles the final result text, ids, and cost from the JSONL event
346    /// stream. Use [`execute_json_lines`](ExecCommand::execute_json_lines) for
347    /// the raw event stream. Requires the `json` feature.
348    #[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/// Resume a previous non-interactive session (`codex exec resume`).
442///
443/// Use [`session_id`](ExecResumeCommand::session_id) to target a specific
444/// session, or [`last`](ExecResumeCommand::last) to pick the most recent.
445#[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    /// Create a new resume command with no options set.
469    #[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    /// Resume a specific session by its ID.
494    #[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    /// Append an additional prompt to the resumed session.
501    #[must_use]
502    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
503        self.prompt = Some(prompt.into());
504        self
505    }
506
507    /// Resume the most recent session (`--last`).
508    #[must_use]
509    pub fn last(mut self) -> Self {
510        self.last = true;
511        self
512    }
513
514    /// Resume all sessions (`--all`).
515    #[must_use]
516    pub fn all(mut self) -> Self {
517        self.all = true;
518        self
519    }
520
521    /// Set the model to use (`--model <model>`).
522    ///
523    /// Panics if `model` is an empty string.
524    #[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    /// Attach an image to the prompt (`--image <path>`).
533    ///
534    /// May be called multiple times to attach several images.
535    #[must_use]
536    pub fn image(mut self, path: impl Into<String>) -> Self {
537        self.images.push(path.into());
538        self
539    }
540
541    /// Emit JSON Lines output (`--json`).
542    #[must_use]
543    pub fn json(mut self) -> Self {
544        self.json = true;
545        self
546    }
547
548    /// Write the last assistant message to a file (`--output-last-message <path>`).
549    #[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    /// Override a config key (`-c key=value`).
556    ///
557    /// May be called multiple times to set several keys.
558    #[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    /// Enable an optional feature flag (`--enable <feature>`).
565    ///
566    /// May be called multiple times.
567    #[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    /// Disable an optional feature flag (`--disable <feature>`).
574    ///
575    /// May be called multiple times.
576    #[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    /// Error on unrecognized config keys (`--strict-config`).
583    #[must_use]
584    pub fn strict_config(mut self) -> Self {
585        self.strict_config = true;
586        self
587    }
588
589    /// Bypass the hook trust prompt (`--dangerously-bypass-hook-trust`).
590    ///
591    /// Allows configured hooks to run without confirmation. Use with caution.
592    #[must_use]
593    pub fn dangerously_bypass_hook_trust(mut self) -> Self {
594        self.dangerously_bypass_hook_trust = true;
595        self
596    }
597
598    /// Run in full-auto mode — no approval prompts (`--full-auto`).
599    #[must_use]
600    pub fn full_auto(mut self) -> Self {
601        self.full_auto = true;
602        self
603    }
604
605    /// Bypass all approval prompts and sandbox restrictions.
606    ///
607    /// Passes `--dangerously-bypass-approvals-and-sandbox`. Use with caution.
608    #[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    /// Skip the git repository check (`--skip-git-repo-check`).
615    #[must_use]
616    pub fn skip_git_repo_check(mut self) -> Self {
617        self.skip_git_repo_check = true;
618        self
619    }
620
621    /// Run in ephemeral mode — no session is persisted (`--ephemeral`).
622    #[must_use]
623    pub fn ephemeral(mut self) -> Self {
624        self.ephemeral = true;
625        self
626    }
627
628    /// Override the retry policy for this command.
629    ///
630    /// Takes precedence over the client-level policy set on [`Codex`].
631    #[must_use]
632    pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
633        self.retry_policy = Some(policy);
634        self
635    }
636
637    /// Execute the command and parse the output as JSON Lines events.
638    ///
639    /// Automatically appends `--json` if not already set. Requires the `json`
640    /// feature.
641    #[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    /// Execute the resume command and return a typed [`QueryResult`].
653    ///
654    /// Assembles the final result text, ids, and cost from the JSONL event
655    /// stream. Requires the `json` feature.
656    #[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    /// Stream JSONL events from the resume command, invoking `handler` for
663    /// each parsed [`JsonLineEvent`] as it arrives.
664    ///
665    /// Automatically appends `--json` if not already set. Requires the `json`
666    /// feature.
667    #[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}