Skip to main content

codex_profiles/
requirements.rs

1use crate::{InstallSource, format_cmd};
2
3pub fn ensure_codex_cli(source: InstallSource) -> Result<(), String> {
4    ensure_codex_cli_with(source, cfg!(debug_assertions))
5}
6
7fn ensure_codex_cli_with(source: InstallSource, is_debug: bool) -> Result<(), String> {
8    if is_debug {
9        return Ok(());
10    }
11    if command_exists("codex") {
12        return Ok(());
13    }
14    let install_cmd = format_cmd(&install_command(source), false);
15    Err(format!(
16        "Error: Codex CLI not found. Install it with {install_cmd}."
17    ))
18}
19
20fn install_command(source: InstallSource) -> String {
21    match source {
22        InstallSource::Npm => "npm install -g @openai/codex".to_string(),
23        InstallSource::Bun => "bun install -g @openai/codex".to_string(),
24        InstallSource::Brew => "brew install --cask codex".to_string(),
25        InstallSource::Unknown => platform_default_install_command(),
26    }
27}
28
29fn command_exists(command: &str) -> bool {
30    let Some(path) = std::env::var_os("PATH") else {
31        return false;
32    };
33    let candidates = command_candidates(command);
34    for dir in std::env::split_paths(&path) {
35        for candidate in &candidates {
36            if dir.join(candidate).is_file() {
37                return true;
38            }
39        }
40    }
41    false
42}
43
44#[cfg(windows)]
45fn command_candidates(command: &str) -> Vec<String> {
46    let mut candidates = Vec::new();
47    let path = std::path::Path::new(command);
48    if path.extension().is_some() {
49        candidates.push(command.to_string());
50        return candidates;
51    }
52    let pathext = std::env::var_os("PATHEXT")
53        .and_then(|value| value.into_string().ok())
54        .unwrap_or_else(|| ".EXE;.CMD;.BAT;.COM".to_string());
55    for ext in pathext.split(';').filter(|ext| !ext.is_empty()) {
56        candidates.push(format!("{command}{ext}"));
57    }
58    candidates
59}
60
61#[cfg(not(windows))]
62fn command_candidates(command: &str) -> Vec<String> {
63    vec![command.to_string()]
64}
65
66#[cfg(windows)]
67fn platform_default_install_command() -> String {
68    "winget install OpenAI.Codex".to_string()
69}
70
71#[cfg(target_os = "macos")]
72fn platform_default_install_command() -> String {
73    "brew install --cask codex or npm install -g @openai/codex".to_string()
74}
75
76#[cfg(all(not(target_os = "macos"), not(windows)))]
77fn platform_default_install_command() -> String {
78    "npm install -g @openai/codex".to_string()
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::test_utils::{ENV_MUTEX, set_env_guard};
85    use std::fs;
86
87    #[test]
88    fn ensure_codex_cli_skips_in_debug() {
89        ensure_codex_cli_with(InstallSource::Npm, true).unwrap();
90    }
91
92    #[test]
93    fn ensure_codex_cli_passes_when_codex_exists() {
94        let _guard = ENV_MUTEX.lock().unwrap();
95        let dir = tempfile::tempdir().expect("tempdir");
96        let bin = dir.path().join("codex");
97        fs::write(&bin, "stub").expect("write bin");
98        #[cfg(unix)]
99        {
100            use std::os::unix::fs::PermissionsExt;
101            let mut perms = fs::metadata(&bin).expect("meta").permissions();
102            perms.set_mode(0o755);
103            fs::set_permissions(&bin, perms).expect("chmod");
104        }
105        let path = dir.path().to_string_lossy().into_owned();
106        let _env = set_env_guard("PATH", Some(&path));
107        ensure_codex_cli_with(InstallSource::Npm, false).unwrap();
108    }
109
110    #[test]
111    fn ensure_codex_cli_errors_when_missing() {
112        let _guard = ENV_MUTEX.lock().unwrap();
113        let _env = set_env_guard("PATH", Some(""));
114        let err = ensure_codex_cli_with(InstallSource::Npm, false).unwrap_err();
115        assert!(err.contains("codex"));
116    }
117
118    #[test]
119    fn install_commands_cover_sources() {
120        assert!(install_command(InstallSource::Npm).contains("npm"));
121        assert!(install_command(InstallSource::Bun).contains("bun"));
122        assert!(install_command(InstallSource::Brew).contains("brew"));
123        assert!(!platform_default_install_command().is_empty());
124    }
125
126    #[test]
127    fn command_candidates_non_windows() {
128        let candidates = command_candidates("codex");
129        assert_eq!(candidates, vec!["codex".to_string()]);
130    }
131}