Public/AppInstaller.ps1

function Invoke-AppInstallerEnumeration {
    [CmdletBinding()]
    param()

    $scriptBlock = @'
$ErrorActionPreference = 'Stop'
 
$packageManager = [Windows.Management.Deployment.PackageManager, Windows.Management.Deployment, ContentType = WindowsRuntime]::new()
$packages = @($packageManager.FindPackagesForUser([string]::Empty))
 
$results = foreach ($package in $packages) {
    $appInstallerInfo = $null
 
    try {
        $appInstallerInfo = $package.GetAppInstallerInfo()
    } catch {
        $appInstallerInfo = $null
    }
 
    if ($null -eq $appInstallerInfo) {
        continue
    }
 
    $appInstallerUri = $null
    foreach ($propertyName in @('AppInstallerUri', 'Uri')) {
        try {
            $value = $appInstallerInfo.$propertyName
        } catch {
            $value = $null
        }
 
        if ($null -ne $value) {
            if ($value -is [System.Uri]) {
                $appInstallerUri = $value.AbsoluteUri
            } else {
                $appInstallerUri = $value.ToString()
            }
            break
        }
    }
 
    if ([string]::IsNullOrWhiteSpace($appInstallerUri)) {
        continue
    }
 
    $identity = $package.Id
    $version = $null
    if ($null -ne $identity.Version) {
        $version = '{0}.{1}.{2}.{3}' -f $identity.Version.Major, $identity.Version.Minor, $identity.Version.Build, $identity.Version.Revision
    }
 
    [PSCustomObject]@{
        Name = $identity.Name
        PackageFullName = $identity.FullName
        PackageFamilyName = $identity.FamilyName
        Publisher = $identity.Publisher
        Version = $version
        Architecture = $identity.Architecture.ToString()
        AppInstallerUri = $appInstallerUri
    }
}
 
$results | ConvertTo-Json -Depth 4
'@


    $output = & powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $scriptBlock 2>&1
    if ($LASTEXITCODE -ne 0) {
        throw "powershell.exe failed to enumerate App Installer applications. Exit code: $LASTEXITCODE. Output: $($output -join [Environment]::NewLine)"
    }

    $json = $output -join [Environment]::NewLine
    if ([string]::IsNullOrWhiteSpace($json)) {
        return
    }

    $json | ConvertFrom-Json
}

function Get-AppInstallerValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowNull()]
        [object]$InputObject,

        [Parameter(Mandatory)]
        [string[]]$PropertyName
    )

    foreach ($name in $PropertyName) {
        $property = $InputObject.PSObject.Properties[$name]
        if ($null -ne $property -and $null -ne $property.Value -and -not [string]::IsNullOrWhiteSpace($property.Value.ToString())) {
            return $property.Value
        }
    }

    return $null
}

function ConvertTo-AppInstallerApplication {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$InputObject
    )

    process {
        $uri = Get-AppInstallerValue -InputObject $InputObject -PropertyName @('AppInstallerUri', 'Uri', 'UpdateUri')
        if ($uri -is [System.Uri]) {
            $uri = $uri.AbsoluteUri
        } elseif ($null -ne $uri) {
            $uri = $uri.ToString()
        }

        [PSCustomObject]@{
            PSTypeName       = 'Shmuelie.Windows.AppInstallerApplication'
            Name             = (Get-AppInstallerValue -InputObject $InputObject -PropertyName @('Name', 'PackageName', 'PackageIdentityName'))
            PackageFullName  = (Get-AppInstallerValue -InputObject $InputObject -PropertyName @('PackageFullName', 'FullName'))
            PackageFamilyName = (Get-AppInstallerValue -InputObject $InputObject -PropertyName @('PackageFamilyName', 'FamilyName'))
            Publisher        = (Get-AppInstallerValue -InputObject $InputObject -PropertyName @('Publisher'))
            Version          = (Get-AppInstallerValue -InputObject $InputObject -PropertyName @('Version'))
            Architecture     = (Get-AppInstallerValue -InputObject $InputObject -PropertyName @('Architecture'))
            AppInstallerUri  = $uri
        }
    }
}

function Get-AppInstallerApp {
    <#
    .SYNOPSIS
    Lists apps installed from App Installer files.
    .DESCRIPTION
    Enumerates MSIX/AppX packages that have an associated .appinstaller file and returns their package identity information plus the configured App Installer update URI.
 
    The Windows Runtime projection needed to read App Installer metadata is only available from Windows PowerShell 5.1, so this command shells out to powershell.exe for enumeration and returns plain objects to PowerShell 7.
    .EXAMPLE
    Get-AppInstallerApp
    Returns apps installed from .appinstaller files for the current user.
    .NOTES
    Windows only.
    #>

    [CmdletBinding()]
    param()

    Assert-WindowsOnly -CommandName $MyInvocation.MyCommand.Name

    Invoke-AppInstallerEnumeration | ConvertTo-AppInstallerApplication
}

function Invoke-AppInstallerUpdate {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$AppInstallerUri
    )

    Add-AppxPackage -AppInstallerFile $AppInstallerUri
}

function Test-AppInstallerAppMatch {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$App,

        [Parameter(Mandatory)]
        [string]$Name
    )

    foreach ($propertyName in @('Name', 'PackageFullName', 'PackageFamilyName')) {
        $value = Get-AppInstallerValue -InputObject $App -PropertyName @($propertyName)
        if ($null -ne $value -and $value.ToString().Equals($Name, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $true
        }
    }

    return $false
}

function Update-AppInstallerApp {
    <#
    .SYNOPSIS
    Triggers update checks for App Installer apps.
    .DESCRIPTION
    Discovers apps installed from .appinstaller files and re-registers their App Installer URI with Add-AppxPackage -AppInstallerFile to trigger an update check.
 
    Pass one or more package names, full names, or family names to update specific apps. When no name is provided, every discovered App Installer app is updated. Objects from Get-AppInstallerApp can be piped to this command by property name.
    .PARAMETER Name
    Package identity name, package full name, or package family name to update. Accepts pipeline input by property name.
    .EXAMPLE
    Update-AppInstallerApp
    Triggers update checks for every discovered App Installer app.
    .EXAMPLE
    Get-AppInstallerApp | Where-Object Name -eq Contoso.App | Update-AppInstallerApp -WhatIf
    Shows the update action that would be triggered for Contoso.App.
    .NOTES
    Windows only.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Position = 0, ValueFromPipelineByPropertyName)]
        [Alias('PackageName', 'PackageFullName', 'PackageFamilyName')]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name
    )

    begin {
        Assert-WindowsOnly -CommandName $MyInvocation.MyCommand.Name
        $requestedNames = [System.Collections.Generic.List[string]]::new()
    }

    process {
        foreach ($requestedName in $Name) {
            if (-not [string]::IsNullOrWhiteSpace($requestedName)) {
                $requestedNames.Add($requestedName)
            }
        }
    }

    end {
        $apps = @(Get-AppInstallerApp)
        if ($requestedNames.Count -gt 0) {
            $apps = @($apps | Where-Object {
                $app = $_
                $isMatch = $false
                foreach ($requestedName in $requestedNames) {
                    if (Test-AppInstallerAppMatch -App $app -Name $requestedName) {
                        $isMatch = $true
                        break
                    }
                }

                $isMatch
            })
        }

        foreach ($app in $apps) {
            $uri = Get-AppInstallerValue -InputObject $app -PropertyName @('AppInstallerUri', 'Uri', 'UpdateUri')
            if ($uri -is [System.Uri]) {
                $uri = $uri.AbsoluteUri
            } elseif ($null -ne $uri) {
                $uri = $uri.ToString()
            }

            if ([string]::IsNullOrWhiteSpace($uri)) {
                continue
            }

            $target = $app.Name
            if ([string]::IsNullOrWhiteSpace($target)) {
                $target = $uri
            }

            if ($PSCmdlet.ShouldProcess($target, "Add-AppxPackage -AppInstallerFile $uri")) {
                Invoke-AppInstallerUpdate -AppInstallerUri $uri
            }
        }
    }
}