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}