codex_profiles/
requirements.rs1use 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}