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:
zipg 2026-06-27 23:54:29 +08:00 committed by GitHub
parent c925f9a9de
commit d65f656704
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 84 additions and 4 deletions

View File

@ -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(