#Requires -Version 5.1 [CmdletBinding(SupportsShouldProcess)] param( [ValidateSet('List','Plan','Apply')][string]$Mode = 'Plan', [string[]]$Browser = @('Zen'), [string[]]$ExtraId = @(), [string[]]$ExcludeId = @(), [switch]$SkipBundled, [switch]$KeepOneDrive, [switch]$KeepEdge, [switch]$RemovePersonal, [switch]$RemoveStore, [switch]$Json, [Parameter(DontShow)][object[]]$ReviewedPlan ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module (Join-Path $PSScriptRoot 'src/Wind.Core.psm1') Import-Module (Join-Path $PSScriptRoot 'src/Wind.Apps.psm1') $Browser=@($Browser|ForEach-Object{$_.Split(',')}|ForEach-Object{$_.Trim()}|Where-Object{$_}|Select-Object -Unique) if(@($Browser|Where-Object{$_ -notin @('Zen','Helium','Tor')}).Count){throw 'Browser must contain only Zen, Helium or Tor.'} if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required.' } if ($Json -and $Mode -eq 'Apply') { throw '-Json is for List/Plan. Apply displays installer output.' } $inventory = @(Get-WindAppInventory) $selection = @{ ExtraId=$ExtraId; ExcludeId=$ExcludeId; SkipBundled=$SkipBundled; KeepOneDrive=$KeepOneDrive; KeepEdge=$KeepEdge; RemovePersonal=$RemovePersonal; RemoveStore=$RemoveStore } $plan = @(Get-WindAppPlan -Inventory $inventory @selection) if ($PSBoundParameters.ContainsKey('ReviewedPlan')) { $reviewed = @($ReviewedPlan | Where-Object Status -eq 'Remove' | Sort-Object Id | Select-Object Id,Name,Version,Publisher,Kind,Scope,Target) $fresh = @($plan | Where-Object Status -eq 'Remove' | Sort-Object Id | Select-Object Id,Name,Version,Publisher,Kind,Scope,Target) if ((ConvertTo-Json -InputObject $reviewed -Compress) -cne (ConvertTo-Json -InputObject $fresh -Compress)) { throw 'The app plan changed since review. Review the refreshed list before applying.' } } if ($Mode -eq 'List') { if ($Json) { ConvertTo-Json -InputObject @($plan) -Depth 5 } else { $plan | Select-Object Name,Status,Scope,Id,Reason | Format-Table -Wrap } return } if ($Json) { [pscustomobject]@{ Browsers=@($Browser); Apps=@($plan | Where-Object Status -ne 'Keep'); Scope='Appx: current user. Desktop: displayed registration scope.' } | ConvertTo-Json -Depth 6 return } Write-Output ('Browsers: ' + ($Browser -join ', ')) $plan | Where-Object Status -ne 'Keep' | Select-Object Name,Status,Scope,Reason | Format-Table -Wrap Write-Output 'Removal can delete app settings/data. Export mail, browser data and unsynced work first. OneDrive cloud-only files remain online; folders are not manually deleted.' Write-Output $(if($RemoveStore){'Microsoft Store is selected for current-user removal. Reinstallation may require an official recovery path.'}else{'Microsoft Store is protected unless -RemoveStore is explicitly selected.'}) Write-Output 'WebView2, runtimes, security tools, drivers, servicing and recovery components remain protected. Appx removal affects this user; provisioning and other users are unchanged.' if ($Mode -eq 'Plan') { return } if (-not $PSCmdlet.ShouldProcess('Displayed app plan', 'Install selected browsers and remove selected apps')) { return } Assert-WindMutationAllowed $context = Get-WindContext if ($context.Build -notin @(19045,22631,26100,26200)) { throw 'App changes are gated to Windows builds 19045, 22631, 26100 and 26200. Use Windows Installed apps on other builds.' } $mutex = [Threading.Mutex]::new($false,'Global\MsPrivatePolicyTransaction') $locked = $false try { try { $locked = $mutex.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true } if (-not $locked) { throw 'Another ms-private transaction is running.' } $reportRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MsPrivate\apps' [IO.Directory]::CreateDirectory($reportRoot) | Out-Null $reportPath = Join-Path $reportRoot ((Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N') + '.json') $journalWriter = Get-Command Save-WindJournal -CommandType Function $save = { param($data) & $journalWriter -Journal $data -Path $reportPath }.GetNewClosure() Write-Output "Operation report: $reportPath" $result = Invoke-WindAppChanges -Plan $plan -Browser $Browser -Backend (New-WindAppBackend) -Save $save -Confirm:$false Write-Output "App operation: $($result.Status). Set your default browser in Windows Settings and verify it opens links." } finally { if ($locked) { $mutex.ReleaseMutex() } $mutex.Dispose() }