From 17f4ee88e8757add7a61b4f2a54887f9d939b6b9 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Thu, 16 Apr 2026 12:48:21 +0800 Subject: [PATCH] fix(process): use semver-aware sorting for Node.js version selection Replace lexicographic sort with numeric (major, minor, patch) sort across all 7 version-directory sort sites in find_node_bin_dir. Lexicographic sort incorrectly orders v20.9 > v20.11 (because '9' > '1') which could select an older Node.js version and cause agent preflight failures for version-gated agents like OpenClaw (requires >=22.12.0). --- src-tauri/src/process.rs | 41 +++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/process.rs b/src-tauri/src/process.rs index 8ee76fe..e58bfd8 100644 --- a/src-tauri/src/process.rs +++ b/src-tauri/src/process.rs @@ -177,6 +177,24 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { let node_bin = if cfg!(windows) { "node.exe" } else { "node" }; + /// Extract a (major, minor, patch) tuple from a version directory name + /// like `v20.11.1` or `20.11.1` for correct numeric sorting. + /// Falls back to (0,0,0) for unparseable names so they sort last. + fn semver_key(path: &std::path::Path) -> (u32, u32, u32) { + let name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .trim_start_matches('v') + .to_string(); + let mut parts = name.split('.').filter_map(|s| s.parse::().ok()); + ( + parts.next().unwrap_or(0), + parts.next().unwrap_or(0), + parts.next().unwrap_or(0), + ) + } + /// Try each `(env_var, suffix_segments)` in order; return as soon as one /// env var is set. If none match, fall back to `home / home_relative`. /// Returns `None` when no env var is set **and** `home` is `None` — @@ -223,13 +241,13 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { let stripped = name.trim_start_matches('v'); stripped.starts_with(alias_stripped) }) - .map(|e| e.path().join("bin")) + .map(|e| e.path()) .collect(); if !matched.is_empty() { - matched.sort(); + matched.sort_by_key(|p| semver_key(p)); matched.reverse(); alias_matched = true; - candidates.append(&mut matched); + candidates.extend(matched.into_iter().map(|p| p.join("bin"))); } } } @@ -237,13 +255,11 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { // Fall back: all installed versions, newest first. // Skipped when alias resolution already produced candidates. - // NOTE: lexicographic sort is imperfect for semver (v8 > v18), - // but acceptable for a best-effort heuristic. if !alias_matched { if let Ok(mut entries) = std::fs::read_dir(&versions_dir) .map(|rd| rd.flatten().map(|e| e.path()).collect::>()) { - entries.sort(); + entries.sort_by_key(|p| semver_key(p)); entries.reverse(); for entry in entries { candidates.push(entry.join("bin")); @@ -267,8 +283,7 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { } } - // All installed versions, newest first (lexicographic — see note - // in the nvm section about semver edge cases). + // All installed versions, newest first. if let Some(nvm_home) = resolve_dir(&[("NVM_HOME", &[]), ("APPDATA", &["nvm"])], None, &[]) { @@ -280,7 +295,7 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { .map(|e| e.path()) .collect::>() }) { - entries.sort(); + entries.sort_by_key(|p| semver_key(p)); entries.reverse(); // nvm-windows places node.exe directly in the version dir candidates.extend(entries); @@ -320,7 +335,7 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { if let Ok(mut entries) = std::fs::read_dir(&fnm_versions) .map(|rd| rd.flatten().map(|e| e.path()).collect::>()) { - entries.sort(); + entries.sort_by_key(|p| semver_key(p)); entries.reverse(); for entry in entries { let installation = entry.join("installation"); @@ -363,7 +378,7 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { if let Ok(mut entries) = std::fs::read_dir(&asdf_nodejs) .map(|rd| rd.flatten().map(|e| e.path()).collect::>()) { - entries.sort(); + entries.sort_by_key(|p| semver_key(p)); entries.reverse(); for entry in entries { candidates.push(entry.join("bin")); @@ -391,7 +406,7 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { if let Ok(mut entries) = std::fs::read_dir(&mise_node) .map(|rd| rd.flatten().map(|e| e.path()).collect::>()) { - entries.sort(); + entries.sort_by_key(|p| semver_key(p)); entries.reverse(); for entry in entries { // mise on Unix places binaries under /bin/; @@ -415,7 +430,7 @@ fn find_node_bin_dir(home: Option<&std::path::Path>) -> Option { if let Ok(mut entries) = std::fs::read_dir(&n_versions) .map(|rd| rd.flatten().map(|e| e.path()).collect::>()) { - entries.sort(); + entries.sort_by_key(|p| semver_key(p)); entries.reverse(); for entry in entries { candidates.push(entry.join("bin"));