From cc78d4f528b37d732bcc1fca1ce3543f60b19843 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 21 Aug 2026 04:33:32 +0000 Subject: [PATCH] fix(ai): include Node runtime in Codex CLI path Closes #6849 --- crates/dbx-core/src/ai_codex_cli.rs | 92 ++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/crates/dbx-core/src/ai_codex_cli.rs b/crates/dbx-core/src/ai_codex_cli.rs index d5a04faf26..f0eeac59b2 100644 --- a/crates/dbx-core/src/ai_codex_cli.rs +++ b/crates/dbx-core/src/ai_codex_cli.rs @@ -122,30 +122,52 @@ fn windows_npm_codex_shim_command(program: &str) -> Option { Some(CodexCommandSpec { program: node, args: vec![codex_js.to_string_lossy().to_string()] }) } -fn codex_process_env(config: &AiConfig, command: &CodexCommandSpec) -> Result, String> { +async fn codex_process_env(config: &AiConfig, command: &CodexCommandSpec) -> Result, String> { let inherited_env_keys = env::vars_os().map(|(key, _)| key.to_string_lossy().into_owned()).collect::>(); - codex_process_env_with_system_proxy( + #[cfg(not(windows))] + let node_runtime_dir = shell_program_path("node").await.and_then(|path| { + Path::new(&path).parent().filter(|parent| !parent.as_os_str().is_empty()).map(|parent| parent.to_path_buf()) + }); + #[cfg(windows)] + let node_runtime_dir: Option = None; + codex_process_env_with_runtime_and_system_proxy( config, command, + node_runtime_dir.as_deref(), crate::update::system_proxy_url().as_deref(), &inherited_env_keys, ) } +#[cfg(test)] fn codex_process_env_with_system_proxy( config: &AiConfig, command: &CodexCommandSpec, system_proxy: Option<&str>, inherited_env_keys: &[String], +) -> Result, String> { + codex_process_env_with_runtime_and_system_proxy(config, command, None, system_proxy, inherited_env_keys) +} + +fn codex_process_env_with_runtime_and_system_proxy( + config: &AiConfig, + command: &CodexCommandSpec, + node_runtime_dir: Option<&Path>, + system_proxy: Option<&str>, + inherited_env_keys: &[String], ) -> Result, String> { let mut env = BTreeMap::from_iter(codex_cli_env(config)?); if let Some(proxy) = system_proxy.filter(|proxy| !proxy.trim().is_empty()) { insert_env_if_absent(&mut env, inherited_env_keys, "HTTP_PROXY", proxy); insert_env_if_absent(&mut env, inherited_env_keys, "HTTPS_PROXY", proxy); } - if let Some(dir) = command.parent_dir() { + let command_dir = command.parent_dir(); + if command_dir.is_some() || node_runtime_dir.is_some() { let user_path = env.get("PATH").map(String::as_str); - env.insert("PATH".to_string(), merged_path_with_dir(&dir, user_path)); + env.insert( + "PATH".to_string(), + merged_codex_path(command_dir.as_deref().map(Path::new), node_runtime_dir, user_path), + ); } Ok(env.into_iter().collect()) } @@ -342,9 +364,20 @@ fn common_executable_dirs() -> Vec { dirs } +#[cfg(test)] fn merged_path_with_dir(dir: &str, user_path: Option<&str>) -> String { + merged_codex_path(Some(Path::new(dir)), None, user_path) +} + +fn merged_codex_path(codex_dir: Option<&Path>, node_runtime_dir: Option<&Path>, user_path: Option<&str>) -> String { let mut seen = BTreeSet::new(); - let mut dirs = vec![PathBuf::from(dir)]; + let mut dirs = Vec::new(); + if let Some(dir) = codex_dir { + dirs.push(dir.to_path_buf()); + } + if let Some(dir) = node_runtime_dir { + dirs.push(dir.to_path_buf()); + } if let Some(path) = user_path { dirs.extend(env::split_paths(path)); } @@ -572,7 +605,7 @@ fn attach_codex_images(command: &mut CodexCommandSpec, image_paths: &[PathBuf]) pub async fn list_codex_models(config: &AiConfig) -> Result, String> { validate_codex_program(config)?; let command = resolve_codex_command(config).await; - let process_env = codex_process_env(config, &command)?; + let process_env = codex_process_env(config, &command).await?; let cache_key = codex_model_cache_key(&command, &process_env); Ok(list_codex_models_cached(&CODEX_MODEL_CACHE, cache_key, || async { list_codex_models_uncached(&command, &process_env).await @@ -946,7 +979,8 @@ pub async fn test_codex_connection(config: &AiConfig) -> Result>(); - assert_eq!(dirs.first().unwrap(), std::path::Path::new("/opt/homebrew/bin")); + assert_eq!( + &dirs[..3], + [ + std::path::PathBuf::from("/Users/test/Library/pnpm"), + node_runtime_dir.to_path_buf(), + std::path::PathBuf::from("/custom/bin"), + ] + ); + assert_eq!(dirs.iter().filter(|dir| dir.as_path() == node_runtime_dir).count(), 1); assert!(dirs.iter().any(|dir| dir == std::path::Path::new("/usr/bin"))); } @@ -1389,17 +1439,27 @@ mod tests { #[test] #[cfg(not(windows))] - fn codex_process_env_keeps_resolved_program_dir_before_user_path() { + fn codex_process_env_keeps_old_path_behavior_when_node_is_missing() { let mut config = codex_config("default"); config.codex_cli_env.insert("PATH".to_string(), "/custom/bin:/usr/bin".to_string()); let command = CliAgentCommandSpec { program: "/opt/homebrew/bin/codex".to_string(), args: Vec::new() }; - let env = codex_process_env(&config, &command).unwrap(); + let env = codex_process_env_with_runtime_and_system_proxy(&config, &command, None, None, &[]).unwrap(); let path = env.iter().find(|(key, _)| key == "PATH").map(|(_, value)| value).unwrap(); let dirs = std::env::split_paths(path).collect::>(); assert_eq!(dirs.first().unwrap(), std::path::Path::new("/opt/homebrew/bin")); assert!(dirs.iter().any(|dir| dir == std::path::Path::new("/custom/bin"))); + assert_eq!(path, &merged_path_with_dir("/opt/homebrew/bin", Some("/custom/bin:/usr/bin"))); + } + + #[test] + #[cfg(not(windows))] + fn native_codex_binary_command_remains_direct() { + let command = codex_command_for_program("/Applications/DBX.app/Contents/MacOS/codex".to_string()); + + assert_eq!(command.program, "/Applications/DBX.app/Contents/MacOS/codex"); + assert!(command.args.is_empty()); } #[test]