lib/Wsl.ps1

function Get-WslConfigConfig {
    <#
    .SYNOPSIS
        Reads the Wsl.Config object from the same config file used by
        Get-PathsConfig. Returns an ordered hashtable shaped like
        @{ section = @{ key = value; ... }; ... } or $null when the key
        is absent.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string] $ConfigFile = $script:ConfigFilePath
    )

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

    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 $null }
    if (-not $parsed.PSObject.Properties['Wsl']) { return $null }
    if (-not $parsed.Wsl.PSObject.Properties['Config']) { return $null }

    $result = [ordered]@{}
    foreach ($sectionProp in $parsed.Wsl.Config.PSObject.Properties) {
        $sectionDict = [ordered]@{}
        foreach ($keyProp in $sectionProp.Value.PSObject.Properties) {
            $sectionDict[$keyProp.Name] = $keyProp.Value
        }
        $result[$sectionProp.Name] = $sectionDict
    }
    return $result
}

function ConvertTo-WslConfigIni {
    <#
    .SYNOPSIS
        Serializes an ordered { section -> { key -> value } } hashtable
        into the INI-style text format that .wslconfig expects.
        Booleans become lowercase "true"/"false".
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Sections
    )

    $lines = [System.Collections.Generic.List[string]]::new()
    foreach ($sectionName in $Sections.Keys) {
        if ($lines.Count -gt 0) { $lines.Add('') }
        $lines.Add("[$sectionName]")

        $sectionData = $Sections[$sectionName]
        foreach ($key in $sectionData.Keys) {
            $val = $sectionData[$key]
            if ($val -is [bool]) {
                $val = if ($val) { 'true' } else { 'false' }
            }
            $lines.Add("$key=$val")
        }
    }
    return ($lines -join [System.Environment]::NewLine)
}

function Initialize-Wsl {
    <#
    .SYNOPSIS
        Refreshes the WSL kernel (`wsl --update`). Assumes the WSL backend
        itself is already installed - the Wsl task declares Microsoft.WSL
        as a winget dependency, so the metadata-driven install pass that
        runs before any task action ensures WSL is present by the time
        this is called.

        Throws on non-zero exit so downstream tasks (Docker Desktop's
        first-run bootstrap, the WSL distro relocation task) don't waste
        time on a known-broken WSL state.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param()

    if (-not (Get-Command wsl -ErrorAction SilentlyContinue)) {
        Write-Status -Level Skip -Message ' [SKIP] wsl.exe is not on PATH; nothing to update.'
        return
    }

    if (-not $PSCmdlet.ShouldProcess('WSL kernel', 'wsl --update')) {
        return
    }

    Write-Status -Level Info -Message ' [INFO] Ensuring WSL kernel is up to date...'
    wsl --update 2>&1 | Format-ToolOutput
    if ($LASTEXITCODE -ne 0) {
        throw ("wsl --update failed with exit code {0}. " +
               "If WSL itself isn't installed, ensure Microsoft.WSL is in WingetPackages " +
               "(or run ``winget install -e --id Microsoft.WSL`` manually) and retry.") -f $LASTEXITCODE
    }
}

function Get-WslDistroBasePath {
    <#
    .SYNOPSIS
        Returns the BasePath of the named WSL distro by reading
        HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss\<id>.
        Strips the "\\?\" NT-path prefix so callers get a normal Windows
        path. Returns $null if the distro isn't registered (or the
        Lxss key doesn't exist - e.g. on non-Windows test runs).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Name
    )

    $lxssRoot = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss'
    if (-not (Test-Path -Path $lxssRoot)) { return $null }

    $entries = Get-ChildItem -Path $lxssRoot -ErrorAction SilentlyContinue
    foreach ($entry in $entries) {
        $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
        if ($props -and $props.DistributionName -eq $Name) {
            $basePath = [string]$props.BasePath
            if ($basePath -match '^\\\\\?\\(.+)$') {
                return $Matches[1]
            }
            return $basePath
        }
    }
    return $null
}

function Get-WslDistrosConfig {
    <#
    .SYNOPSIS
        Reads the Wsl.Distros array from the same config file used by
        Get-PathsConfig. Returns an empty array when the key is absent or
        the file doesn't exist - callers can then fall back to the legacy
        "just relocate what's already installed" behavior.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string] $ConfigFile = $script:ConfigFilePath
    )

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

    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 @() }
    if (-not $parsed.PSObject.Properties['Wsl']) { return @() }
    if (-not $parsed.Wsl.PSObject.Properties['Distros']) { return @() }

    @($parsed.Wsl.Distros | Where-Object { $_ })
}