Notes on winget

With the Windows Package Manager (winget), it is easy to install software from the command line. This makes your installs scriptable and thus repeatable. This article is the WinGet counterpart to my previous Notes on Chocolatey: it shows how to reproduce a machine's installation instructions and update your packages regularly.

Install WinGet

WinGet is supported on Windows 11, Windows 10 version 1809 (build 17763) and later, and Windows Server 2025. On Windows desktop, it is delivered as part of App Installer through the Microsoft Store. On a normal, up-to-date Windows 10 or Windows 11 installation, winget is usually already available. Check it from PowerShell:

winget --version
winget source update

If winget is missing or broken, update App Installer from the Microsoft Store. The PowerShell repair command can also install the latest WinGet client:

Repair-WinGetPackageManager -Latest -Force

Listing installation instructions

Let's generate installation instructions from the applications on a machine. WinGet exports the packages it recognizes to JSON, which we can turn into individual install commands:

& {
  $exportPath = Join-Path $env:TEMP "winget-packages-$([guid]::NewGuid()).json"

  try {
    $exportOutput = & winget export --output $exportPath --disable-interactivity 2>&1
    $exitCode = $LASTEXITCODE

    if ($exitCode -ne 0) {
      throw "WinGet export failed (exit code ${exitCode}):`n$($exportOutput -join [Environment]::NewLine)"
    }

    $export = Get-Content -LiteralPath $exportPath -Raw |
      ConvertFrom-Json -ErrorAction Stop

    foreach ($source in ($export.Sources | Sort-Object { $_.SourceDetails.Name })) {
      $sourceName = $source.SourceDetails.Name.Replace("'", "''")

      foreach ($package in ($source.Packages | Sort-Object PackageIdentifier)) {
        $packageId = $package.PackageIdentifier.Replace("'", "''")
        'winget install --id ''{0}'' --exact --source ''{1}''' -f $packageId, $sourceName
      }
    }
  }
  finally {
    Remove-Item -LiteralPath $exportPath -Force -ErrorAction SilentlyContinue
  }
}

This prints commands you can copy into an installation script; it does not install anything. Each command includes the package's source, so Microsoft Store packages keep --source 'msstore'.

Upgrade Task

Installing packages is one thing, upgrading them regularly is another. Let's create a weekly Windows Task Scheduler task for upgrading. It uses an elevated PowerShell session to do so:

& {
  $day = 'Monday'
  $time = '9:30AM'
  $taskName = 'UpgradeWingetPackages'
  $scriptPath = Join-Path $env:LOCALAPPDATA 'Upgrade-WingetPackages.ps1'

  $upgradeScript = @'
winget source update
if ($LASTEXITCODE -ne 0) {
    throw "WinGet source update failed (exit code $LASTEXITCODE)."
}

winget upgrade --all `
    --silent `
    --disable-interactivity `
    --accept-source-agreements `
    --accept-package-agreements
if ($LASTEXITCODE -ne 0) {
    throw "WinGet upgrade failed (exit code $LASTEXITCODE)."
}
'@

  Set-Content -LiteralPath $scriptPath -Value $upgradeScript -Encoding UTF8

  $quotedScriptPath = $scriptPath.Replace("'", "''")
  $launcher = @"
`$arguments = '-NoProfile -ExecutionPolicy Bypass -File `"$quotedScriptPath`"'
`$process = Start-Process -FilePath 'powershell.exe' -ArgumentList `$arguments -Verb RunAs -Wait -PassThru
exit `$process.ExitCode
"@
  $encodedLauncher = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($launcher))

  $action = New-ScheduledTaskAction `
    -Execute 'powershell.exe' `
    -Argument "-NoProfile -EncodedCommand $encodedLauncher"

  $trigger = New-ScheduledTaskTrigger `
    -Weekly `
    -DaysOfWeek $day `
    -At $time

  $settings = New-ScheduledTaskSettingsSet `
    -AllowStartIfOnBatteries `
    -DontStopIfGoingOnBatteries `
    -StartWhenAvailable `
    -RunOnlyIfNetworkAvailable

  $principal = New-ScheduledTaskPrincipal `
    -UserId ([Security.Principal.WindowsIdentity]::GetCurrent().Name) `
    -LogonType Interactive `
    -RunLevel Limited

  $taskExists = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
  if ($taskExists) {
    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
  }

  Register-ScheduledTask `
    -Action $action `
    -Trigger $trigger `
    -TaskName $taskName `
    -Description 'Upgrade all WinGet packages' `
    -Settings $settings `
    -Principal $principal
}

Make Upgrade Task Compatible with MakeMeAdmin

At Wehkamp we use Make Me Admin. You need to claim admin rights before running administrative tasks. The regular UAC prompt does not let you claim those rights first.

This script updates the task action to launch Make Me Admin, waits for you to request temporary admin rights, and then start the elevated upgrade:

& {
  $taskName = 'UpgradeWingetPackages'
  $taskExists = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
  if (-not $taskExists) {
    throw "Task '$taskName' does not exist. Run upgrade-task.ps1 first."
  }

  $makeMeAdminDir = Join-Path $env:ProgramFiles 'Make Me Admin'
  $makeMeAdminPath = Join-Path $makeMeAdminDir 'MakeMeAdminUI.exe'
  $scriptPath = Join-Path $env:LOCALAPPDATA 'Upgrade-WingetPackages.ps1'

  if (-not (Test-Path -LiteralPath $makeMeAdminPath)) {
    throw "Make Me Admin was not found at '$makeMeAdminPath'."
  }

  if (-not (Test-Path -LiteralPath $scriptPath)) {
    throw "The WinGet upgrade script was not found at '$scriptPath'. Run upgrade-task.ps1 first."
  }

  $quotedMakeMeAdminDir = $makeMeAdminDir.Replace("'", "''")
  $quotedScriptPath = $scriptPath.Replace("'", "''")
  $launcher = @"
Set-Location -LiteralPath '$quotedMakeMeAdminDir'
& (Join-Path '$quotedMakeMeAdminDir' 'MakeMeAdminUI.exe')
Read-Host 'Make sure you are admin before proceeding...'
`$arguments = '-NoProfile -ExecutionPolicy Bypass -File `"$quotedScriptPath`"'
`$process = Start-Process -FilePath 'powershell.exe' -ArgumentList `$arguments -Verb RunAs -Wait -PassThru
exit `$process.ExitCode
"@
  $encodedLauncher = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($launcher))

  $newAction = New-ScheduledTaskAction `
    -Execute 'powershell.exe' `
    -Argument "-NoProfile -EncodedCommand $encodedLauncher"

  Unregister-ScheduledTask -TaskName $taskName -Confirm:$false

  Register-ScheduledTask `
    -Action $newAction `
    -Trigger $taskExists.Triggers `
    -TaskName $taskName `
    -Description 'Upgrade all WinGet packages with Make Me Admin' `
    -Settings $taskExists.Settings `
    -Principal $taskExists.Principal
}

Skip an upgrade

WinGet supports pins for packages that should not be upgraded automatically. For example:

winget pin add --id Git.Git
winget pin list
winget pin remove --id Git.Git

A package with a non-blocking pin can still be included explicitly when upgrading everything:

winget upgrade --all --include-pinned

My arbitrary list of installed packages

I use my articles as notes to my future self. When I get a new machine, I usually install the following packages.

& {
  # development tools
  ## AWS CLI and credentials management for AWS development
  winget install Amazon.AWSCLI ByteNess.AWSVault --exact
  ## Kiro AI development environment
  winget install Amazon.Kiro --exact
  ## Docker desktop alternative and image layer inspection
  winget install SUSE.RancherDesktop wagoodman.dive --exact
  ## Build and deploy cloud-native applications
  winget install Google.ContainerTools.Skaffold --exact
  ## Git, GitHub, signing, and SSH tools
  winget install Git.Git GitHub.cli GnuPG.Gpg4win xhcoding.sshpass-win32 --exact
  ## Edit YAML and JSON files
  winget install MikeFarah.yq jqlang.jq --exact
  ## Node.js version management for Windows
  winget install CoreyButler.NVMforWindows --exact
  ## .NET 10 SDK
  winget install Microsoft.DotNet.SDK.10 --exact
  ## IDEs and development assistants
  winget install Microsoft.VisualStudioCode SST.opencode StablyAI.Orca --exact
  ## Compare files and directories
  winget install ScooterSoftware.BeyondCompare.5 --exact
  ## GNU-compatible command-line utilities for Windows
  winget install Microsoft.Coreutils ezwinports.make --exact

  # image and video tools
  ## Raster image and SVG editing
  winget install dotPDN.PaintDotNet Inkscape.Inkscape --exact
  ## Create diagrams
  winget install JGraph.Draw --exact
  ## Screen recording and video playback
  winget install NickeManarin.ScreenToGif VideoLAN.VLC --exact
  ## FFmpeg tools for video processing and yt-dlp
  winget install Gyan.FFmpeg yt-dlp.FFmpeg yt-dlp.yt-dlp --exact

  # tools
  ## Browse the web
  winget install Brave.Brave --exact
  ## Compare and copy files
  winget install CodeSector.TeraCopy --exact
  ## Find what is using disk space
  winget install JAMSoftware.TreeSize.Free --exact
  ## Configure Logitech peripherals
  winget install Logitech.Options --exact
  ## PowerShell and Windows productivity tools
  winget install Microsoft.PowerShell Microsoft.PowerToys --exact
  ## OpenSSL command-line toolkit for working with certificates and secrets
  winget install ShiningLight.OpenSSL.Light --exact

  # other
  ## Manage and update Ravensburger tiptoi devices
  winget install RavensburgerAG.tiptoiManager --exact
  ## VPN client
  winget install SparkLabs.Viscosity --exact

  # Microsoft Store apps
  ## Daily Bing and Windows Spotlight wallpapers
  winget install --id 9NBLGGH1ZBKW --exact --source msstore
  ## WhatsApp
  winget install --id 9NKSQGP7F2NH --exact --source msstore
  ## ChatGPT
  winget install --id 9PLM9XGG6VKS --exact --source msstore
  ## Spotify
  winget install --id 9NCBCSZSJRSB --exact --source msstore
}

Beyond Compare has separate WinGet packages for versions 4 and 5. I used version 5 above. Package availability can change; use winget search to check before installing.

I like the fact that we can also directly install packages I normally install from the store.

Post installation

After installing the tools, you might want to install a version of Node.js and some global dependencies. These are my go-to global dependencies:

& {
  # download and use Node.js 24 LTS
  nvm install 24
  nvm use 24

  # enable ncu (npm-check-updates) globally for
  # easy updating of node applications:
  npm install --global npm-check-updates
}

Enjoy!

The code is on GitHub, so check it out: 18.winget.

expand_less brightness_auto