Update-AllPackages.ps1

function Update-AllPackages {
    <#
    .SYNOPSIS
        Update packages through the available package-provider integrations.
    .DESCRIPTION
        Selects all known providers by default, in catalog order. Unsupported
        platforms, missing dependencies, and integrations not yet implemented
        produce Skipped results with reasons. Providers are imported only when
        selected; importing this module does not require any provider module.
 
        This initial foundation includes the orchestration contract, not the
        provider adapters. Until adapters ship, all providers report Skipped.
 
        Each integration discovers targets without mutation. ShouldProcess
        gates each target's update callback. WhatIf runs only discovery and
        returns Planned results; it never invokes an update callback.
 
        Provider failures, including nonterminating errors, are returned as
        typed Failed results rather than terminating independent providers.
        Invalid selection or options terminate before discovery or updates.
    .PARAMETER Provider
        Include only these provider names. Defaults to all known providers.
        Names are case-insensitive and support tab completion. Duplicates do
        not run a provider twice; execution always follows catalog order.
    .PARAMETER ExcludeProvider
        Remove these providers from the selected set. Exclusion wins.
    .PARAMETER ProviderOptions
        Hashtable keyed by provider name. Each value is a hashtable of options
        explicitly supported by that integration. Unknown providers, option
        names, or non-hashtable values are rejected before any work.
        Options for unselected providers are validated but not executed.
        Provider and option keys are case-insensitive, including JSON-derived
        hashtables. Case-equivalent duplicate keys are rejected. The caller's
        maps are not modified.
    .PARAMETER StopOnFailure
        Stop after the first failing callback, before another target or
        provider starts. Preserve all results produced by that callback.
        Skipped providers are not failures.
    .OUTPUTS
        Shmuelie.PackageManagement.UpdateResult
        Provider, Target, PreviousVersion, ResultingVersion, Status, Error,
        and Reason. Status is Planned, Updated, Unchanged, Skipped, or Failed.
        Error is the original ErrorRecord when available. Unknown versions
        are null. Planned ResultingVersion is the proposed version, not an
        observed installed version.
    .EXAMPLE
        Update-AllPackages -WhatIf
        Preview all available integrations. Unimplemented integrations skip.
    .EXAMPLE
        Update-AllPackages -Provider Npm, DotNet -ExcludeProvider Npm -StopOnFailure
        Select only DotNet and stop if its integration fails.
    .EXAMPLE
        Update-AllPackages -ProviderOptions @{ Npm = @{} } -Confirm:$false
        Supply a provider options map and disable interactive confirmation.
        The foundation accepts empty options only; adapters define their own.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Shmuelie.PackageManagement.UpdateResult')]
    param(
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete)
            & (Get-Module Shmuelie.PackageManagement) { Get-PackageProvider } |
                Where-Object Name -Like "$wordToComplete*" | ForEach-Object { $_.Name }
        })]
        [string[]]$Provider = @(),

        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete)
            & (Get-Module Shmuelie.PackageManagement) { Get-PackageProvider } |
                Where-Object Name -Like "$wordToComplete*" | ForEach-Object { $_.Name }
        })]
        [string[]]$ExcludeProvider = @(),

        [ValidateNotNull()]
        [hashtable]$ProviderOptions = @{},

        [switch]$StopOnFailure
    )

    # JSON-derived and custom hashtables can use case-sensitive comparers.
    $normalizedProviderOptions = [hashtable]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($name in $ProviderOptions.Keys) {
        if ($normalizedProviderOptions.ContainsKey($name)) {
            throw "Duplicate provider key '$name' in ProviderOptions; provider keys are case-insensitive."
        }
        $options = $ProviderOptions[$name]
        if ($options -isnot [hashtable]) {
            throw "Options for provider '$name' must be a hashtable."
        }
        $normalizedOptions = [hashtable]::new([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($option in $options.Keys) {
            if ($normalizedOptions.ContainsKey($option)) {
                throw "Duplicate option key '$option' for provider '$name'; option keys are case-insensitive."
            }
            $normalizedOptions.Add($option, $options[$option])
        }
        $normalizedProviderOptions.Add($name, $normalizedOptions)
    }

    $catalog = @(Get-PackageProvider)
    $names = @($catalog.Name)
    foreach ($name in @($Provider) + @($ExcludeProvider) + @($normalizedProviderOptions.Keys)) {
        if ($name -notin $names) {
            throw "Unknown provider '$name'. Valid providers: $($names -join ', ')."
        }
    }
    foreach ($name in $normalizedProviderOptions.Keys) {
        $options = $normalizedProviderOptions[$name]
        $descriptor = $catalog | Where-Object Name -EQ $name
        foreach ($option in $options.Keys) {
            if ($option -notin $descriptor.OptionNames) {
                throw "Unknown option '$option' for provider '$name'. Supported options: $($descriptor.OptionNames -join ', ')."
            }
        }
    }

    foreach ($descriptor in $catalog) {
        $name = $descriptor.Name
        if (($Provider -and $name -notin $Provider) -or $name -in $ExcludeProvider) { continue }
        $options = if ($normalizedProviderOptions.ContainsKey($name)) { $normalizedProviderOptions[$name].Clone() } else { @{} }
        $availability = @(Invoke-PackageProviderCallback -Callback {
            param($Descriptor, $Options)
            Get-PackageProviderAvailability -Descriptor $Descriptor -Options $Options
        } -Arguments @{ Descriptor = $descriptor; Options = $options })
        $errors = @($availability | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })
        if ($errors.Count) {
            foreach ($providerError in $errors) {
                New-PackageUpdateResult -Provider $name -Target $name -Status Failed -Error $providerError
            }
            if ($StopOnFailure) { return }
            continue
        }
        if ($availability.Count -ne 1 -or $availability[0].Available -isnot [bool] -or
            (-not $availability[0].Available -and [string]::IsNullOrWhiteSpace($availability[0].Reason))) {
            New-PackageProviderFailure -Provider $name -Target $name -Message "Provider '$name' returned invalid availability; expected Available (Boolean) and a Reason when unavailable."
            if ($StopOnFailure) { return }
            continue
        }
        if (-not $availability[0].Available) {
            New-PackageUpdateResult -Provider $name -Target $name -Status Skipped -Reason $availability[0].Reason
            continue
        }

        $targets = @(Invoke-PackageProviderCallback -Callback $descriptor.GetTargets -Arguments @{ Options = $options })
        $errors = @($targets | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] })
        if ($errors.Count) {
            foreach ($providerError in $errors) {
                New-PackageUpdateResult -Provider $name -Target $name -Status Failed -Error $providerError
            }
            if ($StopOnFailure) { return }
            continue
        }
        $invalidTargets = @($targets | Where-Object {
            $_.PSTypeNames -notcontains 'Shmuelie.PackageManagement.UpdateTarget' -or
            $_.Target -isnot [string] -or
            [string]::IsNullOrWhiteSpace($_.Target) -or
            -not $_.PSObject.Properties['PreviousVersion'] -or
            -not $_.PSObject.Properties['ProposedVersion'] -or
            -not $_.PSObject.Properties['Data'] -or
            ($null -ne $_.PreviousVersion -and $_.PreviousVersion -isnot [string]) -or
            ($null -ne $_.ProposedVersion -and $_.ProposedVersion -isnot [string])
        })
        if ($invalidTargets.Count) {
            New-PackageProviderFailure -Provider $name -Target $name -Message "Provider '$name' returned invalid targets; use New-PackageUpdateTarget."
            if ($StopOnFailure) { return }
            continue
        }
        if (-not $targets.Count) {
            New-PackageUpdateResult -Provider $name -Target $name -Status Unchanged -Reason 'Provider reported no update targets.'
            continue
        }
        foreach ($target in $targets) {
            if (-not $PSCmdlet.ShouldProcess("$name / $($target.Target)", 'Update package')) {
                if ($WhatIfPreference) {
                    New-PackageUpdateResult -Provider $name -Target $target.Target -PreviousVersion $target.PreviousVersion -ResultingVersion $target.ProposedVersion -Status Planned
                } else {
                    New-PackageUpdateResult -Provider $name -Target $target.Target -PreviousVersion $target.PreviousVersion -Status Skipped -Reason 'Update was not confirmed.'
                }
                continue
            }

            $results = @(Invoke-PackageProviderCallback -Callback $descriptor.Update -Arguments @{ Target = $target; Options = $options })
            $failed = $false
            if (-not $results.Count) {
                New-PackageProviderFailure -Provider $name -Target $target.Target -PreviousVersion $target.PreviousVersion -Message "Provider '$name' returned no update result; the outcome is unknown."
                $failed = $true
            }
            foreach ($result in $results) {
                if ($result -is [System.Management.Automation.ErrorRecord]) {
                    New-PackageUpdateResult -Provider $name -Target $target.Target -PreviousVersion $target.PreviousVersion -Status Failed -Error $result
                    $failed = $true
                } elseif (-not (Test-PackageUpdateResult -Result $result -Provider $name -Target $target.Target)) {
                    New-PackageProviderFailure -Provider $name -Target $target.Target -PreviousVersion $target.PreviousVersion -Message "Provider '$name' returned invalid update output; use New-PackageUpdateResult for this target."
                    $failed = $true
                } else {
                    $result
                    if ($result.Status -eq 'Failed') { $failed = $true }
                }
            }
            if ($failed -and $StopOnFailure) { return }
        }
    }
}