From 9884042c40ece19840d4166f69be0ed2392651ca Mon Sep 17 00:00:00 2001 From: humanwritten <206531610+humanwritten@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:38:40 +0100 Subject: [PATCH] fix: NVM Node.js PATH issue when opcode launched from /Applications Problem: When opcode is launched from /Applications on macOS, Claude commands fail with 'env: node: No such file or directory' for npm-local Claude installations that depend on Node.js from NVM. Root cause: GUI-launched apps receive minimal PATH that lacks NVM directories, and existing code only added NVM paths when Claude itself was in NVM. Solution: - Added NVM detection in claude_binary.rs for when Claude needs Node from NVM - Fixed unused _std_cmd variable in commands/claude.rs to actually use enhanced PATH - Added targeted override that only applies when Claude is NOT in NVM but needs it This is a minimal, targeted fix with zero impact on other installation types. Tested and confirmed working on macOS where the edge case was occurring. Not tested on other environments or operating systems. --- src-tauri/src/claude_binary.rs | 31 +++++++++++++++++++++++++++++++ src-tauri/src/commands/claude.rs | 15 ++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 2d1c7e36..2ea43ac7 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -502,6 +502,37 @@ pub fn create_command_with_env(program: &str) -> Command { } } } + // Also add NVM Node.js when Claude isn't in NVM but needs Node.js + else { + let current_path = std::env::var("PATH").unwrap_or_default(); + if !current_path.contains("/.nvm/versions/node/") { + if let Ok(home) = std::env::var("HOME") { + let nvm_base = PathBuf::from(&home).join(".nvm").join("versions").join("node"); + if nvm_base.exists() { + if let Ok(entries) = std::fs::read_dir(&nvm_base) { + let mut node_versions: Vec = entries + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().map(|t| t.is_dir()).unwrap_or(false)) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .collect(); + + // Sort to get latest version (simple string sort works for NVM format) + node_versions.sort_by(|a, b| b.cmp(a)); + + if let Some(latest_version) = node_versions.first() { + let node_bin_path = nvm_base.join(latest_version).join("bin"); + if node_bin_path.exists() { + let node_bin_str = node_bin_path.to_string_lossy(); + let new_path = format!("{}:{}", node_bin_str, current_path); + debug!("Adding NVM Node.js to PATH for Claude execution: {}", node_bin_str); + cmd.env("PATH", new_path); + } + } + } + } + } + } + } // Add Homebrew support if the program is in a Homebrew directory if program.contains("/homebrew/") || program.contains("/opt/homebrew/") { diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index 94ad3c55..ad845ee0 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -226,7 +226,7 @@ fn extract_first_user_message(jsonl_path: &PathBuf) -> (Option, Option Command { // Convert std::process::Command to tokio::process::Command - let _std_cmd = crate::claude_binary::create_command_with_env(program); + let std_cmd = crate::claude_binary::create_command_with_env(program); // Create a new tokio Command from the program path let mut tokio_cmd = Command::new(program); @@ -275,6 +275,19 @@ fn create_command_with_env(program: &str) -> Command { } } } + + // Only use enhanced PATH if Claude is NOT in NVM but needs Node from NVM (our specific fix) + if !program.contains("/.nvm/versions/node/") { + if let Some(enhanced_path) = std_cmd.get_envs().find_map(|(k, v)| { + if k == "PATH" { v.and_then(|val| val.to_str().map(String::from)) } else { None } + }) { + // Only override if the enhanced PATH added NVM paths + if enhanced_path.contains("/.nvm/versions/node/") { + log::debug!("Claude not in NVM but needs Node - using enhanced PATH with NVM: {}", enhanced_path); + tokio_cmd.env("PATH", enhanced_path); + } + } + } tokio_cmd }