sqlite_graphrag/commands/ingest_claude/
binary.rs1use super::types::MIN_CLAUDE_VERSION;
4use crate::errors::AppError;
5use std::path::{Path, PathBuf};
6use std::process::{Command, Stdio};
7
8pub 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
48pub(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 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}