Skip to main content

claude_code_agent_sdk/
version.rs

1//! Version information for the Claude Agent SDK
2
3use std::sync::OnceLock;
4
5/// The version of this SDK
6pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
7
8/// Minimum required Claude Code CLI version
9pub const MIN_CLI_VERSION: &str = "2.0.0";
10
11/// Bundled Claude Code CLI version (build.rs downloads this version when `bundled-cli` feature is enabled)
12pub const CLI_VERSION: &str = "2.1.38";
13
14/// Bundled CLI storage directory (relative to home directory)
15pub(crate) const BUNDLED_CLI_DIR: &str = ".claude/sdk/bundled";
16
17/// Get the full path to the bundled CLI binary.
18///
19/// Returns `Some(path)` where path is `~/.claude/sdk/bundled/{CLI_VERSION}/claude` (or `claude.exe` on Windows).
20/// Returns `None` if the home directory cannot be determined.
21pub fn bundled_cli_path() -> Option<std::path::PathBuf> {
22    dirs::home_dir().map(|home| {
23        let cli_name = if cfg!(target_os = "windows") {
24            "claude.exe"
25        } else {
26            "claude"
27        };
28        home.join(BUNDLED_CLI_DIR).join(CLI_VERSION).join(cli_name)
29    })
30}
31
32/// Environment variable to skip version check
33pub const SKIP_VERSION_CHECK_ENV: &str = "CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK";
34
35/// Entrypoint identifier for subprocess
36pub const ENTRYPOINT: &str = "sdk-rs";
37
38/// Parse a semantic version string into (major, minor, patch)
39pub fn parse_version(version: &str) -> Option<(u32, u32, u32)> {
40    let parts: Vec<&str> = version.trim_start_matches('v').split('.').collect();
41    if parts.len() < 3 {
42        return None;
43    }
44
45    let major = parts[0].parse().ok()?;
46    let minor = parts[1].parse().ok()?;
47    let patch = parts[2].parse().ok()?;
48
49    Some((major, minor, patch))
50}
51
52/// Check if the CLI version meets the minimum requirement
53pub fn check_version(cli_version: &str) -> bool {
54    let Some((cli_maj, cli_min, cli_patch)) = parse_version(cli_version) else {
55        return false;
56    };
57
58    let Some((req_maj, req_min, req_patch)) = parse_version(MIN_CLI_VERSION) else {
59        return false;
60    };
61
62    if cli_maj > req_maj {
63        return true;
64    }
65    if cli_maj < req_maj {
66        return false;
67    }
68
69    // Major versions are equal
70    if cli_min > req_min {
71        return true;
72    }
73    if cli_min < req_min {
74        return false;
75    }
76
77    // Major and minor are equal
78    cli_patch >= req_patch
79}
80
81/// Cached Claude Code CLI version
82static CLAUDE_CODE_VERSION: OnceLock<Option<String>> = OnceLock::new();
83
84/// Get Claude Code CLI version
85///
86/// This function uses OnceLock to cache the result, so the CLI is only called once.
87/// Returns None if CLI is not found or version cannot be determined.
88pub fn get_claude_code_version() -> Option<&'static str> {
89    CLAUDE_CODE_VERSION
90        .get_or_init(|| {
91            std::process::Command::new("claude")
92                .arg("--version")
93                .output()
94                .ok()
95                .filter(|output| output.status.success())
96                .and_then(|output| {
97                    let version_output = String::from_utf8_lossy(&output.stdout);
98                    version_output
99                        .lines()
100                        .next()
101                        .and_then(|line| line.split_whitespace().next())
102                        .map(|v| v.trim().to_string())
103                })
104        })
105        .as_deref()
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_parse_version() {
114        assert_eq!(parse_version("1.2.3"), Some((1, 2, 3)));
115        assert_eq!(parse_version("v1.2.3"), Some((1, 2, 3)));
116        assert_eq!(parse_version("10.20.30"), Some((10, 20, 30)));
117        assert_eq!(parse_version("1.2"), None);
118        assert_eq!(parse_version("invalid"), None);
119    }
120
121    #[test]
122    fn test_check_version() {
123        assert!(check_version("2.0.0"));
124        assert!(check_version("2.0.1"));
125        assert!(check_version("2.1.0"));
126        assert!(check_version("3.0.0"));
127        assert!(!check_version("1.9.9"));
128        assert!(!check_version("1.99.99"));
129    }
130
131    #[test]
132    fn test_cli_version_format() {
133        assert!(
134            parse_version(CLI_VERSION).is_some(),
135            "CLI_VERSION must be a valid semver string"
136        );
137    }
138
139    #[test]
140    fn test_cli_version_meets_minimum() {
141        assert!(
142            check_version(CLI_VERSION),
143            "CLI_VERSION ({}) must meet MIN_CLI_VERSION ({})",
144            CLI_VERSION,
145            MIN_CLI_VERSION
146        );
147    }
148
149    #[test]
150    fn test_bundled_cli_path_format() {
151        if let Some(path) = bundled_cli_path() {
152            let path_str = path.to_string_lossy();
153            assert!(
154                path_str.contains(".claude/sdk/bundled"),
155                "bundled path must contain '.claude/sdk/bundled': {}",
156                path_str
157            );
158            assert!(
159                path_str.contains(CLI_VERSION),
160                "bundled path must contain CLI_VERSION ({}): {}",
161                CLI_VERSION,
162                path_str
163            );
164        }
165    }
166}