Skip to main content

codex_wrapper/command/
mod.rs

1//! Command builders for every Codex CLI subcommand.
2//!
3//! Each subcommand is a builder struct that implements [`CodexCommand`].
4//! Builders accumulate flags via method chaining, then call
5//! [`CodexCommand::execute`] with a [`Codex`] client to run.
6
7pub mod apply;
8pub mod completion;
9pub mod doctor;
10pub mod exec;
11pub mod features;
12pub mod fork;
13pub mod login;
14pub mod mcp;
15pub mod mcp_server;
16pub mod plugin;
17pub mod raw;
18pub mod resume;
19pub mod review;
20pub mod sandbox;
21pub mod session_mgmt;
22pub mod update;
23pub mod version;
24
25use std::future::Future;
26
27use crate::Codex;
28use crate::error::Result;
29
30/// Trait implemented by all Codex CLI command builders.
31///
32/// [`args`](CodexCommand::args) returns the CLI arguments the builder would
33/// pass to the `codex` binary. [`execute`](CodexCommand::execute) spawns the
34/// process and returns typed output.
35pub trait CodexCommand: Send + Sync {
36    /// The type returned on success.
37    type Output: Send;
38
39    /// Build the argument list for this command.
40    fn args(&self) -> Vec<String>;
41
42    /// Execute the command against the given [`Codex`] client.
43    fn execute(&self, codex: &Codex) -> impl Future<Output = Result<Self::Output>> + Send;
44
45    /// Render the exact command line this builder will spawn, quoted for a
46    /// POSIX shell.
47    ///
48    /// Useful for logging a reproduction or checking an invocation before
49    /// running it. The client's global args precede the command's own, the
50    /// same order the spawn uses, because both go through one assembly
51    /// function: the preview cannot drift from what runs.
52    ///
53    /// ```no_run
54    /// use codex_wrapper::{Codex, CodexCommand, ExecCommand};
55    ///
56    /// # fn example() -> codex_wrapper::Result<()> {
57    /// let codex = Codex::builder().build()?;
58    /// let cmd = ExecCommand::new("fix the failing tests").ephemeral();
59    /// println!("{}", cmd.to_command_string(&codex));
60    /// // codex exec --ephemeral 'fix the failing tests'
61    /// # Ok(())
62    /// # }
63    /// ```
64    ///
65    /// The rendering is for humans. It is faithful to the argv, but the args
66    /// are passed to the process directly rather than through a shell, so a
67    /// shell is never involved at spawn time.
68    fn to_command_string(&self, codex: &Codex) -> String {
69        crate::exec::command_string(codex, self.args())
70    }
71}
72
73#[cfg(all(test, unix))]
74mod tests {
75    use super::*;
76    use crate::command::exec::{ExecCommand, ExecResumeCommand};
77
78    /// Run a fake codex that echoes its argv, one argument per line.
79    async fn spawned_args(cmd: &impl CodexCommand, codex: &Codex) -> Vec<String> {
80        let output = crate::exec::run_codex(codex, cmd.args()).await.unwrap();
81        output.stdout.lines().map(str::to_string).collect()
82    }
83
84    fn echoing_codex() -> Codex {
85        let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
86            .join("tests")
87            .join("fake-codex-echo-args.sh");
88        Codex::builder()
89            .binary("/bin/bash")
90            .arg(script.to_str().unwrap())
91            .config("model=\"gpt-5\"")
92            .build()
93            .expect("bash must exist")
94    }
95
96    /// The preview is only worth anything if it matches the real spawn. This
97    /// compares against the argv a process actually received, rather than
98    /// against the assembly function the preview itself calls: a test written
99    /// that way would still pass if the spawn path stopped using it.
100    ///
101    /// Quoting is shared with the implementation here, and covered on its own
102    /// in `exec::tests`. What this pins is the part a shared function cannot
103    /// prove by itself: that the args reaching the process are the same ones,
104    /// in the same order, that the preview claims.
105    #[tokio::test]
106    async fn preview_matches_the_argv_a_spawn_receives() {
107        let codex = echoing_codex();
108        let cmd = ExecCommand::new("fix the failing tests").ephemeral();
109
110        let spawned = spawned_args(&cmd, &codex).await;
111        let preview = cmd.to_command_string(&codex);
112
113        // The fake is `bash <script>`, so the echoed argv is the preview with
114        // the binary and the script path removed from the front.
115        let rendered: Vec<String> = spawned
116            .iter()
117            .map(|a| crate::exec::shell_quote(a))
118            .collect();
119        assert!(
120            preview.ends_with(&rendered.join(" ")),
121            "preview {preview:?} does not end with the spawned argv {rendered:?}"
122        );
123        // The global -c pair from the client is in there, ahead of the args.
124        assert_eq!(spawned[0], "-c");
125        assert_eq!(spawned[1], "model=\"gpt-5\"");
126        assert_eq!(spawned[2], "exec");
127    }
128
129    #[test]
130    fn preview_puts_global_args_before_the_subcommand() {
131        let codex = echoing_codex();
132        let preview = ExecCommand::new("hi").ephemeral().to_command_string(&codex);
133        assert!(
134            preview.contains(r#"-c 'model="gpt-5"' exec"#),
135            "globals must precede the subcommand: {preview}"
136        );
137        assert!(preview.ends_with("--ephemeral hi"), "{preview}");
138    }
139
140    #[test]
141    fn preview_is_available_on_every_builder() {
142        let codex = echoing_codex();
143        // Provided on the trait, so a resume builder gets it too.
144        let preview = ExecResumeCommand::new().last().to_command_string(&codex);
145        assert!(preview.contains("exec resume --last"), "{preview}");
146    }
147}