Public/Tools/Enable-FastNodeManager.ps1
|
function Enable-FastNodeManager { <# .SYNOPSIS Installs (if necessary) and activates Fast Node Manager (fnm) for the session. .DESCRIPTION Runs two nested Invoke-Step substeps: - Install: if fnm.exe isn't on PATH, installs it with winget (Schniz.fnm, a portable package) and patches the current session's PATH so the Initialize substep can see it immediately. - Initialize: applies `fnm env` (multishell PATH + FNM_* variables, recursive version-file strategy) and registers fnm completions, then registers a LocationChangedAction hook so changing into a Node project auto-switches the node version (via `fnm use`). The directory hook uses PowerShell's $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction (6.2+), which fires after *any* location change — `cd`, `z`/`cdi`, `Set-Location`, `Push-Location`, `..` — so it works whether or not zoxide is enabled and regardless of zoxide's jump command. On every filesystem change it runs `fnm use --silent-if-unchanged`: fnm resolves the version recursively (the strategy set by `fnm env`), reverting to the default version outside a Node project, and emits nothing unless the active version actually changes — so moving around a non-Node tree is silent and produces no error. It chains any pre-existing LocationChangedAction and is guarded against re-registering on profile reload. If the install doesn't produce fnm.exe on PATH, a warning is emitted (with winget's captured output) and Initialize is skipped (guarded by Get-Command) so profile startup continues. .EXAMPLE Enable-FastNodeManager .NOTES Independent of zoxide and of call order: the directory hook is a LocationChangedAction, not a wrap of zoxide's cd helper, so no "call after Enable-Zoxide" requirement applies. #> [CmdletBinding()] param() Invoke-Step "Install" { # fnm is a winget portable: its exe lands in the default Links dir. Install-WingetPackageSafe -Id 'Schniz.fnm' -Exe 'fnm.exe' -CallerName 'Enable-FastNodeManager' } Invoke-Step "Initialize" { if (Get-Command fnm.exe -ErrorAction SilentlyContinue) { # Run in the global scope (not this module's) so the emitted env/completion helpers # aren't tagged to the module — see Private/Invoke-InGlobalScope.ps1. Invoke-InGlobalScope (fnm env --version-file-strategy=recursive --shell powershell | Out-String) Invoke-InGlobalScope (fnm completions --shell powershell | Out-String) # Auto-switch the node version on every directory change via PowerShell's # LocationChangedAction (fires for cd, z/cdi, Set-Location, Push-Location, .., etc.), # so it works without zoxide and regardless of zoxide's --cmd. Run in the global scope # so the handler and its $global:__fnm_loc_base capture aren't tagged to the module and # resolve when the hook fires later from the prompt. This matches fnm's own --use-on-cd # integration: a thin `fnm use --silent-if-unchanged` on each change. # # Capture any pre-existing handler ONCE (guarded by $global:__fnm_loc_hooked) so a # profile reload doesn't re-capture our own wrapper and stack fnm calls. The base is # Enable-Zoxide's LocationChangedAction (it runs first and also hooks here) or $null; # either way fnm chains onto it so both fire. But always (re)install the wrapper, so # reloading the profile in a live session repairs or updates the hook rather than leaving # a stale one frozen behind the guard. Invoke-InGlobalScope @' if (-not (Get-Variable -Name __fnm_loc_hooked -Scope Global -ErrorAction SilentlyContinue)) { $global:__fnm_loc_base = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction $global:__fnm_loc_hooked = $true } $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = { param($source, $eventArgs) # The captured base is an EventHandler delegate (the property's type), so call .Invoke. if ($null -ne $global:__fnm_loc_base) { $global:__fnm_loc_base.Invoke($source, $eventArgs) } # Switch the node version for the new directory. fnm resolves the version recursively (the # FNM_VERSION_FILE_STRATEGY set by `fnm env`); outside a Node project it falls back to the # default version, and with --silent-if-unchanged it emits nothing to stdout/stderr unless the # active version actually changes — so no version-file gate is needed (verified on fnm 1.39). # Guard on the FileSystem provider so cd into Registry:/Cert: is a no-op. Pipe through Out-Host: # PowerShell discards stdout emitted inside a LocationChangedAction, and fnm writes its # "Using Node vX.X.X" confirmation to stdout — so without Out-Host the switch is invisible. $new = $eventArgs.NewPath if ($new -and $new.Provider.Name -eq 'FileSystem') { fnm use --silent-if-unchanged | Out-Host } } '@ } } } |