
Winmd Api Search
- 5.6k installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
winmd-api-search is an agent skill for Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc
About
Find and explore Windows desktop APIs Use when building features that need platform capabilities camera file access notifications UI controls AI ML sensors networking etc Discovers the right API for a task and retrieves full type details methods properties events enumeration values name winmd-api-search description Find and explore Windows desktop APIs Use when building features that need platform capabilities camera file access notifications UI controls AI ML sensors networking etc Discovers the right API for a task and retrieves full type details methods properties events enumeration values license Complete terms in LICENSE txt WinMD API Search This skill helps you find the right Windows API for any capability and get its full details It searches a local cache of all WinMD metadata from Windows Platform SDK all Windows WinRT APIs always available no restore needed WinAppSDK WinUI bundled as a baseline in the cache generator always available no restore needed NuGet packages any additional packages in restored projects that contain winmd files Project-output WinMD class libraries C WinRT C that
- **Windows Platform SDK** - all `Windows.*` WinRT APIs (always available, no restore needed)
- **WinAppSDK / WinUI** - bundled as a baseline in the cache generator (always available, no restore needed)
- **NuGet packages** - any additional packages in restored projects that contain `.winmd` files
- **Project-output WinMD** - class libraries (C++/WinRT, C#) that produce `.winmd` as build output
- User wants to build a feature and you need to find which API provides that capability
Winmd Api Search by the numbers
- 5,644 all-time installs (skills.sh)
- +22 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #126 of 1,881 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
winmd-api-search capabilities & compatibility
- Capabilities
- **windows platform sdk** — all `windows.*` winrt · **winappsdk / winui** — bundled as a baseline in · **nuget packages** — any additional packages in · **project output winmd** — class libraries (c++/ · user wants to build a feature and you need to fi
- Use cases
- documentation
What winmd-api-search says it does
--- name: winmd-api-search description: 'Find and explore Windows desktop APIs.
Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc.
Install from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) if not available.
npx skills add https://github.com/github/awesome-copilot --skill winmd-api-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.6k |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
When should developers use winmd-api-search and what problem does it solve?
Find and explore Windows desktop APIs. Use when building features that need platform capabilities - camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the r.
Who is it for?
Developers working with winmd-api-search patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the righ
What you get
Grounded guidance and workflows from SKILL.md for winmd-api-search.
- API name matches
- full WinRT type signatures
Files
WinMD API Search
This skill helps you find the right Windows API for any capability and get its full details. It searches a local cache of all WinMD metadata from:
- Windows Platform SDK — all
Windows.*WinRT APIs (always available, no restore needed) - WinAppSDK / WinUI — bundled as a baseline in the cache generator (always available, no restore needed)
- NuGet packages — any additional packages in restored projects that contain
.winmdfiles - Project-output WinMD — class libraries (C++/WinRT, C#) that produce
.winmdas build output
Even on a fresh clone with no restore or build, you still get full Platform SDK + WinAppSDK coverage.
When to Use This Skill
- User wants to build a feature and you need to find which API provides that capability
- User asks "how do I do X?" where X involves a platform feature (camera, files, notifications, sensors, AI, etc.)
- You need the exact methods, properties, events, or enumeration values of a type before writing code
- You're unsure which control, class, or interface to use for a UI or system task
Prerequisites
- .NET SDK 8.0 or later — required to build the cache generator. Install from dotnet.microsoft.com if not available.
Cache Setup (Required Before First Use)
All query and search commands read from a local JSON cache. You must generate the cache before running any queries.
# All projects in the repo (recommended for first run)
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1
# Single project
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1 -ProjectDir <project-folder>No project restore or build is needed for baseline coverage (Platform SDK + WinAppSDK). For additional NuGet packages, the project needs dotnet restore (which generates project.assets.json) or a packages.config file.
Cache is stored at Generated Files\winmd-cache\, deduplicated per-package+version.
What gets indexed
| Source | When available |
|---|---|
| Windows Platform SDK | Always (reads from local SDK install) |
| WinAppSDK (latest) | Always (bundled as baseline in cache generator) |
| WinAppSDK Runtime | When installed on the system (detected via Get-AppxPackage) |
| Project NuGet packages | After dotnet restore or with packages.config |
Project-output .winmd | After project build (class libraries that produce WinMD) |
Note: This cache directory should be in .gitignore — it's generated, not source.How to Use
Pick the path that matches the situation:
---
Discover — "I don't know which API to use"
The user describes a capability in their own words. You need to find the right API.
0. Ensure the cache exists
If the cache hasn't been generated yet, run Update-WinMdCache.ps1 first — see Cache Setup above.
1. Translate user language → search keywords
Map the user's daily language to programming terms. Try multiple variations:
| User says | Search keywords to try (in order) |
|---|---|
| "take a picture" | camera, capture, photo, MediaCapture |
| "load from disk" | file open, picker, FileOpen, StorageFile |
| "describe what's in it" | image description, Vision, Recognition |
| "show a popup" | dialog, flyout, popup, ContentDialog |
| "drag and drop" | drag, drop, DragDrop |
| "save settings" | settings, ApplicationData, LocalSettings |
Start with simple everyday words. If results are weak or irrelevant, try the more technical variation.
2. Run searches
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action search -Query "<keyword>"This returns ranked namespaces with top matching types and the JSON file path.
If results have low scores (below 60) or are irrelevant, fall back to searching online documentation:
1. Use web search to find the right API on Microsoft Learn, for example:
site:learn.microsoft.com/uwp/api <capability keywords>forWindows.*APIssite:learn.microsoft.com/windows/windows-app-sdk/api/winrt <capability keywords>forMicrosoft.*WinAppSDK APIs
2. Read the documentation pages to identify which type matches the user's requirement. 3. Once you know the type name, come back and use -Action members or -Action enums to get the exact local signatures.
3. Read the JSON to choose the right API
Read the file at the path(s) from the top results. The JSON has all types in that namespace — full members, signatures, parameters, return types, enumeration values.
Read and decide which types and members fit the user's requirement.
4. Look up official documentation for context
The cache contains only signatures — no descriptions or usage guidance. For explanations, examples, and remarks, look up the type on Microsoft Learn:
| Namespace prefix | Documentation base URL |
|---|---|
Windows.* | https://learn.microsoft.com/uwp/api/{fully.qualified.typename} |
Microsoft.* (WinAppSDK) | https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/{fully.qualified.typename} |
For example, Microsoft.UI.Xaml.Controls.NavigationView maps to: https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.navigationview
5. Use the API knowledge to answer or write code
---
Lookup — "I know the API, show me the details"
You already know (or suspect) the type or namespace name. Go direct:
# Get all members of a known type
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.NavigationView"
# Get enum values
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
# List all types in a namespace
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
# Browse namespaces
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"If you need full detail beyond what -Action members shows, use -Action search to get the JSON file path, then read the JSON file directly.
---
Other Commands
# List cached projects
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action projects
# List packages for a project
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action packages
# Show stats
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action statsIf only one project is cached, -Project is auto-selected.If multiple projects exist, add-Project <name>(use-Action projectsto see available names).
In scan mode, manifest names include a short hash suffix to avoid collisions; you can pass the base project name without the suffix if it's unambiguous.
Search Scoring
The search ranks type names and member names against your query:
| Score | Match type | Example |
|---|---|---|
| 100 | Exact name | Button → Button |
| 80 | Starts with | Navigation → NavigationView |
| 60 | Contains | Dialog → ContentDialog |
| 50 | PascalCase initials | ASB → AutoSuggestBox |
| 40 | Multi-keyword AND | navigation item → NavigationViewItem |
| 20 | Fuzzy character match | NavVw → NavigationView |
Results are grouped by namespace. Higher-scored namespaces appear first.
Troubleshooting
| Issue | Fix |
|---|---|
| "Cache not found" | Run Update-WinMdCache.ps1 |
| "Multiple projects cached" | Add -Project <name> |
| "Namespace not found" | Use -Action namespaces to list available ones |
| "Type not found" | Use fully qualified name (e.g., Microsoft.UI.Xaml.Controls.Button) |
| Stale after NuGet update | Re-run Update-WinMdCache.ps1 |
| Cache in git history | Add Generated Files/ to .gitignore |
References
- Windows Platform SDK API reference — documentation for
Windows.*namespaces - Windows App SDK API reference — documentation for
Microsoft.*WinAppSDK namespaces
The MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- Default fallback; Update-WinMdCache.ps1 overrides via -p:TargetFramework=net{X}.0 -->
<TargetFramework Condition="'$(TargetFramework)' == ''">net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<!-- System.Reflection.Metadata is inbox in net9.0+, only needed for net8.0 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.Reflection.Metadata" Version="8.0.1" />
</ItemGroup>
<!--
Baseline WinAppSDK packages: downloaded during restore so the cache generator
can always index WinAppSDK APIs, even if the target project hasn't been restored.
ExcludeAssets="all" means they're downloaded but don't affect this tool's build.
When the repo has a known version (passed via -p:WinAppSdkVersion=X.Y.Z from
Update-WinMdCache.ps1), prefer that version to avoid unnecessary NuGet downloads.
Falls back to Version="*" (latest) on fresh clones with no restore.
-->
<ItemGroup Condition="'$(WinAppSdkVersion)' != ''">
<PackageReference Include="Microsoft.WindowsAppSDK" Version="$(WinAppSdkVersion)" ExcludeAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(WinAppSdkVersion)' == ''">
<PackageReference Include="Microsoft.WindowsAppSDK" Version="*" ExcludeAssets="all" />
</ItemGroup>
</Project>
<Project>
<!-- Isolate this standalone tool from the repo-level build configuration -->
</Project>
<Project>
<!-- Isolate this standalone tool from the repo-level build targets -->
</Project>
<Project>
<!-- Isolate this standalone tool from the repo-level Central Package Management -->
</Project>
// Standalone WinMD cache generator — per-package deduplicate, multi-project support.
// Parses WinMD files from NuGet packages and Windows SDK, exports JSON cache
// keyed by package+version to avoid duplication across projects.
//
// Usage:
// CacheGenerator <project-dir> <output-dir>
// CacheGenerator --scan <root-dir> <output-dir>
using System.Collections.Immutable;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Security.Cryptography;
using System.Xml.Linq;
// --- Arg parsing ---
var scanMode = args.Contains("--scan");
// Parse --winappsdk-runtime <path> option
string? winAppSdkRuntimePath = null;
for (int i = 0; i < args.Length - 1; i++)
{
if (args[i].Equals("--winappsdk-runtime", StringComparison.OrdinalIgnoreCase))
{
winAppSdkRuntimePath = args[i + 1];
break;
}
}
var positionalArgs = args
.Where(a => !a.StartsWith('-'))
.Where(a => a != winAppSdkRuntimePath) // exclude the runtime path value
.ToArray();
if (positionalArgs.Length < 2)
{
Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(" CacheGenerator <project-dir> <output-dir>");
Console.Error.WriteLine(" CacheGenerator --scan <root-dir> <output-dir>");
Console.Error.WriteLine(" CacheGenerator --winappsdk-runtime <path> <project-dir> <output-dir>");
Console.Error.WriteLine();
Console.Error.WriteLine(" project-dir: Path containing .csproj/.vcxproj (or a project file itself)");
Console.Error.WriteLine(" root-dir: Root to scan recursively for project files");
Console.Error.WriteLine(" output-dir: Cache output (e.g. \"Generated Files\\winmd-cache\")");
Console.Error.WriteLine(" --winappsdk-runtime: Path to installed WinAppSDK runtime (from Get-AppxPackage)");
return 1;
}
var inputPath = Path.GetFullPath(positionalArgs[0]);
var outputDir = Path.GetFullPath(positionalArgs[1]);
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter() },
};
// --- Discover project files ---
var projectFiles = new List<string>();
if (scanMode)
{
if (!Directory.Exists(inputPath))
{
Console.Error.WriteLine($"Error: Root directory not found: {inputPath}");
return 1;
}
var enumerationOptions = new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
MatchType = MatchType.Simple,
};
projectFiles.AddRange(Directory.EnumerateFiles(inputPath, "*.csproj", enumerationOptions));
projectFiles.AddRange(Directory.EnumerateFiles(inputPath, "*.vcxproj", enumerationOptions));
// Exclude common non-source directories
projectFiles = projectFiles
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}node_modules{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
.ToList();
}
else if (File.Exists(inputPath) && (inputPath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) ||
inputPath.EndsWith(".vcxproj", StringComparison.OrdinalIgnoreCase)))
{
projectFiles.Add(inputPath);
}
else if (Directory.Exists(inputPath))
{
projectFiles.AddRange(Directory.GetFiles(inputPath, "*.csproj"));
projectFiles.AddRange(Directory.GetFiles(inputPath, "*.vcxproj"));
}
else
{
Console.Error.WriteLine($"Error: Path not found: {inputPath}");
return 1;
}
if (projectFiles.Count == 0)
{
Console.Error.WriteLine($"No .csproj or .vcxproj files found in: {inputPath}");
return 1;
}
// Always include CacheGenerator.csproj as a baseline source of WinAppSDK WinMD files.
// It references Microsoft.WindowsAppSDK with ExcludeAssets="all" so the packages are
// downloaded during restore/build but don't affect the tool's compilation.
var selfCsproj = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "CacheGenerator.csproj");
selfCsproj = Path.GetFullPath(selfCsproj);
if (File.Exists(selfCsproj) && !projectFiles.Any(f =>
Path.GetFullPath(f).Equals(selfCsproj, StringComparison.OrdinalIgnoreCase)))
{
projectFiles.Add(selfCsproj);
}
Console.WriteLine($"WinMD Cache Generator (per-package deduplicate)");
Console.WriteLine($" Output: {outputDir}");
Console.WriteLine($" Projects: {projectFiles.Count}");
// --- Process each project ---
var totalPackagesCached = 0;
var totalPackagesSkipped = 0;
var totalProjectsProcessed = 0;
foreach (var projectFile in projectFiles)
{
var projectDir = Path.GetDirectoryName(projectFile)!;
var projectName = Path.GetFileNameWithoutExtension(projectFile);
Console.WriteLine($"\n--- {projectName} ({Path.GetFileName(projectFile)}) ---");
// Find packages that contain WinMD files
var packages = NuGetResolver.FindPackagesWithWinMd(projectDir, projectFile, winAppSdkRuntimePath);
if (packages.Count == 0)
{
Console.WriteLine(" No packages with WinMD files (is the project restored?)");
continue;
}
Console.WriteLine($" {packages.Count} package(s) with WinMD files");
totalProjectsProcessed++;
var projectPackages = new List<ProjectPackageRef>();
foreach (var pkg in packages)
{
var pkgCacheDir = Path.Combine(outputDir, "packages", pkg.Id, pkg.Version);
var metaPath = Path.Combine(pkgCacheDir, "meta.json");
if (File.Exists(metaPath))
{
Console.WriteLine($" [cached] {pkg.Id}@{pkg.Version}");
totalPackagesSkipped++;
}
else
{
Console.WriteLine($" [parse] {pkg.Id}@{pkg.Version} ({pkg.WinMdFiles.Count} WinMD file(s))");
ExportPackageCache(pkg, pkgCacheDir);
totalPackagesCached++;
}
projectPackages.Add(new ProjectPackageRef { Id = pkg.Id, Version = pkg.Version });
}
// Write project manifest
var manifest = new ProjectManifest
{
ProjectName = projectName,
ProjectDir = projectDir,
ProjectFile = Path.GetFileName(projectFile),
Packages = projectPackages,
GeneratedAt = DateTime.UtcNow.ToString("o"),
};
var projectsDir = Path.Combine(outputDir, "projects");
Directory.CreateDirectory(projectsDir);
// In scan mode, different directories may contain same-named projects.
// Append a short path hash to avoid overwriting manifests.
var manifestFileName = projectName;
if (scanMode)
{
var hashBytes = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(projectFile));
var hashSuffix = Convert.ToHexString(hashBytes)[..8].ToLowerInvariant();
manifestFileName = $"{projectName}_{hashSuffix}";
}
File.WriteAllText(
Path.Combine(projectsDir, $"{manifestFileName}.json"),
JsonSerializer.Serialize(manifest, jsonOptions));
}
Console.WriteLine($"\nDone: {totalProjectsProcessed} project(s) processed, " +
$"{totalPackagesCached} package(s) parsed, " +
$"{totalPackagesSkipped} reused from cache");
return 0;
// =============================================================================
// Export a single package's WinMD data to cache
// =============================================================================
void ExportPackageCache(PackageWithWinMd pkg, string cacheDir)
{
var typesDir = Path.Combine(cacheDir, "types");
Directory.CreateDirectory(typesDir);
var allTypes = new List<WinMdTypeInfo>();
foreach (var file in pkg.WinMdFiles)
{
allTypes.AddRange(WinMdParser.ParseFile(file));
}
var typesByNamespace = allTypes
.GroupBy(t => t.Namespace)
.ToDictionary(g => g.Key, g => g.ToList());
var namespaces = typesByNamespace.Keys
.Where(ns => !string.IsNullOrEmpty(ns))
.OrderBy(ns => ns)
.ToList();
// Include global (empty) namespace types under a reserved bucket name
var hasGlobalNs = typesByNamespace.ContainsKey(string.Empty)
&& typesByNamespace[string.Empty].Count > 0;
const string globalNsBucket = "_GlobalNamespace";
if (hasGlobalNs)
{
namespaces.Insert(0, globalNsBucket);
}
// meta.json
var meta = new
{
PackageId = pkg.Id,
Version = pkg.Version,
WinMdFiles = pkg.WinMdFiles
.Select(Path.GetFileName)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(),
TotalTypes = allTypes.Count,
TotalMembers = allTypes.Sum(t => t.Members.Count),
TotalNamespaces = namespaces.Count,
GeneratedAt = DateTime.UtcNow.ToString("o"),
};
File.WriteAllText(
Path.Combine(cacheDir, "meta.json"),
JsonSerializer.Serialize(meta, jsonOptions));
// namespaces.json
File.WriteAllText(
Path.Combine(cacheDir, "namespaces.json"),
JsonSerializer.Serialize(namespaces, jsonOptions));
// types/<Namespace>.json
foreach (var ns in namespaces)
{
var lookupKey = ns == globalNsBucket ? string.Empty : ns;
var types = typesByNamespace[lookupKey];
var safeFileName = ns.Replace('.', '_') + ".json";
File.WriteAllText(
Path.Combine(typesDir, safeFileName),
JsonSerializer.Serialize(types, jsonOptions));
}
}
// =============================================================================
// Data Models
// =============================================================================
enum TypeKind { Class, Struct, Enum, Interface, Delegate }
enum MemberKind { Method, Property, Event, Field }
sealed class WinMdTypeInfo
{
public required string Namespace { get; init; }
public required string Name { get; init; }
public required string FullName { get; init; }
public required TypeKind Kind { get; init; }
public string? BaseType { get; init; }
public required List<WinMdMemberInfo> Members { get; init; }
public List<string>? EnumValues { get; init; }
public required string SourceFile { get; init; }
}
sealed class WinMdMemberInfo
{
public required string Name { get; init; }
public required MemberKind Kind { get; init; }
public required string Signature { get; init; }
public string? ReturnType { get; init; }
public List<WinMdParameterInfo>? Parameters { get; init; }
}
sealed class WinMdParameterInfo
{
public required string Name { get; init; }
public required string Type { get; init; }
}
sealed class ProjectPackageRef
{
public required string Id { get; init; }
public required string Version { get; init; }
}
sealed class ProjectManifest
{
public required string ProjectName { get; init; }
public required string ProjectDir { get; init; }
public required string ProjectFile { get; init; }
public required List<ProjectPackageRef> Packages { get; init; }
public required string GeneratedAt { get; init; }
}
// =============================================================================
// NuGet Resolver — finds packages with WinMD files, returns structured data
// =============================================================================
record PackageWithWinMd(string Id, string Version, List<string> WinMdFiles);
static class NuGetResolver
{
public static List<PackageWithWinMd> FindPackagesWithWinMd(string projectDir, string projectFile, string? winAppSdkRuntimePath)
{
var result = new List<PackageWithWinMd>();
// 1. Try project.assets.json (PackageReference — .csproj and modern .vcxproj)
var assetsPath = FindProjectAssetsJson(projectDir);
if (assetsPath is not null)
{
result.AddRange(FindPackagesFromAssets(assetsPath));
}
// 2. Try packages.config (older .vcxproj / .csproj using NuGet packages.config)
if (result.Count == 0)
{
var packagesConfig = Path.Combine(projectDir, "packages.config");
if (File.Exists(packagesConfig))
{
result.AddRange(FindPackagesFromConfig(packagesConfig, projectDir));
}
}
// 3. Project references — parse <ProjectReference> from .csproj/.vcxproj XML,
// then check each referenced project's bin/ for .winmd build output.
// This is the reliable way to find class libraries that generate WinMD.
result.AddRange(FindWinMdFromProjectReferences(projectFile));
// 4. Windows SDK as a synthetic "package"
var sdkWinMd = FindWindowsSdkWinMd();
if (sdkWinMd.Files.Count > 0)
{
result.Add(new PackageWithWinMd("WindowsSDK", sdkWinMd.Version, sdkWinMd.Files));
}
// 5. Installed WinAppSDK runtime as a synthetic "package"
// Useful for Electron/Node.js apps that don't reference WinAppSDK via NuGet.
var runtimeWinMd = FindWinAppSdkRuntimeWinMd(winAppSdkRuntimePath);
if (runtimeWinMd.Files.Count > 0)
{
result.Add(new PackageWithWinMd("WinAppSdkRuntime", runtimeWinMd.Version, runtimeWinMd.Files));
}
// Deduplicate by (Id, Version), merging WinMdFiles from multiple sources
return result
.GroupBy(p => (p.Id.ToLowerInvariant(), p.Version.ToLowerInvariant()))
.Select(g =>
{
var merged = g.SelectMany(p => p.WinMdFiles)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var first = g.First();
return new PackageWithWinMd(first.Id, first.Version, merged);
})
.ToList();
}
/// <summary>
/// Parse <ProjectReference> from .csproj/.vcxproj and find .winmd output
/// from each referenced project's bin/ directory.
/// </summary>
internal static List<PackageWithWinMd> FindWinMdFromProjectReferences(string projectFile)
{
var result = new List<PackageWithWinMd>();
try
{
var doc = XDocument.Load(projectFile);
var ns = doc.Root?.Name.Namespace ?? XNamespace.None;
var projectRefs = doc.Descendants(ns + "ProjectReference")
.Select(e => e.Attribute("Include")?.Value)
.Where(v => v is not null)
.ToList();
if (projectRefs.Count == 0)
{
return result;
}
var projectDir = Path.GetDirectoryName(projectFile)!;
foreach (var refPath in projectRefs)
{
var refFullPath = Path.GetFullPath(Path.Combine(projectDir, refPath!));
if (!File.Exists(refFullPath))
{
continue;
}
var refProjectDir = Path.GetDirectoryName(refFullPath)!;
var refProjectName = Path.GetFileNameWithoutExtension(refFullPath);
var refBinDir = Path.Combine(refProjectDir, "bin");
if (!Directory.Exists(refBinDir))
{
continue;
}
var winmdFiles = Directory.GetFiles(refBinDir, "*.winmd", SearchOption.AllDirectories)
.Where(f => !Path.GetFileName(f).Equals("Windows.winmd", StringComparison.OrdinalIgnoreCase))
.ToList();
// Deduplicate by filename (same WinMD across Debug/Release/x64/etc.)
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
winmdFiles = winmdFiles
.Where(f => seen.Add(Path.GetFileName(f)))
.ToList();
if (winmdFiles.Count > 0)
{
result.Add(new PackageWithWinMd($"ProjectRef.{refProjectName}", "local", winmdFiles));
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Warning: Failed to parse project references: {ex.Message}");
}
return result;
}
internal static string? FindProjectAssetsJson(string projectDir)
{
// Standard location
var assetsPath = Path.Combine(projectDir, "obj", "project.assets.json");
if (File.Exists(assetsPath))
{
return assetsPath;
}
// Sometimes under platform-specific subdirectories
var objDir = Path.Combine(projectDir, "obj");
if (Directory.Exists(objDir))
{
var found = Directory.GetFiles(objDir, "project.assets.json", SearchOption.AllDirectories);
if (found.Length > 0)
{
// Pick the most recently written file to avoid non-deterministic
// selection when multi-targeting creates multiple assets files.
string? bestPath = null;
DateTime bestWriteTime = DateTime.MinValue;
foreach (var path in found)
{
try
{
var writeTime = File.GetLastWriteTimeUtc(path);
if (writeTime > bestWriteTime)
{
bestWriteTime = writeTime;
bestPath = path;
}
}
catch
{
// Ignore files we cannot access metadata for
}
}
if (bestPath is not null)
{
return bestPath;
}
}
}
return null;
}
internal static List<PackageWithWinMd> FindPackagesFromAssets(string assetsPath)
{
var result = new List<PackageWithWinMd>();
try
{
using var doc = JsonDocument.Parse(File.ReadAllText(assetsPath));
var root = doc.RootElement;
var packageFolders = new List<string>();
if (root.TryGetProperty("packageFolders", out var folders))
{
foreach (var folder in folders.EnumerateObject())
{
packageFolders.Add(folder.Name);
}
}
if (!root.TryGetProperty("libraries", out var libraries))
{
return result;
}
foreach (var lib in libraries.EnumerateObject())
{
// Only treat libraries with type == "package" as NuGet packages;
// skip project references and other entry types.
if (!lib.Value.TryGetProperty("type", out var typeProp) ||
!string.Equals(typeProp.GetString(), "package", StringComparison.OrdinalIgnoreCase))
{
continue;
}
// Key format: "PackageId/Version"
var slashIdx = lib.Name.IndexOf('/');
if (slashIdx < 0)
{
continue;
}
var packageId = lib.Name[..slashIdx];
var version = lib.Name[(slashIdx + 1)..];
if (!lib.Value.TryGetProperty("path", out var pathProp))
{
continue;
}
var libPath = pathProp.GetString();
if (libPath is null)
{
continue;
}
var winmdFiles = new List<string>();
foreach (var folder in packageFolders)
{
var fullPath = Path.Combine(folder, libPath);
if (!Directory.Exists(fullPath))
{
continue;
}
winmdFiles.AddRange(
Directory.GetFiles(fullPath, "*.winmd", SearchOption.AllDirectories));
}
// Deduplicate by filename (WinMD is arch-neutral metadata)
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
winmdFiles = winmdFiles
.Where(f => seen.Add(Path.GetFileName(f)))
.ToList();
if (winmdFiles.Count > 0)
{
result.Add(new PackageWithWinMd(packageId, version, winmdFiles));
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Warning: Failed to parse project.assets.json: {ex.Message}");
}
return result;
}
/// <summary>
/// Parses packages.config (older NuGet format used by some .vcxproj and legacy .csproj).
/// Looks for a solution-level "packages/" folder or the NuGet global cache.
/// </summary>
internal static List<PackageWithWinMd> FindPackagesFromConfig(string configPath, string projectDir)
{
var result = new List<PackageWithWinMd>();
try
{
var doc = System.Xml.Linq.XDocument.Load(configPath);
var packages = doc.Root?.Elements("package");
if (packages is null)
{
return result;
}
// packages.config repos typically have a solution-level "packages/" folder.
// Walk up from project dir to find it.
var packagesFolder = FindSolutionPackagesFolder(projectDir);
// Also check NuGet global packages cache (respect NUGET_PACKAGES override)
var globalPackages = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
if (string.IsNullOrWhiteSpace(globalPackages))
{
globalPackages = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".nuget", "packages");
}
foreach (var pkg in packages)
{
var id = pkg.Attribute("id")?.Value;
var version = pkg.Attribute("version")?.Value;
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(version))
{
continue;
}
var winmdFiles = new List<string>();
// Check solution-level packages/ folder (format: packages/<id>.<version>/)
if (packagesFolder is not null)
{
var pkgDir = Path.Combine(packagesFolder, $"{id}.{version}");
if (Directory.Exists(pkgDir))
{
winmdFiles.AddRange(
Directory.GetFiles(pkgDir, "*.winmd", SearchOption.AllDirectories));
}
}
// Fallback: NuGet global cache (format: <id>/<version>/)
if (winmdFiles.Count == 0 && Directory.Exists(globalPackages))
{
var pkgDir = Path.Combine(globalPackages, id.ToLowerInvariant(), version);
if (Directory.Exists(pkgDir))
{
winmdFiles.AddRange(
Directory.GetFiles(pkgDir, "*.winmd", SearchOption.AllDirectories));
}
}
// Deduplicate by filename
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
winmdFiles = winmdFiles
.Where(f => seen.Add(Path.GetFileName(f)))
.ToList();
if (winmdFiles.Count > 0)
{
result.Add(new PackageWithWinMd(id, version, winmdFiles));
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Warning: Failed to parse packages.config: {ex.Message}");
}
return result;
}
/// <summary>
/// Walk up from project dir to find a solution-level "packages/" folder.
/// </summary>
internal static string? FindSolutionPackagesFolder(string startDir)
{
var dir = startDir;
for (var i = 0; i < 5; i++) // Walk up at most 5 levels
{
var packagesDir = Path.Combine(dir, "packages");
if (Directory.Exists(packagesDir))
{
return packagesDir;
}
var parent = Directory.GetParent(dir);
if (parent is null)
{
break;
}
dir = parent.FullName;
}
return null;
}
internal static (List<string> Files, string Version) FindWindowsSdkWinMd()
{
var windowsKitsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
"Windows Kits", "10", "UnionMetadata");
if (!Directory.Exists(windowsKitsPath))
{
return ([], "unknown");
}
// Filter to version-numbered directories only (skip "Facade" etc.) and
// sort by numeric version, not lexicographically, to pick the highest SDK.
var versionDirs = Directory.GetDirectories(windowsKitsPath)
.Select(d => (Dir: d, Name: Path.GetFileName(d)))
.Where(x => !string.IsNullOrEmpty(x.Name) && char.IsDigit(x.Name[0]))
.Select(x => Version.TryParse(x.Name, out var v)
? (Dir: x.Dir, Version: v)
: (Dir: (string?)null, Version: (Version?)null))
.Where(x => x.Dir is not null && x.Version is not null)
.OrderByDescending(x => x.Version)
.Select(x => x.Dir!)
.ToList();
foreach (var versionDir in versionDirs)
{
var windowsWinMd = Path.Combine(versionDir, "Windows.winmd");
if (File.Exists(windowsWinMd))
{
var version = Path.GetFileName(versionDir);
return ([windowsWinMd], version);
}
}
return ([], "unknown");
}
/// <summary>
/// Read WinMD files from the installed WinAppSDK runtime path (discovered via
/// Get-AppxPackage in PowerShell and passed as --winappsdk-runtime argument).
/// The WindowsApps folder is ACL-restricted so C# cannot enumerate it directly.
/// </summary>
internal static (List<string> Files, string Version) FindWinAppSdkRuntimeWinMd(string? runtimePath)
{
if (string.IsNullOrEmpty(runtimePath) || !Directory.Exists(runtimePath))
{
return ([], "unknown");
}
try
{
var winmdFiles = Directory.EnumerateFiles(runtimePath, "*.winmd", SearchOption.TopDirectoryOnly)
.ToList();
if (winmdFiles.Count > 0)
{
// Extract SDK version from path: ...Microsoft.WindowsAppRuntime.1.8_... -> "1.8"
var dirName = Path.GetFileName(runtimePath);
var prefix = dirName.Split('_')[0]; // "Microsoft.WindowsAppRuntime.1.8"
var sdkVersion = prefix.Length > "Microsoft.WindowsAppRuntime.".Length
? prefix["Microsoft.WindowsAppRuntime.".Length..]
: dirName;
return (winmdFiles, sdkVersion);
}
}
catch
{
// Path may be inaccessible; degrade gracefully
}
return ([], "unknown");
}
}
// =============================================================================
// Signature Type Provider — decodes metadata signatures to readable strings
// =============================================================================
sealed class SimpleTypeProvider : ISignatureTypeProvider<string, object?>
{
public string GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode switch
{
PrimitiveTypeCode.Boolean => "Boolean",
PrimitiveTypeCode.Byte => "Byte",
PrimitiveTypeCode.SByte => "SByte",
PrimitiveTypeCode.Char => "Char",
PrimitiveTypeCode.Int16 => "Int16",
PrimitiveTypeCode.UInt16 => "UInt16",
PrimitiveTypeCode.Int32 => "Int32",
PrimitiveTypeCode.UInt32 => "UInt32",
PrimitiveTypeCode.Int64 => "Int64",
PrimitiveTypeCode.UInt64 => "UInt64",
PrimitiveTypeCode.Single => "Single",
PrimitiveTypeCode.Double => "Double",
PrimitiveTypeCode.String => "String",
PrimitiveTypeCode.Object => "Object",
PrimitiveTypeCode.Void => "void",
PrimitiveTypeCode.IntPtr => "IntPtr",
PrimitiveTypeCode.UIntPtr => "UIntPtr",
PrimitiveTypeCode.TypedReference => "TypedReference",
_ => typeCode.ToString(),
};
public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
{
var typeDef = reader.GetTypeDefinition(handle);
var name = reader.GetString(typeDef.Name);
var ns = reader.GetString(typeDef.Namespace);
return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}";
}
public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
{
var typeRef = reader.GetTypeReference(handle);
var name = reader.GetString(typeRef.Name);
var ns = reader.GetString(typeRef.Namespace);
return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}";
}
public string GetSZArrayType(string elementType) => $"{elementType}[]";
public string GetArrayType(string elementType, ArrayShape shape) =>
$"{elementType}[{new string(',', shape.Rank - 1)}]";
public string GetByReferenceType(string elementType) => $"ref {elementType}";
public string GetPointerType(string elementType) => $"{elementType}*";
public string GetPinnedType(string elementType) => elementType;
public string GetGenericInstantiation(string genericType, ImmutableArray<string> typeArguments)
{
var name = genericType;
var backtick = name.IndexOf('`');
if (backtick >= 0)
{
name = name[..backtick];
}
return $"{name}<{string.Join(", ", typeArguments)}>";
}
public string GetGenericMethodParameter(object? genericContext, int index) => $"TMethod{index}";
public string GetGenericTypeParameter(object? genericContext, int index) => $"T{index}";
public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired) => unmodifiedType;
public string GetFunctionPointerType(MethodSignature<string> signature) => "delegate*";
public string GetTypeFromSpecification(MetadataReader reader, object? genericContext,
TypeSpecificationHandle handle, byte rawTypeKind)
{
return reader.GetTypeSpecification(handle).DecodeSignature(this, genericContext);
}
}
// =============================================================================
// WinMD Parser — reads WinMD files into structured type info
// =============================================================================
static class WinMdParser
{
public static List<WinMdTypeInfo> ParseFile(string filePath)
{
var types = new List<WinMdTypeInfo>();
try
{
using var stream = File.OpenRead(filePath);
using var peReader = new PEReader(stream);
if (!peReader.HasMetadata)
{
return types;
}
var reader = peReader.GetMetadataReader();
var typeProvider = new SimpleTypeProvider();
foreach (var typeDefHandle in reader.TypeDefinitions)
{
var typeDef = reader.GetTypeDefinition(typeDefHandle);
var name = reader.GetString(typeDef.Name);
var ns = reader.GetString(typeDef.Namespace);
if (ShouldSkipType(name, typeDef))
{
continue;
}
var kind = DetermineTypeKind(reader, typeDef);
var baseType = GetBaseTypeName(reader, typeDef);
var members = ParseMembers(reader, typeDef, typeProvider);
var enumValues = kind == TypeKind.Enum ? ParseEnumValues(reader, typeDef) : null;
var fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}";
types.Add(new WinMdTypeInfo
{
Namespace = ns,
Name = name,
FullName = fullName,
Kind = kind,
BaseType = baseType,
Members = members,
EnumValues = enumValues,
SourceFile = Path.GetFileName(filePath),
});
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Warning: Failed to parse {filePath}: {ex.Message}");
}
return types;
}
internal static bool ShouldSkipType(string name, TypeDefinition typeDef)
{
if (string.IsNullOrEmpty(name) || name == "<Module>" || name.StartsWith('<'))
{
return true;
}
var visibility = typeDef.Attributes & TypeAttributes.VisibilityMask;
return visibility != TypeAttributes.Public && visibility != TypeAttributes.NestedPublic;
}
internal static TypeKind DetermineTypeKind(MetadataReader reader, TypeDefinition typeDef)
{
if ((typeDef.Attributes & TypeAttributes.Interface) != 0)
{
return TypeKind.Interface;
}
var baseType = GetBaseTypeName(reader, typeDef);
return baseType switch
{
"System.Enum" => TypeKind.Enum,
"System.ValueType" => TypeKind.Struct,
"System.MulticastDelegate" or "System.Delegate" => TypeKind.Delegate,
_ => TypeKind.Class,
};
}
private static string? GetBaseTypeName(MetadataReader reader, TypeDefinition typeDef)
{
if (typeDef.BaseType.IsNil)
{
return null;
}
return typeDef.BaseType.Kind switch
{
HandleKind.TypeDefinition => GetTypeDefName(reader, (TypeDefinitionHandle)typeDef.BaseType),
HandleKind.TypeReference => GetTypeRefName(reader, (TypeReferenceHandle)typeDef.BaseType),
_ => null,
};
}
private static string GetTypeDefName(MetadataReader reader, TypeDefinitionHandle handle)
{
var td = reader.GetTypeDefinition(handle);
var ns = reader.GetString(td.Namespace);
var name = reader.GetString(td.Name);
return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}";
}
private static string GetTypeRefName(MetadataReader reader, TypeReferenceHandle handle)
{
var tr = reader.GetTypeReference(handle);
var ns = reader.GetString(tr.Namespace);
var name = reader.GetString(tr.Name);
return string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}";
}
private static List<WinMdMemberInfo> ParseMembers(
MetadataReader reader, TypeDefinition typeDef, SimpleTypeProvider typeProvider)
{
var members = new List<WinMdMemberInfo>();
// Collect property/event accessor methods so we can skip them in the methods loop
var accessorMethods = new HashSet<MethodDefinitionHandle>();
foreach (var propHandle in typeDef.GetProperties())
{
var accessors = reader.GetPropertyDefinition(propHandle).GetAccessors();
if (!accessors.Getter.IsNil) accessorMethods.Add(accessors.Getter);
if (!accessors.Setter.IsNil) accessorMethods.Add(accessors.Setter);
}
foreach (var eventHandle in typeDef.GetEvents())
{
var accessors = reader.GetEventDefinition(eventHandle).GetAccessors();
if (!accessors.Adder.IsNil) accessorMethods.Add(accessors.Adder);
if (!accessors.Remover.IsNil) accessorMethods.Add(accessors.Remover);
if (!accessors.Raiser.IsNil) accessorMethods.Add(accessors.Raiser);
}
// Methods
foreach (var methodHandle in typeDef.GetMethods())
{
if (accessorMethods.Contains(methodHandle))
{
continue;
}
var method = reader.GetMethodDefinition(methodHandle);
var methodName = reader.GetString(method.Name);
if (methodName.StartsWith('.') || methodName.StartsWith('<'))
{
continue;
}
if ((method.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public)
{
continue;
}
try
{
var sig = method.DecodeSignature(typeProvider, null);
var parameters = GetMethodParameters(reader, method, sig);
var paramStr = string.Join(", ", parameters.Select(p => $"{p.Type} {p.Name}"));
members.Add(new WinMdMemberInfo
{
Name = methodName,
Kind = MemberKind.Method,
Signature = $"{sig.ReturnType} {methodName}({paramStr})",
ReturnType = sig.ReturnType,
Parameters = parameters,
});
}
catch
{
members.Add(new WinMdMemberInfo
{
Name = methodName,
Kind = MemberKind.Method,
Signature = $"{methodName}(/* signature not decodable */)",
});
}
}
// Properties
foreach (var propHandle in typeDef.GetProperties())
{
var prop = reader.GetPropertyDefinition(propHandle);
var propName = reader.GetString(prop.Name);
try
{
var propSig = prop.DecodeSignature(typeProvider, null);
var propType = propSig.ReturnType;
var accessors = prop.GetAccessors();
var hasGetter = false;
if (!accessors.Getter.IsNil)
{
var getterDef = reader.GetMethodDefinition(accessors.Getter);
if ((getterDef.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public)
{
hasGetter = true;
}
}
var hasSetter = false;
if (!accessors.Setter.IsNil)
{
var setterDef = reader.GetMethodDefinition(accessors.Setter);
if ((setterDef.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public)
{
hasSetter = true;
}
}
// Skip properties where neither accessor is public
if (!hasGetter && !hasSetter)
{
continue;
}
var accessStr = (hasGetter, hasSetter) switch
{
(true, true) => "{ get; set; }",
(true, false) => "{ get; }",
(false, true) => "{ set; }",
_ => "{ }",
};
members.Add(new WinMdMemberInfo
{
Name = propName,
Kind = MemberKind.Property,
Signature = $"{propType} {propName} {accessStr}",
ReturnType = propType,
});
}
catch
{
members.Add(new WinMdMemberInfo
{
Name = propName,
Kind = MemberKind.Property,
Signature = $"/* type not decodable */ {propName}",
});
}
}
// Events
foreach (var eventHandle in typeDef.GetEvents())
{
var evt = reader.GetEventDefinition(eventHandle);
var evtName = reader.GetString(evt.Name);
var accessors = evt.GetAccessors();
var isPublicEvent = false;
if (!accessors.Adder.IsNil)
{
var adder = reader.GetMethodDefinition(accessors.Adder);
if ((adder.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public)
{
isPublicEvent = true;
}
}
if (!isPublicEvent && !accessors.Remover.IsNil)
{
var remover = reader.GetMethodDefinition(accessors.Remover);
if ((remover.Attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public)
{
isPublicEvent = true;
}
}
if (!isPublicEvent)
{
continue;
}
var evtType = GetHandleTypeName(reader, evt.Type);
members.Add(new WinMdMemberInfo
{
Name = evtName,
Kind = MemberKind.Event,
Signature = $"event {evtType} {evtName}",
ReturnType = evtType,
});
}
return members;
}
private static List<WinMdParameterInfo> GetMethodParameters(
MetadataReader reader, MethodDefinition method, MethodSignature<string> sig)
{
var parameters = new List<WinMdParameterInfo>();
var paramHandles = method.GetParameters().ToList();
var paramNames = new List<string>();
foreach (var ph in paramHandles)
{
var param = reader.GetParameter(ph);
if (param.SequenceNumber > 0)
{
paramNames.Add(reader.GetString(param.Name));
}
}
for (var i = 0; i < sig.ParameterTypes.Length; i++)
{
parameters.Add(new WinMdParameterInfo
{
Name = i < paramNames.Count ? paramNames[i] : $"arg{i}",
Type = sig.ParameterTypes[i],
});
}
return parameters;
}
internal static List<string> ParseEnumValues(MetadataReader reader, TypeDefinition typeDef)
{
var values = new List<string>();
foreach (var fieldHandle in typeDef.GetFields())
{
var field = reader.GetFieldDefinition(fieldHandle);
var fieldName = reader.GetString(field.Name);
if (fieldName == "value__")
{
continue;
}
if ((field.Attributes & FieldAttributes.FieldAccessMask) == FieldAttributes.Public &&
(field.Attributes & FieldAttributes.Static) != 0)
{
values.Add(fieldName);
}
}
return values;
}
private static string GetHandleTypeName(MetadataReader reader, EntityHandle handle) => handle.Kind switch
{
HandleKind.TypeDefinition => GetTypeDefName(reader, (TypeDefinitionHandle)handle),
HandleKind.TypeReference => GetTypeRefName(reader, (TypeReferenceHandle)handle),
HandleKind.TypeSpecification => DecodeTypeSpecification(reader, (TypeSpecificationHandle)handle),
_ => "unknown",
};
private static string DecodeTypeSpecification(MetadataReader reader, TypeSpecificationHandle handle)
{
try
{
var typeSpec = reader.GetTypeSpecification(handle);
return typeSpec.DecodeSignature(new SimpleTypeProvider(), null);
}
catch
{
return "unknown";
}
}
}
<#
.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 }
}
<#
.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
}
Related skills
How it compares
Use winmd-api-search for offline WinMD discovery; use Microsoft Learn when you need narrative docs and samples beyond raw type signatures.
FAQ
What does winmd-api-search do?
Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and
When should I invoke winmd-api-search?
Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is Winmd Api Search safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.