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