private/Update-OSDeployCoreDriversCatalog.ps1

#Requires -PSEdition Core

function Update-OSDeployCoreDriversCatalog {
    <#
    .SYNOPSIS
        Updates the local WinPE driver catalog
 
    .DESCRIPTION
        Reads refreshable WinPE driver sources from
        $global:OSDeployModule.WinPEDrivers. Sources with an UpdateUri are resolved by a
        vendor-specific discovery function. Sources with only a DownloadUri use the
        static metadata in the module configuration.
 
        When Name is omitted, the function processes every source that has an UpdateUri
        or DownloadUri. When Name is specified, it processes only valid matching sources.
        Existing entries for sources that are not processed, return no data, or fail
        discovery are preserved.
 
        The function sets CatalogDate to the current date and writes the merged catalog
        to $Script:OSDeployWinPEDriversUserConfig. ShouldProcess gates the catalog write,
        but creation of its parent directory occurs before that gate. Discovery failures
        generate warnings and do not stop the remaining sources from being processed.
 
    .PARAMETER Name
        Specifies one or more source names to process. Position 0 is supported. The
        argument completer suggests configured sources that have an UpdateUri or
        DownloadUri. Invalid or non-refreshable names generate warnings and are skipped.
 
    .PARAMETER Force
        This switch is accepted for compatibility but currently does not alter the
        function's behavior.
 
    .EXAMPLE
        PS> Update-OSDeployCoreDriversCatalog
 
        Processes every refreshable source and writes the merged catalog.
 
    .EXAMPLE
        PS> Update-OSDeployCoreDriversCatalog -Name 'dell', 'hp' -Verbose
 
        Processes the dell and hp sources while displaying verbose discovery details.
 
    .EXAMPLE
        PS> Update-OSDeployCoreDriversCatalog -Name 'dell' -WhatIf
 
        Discovers the dell metadata and shows the catalog write without performing it.
        The parent catalog directory can still be created.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.IO.FileInfo. Returns the catalog file when the configured path exists.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 0.1.0
        Date: 2026-08-28
 
        Requires PowerShell Core and initialized $global:OSDeployModule.WinPEDrivers and
        $Script:OSDeployWinPEDriversUserConfig values. Dynamic sources require access to
        their vendor metadata endpoints.
    #>

    [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 "[$($MyInvocation.MyCommand.Name)] Starting"
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] OSDeployWinPEDriversUserConfig='$Script:OSDeployWinPEDriversUserConfig'"
        Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] 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 "[$($MyInvocation.MyCommand.Name)] Active sources defined in winpedrivers.json: $($allRefreshable -join ', ')"

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

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

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

        # Load existing catalog for merge (or create empty shell)
        $catalog = if (Test-Path -Path $Script:OSDeployWinPEDriversUserConfig) {
            Write-Verbose "[$($MyInvocation.MyCommand.Name)] Existing catalog found — loading for merge"
            Get-Content -Path $Script:OSDeployWinPEDriversUserConfig -Raw | ConvertFrom-Json
        }
        else {
            Write-Verbose "[$($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-WinPEDriverPackageIntelEthernet'
            'intel-wifi'         = 'Get-WinPEDriverPackageIntelWifi'
            'dell'               = 'Get-WinPEDriverPackageDell'
            'hp'                 = 'Get-WinPEDriverPackageHp'
            'vmware'             = 'Get-WinPEDriverPackageVMware'
            'vmware-arm64'       = 'Get-WinPEDriverPackageVMware'
        }

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

            try {
                $info = if (-not $isDynamic) {
                    Write-Verbose "[$($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)] No discovery function registered for '$n'. Skipping."
                        continue
                    }

                    Write-Verbose "[$($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)] '$n' returned no data. Existing catalog entry preserved."
                    continue
                }

                Write-Verbose "[$($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 "[$($MyInvocation.MyCommand.Name)] '$n' updated successfully."
            }
            catch {
                Write-Warning "[$(Get-Date -Format s)] '$n' discovery failed — $($_.Exception.Message). Existing catalog entry preserved."
            }
        }

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

        # Write catalog
        $catalogDir = Split-Path -Path $Script:OSDeployWinPEDriversUserConfig -Parent
        if (-not (Test-Path -Path $catalogDir)) {
            Write-Verbose "[$($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 "[$($MyInvocation.MyCommand.Name)] Catalog written to '$Script:OSDeployWinPEDriversUserConfig'"
        }

        Get-Item -Path $Script:OSDeployWinPEDriversUserConfig
    }

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