tasks/20-NPM.ps1

@{
    Id             = 'NPM'
    DisplayName    = 'Node.js + npm cache'
    Description    = 'Installs the configured Node.js major version via nvm, then relocates the npm cache'
    WingetPackages = @('CoreyButler.NVMforWindows')
    Action         = {
        [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
        param(
            [Parameter(Mandatory)]
            [System.Collections.IDictionary] $Paths
        )

        Write-Header -Name 'NPM'

        # --- Step 1: install + activate the configured Node.js major via nvm ---
        $nodeMajor = Get-NodeJsVersionConfig
        if ($nodeMajor) {
            # The metadata-driven winget pass already installed nvm, but
            # PATH may not have propagated to this process - especially
            # if the installer wrote PATH after Reset-EnvPath ran.
            # Re-read it here as a last chance.
            if (-not (Get-Command nvm -ErrorAction SilentlyContinue)) {
                Reset-EnvPath
            }
            if (-not (Get-Command nvm -ErrorAction SilentlyContinue)) {
                Write-Status -Level Warn -Message ' [WARN] nvm is not on PATH after the winget install pass; dumping diagnostics.'

                # Env vars (process + registry).
                Write-Status -Level Debug -Message " [DEBUG] PATH (process): $env:PATH"
                Write-Status -Level Debug -Message " [DEBUG] PATH (Machine reg): $([System.Environment]::GetEnvironmentVariable('PATH','Machine'))"
                Write-Status -Level Debug -Message " [DEBUG] PATH (User reg): $([System.Environment]::GetEnvironmentVariable('PATH','User'))"
                foreach ($scope in 'Process','Machine','User') {
                    foreach ($name in 'NVM_HOME','NVM_SYMLINK') {
                        $val = [System.Environment]::GetEnvironmentVariable($name, $scope)
                        Write-Status -Level Debug -Message " [DEBUG] $name ($scope) = $val"
                    }
                }

                # If NVM_HOME is set, that's the install dir. Otherwise
                # search common locations.
                $nvmHome = [System.Environment]::GetEnvironmentVariable('NVM_HOME','User')
                if (-not $nvmHome) {
                    $nvmHome = [System.Environment]::GetEnvironmentVariable('NVM_HOME','Machine')
                }
                $candidates = @()
                if ($nvmHome) { $candidates += (Join-Path $nvmHome 'nvm.exe') }
                $candidates += @(
                    (Join-Path $env:APPDATA 'nvm\nvm.exe'),
                    (Join-Path $env:LOCALAPPDATA 'nvm\nvm.exe'),
                    "$env:ProgramFiles\nvm\nvm.exe",
                    "${env:ProgramFiles(x86)}\nvm\nvm.exe",
                    'C:\nvm\nvm.exe'
                )
                foreach ($p in $candidates) {
                    Write-Status -Level Debug -Message " [DEBUG] Test-Path $p = $(Test-Path -LiteralPath $p)"
                }

                Write-Status -Level Skip -Message ' [SKIP] nvm is not on PATH; Node install would happen on a real run after winget bootstraps it.'
            }
            else {
                # Parse `nvm list` output for installed major versions.
                # Lines look like "* 22.5.1 (Currently using 64-bit executable)"
                # or " 20.10.0".
                $listOutput = nvm list 2>&1
                $installedMajors = @($listOutput | ForEach-Object {
                    if ($_ -match '^\s*\*?\s*(\d+)\.\d+\.\d+') {
                        $Matches[1]
                    }
                })

                if ($nodeMajor -in $installedMajors) {
                    Write-Status -Level Info -Message " [INFO] Node.js $nodeMajor.x is already installed via nvm."
                }
                elseif ($PSCmdlet.ShouldProcess("Node.js $nodeMajor (latest .x)", 'nvm install')) {
                    Write-Status -Level Info -Message " [INFO] Installing Node.js $nodeMajor.x via nvm..."
                    nvm install $nodeMajor 2>&1 | Format-ToolOutput
                    if ($LASTEXITCODE -ne 0) {
                        throw "nvm install $nodeMajor failed with exit code $LASTEXITCODE (nvm-windows usually requires elevation)."
                    }
                    Write-Status -Level Info -Message " [INFO] Node.js $nodeMajor installed."
                }

                # Only fire ShouldProcess if the active major isn't already
                # what we want; otherwise the "What if: nvm use ..." line
                # bleeds out of the spinner buffer on every dry run even
                # when nothing would change.
                $currentRaw      = nvm current 2>&1 | Out-String
                $alreadyActive   = $currentRaw -match "v?$([regex]::Escape($nodeMajor))\."

                if ($alreadyActive) {
                    Write-Status -Level Info -Message " [INFO] Node.js $nodeMajor.x is already the active nvm version."
                }
                elseif ($PSCmdlet.ShouldProcess("Node.js $nodeMajor", 'nvm use')) {
                    # nvm use exit codes are unreliable across versions;
                    # silence its noisy output and let downstream steps fail
                    # if Node still isn't available.
                    nvm use $nodeMajor 2>&1 | Out-Null
                }
            }
        }

        # --- Step 2: relocate the npm cache ---
        $params = @{
            Name          = 'npm_config_cache'
            TargetRoot    = $Paths.NpmCache
            DefaultSource = '%LocalAppData%\npm-cache'
        }
        Set-CacheLocation @params
    }
}