1pub 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}