Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,35 @@ The `version-bump` and `changelog-entry` CI jobs enforce this.

### Added

- New `winui.exe` AOT sidecar in `src/tools/winui-cli/` exposing a unified
`winui <noun> <verb>` surface (`api`, `controls`, `project`, `analyzer`) over
`winmd-cli`, `winui-search`, and the embedded WinUI 3 / Windows App SDK Roslyn
analyzer. Intended for framework-agnostic hosts (e.g. `winappcli`) that want
one sidecar instead of bundling multiple exes.
- Committed JSON Schemas (Draft 2020-12) at `src/tools/winui-cli/schemas/*.schema.json`,
five total: one `winui.text-result.v1` shared by all verbs that wrap inner-CLI
text output, plus structured `winui.project.build.v1`, `winui.analyzer.info.v1`,
`winui.error.v1`, `winui.help.v1`. Auto-generated from `[WinUiJsonSchema]`-tagged
records in `src/tools/winui-cli/Schemas/JsonPayloads.cs`. The `schema`
discriminator in every payload is `const`-locked to its shape id; text-wrapper
consumers dispatch on a separate `verb` field (e.g. `"api.update"`,
`"controls.search"`).
- `scripts/build-tools.ps1 -CheckSchemaDrift` snapshots emitted schemas to a
staging directory and diffs against the committed copies — fails the build if
a record's shape changes without regenerating the on-disk schema. Intended for
CI.
- `winui project build` transiently injects the embedded analyzer via a temp
`Directory.Build.props` next to the `.csproj` (then cleans it up in `finally`
with retry-with-backoff), mirroring step 4a of the legacy `BuildAndRun.ps1`.

### Changed

- `winmd-cli`: `api search` now applies `--max` to ambiguous-name results
(previously ignored, returning the full ambiguous set regardless).
- `winui-search` and `winmd-cli`: exposed runner entry points via small library
csproj wrappers (`winmd-lib`, `winui-search-lib`) so `winui.exe` can call them
in-process without spawning subprocesses.

### Fixed

### Removed
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ plugins/winui/ Copilot CLI plugin manifest + agent + skill files
src/tools/ Source for the in-repo tools shipped with the skills
winmd-cli/ Native-AOT WinRT/.NET metadata indexer (winmd.exe)
winui-search/ Native-AOT search over WinUI Gallery + Toolkit (winui-search.exe)
winui-cli/ Native-AOT sidecar that subsumes the above behind a single
`winui <noun> <verb>` surface (winui.exe), plus JSON-schema emitter
winui-analyzer/ Microsoft.WindowsAppSDK.Analyzers Roslyn analyzer
scripts/ Helper scripts (see scripts/build-tools.ps1)
```
Expand Down Expand Up @@ -151,6 +153,7 @@ Several skills ship helper binaries and PowerShell scripts that run under your u
| **`Microsoft.WindowsAppSDK.Analyzers.dll`** (Roslyn analyzer) | [`src/tools/winui-analyzer/`](src/tools/winui-analyzer/) | Catches common WinUI 3 / WinAppSDK pitfalls at build time: UWP namespace leaks, `Window.Current`, `CoreDispatcher`, `WebView2` without `EnsureCoreWebView2Async`, raw `TabView` content, attached-property syntax bugs, removed ONNX GenAI APIs, the old field-backed `[ObservableProperty]` pattern, and more. Every rule ships at `Warning` severity (no `Error`s) and includes a `helpLinkUri`. Verified against source on every PR by the `analyzer-provenance` CI job. | Publish as the `Microsoft.WindowsAppSDK.Analyzers` NuGet package; skill stops shipping the prebuilt DLL and projects pick it up via `<PackageReference>`. |
| **`winmd.exe`** (winmd-cli) | [`src/tools/winmd-cli/`](src/tools/winmd-cli/) | Native-AOT WinRT/.NET metadata indexer. The agent uses it to verify an API actually exists and has the signature it thinks it does — *before* writing code that won't compile. Reads `.winmd` and managed `.dll` metadata from NuGet, the Windows SDK, and WinAppSDK and returns the same XML doc text Visual Studio IntelliSense uses. | Publish as a `dotnet tool` on NuGet, or fold relevant subcommands into [`winappcli`](https://github.com/microsoft/winappcli). |
| **`winui-search.exe`** (winui-search) | [`src/tools/winui-search/`](src/tools/winui-search/) | Native-AOT BM25 search over [WinUI Gallery](https://github.com/microsoft/WinUI-Gallery) and [CommunityToolkit/Windows](https://github.com/CommunityToolkit/Windows) scenarios. Lets the agent see real shipping samples for a control before writing a single line of XAML. Embedded JSON snapshots ship offline; live refresh via `winui-search update`. **Distributed today as a prebuilt unsigned exe inside the `winui-design` skill payload**, verified on every PR by the `winui-search-provenance` CI job. | Same as `winmd-cli` — `dotnet tool`, fold into `winappcli`, or expose over a small MCP server. |
| **`winui.exe`** (winui-cli sidecar) | [`src/tools/winui-cli/`](src/tools/winui-cli/) | Native-AOT sidecar that exposes one `winui <noun> <verb>` surface over the other in-repo tools (`winui api search`/`members`/…, `winui controls search`/`detail`/…, `winui project build`, `winui analyzer info`). `winui project build` transiently injects the embedded `Microsoft.WindowsAppSDK.Analyzers` for the duration of a single build — the project tree is never modified. Designed to be sideloaded by framework-agnostic hosts (e.g. `winappcli`) so they don't have to bundle four separate exes. Every JSON-mode payload is versioned by a schema in [`src/tools/winui-cli/schemas/`](src/tools/winui-cli/schemas/), generated from source via the `winui-schema-emit` tool and checked for drift via `./scripts/build-tools.ps1 -CheckSchemaDrift`. | Publish as a `dotnet tool` on NuGet, or fold into [`winappcli`](https://github.com/microsoft/winappcli). |
| **`BuildAndRun.ps1`** | [`plugins/winui/skills/winui-dev-workflow/BuildAndRun.ps1`](plugins/winui/skills/winui-dev-workflow/BuildAndRun.ps1) | Picks MSBuild over `dotnet build` to work around the XAML-compiler diagnostic gap called out above. After a successful build it hands off to `winapp run`. | **Removed entirely once the next Windows App SDK release fixes the XAML compiler under `dotnet build`** — the skills will switch to `dotnet build` / `dotnet run` directly. |
| **`Analyze-Session.ps1`** | [`plugins/winui/skills/winui-session-report/Analyze-Session.ps1`](plugins/winui/skills/winui-session-report/Analyze-Session.ps1) | Reads your local Copilot session events and produces a `session-report.md` for bug filing. **The report can include excerpts of your prompts, file paths, and command output — review it before sharing.** The skill prints a privacy notice when it runs. | Fold into the `copilot` CLI as a session-report subcommand, or publish as a `dotnet tool`. |

Expand All @@ -159,7 +162,7 @@ If any of this is a deal-breaker for your environment, please [open an issue](ht
### Building the in-repo tools yourself

```powershell
# Build all three tools (AOT-publishes winmd-cli and winui-search), run the
# Build all tools (AOT-publishes winmd-cli, winui-search, and winui-cli), run the
# analyzer test suite, and refresh both committed payloads (analyzer DLL inside
# winui-dev-workflow, winui-search.exe inside winui-design). This is the same
# build the pr-validation workflow runs in CI.
Expand All @@ -171,6 +174,7 @@ Per-tool READMEs cover what they do and how to consume them in more detail:
* [`src/tools/winui-analyzer/README.md`](src/tools/winui-analyzer/README.md) — analyzer + rule catalog
* [`src/tools/winmd-cli/README.md`](src/tools/winmd-cli/README.md) — `winmd` CLI usage
* [`src/tools/winui-search/README.md`](src/tools/winui-search/README.md) — `winui-search` CLI usage
* [`src/tools/winui-cli/README.md`](src/tools/winui-cli/README.md) — `winui` sidecar usage (noun/verb surface + JSON schemas)

## Pinning to a release

Expand Down
Binary file modified plugins/winui/skills/winui-design/winui-search.exe
Binary file not shown.
Binary file not shown.
116 changes: 110 additions & 6 deletions scripts/build-tools.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
<#
.SYNOPSIS
One-shot build for every C# tool in this repo, including the analyzer DLL
payload refresh that the winui-dev-workflow skill ships with.
payload refresh.

.DESCRIPTION
Builds and tests the WinUI 3 / Windows App SDK Roslyn analyzer, then
AOT-publishes winmd-cli and winui-search and refreshes the skill payload
folders that ship the resulting binaries.
AOT-publishes winmd-cli, winui-search, and the winui-cli sidecar, emits
JSON schemas for the winui-cli JSON payloads, and refreshes the skill /
plugin payload folders that ship the resulting binaries (analyzer DLL,
winui-search.exe, src/tools/winui-cli/schemas/).

This script exists to give contributors one verb to run before opening
a PR. The pr-validation.yml workflow will rebuild everything in CI
Expand All @@ -23,8 +25,13 @@

.PARAMETER SkipPayloadRefresh
Don't copy the freshly built artifacts into the
plugins/winui/skills/.../ payload folders. Default: payloads are
refreshed (this is what keeps CI provenance happy).
plugins/winui/skills/.../ payload folders. Default:
payloads are refreshed (this is what keeps CI provenance happy).

.PARAMETER CheckSchemaDrift
Fail the script if the freshly-emitted src/tools/winui-cli/schemas/*.json set
(added / changed / removed files) does not match the committed copy.
Use in CI / pre-commit to catch stale schemas. Default: off.

.EXAMPLE
./scripts/build-tools.ps1
Expand All @@ -33,17 +40,26 @@
.EXAMPLE
./scripts/build-tools.ps1 -SkipTests -SkipPayloadRefresh
# Quick build only — skip tests and payload copy. Useful while iterating.

.EXAMPLE
./scripts/build-tools.ps1 -SkipTests -SkipPayloadRefresh -CheckSchemaDrift
# Verify committed winui-cli JSON schemas are still in sync with source.
#>

[CmdletBinding()]
param(
[string]$Configuration = 'Release',
[switch]$SkipTests,
[switch]$SkipPayloadRefresh
[switch]$SkipPayloadRefresh,
[switch]$CheckSchemaDrift
)

$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $PSScriptRoot
$vsInstallerDir = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer'
if (Test-Path (Join-Path $vsInstallerDir 'vswhere.exe')) {
$env:PATH = "$vsInstallerDir;$env:PATH"
}

function Step([string]$msg) {
Write-Host ""
Expand Down Expand Up @@ -115,12 +131,100 @@ if (-not $SkipPayloadRefresh) {
Warn "skipping winui-search payload refresh (-SkipPayloadRefresh)"
}

# -------------------- 4. winui-cli ------------------------------------------

$winuiProj = Join-Path $repoRoot 'src/tools/winui-cli/winui-cli.csproj'
$schemaEmitProj = Join-Path $repoRoot 'src/tools/winui-cli/SchemaGen/WinUi.SchemaEmit.csproj'

Step "Building winui-cli ($Configuration)"
dotnet publish $winuiProj -c $Configuration -r win-x64 --self-contained true /p:PublishAot=true /p:StripSymbols=true --nologo
if ($LASTEXITCODE -ne 0) { throw "winui-cli build failed" }
Ok "winui-cli built"

Step "Building winui schema emitter ($Configuration)"
dotnet build $schemaEmitProj -c $Configuration --nologo
if ($LASTEXITCODE -ne 0) { throw "winui schema emitter build failed" }
Ok "winui schema emitter built"

Step "Emitting JSON schemas from winui-cli payloads"
$winuiManagedDll = Join-Path $repoRoot "src/tools/winui-cli/bin/$Configuration/net10.0/win-x64/winui.dll"
if (-not (Test-Path $winuiManagedDll)) { throw "Managed winui.dll not found at: $winuiManagedDll" }
$schemasOutDir = Join-Path $repoRoot 'src/tools/winui-cli/schemas'
$schemaEmitDll = Join-Path $repoRoot "src/tools/winui-cli/SchemaGen/bin/$Configuration/net10.0/winui-schema-emit.dll"

# Always emit into a clean staging dir, then sync to the committed dir.
# This is what lets -CheckSchemaDrift detect deletions/renames: the committed
# dir is the baseline, and the staging dir is authoritative for what the
# emitter currently produces.
$stagingDir = Join-Path ([System.IO.Path]::GetTempPath()) "winui-schemas-$([Guid]::NewGuid().ToString('N'))"
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
try {
dotnet $schemaEmitDll $winuiManagedDll $stagingDir
if ($LASTEXITCODE -ne 0) { throw "schema emission failed" }

if ($CheckSchemaDrift) {
$committedFiles = @{}
if (Test-Path $schemasOutDir) {
foreach ($f in Get-ChildItem -Path $schemasOutDir -Filter '*.json' -File) {
$committedFiles[$f.Name] = [System.IO.File]::ReadAllBytes($f.FullName)
}
}
$stagedFiles = @{}
foreach ($f in Get-ChildItem -Path $stagingDir -Filter '*.json' -File) {
$stagedFiles[$f.Name] = [System.IO.File]::ReadAllBytes($f.FullName)
}

$driftReasons = @()
foreach ($name in $stagedFiles.Keys) {
if (-not $committedFiles.ContainsKey($name)) {
$driftReasons += "added: $name"
} elseif (-not [Linq.Enumerable]::SequenceEqual([byte[]]$committedFiles[$name], [byte[]]$stagedFiles[$name])) {
$driftReasons += "changed: $name"
}
}
foreach ($name in $committedFiles.Keys) {
if (-not $stagedFiles.ContainsKey($name)) {
$driftReasons += "removed: $name"
}
}
if ($driftReasons.Count -gt 0) {
Write-Host ""
Write-Host "ERROR: Schema drift detected." -ForegroundColor Red
foreach ($r in $driftReasons) { Write-Host " $r" -ForegroundColor Red }
Write-Host " Committed src/tools/winui-cli/schemas/ does not match the live winui-cli payload shape." -ForegroundColor Red
Write-Host " Run './scripts/build-tools.ps1' locally and commit the regenerated schemas." -ForegroundColor Red
throw "schema drift"
}
Ok "schemas in sync with payload (no drift)"
}

# Sync staging -> committed: remove obsolete .json, copy current.
New-Item -ItemType Directory -Force -Path $schemasOutDir | Out-Null
$stagedNames = @{}
foreach ($f in Get-ChildItem -Path $stagingDir -Filter '*.json' -File) {
$stagedNames[$f.Name] = $true
Copy-Item $f.FullName (Join-Path $schemasOutDir $f.Name) -Force
}
foreach ($f in Get-ChildItem -Path $schemasOutDir -Filter '*.json' -File) {
if (-not $stagedNames.ContainsKey($f.Name)) {
Remove-Item $f.FullName -Force
}
}
Ok "schemas emitted: $schemasOutDir"
}
finally {
if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force }
}

# -------------------- Done --------------------------------------------------

Step "All tools built successfully"
Write-Host " Analyzer payload: plugins/winui/skills/winui-dev-workflow/analyzer/" -ForegroundColor DarkGray
Write-Host " AOT exes:" -ForegroundColor DarkGray
Write-Host " src/tools/winmd-cli/bin/$Configuration/net10.0/<rid>/publish/winmd.exe" -ForegroundColor DarkGray
Write-Host " src/tools/winui-search/bin/$Configuration/net10.0/<rid>/publish/winui-search.exe" -ForegroundColor DarkGray
Write-Host " src/tools/winui-cli/bin/$Configuration/net10.0/win-x64/publish/winui.exe" -ForegroundColor DarkGray
Write-Host " Skill payloads:" -ForegroundColor DarkGray
Write-Host " plugins/winui/skills/winui-design/winui-search.exe (refreshed)" -ForegroundColor DarkGray
Write-Host " Schemas:" -ForegroundColor DarkGray
Write-Host " src/tools/winui-cli/schemas/*.schema.json (auto-generated from [WinUiJsonSchema] records)" -ForegroundColor DarkGray
46 changes: 43 additions & 3 deletions src/tools/winmd-cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,26 @@
using System.Xml.Linq;

[CompilerGenerated]
internal class Program
public static class WinMdCliRunner
{
private static int Main(string[] args)
public static int Run(string[] args, TextWriter stdout, TextWriter stderr, bool json)
{
TextWriter oldOut = Console.Out;
TextWriter oldErr = Console.Error;
try
{
Console.SetOut(stdout);
Console.SetError(stderr);
return Run(args);
}
finally
{
Console.SetOut(oldOut);
Console.SetError(oldErr);
}
}

public static int Run(string[] args)
{
if (args.Length == 0)
{
Expand Down Expand Up @@ -446,7 +463,25 @@ static string ResolveCacheDir(CliArgs cli)
}
if (files.Length == 1)
{
return DeserializeManifest(files[0]);
ProjectManifest only = DeserializeManifest(files[0]);
if (only != null)
{
string cwd = Path.GetFullPath(cli.ProjectDir ?? Directory.GetCurrentDirectory());
string? cwdName = FindProjectNameInDir(cwd);
bool cwdMatches = only.ProjectDir != null && only.ProjectDir.Equals(cwd, StringComparison.OrdinalIgnoreCase);
bool nameMatches = cwdName != null && (Path.GetFileNameWithoutExtension(files[0]).Equals(cwdName, StringComparison.OrdinalIgnoreCase) || Path.GetFileNameWithoutExtension(files[0]).StartsWith(cwdName + "_", StringComparison.OrdinalIgnoreCase));
if (cwdMatches || nameMatches || cwdName == null)
{
if (!cwdMatches && cwdName == null)
{
Console.Error.WriteLine("Note: Using only cached project '" + Path.GetFileNameWithoutExtension(files[0]) + "' (no project detected in current directory).");
}
return only;
}
Console.Error.WriteLine("Cached project '" + Path.GetFileNameWithoutExtension(files[0]) + "' does not match current directory. Run '" + WinMdInvocation.CommandPrefix + " update' to index this project, or pass --project.");
return null;
}
return null;
}
string text2 = FindProjectNameInDir(cli.ProjectDir ?? Directory.GetCurrentDirectory());
if (text2 != null)
Expand Down Expand Up @@ -559,3 +594,8 @@ static int RunUpdate(CliArgs cli)
}
}
}

internal sealed class Program
{
private static int Main(string[] args) => WinMdCliRunner.Run(args);
}
Loading
Loading