@@ -1147,30 +1147,135 @@ fn node_current_dir(base_dir: &str) -> std::path::PathBuf {
11471147 node_root_dir ( base_dir) . join ( "current" )
11481148}
11491149
1150- fn detect_active_node_version_internal ( base_dir : & str ) -> Result < Option < String > , String > {
1151- let current_exe = node_current_dir ( base_dir) . join ( "node.exe" ) ;
1152- if !current_exe. exists ( ) {
1153- return Ok ( None ) ;
1150+ #[ cfg( target_os = "windows" ) ]
1151+ fn node_search_paths_from_path ( path_value : & str ) -> Vec < String > {
1152+ path_value
1153+ . split ( ';' )
1154+ . map ( |part| part. trim ( ) )
1155+ . filter ( |part| !part. is_empty ( ) )
1156+ . map ( |part| part. trim_end_matches ( '\\' ) . replace ( "/" , "\\ " ) )
1157+ . filter_map ( |dir| {
1158+ let candidate = Path :: new ( & dir) . join ( "node.exe" ) ;
1159+ if candidate. exists ( ) {
1160+ Some ( candidate. to_string_lossy ( ) . replace ( "/" , "\\ " ) )
1161+ } else {
1162+ None
1163+ }
1164+ } )
1165+ . collect ( )
1166+ }
1167+
1168+ #[ cfg( target_os = "windows" ) ]
1169+ fn read_user_path_value ( ) -> Result < String , String > {
1170+ use winreg:: RegKey ;
1171+ use winreg:: enums:: HKEY_CURRENT_USER ;
1172+
1173+ let hkcu = RegKey :: predef ( HKEY_CURRENT_USER ) ;
1174+ let env = hkcu
1175+ . open_subkey ( "Environment" )
1176+ . map_err ( |e| format ! ( "Failed to open HKCU\\ Environment: {}" , e) ) ?;
1177+
1178+ match env. get_value :: < String , _ > ( "Path" ) {
1179+ Ok ( path) => Ok ( path) ,
1180+ Err ( _) => Ok ( String :: new ( ) ) ,
11541181 }
1182+ }
1183+
1184+ #[ cfg( target_os = "windows" ) ]
1185+ fn write_user_path_value ( path_value : & str ) -> Result < ( ) , String > {
1186+ use winreg:: RegKey ;
1187+ use winreg:: enums:: HKEY_CURRENT_USER ;
1188+
1189+ let hkcu = RegKey :: predef ( HKEY_CURRENT_USER ) ;
1190+ let ( env, _) = hkcu
1191+ . create_subkey ( "Environment" )
1192+ . map_err ( |e| format ! ( "Failed to open HKCU\\ Environment for writing: {}" , e) ) ?;
1193+ env. set_value ( "Path" , & path_value)
1194+ . map_err ( |e| format ! ( "Failed to write User PATH: {}" , e) )
1195+ }
11551196
1156- let output = std:: process:: Command :: new ( & current_exe)
1157- . arg ( "-v" )
1197+ #[ cfg( target_os = "windows" ) ]
1198+ fn create_junction ( link : & Path , target : & Path ) -> Result < ( ) , String > {
1199+ use std:: os:: windows:: process:: CommandExt ;
1200+ const CREATE_NO_WINDOW : u32 = 0x08000000 ;
1201+
1202+ let link_str = link. to_string_lossy ( ) . replace ( "/" , "\\ " ) ;
1203+ let target_str = target. to_string_lossy ( ) . replace ( "/" , "\\ " ) ;
1204+ let output = Command :: new ( "cmd" )
1205+ . args ( [ "/C" , "mklink" , "/J" , & link_str, & target_str] )
1206+ . creation_flags ( CREATE_NO_WINDOW )
11581207 . output ( )
1159- . map_err ( |e| format ! ( "Failed to query active Node version: {}" , e) ) ?;
1208+ . map_err ( |e| format ! ( "Failed to create Node junction: {}" , e) ) ?;
1209+
1210+ if output. status . success ( ) {
1211+ Ok ( ( ) )
1212+ } else {
1213+ let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
1214+ Err ( if stderr. is_empty ( ) {
1215+ "Failed to create Node junction" . into ( )
1216+ } else {
1217+ stderr
1218+ } )
1219+ }
1220+ }
11601221
1161- if !output. status . success ( ) {
1222+ #[ cfg( target_os = "windows" ) ]
1223+ fn remove_junction ( link : & Path ) -> Result < ( ) , String > {
1224+ use std:: os:: windows:: process:: CommandExt ;
1225+ const CREATE_NO_WINDOW : u32 = 0x08000000 ;
1226+
1227+ if !link. exists ( ) {
1228+ return Ok ( ( ) ) ;
1229+ }
1230+
1231+ let link_str = link. to_string_lossy ( ) . replace ( "/" , "\\ " ) ;
1232+ let output = Command :: new ( "cmd" )
1233+ . args ( [ "/C" , "rmdir" , & link_str] )
1234+ . creation_flags ( CREATE_NO_WINDOW )
1235+ . output ( )
1236+ . map_err ( |e| format ! ( "Failed to remove Node junction: {}" , e) ) ?;
1237+
1238+ if output. status . success ( ) {
1239+ Ok ( ( ) )
1240+ } else {
1241+ let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
1242+ Err ( if stderr. is_empty ( ) {
1243+ "Failed to remove Node junction" . into ( )
1244+ } else {
1245+ stderr
1246+ } )
1247+ }
1248+ }
1249+
1250+ fn detect_active_node_version_internal ( base_dir : & str ) -> Result < Option < String > , String > {
1251+ let current_dir = node_current_dir ( base_dir) ;
1252+ if !current_dir. exists ( ) {
11621253 return Ok ( None ) ;
11631254 }
11641255
1165- let version = String :: from_utf8_lossy ( & output. stdout )
1166- . trim ( )
1167- . trim_start_matches ( 'v' )
1168- . to_string ( ) ;
1256+ #[ cfg( target_os = "windows" ) ]
1257+ {
1258+ let target = fs:: canonicalize ( & current_dir)
1259+ . map_err ( |e| format ! ( "Failed to resolve active Node version: {}" , e) ) ?
1260+ . to_string_lossy ( )
1261+ . replace ( "/" , "\\ " ) ;
1262+
1263+ let folder_name = Path :: new ( & target)
1264+ . file_name ( )
1265+ . map ( |value| value. to_string_lossy ( ) . to_string ( ) )
1266+ . unwrap_or_default ( ) ;
1267+
1268+ if let Some ( version) = folder_name. strip_prefix ( "node-v" ) {
1269+ if !version. is_empty ( ) {
1270+ return Ok ( Some ( version. to_string ( ) ) ) ;
1271+ }
1272+ }
11691273
1170- if version. is_empty ( ) {
11711274 Ok ( None )
1172- } else {
1173- Ok ( Some ( version) )
1275+ }
1276+ #[ cfg( not( target_os = "windows" ) ) ]
1277+ {
1278+ Ok ( None )
11741279 }
11751280}
11761281
@@ -1218,48 +1323,20 @@ fn list_node_versions(base_dir: String) -> Result<Vec<NodeVersionState>, String>
12181323fn activate_node_version ( base_dir : String , version : String ) -> Result < String , String > {
12191324 #[ cfg( target_os = "windows" ) ]
12201325 {
1221- let normalized = normalize_node_version_tag ( & version) ?;
1222- let root = node_root_dir ( & base_dir) ;
1223- fs:: create_dir_all ( & root) . map_err ( |e| e. to_string ( ) ) ?;
1224-
1225- let target_dir = node_version_dir ( & base_dir, & normalized) ;
1226- if !target_dir. join ( "node.exe" ) . exists ( ) {
1227- return Err ( format ! ( "Node {} is not installed" , normalized) ) ;
1228- }
1229-
1230- let current_dir = node_current_dir ( & base_dir) ;
1231- let script = format ! (
1232- r#"
1233- $current = '{current}'
1234- $target = '{target}'
1235-
1236- if (Test-Path -LiteralPath $current) {{
1237- $item = Get-Item -LiteralPath $current -Force
1238- if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {{
1239- cmd /c rmdir "$current" | Out-Null
1240- }} else {{
1241- Remove-Item -LiteralPath $current -Force -Recurse
1242- }}
1243- }}
1326+ let normalized = normalize_node_version_tag ( & version) ?;
1327+ let root = node_root_dir ( & base_dir) ;
1328+ fs:: create_dir_all ( & root) . map_err ( |e| e. to_string ( ) ) ?;
12441329
1245- New-Item -ItemType Junction -Path $current -Target $target | Out-Null
1246- Write-Output $current
1247- "# ,
1248- current = current_dir. to_string_lossy( ) . replace( '\'' , "''" ) ,
1249- target = target_dir. to_string_lossy( ) . replace( '\'' , "''" ) ,
1250- ) ;
1330+ let target_dir = node_version_dir ( & base_dir, & normalized) ;
1331+ if !target_dir. join ( "node.exe" ) . exists ( ) {
1332+ return Err ( format ! ( "Node {} is not installed" , normalized) ) ;
1333+ }
12511334
1252- let output = run_hidden_powershell ( & script) ?;
1253- if !output. status . success ( ) {
1254- let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
1255- return Err ( if stderr. is_empty ( ) {
1256- format ! ( "Failed to activate Node {}" , normalized)
1257- } else {
1258- stderr
1259- } ) ;
1260- }
1335+ let current_dir = node_current_dir ( & base_dir) ;
1336+ remove_junction ( & current_dir) ?;
1337+ create_junction ( & current_dir, & target_dir) ?;
12611338
1262- Ok ( current_dir. to_string_lossy ( ) . replace ( "\\ " , "/" ) )
1339+ Ok ( current_dir. to_string_lossy ( ) . replace ( "\\ " , "/" ) )
12631340 }
12641341 #[ cfg( not( target_os = "windows" ) ) ]
12651342 {
@@ -1274,37 +1351,7 @@ fn deactivate_node_version(base_dir: String) -> Result<(), String> {
12741351 #[ cfg( target_os = "windows" ) ]
12751352 {
12761353 let current_dir = node_current_dir ( & base_dir) ;
1277- if !current_dir. exists ( ) {
1278- return Ok ( ( ) ) ;
1279- }
1280-
1281- let script = format ! (
1282- r#"
1283- $current = '{current}'
1284-
1285- if (Test-Path -LiteralPath $current) {{
1286- $item = Get-Item -LiteralPath $current -Force
1287- if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {{
1288- cmd /c rmdir "$current" | Out-Null
1289- }} else {{
1290- Remove-Item -LiteralPath $current -Force -Recurse
1291- }}
1292- }}
1293- "# ,
1294- current = current_dir. to_string_lossy( ) . replace( '\'' , "''" ) ,
1295- ) ;
1296-
1297- let output = run_hidden_powershell ( & script) ?;
1298- if output. status . success ( ) {
1299- Ok ( ( ) )
1300- } else {
1301- let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
1302- Err ( if stderr. is_empty ( ) {
1303- "Failed to deactivate current Node version" . into ( )
1304- } else {
1305- stderr
1306- } )
1307- }
1354+ remove_junction ( & current_dir)
13081355 }
13091356 #[ cfg( not( target_os = "windows" ) ) ]
13101357 {
@@ -1330,25 +1377,8 @@ fn get_node_path_status(base_dir: String) -> Result<NodePathStatus, String> {
13301377 . replace ( "/" , "\\ " )
13311378 . trim_end_matches ( '\\' )
13321379 . to_string ( ) ;
1333-
1334- let output = run_hidden_powershell (
1335- r#"
1336- $cmd = Get-Command node -All -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -Unique
1337- if ($cmd) {
1338- $cmd | ForEach-Object { Write-Output $_ }
1339- }
1340- "# ,
1341- ) ?;
1342-
1343- if !output. status . success ( ) {
1344- return Err ( String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ) ;
1345- }
1346-
1347- let paths = String :: from_utf8_lossy ( & output. stdout )
1348- . lines ( )
1349- . map ( |line| line. trim ( ) . replace ( "/" , "\\ " ) )
1350- . filter ( |line| !line. is_empty ( ) )
1351- . collect :: < Vec < _ > > ( ) ;
1380+ let process_path = std:: env:: var ( "PATH" ) . unwrap_or_default ( ) ;
1381+ let paths = node_search_paths_from_path ( & process_path) ;
13521382
13531383 let current_node_path = paths. first ( ) . cloned ( ) ;
13541384 let devstack_prefix = format ! ( "{}\\ node.exe" , devstack_current) ;
@@ -1357,7 +1387,7 @@ if ($cmd) {
13571387 . map ( |path| path. eq_ignore_ascii_case ( & devstack_prefix) )
13581388 . unwrap_or ( false ) ;
13591389
1360- let user_path = std :: env :: var ( "PATH" ) . unwrap_or_default ( ) ;
1390+ let user_path = read_user_path_value ( ) ? ;
13611391 let user_path_contains_devstack = user_path
13621392 . split ( ';' )
13631393 . map ( |part| part. trim ( ) . replace ( "/" , "\\ " ) . trim_end_matches ( '\\' ) . to_string ( ) )
@@ -1391,43 +1421,20 @@ fn set_node_global_path(base_dir: String) -> Result<(), String> {
13911421 . replace ( "/" , "\\ " )
13921422 . trim_end_matches ( '\\' )
13931423 . to_string ( ) ;
1394- let escaped = target. replace ( '\'' , "''" ) ;
1395-
1396- let script = format ! (
1397- r#"
1398- $target = '{target}'
1399- $normalizedTarget = $target.ToLowerInvariant().TrimEnd('\')
1400- $existing = [Environment]::GetEnvironmentVariable('Path', 'User')
1401- $parts = @()
1402-
1403- if ($existing) {{
1404- $parts = $existing -split ';' | Where-Object {{
1405- $_ -and ($_.Trim() -ne '')
1406- }} | ForEach-Object {{
1407- $_.Trim()
1408- }} | Where-Object {{
1409- ($_.ToLowerInvariant().Replace('/','\').TrimEnd('\')) -ne $normalizedTarget
1410- }}
1411- }}
1412-
1413- $newParts = @($target) + $parts
1414- $newPath = ($newParts -join ';').Trim(';')
1415- [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
1416- "# ,
1417- target = escaped
1418- ) ;
1424+ let normalized_target = target. to_lowercase ( ) ;
1425+ let existing = read_user_path_value ( ) ?;
1426+ let parts = existing
1427+ . split ( ';' )
1428+ . map ( |part| part. trim ( ) )
1429+ . filter ( |part| !part. is_empty ( ) )
1430+ . map ( |part| part. replace ( "/" , "\\ " ) . trim_end_matches ( '\\' ) . to_string ( ) )
1431+ . filter ( |part| part. to_lowercase ( ) != normalized_target)
1432+ . collect :: < Vec < _ > > ( ) ;
14191433
1420- let output = run_hidden_powershell ( & script) ?;
1421- if output. status . success ( ) {
1422- Ok ( ( ) )
1423- } else {
1424- let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
1425- Err ( if stderr. is_empty ( ) {
1426- "Failed to update User PATH for Node.js" . into ( )
1427- } else {
1428- stderr
1429- } )
1430- }
1434+ let mut new_parts = vec ! [ target] ;
1435+ new_parts. extend ( parts) ;
1436+ let new_path = new_parts. join ( ";" ) ;
1437+ write_user_path_value ( & new_path)
14311438 }
14321439 #[ cfg( not( target_os = "windows" ) ) ]
14331440 {
0 commit comments