Skip to main content

codex_codes/
version.rs

1//! Version checking utilities for Codex CLI compatibility.
2
3use crate::error::Result;
4use log::{debug, warn};
5use std::process::Command;
6use std::sync::Once;
7
8/// The latest Codex CLI version we've tested against.
9const TESTED_VERSION: &str = "0.147.0";
10
11/// The Codex CLI release this crate's live integration suite last passed
12/// against — the machine-readable form of the crate's version convention.
13/// Kept in lockstep with the README's `Tested against:` line by CI.
14pub fn tested_cli_version() -> &'static str {
15    TESTED_VERSION
16}
17
18/// Ensures version warning is only shown once per session.
19static VERSION_CHECK: Once = Once::new();
20
21/// Check the Codex CLI version and warn if newer than tested.
22///
23/// This will only issue a warning once per program execution.
24pub fn check_codex_version() -> Result<()> {
25    VERSION_CHECK.call_once(|| {
26        if let Err(e) = check_version_impl() {
27            debug!("Failed to check Codex CLI version: {}", e);
28        }
29    });
30    Ok(())
31}
32
33fn check_version_impl() -> Result<()> {
34    let mut command = Command::new("codex");
35    command.arg("--version");
36    crate::process::configure_no_window(&mut command);
37    let output = command.output().map_err(crate::error::Error::Io)?;
38
39    if !output.status.success() {
40        debug!("Failed to check Codex CLI version - command failed");
41        return Ok(());
42    }
43
44    let version_str = String::from_utf8_lossy(&output.stdout);
45    let version_line = version_str.lines().next().unwrap_or("");
46
47    // Format: "codex-cli X.Y.Z"
48    if let Some(version) = version_line.split_whitespace().last() {
49        if is_version_newer(version, TESTED_VERSION) {
50            warn!(
51                "Codex CLI version {} is newer than tested version {}. \
52                 Please report compatibility at: https://github.com/meawoppl/rust-code-agent-sdks/issues",
53                version, TESTED_VERSION
54            );
55        } else {
56            debug!(
57                "Codex CLI version {} is compatible (tested: {})",
58                version, TESTED_VERSION
59            );
60        }
61    } else {
62        warn!(
63            "Could not parse Codex CLI version from output: '{}'. \
64             Please report compatibility at: https://github.com/meawoppl/rust-code-agent-sdks/issues",
65            version_line
66        );
67    }
68
69    Ok(())
70}
71
72/// Compare two version strings (e.g., "0.104.0" vs "0.103.0").
73fn is_version_newer(version: &str, tested: &str) -> bool {
74    let v_parts: Vec<u32> = version.split('.').filter_map(|s| s.parse().ok()).collect();
75    let t_parts: Vec<u32> = tested.split('.').filter_map(|s| s.parse().ok()).collect();
76
77    use std::cmp::Ordering;
78
79    for i in 0..v_parts.len().min(t_parts.len()) {
80        match v_parts[i].cmp(&t_parts[i]) {
81            Ordering::Greater => return true,
82            Ordering::Less => return false,
83            Ordering::Equal => continue,
84        }
85    }
86
87    v_parts.len() > t_parts.len()
88}
89
90/// Async version check for tokio-based clients.
91#[cfg(feature = "async-client")]
92pub async fn check_codex_version_async() -> Result<()> {
93    use tokio::sync::OnceCell;
94
95    static ASYNC_VERSION_CHECK: OnceCell<()> = OnceCell::const_new();
96
97    ASYNC_VERSION_CHECK
98        .get_or_init(|| async {
99            if let Err(e) = check_version_impl_async().await {
100                debug!("Failed to check Codex CLI version: {}", e);
101            }
102        })
103        .await;
104
105    Ok(())
106}
107
108#[cfg(feature = "async-client")]
109async fn check_version_impl_async() -> Result<()> {
110    use tokio::process::Command;
111
112    let mut command = Command::new("codex");
113    command.arg("--version");
114    crate::process::configure_no_window(command.as_std_mut());
115    let output = command.output().await.map_err(crate::error::Error::Io)?;
116
117    if !output.status.success() {
118        debug!("Failed to check Codex CLI version - command failed");
119        return Ok(());
120    }
121
122    let version_str = String::from_utf8_lossy(&output.stdout);
123    let version_line = version_str.lines().next().unwrap_or("");
124
125    // Format: "codex-cli X.Y.Z"
126    if let Some(version) = version_line.split_whitespace().last() {
127        if is_version_newer(version, TESTED_VERSION) {
128            warn!(
129                "Codex CLI version {} is newer than tested version {}. \
130                 Please report compatibility at: https://github.com/meawoppl/rust-code-agent-sdks/issues",
131                version, TESTED_VERSION
132            );
133        } else {
134            debug!(
135                "Codex CLI version {} is compatible (tested: {})",
136                version, TESTED_VERSION
137            );
138        }
139    } else {
140        warn!(
141            "Could not parse Codex CLI version from output: '{}'. \
142             Please report compatibility at: https://github.com/meawoppl/rust-code-agent-sdks/issues",
143            version_line
144        );
145    }
146
147    Ok(())
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_version_comparison() {
156        assert!(is_version_newer("0.105.0", "0.104.0"));
157        assert!(!is_version_newer("0.104.0", "0.104.0"));
158        assert!(!is_version_newer("0.103.0", "0.104.0"));
159
160        assert!(is_version_newer("1.0.0", "0.104.0"));
161        assert!(!is_version_newer("0.0.1", "0.104.0"));
162        assert!(is_version_newer("0.104.1", "0.104.0"));
163    }
164}