mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request Adds a new Copilot agent skill (`winmd-api-search`) that lets AI agents discover and explore Windows desktop APIs by searching a local cache of WinMD metadata. The skill covers Windows Platform SDK, WinAppSDK/WinUI, NuGet package WinMDs, and project-output WinMDs — providing full API surface details (types, members, enumeration values, namespaces) without needing external documentation lookups. **Key components:** - `.github/skills/winmd-api-search/SKILL.md` — Skill definition with usage instructions, search/detail workflows, and scoring guidance - `scripts/Invoke-WinMdQuery.ps1` — PowerShell query engine supporting actions: `search`, `type`, `members`, `enums`, `namespaces`, `stats`, `projects` - `scripts/Update-WinMdCache.ps1` — Orchestrator that builds the C# cache generator, discovers project files, and runs the generator - `scripts/cache-generator/CacheGenerator.csproj` + `Program.cs` — .NET console app using `System.Reflection.Metadata` to parse WinMD files from NuGet packages, project references, Windows SDK, and packages.config into per-package JSON caches - `scripts/cache-generator/Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props` — Empty isolation files to prevent repo-level Central Package Management and build targets from interfering with this standalone tool <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx <!-- Replace with issue number if applicable --> - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass — N/A: This is an offline agent skill (PowerShell + standalone .NET tool) with no integration into the main product build or runtime. Validated manually by running the cache generator across multiple project contexts (ColorPickerUI, CmdPal.UI, runner, ImageResizer, etc.) and exercising all query actions. - [ ] **Localization:** All end-user-facing strings can be localized — N/A: No end-user-facing strings; this is an internal developer/agent tool - [ ] **Dev docs:** Added/updated — The SKILL.md itself serves as the documentation - [ ] **New binaries:** Added on the required places — N/A: The cache generator is a standalone dev-time tool, not shipped in the installer - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: N/A <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments ### Cache Generator (`Program.cs`, ~1000 lines) A self-contained .NET console app that: 1. **Discovers WinMD sources** from four channels: - `project.assets.json` (PackageReference — modern .csproj/.vcxproj) - `packages.config` (legacy NuGet format) - `<ProjectReference>` bin/ output (class libraries producing `.winmd`) - Windows SDK `UnionMetadata/` (highest installed version) 2. **Parses WinMD files** using `System.Reflection.Metadata` / `PEReader` to extract: - Types (classes, structs, interfaces, enums, delegates) with full namespace - Members (methods with decoded signatures/parameters, properties with accessors, events) - Enum values - Base types and type kinds 3. **Outputs per-package JSON** under `Generated Files/winmd-cache/`: - `packages/<Id>/<Version>/meta.json` — package summary (type/member/namespace counts) - `packages/<Id>/<Version>/namespaces.json` — ordered namespace list - `packages/<Id>/<Version>/types/<Namespace>.json` — full type detail per namespace - `projects/<ProjectName>.json` — maps each project to its package set 4. **Deduplicates** at the package level — if a package+version is already cached, it's skipped on subsequent runs. ### Build Isolation Three empty MSBuild files (`Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props`) in the cache-generator folder prevent the repo's Central Package Management and shared build configuration from interfering with this standalone tool. ### Query Engine (`Invoke-WinMdQuery.ps1`) Supports seven actions: `search` (fuzzy text search across types/members), `type` (full detail for a specific type), `members` (filtered members of a type), `enums` (enumeration values), `namespaces` (list all namespaces), `stats` (cache statistics), and `projects` (list cached projects with their packages). <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed 1. **Cache generation:** Ran `Update-WinMdCache.ps1` across 310+ project files in the repo — 8 packages parsed, 316 reused from cache, all completed without errors 2. **Query testing on multiple projects:** - `ColorPickerUI` — verified Windows SDK baseline (7,023 types) - `Microsoft.CmdPal.UI.ViewModels` (after restore) — verified 13 packages, 49,799 types, 112,131 members including WinAppSDK, AdaptiveCards, CsWinRT, Win32Metadata - `runner` (C++ vcxproj) — verified packages.config fallback path - `ImageResizerExt` — verified project reference WinMD discovery 3. **All seven query actions validated:** `stats`, `search`, `namespaces`, `type`, `enums`, `members`, `projects` — all returned correct results 4. **Spell-check compliance:** SKILL.md vocabulary reviewed against repo's check-spelling dictionaries; replaced flagged words with standard alternatives --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
209 lines
7.4 KiB
PowerShell
209 lines
7.4 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Generate or refresh the WinMD cache for the Agent Skill.
|
|
|
|
.DESCRIPTION
|
|
Builds and runs the standalone cache generator to export cached JSON files
|
|
from all WinMD metadata found in project NuGet packages and Windows SDK.
|
|
|
|
The cache is per-package+version: if two projects reference the same
|
|
package at the same version, the WinMD data is parsed once and shared.
|
|
|
|
Supports single project or recursive scan of an entire repo.
|
|
|
|
.PARAMETER ProjectDir
|
|
Path to a project directory (contains .csproj/.vcxproj), or a project file itself.
|
|
Defaults to scanning the workspace root.
|
|
|
|
.PARAMETER Scan
|
|
Recursively discover all .csproj/.vcxproj files under ProjectDir.
|
|
|
|
.PARAMETER OutputDir
|
|
Path to the cache output directory. Defaults to "Generated Files\winmd-cache".
|
|
|
|
.EXAMPLE
|
|
.\Update-WinMdCache.ps1
|
|
.\Update-WinMdCache.ps1 -ProjectDir BlankWinUI
|
|
.\Update-WinMdCache.ps1 -Scan -ProjectDir .
|
|
.\Update-WinMdCache.ps1 -ProjectDir "src\MyApp\MyApp.csproj"
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$ProjectDir,
|
|
[switch]$Scan,
|
|
[string]$OutputDir = 'Generated Files\winmd-cache'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
|
|
# so workspace root is 4 levels up from $PSScriptRoot.
|
|
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path
|
|
$generatorProj = Join-Path (Join-Path $PSScriptRoot 'cache-generator') 'CacheGenerator.csproj'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# WinAppSDK version detection -- look only at the repo root folder (no recursion)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Get-WinAppSdkVersionFromDirectoryPackagesProps {
|
|
<#
|
|
.SYNOPSIS
|
|
Extract Microsoft.WindowsAppSDK version from a Directory.Packages.props
|
|
(Central Package Management) at the repo root.
|
|
#>
|
|
param([string]$RepoRoot)
|
|
$propsFile = Join-Path $RepoRoot 'Directory.Packages.props'
|
|
if (-not (Test-Path $propsFile)) { return $null }
|
|
try {
|
|
[xml]$xml = Get-Content $propsFile -Raw
|
|
$node = $xml.SelectNodes('//PackageVersion') |
|
|
Where-Object { $_.Include -eq 'Microsoft.WindowsAppSDK' } |
|
|
Select-Object -First 1
|
|
if ($node) { return $node.Version }
|
|
} catch {
|
|
Write-Verbose "Could not parse $propsFile : $_"
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Get-WinAppSdkVersionFromPackagesConfig {
|
|
<#
|
|
.SYNOPSIS
|
|
Extract Microsoft.WindowsAppSDK version from a packages.config at the repo root.
|
|
#>
|
|
param([string]$RepoRoot)
|
|
$configFile = Join-Path $RepoRoot 'packages.config'
|
|
if (-not (Test-Path $configFile)) { return $null }
|
|
try {
|
|
[xml]$xml = Get-Content $configFile -Raw
|
|
$node = $xml.SelectNodes('//package') |
|
|
Where-Object { $_.id -eq 'Microsoft.WindowsAppSDK' } |
|
|
Select-Object -First 1
|
|
if ($node) { return $node.version }
|
|
} catch {
|
|
Write-Verbose "Could not parse $configFile : $_"
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# Try Directory.Packages.props first (CPM), then packages.config
|
|
$winAppSdkVersion = Get-WinAppSdkVersionFromDirectoryPackagesProps -RepoRoot $root
|
|
if (-not $winAppSdkVersion) {
|
|
$winAppSdkVersion = Get-WinAppSdkVersionFromPackagesConfig -RepoRoot $root
|
|
}
|
|
if ($winAppSdkVersion) {
|
|
Write-Host "Detected WinAppSDK version from repo: $winAppSdkVersion" -ForegroundColor Cyan
|
|
} else {
|
|
Write-Host "No WinAppSDK version found at repo root; will use latest (Version=*)" -ForegroundColor Yellow
|
|
}
|
|
|
|
# Default: if no ProjectDir, scan the workspace root
|
|
if (-not $ProjectDir) {
|
|
$ProjectDir = $root
|
|
$Scan = $true
|
|
}
|
|
|
|
Push-Location $root
|
|
|
|
try {
|
|
# Detect installed .NET SDK -- require >= 8.0, prefer stable over preview
|
|
$dotnetSdks = dotnet --list-sdks 2>$null
|
|
$bestMajor = $dotnetSdks |
|
|
Where-Object { $_ -notmatch 'preview|rc|alpha|beta' } |
|
|
ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } |
|
|
Where-Object { $_ -ge 8 } |
|
|
Sort-Object -Descending |
|
|
Select-Object -First 1
|
|
|
|
# Fall back to preview SDKs if no stable SDK found
|
|
if (-not $bestMajor) {
|
|
$bestMajor = $dotnetSdks |
|
|
ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } |
|
|
Where-Object { $_ -ge 8 } |
|
|
Sort-Object -Descending |
|
|
Select-Object -First 1
|
|
}
|
|
|
|
if (-not $bestMajor) {
|
|
Write-Error "No .NET SDK >= 8.0 found. Install from https://dotnet.microsoft.com/download"
|
|
exit 1
|
|
}
|
|
|
|
$targetFramework = "net$bestMajor.0"
|
|
Write-Host "Using .NET SDK: $targetFramework" -ForegroundColor Cyan
|
|
|
|
# Build MSBuild properties -- pass detected WinAppSDK version when available
|
|
$sdkVersionProp = ''
|
|
if ($winAppSdkVersion) {
|
|
$sdkVersionProp = "-p:WinAppSdkVersion=$winAppSdkVersion"
|
|
}
|
|
|
|
Write-Host "Building cache generator..." -ForegroundColor Cyan
|
|
$restoreArgs = @($generatorProj, "-p:TargetFramework=$targetFramework", '--nologo', '-v', 'q')
|
|
if ($sdkVersionProp) { $restoreArgs += $sdkVersionProp }
|
|
dotnet restore @restoreArgs
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Restore failed"
|
|
exit 1
|
|
}
|
|
$buildArgs = @($generatorProj, '-c', 'Release', '--nologo', '-v', 'q', "-p:TargetFramework=$targetFramework", '--no-restore')
|
|
if ($sdkVersionProp) { $buildArgs += $sdkVersionProp }
|
|
dotnet build @buildArgs
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Build failed"
|
|
exit 1
|
|
}
|
|
|
|
# Run the built executable directly (avoids dotnet run target framework mismatch issues)
|
|
$generatorDir = Join-Path $PSScriptRoot 'cache-generator'
|
|
$exePath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.exe"
|
|
if (-not (Test-Path $exePath)) {
|
|
# Fallback: try dll with dotnet
|
|
$dllPath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.dll"
|
|
if (Test-Path $dllPath) {
|
|
$exePath = $null
|
|
} else {
|
|
Write-Error "Built executable not found at: $exePath"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$runArgs = @()
|
|
if ($Scan) {
|
|
$runArgs += '--scan'
|
|
}
|
|
|
|
# Detect installed WinAppSDK runtime via Get-AppxPackage (the WindowsApps
|
|
# folder is ACL-restricted so C# cannot enumerate it directly).
|
|
# WinMD files are architecture-independent metadata, so pick whichever arch
|
|
# matches the current OS to ensure the package is present.
|
|
$osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
|
|
$runtimePkg = Get-AppxPackage -Name 'Microsoft.WindowsAppRuntime.*' -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -notmatch 'CBS' -and $_.Architecture -eq $osArch } |
|
|
Sort-Object -Property Version -Descending |
|
|
Select-Object -First 1
|
|
if ($runtimePkg -and $runtimePkg.InstallLocation -and (Test-Path $runtimePkg.InstallLocation)) {
|
|
Write-Host "Detected WinAppSDK runtime: $($runtimePkg.Name) v$($runtimePkg.Version)" -ForegroundColor Cyan
|
|
$runArgs += '--winappsdk-runtime'
|
|
$runArgs += $runtimePkg.InstallLocation
|
|
}
|
|
|
|
$runArgs += $ProjectDir
|
|
$runArgs += $OutputDir
|
|
|
|
Write-Host "Exporting WinMD cache..." -ForegroundColor Cyan
|
|
if ($exePath) {
|
|
& $exePath @runArgs
|
|
} else {
|
|
dotnet $dllPath @runArgs
|
|
}
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Cache export failed"
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "Cache updated at: $OutputDir" -ForegroundColor Green
|
|
} finally {
|
|
Pop-Location
|
|
}
|