private/WinPEDrivers/Update-OSDeployWinPEDriversCatalog.ps1

#Requires -PSEdition Core

function Update-OSDeployWinPEDriversCatalog {
    <#
    .SYNOPSIS
        Discovers the latest WinPE driver pack metadata and writes a local catalog file.
 
    .DESCRIPTION
        Iterates over active sources defined in winpedrivers.json and writes the results
        to $env:ProgramData\OSDeployCore\cache\config\winpedrivers.json. Dynamic sources (those with an
        UpdateUri property) are resolved by calling the appropriate per-vendor discovery
        function. Static sources (those with a DownloadUri property) are copied directly
        from winpedrivers.json.
 
        When -Name is specified, only those sources are refreshed; all other sources
        already in the catalog are preserved unchanged (merge behaviour).
 
        If a vendor page cannot be reached or parsed, a warning is emitted and that
        source is skipped — the rest of the catalog is still updated.
 
    .PARAMETER Name
        One or more source names to refresh. Tab-completion is supported.
        When omitted, all active sources are written.
 
    .PARAMETER Force
        Overwrite the catalog even if the CatalogDate is already today.
 
    .EXAMPLE
        PS> Update-OSDeployWinPEDriversCatalog
        Refreshes all configured sources and writes the catalog to
        $env:ProgramData\OSDeployCore\cache\config\winpedrivers.json.
 
    .EXAMPLE
        PS> Update-OSDeployWinPEDriversCatalog -Name 'dell'
        Refreshes only the dell entry, preserving all other catalog entries.
 
    .EXAMPLE
        PS> Update-OSDeployWinPEDriversCatalog -Name 'dell', 'hp' -Verbose
        Refreshes dell and hp with verbose progress output.
 
    .OUTPUTS
        [System.IO.FileInfo] The written catalog file.
 
    .NOTES
        Author: David Segura
        Version: 0.1.0
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Position = 0)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            $global:OSDeployModule.WinPEDrivers.PSObject.Properties |
                Where-Object { ($_.Value.UpdateUri -or $_.Value.DownloadUri) -and $_.Name -like "$wordToComplete*" } |
                ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.Name) }
        })]
        [string[]]$Name,

        [Parameter()]
        [switch]$Force
    )

    begin {
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Starting"
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] OSDeployWinPEDriversUserConfig='$Script:OSDeployWinPEDriversUserConfig'"
        Write-OSDeployWinPEDriversProgress "Checking for updated WinPE drivers ..."
    }

    process {
        # Determine which source names can be written to the catalog.
        $allRefreshable = @(
            $global:OSDeployModule.WinPEDrivers.PSObject.Properties |
                Where-Object { $_.Value.UpdateUri -or $_.Value.DownloadUri } |
                Select-Object -ExpandProperty Name
        )
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Active sources defined in winpedrivers.json: $($allRefreshable -join ', ')"

        $targetNames = if ($Name) {
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] -Name specified — filtering to: $($Name -join ', ')"
            $Name | Where-Object {
                if ($_ -notin $allRefreshable) {
                    Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] '$_' is not a refreshable source. Skipping."
                    $false
                }
                else { $true }
            }
        }
        else {
            $allRefreshable
        }

        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Sources to refresh: $($targetNames -join ', ')"

        if (-not $targetNames) {
            Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] No valid sources to refresh."
            return
        }

        # Load existing catalog for merge (or create empty shell)
        $catalog = if (Test-Path -Path $Script:OSDeployWinPEDriversUserConfig) {
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Existing catalog found — loading for merge"
            Get-Content -Path $Script:OSDeployWinPEDriversUserConfig -Raw | ConvertFrom-Json
        }
        else {
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] No existing catalog — starting with empty shell"
            [PSCustomObject]@{
                CatalogDate = ''
                Sources     = [PSCustomObject]@{}
            }
        }

        # Map Dynamic source name → discovery function name
        $parserMap = @{
            'intel-ethernet'     = 'Get-CloudWinPEDriverIntelEthernet'
            'intel-wifi'         = 'Get-CloudWinPEDriverIntelWifi'
            'dell'               = 'Get-CloudWinPEDriverOemDell'
            'hp'                 = 'Get-CloudWinPEDriverOemHp'
            'vmware'             = 'Get-CloudWinPEDriverOemVMware'
            'vmware-arm64'       = 'Get-CloudWinPEDriverOemVMware'
        }

        foreach ($n in $targetNames) {
            $sourceData = $global:OSDeployModule.WinPEDrivers.$n
            $isDynamic  = [bool]$sourceData.UpdateUri
            $parserFn   = $parserMap[$n]

            try {
                $info = if (-not $isDynamic) {
                    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Copying static metadata for '$n' from winpedrivers.json"
                    [PSCustomObject]@{
                        Architecture = $sourceData.Architecture
                        ReadmeUri    = $null
                        PackageId    = $sourceData.PackageId
                        Version      = $null
                        ReleaseDate  = $null
                        FileName     = $sourceData.FileName
                        FileSizeMB   = $sourceData.FileSizeMB
                        DownloadUri  = $sourceData.DownloadUri
                        Checksums    = [PSCustomObject]@{}
                    }
                }
                else {
                    if (-not $parserFn) {
                        Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] No discovery function registered for '$n'. Skipping."
                        continue
                    }

                    Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Discovering '$n' via $parserFn"
                    $raw = & $parserFn

                    # VMware returns both architectures — filter to the one matching this source
                    $arch = $sourceData.Architecture
                    if (@($raw).Count -gt 1) {
                        $raw | Where-Object { $_.Architecture -eq $arch } | Select-Object -First 1
                    }
                    else {
                        $raw
                    }
                }

                if ($null -eq $info) {
                    Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] '$n' returned no data. Existing catalog entry preserved."
                    continue
                }

                Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] '$n' — Architecture='$($info.Architecture)' PackageId='$($info.PackageId)' Version='$($info.Version)' ReleaseDate='$($info.ReleaseDate)' FileName='$($info.FileName)'"

                # Store as a single object per source
                $catalog.Sources | Add-Member -MemberType NoteProperty -Name $n -Value $info -Force
                Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] '$n' updated successfully."
            }
            catch {
                Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] '$n' discovery failed — $($_.Exception.Message). Existing catalog entry preserved."
            }
        }

        $catalog.CatalogDate = Get-Date -Format 'yyyy-MM-dd'
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Setting CatalogDate='$($catalog.CatalogDate)'"

        # Write catalog
        $catalogDir = Split-Path -Path $Script:OSDeployWinPEDriversUserConfig -Parent
        if (-not (Test-Path -Path $catalogDir)) {
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Creating catalog directory '$catalogDir'"
            New-Item -ItemType Directory -Path $catalogDir -Force | Out-Null
        }

        if ($PSCmdlet.ShouldProcess($Script:OSDeployWinPEDriversUserConfig, 'Write WinPE driver catalog')) {
            $catalog | ConvertTo-Json -Depth 10 |
                Set-Content -Path $Script:OSDeployWinPEDriversUserConfig -Encoding UTF8 -ErrorAction Stop
            Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Catalog written to '$Script:OSDeployWinPEDriversUserConfig'"
        }

        Get-Item -Path $Script:OSDeployWinPEDriversUserConfig
    }

    end {
        Write-Verbose "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Complete"
    }
}