|
| 1 | +Set-ExecutionPolicy Bypass -Scope Process -Force |
| 2 | +Clear-Host |
| 3 | + |
| 4 | +<# |
| 5 | +.SYNOPSIS |
| 6 | + Quietly updates all Microsoft Store apps using winget (v1.11.x compatible). |
| 7 | +
|
| 8 | +.DESCRIPTION |
| 9 | + - Runs fully non-interactive (no prompts). |
| 10 | + - Properly orders command + flags for winget 1.11.x. |
| 11 | + - Accepts msstore/package agreements on upgrade/install only. |
| 12 | + - Uses include-unknown and a forced second pass to catch stubborn Store apps. |
| 13 | + - Detects if winget/App Installer updated itself mid-run and retries once. |
| 14 | + - Executes winget by absolute path (no App Execution Alias quirks). |
| 15 | + - Logs to C:\ProgramData\Winget-StoreUpgrade\upgrade-YYYYMMDD-HHMMSS.log |
| 16 | +
|
| 17 | +.NOTES |
| 18 | + Recommended to run in an elevated session to service machine-wide apps. |
| 19 | +#> |
| 20 | + |
| 21 | +# ===== Global non-interactive settings ===== |
| 22 | +$ErrorActionPreference = 'Stop' |
| 23 | +$ProgressPreference = 'SilentlyContinue' |
| 24 | +$env:WINGET_DISABLE_INTERACTIVITY = '1' # environment-based; no CLI side effects |
| 25 | + |
| 26 | +# ===== Logging ===== |
| 27 | +$LogRoot = Join-Path $env:ProgramData 'Winget-StoreUpgrade' |
| 28 | +if (-not (Test-Path $LogRoot)) { New-Item -Path $LogRoot -ItemType Directory -Force | Out-Null } |
| 29 | +$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' |
| 30 | +$LogFile = Join-Path $LogRoot "upgrade-$Timestamp.log" |
| 31 | + |
| 32 | +function Write-Info { param([string]$m) "[INFO ] $m" | Tee-Object -FilePath $LogFile -Append } |
| 33 | +function Write-Warn { param([string]$m) "[WARN ] $m" | Tee-Object -FilePath $LogFile -Append } |
| 34 | +function Write-Err { param([string]$m) "[ERROR] $m" | Tee-Object -FilePath $LogFile -Append } |
| 35 | + |
| 36 | +Write-Info "Log file: $LogFile" |
| 37 | +Write-Info "Starting Microsoft Store updates..." |
| 38 | + |
| 39 | +# ===== Robust winget resolution and invoker ===== |
| 40 | +function Resolve-WinGetExe { |
| 41 | + # Prefer packaged App Installer location (more stable than user alias) |
| 42 | + $pkg = Get-AppxPackage -Name Microsoft.DesktopAppInstaller -ErrorAction SilentlyContinue |
| 43 | + if ($pkg) { |
| 44 | + $candidate = Join-Path $pkg.InstallLocation 'winget.exe' |
| 45 | + if (Test-Path $candidate) { return $candidate } |
| 46 | + } |
| 47 | + # Fallback to whatever PowerShell resolves |
| 48 | + return (Get-Command winget.exe -ErrorAction Stop).Source |
| 49 | +} |
| 50 | + |
| 51 | +$script:WinGetExe = Resolve-WinGetExe |
| 52 | + |
| 53 | +function Invoke-WinGet { |
| 54 | + param( |
| 55 | + [Parameter(Mandatory)][string[]]$Args, # e.g. @('upgrade','--all',...) |
| 56 | + [switch]$RetryOnSelfUpdate # retry once if winget self-updated |
| 57 | + ) |
| 58 | + # Execute the EXE directly — do NOT call 'winget' then pass a path |
| 59 | + $output = & $script:WinGetExe @Args 2>&1 |
| 60 | + $text = $output | Out-String |
| 61 | + $text | Tee-Object -FilePath $LogFile -Append | Out-Null |
| 62 | + |
| 63 | + if ($RetryOnSelfUpdate -and $text -match 'Restart the application to complete the upgrade') { |
| 64 | + Write-Info "winget/App Installer updated itself; re-resolving path and retrying once..." |
| 65 | + Start-Sleep -Milliseconds 500 |
| 66 | + $script:WinGetExe = Resolve-WinGetExe |
| 67 | + $output = & $script:WinGetExe @Args 2>&1 |
| 68 | + $text = $output | Out-String |
| 69 | + $text | Tee-Object -FilePath $LogFile -Append | Out-Null |
| 70 | + } |
| 71 | + return $text |
| 72 | +} |
| 73 | + |
| 74 | +# ===== Preflight: confirm winget exists ===== |
| 75 | +try { |
| 76 | + Invoke-WinGet -Args @('--version') | Out-Null |
| 77 | +} catch { |
| 78 | + Write-Err "winget (App Installer) not found. Install/update 'App Installer' from Microsoft Store and re-run." |
| 79 | + exit 1 |
| 80 | +} |
| 81 | + |
| 82 | +# ===== Ensure Microsoft Store Install Service is running (helps Store updates) ===== |
| 83 | +try { |
| 84 | + $svc = Get-Service -Name InstallService -ErrorAction SilentlyContinue |
| 85 | + if ($svc -and $svc.Status -ne 'Running') { |
| 86 | + Write-Info "Starting Microsoft Store Install Service (InstallService)..." |
| 87 | + Start-Service -Name InstallService -ErrorAction SilentlyContinue |
| 88 | + } |
| 89 | +} catch { Write-Warn "Could not verify/start InstallService. Continuing..." } |
| 90 | + |
| 91 | +# ===== Ensure msstore source exists and refresh (NO accept flags here) ===== |
| 92 | +try { |
| 93 | + $sources = Invoke-WinGet -Args @('source','list','--disable-interactivity') |
| 94 | + if ($sources -notmatch '(?im)^\s*msstore\b') { |
| 95 | + Write-Info "msstore source not found; resetting..." |
| 96 | + Invoke-WinGet -Args @('source','reset','--force','msstore','--disable-interactivity') |
| 97 | + } |
| 98 | + Invoke-WinGet -Args @('source','update','--disable-interactivity') | Out-Null |
| 99 | +} catch { |
| 100 | + Write-Warn "Winget source operations reported issues; continuing..." |
| 101 | +} |
| 102 | + |
| 103 | +# ===== Optional: prevent winget self-upgrade during the session (pin App Installer) ===== |
| 104 | +# (Uncomment if you want to avoid mid-run self-update entirely) |
| 105 | +# Invoke-WinGet -Args @('pin','add','--id','Microsoft.AppInstaller','--disable-interactivity') | Out-Null |
| 106 | + |
| 107 | +# ===== PASS 1: Accept msstore terms & upgrade what winget can detect ===== |
| 108 | +Write-Info "Pass 1: upgrading Microsoft Store apps (include-unknown)..." |
| 109 | +Invoke-WinGet -Args @( |
| 110 | + 'upgrade','--all', |
| 111 | + '--source','msstore', |
| 112 | + '--include-unknown', |
| 113 | + '--silent', |
| 114 | + '--accept-source-agreements','--accept-package-agreements', |
| 115 | + '--disable-interactivity' |
| 116 | +) -RetryOnSelfUpdate | Out-Null |
| 117 | + |
| 118 | +# ===== PASS 2: Force re-install latest for stragglers where version compare is unknown ===== |
| 119 | +Write-Info "Pass 2: forced upgrade (msstore) for remaining/unknown version apps..." |
| 120 | +Invoke-WinGet -Args @( |
| 121 | + 'upgrade','--all', |
| 122 | + '--source','msstore', |
| 123 | + '--include-unknown', |
| 124 | + '--force', |
| 125 | + '--silent', |
| 126 | + '--accept-source-agreements','--accept-package-agreements', |
| 127 | + '--disable-interactivity' |
| 128 | +) -RetryOnSelfUpdate | Out-Null |
| 129 | + |
| 130 | +# ===== OPTIONAL Safety net pass: catch apps mapped under other sources ===== |
| 131 | +Write-Info "Safety net: unfiltered pass to catch any remaining packages..." |
| 132 | +Invoke-WinGet -Args @( |
| 133 | + 'upgrade','--all', |
| 134 | + '--include-unknown', |
| 135 | + '--silent', |
| 136 | + '--accept-source-agreements','--accept-package-agreements', |
| 137 | + '--disable-interactivity' |
| 138 | +) -RetryOnSelfUpdate | Out-Null |
| 139 | + |
| 140 | +# ===== Summary: show any remaining Store upgrades (no accept flags here) ===== |
| 141 | +Write-Info "Summary check for remaining Microsoft Store upgrades..." |
| 142 | +Invoke-WinGet -Args @('upgrade','--source','msstore','--disable-interactivity') | Out-Null |
| 143 | + |
| 144 | +# ===== Optional: unpin App Installer after run ===== |
| 145 | +# Invoke-WinGet -Args @('pin','remove','--id','Microsoft.AppInstaller','--disable-interactivity') | Out-Null |
| 146 | + |
| 147 | +Write-Info "Completed. Full log: $LogFile" |
0 commit comments