mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-12 20:25:11 +00:00
* feat: add winmd-api-search skill for Windows desktop API discovery Add a skill that helps find and explore Windows desktop APIs (WinRT/WinAppSDK). It searches a local WinMD metadata cache to discover APIs for platform capabilities like camera, file access, notifications, UI controls, AI/ML, sensors, and networking. Includes bundled scripts: - Update-WinMdCache.ps1: generates the JSON cache from SDK and NuGet packages - Invoke-WinMdQuery.ps1: searches types, members, enums, and namespaces - cache-generator: .NET tool that parses WinMD files into JSON Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: sync winmd-api-search with latest upstream changes Updates from PowerToys source branch: - Detect installed WinAppSDK runtime via Get-AppxPackage - Respect NUGET_PACKAGES env var for global packages path - Use OS architecture for runtime package detection - Fix method/event visibility to use MemberAccessMask equality - Fix EnumerationOptions, TypeSpecification decoding, search sort - Robust scan with case-insensitive dedup and multi-path search - Deduplicate packages by (Id, Version) - Fix assets.json selection and pin SRM version - Fix SDK version sorting and global namespace handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove en-us locale from Microsoft Learn URLs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: filter assets.json by package type; unique scan-mode manifests - Filter assets.json libraries by type==package to skip project references - Append short path hash to manifest names in scan mode to avoid collisions - Support prefix match in query script for scan-mode manifest names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
506 lines
19 KiB
PowerShell
506 lines
19 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Query WinMD API metadata from cached JSON files.
|
|
|
|
.DESCRIPTION
|
|
Reads pre-built JSON cache of WinMD types, members, and namespaces.
|
|
The cache is organized per-package (deduplicated) with project manifests
|
|
that map each project to its referenced packages.
|
|
|
|
Supports listing namespaces, types, members, searching, enum value lookup,
|
|
and listing cached projects/packages.
|
|
|
|
.PARAMETER Action
|
|
The query action to perform:
|
|
- projects : List cached projects
|
|
- packages : List packages for a project
|
|
- stats : Show aggregate statistics for a project
|
|
- namespaces : List all namespaces (optional -Filter prefix)
|
|
- types : List types in a namespace (-Namespace required)
|
|
- members : List members of a type (-TypeName required)
|
|
- search : Search types and members by name (-Query required)
|
|
- enums : List enum values (-TypeName required)
|
|
|
|
.PARAMETER Project
|
|
Project name to query. Auto-selected if only one project is cached.
|
|
Use -Action projects to list available projects.
|
|
|
|
.PARAMETER Namespace
|
|
Namespace to query types from (used with -Action types).
|
|
|
|
.PARAMETER TypeName
|
|
Full type name e.g. "Microsoft.UI.Xaml.Controls.Button" (used with -Action members, enums).
|
|
|
|
.PARAMETER Query
|
|
Search query string (used with -Action search).
|
|
|
|
.PARAMETER Filter
|
|
Optional prefix filter for namespaces (used with -Action namespaces).
|
|
|
|
.PARAMETER CacheDir
|
|
Path to the winmd-cache directory. Defaults to "Generated Files\winmd-cache"
|
|
relative to the workspace root.
|
|
|
|
.PARAMETER MaxResults
|
|
Maximum number of results to return for search. Defaults to 30.
|
|
|
|
.EXAMPLE
|
|
.\Invoke-WinMdQuery.ps1 -Action projects
|
|
.\Invoke-WinMdQuery.ps1 -Action packages -Project BlankWinUI
|
|
.\Invoke-WinMdQuery.ps1 -Action stats -Project BlankWinUI
|
|
.\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
|
|
.\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
|
|
.\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.Button"
|
|
.\Invoke-WinMdQuery.ps1 -Action search -Query "NavigationView"
|
|
.\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[ValidateSet('projects', 'packages', 'stats', 'namespaces', 'types', 'members', 'search', 'enums')]
|
|
[string]$Action,
|
|
|
|
[string]$Project,
|
|
[string]$Namespace,
|
|
[string]$TypeName,
|
|
[string]$Query,
|
|
[string]$Filter,
|
|
[string]$CacheDir,
|
|
[int]$MaxResults = 30
|
|
)
|
|
|
|
# ─── Resolve cache directory ─────────────────────────────────────────────────
|
|
|
|
if (-not $CacheDir) {
|
|
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
|
|
# so workspace root is 4 levels up from $PSScriptRoot.
|
|
$scriptDir = $PSScriptRoot
|
|
$root = (Resolve-Path (Join-Path $scriptDir '..\..\..\..')).Path
|
|
$CacheDir = Join-Path $root 'Generated Files\winmd-cache'
|
|
}
|
|
|
|
if (-not (Test-Path $CacheDir)) {
|
|
Write-Error "Cache not found at: $CacheDir`nRun: .\Update-WinMdCache.ps1 (from .github\skills\winmd-api-search\scripts\)"
|
|
exit 1
|
|
}
|
|
|
|
# ─── Project resolution helpers ──────────────────────────────────────────────
|
|
|
|
function Get-CachedProjects {
|
|
$projectsDir = Join-Path $CacheDir 'projects'
|
|
if (-not (Test-Path $projectsDir)) { return @() }
|
|
Get-ChildItem $projectsDir -Filter '*.json' | ForEach-Object { $_.BaseName }
|
|
}
|
|
|
|
function Resolve-ProjectManifest {
|
|
param([string]$Name)
|
|
|
|
$projectsDir = Join-Path $CacheDir 'projects'
|
|
if (-not (Test-Path $projectsDir)) {
|
|
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
|
|
exit 1
|
|
}
|
|
|
|
if ($Name) {
|
|
$path = Join-Path $projectsDir "$Name.json"
|
|
if (-not (Test-Path $path)) {
|
|
# Scan mode appends a hash suffix -- try prefix match
|
|
$matching = @(Get-ChildItem $projectsDir -Filter "${Name}_*.json" -ErrorAction SilentlyContinue)
|
|
if ($matching.Count -eq 1) {
|
|
return Get-Content $matching[0].FullName -Raw | ConvertFrom-Json
|
|
}
|
|
if ($matching.Count -gt 1) {
|
|
$names = ($matching | ForEach-Object { $_.BaseName }) -join ', '
|
|
Write-Error "Multiple projects match '$Name'. Specify the full name: $names"
|
|
exit 1
|
|
}
|
|
$available = (Get-CachedProjects) -join ', '
|
|
Write-Error "Project '$Name' not found. Available: $available"
|
|
exit 1
|
|
}
|
|
return Get-Content $path -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
# Auto-select if only one project
|
|
$manifests = Get-ChildItem $projectsDir -Filter '*.json' -ErrorAction SilentlyContinue
|
|
if ($manifests.Count -eq 0) {
|
|
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
|
|
exit 1
|
|
}
|
|
if ($manifests.Count -eq 1) {
|
|
return Get-Content $manifests[0].FullName -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
$available = ($manifests | ForEach-Object { $_.BaseName }) -join ', '
|
|
Write-Error "Multiple projects cached -- use -Project to specify. Available: $available"
|
|
exit 1
|
|
}
|
|
|
|
function Get-PackageCacheDirs {
|
|
param($Manifest)
|
|
$dirs = @()
|
|
foreach ($pkg in $Manifest.packages) {
|
|
$dir = Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version
|
|
if (Test-Path $dir) {
|
|
$dirs += $dir
|
|
}
|
|
}
|
|
return $dirs
|
|
}
|
|
|
|
# ─── Action: projects ────────────────────────────────────────────────────────
|
|
|
|
function Show-Projects {
|
|
$projects = Get-CachedProjects
|
|
if ($projects.Count -eq 0) {
|
|
Write-Output "No projects cached."
|
|
return
|
|
}
|
|
Write-Output "Cached projects ($($projects.Count)):"
|
|
foreach ($p in $projects) {
|
|
$manifest = Get-Content (Join-Path (Join-Path $CacheDir 'projects') "$p.json") -Raw | ConvertFrom-Json
|
|
$pkgCount = $manifest.packages.Count
|
|
Write-Output " $p ($pkgCount package(s))"
|
|
}
|
|
}
|
|
|
|
# ─── Action: packages ────────────────────────────────────────────────────────
|
|
|
|
function Show-Packages {
|
|
$manifest = Resolve-ProjectManifest -Name $Project
|
|
Write-Output "Packages for project '$($manifest.projectName)' ($($manifest.packages.Count)):"
|
|
foreach ($pkg in $manifest.packages) {
|
|
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
|
|
if (Test-Path $metaPath) {
|
|
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
|
Write-Output " $($pkg.id)@$($pkg.version) -- $($meta.totalTypes) types, $($meta.totalMembers) members"
|
|
} else {
|
|
Write-Output " $($pkg.id)@$($pkg.version) -- (cache missing)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# ─── Action: stats ───────────────────────────────────────────────────────────
|
|
|
|
function Show-Stats {
|
|
$manifest = Resolve-ProjectManifest -Name $Project
|
|
$totalTypes = 0
|
|
$totalMembers = 0
|
|
$totalNamespaces = 0
|
|
$totalWinMd = 0
|
|
|
|
foreach ($pkg in $manifest.packages) {
|
|
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
|
|
if (Test-Path $metaPath) {
|
|
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
|
$totalTypes += $meta.totalTypes
|
|
$totalMembers += $meta.totalMembers
|
|
$totalNamespaces += $meta.totalNamespaces
|
|
$totalWinMd += $meta.winMdFiles.Count
|
|
}
|
|
}
|
|
|
|
Write-Output "WinMD Index Statistics -- $($manifest.projectName)"
|
|
Write-Output "======================================"
|
|
Write-Output " Packages: $($manifest.packages.Count)"
|
|
Write-Output " Namespaces: $totalNamespaces (may overlap across packages)"
|
|
Write-Output " Types: $totalTypes"
|
|
Write-Output " Members: $totalMembers"
|
|
Write-Output " WinMD files: $totalWinMd"
|
|
}
|
|
|
|
# ─── Action: namespaces ──────────────────────────────────────────────────────
|
|
|
|
function Get-Namespaces {
|
|
param([string]$Prefix)
|
|
$manifest = Resolve-ProjectManifest -Name $Project
|
|
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
|
$allNs = @()
|
|
|
|
foreach ($dir in $dirs) {
|
|
$nsFile = Join-Path $dir 'namespaces.json'
|
|
if (Test-Path $nsFile) {
|
|
$allNs += (Get-Content $nsFile -Raw | ConvertFrom-Json)
|
|
}
|
|
}
|
|
|
|
$allNs = $allNs | Sort-Object -Unique
|
|
if ($Prefix) {
|
|
$allNs = $allNs | Where-Object { $_ -like "$Prefix*" }
|
|
}
|
|
$allNs | ForEach-Object { Write-Output $_ }
|
|
}
|
|
|
|
# ─── Action: types ───────────────────────────────────────────────────────────
|
|
|
|
function Get-TypesInNamespace {
|
|
param([string]$Ns)
|
|
if (-not $Ns) {
|
|
Write-Error "-Namespace is required for 'types' action."
|
|
exit 1
|
|
}
|
|
|
|
$manifest = Resolve-ProjectManifest -Name $Project
|
|
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
|
$safeFile = $Ns.Replace('.', '_') + '.json'
|
|
$found = $false
|
|
$seen = @{}
|
|
|
|
foreach ($dir in $dirs) {
|
|
$filePath = Join-Path $dir "types\$safeFile"
|
|
if (-not (Test-Path $filePath)) { continue }
|
|
$found = $true
|
|
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
|
foreach ($t in $types) {
|
|
if ($seen.ContainsKey($t.fullName)) { continue }
|
|
$seen[$t.fullName] = $true
|
|
Write-Output "$($t.kind) $($t.fullName)$(if ($t.baseType) { " : $($t.baseType)" } else { '' })"
|
|
}
|
|
}
|
|
|
|
if (-not $found) {
|
|
Write-Error "Namespace not found: $Ns"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# ─── Action: members ─────────────────────────────────────────────────────────
|
|
|
|
function Get-MembersOfType {
|
|
param([string]$FullName)
|
|
if (-not $FullName) {
|
|
Write-Error "-TypeName is required for 'members' action."
|
|
exit 1
|
|
}
|
|
|
|
$lastDot = $FullName.LastIndexOf('.')
|
|
if ($lastDot -lt 0) {
|
|
Write-Error "-TypeName must include a namespace (for example: 'MyNamespace.MyType'). Provided: $FullName"
|
|
exit 1
|
|
}
|
|
|
|
$ns = $FullName.Substring(0, $lastDot)
|
|
$safeFile = $ns.Replace('.', '_') + '.json'
|
|
|
|
$manifest = Resolve-ProjectManifest -Name $Project
|
|
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
|
|
|
foreach ($dir in $dirs) {
|
|
$filePath = Join-Path $dir "types\$safeFile"
|
|
if (-not (Test-Path $filePath)) { continue }
|
|
|
|
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
|
$type = $types | Where-Object { $_.fullName -eq $FullName }
|
|
if (-not $type) { continue }
|
|
|
|
Write-Output "$($type.kind) $($type.fullName)"
|
|
if ($type.baseType) { Write-Output " Extends: $($type.baseType)" }
|
|
Write-Output ""
|
|
foreach ($m in $type.members) {
|
|
Write-Output " [$($m.kind)] $($m.signature)"
|
|
}
|
|
return
|
|
}
|
|
|
|
Write-Error "Type not found: $FullName"
|
|
exit 1
|
|
}
|
|
|
|
# ─── Action: search ──────────────────────────────────────────────────────────
|
|
# Ranks namespaces by best match score on type names and member names.
|
|
# Outputs: ranked namespaces with top matching types and the JSON file path.
|
|
# The agent can then read the JSON file to inspect all members intelligently.
|
|
|
|
function Search-WinMd {
|
|
param([string]$SearchQuery, [int]$Max)
|
|
if (-not $SearchQuery) {
|
|
Write-Error "-Query is required for 'search' action."
|
|
exit 1
|
|
}
|
|
|
|
$manifest = Resolve-ProjectManifest -Name $Project
|
|
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
|
|
|
# Collect: namespace -> { bestScore, matchingTypes[], filePath }
|
|
$nsResults = @{}
|
|
|
|
foreach ($dir in $dirs) {
|
|
$nsFile = Join-Path $dir 'namespaces.json'
|
|
if (-not (Test-Path $nsFile)) { continue }
|
|
$nsList = Get-Content $nsFile -Raw | ConvertFrom-Json
|
|
|
|
foreach ($n in $nsList) {
|
|
$safeFile = $n.Replace('.', '_') + '.json'
|
|
$filePath = Join-Path $dir "types\$safeFile"
|
|
if (-not (Test-Path $filePath)) { continue }
|
|
|
|
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
|
foreach ($t in $types) {
|
|
$typeScore = Get-MatchScore -Name $t.name -FullName $t.fullName -Query $SearchQuery
|
|
|
|
# Also search member names for matches
|
|
$bestMemberScore = 0
|
|
$matchingMember = $null
|
|
if ($t.members) {
|
|
foreach ($m in $t.members) {
|
|
$memberName = $m.name
|
|
$mScore = Get-MatchScore -Name $memberName -FullName "$($t.fullName).$memberName" -Query $SearchQuery
|
|
if ($mScore -gt $bestMemberScore) {
|
|
$bestMemberScore = $mScore
|
|
$matchingMember = $m.signature
|
|
}
|
|
}
|
|
}
|
|
|
|
$score = [Math]::Max($typeScore, $bestMemberScore)
|
|
if ($score -le 0) { continue }
|
|
|
|
if (-not $nsResults.ContainsKey($n)) {
|
|
$nsResults[$n] = @{ BestScore = 0; Types = @(); FilePaths = @() }
|
|
}
|
|
$entry = $nsResults[$n]
|
|
if ($score -gt $entry.BestScore) { $entry.BestScore = $score }
|
|
if ($entry.FilePaths -notcontains $filePath) {
|
|
$entry.FilePaths += $filePath
|
|
}
|
|
|
|
if ($typeScore -ge $bestMemberScore) {
|
|
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) [$typeScore]"; Score = $typeScore }
|
|
} else {
|
|
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) -> $matchingMember [$bestMemberScore]"; Score = $bestMemberScore }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($nsResults.Count -eq 0) {
|
|
Write-Output "No results found for: $SearchQuery"
|
|
return
|
|
}
|
|
|
|
$ranked = $nsResults.GetEnumerator() |
|
|
Sort-Object { $_.Value.BestScore } -Descending |
|
|
Select-Object -First $Max
|
|
|
|
foreach ($r in $ranked) {
|
|
$ns = $r.Key
|
|
$info = $r.Value
|
|
Write-Output "[$($info.BestScore)] $ns"
|
|
foreach ($fp in $info.FilePaths) {
|
|
Write-Output " File: $fp"
|
|
}
|
|
# Show top 5 highest-scoring matching types in this namespace
|
|
$info.Types | Sort-Object { $_.Score } -Descending |
|
|
Select-Object -First 5 |
|
|
ForEach-Object { Write-Output " $($_.Text)" }
|
|
Write-Output ""
|
|
}
|
|
}
|
|
|
|
# ─── Search scoring ──────────────────────────────────────────────────────────
|
|
# Simple ranked scoring on type names. Higher = better.
|
|
# 100 = exact name 80 = starts-with 60 = substring
|
|
# 50 = PascalCase 40 = multi-keyword 20 = fuzzy subsequence
|
|
|
|
function Get-MatchScore {
|
|
param([string]$Name, [string]$FullName, [string]$Query)
|
|
|
|
$q = $Query.Trim()
|
|
if (-not $q) { return 0 }
|
|
|
|
if ($Name -eq $q) { return 100 }
|
|
if ($Name -like "$q*") { return 80 }
|
|
if ($Name -like "*$q*" -or $FullName -like "*$q*") { return 60 }
|
|
|
|
$initials = ($Name.ToCharArray() | Where-Object { [char]::IsUpper($_) }) -join ''
|
|
if ($initials.Length -ge 2 -and $initials -like "*$q*") { return 50 }
|
|
|
|
$words = $q -split '\s+' | Where-Object { $_.Length -gt 0 }
|
|
if ($words.Count -gt 1) {
|
|
$allFound = $true
|
|
foreach ($w in $words) {
|
|
if ($Name -notlike "*$w*" -and $FullName -notlike "*$w*") {
|
|
$allFound = $false
|
|
break
|
|
}
|
|
}
|
|
if ($allFound) { return 40 }
|
|
}
|
|
|
|
if (Test-FuzzySubsequence -Text $Name -Pattern $q) { return 20 }
|
|
|
|
return 0
|
|
}
|
|
|
|
function Test-FuzzySubsequence {
|
|
param([string]$Text, [string]$Pattern)
|
|
$ti = 0
|
|
$tLower = $Text.ToLowerInvariant()
|
|
$pLower = $Pattern.ToLowerInvariant()
|
|
foreach ($ch in $pLower.ToCharArray()) {
|
|
$idx = $tLower.IndexOf($ch, $ti)
|
|
if ($idx -lt 0) { return $false }
|
|
$ti = $idx + 1
|
|
}
|
|
return $true
|
|
}
|
|
|
|
# ─── Action: enums ───────────────────────────────────────────────────────────
|
|
|
|
function Get-EnumValues {
|
|
param([string]$FullName)
|
|
if (-not $FullName) {
|
|
Write-Error "-TypeName is required for 'enums' action."
|
|
exit 1
|
|
}
|
|
|
|
$lastDot = $FullName.LastIndexOf('.')
|
|
if ($lastDot -lt 1) {
|
|
Write-Error "-TypeName must be a fully-qualified type name including namespace, e.g. 'Namespace.TypeName'. Provided: $FullName"
|
|
exit 1
|
|
}
|
|
|
|
$ns = $FullName.Substring(0, $lastDot)
|
|
$safeFile = $ns.Replace('.', '_') + '.json'
|
|
|
|
$manifest = Resolve-ProjectManifest -Name $Project
|
|
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
|
|
|
foreach ($dir in $dirs) {
|
|
$filePath = Join-Path $dir "types\$safeFile"
|
|
if (-not (Test-Path $filePath)) { continue }
|
|
|
|
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
|
$type = $types | Where-Object { $_.fullName -eq $FullName }
|
|
if (-not $type) { continue }
|
|
|
|
if ($type.kind -ne 'Enum') {
|
|
Write-Error "$FullName is not an Enum (kind: $($type.kind))"
|
|
exit 1
|
|
}
|
|
Write-Output "Enum $($type.fullName)"
|
|
if ($type.enumValues) {
|
|
$type.enumValues | ForEach-Object { Write-Output " $_" }
|
|
} else {
|
|
Write-Output " (no values)"
|
|
}
|
|
return
|
|
}
|
|
|
|
Write-Error "Type not found: $FullName"
|
|
exit 1
|
|
}
|
|
|
|
# ─── Dispatch ─────────────────────────────────────────────────────────────────
|
|
|
|
switch ($Action) {
|
|
'projects' { Show-Projects }
|
|
'packages' { Show-Packages }
|
|
'stats' { Show-Stats }
|
|
'namespaces' { Get-Namespaces -Prefix $Filter }
|
|
'types' { Get-TypesInNamespace -Ns $Namespace }
|
|
'members' { Get-MembersOfType -FullName $TypeName }
|
|
'search' { Search-WinMd -SearchQuery $Query -Max $MaxResults }
|
|
'enums' { Get-EnumValues -FullName $TypeName }
|
|
}
|