Skip to main content

claude_codes/
version.rs

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