Public/Invoke-TechCareUpdate.ps1

function Invoke-TechCareUpdate {
    <#
    .SYNOPSIS
        Instala actualizaciones de Windows (incluidos drivers) y de winget.
    .DESCRIPTION
        Usa la API COM de Windows Update, que viene en el sistema, para no imponer
        ninguna dependencia obligatoria. Si PSWindowsUpdate esta presente lo avisa
        como alternativa mas rica, pero no lo requiere. Despues ejecuta
        winget upgrade si winget existe.

        Nunca reinicia el equipo: si una actualizacion lo pide, queda como
        recomendacion en el informe.
    .PARAMETER SkipWindowsUpdate
        Omite Windows Update y pasa directamente a winget.
    .PARAMETER SkipWinget
        Omite winget y solo aplica Windows Update.
    .EXAMPLE
        Invoke-TechCareUpdate -WhatIf

        Muestra que se buscaria e instalaria, sin descargar nada.
    .EXAMPLE
        Invoke-TechCareUpdate -SkipWinget -Confirm:$false

        Solo Windows Update, sin tocar las aplicaciones de terceros.
    .EXAMPLE
        Invoke-TechCareUpdate -SkipWindowsUpdate -Confirm:$false

        Solo actualiza aplicaciones con winget, util cuando WSUS controla el SO.
    .OUTPUTS
        PSCustomObject
    .LINK
        https://github.com/earbona23/TechCare
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    [OutputType([pscustomobject])]
    param(
        [switch]$SkipWindowsUpdate,
        [switch]$SkipWinget
    )

    if (-not (Test-TechCarePlatform)) { throw $script:PlatformMessage }

    $findings = New-Object -TypeName System.Collections.ArrayList
    $actions  = New-Object -TypeName System.Collections.ArrayList
    $recs     = New-Object -TypeName System.Collections.ArrayList
    $status   = 'Green'
    $mod      = 'Update'

    if (-not (Test-TechCareAdmin)) {
        [void]$findings.Add('Sin privilegios de administrador: Update omitido.')
        [void]$recs.Add('Relanzar como administrador para aplicar actualizaciones.')
        Write-TechCareLog -ModuleName $mod -Level Warning -Message 'Update omitido por falta de privilegios.'
        return Add-TechCareResult -Module $mod -Status 'Gray' -Findings $findings.ToArray() -Recommendations $recs.ToArray()
    }

    if (-not $SkipWindowsUpdate) {
        if ($null -eq (Get-Command -Name 'Get-WindowsUpdate' -ErrorAction SilentlyContinue)) {
            [void]$recs.Add('Opcional: Install-Module PSWindowsUpdate da un flujo mas rico. TechCare no lo necesita.')
        }

        if ($PSCmdlet.ShouldProcess('Windows Update', 'Buscar e instalar actualizaciones de software y drivers')) {
            Write-Progress -Activity 'TechCare' -Status 'Buscando actualizaciones' -PercentComplete 25
            try {
                $svc = Get-Service -Name 'wuauserv' -ErrorAction SilentlyContinue
                if ($null -ne $svc -and $svc.Status -ne 'Running') {
                    # El servicio puede estar parado por una limpieza anterior; sin
                    # el, la busqueda COM falla con un error poco descriptivo.
                    Start-Service -Name 'wuauserv' -ErrorAction SilentlyContinue
                }

                $session  = New-Object -ComObject Microsoft.Update.Session
                $searcher = $session.CreateUpdateSearcher()
                $collection = New-Object -TypeName System.Collections.ArrayList
                # Software y Driver van en dos busquedas porque el criterio Type no
                # admite una disyuncion en la misma consulta.
                foreach ($criteria in @("IsInstalled=0 and IsHidden=0 and Type='Software'", "IsInstalled=0 and IsHidden=0 and Type='Driver'")) {
                    $found = $searcher.Search($criteria)
                    foreach ($u in $found.Updates) { [void]$collection.Add($u) }
                }

                [void]$findings.Add(('Actualizaciones disponibles: {0}' -f $collection.Count))
                foreach ($u in $collection) { [void]$findings.Add(' - ' + $u.Title) }

                if ($collection.Count -gt 0) {
                    $toInstall = New-Object -ComObject Microsoft.Update.UpdateColl
                    foreach ($u in $collection) {
                        try {
                            if (-not $u.EulaAccepted) { $u.AcceptEula() }
                            [void]$toInstall.Add($u)
                        } catch {
                            $status = 'Yellow'
                            [void]$findings.Add(('No se pudo preparar "{0}": {1}' -f $u.Title, $_.Exception.Message))
                        }
                    }

                    if ($toInstall.Count -gt 0) {
                        Write-Progress -Activity 'TechCare' -Status 'Descargando actualizaciones' -PercentComplete 45
                        $downloader = $session.CreateUpdateDownloader()
                        $downloader.Updates = $toInstall
                        [void]$downloader.Download()

                        Write-Progress -Activity 'TechCare' -Status 'Instalando actualizaciones' -PercentComplete 65
                        $installer = $session.CreateUpdateInstaller()
                        $installer.Updates = $toInstall
                        $installer.IsForced = $false
                        $installResult = $installer.Install()

                        for ($i = 0; $i -lt $toInstall.Count; $i++) {
                            $ur = $installResult.GetUpdateResult($i)
                            # ResultCode 2 es orcSucceeded; 3 es succeeded with errors.
                            if ($ur.ResultCode -eq 2) {
                                [void]$actions.Add('Instalada: ' + $toInstall.Item($i).Title)
                            } else {
                                $status = 'Yellow'
                                [void]$findings.Add(('Fallo (ResultCode {0}): {1}' -f $ur.ResultCode, $toInstall.Item($i).Title))
                            }
                        }
                        if ($installResult.RebootRequired) {
                            [void]$recs.Add('Windows Update requiere reinicio. TechCare no reinicia: hazlo en ventana acordada.')
                        }
                    }
                }
            } catch {
                $status = 'Yellow'
                [void]$findings.Add('Error consultando o aplicando Windows Update: ' + $_.Exception.Message)
                Write-TechCareLog -ModuleName $mod -Level Error -Message 'Fallo en Windows Update.' -ErrorText $_.Exception.Message
            }
        }
    }

    if (-not $SkipWinget) {
        $winget = Get-Command -Name 'winget' -ErrorAction SilentlyContinue
        if ($null -eq $winget) {
            [void]$findings.Add('winget no esta disponible en este equipo.')
            [void]$recs.Add('Instalar App Installer desde Microsoft Store para habilitar winget.')
        } elseif ($PSCmdlet.ShouldProcess('Aplicaciones de winget', 'winget upgrade --all --silent')) {
            Write-Progress -Activity 'TechCare' -Status 'winget upgrade' -PercentComplete 85
            $r = Invoke-TechCareProcess -FilePath $winget.Source -ArgumentList @(
                'upgrade', '--all', '--silent',
                '--accept-package-agreements', '--accept-source-agreements',
                '--disable-interactivity'
            )
            if ($r.ExitCode -eq 0) {
                [void]$actions.Add('winget upgrade --all completado.')
            } else {
                $status = 'Yellow'
                [void]$findings.Add(('winget devolvio codigo {0}; revisa el log para el paquete concreto.' -f $r.ExitCode))
            }
            Write-TechCareLog -ModuleName $mod -Level Detail -Message 'Salida de winget registrada.' -ErrorText $r.Output
        }
    }

    Write-Progress -Activity 'TechCare' -Status 'Actualizaciones completadas' -Completed
    return Add-TechCareResult -Module $mod -Status $status -Findings $findings.ToArray() -Actions $actions.ToArray() -Recommendations $recs.ToArray()
}