Skip to main content

sqlite_graphrag/commands/ingest_claude/
binary.rs

1//! Claude Code binary discovery and version validation.
2
3use super::types::MIN_CLAUDE_VERSION;
4use crate::errors::AppError;
5use std::path::{Path, PathBuf};
6use std::process::{Command, Stdio};
7
8/// Locates the Claude Code binary on the system.
9pub fn find_claude_binary(explicit: Option<&Path>) -> Result<PathBuf, AppError> {
10    if let Some(p) = explicit {
11        if p.exists() {
12            return Ok(p.to_path_buf());
13        }
14        return Err(AppError::Validation(
15            crate::i18n::validation::binary_not_found_at_path(
16                "Claude Code",
17                &p.display().to_string(),
18            ),
19        ));
20    }
21
22    if let Some(env_path) = crate::runtime_config::claude_binary() {
23        let p = PathBuf::from(&env_path);
24        if p.exists() {
25            return Ok(p);
26        }
27    }
28
29    let name = if cfg!(windows) {
30        "claude.exe"
31    } else {
32        "claude"
33    };
34    if let Some(path_var) = std::env::var_os("PATH") {
35        for dir in std::env::split_paths(&path_var) {
36            let candidate = dir.join(name);
37            if candidate.exists() {
38                return Ok(candidate);
39            }
40        }
41    }
42
43    Err(AppError::Validation(
44        "Claude Code binary not found in PATH. Install it from https://docs.anthropic.com/claude-code or specify --claude-binary".to_string(),
45    ))
46}
47
48/// Validates that the Claude Code binary meets the minimum version.
49pub(crate) fn validate_claude_version(binary: &Path) -> Result<String, AppError> {
50    let output = Command::new(binary)
51        .arg("--version")
52        .stdin(Stdio::null())
53        .stdout(Stdio::piped())
54        .stderr(Stdio::piped())
55        .output()
56        .map_err(AppError::Io)?;
57
58    if !output.status.success() {
59        return Err(AppError::Validation(
60            "failed to run 'claude --version'".to_string(),
61        ));
62    }
63
64    let version_str = String::from_utf8(output.stdout)
65        .map_err(|_| AppError::Validation(crate::i18n::validation::claude_version_not_utf8()))?;
66    let version = version_str.trim().to_string();
67
68    // Extract the numeric version part before first space or paren, e.g. "2.1.149 (Claude Code)" -> "2.1.149"
69    let numeric = version.split([' ', '(']).next().unwrap_or("").trim();
70
71    fn parse_semver(s: &str) -> Option<(u64, u64, u64)> {
72        let parts: Vec<&str> = s.splitn(3, '.').collect();
73        if parts.len() < 2 {
74            return None;
75        }
76        let major = parts[0].parse::<u64>().ok()?;
77        let minor = parts[1].parse::<u64>().ok()?;
78        let patch = parts
79            .get(2)
80            .and_then(|p| p.parse::<u64>().ok())
81            .unwrap_or(0);
82        Some((major, minor, patch))
83    }
84
85    if let (Some(actual), Some(min)) = (parse_semver(numeric), parse_semver(MIN_CLAUDE_VERSION)) {
86        if actual < min {
87            return Err(AppError::Validation(
88                crate::i18n::validation::version_below_minimum(
89                    "Claude Code",
90                    numeric,
91                    MIN_CLAUDE_VERSION,
92                ),
93            ));
94        }
95    }
96
97    Ok(version)
98}