@@ -200,8 +200,167 @@ private string BuildSearchArguments(string query) =>
200200 + CoreTools . EscapePowerShellSingleQuoted ( query )
201201 + " --json" ;
202202
203- public override IReadOnlyList < string > FindCandidateExecutableFiles ( ) =>
204- CoreTools . WhichMultiple ( OperatingSystem . IsWindows ( ) ? "npm.cmd" : "npm" ) ;
203+ public override IReadOnlyList < string > FindCandidateExecutableFiles ( )
204+ {
205+ if ( OperatingSystem . IsWindows ( ) )
206+ return CoreTools . WhichMultiple ( "npm.cmd" ) ;
207+
208+ return CoreTools . WhichMultiple ( "npm" )
209+ . Concat (
210+ GetNvmBinDirectories (
211+ Environment . GetEnvironmentVariable ,
212+ Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile )
213+ )
214+ . Select ( directory => Path . Join ( directory , "npm" ) )
215+ . Where ( IsExecutableFile )
216+ )
217+ . Distinct ( StringComparer . Ordinal )
218+ . ToArray ( ) ;
219+ }
220+
221+ internal static IReadOnlyList < string > GetNvmBinDirectories (
222+ Func < string , string ? > readEnvironmentVariable ,
223+ string userProfileDirectory
224+ )
225+ {
226+ string nvmDirectory ;
227+ if ( readEnvironmentVariable ( "NVM_DIR" ) ? . Trim ( ) is { Length : > 0 } configured )
228+ nvmDirectory = configured ;
229+ else if ( userProfileDirectory . Trim ( ) is { Length : > 0 } userProfile )
230+ nvmDirectory = Path . Join ( userProfile , ".nvm" ) ;
231+ else
232+ return [ ] ;
233+
234+ List < ( Version Version , string Directory ) > installed = [ ] ;
235+ try
236+ {
237+ string versionsDirectory = Path . Join ( nvmDirectory , "versions" , "node" ) ;
238+ if ( ! Directory . Exists ( versionsDirectory ) )
239+ return [ ] ;
240+
241+ foreach ( string directory in Directory . EnumerateDirectories ( versionsDirectory ) )
242+ {
243+ string name = Path . GetFileName ( directory ) ;
244+ if ( name . StartsWith ( 'v' ) && Version . TryParse ( name [ 1 .. ] , out Version ? version ) )
245+ installed . Add ( ( version , directory ) ) ;
246+ }
247+ }
248+ catch ( IOException )
249+ {
250+ return [ ] ;
251+ }
252+ catch ( UnauthorizedAccessException )
253+ {
254+ return [ ] ;
255+ }
256+
257+ installed . Sort ( ( a , b ) => b . Version . CompareTo ( a . Version ) ) ;
258+
259+ if ( ResolveNvmAlias ( nvmDirectory , "default" ) is { } defaultVersion )
260+ {
261+ int index = installed . FindIndex ( entry => MatchesNvmVersion ( entry . Version , defaultVersion ) ) ;
262+ if ( index > 0 )
263+ {
264+ var entry = installed [ index ] ;
265+ installed . RemoveAt ( index ) ;
266+ installed . Insert ( 0 , entry ) ;
267+ }
268+ }
269+
270+ return installed . Select ( entry => Path . Join ( entry . Directory , "bin" ) ) . ToArray ( ) ;
271+ }
272+
273+ private static string ? ResolveNvmAlias ( string nvmDirectory , string alias )
274+ {
275+ for ( int hop = 0 ; hop < 8 ; hop ++ )
276+ {
277+ string value ;
278+ try
279+ {
280+ string aliasFile = Path . Join ( nvmDirectory , "alias" , alias ) ;
281+ if ( ! File . Exists ( aliasFile ) )
282+ return null ;
283+ value = File . ReadAllText ( aliasFile ) . Trim ( ) ;
284+ }
285+ catch ( IOException )
286+ {
287+ return null ;
288+ }
289+ catch ( UnauthorizedAccessException )
290+ {
291+ return null ;
292+ }
293+
294+ if ( value . Length is 0 || value is "node" or "stable" )
295+ return null ;
296+ if ( IsNvmVersionSpecification ( value ) )
297+ return value ;
298+ alias = value ;
299+ }
300+
301+ return null ;
302+ }
303+
304+ private static bool IsNvmVersionSpecification ( string value )
305+ {
306+ string [ ] parts = ( value . StartsWith ( 'v' ) ? value [ 1 .. ] : value ) . Split ( '.' ) ;
307+ return parts . Length is >= 1 and <= 3
308+ && parts . All ( part => part . Length > 0 && part . All ( char . IsAsciiDigit ) ) ;
309+ }
310+
311+ private static bool MatchesNvmVersion ( Version version , string specification )
312+ {
313+ string [ ] parts = specification . TrimStart ( 'v' ) . Split ( '.' ) ;
314+ int [ ] components = [ version . Major , version . Minor , version . Build ] ;
315+ if ( parts . Length > components . Length )
316+ return false ;
317+
318+ for ( int i = 0 ; i < parts . Length ; i ++ )
319+ {
320+ if ( ! int . TryParse ( parts [ i ] , out int value ) || value != components [ i ] )
321+ return false ;
322+ }
323+
324+ return true ;
325+ }
326+
327+ private static bool IsExecutableFile ( string path )
328+ {
329+ if ( ! File . Exists ( path ) )
330+ return false ;
331+
332+ if ( OperatingSystem . IsWindows ( ) )
333+ return true ;
334+
335+ const UnixFileMode ExecutableBits =
336+ UnixFileMode . UserExecute | UnixFileMode . GroupExecute | UnixFileMode . OtherExecute ;
337+
338+ try
339+ {
340+ return ( File . GetUnixFileMode ( path ) & ExecutableBits ) is not 0 ;
341+ }
342+ catch ( IOException )
343+ {
344+ return false ;
345+ }
346+ catch ( UnauthorizedAccessException )
347+ {
348+ return false ;
349+ }
350+ }
351+
352+ internal static string AppendDirectoryToPath ( string ? currentPath , string directory )
353+ {
354+ if ( string . IsNullOrEmpty ( currentPath ) )
355+ return directory ;
356+
357+ string trimmedDirectory = Path . TrimEndingDirectorySeparator ( directory ) ;
358+ bool alreadyPresent = currentPath
359+ . Split ( Path . PathSeparator , StringSplitOptions . RemoveEmptyEntries )
360+ . Any ( entry => Path . TrimEndingDirectorySeparator ( entry ) == trimmedDirectory ) ;
361+
362+ return alreadyPresent ? currentPath : currentPath + Path . PathSeparator + directory ;
363+ }
205364
206365 protected override void _loadManagerExecutableFile (
207366 out bool found ,
@@ -221,6 +380,17 @@ out string callArguments
221380 return ;
222381 }
223382
383+ if ( found && Path . GetDirectoryName ( _executable ) is { Length : > 0 } executableDirectory )
384+ {
385+ string ? currentPath = Environment . GetEnvironmentVariable ( "PATH" ) ;
386+ string updatedPath = AppendDirectoryToPath ( currentPath , executableDirectory ) ;
387+ if ( updatedPath != currentPath )
388+ {
389+ Environment . SetEnvironmentVariable ( "PATH" , updatedPath ) ;
390+ Logger . Info ( $ "Added { executableDirectory } to the PATH so npm can reach its node") ;
391+ }
392+ }
393+
224394 path = _executable ;
225395 callArguments = "" ;
226396 }
0 commit comments