Skip to main content

sqlite_graphrag/commands/ingest_codex/
binary.rs

1//! Codex CLI binary discovery and version validation.
2
3use super::types::MIN_CODEX_VERSION;
4use crate::errors::AppError;
5use std::path::{Path, PathBuf};
6use std::process::{Command, Stdio};
7
8/// 3. PATH search for `codex` (or `codex.exe` on Windows).
9pub fn find_codex_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                "Codex CLI",
17                &p.display().to_string(),
18            ),
19        ));
20    }
21
22    if let Some(env_path) = crate::runtime_config::codex_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) { "codex.exe" } else { "codex" };
30    if let Some(path_var) = std::env::var_os("PATH") {
31        for dir in std::env::split_paths(&path_var) {
32            let candidate = dir.join(name);
33            if candidate.exists() {
34                return Ok(candidate);
35            }
36        }
37    }
38
39    Err(AppError::Validation(
40        "Codex CLI binary not found in PATH. Install it from https://github.com/openai/codex or specify --codex-binary".to_string(),
41    ))
42}
43
44/// Validates that the Codex CLI binary meets the minimum version requirement.
45///
46/// # Errors
47///
48/// Returns `AppError::Validation` when the binary cannot be executed or the
49/// version is below `MIN_CODEX_VERSION`.
50pub(crate) fn validate_codex_version(binary: &Path) -> Result<String, AppError> {
51    let resolved = which::which(binary).map_err(|_| {
52        AppError::Validation(crate::i18n::validation::executable_not_in_path(
53            &binary.display().to_string(),
54        ))
55    })?;
56    let output = Command::new(&resolved)
57        .arg("--version")
58        .stdin(Stdio::null())
59        .stdout(Stdio::piped())
60        .stderr(Stdio::piped())
61        .output()
62        .map_err(AppError::Io)?;
63
64    let raw = String::from_utf8(output.stdout)
65        .map_err(|_| AppError::Validation(crate::i18n::validation::codex_version_not_utf8()))?;
66
67    let version_str = raw.trim().to_string();
68
69    // Codex CLI outputs: "codex-cli 0.133.0" or just "0.133.0"
70    let numeric = version_str.split_whitespace().last().unwrap_or("").trim();
71
72    fn parse_semver(s: &str) -> Option<(u64, u64, u64)> {
73        let parts: Vec<&str> = s.splitn(3, '.').collect();
74        if parts.len() < 2 {
75            return None;
76        }
77        let major = parts[0].parse::<u64>().ok()?;
78        let minor = parts[1].parse::<u64>().ok()?;
79        let patch = parts
80            .get(2)
81            .and_then(|p| p.parse::<u64>().ok())
82            .unwrap_or(0);
83        Some((major, minor, patch))
84    }
85
86    if let (Some(actual), Some(min)) = (parse_semver(numeric), parse_semver(MIN_CODEX_VERSION)) {
87        if actual < min {
88            return Err(AppError::Validation(
89                crate::i18n::validation::version_below_minimum(
90                    "Codex CLI",
91                    numeric,
92                    MIN_CODEX_VERSION,
93                ),
94            ));
95        }
96    }
97
98    Ok(version_str)
99}