Skip to main content

codex_wrapper/
lib.rs

1//! A type-safe Codex CLI wrapper for Rust.
2//!
3//! `codex-wrapper` mirrors the builder-oriented shape of `claude-wrapper`, but
4//! targets the current Codex CLI surface.
5
6pub mod command;
7pub mod error;
8pub mod exec;
9pub mod retry;
10pub mod types;
11pub mod version;
12
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::time::Duration;
16
17pub use command::CodexCommand;
18pub use command::exec::{ExecCommand, ExecResumeCommand};
19pub use command::login::{LoginCommand, LoginStatusCommand, LogoutCommand};
20pub use command::mcp::{
21    McpAddCommand, McpGetCommand, McpListCommand, McpLoginCommand, McpLogoutCommand,
22    McpRemoveCommand,
23};
24pub use command::raw::RawCommand;
25pub use command::review::ReviewCommand;
26pub use command::version::VersionCommand;
27pub use error::{Error, Result};
28pub use exec::CommandOutput;
29pub use retry::{BackoffStrategy, RetryPolicy};
30pub use types::*;
31pub use version::{CliVersion, VersionParseError};
32
33#[derive(Debug, Clone)]
34pub struct Codex {
35    pub(crate) binary: PathBuf,
36    pub(crate) working_dir: Option<PathBuf>,
37    pub(crate) env: HashMap<String, String>,
38    pub(crate) global_args: Vec<String>,
39    pub(crate) timeout: Option<Duration>,
40    pub(crate) retry_policy: Option<RetryPolicy>,
41}
42
43impl Codex {
44    #[must_use]
45    pub fn builder() -> CodexBuilder {
46        CodexBuilder::default()
47    }
48
49    #[must_use]
50    pub fn binary(&self) -> &Path {
51        &self.binary
52    }
53
54    #[must_use]
55    pub fn working_dir(&self) -> Option<&Path> {
56        self.working_dir.as_deref()
57    }
58
59    #[must_use]
60    pub fn with_working_dir(&self, dir: impl Into<PathBuf>) -> Self {
61        let mut clone = self.clone();
62        clone.working_dir = Some(dir.into());
63        clone
64    }
65
66    pub async fn cli_version(&self) -> Result<CliVersion> {
67        let output = VersionCommand::new().execute(self).await?;
68        CliVersion::parse_version_output(&output.stdout).map_err(|e| Error::Io {
69            message: format!("failed to parse CLI version: {e}"),
70            source: std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()),
71            working_dir: None,
72        })
73    }
74
75    pub async fn check_version(&self, minimum: &CliVersion) -> Result<CliVersion> {
76        let version = self.cli_version().await?;
77        if version.satisfies_minimum(minimum) {
78            Ok(version)
79        } else {
80            Err(Error::VersionMismatch {
81                found: version,
82                minimum: *minimum,
83            })
84        }
85    }
86}
87
88#[derive(Debug, Default)]
89pub struct CodexBuilder {
90    binary: Option<PathBuf>,
91    working_dir: Option<PathBuf>,
92    env: HashMap<String, String>,
93    global_args: Vec<String>,
94    timeout: Option<Duration>,
95    retry_policy: Option<RetryPolicy>,
96}
97
98impl CodexBuilder {
99    #[must_use]
100    pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
101        self.binary = Some(path.into());
102        self
103    }
104
105    #[must_use]
106    pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
107        self.working_dir = Some(path.into());
108        self
109    }
110
111    #[must_use]
112    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
113        self.env.insert(key.into(), value.into());
114        self
115    }
116
117    #[must_use]
118    pub fn envs(
119        mut self,
120        vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
121    ) -> Self {
122        for (key, value) in vars {
123            self.env.insert(key.into(), value.into());
124        }
125        self
126    }
127
128    #[must_use]
129    pub fn timeout_secs(mut self, seconds: u64) -> Self {
130        self.timeout = Some(Duration::from_secs(seconds));
131        self
132    }
133
134    #[must_use]
135    pub fn timeout(mut self, duration: Duration) -> Self {
136        self.timeout = Some(duration);
137        self
138    }
139
140    #[must_use]
141    pub fn arg(mut self, arg: impl Into<String>) -> Self {
142        self.global_args.push(arg.into());
143        self
144    }
145
146    #[must_use]
147    pub fn config(mut self, key_value: impl Into<String>) -> Self {
148        self.global_args.push("-c".into());
149        self.global_args.push(key_value.into());
150        self
151    }
152
153    #[must_use]
154    pub fn enable(mut self, feature: impl Into<String>) -> Self {
155        self.global_args.push("--enable".into());
156        self.global_args.push(feature.into());
157        self
158    }
159
160    #[must_use]
161    pub fn disable(mut self, feature: impl Into<String>) -> Self {
162        self.global_args.push("--disable".into());
163        self.global_args.push(feature.into());
164        self
165    }
166
167    #[must_use]
168    pub fn retry(mut self, policy: RetryPolicy) -> Self {
169        self.retry_policy = Some(policy);
170        self
171    }
172
173    pub fn build(self) -> Result<Codex> {
174        let binary = match self.binary {
175            Some(path) => path,
176            None => which::which("codex").map_err(|_| Error::NotFound)?,
177        };
178
179        Ok(Codex {
180            binary,
181            working_dir: self.working_dir,
182            env: self.env,
183            global_args: self.global_args,
184            timeout: self.timeout,
185            retry_policy: self.retry_policy,
186        })
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn builder_with_binary() {
196        let codex = Codex::builder()
197            .binary("/usr/local/bin/codex")
198            .env("FOO", "bar")
199            .timeout_secs(60)
200            .build()
201            .unwrap();
202
203        assert_eq!(codex.binary, PathBuf::from("/usr/local/bin/codex"));
204        assert_eq!(codex.env.get("FOO").unwrap(), "bar");
205        assert_eq!(codex.timeout, Some(Duration::from_secs(60)));
206    }
207
208    #[test]
209    fn builder_global_args() {
210        let codex = Codex::builder()
211            .binary("/usr/local/bin/codex")
212            .config("model=\"gpt-5\"")
213            .enable("foo")
214            .disable("bar")
215            .build()
216            .unwrap();
217
218        assert_eq!(
219            codex.global_args,
220            vec![
221                "-c",
222                "model=\"gpt-5\"",
223                "--enable",
224                "foo",
225                "--disable",
226                "bar"
227            ]
228        );
229    }
230}