PackageProviders.ps1
|
function Get-PackageProvider { [CmdletBinding()] param() # Only checked-in integrations belong in this catalog, never user-supplied code. foreach ($name in 'PSResourceGet', 'DotNet', 'Npm', 'Pip', 'Uv', 'VSCode', 'WinGet', 'AppInstaller') { [pscustomobject]@{ Name = $name Platforms = if ($name -in 'WinGet', 'AppInstaller') { @('Windows') } else { @('Windows', 'Linux', 'MacOS') } RequiredModules = @() RequiredCommands = @() OptionNames = @() TestAvailable = $null GetTargets = $null Update = $null } } } function Get-PackageProviderPlatform { if ($IsWindows) { return 'Windows' } if ($IsLinux) { return 'Linux' } if ($IsMacOS) { return 'MacOS' } return 'Unknown' } function Get-PackageProviderAvailability { [CmdletBinding()] param( [Parameter(Mandatory)][psobject]$Descriptor, [Parameter(Mandatory)][hashtable]$Options ) $platform = Get-PackageProviderPlatform if ($platform -notin $Descriptor.Platforms) { return [pscustomobject]@{ Available = $false; Reason = "Provider '$($Descriptor.Name)' does not support $platform." } } if ($Descriptor.GetTargets -isnot [scriptblock] -or $Descriptor.Update -isnot [scriptblock]) { return [pscustomobject]@{ Available = $false; Reason = "Provider '$($Descriptor.Name)' integration is not implemented in this module version." } } foreach ($module in $Descriptor.RequiredModules) { if (-not (Get-Module -Name $module) -and -not (Get-Module -ListAvailable -Name $module -ErrorAction Stop)) { return [pscustomobject]@{ Available = $false; Reason = "Install the required module '$module' to use provider '$($Descriptor.Name)'." } } Import-Module -Name $module -ErrorAction Stop } foreach ($command in $Descriptor.RequiredCommands) { if (-not (Get-Command -Name $command -ListImported -ErrorAction Ignore)) { return [pscustomobject]@{ Available = $false; Reason = "Install or expose the required command '$command' on PATH for provider '$($Descriptor.Name)'." } } } if ($Descriptor.TestAvailable) { & $Descriptor.TestAvailable -Options $Options } else { [pscustomobject]@{ Available = $true; Reason = $null } } } function Invoke-PackageProviderCallback { [CmdletBinding()] param( [Parameter(Mandatory)][scriptblock]$Callback, [Parameter(Mandatory)][hashtable]$Arguments ) # Capture both error streams and throws without losing output already emitted. # Provider failures are result data, independent of the caller's ErrorAction. $ErrorActionPreference = 'Continue' $PSNativeCommandUseErrorActionPreference = $true try { & $Callback @Arguments 2>&1 } catch { $_ } } function New-PackageUpdateTarget { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Target, [string]$PreviousVersion, [string]$ProposedVersion, [object]$Data ) [pscustomobject]@{ PSTypeName = 'Shmuelie.PackageManagement.UpdateTarget' Target = $Target PreviousVersion = if ($PreviousVersion) { $PreviousVersion } else { $null } ProposedVersion = if ($ProposedVersion) { $ProposedVersion } else { $null } Data = $Data } } function New-PackageUpdateResult { [CmdletBinding()] param( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Provider, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Target, [string]$PreviousVersion, [string]$ResultingVersion, [Parameter(Mandatory)][ValidateSet('Planned', 'Updated', 'Unchanged', 'Skipped', 'Failed')][string]$Status, [System.Management.Automation.ErrorRecord]$Error, [string]$Reason ) if ($Status -eq 'Failed' -and -not $Error) { throw 'Failed results must include an ErrorRecord.' } if ($Status -ne 'Failed' -and $Error) { throw 'Only Failed results may contain an ErrorRecord.' } if ($Status -eq 'Skipped' -and [string]::IsNullOrWhiteSpace($Reason)) { throw 'Skipped results must include a reason.' } [pscustomobject]@{ PSTypeName = 'Shmuelie.PackageManagement.UpdateResult' Provider = $Provider Target = $Target PreviousVersion = if ($PreviousVersion) { $PreviousVersion } else { $null } ResultingVersion = if ($ResultingVersion) { $ResultingVersion } else { $null } Status = $Status Error = $Error Reason = if ($Reason) { $Reason } elseif ($Error) { $Error.Exception.Message } else { $null } } } function New-PackageProviderFailure { [CmdletBinding()] param( [Parameter(Mandatory)][string]$Provider, [Parameter(Mandatory)][string]$Target, [Parameter(Mandatory)][string]$Message, [string]$PreviousVersion ) $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new($Message), 'InvalidPackageProviderOutput', [System.Management.Automation.ErrorCategory]::InvalidData, $Target ) New-PackageUpdateResult -Provider $Provider -Target $Target -PreviousVersion $PreviousVersion -Status Failed -Error $errorRecord } function Test-PackageUpdateResult { param( [Parameter(Mandatory)][AllowNull()][object]$Result, [Parameter(Mandatory)][string]$Provider, [Parameter(Mandatory)][string]$Target ) if ($null -eq $Result -or $Result.PSTypeNames -notcontains 'Shmuelie.PackageManagement.UpdateResult') { return $false } foreach ($property in 'Provider', 'Target', 'PreviousVersion', 'ResultingVersion', 'Status', 'Error', 'Reason') { if (-not $Result.PSObject.Properties[$property]) { return $false } } if ($Result.Provider -cne $Provider -or $Result.Target -cne $Target) { return $false } if ($Result.Status -cnotin 'Updated', 'Unchanged', 'Skipped', 'Failed') { return $false } foreach ($property in 'PreviousVersion', 'ResultingVersion', 'Reason') { if ($null -ne $Result.$property -and $Result.$property -isnot [string]) { return $false } } if ($Result.Status -eq 'Failed') { return $Result.Error -is [System.Management.Automation.ErrorRecord] } if ($null -ne $Result.Error) { return $false } if ($Result.Status -eq 'Skipped' -and [string]::IsNullOrWhiteSpace($Result.Reason)) { return $false } return $true } |