lib/PathsConfig.ps1

function Get-PathsConfig {
    <#
    .SYNOPSIS
        Returns the merged $Paths dictionary. Reads JSON from $ConfigFile when
        it exists and overlays the values on top of $Defaults; missing keys
        fall through to the defaults so a partial config still works.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowNull()]
        [AllowEmptyString()]
        [string] $ConfigFile,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Defaults
    )

    if (-not $ConfigFile -or -not (Test-Path -Path $ConfigFile -PathType Leaf)) {
        return $Defaults
    }

    try {
        $raw    = Get-Content -Path $ConfigFile -Raw -Encoding UTF8
        $parsed = $raw | ConvertFrom-Json -ErrorAction Stop
    }
    catch {
        throw "Failed to parse '$ConfigFile': $($_.Exception.Message)"
    }

    if ($null -eq $parsed) { return $Defaults }

    # Most $Paths keys come from a flat top-level entry in the JSON. A
    # couple live inside a per-task nested object so related settings
    # cluster together (Wsl.Home alongside Wsl.Config / Wsl.Distros;
    # DockerDesktop.Home alongside DockerDesktop.WslHome). Map those
    # explicitly here.
    $nestedMap = @{
        'WslHome'    = @('Wsl', 'Home')
        'DockerHome' = @('DockerDesktop', 'Home')
    }

    $merged = [ordered]@{}
    foreach ($key in $Defaults.Keys) {
        $value = $null
        if ($nestedMap.ContainsKey($key)) {
            $node = $parsed
            foreach ($segment in $nestedMap[$key]) {
                if ($null -eq $node) { break }
                if (-not $node.PSObject.Properties[$segment]) { $node = $null; break }
                $node = $node.PSObject.Properties[$segment].Value
            }
            if ($node -is [string] -and $node) { $value = $node }
        }
        elseif ($parsed.PSObject.Properties[$key]) {
            $value = [string]$parsed.PSObject.Properties[$key].Value
        }

        $merged[$key] = if ($null -ne $value) { $value } else { $Defaults[$key] }
    }

    # Note: we deliberately do NOT carry over unknown string-valued keys
    # from the config. Other config sections (WingetPackages, Wsl.Distros,
    # NodeJsVersion, ...) have their own dedicated Get-*Config loaders and
    # leaking them into $Paths would trip Test-PathDriveAvailability (it
    # would treat "22" as a path and try to validate its drive root).
    return $merged
}