fix(codex): fix Codex CLI path resolution on Windows under nvm
Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
parent
c925f9a9de
commit
d65f656704
|
|
@ -101,14 +101,29 @@ async fn resolve_program_path(program: &str) -> Option<String> {
|
|||
|
||||
fn direct_program_path(program: &str) -> Option<String> {
|
||||
let path = Path::new(program);
|
||||
if path.is_absolute() && path.is_file() {
|
||||
if !path.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
return launchable_program_in_dir(path, "codex");
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
#[cfg(windows)]
|
||||
return windows_launchable_program_path(path);
|
||||
#[cfg(not(windows))]
|
||||
return Some(path.to_string_lossy().to_string());
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn launchable_program_in_dir(dir: &Path, program: &str) -> Option<String> {
|
||||
program_path_candidates(dir, program)
|
||||
.into_iter()
|
||||
.find(|candidate| is_launchable_program_path(candidate) && candidate.is_file())
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
|
@ -313,6 +328,13 @@ fn validate_codex_program(config: &AiConfig) -> Result<String, String> {
|
|||
if starts_with_env_assignment(&program) {
|
||||
return Err("[codexCliPathInvalid] Codex CLI path should contain only the executable path. Add environment variables in the Codex CLI environment variables section.".to_string());
|
||||
}
|
||||
if is_path_like_program(&program) {
|
||||
let expanded = expand_tilde(&program);
|
||||
let path = Path::new(&expanded);
|
||||
if path.is_dir() && direct_program_path(&expanded).is_none() {
|
||||
return Err("[codexCliPathInvalid] Codex CLI path should point to the Codex executable or a directory containing codex.".to_string());
|
||||
}
|
||||
}
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +580,8 @@ mod tests {
|
|||
use super::{codex_process_env, common_executable_dirs, merged_path_with_dir};
|
||||
#[cfg(windows)]
|
||||
use super::{
|
||||
direct_program_path, first_windows_program_path, program_path_candidates, windows_npm_codex_shim_command,
|
||||
direct_program_path, first_windows_program_path, program_path_candidates, resolve_codex_command,
|
||||
windows_npm_codex_shim_command,
|
||||
};
|
||||
use crate::agent_events::AgentEvent;
|
||||
use crate::ai::{AiApiStyle, AiAuthMethod, AiConfig, AiProvider, AiReasoningLevel};
|
||||
|
|
@ -712,6 +735,50 @@ mod tests {
|
|||
let _ = std::fs::remove_dir(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_direct_directory_path_uses_codex_cmd_inside() {
|
||||
let dir = std::env::temp_dir().join(format!("dbx-codex-direct-dir-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let extensionless = dir.join("codex");
|
||||
let cmd = dir.join("codex.cmd");
|
||||
std::fs::write(&extensionless, "#!/bin/sh\n").unwrap();
|
||||
std::fs::write(&cmd, "@echo off\n").unwrap();
|
||||
|
||||
let resolved = direct_program_path(dir.to_string_lossy().as_ref()).unwrap();
|
||||
|
||||
assert_eq!(resolved, cmd.to_string_lossy().as_ref());
|
||||
let _ = std::fs::remove_file(extensionless);
|
||||
let _ = std::fs::remove_file(cmd);
|
||||
let _ = std::fs::remove_dir(dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(windows)]
|
||||
async fn windows_configured_nvm_directory_resolves_codex_cmd_shim() {
|
||||
let dir = std::env::temp_dir().join(format!("dbx-codex-nvm-dir-test-{}", std::process::id()));
|
||||
let bin_dir = dir.join("node_modules").join("@openai").join("codex").join("bin");
|
||||
std::fs::create_dir_all(&bin_dir).unwrap();
|
||||
let cmd = dir.join("codex.cmd");
|
||||
let node = dir.join("node.exe");
|
||||
let codex_js = bin_dir.join("codex.js");
|
||||
std::fs::write(&cmd, "@echo off\nnode codex.js %*\n").unwrap();
|
||||
std::fs::write(&node, "").unwrap();
|
||||
std::fs::write(&codex_js, "#!/usr/bin/env node\n").unwrap();
|
||||
|
||||
let mut config = codex_config("default");
|
||||
config.codex_cli_path = Some(dir.to_string_lossy().to_string());
|
||||
|
||||
let command = resolve_codex_command(&config).await;
|
||||
|
||||
assert_eq!(command.program, node.to_string_lossy().as_ref());
|
||||
assert_eq!(command.args, vec![codex_js.to_string_lossy().to_string()]);
|
||||
let _ = std::fs::remove_file(cmd);
|
||||
let _ = std::fs::remove_file(node);
|
||||
let _ = std::fs::remove_file(codex_js);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_npm_codex_cmd_shim_uses_node_directly() {
|
||||
|
|
@ -844,6 +911,19 @@ mod tests {
|
|||
assert!(err.contains("environment variables section"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_directory_without_codex_in_codex_cli_path() {
|
||||
let dir = std::env::temp_dir().join(format!("dbx-codex-empty-dir-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut config = codex_config("default");
|
||||
config.codex_cli_path = Some(dir.to_string_lossy().to_string());
|
||||
|
||||
let err = validate_codex_program(&config).unwrap_err();
|
||||
|
||||
assert!(err.contains("[codexCliPathInvalid]"));
|
||||
let _ = std::fs::remove_dir(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_codex_model_catalog_without_service_tiers() {
|
||||
let models = parse_codex_models(
|
||||
|
|
|
|||
Loading…
Reference in New Issue