Files
win-remote-toolkit/scripts/powershell/remove-apps.ps1
2026-04-24 17:44:02 +08:00

103 lines
4.3 KiB
PowerShell

[Console]::OutputEncoding = [Text.Encoding]::UTF8
$ErrorActionPreference = 'Continue'
$configPath = Join-Path $PSScriptRoot 'remove-apps-config.json'
if (-not (Test-Path $configPath)) { throw "Config not found: $configPath" }
$cfg = Get-Content $configPath -Raw -Encoding UTF8 | ConvertFrom-Json
$results = @()
function Record([string]$name, [string]$result, [string]$detail='') {
$script:results += [pscustomobject]@{ Name = $name; Result = $result; Detail = $detail }
}
Write-Host "===== Removing Appx packages ====="
foreach ($name in @($cfg.RemoveAppx)) {
$pkgs = @(Get-AppxPackage -AllUsers -Name $name -ErrorAction SilentlyContinue)
$prov = @(Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -eq $name })
if ($pkgs.Count -eq 0 -and $prov.Count -eq 0) {
Write-Host "[=] $name : not installed"
Record $name 'not-installed'
continue
}
foreach ($p in $pkgs) {
try {
Write-Host "[*] Remove-AppxPackage $($p.PackageFullName)"
Remove-AppxPackage -AllUsers -Package $p.PackageFullName -ErrorAction Stop
} catch {
# 0x80073D19 = NonRemovable system package. Worth noting but often unavoidable.
if ($_.Exception.Message -match '0x80073D19|NonRemovable') {
Write-Host "[!] $name marked NonRemovable by Windows - skipping"
Record $name 'nonremovable' $_.Exception.Message
} else {
Write-Host "[!] $name install removal failed: $($_.Exception.Message)"
Record $name 'failed-install' $_.Exception.Message
}
}
}
foreach ($p in $prov) {
try {
Remove-AppxProvisionedPackage -Online -PackageName $p.PackageName -ErrorAction Stop | Out-Null
Write-Host "[*] Remove-AppxProvisionedPackage $($p.PackageName)"
} catch {
# Provisioned removal often fails silently when package was already deprovisioned by other tools.
# Only worth logging, not flagging as a hard failure.
}
}
# Verify post-state
$after = @(Get-AppxPackage -AllUsers -Name $name -ErrorAction SilentlyContinue)
if ($after.Count -eq 0) {
Record $name 'removed'
} elseif (($results | Where-Object Name -eq $name | Select-Object -Last 1).Result -ne 'nonremovable') {
Record $name 'still-present' "after removal attempt: $(($after | Select -Expand PackageFullName) -join ',')"
}
}
Write-Host ""
Write-Host "===== Office (M365) removal ====="
if ($cfg.RemoveOffice) {
$c2rClient = 'C:\Program Files\Common Files\microsoft shared\ClickToRun\OfficeClickToRun.exe'
if (Test-Path $c2rClient) {
Write-Host "[*] Click-to-Run detected - running silent uninstall"
$p = Start-Process -FilePath $c2rClient -ArgumentList 'scenario=uninstall','sourcetype=None','DisplayLevel=False' -Wait -PassThru
Write-Host "[=] OfficeClickToRun exited $($p.ExitCode)"
} else {
Write-Host "[=] No Click-to-Run Office present"
}
$anyOfficeAppx = $false
foreach ($pat in @('Microsoft.MicrosoftOfficeHub','Microsoft.Office.*','Microsoft.OutlookForWindows')) {
$hit = @(Get-AppxPackage -AllUsers -Name $pat -ErrorAction SilentlyContinue)
foreach ($p in $hit) {
$anyOfficeAppx = $true
try {
Write-Host "[*] Remove-AppxPackage $($p.PackageFullName)"
Remove-AppxPackage -AllUsers -Package $p.PackageFullName -ErrorAction Stop
Record $p.Name 'removed'
} catch {
Write-Host "[!] $($p.Name) failed: $($_.Exception.Message)"
Record $p.Name 'failed-install' $_.Exception.Message
}
}
}
if ((-not (Test-Path $c2rClient)) -and (-not $anyOfficeAppx)) {
Write-Host "[=] No Office / M365 traces found - nothing to do"
}
}
Write-Host ""
Write-Host "===== Summary ====="
$byResult = $results | Group-Object Result | Sort-Object Name
foreach ($g in $byResult) {
Write-Host ("{0,-15}: {1}" -f $g.Name, $g.Count)
}
$problem = $results | Where-Object { $_.Result -in 'failed-install','still-present' }
if ($problem) {
Write-Host ""
Write-Host "Needs attention:"
$problem | ForEach-Object { Write-Host " - $($_.Name) [$($_.Result)] $($_.Detail)" }
}