A small collection of PowerShell utilities for working with the Windows Package Manager (winget) beyond what the official CLI exposes.
The repository includes a manual sandbox workflow at .github/workflows/extract-icons.yml for one batch at a time, plus .github/workflows/extract-icons-campaign.yml for scheduling a full multi-batch campaign inside GitHub Actions and checking back later.
The workflow is best-effort by design: package-level install or extraction failures are recorded in metadata.json for the affected package, but they do not block the rest of the batch from being published or auto-committed.
The repository now also includes project skills under .agents/skills/ for prompt-driven extraction campaigns and fast package-index lookups.
The extraction skill at .agents/skills/winget-extract-icons/ is intended to let an agent handle the whole loop from a simple request such as “extract 10 more winget app icons”:
- select the next unprocessed package IDs from the cached
svrooij/winget-pkgs-indexcatalog - build a validated campaign plan locally
- dispatch one CI-native campaign workflow run with traceable campaign and batch labels
- let Actions sequence the later batch jobs without a live local watcher
- rely on workflow auto-commit to update
winget-app-icons/ - query CI state later instead of depending on a local lock file
The skill wrapper script is .agents/skills/winget-extract-icons/scripts/run-default-campaign.ps1, which now defaults to CI-native campaign dispatch. The helper .agents/skills/winget-extract-icons/scripts/get-ci-campaign-status.ps1 summarizes campaign progress later from GitHub Actions.
The index skill at .agents/skills/winget-package-index/ uses svrooij/winget-pkgs-index as a fast availability source through a cached out/cache/winget-pkgs-index/index.v2.json file parsed in PowerShell instead of local winget package-index probes. Its wrapper script is .agents/skills/winget-package-index/scripts/run-index-backed-campaign.ps1, which calls the same campaign runner with repository-friendly index-cache defaults.
The catalog skill at .agents/skills/winget-icon-catalog/ queries the local winget-app-icons/ registry so an agent can count packages with icons, list failures, filter by failure category, and summarize extraction reasons from metadata.json.
extract-icons.yml inputs:
| Input | Description |
|---|---|
package_ids_csv |
Required comma-separated list of exact winget package IDs. Maximum 25 package IDs per run. |
uninstall_after |
When true, the workflow uninstalls each package after extraction. |
per_package_timeout |
Install timeout in seconds for each package. |
auto_commit_results |
When true, the workflow also commits the refreshed package folders back to the repository. |
campaign_id |
Optional automation-supplied campaign identifier used to label and correlate runs. |
batch_index / batch_total |
Optional automation metadata for multi-batch campaigns. |
dispatch_token |
Optional unique token used by the campaign runner to match a workflow-dispatch call to the resulting Actions run. |
request_label |
Optional run label shown in the Actions UI and workflow summaries. |
extract-icons-campaign.yml inputs:
| Input | Description |
|---|---|
campaign_id |
Required campaign identifier shown in the Actions UI. |
campaign_run_label |
Optional campaign run label shown in the Actions UI. |
campaign_gzip_base64 |
Required compressed campaign plan payload created from Invoke-IconExtractionCampaign.ps1 -Mode plan. |
uninstall_after |
When true, each batch uninstalls packages after extraction. |
per_package_timeout |
Install timeout in seconds for each package in each batch. |
auto_commit_results |
When true, each batch auto-commits refreshed package folders. |
continue_on_batch_failure |
When true, later batches still run if an earlier batch fails. |
Every processed package gets its own folder under winget-app-icons/<PackageId>/.
Files written per package:
metadata.jsonis always written. Its presence means that package has already gone through the extraction pipeline at least once.app-icon.icois written only when the latest run extracted a canonical icon successfully.
metadata.json includes the latest attempt status, timestamps, install and extract timings, uninstall timing, exit codes, icon hashes, and other run details. If a package is refreshed and the new run does not find an icon, any stale app-icon.ico is removed so the folder reflects the latest result.
The workflow always uploads a batch artifact containing:
winget-app-icons-batch-<run_id>.zipsummary.jsonrequested-packages.json
The zip contains only the winget-app-icons/<PackageId>/... folders for that batch, so you can extract it at the repository root and overwrite the existing winget-app-icons tree.
When auto_commit_results is enabled, the workflow imports that same batch artifact and commits only the refreshed package folders.
Builds and optionally executes a repeatable GitHub Actions extraction campaign for a large package set (for example, 100 packages split into 10-package batches).
What it does:
- Parses candidate IDs from
-CandidatePathwhen provided, otherwise selects candidates from the cachedsvrooij/winget-pkgs-indexcatalog. - Excludes IDs already present under
winget-app-icons/unless-IncludeExistingis set. - Validates each selected package ID against the cached
svrooij/winget-pkgs-indexJSON file. - Writes a campaign plan JSON file containing selected IDs and batch CSV payloads.
- Writes a status TSV alongside the plan so automation can summarize batch outcomes without scraping multiple terminals.
- In
runmode, dispatches.github/workflows/extract-icons.ymlper batch. - That
runmode is now the legacy local-watcher path; CI-native campaign dispatch uses the same plan output but runs later batches entirely in Actions. - Waits for existing workflow-dispatch runs to finish before dispatching the
next batch, then correlates each batch via
dispatch_tokenandrequest_label. - Optional: downloads each workflow artifact, expands
winget-app-icons-batch-<run_id>.zipat repo root, then commits only the requested package folders.
| Parameter | Description |
|---|---|
| `-Mode plan | run` |
-TargetCount |
Number of validated IDs to include (default 100). |
-BatchSize |
Packages per workflow run (default 10, max 25). |
-CampaignPath |
Output JSON plan path (default out/icon-campaign-100.json). |
-StatusPath |
Optional explicit path for the status TSV written during plan/run flows. |
-CampaignId |
Optional explicit campaign identifier used in local status files and workflow inputs. |
-DownloadAndImportArtifacts |
After each run, downloads artifact and imports extracted package folders locally. |
-PushAfterCommit |
With import mode, pushes each local commit to origin/master. |
# Create a validated 100-package, 10-batch plan only
.\scripts\Invoke-IconExtractionCampaign.ps1 -Mode plan
# Create a plan from a custom candidate file
.\scripts\Invoke-IconExtractionCampaign.ps1 -Mode plan -CandidatePath .\out\package-ids.txt
# Execute the campaign with manual artifact import + local commit/push
.\scripts\Invoke-IconExtractionCampaign.ps1 -Mode run -DownloadAndImportArtifacts -PushAfterCommit
# Execute with workflow auto-commit enabled (no local artifact import)
.\scripts\Invoke-IconExtractionCampaign.ps1 -Mode run -AutoCommitResults $true
# Plan with the external svrooij package index JSON cache
.\scripts\Invoke-IconExtractionCampaign.ps1 -Mode plan -RefreshWingetIndexCache
# Schedule the plan as a CI-native campaign that can continue without a local watcher
.\.agents\skills\winget-extract-icons\scripts\start-ci-campaign.ps1 -TargetCount 100 -BatchSize 10
# Query a scheduled CI campaign later
.\.agents\skills\winget-extract-icons\scripts\get-ci-campaign-status.ps1 -CampaignId my-campaign-idGenerates cleaned package-manager databases from
unigetui/screenshot-database-v2.json:
unigetui/choco-database.jsonunigetui/winget-database.jsonunigetui/scoop-database.jsonunigetui/python-database.jsonunigetui/npm-database.json
Each output record keeps the original UniGetUI key under unigetui, the
manager-specific package ID for that database, the mapped package IDs in the
other supported managers when confident matches exist, and the original icon
/ images payload.
The script follows UniGetUI's documented normalized IDs:
- IDs are lowercased.
- Spaces, underscores, and dots are replaced with dashes.
- WinGet IDs drop the publisher segment before normalization.
- Chocolatey IDs drop
.installand.portablebefore normalization. - Scoop package IDs follow the general normalized-ID rules.
- Python package IDs follow PEP 503 canonicalization.
- npm package IDs drop the leading
@on scoped packages for the UniGetUI key.
Matching order is intentionally conservative:
- exact package ID match
- exact normalized-ID match
- separator-insensitive normalized fallback for cases like
xmediarecodevsxmedia-recode - WinGet-only full-ID alias fallback for source keys that still include more of the original package ID than UniGetUI's standard Winget normalization
If multiple catalog packages still match after those steps, the cross-manager
field is left null instead of guessing.
| Parameter | Description |
|---|---|
-SourcePath |
Source UniGetUI JSON file. Default: unigetui/screenshot-database-v2.json. |
-OutDir |
Output directory for the generated databases. Default: unigetui/. |
-ChocoOutputPath |
Optional explicit output path for choco-database.json. |
-WingetOutputPath |
Optional explicit output path for winget-database.json. |
-ScoopOutputPath |
Optional explicit output path for scoop-database.json. |
-PythonOutputPath |
Optional explicit output path for python-database.json. |
-NpmOutputPath |
Optional explicit output path for npm-database.json. |
-PassThru |
Emit a summary object with source and output counts. |
.\unigetui\scripts\Generate-UniGetUiPackageDatabases.ps1 -PassThru- Chocolatey package IDs are pulled from the public OData feed.
- WinGet package IDs are pulled from the official
source.msixindex database. - Scoop package IDs are pulled from the official Scoop buckets on GitHub.
- Python package IDs are filtered from the PyPI Simple API index.
- npm package IDs are filtered from the npm replicate catalog.
- The generated files are deterministic for a fixed source JSON and fixed upstream package catalogs.
- Ambiguous cross-manager mappings are intentionally preserved as
null.
Summarizes which UniGetUI source keys still are not represented by any generated database record after running the database generator.
The report is intentionally lightweight and helps separate two broad cases:
- likely alias or package-variant keys such as
7zip-alpha-exewhose base form may already be covered - likely unsupported-manager or genuinely unmapped keys that still need another source or a manual alias rule
| Parameter | Description |
|---|---|
-SourcePath |
Source UniGetUI JSON file. Default: unigetui/screenshot-database-v2.json. |
-DatabasePaths |
Generated databases to compare against. Defaults to the five package-manager outputs under unigetui/. |
-ReportPath |
Optional JSON output path for the generated report. |
-SampleCount |
Number of sample items to emit for unmatched keys and alias examples. Default: 20. |
-PassThru |
Emit the report object instead of printing JSON text. |
.\unigetui\scripts\Get-UniGetUiUnmatchedReport.ps1 -PassThruFetches the raw, unlocalized WinGet manifest for a package — without going
through winget show (which reformats and localizes its output).
The script tries up to three strategies, in priority order, and exposes knobs to force a specific one:
- FileCache — reads the cached YAML manifest from
%TEMP%\WinGet\...\cache\V{1,2}_M\{SourceFamilyName}\.... Fast and offline. Only populated forMicrosoft.PreIndexed.Packagesources after winget has already fetched that manifest. - CDN — issues a direct HTTP GET to the source's base URL using the
winget PreIndexed path layout:
{base}/manifests/{c}/{Publisher}/{Package}/{Version}/{PackageId}.yaml. Requires-Version. V2 hash-named manifests cannot be reached this way. - REST API — issues
GET {base}/packageManifests/{PackageId}against aMicrosoft.Restsource. Returns JSON natively.
| Parameter | Description |
|---|---|
-PackageId (required) |
Exact WinGet identifier, e.g. Git.Git. |
-Version |
Specific version to fetch. Required for CDN strategy when FileCache is empty. |
-PathOnly |
Output only the path to the cached manifest file. Requires a FileCache hit. |
-WarmCache |
Run winget show first to populate the FileCache. |
-SourceName |
Target a non-default source (enterprise, self-hosted, REST). |
-AsYaml / -AsJson |
Force output format. Converts between YAML and JSON as needed (requires the Yayaml module). |
-Mode |
Auto (default), FileCache (offline), or Online (skip local cache). |
# Default: FileCache first, fall back to online fetch
.\scripts\Get-WinGetManifest.ps1 -PackageId Git.Git
# Offline only — never touch the network
.\scripts\Get-WinGetManifest.ps1 -PackageId Git.Git -Mode FileCache
# Guaranteed local read (winget warms, then we read from disk)
.\scripts\Get-WinGetManifest.ps1 -PackageId Git.Git -Mode FileCache -WarmCache -PathOnly | Get-Content
# Force a fresh online fetch — skip whatever is cached
.\scripts\Get-WinGetManifest.ps1 -PackageId Git.Git -Version 2.47.1.2 -Mode Online
# Get JSON output from a YAML community source
.\scripts\Get-WinGetManifest.ps1 -PackageId Git.Git -AsJson
# Target a non-default REST source
.\scripts\Get-WinGetManifest.ps1 -PackageId Contoso.App -SourceName MyEnterpriseSourceExtracts the raw .ico of an installed WinGet package — the same way winget
itself does it (see winget-cli's IconExtraction.cpp and ARPHelper.cpp),
but exposed as a stand-alone script. Uses Get-WinGetManifest.ps1 to resolve
the WinGet PackageId to one or more correlation hints (ProductCode and/or
DisplayName + Publisher), walks the Uninstall registry hives across both
WOW64 views to find matching ARP entries, and then reproduces the C++ icon
extraction:
- MSI installs →
MsiGetProductInfoW(ProductIcon) - Everything else → the ARP
DisplayIconvalue - Path is unquoted + index parsed via
shlwapi, env vars expanded .icofiles are copied verbatim.exe/.dllsources are walked viaEnumResourceNamesEx(RT_GROUP_ICON, …)and reassembled into a properICONDIR+ICONDIRENTRY+ concatenatedRT_ICONpayloads, byte-for-byte matchingExtractIconFromBinaryFile
Native work is done in a small C# helper compiled on first call via
Add-Type — the script stays a single drop-in .ps1.
| Parameter | Description |
|---|---|
-PackageId (required) |
Exact WinGet identifier, e.g. Git.Git. |
-Scope |
User, Machine, or Both (default). Picks which Uninstall hives to search. |
-OutDir |
Output directory. Default: $env:TEMP\winget-icons. |
-Force |
Overwrite existing files. |
Output filename pattern: {SanitizedDisplayName}.{ProductCode}.ico.
Each emitted PSCustomObject has PackageId, ProductCode, DisplayName, Publisher, Hive, MatchKind, Source, IconIndex, IconPath, SizeBytes.
# Default: extract icon for an installed package
.\scripts\Get-WinGetIcon.ps1 -PackageId Git.Git
# Machine-scope only, custom output directory
.\scripts\Get-WinGetIcon.ps1 -PackageId Docker.DockerDesktop -Scope Machine -OutDir .\icons -Force
# Pipe the result for downstream use
.\scripts\Get-WinGetIcon.ps1 -PackageId Git.Git | Select-Object DisplayName, IconPath, SizeBytes- Requires the package to be installed locally — there's no out-of-the-box way to extract an icon for a package that has only been downloaded.
- MSIX / Microsoft Store packages are out of scope (no ARP entry; winget's
own
IconExtractiondoesn't handle them either). - Some MSI packages legitimately have no
ProductIconset; the script warns and exits 0 (faithful to winget's behavior).
-
PowerShell 7+
-
winget installed (used for source discovery and cache warming)
-
YayamlPowerShell module — only required for-AsYaml/-AsJsoncross-format conversion:Install-Module Yayaml
MIT