diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml
index adeaddd50..76b6e7e00 100644
--- a/.github/workflows/bootstrap.yml
+++ b/.github/workflows/bootstrap.yml
@@ -2,7 +2,7 @@ name: Bootstrap
# Exercises bootstrap.ps1 end-to-end on a fresh windows-latest runner to
# validate the source-checkout developer flow (`git clone` → `bootstrap.ps1`
-# → `dotnet new reactorapp`). The script is invoked non-interactively via
+# → `dotnet new reactor`). The script is invoked non-interactively via
# `-InstallWinAppSdk -SkipPlugin`, which also exercises the winget install
# path for the Windows App Runtime when the runner image doesn't already
# have it.
@@ -13,7 +13,6 @@ on:
- 'bootstrap.ps1'
- 'tools/BootstrapFeedResolver.ps1'
- 'src/Reactor.Cli/**'
- - 'tools/Templates/**'
- 'Directory.Build.props'
- 'Directory.Build.targets'
- 'Directory.Packages.props'
@@ -24,7 +23,6 @@ on:
- 'bootstrap.ps1'
- 'tools/BootstrapFeedResolver.ps1'
- 'src/Reactor.Cli/**'
- - 'tools/Templates/**'
- 'Directory.Build.props'
- 'Directory.Build.targets'
- 'Directory.Packages.props'
@@ -144,7 +142,6 @@ jobs:
run: |
$expected = @(
'local-nupkgs/Microsoft.UI.Reactor.0.0.0-local.nupkg',
- 'local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg',
'local-nupkgs/Microsoft.UI.Reactor.Cli.1.0.0.nupkg'
)
$missing = @()
@@ -166,22 +163,62 @@ jobs:
if ($cliPkgs.Count -eq 0) { throw "No Microsoft.UI.Reactor.Cli.*.nupkg found in local-nupkgs/" }
Write-Host " [ok] CLI tool nupkg: $($cliPkgs[0].Name)"
- - name: Verify dotnet new reactorapp template registered
+ - name: Verify dotnet new reactor templates registered
shell: pwsh
run: |
- $listing = dotnet new list reactorapp 2>&1
+ # Reactor's app templates ship in the Windows App SDK `dotnet new`
+ # pack, which bootstrap.ps1 §5 installs via `winapp new --list`.
+ $listing = dotnet new list reactor 2>&1
$rc = $LASTEXITCODE
Write-Host $listing
- if ($rc -ne 0) { throw "dotnet new list reactorapp exited $rc" }
+ if ($rc -ne 0) { throw "dotnet new list reactor exited $rc" }
# `dotnet new list` writes a multi-line table; PowerShell stores the
# output as a [string[]]. -match / -notmatch against an array filter
- # element-wise, so `-notmatch 'reactorapp'` returns the non-matching
+ # element-wise, so `-notmatch 'reactor'` returns the non-matching
# lines (header, separator) which evaluates truthy even when the
# template is present. Join + match to do a whole-output substring
# check instead.
- if (($listing -join "`n") -notmatch 'reactorapp') {
- throw "reactorapp template not found in `dotnet new list` output"
+ $joined = $listing -join "`n"
+ # The "not found" message repeats the search term and prints a
+ # `dotnet new search reactor` hint, both of which contain `reactor` as
+ # a whole token — so the token match below reports success precisely
+ # when the template is absent. Check the negative marker first, exactly
+ # as InterpretTemplateListOutput does.
+ if ($joined -match 'No templates found') {
+ throw "``dotnet new list reactor`` found no templates"
}
+ # Match each short name as a whole token. A plain substring check (or a
+ # \b regex) also matches `reactor-mvu` and `winui-reactor`, because '-'
+ # is a word boundary — so a pack carrying only the richer shells would
+ # pass the `reactor` probe. Short names are comma-separated inside a
+ # whitespace-delimited column, so require one of those on each side.
+ # Case-sensitive on purpose: the Template Name column carries the
+ # capitalised prose word ("Reactor MVU App"), which is a standalone
+ # token and would satisfy a case-insensitive match on its own.
+ foreach ($shortName in @('reactor', 'reactor-mvu', 'reactor-navview', 'reactor-tabview')) {
+ $token = '(?&1 | Out-String) -split "`r?`n" | ForEach-Object { $_.Trim() }
+ if ($packageLines -notcontains 'Microsoft.WindowsAppSDK.WinUI.CSharp.Templates') {
+ throw "Microsoft.WindowsAppSDK.WinUI.CSharp.Templates is not installed"
+ }
+ Write-Host " [ok] Microsoft.WindowsAppSDK.WinUI.CSharp.Templates registered"
+ # The in-repo Microsoft.UI.Reactor.ProjectTemplates pack was deleted.
+ # Nothing may resurrect it: a stray install would hand new developers
+ # the unpackaged `reactorapp` template the docs no longer describe.
+ if ($packageLines -contains 'Microsoft.UI.Reactor.ProjectTemplates') {
+ throw "the removed Microsoft.UI.Reactor.ProjectTemplates pack is installed; it should no longer exist"
+ }
+ Write-Host " [ok] removed Microsoft.UI.Reactor.ProjectTemplates not installed"
- name: Scaffold a TestApp and restore against the local feed
shell: pwsh
@@ -189,22 +226,31 @@ jobs:
# Inside the repo so the root nuget.config (which maps local-nupkgs/)
# is picked up by NuGet's parent-dir walk.
New-Item -ItemType Directory -Path TestProjects | Out-Null
- # The scaffolded TestApp lives under the repo, so NuGet's parent-dir
- # walk would otherwise inherit the repo-root Directory.Packages.props
- # and flip the app into Central Package Management — conflicting with
- # the template's inline PackageReference versions (NU1008). Drop a
- # nearer Directory.Packages.props that shadows the root one and turns
- # CPM off, faithfully mimicking a real end-user scaffold (which has no
- # central package props above it).
+ # The scaffolded TestApp lives under the repo, so MSBuild's and NuGet's
+ # parent-dir walks would otherwise apply the repo's own build rules to
+ # what is meant to be an end-user scaffold. Both walks stop at the first
+ # file they find, so shadowing them here isolates the app — which is
+ # what a real scaffold outside the repo gets.
+ #
+ # Directory.Packages.props — the root one flips the app into Central
+ # Package Management, conflicting with the template's inline
+ # PackageReference versions (NU1008).
+ # Directory.Build.props/.targets — the root pair sets
+ # IsAotCompatible=true and promotes IL trim warnings to hard errors.
+ # A packaged Release build runs ILLink, and CsWinRT's generated ABI
+ # code emits IL2081 by design, so the app fails to build with 36
+ # errors and NETSDK1144. Outside the repo those stay warnings.
Set-Content -Path TestProjects/Directory.Packages.props -Value 'false'
+ Set-Content -Path TestProjects/Directory.Build.props -Value ''
+ Set-Content -Path TestProjects/Directory.Build.targets -Value ''
Push-Location TestProjects
try {
# Bootstrap validates the local 0.0.0-local feed produced by
# mur pack-local. The template's normal default may point at a
# not-yet-published public preview while a release-prep PR is in
- # flight, so opt into the local package version explicitly here.
- dotnet new reactorapp -n TestApp --MSUIReactorVersion 0.0.0-local
- if ($LASTEXITCODE -ne 0) { throw "dotnet new reactorapp exited $LASTEXITCODE" }
+ # flight, so opt into the local package version explicitly here
+ dotnet new reactor -n TestApp --reactor-version 0.0.0-local
+ if ($LASTEXITCODE -ne 0) { throw "dotnet new reactor exited $LASTEXITCODE" }
if (-not (Test-Path 'TestApp/TestApp.csproj')) { throw "TestApp/TestApp.csproj not produced" }
dotnet restore TestApp/TestApp.csproj --nologo -v:m
@@ -213,14 +259,12 @@ jobs:
Pop-Location
}
- - name: Build TestApp (template ships WindowsAppSDKSelfContained=true)
+ - name: Build TestApp (packaged single-project MSIX)
shell: pwsh
run: |
- # The scaffolded template (tools/Templates/templates/WinUIApp-CSharp)
- # explicitly sets WindowsAppSDKSelfContained=true so end users get a
- # standalone deployable. SelfContained=true requires a concrete arch
- # to embed the runtime under — AnyCPU is rejected by the
- # SelfContained target. Pass the host arch explicitly.
+ # The Windows App SDK Reactor templates scaffold a *packaged*
+ # (single-project MSIX) app, which requires a concrete architecture —
+ # AnyCPU is rejected. Pass the host arch explicitly.
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'ARM64' } else { 'x64' }
dotnet build TestProjects/TestApp/TestApp.csproj `
-c Release `
@@ -228,11 +272,119 @@ jobs:
--nologo -v:m
if ($LASTEXITCODE -ne 0) { throw "TestApp build exited $LASTEXITCODE" }
+ # A packaged app can build cleanly and still fail to register or activate,
+ # and "scaffolded apps are now packaged" is the central behaviour change in
+ # this migration — so building is not end-to-end coverage. The integration
+ # test this PR deleted did scaffold→`dotnet run`→UIA against the *unpackaged*
+ # `reactorapp` template; this is its packaged replacement, asserting the two
+ # things the build cannot: the loose layout registers, and the app activates.
+ #
+ # Registering an unsigned loose MSIX layout requires Developer Mode. GitHub's
+ # windows runners are administrators, so the AppModelUnlock switch is settable
+ # directly — same approach as the Packaged Selftests job in ci.yml.
+ - name: Enable Developer Mode
+ shell: pwsh
+ run: |
+ $key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock'
+ New-Item -Path $key -Force | Out-Null
+ New-ItemProperty -Path $key -Name AllowDevelopmentWithoutDevLicense `
+ -PropertyType DWord -Value 1 -Force | Out-Null
+ $v = (Get-ItemProperty -Path $key -Name AllowDevelopmentWithoutDevLicense).AllowDevelopmentWithoutDevLicense
+ if ($v -ne 1) { throw "Developer Mode is not enabled (AllowDevelopmentWithoutDevLicense=$v)." }
+
+ - name: Launch TestApp (packaged registration + activation)
+ shell: pwsh
+ run: |
+ $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'ARM64' } else { 'x64' }
+ $job = $null
+ $proc = $null
+ Push-Location TestProjects/TestApp
+ try {
+ # `dotnet run` on a single-project MSIX registers a loose-layout
+ # package and activates it. It does not return while the app is up,
+ # so run it as a job and poll.
+ $job = Start-Job -ScriptBlock {
+ param($dir, $arch)
+ Set-Location $dir
+ dotnet run -c Release "-p:Platform=$arch" --nologo 2>&1
+ } -ArgumentList (Get-Location).Path, $arch
+
+ for ($i = 0; $i -lt 90; $i++) {
+ Start-Sleep -Seconds 2
+ $proc = Get-Process -Name 'TestApp' -ErrorAction SilentlyContinue | Select-Object -First 1
+ if ($proc) { break }
+ if ($job.State -eq 'Completed' -or $job.State -eq 'Failed') { break }
+ }
+
+ # The Reactor manifest's Identity/@Name is a generated GUID, not the
+ # project name, so `Get-AppxPackage -Name TestApp` finds nothing even
+ # on a perfectly successful activation. Read the identity the
+ # template actually emitted.
+ [xml]$manifest = Get-Content 'Package.appxmanifest' -Raw
+ $identity = $manifest.Package.Identity.Name
+ Write-Host " package identity: $identity"
+ $pkg = Get-AppxPackage -Name $identity -ErrorAction SilentlyContinue
+
+ if (-not $proc) {
+ Write-Host '--- dotnet run output ---'
+ Receive-Job $job 2>&1 | Write-Host
+ throw 'TestApp never started — the packaged app built but did not register/activate.'
+ }
+
+ Write-Host " [ok] TestApp activated (pid $($proc.Id))"
+ if ($pkg) { Write-Host " [ok] registered package $($pkg.PackageFullName)" }
+ else { throw "TestApp is running but no loose-layout package is registered for identity '$identity'." }
+
+ # A process that starts and a package that registers do not prove the
+ # Reactor UI rendered — an app can come up in Reactor's render-error
+ # fallback with a broken App.cs or package reference and still satisfy
+ # both. The deleted CreateTemplateTests caught that by asserting a UIA
+ # element; this is the packaged equivalent. The blank template renders
+ # `TitleBar("$projectname$")`, so a TitleBar named TestApp is present
+ # exactly when the scaffold rendered.
+ # Resolve winapp the same way bootstrap's Get-WinAppCliPath does:
+ # winget's MSIX install drops an app-execution alias that exists on
+ # disk without always being resolvable through this process's PATH,
+ # so a bare `winapp` can fail on exactly that installation state.
+ $winApp = (Get-Command winapp -ErrorAction SilentlyContinue).Source
+ if (-not $winApp) {
+ $alias = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\winapp.exe'
+ if (Test-Path $alias) { $winApp = $alias }
+ }
+ if (-not $winApp) { throw 'winapp CLI not found, so the rendered-UI assertion cannot run.' }
+
+ $rendered = $false
+ for ($i = 0; $i -lt 20; $i++) {
+ $hit = & $winApp ui search 'TestApp' --window $proc.MainWindowHandle --json 2>&1 | Out-String
+ if ($LASTEXITCODE -eq 0 -and $hit -match 'TestApp') { $rendered = $true; break }
+ Start-Sleep -Seconds 3
+ $proc.Refresh()
+ }
+ if (-not $rendered) {
+ Write-Host '--- UI tree ---'
+ & $winApp ui inspect --app TestApp 2>&1 | Write-Host
+ throw 'TestApp activated but its Reactor UI did not render (no element named TestApp in the tree).'
+ }
+ Write-Host ' [ok] Reactor UI rendered (TitleBar present)'
+ } finally {
+ # Every throw above leaves a live packaged app and a blocked
+ # `dotnet run` job behind. Tear them down here, not on the success
+ # path only: a leaked TestApp contaminates the idempotence steps that
+ # follow, and a leaked job can leave the runner waiting on it.
+ if ($proc) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }
+ if ($job) {
+ Stop-Job $job -ErrorAction SilentlyContinue
+ Remove-Job $job -Force -ErrorAction SilentlyContinue
+ }
+ Pop-Location
+ }
+
- name: Verify mur upgrade is idempotent
shell: pwsh
run: |
# `mur upgrade` should succeed against an already-bootstrapped tree:
- # re-pack, re-install template (uninstall-first), refresh plugin.
+ # re-pack, ensure the `dotnet new reactor` templates are installed,
+ # refresh plugin.
$env:Path = "$env:USERPROFILE\.dotnet\tools;$env:Path"
mur upgrade --skip-plugin
if ($LASTEXITCODE -ne 0) { throw "mur upgrade exited $LASTEXITCODE" }
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 095dae877..d7ff45ef4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -189,7 +189,7 @@ jobs:
- name: Pack Devtools NuGet
# Microsoft.UI.Reactor.Devtools is referenced by the scaffolded
- # `dotnet new reactorapp` csproj's Debug-only ItemGroup (the devtools menu + the
+ # `dotnet new reactor` csproj's Debug-only ItemGroup (the devtools menu + the
# Reactor Visual Studio embedded-preview extension both need it). Ship it alongside
# the framework so the template's restore succeeds against the published feed.
# Version is locked to the framework version (spec 022). Packs from the AnyCPU
@@ -201,21 +201,6 @@ jobs:
-p:Version=${{ steps.version.outputs.version }}
-o artifacts/nupkg
- - name: Pack Templates
- # MicrosoftUIReactorVersion stamps the framework version that generated
- # apps reference (baked into template.json by the csproj's BeforePack
- # target). Lock it to the version published in this same run so the
- # template can never drift behind the framework — same value used for
- # the framework/Advanced/Devtools packs above. Without this it silently
- # falls back to the csproj's local-dev default.
- run: >
- dotnet pack tools\Templates\Microsoft.UI.Reactor.Templates.csproj
- --no-build --configuration Release
- -p:Version=${{ steps.version.outputs.version }}
- -p:MicrosoftUIReactorVersion=${{ steps.version.outputs.version }}
- -p:Platform=AnyCPU
- -o artifacts/nupkg
-
- name: Build VSIX
shell: pwsh
run: |
@@ -381,7 +366,7 @@ jobs:
### `Microsoft.UI.Reactor.Devtools.${{ steps.version.outputs.version }}.nupkg` — optional devtools host (debug + VS preview)
- Referenced by the scaffolded `dotnet new reactorapp` csproj in a
+ Referenced by the scaffolded `dotnet new reactor` csproj in a
Debug-only `ItemGroup`, paired with `RuntimeHostConfigurationOption
Reactor.DevtoolsSupport=true`. Required to light up the right-click
devtools menu in Debug F5 launches and for the Reactor VS embedded
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fb270f41..1b9f4bbfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,6 +45,13 @@ Conventions for contributors:
shape acts as a positive control, so a broken probe is reported as unverified rather than
blamed on the deployment. (issue #1268)
+- **`mur templates status`** reports whether `dotnet new reactor` actually resolves, rather than
+ merely whether the template pack id is registered — an installed-but-too-old pack (`0.0.6-alpha`
+ shipped before the Reactor templates existed) is a PASS on the package check and a failure on the
+ next scaffold. Exit codes are load-bearing: `0` available, `1` probe failed, `2` pack installed
+ but too old, `3` pack not installed. It backs `mur doctor` and `bootstrap.ps1`'s verification
+ step, which give different remediation for each.
+
### Changed
- **The search index emits every clean `SampleCard` on a page, not just the first
@@ -65,8 +72,46 @@ Conventions for contributors:
### Deprecated
+- **`Microsoft.UI.Reactor.ProjectTemplates` is deprecated on NuGet.org.** Published versions
+ remain restorable but are marked deprecated with a pointer to `dotnet new reactor`. Scaffold the
+ replacement with `winapp new -t reactor -n MyApp`, which installs the Windows App SDK template
+ pack on demand. To install the pack without scaffolding, pin it explicitly —
+ `dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::` — since it is
+ prerelease-only and a bare `dotnet new install` resolves stable versions.
+
### Removed
+- **Removed the in-repo `Microsoft.UI.Reactor.ProjectTemplates` package and its
+ `dotnet new reactorapp` template** (`tools/Templates/`). Reactor's project templates now ship in
+ the official Windows App SDK `dotnet new` pack
+ ([`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.WinUI.CSharp.Templates)
+ `0.0.7-alpha` and later), which provides `reactor`, `reactor-mvu`, `reactor-navview`, and
+ `reactor-tabview` (microsoft/WindowsAppSDK#6620, microsoft/WindowsAppSDK#6786).
+
+ **Behaviour change:** scaffolded apps are now **packaged** (single-project MSIX) rather than
+ unpackaged, so `dotnet run` launches with full package identity and requires Developer Mode.
+ No template produces the unpackaged shape any more — scaffold with `dotnet new reactor` and set
+ `None` yourself (see the packaging guide).
+
+ `mur pack-local` no longer produces a templates nupkg and its `--framework-version` flag is
+ gone; the release workflow no longer packs or publishes the package.
+
+- **Removed `mur templates install`.** Installing the Windows App SDK template pack is the
+ Windows App SDK CLI's job: `winapp new -t reactor -n MyApp` installs the pack on demand and
+ scaffolds in one step, and `winapp new --list` installs it without scaffolding. `bootstrap.ps1`
+ now drives that command, and `mur upgrade` reports on the templates instead of installing them.
+ `mur templates status` (see Added) still backs `mur doctor` and bootstrap's verification.
+
+ Reactor carried its own installer because `dotnet new install` has no `--prerelease` switch and
+ resolves stable-only, which fails outright while the pack is prerelease-only; working around
+ that meant resolving versions off the NuGet flat container and tiptoeing around
+ `dotnet new install --force`, which uninstalls the existing pack *before* downloading the
+ replacement. `winapp` handles all of it, so roughly 700 lines of that machinery are gone.
+
+ The `-WinAppSdkTemplatesSource` bootstrap parameter is removed with it — `winapp` has no
+ local-folder equivalent. `-WinAppSdkTemplatesVersion` still works and now maps to
+ `winapp new --template-version`.
+
### Fixed
- **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue
diff --git a/Directory.Build.props b/Directory.Build.props
index 0d96b4097..294cb62f9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -40,12 +40,8 @@
diff --git a/README.md b/README.md
index 8cf4e017a..35ce136ab 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,22 @@ Many of the experiments in this repo — the charting stack, accessibility valid
## Quick start
-Reactor ships the public preview package [`Microsoft.UI.Reactor`](https://www.nuget.org/packages/Microsoft.UI.Reactor) on NuGet.org; see the [NuGet page](https://www.nuget.org/packages/Microsoft.UI.Reactor) or [GitHub Releases](https://github.com/microsoft/microsoft-ui-reactor/releases) for the current version. The project template is still installed from source for now; `bootstrap.ps1` installs the `mur` CLI, packs/registers the local `reactorapp` template, and that template references the public preview package by default.
+Reactor ships the public preview package [`Microsoft.UI.Reactor`](https://www.nuget.org/packages/Microsoft.UI.Reactor) on NuGet.org; see the [NuGet page](https://www.nuget.org/packages/Microsoft.UI.Reactor) or [GitHub Releases](https://github.com/microsoft/microsoft-ui-reactor/releases) for the current version. The project templates ship in the official Windows App SDK `dotnet new` pack, so building an app needs no source checkout:
+
+```powershell
+# `winapp` is the Windows App SDK CLI; `winapp new` installs the template pack
+# on demand, so there's no separate install step and no version to pin.
+winget install Microsoft.WinAppCli
+winapp new -t reactor -n MyApp
+cd MyApp
+dotnet run
+```
+
+Scaffolded apps are packaged (single-project MSIX), so `dotnet run` launches them with full package identity — the F5 equivalent. `reactor-mvu`, `reactor-navview`, and `reactor-tabview` start from richer shells (`winapp new --list` prints them all). Requires Developer Mode (Settings → System → For developers).
+
+Prefer plain .NET tooling? `dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::` followed by `dotnet new reactor -n MyApp` does the same thing. The pack is prerelease-only today and `dotnet new install` resolves stable versions unless you pin one explicitly, which is the step `winapp new` removes.
+
+### Contributing to Reactor itself
```powershell
git clone https://github.com/microsoft/microsoft-ui-reactor.git
@@ -78,7 +93,7 @@ cd microsoft-ui-reactor
# calling the bootstrap system under the correct PowerShell version you're on
& (Get-Process -Id $PID).Path -ExecutionPolicy Bypass -File .\bootstrap.ps1
-dotnet new reactorapp -n MyApp
+dotnet new reactor -n MyApp
cd MyApp
dotnet run -p:Platform=x64
```
@@ -97,7 +112,9 @@ dotnet run -p:Platform=x64
> causes `WindowsAppSDKSelfContained` errors. This applies to `dotnet build`,
> `dotnet run`, and `mur check` invocations alike.
-`bootstrap.ps1` packs `mur` as a `dotnet tool` global install (cross-shell PATH, no per-arch `$env:Path` edits), packs local framework snapshots plus project templates into `local-nupkgs/`, registers the `dotnet new reactorapp` template, and installs the Reactor agent plugin under `~/.claude/plugins/reactor`. Apps created by the template reference the public `Microsoft.UI.Reactor` package from NuGet.org by default; pass `--MSUIReactorVersion 0.0.0-local` when you intentionally want a scaffolded app to consume the local source-built package instead. The optional `Microsoft.UI.Reactor.Advanced` and `Microsoft.UI.Reactor.Devtools` sibling packages are version-matched to the framework package when published. Re-run `bootstrap.ps1` (or `mur upgrade` for a lighter refresh) after `git pull` when you want updated local templates or CLI/plugin bits. Verify a working developer install with `mur doctor`.
+`bootstrap.ps1` packs `mur` as a `dotnet tool` global install (cross-shell PATH, no per-arch `$env:Path` edits), packs local framework snapshots into `local-nupkgs/`, installs the Windows App SDK `dotnet new` template pack via the Windows App SDK CLI (`winapp new --list`, which provides `dotnet new reactor`), and installs the Reactor agent plugin under `~/.claude/plugins/reactor`. Apps created by the template reference the public `Microsoft.UI.Reactor` package from NuGet.org by default; pass `--reactor-version 0.0.0-local` when you intentionally want a scaffolded app to consume the local source-built package instead. To hold the template pack at a known-good version, run `./bootstrap.ps1 -WinAppSdkTemplatesVersion `. The optional `Microsoft.UI.Reactor.Advanced` and `Microsoft.UI.Reactor.Devtools` sibling packages are version-matched to the framework package when published. Re-run `bootstrap.ps1` (or `mur upgrade` for a lighter refresh) after `git pull` when you want updated CLI/plugin bits. Verify a working developer install with `mur doctor`.
+
+> **Scaffolding.** Reactor's templates ship in the official Windows App SDK template pack. `winapp new -t reactor` installs that pack on demand and scaffolds in one step; `dotnet new reactor` works too once the pack is installed. The in-repo `Microsoft.UI.Reactor.ProjectTemplates` package that used to provide `dotnet new reactorapp` has been removed; its published versions are deprecated on NuGet.org.
On networks where the public npm or NuGet registries are unreachable, bootstrap detects a recognised package mirror already configured in the user's `~/.npmrc` and NuGet.Config, verifies unauthenticated package access, and uses it only for the bootstrap process — including the optional Visual Studio extension build. Contributors on an unrestricted network keep the public defaults, unchanged. Any mirror can be selected explicitly with `-NpmRegistry ` and `-NuGetConfig `; credentials remain in user configuration and are never written to the repository. Package feed URLs must use HTTPS (except loopback development feeds) and cannot embed credentials, query strings, or fragments. The npm mirror must permit direct package downloads because the Copilot SDK's MSBuild download task cannot forward npm credentials.
diff --git a/Reactor.slnx b/Reactor.slnx
index 147f15085..8d7225f04 100644
--- a/Reactor.slnx
+++ b/Reactor.slnx
@@ -494,9 +494,6 @@
-
-
-
diff --git a/SKILL.md b/SKILL.md
index 09cd9c3e4..86ae6220d 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -217,12 +217,13 @@ In selfhost the version is `0.0.0-local` (produced by `mur pack-local` —
see "Which mode are you in?" above). Outside the source clone, replace it
with whatever Microsoft.UI.Reactor version you depend on.
-**After `dotnet new reactorapp -n `, the workspace contains
-exactly two source files: `App.cs` (entry point + initial component)
-and `.csproj`.** There is no `Program.cs` and no
-`GlobalUsings.cs` — modify `App.cs` in place. The `.csproj` does
-**not** enable implicit usings; `App.cs` has its own `using`
-directives at the top — the canonical set (System + Reactor +
+**After `dotnet new reactor -n `, the entry point is `App.cs`**
+(entry point + initial component), next to `.csproj`. The template
+also emits packaging scaffolding you normally don't touch:
+`Package.appxmanifest`, `app.manifest`, `Assets/`, and
+`Properties/launchSettings.json` + `Properties/PublishProfiles/`. There is no
+`Program.cs` and no `GlobalUsings.cs` — modify `App.cs` in place. `App.cs` has
+its own `using` directives at the top — the canonical set (System + Reactor +
Reactor.Core + Reactor.Layout + Xaml + Xaml.Controls + static
Factories) — which is the only place you add new namespaces (e.g. `using System.Linq;` when
you reach for `.Select(...)`). Don't probe the `.csproj` after
@@ -230,6 +231,11 @@ scaffolding unless you're adding a `PackageReference` or changing a
property — `Restore succeeded.` in the scaffold stdout is the only
confirmation you need.
+Apps from `dotnet new reactor` are **packaged** (single-project MSIX), so
+`dotnet run` launches them with package identity and needs Developer Mode on.
+The other short names are `reactor-mvu`, `reactor-navview`, and
+`reactor-tabview`.
+
**Verify your edits with `mur check`** before declaring done. From the
project directory: `mur check` (no arguments) runs `dotnet build` and
emits one compressed line per diagnostic with a `→ try:` suggestion
@@ -258,8 +264,11 @@ next to it pointing at the clone's `local-nupkgs/` (absolute path):
Inside the clone you don't need this — the repo-level `nuget.config`
already configures the feed.
-`WindowsPackageType` MUST be `None` (unpackaged, no App.xaml). `UseWinUI`
-MUST be `true`. No XAML files of any kind.
+For a **hand-authored** csproj like the one above, `WindowsPackageType` MUST be
+`None` (unpackaged, no App.xaml) and `UseWinUI` MUST be `true`. Apps scaffolded
+by `dotnet new reactor` are packaged instead and omit `WindowsPackageType`
+entirely — leave their packaging properties alone. Either way: no XAML files of
+any kind.
### Required imports
diff --git a/bootstrap.ps1 b/bootstrap.ps1
index 9959ca913..012708f17 100644
--- a/bootstrap.ps1
+++ b/bootstrap.ps1
@@ -5,9 +5,10 @@
.DESCRIPTION
Builds the `mur` CLI, installs it as a dotnet global tool, packs the
- framework + ProjectTemplates into local-nupkgs/, installs the
- `dotnet new reactorapp` template, and (optionally) drops the Claude
- Code plugin under ~/.claude/plugins/reactor.
+ framework packages into local-nupkgs/, installs the Windows
+ App SDK `dotnet new` template pack (which ships `dotnet new reactor`),
+ and (optionally) drops the Claude Code plugin under
+ ~/.claude/plugins/reactor.
Idempotent — safe to re-run after `git pull` to refresh everything.
For a less heavyweight refresh (mur stays put), run `mur upgrade`.
@@ -62,6 +63,18 @@
NuGet.Config if it is reachable; otherwise the repo's public config remains
in effect.
+.PARAMETER WinAppSdkTemplatesVersion
+ Pin the Windows App SDK template pack to an explicit version, passed through
+ to `winapp new --template-version`. By default bootstrap installs the pack
+ only when it is missing and otherwise keeps what is already installed, so
+ this is only needed to hold the templates at a known-good version or to force
+ an update (`latest`).
+
+.PARAMETER SkipTemplates
+ Skip installing the Windows App SDK `dotnet new` template pack. The rest of
+ the bootstrap (mur, framework nupkgs, plugin, VS extension) still completes;
+ you just won't get `dotnet new reactor` from this run.
+
.PARAMETER Verbose
Common parameter (enabled by [CmdletBinding]). Surfaces extra
diagnostic output at every decision point: detected SDK list,
@@ -87,6 +100,11 @@
./bootstrap.ps1 -Verbose
Print extra `VERBOSE:` diagnostics at every decision point. Useful
for debugging install failures or unexpected branch behavior.
+
+.EXAMPLE
+ ./bootstrap.ps1 -WinAppSdkTemplatesVersion 0.0.7-alpha
+ Hold the `dotnet new reactor` template pack at a known-good version
+ instead of taking whatever `winapp new` installs by default.
#>
[CmdletBinding()]
param(
@@ -98,7 +116,9 @@ param(
[switch]$NoWinAppSdk,
[switch]$SkipWinAppCli,
[string]$NpmRegistry,
- [string]$NuGetConfig
+ [string]$NuGetConfig,
+ [string]$WinAppSdkTemplatesVersion,
+ [switch]$SkipTemplates
)
if ($InstallWinAppSdk -and $NoWinAppSdk) {
@@ -274,8 +294,7 @@ function Get-VsExtensionSkipReason {
# The repo defaults WindowsAppSDKSelfContained=false (see
# Directory.Build.props) so samples and perf benches share a single
# machine-wide Microsoft.WindowsAppRuntime install rather than bundling a
-# copy of the runtime into every build output. The scaffolded template
-# (tools/Templates/templates/WinUIApp-CSharp) and the AOT-publish trim
+# copy of the runtime into every build output. The AOT-publish trim
# proofs (tests/aot_trim_proof/*) keep =true explicitly so their build
# output stays a standalone deployable.
#
@@ -419,11 +438,27 @@ if (-not $winAppRuntimeId) {
# works out of the box — a missing winget only warns, it never fails bootstrap.
Write-Step 'Checking winapp CLI (E2E UI test driver)'
-function Test-WinAppCli {
- if (Get-Command winapp -ErrorAction SilentlyContinue) { return $true }
+function Get-WinAppCliPath {
+ <#
+ .SYNOPSIS
+ Path to the winapp CLI, or $null when it isn't installed.
+
+ .DESCRIPTION
+ Prefers PATH, then the app-execution alias winget's MSIX install drops.
+ Callers must invoke *this* result rather than a bare `winapp`: the alias
+ exists on disk without necessarily being resolvable through the current
+ process's PATH, which is the whole reason the fallback exists.
+ #>
+ $onPath = Get-Command winapp -ErrorAction SilentlyContinue
+ if ($onPath) { return $onPath.Source }
$alias = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\winapp.exe'
Write-Dbg "winapp not on PATH; probing alias $alias"
- return (Test-Path $alias)
+ if (Test-Path $alias) { return $alias }
+ return $null
+}
+
+function Test-WinAppCli {
+ return $null -ne (Get-WinAppCliPath)
}
if ($SkipWinAppCli) {
@@ -594,19 +629,16 @@ if ($SkipMurInstall) {
}
# ---------------------------------------------------------------------------
-# 4. Pack the in-source framework + templates via the freshly-installed mur
+# 4. Pack the in-source framework packages via the freshly-installed mur
# ---------------------------------------------------------------------------
-Write-Step 'Packing local Microsoft.UI.Reactor + ProjectTemplates (`mur pack-local`)'
+Write-Step 'Packing local Microsoft.UI.Reactor packages (`mur pack-local`)'
# Use the freshly-installed `mur` if available; otherwise call the source
# project directly (works for -SkipMurInstall too).
#
-# `--framework-version latest` stamps the newest *published* Microsoft.UI.Reactor
-# into the scaffolded `reactorapp` template's , so `dotnet new
-# reactorapp` in this clone tracks the current release automatically instead of a
-# hand-maintained default. Best-effort: if NuGet is unreachable it falls back to
-# the template's built-in default. Pass `--MSUIReactorVersion 0.0.0-local` to a
-# scaffold to consume this local source build instead.
+# Produces the source-built 0.0.0-local framework, Advanced and Devtools nupkgs
+# so recipes, samples and scaffolded apps in this clone can resolve the working
+# tree instead of the published package.
$packLocalExit = 0
Invoke-ReactorWithRestoreEnvironment `
-NuGetConfig $effectiveNuGetConfig `
@@ -616,7 +648,7 @@ Invoke-ReactorWithRestoreEnvironment `
$murResolved = Get-Command mur -ErrorAction SilentlyContinue
if ($murResolved) {
Write-Dbg "Using installed mur at $($murResolved.Source)"
- & mur pack-local --framework-version latest
+ & mur pack-local
} else {
Write-Dbg "mur not on PATH; falling back to 'dotnet run' against Reactor.Cli source"
$murRestoreArgs = Get-ReactorRestoreArguments `
@@ -629,57 +661,145 @@ Invoke-ReactorWithRestoreEnvironment `
"-p:Platform=$hostArch" `
--nologo `
@murRestoreArgs `
- -- pack-local --framework-version latest
+ -- pack-local
}
}
if ($packLocalExit -ne 0) { Fail 'mur pack-local failed' }
# ---------------------------------------------------------------------------
-# 5. Install the `dotnet new reactorapp` template
+# 5. Install the Windows App SDK `dotnet new` templates (ships `reactor`)
# ---------------------------------------------------------------------------
-Write-Step 'Installing `dotnet new reactorapp` template'
+# Reactor's app templates ship inside the Windows App SDK template pack
+# (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`), next to the WinUI 3 XAML
+# ones. That pack owns the `reactor`, `reactor-mvu`, `reactor-navview`, and
+# `reactor-tabview` short names, and the apps it scaffolds are **packaged**
+# (single-project MSIX) — so `dotnet run` launches them with package identity,
+# equivalent to F5 in Visual Studio.
+#
+# This repo used to ship its own `Microsoft.UI.Reactor.ProjectTemplates` pack
+# (`dotnet new reactorapp`, unpackaged). It was removed once the Windows App SDK
+# pack shipped the Reactor templates; the published versions are deprecated on
+# NuGet.org with a pointer here. For an unpackaged app, scaffold with
+# `dotnet new reactor` and set `None`
+# (see docs/guide/packaging.md).
+#
+# Hoisted so the closing "Next:" guidance can gate on it: printing
+# `dotnet new reactor` after verification failed would hand the user the exact
+# command bootstrap just proved unavailable.
+$templatesVerified = $false
+if ($SkipTemplates) {
+ Write-Host ''
+ Write-Host ' Skipping `dotnet new` template install (per -SkipTemplates).' -ForegroundColor Yellow
+} else {
+ Write-Step 'Installing Windows App SDK `dotnet new` templates (`dotnet new reactor`)'
-$templateNupkg = Join-Path $feed 'Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg'
-if (-not (Test-Path $templateNupkg)) {
- Fail "Template nupkg not produced at $templateNupkg"
-}
+ $wasdkTemplatePackageId = 'Microsoft.WindowsAppSDK.WinUI.CSharp.Templates'
-# Uninstall first so the template engine drops its cached copy by id —
-# otherwise the previous install can win against a same-version repack.
-# `dotnet new uninstall` (no args) lists installed template packages; skip the
-# uninstall on first run when our package isn't there yet (else the non-zero
-# exit code becomes a terminating error under $ErrorActionPreference = 'Stop'
-# in PS 7.4+, which `2>$null` doesn't intercept).
-Write-Dbg "Probing installed templates via 'dotnet new uninstall' (no args)"
-if ($script:VerboseOn) {
- # Capture the full listing so we can both echo each line as a debug
- # breadcrumb AND substring-match for our template id below.
- $installedTemplates = & dotnet new uninstall 2>&1 | Out-String
- foreach ($line in ($installedTemplates -split "`r?`n")) {
- if ($line.Trim()) { Write-Dbg " templates> $line" }
+ # Delegate to the Windows App SDK CLI, which owns this pack.
+ #
+ # `winapp new --list` installs the pack on demand and prints the templates;
+ # with --use-defaults it never prompts and keeps an already-installed pack,
+ # which is exactly bootstrap's "self-healing install-if-missing, not a
+ # reinstall" semantics. Reactor used to carry its own installer for this
+ # because `dotnet new install` has no --prerelease switch and resolves
+ # stable-only (so it fails outright while the pack is prerelease-only), and
+ # working around that meant version-resolving off the NuGet flat container
+ # and tiptoeing around `dotnet new install --force`, which uninstalls before
+ # it downloads. winapp handles all of it.
+ $winAppNewArgs = @('new', '--list', '--use-defaults')
+ if ($WinAppSdkTemplatesVersion) {
+ Write-Dbg "Template version pin: $WinAppSdkTemplatesVersion"
+ $winAppNewArgs += @('--template-version', $WinAppSdkTemplatesVersion)
}
- $hasReactorTemplate = $installedTemplates -match 'Microsoft\.UI\.Reactor\.ProjectTemplates'
-} else {
- # Quiet path: stream through Select-String -Quiet so we never materialize
- # the multi-KB listing for what is, semantically, a single boolean test.
- $hasReactorTemplate = [bool](& dotnet new uninstall 2>&1 |
- Select-String -SimpleMatch 'Microsoft.UI.Reactor.ProjectTemplates' -Quiet)
-}
-if ($hasReactorTemplate) {
- Write-Dbg "Existing Microsoft.UI.Reactor.ProjectTemplates detected; uninstalling stale copy"
- if ($script:VerboseOn) {
- & dotnet new uninstall Microsoft.UI.Reactor.ProjectTemplates
+
+ $templatesExit = 0
+ $winAppExe = Get-WinAppCliPath
+ if (-not $winAppExe) {
+ # winapp is installed in step 4; it can legitimately be absent when that
+ # step was skipped or winget is unavailable. Don't fail the whole
+ # bootstrap over it — say what to run and let the verification below
+ # report the real state.
+ Write-Host ' [warn] winapp CLI not available, so the template pack cannot be installed here.' -ForegroundColor Yellow
+ Write-Host ' Install it and re-run, or install the pack directly:'
+ Write-Host ' winget install Microsoft.WinAppCli'
+ Write-Host ' winapp new --list'
+ $templatesExit = 2
} else {
- & dotnet new uninstall Microsoft.UI.Reactor.ProjectTemplates | Out-Null
+ # Invoke the resolved path, not a bare `winapp`: Get-WinAppCliPath also
+ # accepts the app-execution alias, which exists on disk without always
+ # being resolvable through this process's PATH.
+ Write-Dbg "$winAppExe $($winAppNewArgs -join ' ')"
+ & $winAppExe @winAppNewArgs | Out-Null
+ $templatesExit = $LASTEXITCODE
+ }
+ # Exit 2 is bootstrap's own "winapp unavailable" marker above. Treat it the
+ # same way as an installed-but-unusable pack: not an install failure, because
+ # the verification below reports the real state and the closing guidance
+ # adapts. Collapsing it into this Fail would make both of those unreachable.
+ if ($templatesExit -ne 0 -and $templatesExit -ne 2) {
+ Fail (@(
+ "Installing $wasdkTemplatePackageId failed.",
+ " The Reactor templates ship in the Windows App SDK template pack,",
+ " installed here via the Windows App SDK CLI (``winapp new --list``).",
+ " - To pin a specific pack version:",
+ " ./bootstrap.ps1 -WinAppSdkTemplatesVersion ",
+ " - To skip this step entirely: ./bootstrap.ps1 -SkipTemplates"
+ ) -join [Environment]::NewLine)
+ }
+ # `winapp new --list --use-defaults` deliberately keeps an already-installed
+ # pack rather than updating it, and that pack can predate the Reactor
+ # templates (0.0.6-alpha shipped without them), so confirm the short name
+ # actually resolves before claiming success.
+ #
+ # Reuse the CLI probe rather than grepping the listing here: `mur templates
+ # status` matches the short name as a whole token, which a naive regex does
+ # not — `\breactor\b` also matches `reactor-mvu` and `winui-reactor`, so a
+ # listing without the blank template would read as success.
+ $statusExit = 0
+ Invoke-ReactorWithRestoreEnvironment `
+ -NuGetConfig $effectiveNuGetConfig `
+ -NuGetSource $effectiveNuGetSource `
+ -ExitCode ([ref]$statusExit) `
+ -Action {
+ $murResolved = Get-Command mur -ErrorAction SilentlyContinue
+ if ($murResolved) { & mur templates status | Out-Null }
+ else {
+ & dotnet run `
+ --project (Join-Path $repoRoot 'src\Reactor.Cli\Reactor.Cli.csproj') `
+ -c $Configuration "-p:Platform=$hostArch" --nologo `
+ -- templates status | Out-Null
+ }
+ }
+ $templatesVerified = ($statusExit -eq 0)
+
+ if ($templatesVerified) {
+ Write-Ok '`dotnet new reactor` templates registered'
+ } else {
+ # `mur templates status` reports three distinct not-usable situations
+ # (see TemplatesCommand.StatusExit). They need different advice: telling
+ # someone their pack "predates the Reactor templates" when it is not
+ # installed at all — or when the probe itself failed — sends them to
+ # re-pin a version that was never the problem.
+ Write-Host ''
+ switch ($statusExit) {
+ 2 {
+ Write-Host " [warn] $wasdkTemplatePackageId is installed but does not provide ``dotnet new reactor``." -ForegroundColor Yellow
+ Write-Host " That version predates the Reactor templates. Update the pack:"
+ Write-Host " winapp new --list --template-version latest"
+ }
+ 3 {
+ Write-Host " [warn] $wasdkTemplatePackageId is not installed, so ``dotnet new reactor`` is unavailable." -ForegroundColor Yellow
+ Write-Host " Re-run with network access, or install the pack directly:"
+ Write-Host " winapp new --list"
+ }
+ default {
+ Write-Host " [warn] Could not enumerate ``dotnet new`` templates, so ``dotnet new reactor`` is unverified." -ForegroundColor Yellow
+ Write-Host " This is a probe failure, not a known-bad install. Check with:"
+ Write-Host " mur templates status"
+ }
+ }
}
- if ($LASTEXITCODE -ne 0) { Fail '`dotnet new uninstall` failed' }
-} else {
- Write-Dbg "No prior install of Microsoft.UI.Reactor.ProjectTemplates; skipping uninstall (first-run path)"
}
-Write-Dbg "dotnet new install $templateNupkg"
-& dotnet new install $templateNupkg
-if ($LASTEXITCODE -ne 0) { Fail '`dotnet new install` failed' }
-Write-Ok 'reactorapp template registered'
# ---------------------------------------------------------------------------
# 6. Claude Code plugin (optional)
@@ -825,9 +945,28 @@ Write-Host ''
Write-Host 'Bootstrap complete.' -ForegroundColor Green
Write-Host ''
Write-Host 'Next:'
-Write-Host ' dotnet new reactorapp -n MyApp'
-Write-Host ' cd MyApp'
-Write-Host ' dotnet run'
+if ($SkipTemplates) {
+ # Advertising `dotnet new reactor` here would be a false promise: this run
+ # deliberately did not install the pack, so the command may not resolve.
+ Write-Host ' Template install was skipped (-SkipTemplates).'
+ Write-Host ' To scaffold an app, let the Windows App SDK CLI install the pack:'
+ Write-Host ' winapp new -t reactor -n MyApp'
+} elseif (-not $templatesVerified) {
+ # Same false promise, for the harder case: the pack is installed but step 5
+ # proved `dotnet new reactor` does not resolve from it (an older pack such as
+ # 0.0.6-alpha predates the Reactor templates).
+ Write-Host ' `dotnet new reactor` is not available — see the warning above.'
+ Write-Host ' Install a version that provides it, then scaffold:'
+ Write-Host ' winapp new --list --template-version latest'
+ Write-Host ' dotnet new reactor -n MyApp'
+} else {
+ Write-Host ' dotnet new reactor -n MyApp'
+ Write-Host ' cd MyApp'
+ Write-Host ' dotnet run'
+ Write-Host ''
+ Write-Host 'Other Reactor templates: reactor-mvu, reactor-navview, reactor-tabview'
+ Write-Host ' dotnet new list reactor'
+}
Write-Host ''
Write-Host 'Other useful commands:'
Write-Host ' mur doctor verify your install'
diff --git a/build/pipelines/templates/reactor-build-steps.yml b/build/pipelines/templates/reactor-build-steps.yml
index 65a6fd890..6ec992fc9 100644
--- a/build/pipelines/templates/reactor-build-steps.yml
+++ b/build/pipelines/templates/reactor-build-steps.yml
@@ -357,14 +357,6 @@ steps:
-o "${{ parameters.outputDirectory }}\nupkg"
displayName: 'Pack Devtools NuGet'
- - pwsh: |
- dotnet pack tools/Templates/Microsoft.UI.Reactor.Templates.csproj `
- --no-build --configuration ${{ parameters.configuration }} `
- -p:Version=$(PackageVersion) `
- -p:Platform=AnyCPU `
- -o "${{ parameters.outputDirectory }}\nupkg"
- displayName: 'Pack Templates'
-
# ── NuGet.org Microsoft-compliance pre-flight (fail BEFORE the gated publish). ──
# nuget.org enforces https://aka.ms/Microsoft-NuGet-Compliance on every
# Microsoft-signed package: a missing ProjectUrl (or other required field) is
@@ -379,7 +371,7 @@ steps:
# never drift from what nuget.org actually enforces — the tool embeds the same
# default rule set the server uses (required ProjectUrl + license, authors must
# be "Microsoft", allowed copyright notices, etc.) and emits the identical
- # "missing required ProjectUrl" message we saw reject ProjectTemplates at push
+ # "missing required ProjectUrl" message that has rejected a package at push
# time. It is a net472 console exe shipped INSIDE the package's tools/ folder
# (NOT a dotnet tool — `dotnet tool install` rejects it), so we restore the
# package from the INTERNAL feed (it has been seeded there; the sealed build
diff --git a/docs/_pipeline/templates/dev-tooling.md.dt b/docs/_pipeline/templates/dev-tooling.md.dt
index 6e81f6799..d668fcd5b 100644
--- a/docs/_pipeline/templates/dev-tooling.md.dt
+++ b/docs/_pipeline/templates/dev-tooling.md.dt
@@ -160,10 +160,17 @@ subcommands map one-to-one to the workflows below.
| `mur loc` | Run the localization pipeline (`extract`, `translate`, `validate`, `status`, `prune`) | `mur loc extract` |
| `mur devtools` | Launch the project with `--devtools run`, supervise reloads, and host the MCP endpoint | `mur devtools` |
| `mur check` | Repo-health checks (cref validity, namespace policy, "did you mean" suggestions) | `mur check` |
-| `mur doctor` | Verify the install — SDK, `mur`, local feed, template, plugin | `mur doctor` |
-| `mur upgrade` | Re-pack framework + templates and refresh the plugin after a `git pull` | `mur upgrade` |
+| `mur doctor` | Verify the install — SDK, `mur`, local feed, templates, plugin | `mur doctor` |
+| `mur upgrade` | Re-pack the framework and refresh the plugin after a `git pull`; checks template availability but does not install the pack (that's `winapp new`) | `mur upgrade` |
| `mur figma watch` | Poll a Figma file for design changes | `mur figma watch` |
-| `mur pack-local` / `mur clean-local` | Package / clean the local NuGet feed for source-built framework smoke tests; the app template defaults to the public Reactor preview unless `--MSUIReactorVersion` is supplied | `mur pack-local` |
+| `mur pack-local` / `mur clean-local` | Package / clean the local NuGet feed for source-built framework smoke tests; scaffolded apps default to the public Reactor preview unless `--reactor-version` is supplied | `mur pack-local` |
+| `mur templates status` | Report whether `dotnet new reactor` resolves (exit `0` available, `1` probe failed, `2` pack too old, `3` pack missing) | `mur templates status` |
+
+To *install* the template pack, use the Windows App SDK CLI rather than `mur`:
+`winapp new -t reactor -n MyApp` installs it on demand and scaffolds in one
+step, and `winapp new --list` installs it without scaffolding. `bootstrap.ps1`
+drives that same command, so a bootstrapped checkout can go straight to
+`dotnet new reactor`.
Beyond the subcommands there are four top-level options worth knowing:
`mur --create ` scaffolds a new Reactor project, `mur --skill`
diff --git a/docs/_pipeline/templates/getting-started.md.dt b/docs/_pipeline/templates/getting-started.md.dt
index d1d9f33c8..41b354b8d 100644
--- a/docs/_pipeline/templates/getting-started.md.dt
+++ b/docs/_pipeline/templates/getting-started.md.dt
@@ -31,54 +31,92 @@ the rest of the docset elaborates.
> **Public preview package available.** Reactor ships `Microsoft.UI.Reactor`
-> `{{reactorVersion}}` on NuGet.org. The project template package is still
-> installed from source for now; `bootstrap.ps1` installs `mur`, packs/registers
-> the local `reactorapp` template, and stamps generated apps to reference the
-> public preview package by default. Broader signed distribution is tracked in
+> `{{reactorVersion}}` on NuGet.org, and the project templates ship in the
+> official Windows App SDK `dotnet new` pack
+> (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`) — so `dotnet new reactor`
+> works from a plain .NET SDK install, no source checkout required.
+> `bootstrap.ps1` is for *contributors*: it installs `mur`, packs source-built
+> framework snapshots, and registers those same templates. Broader signed
+> distribution is tracked in
> [spec 022](https://github.com/microsoft/microsoft-ui-reactor/blob/main/docs/specs/022-packaging-and-distribution.md).
Reactor is a declarative UI framework for building native Windows apps in pure C#.
No XAML, no data binding, no view models. You describe your UI as a function of
state and Reactor keeps the screen in sync.
-## Setup (one-time)
+## Setup
+
+If you just want to build an app, scaffold one and go — you do not need to
+clone this repo:
```powershell
-git clone https://github.com/microsoft/microsoft-ui-reactor.git
-cd microsoft-ui-reactor
-./bootstrap.ps1
+winget install Microsoft.WinAppCli
+winapp new -t reactor -n MyApp
+cd MyApp
+dotnet run
```
-That's it. `bootstrap.ps1` packs and installs `mur` as a `dotnet tool` global
-install (so it's on PATH cross-shell with no manual `$env:Path` edits), runs
-`mur pack-local` to produce local source-built framework snapshots and the
-matching `ProjectTemplates` nupkg, registers the `dotnet new reactorapp`
-template, and drops the Reactor agent plugin under `~/.claude/plugins/reactor`
-(symlink when allowed, copy otherwise). Apps created from that template reference
-`Microsoft.UI.Reactor` version `{{reactorVersion}}` from NuGet.org by default.
+`winapp` is the Windows App SDK CLI. `winapp new` installs the template pack on
+demand (and offers to update a stale one), so there is no separate install step
+and no version to pin.
+
+That gives you four starting points — `reactor` (blank), `reactor-mvu`
+(Model-View-Update via `UseReducer`), `reactor-navview` (`NavigationView` shell)
+and `reactor-tabview` (`TabView` shell). `winapp new --list` prints them all, and
+omitting `-t` picks one interactively.
-When it finishes you can immediately run:
+Prefer plain .NET tooling? Install the pack yourself, then scaffold with
+`dotnet new`:
```powershell
-dotnet new reactorapp -n MyApp
-cd MyApp
-dotnet run
+dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::
+dotnet new reactor -n MyApp
+```
+
+The pack is prerelease-only today, and `dotnet new install` resolves stable
+versions unless you pin one explicitly — which is the step `winapp new` removes.
+
+Scaffolded apps are **packaged** (single-project MSIX), so `dotnet run`
+registers a loose-layout package and launches the app with full package
+identity — the same thing F5 does in Visual Studio. That requires **Developer
+Mode** (Settings → System → For developers). See [Packaging](packaging.md) for
+the unpackaged alternative.
+
+## Contributor setup (one-time)
+
+Working *on* Reactor rather than with it? Clone and bootstrap:
+
+```powershell
+git clone https://github.com/microsoft/microsoft-ui-reactor.git
+cd microsoft-ui-reactor
+./bootstrap.ps1
```
+`bootstrap.ps1` packs and installs `mur` as a `dotnet tool` global
+install (so it's on PATH cross-shell with no manual `$env:Path` edits), runs
+`mur pack-local` to produce local source-built framework snapshots, installs the
+Windows App SDK template pack,
+and drops the Reactor agent plugin under `~/.claude/plugins/reactor`
+(symlink when allowed, copy otherwise).
+
### After `git pull`
-The source checkout changes — your local template package, CLI, plugin, and
-optional source-built framework snapshots do not, unless you repack them. Two
-options:
+The source checkout changes — your local framework snapshots, CLI and plugin
+do not, unless you repack them. Two options:
```powershell
-mur upgrade # repacks the framework + templates and refreshes plugin
-./bootstrap.ps1 # same, plus updates the `mur` global tool itself
+mur upgrade # repacks the framework and refreshes the plugin
+./bootstrap.ps1 # same, plus the `mur` global tool and the template pack
```
`mur upgrade` is the lightweight path. Re-run `bootstrap.ps1` when you want to
pick up CLI changes (a `mur` process can't replace its own binary mid-run).
+The `dotnet new reactor` templates are not repacked by either: they ship in the
+Windows App SDK pack, so `git pull` never invalidates them. `mur upgrade` only
+*checks* that they still resolve; `bootstrap.ps1` installs the pack when it is
+missing, and `winapp new --list --template-version latest` updates it.
+
### Verify the install
```powershell
@@ -86,17 +124,16 @@ mur doctor
```
Lists every dependency the rest of this guide assumes — .NET 10+ SDK, `mur` on
-PATH, current `local-nupkgs/` developer feed, the `reactorapp` template
+PATH, current `local-nupkgs/` developer feed, the `dotnet new reactor` template
registration, and the optional Claude plugin. Each line is PASS / WARN / FAIL
with a one-line remediation for anything broken.
> **What this gets you.** A globally-resolvable `mur` (via `~/.dotnet/tools`),
-> a locally installed `reactorapp` template that references
-> ` Version="{{reactorVersion}}" />`, a local NuGet feed at `/local-nupkgs/`
+> the `dotnet new reactor` templates, a local NuGet feed at
+> `/local-nupkgs/`
> for source-built smoke tests, and an agent plugin so AI assistants generate
> against the real factories (`mur --skill` / `mur --api` print the same
-> content). Run `mur upgrade` whenever you pull new template, CLI, plugin, or
+> content). Run `mur upgrade` whenever you pull new CLI, plugin, or
> framework changes.
> **Only need the framework package?** Reference the published
@@ -119,8 +156,8 @@ anything goes wrong.
| 2 | `git clone` + `cd` | Local source checkout |
| 3 | `dotnet pack src/Reactor.Cli` | `Microsoft.UI.Reactor.Cli..nupkg` in `local-nupkgs/` |
| 4 | `dotnet tool install -g` | `mur` resolvable cross-shell from `~/.dotnet/tools` |
-| 5 | `mur pack-local` | Source-built framework snapshots plus a local `ProjectTemplates` nupkg; generated apps default to the public Reactor preview |
-| 6 | `dotnet new uninstall` + `install` | `dotnet new reactorapp` template registered |
+| 5 | `mur pack-local` | Source-built framework snapshots in `local-nupkgs/` |
+| 6 | `dotnet new install` | `dotnet new reactor` templates registered |
| 7 | Symlink/copy `plugins/reactor` | Reactor agent kit under `~/.claude/plugins/reactor` (optional) |
| 8 | `mur doctor` | Verification that 1–7 all stuck |
@@ -180,18 +217,16 @@ $env:Path = "$env:USERPROFILE\.dotnet\tools;$env:Path"
New PowerShell windows pick up the user-PATH change on their own.
-**5. Pack local framework snapshots and project templates.** This produces the
-source-built `0.0.0-local` framework nupkgs for smoke tests plus the local
-`ProjectTemplates` nupkg that installs `dotnet new reactorapp`. The template's
-normal default references the public `Microsoft.UI.Reactor` `{{reactorVersion}}`
-package.
+**5. Pack local framework snapshots.** This produces the source-built
+`0.0.0-local` framework nupkgs so recipes and smoke tests in this clone resolve
+your working tree instead of the published package.
```powershell
mur pack-local
# Produces:
# local-nupkgs/Microsoft.UI.Reactor.0.0.0-local.nupkg
# local-nupkgs/Microsoft.UI.Reactor.Advanced.0.0.0-local.nupkg
-# local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg
+# local-nupkgs/Microsoft.UI.Reactor.Devtools.0.0.0-local.nupkg
```
If you'd rather not depend on the freshly-installed `mur`, you can invoke
@@ -202,15 +237,17 @@ dotnet run --project src/Reactor.Cli/Reactor.Cli.csproj `
-c Release "-p:Platform=$hostArch" -- pack-local
```
-**6. Install the `dotnet new reactorapp` template.** The template engine
-caches by package id, so a same-version repack can lose to the cached copy.
-Always uninstall first.
+**6. Install the `dotnet new reactor` templates.** These come from the Windows
+App SDK template pack on NuGet.org, not from this checkout:
```powershell
-dotnet new uninstall Microsoft.UI.Reactor.ProjectTemplates 2>$null
-dotnet new install local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg
+dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::
```
+The pack is prerelease-only today, so pin a version explicitly — a bare
+`dotnet new install` resolves stable versions only. `winapp new -t reactor`
+collapses both steps: it installs the newest pack on demand, then scaffolds.
+
**7. (Optional) Install the Reactor agent plugin.** If you use Claude Code
or another agent and want it to author Reactor code with the right
factories, drop the in-repo plugin folder into your agent's plugin path. A
@@ -257,43 +294,41 @@ the install must happen from a shell that isn't already running `mur`).
> upgrade verb.
-The core framework package is public, but the `reactorapp` project-template
-package is still source-installed. If `dotnet new reactorapp` is missing, run
-`bootstrap.ps1` (or `mur upgrade` from an already bootstrapped checkout) to
-repack and reinstall `Microsoft.UI.Reactor.ProjectTemplates` from
-`local-nupkgs/`. The template installer caches by package id, so a same-version
-repack can lose to the cached copy — `mur upgrade` handles this by running
-`dotnet new uninstall` first.
+The Reactor templates ship in the Windows App SDK `dotnet new` pack
+(`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`), so `dotnet new reactor`
+needs no source checkout. If it's missing, `winapp new -t reactor` installs the
+pack on demand and scaffolds in one step. To install it without scaffolding, run
+`dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::` —
+the pack is prerelease-only today, so the explicit version pin is required.
## Creating a Project
-With the template installed, scaffold a new app from anywhere on disk:
+With the templates installed, scaffold a new app from anywhere on disk:
```powershell
-dotnet new reactorapp -n MyApp
+dotnet new reactor -n MyApp
cd MyApp
dotnet run
```
The template wires up the `Microsoft.UI.Reactor` package reference, the
-WinUI 3 target framework, and a working `App.cs` that mounts a single
-Reactor component. No `App.xaml`, no `MainWindow.xaml.cs` — just one C#
-file.
+WinUI 3 target framework, MSIX packaging, and a working `App.cs` that mounts a
+single Reactor component. No `App.xaml`, no `MainWindow.xaml.cs` — just one C#
+file. Swap `reactor` for `reactor-mvu`, `reactor-navview` or `reactor-tabview`
+to start from a richer shell.
-By default that package reference is
-``.
For local framework smoke tests, generate with
-`dotnet new reactorapp -n MyLocalApp --MSUIReactorVersion 0.0.0-local` and run
+`dotnet new reactor -n MyLocalApp --reactor-version 0.0.0-local` and run
from inside the source checkout or another folder that has the local feed
configured.
> **Why a custom template?** A `dotnet new console` does not produce a WinUI
> app — it builds a console target with no UI thread, no `OutputType=WinExe`,
-> no WindowsAppSDK reference, and no `[STAThread]` entry point. `reactorapp`
-> sets all of those plus the Reactor package reference and a backdrop-aware
-> root component, so you get a window on first `dotnet run` instead of a
-> console-host stub.
+> no WindowsAppSDK reference, and no `[STAThread]` entry point. `reactor`
+> sets all of those plus the Reactor package reference, MSIX packaging and a
+> backdrop-aware root component, so you get a window on first `dotnet run`
+> instead of a console-host stub.
## Your First App
@@ -446,7 +481,7 @@ side effects, `UseRef` for non-rendering bookkeeping).
The dev menu needs **two** independent signals, and neither one is
`#if DEBUG`. First the build-time capability — the `Reactor.DevtoolsSupport`
-feature switch, which `dotnet new reactorapp` already sets in Debug
+feature switch, which `dotnet new reactor` already sets in Debug
configurations along with the `Microsoft.UI.Reactor.Devtools` package. Second a
session opt-in on the command line:
diff --git a/docs/_pipeline/templates/index.md.dt b/docs/_pipeline/templates/index.md.dt
index 711ea1243..daec554e3 100644
--- a/docs/_pipeline/templates/index.md.dt
+++ b/docs/_pipeline/templates/index.md.dt
@@ -187,7 +187,7 @@ uniform Summary / Parameters / Returns / Discussion / Examples / See Also page.
## Minimal Project Setup
-The fastest path is [`dotnet new reactorapp`](getting-started.md), which
+The fastest path is [`dotnet new reactor`](getting-started.md), which
scaffolds all of this for you. To wire it up by hand instead, create a console
project and edit the `.csproj`:
diff --git a/docs/_pipeline/templates/packaging.md.dt b/docs/_pipeline/templates/packaging.md.dt
index 5a5f9d889..5fac5b182 100644
--- a/docs/_pipeline/templates/packaging.md.dt
+++ b/docs/_pipeline/templates/packaging.md.dt
@@ -4,13 +4,13 @@ app: packaging
order: 2.8
audience: intermediate
goal: |
- How to ship a Reactor app: the unpackaged WindowsPackageType=None
- shape that the project template produces, MSIX for Store /
- sideloading, single-file publish, ARM64 as a second runtime
- identifier, and what does and doesn't work under Native AOT given
- Reactor's reflection-heavy DataGrid / devtools paths. Solid tier —
- CSPROJ-driven snippets pulled from real samples + the
- `dotnet new reactorapp` template.
+ How to ship a Reactor app: the packaged single-project MSIX shape
+ that `dotnet new reactor` produces by default, the unpackaged
+ WindowsPackageType=None alternative, single-file publish, ARM64 as a
+ second runtime identifier, and what does and doesn't work under
+ Native AOT given Reactor's reflection-heavy DataGrid / devtools
+ paths. Solid tier — CSPROJ-driven snippets pulled from real samples
+ + the `dotnet new reactor` templates.
tier: solid
---
@@ -19,9 +19,9 @@ tier: solid
A Microsoft.UI.Reactor (Reactor) app is a normal WinUI 3 / Windows App SDK executable —
`dotnet publish` produces the deployable artifact and the framework
itself adds nothing exotic to the project file. What you choose at
-publish time is the **shape** of that artifact: an unpackaged folder
-(the [`dotnet new reactorapp`](getting-started.md) default), a signed
-MSIX, a single-file bundle, or a Native AOT native binary — each
+publish time is the **shape** of that artifact: a packaged MSIX
+(the [`dotnet new reactor`](getting-started.md) default), an unpackaged
+folder, a single-file bundle, or a Native AOT native binary — each
combined with a `win-x64` or `win-arm64` runtime identifier. The
trade-offs are the same ones any WinUI 3 app faces; the
Reactor-specific notes on this page cover what changes when your
@@ -32,11 +32,19 @@ INPC walker).
| Publish shape | Key properties | Runtime identifier | What you get |
|---|---|---|---|
-| Unpackaged (template default) | `WindowsPackageType=None`, `WindowsAppSDKSelfContained=true` | `win-x64` / `win-arm64` | A folder with `MyApp.exe` and the WinUI 3 runtime alongside it. Run from anywhere; ship as a zip. |
+| Unpackaged | `WindowsPackageType=None`, `WindowsAppSDKSelfContained=true` | `win-x64` / `win-arm64` | A folder with `MyApp.exe` and the WinUI 3 runtime alongside it. Run from anywhere; ship as a zip. |
| MSIX | `WindowsPackageType=MSIX`, `GenerateAppxPackageOnBuild=true`, signed via `PackageCertificateThumbprint` or `PackageCertificateKeyFile` | `win-x64` / `win-arm64` | A signed `.msix`. Required for Microsoft Store; the cleanest sideload story for enterprise. |
| Single-file | `PublishSingleFile=true`, `IncludeNativeLibrariesForSelfExtract=true` | `win-x64` / `win-arm64` (must be set) | One `.exe` that self-extracts the WinUI runtime to `%TEMP%/.net/` on first launch. |
| Native AOT | `PublishAot=true`, `InvariantGlobalization=true` (recommended) | `win-x64` / `win-arm64` (required) | A native binary with no JIT, no `Assembly.GetTypes()`, no `Reflection.Emit`. Fastest cold start; trim-only. |
+A scaffolded app starts out **packaged**: `dotnet new reactor` emits a
+`Package.appxmanifest` and sets `EnableMsixTooling`, so `dotnet run`
+registers a loose-layout package and the app has identity from the
+first launch. The properties in the MSIX row above are the *publish*
+knobs for producing and signing a redistributable `.msix` — a separate
+concern the scaffold deliberately leaves off, since a fresh clone
+should build without a developer certificate.
+
The four shapes are not mutually exclusive — MSIX wraps any of the
three publish outputs, and AOT layers on top of either an unpackaged
folder or an MSIX. The decision is usually distribution-channel-first
@@ -46,8 +54,8 @@ folder or an MSIX. The decision is usually distribution-channel-first
## The unpackaged shape
-`dotnet new reactorapp` scaffolds an unpackaged WinUI 3 project — the
-shape every sample in this repo also uses:
+Prefer a zip-and-go folder over an MSIX? Set `WindowsPackageType=None`
+on a scaffolded project — the shape every sample in this repo uses:
```xml snippet="source:samples/TodoApp/TodoApp.csproj#unpackaged-shape"
```
@@ -112,7 +120,16 @@ shape adds three properties on top of the unpackaged CSPROJ:
```
`Package.appxmanifest` declares the package identity (Publisher,
-PackageFamilyName, capabilities, file-type associations). The
+PackageFamilyName, capabilities, file-type associations). Here is the
+identity block from this repo's own packaged selftest host, which is a
+real single-project MSIX app:
+
+```xml snippet="source:tests/Reactor.PackagedTests.Host/Package.appxmanifest#msix-identity"
+```
+
+`Publisher` must match the subject of the signing certificate, and
+`Name` plus `Publisher` together derive the PackageFamilyName that
+Windows uses to identify the app. The
[WinUI 3 packaging docs](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/packaging/)
cover the manifest surface in full. The signing certificate is
either a Microsoft Store-issued cert (for Store submissions) or a
@@ -170,8 +187,8 @@ and any `System.Drawing.Common` / `TraceEvent` natives transitively
pulled in by Reactor) ship per-RID, which is why the runtime
identifier matters even for managed-only Reactor code. The repo's
sample apps default to `x64;ARM64`; the
-`reactorapp` template uses `x64;ARM64;X86`
-(X86 retained for parity with the WinUI 3 templates), but Reactor
+`dotnet new reactor` templates use `x86;x64;ARM64`
+(x86 retained for parity with the WinUI 3 templates), but Reactor
itself is only tested on x64 / ARM64.
## Native AOT
@@ -186,14 +203,21 @@ and a runtime identifier:
`dotnet publish -c Release -r win-x64` produces a native binary —
no `coreclr.dll`, no JIT, ~50 ms cold start versus ~250 ms for the
-JIT-based build on the same hardware. The project template gates
-the same shape behind a `NativeAot` parameter:
+JIT-based build on the same hardware.
-```xml snippet="source:tools/Templates/templates/WinUIApp-CSharp/Company.ReactorApp1.csproj#template-shape"
+The `dotnet new reactor` templates do **not** enable AOT — they ship
+`PublishReadyToRun` + `PublishTrimmed` for non-Debug configurations
+instead. To go all the way to AOT, add the properties to the scaffolded
+CSPROJ yourself:
+
+```xml
+
+ true
+ true
+
```
-Pass `dotnet new reactorapp --NativeAot true` to get the AOT-enabled
-variant. `InvariantGlobalization=true` is paired with `PublishAot`
+`InvariantGlobalization=true` is paired with `PublishAot`
because the alternative — shipping the full ICU data — pulls in
trim warnings that the AOT analyzer flags as actionable.
@@ -302,7 +326,7 @@ in Release too.
## Next Steps
- **[Dev Tooling](dev-tooling.md)** — Previous: the inner-loop side of the build pipeline (`mur pack-local`, `dotnet watch`, hot reload).
-- **[Getting Started](getting-started.md)** — Where the `dotnet new reactorapp` template that produces the unpackaged shape comes from.
+- **[Getting Started](getting-started.md)** — Where the `dotnet new reactor` templates that produce the packaged shape come from.
- **[Performance](performance.md)** — When you should reach for AOT (cold-start budgets, startup-perf benchmarks).
- **[Perf Instrumentation](perf-instrumentation.md)** — The ETW / EventPipe pipeline that survives AOT publish unchanged.
- **[Dev Tooling](dev-tooling.md)** — How the `Reactor.DevtoolsSupport` capability switch combines with `--devtools` activation.
diff --git a/docs/contributing/release-runbook.md b/docs/contributing/release-runbook.md
index e5ae1c4a6..bdaf28c8f 100644
--- a/docs/contributing/release-runbook.md
+++ b/docs/contributing/release-runbook.md
@@ -65,7 +65,6 @@ Also check NuGet.org for already-published packages:
- `Microsoft.UI.Reactor`
- `Microsoft.UI.Reactor.Advanced`
- `Microsoft.UI.Reactor.Devtools`
-- `Microsoft.UI.Reactor.ProjectTemplates`
## Prepare the release PR
@@ -90,19 +89,15 @@ per-file bump**:
- **Guide docs** — `docs/_pipeline/templates/*.md.dt` reference the version through the
`{{reactorVersion}}` token, which `mur docs compile` substitutes from this property.
-- **Template fallback** — `tools/Templates/Microsoft.UI.Reactor.Templates.csproj` derives its
- `MicrosoftUIReactorVersion` fallback default from `$(ReactorPublicVersion)`.
- **README** — is deliberately version-agnostic (it names no version and links to NuGet /
Releases), so it needs no edit at all and `mur docs compile` never touches it.
-The template's framework reference is *also* stamped automatically for the published package:
-the release workflow's *Pack Templates* step passes `-p:MicrosoftUIReactorVersion=` (guarded by `TemplateMetadataTests`), so the published `ProjectTemplates` package
-always references the framework version shipped in the same run. `bootstrap.ps1` runs `mur
-pack-local --framework-version latest`, which resolves the newest published package from NuGet
-for local scaffolds. The `$(ReactorPublicVersion)`-derived value is therefore only a *fallback*
-(a bare `mur pack-local`, or when the `latest` lookup can't reach NuGet) — but keeping it
-current is free now, since you bump the one property anyway.
+Project scaffolding is **not** released from this repo. The `dotnet new reactor` templates ship
+in the Windows App SDK template pack (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`), versioned
+and published by the WindowsAppSDK repo. When a release changes the framework version those
+templates should reference, that bump happens there — see
+[microsoft/WindowsAppSDK#6786](https://github.com/microsoft/WindowsAppSDK/pull/6786) for the shape
+of that change. The in-repo `Microsoft.UI.Reactor.ProjectTemplates` package was removed.
Two guards keep the bump honest so it can't silently go stale:
@@ -125,39 +120,31 @@ mur docs compile --skip-screenshots --skip-diagrams
## Validate the release PR
-Run the focused template tests:
+Run the focused version-consistency tests:
```powershell
dotnet test tests/Reactor.Tests/Reactor.Tests.csproj `
-p:Platform=x64 `
- --filter FullyQualifiedName~TemplateMetadataTests
+ --filter FullyQualifiedName~VersionSingleSourceTests
```
-Pack the template locally and inspect the generated default. Pass
-`-p:MicrosoftUIReactorVersion=$version` to mirror what the release workflow stamps, so the
-generated app references the version being released:
+Scaffolding is validated against the **published** Windows App SDK template pack rather than a
+locally packed one. After the tag is published and the framework package is live on NuGet.org,
+confirm a scaffold picks it up:
```powershell
-dotnet pack tools/Templates/Microsoft.UI.Reactor.Templates.csproj `
- --configuration Release `
- -p:Version=0.0.0-local `
- -p:MicrosoftUIReactorVersion=$version `
- -p:Platform=AnyCPU `
- -o local-nupkgs
-```
-
-Create a throwaway app from the packed template and verify its `.csproj` references the chosen public version. Skip restore before the tag is published because the new package version will not exist on NuGet.org yet:
-
-```powershell
-dotnet new uninstall Microsoft.UI.Reactor.ProjectTemplates
-dotnet new install local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg
+winapp new --list # installs the published template pack on demand
$scratch = Join-Path $env:TEMP "reactor-template-smoke"
Remove-Item $scratch -Recurse -Force -ErrorAction SilentlyContinue
-dotnet new reactorapp -n ReactorTemplateSmoke -o $scratch --no-restore
+dotnet new reactor -n ReactorTemplateSmoke -o $scratch --no-restore --reactor-version $version
Select-String "$scratch\ReactorTemplateSmoke.csproj" -Pattern $version
```
+Note the templates' *default* Reactor version is owned by the WindowsAppSDK repo and only changes
+when a PR there bumps it, so a fresh release is not reflected in the default until that lands —
+`--reactor-version` is how you check the new version scaffolds and restores cleanly in the meantime.
+
Open the PR and wait for CI. Do not tag until the release PR is merged.
## Tag the merged release commit
diff --git a/docs/guide/README.md b/docs/guide/README.md
index db51faf5a..a58d7b578 100644
--- a/docs/guide/README.md
+++ b/docs/guide/README.md
@@ -213,7 +213,7 @@ uniform Summary / Parameters / Returns / Discussion / Examples / See Also page.
## Minimal Project Setup
-The fastest path is [`dotnet new reactorapp`](getting-started.md), which
+The fastest path is [`dotnet new reactor`](getting-started.md), which
scaffolds all of this for you. To wire it up by hand instead, create a console
project and edit the `.csproj`:
diff --git a/docs/guide/dev-tooling.md b/docs/guide/dev-tooling.md
index 1c48dfa0a..d777cd014 100644
--- a/docs/guide/dev-tooling.md
+++ b/docs/guide/dev-tooling.md
@@ -179,10 +179,17 @@ subcommands map one-to-one to the workflows below.
| `mur loc` | Run the localization pipeline (`extract`, `translate`, `validate`, `status`, `prune`) | `mur loc extract` |
| `mur devtools` | Launch the project with `--devtools run`, supervise reloads, and host the MCP endpoint | `mur devtools` |
| `mur check` | Repo-health checks (cref validity, namespace policy, "did you mean" suggestions) | `mur check` |
-| `mur doctor` | Verify the install — SDK, `mur`, local feed, template, plugin | `mur doctor` |
-| `mur upgrade` | Re-pack framework + templates and refresh the plugin after a `git pull` | `mur upgrade` |
+| `mur doctor` | Verify the install — SDK, `mur`, local feed, templates, plugin | `mur doctor` |
+| `mur upgrade` | Re-pack the framework and refresh the plugin after a `git pull`; checks template availability but does not install the pack (that's `winapp new`) | `mur upgrade` |
| `mur figma watch` | Poll a Figma file for design changes | `mur figma watch` |
-| `mur pack-local` / `mur clean-local` | Package / clean the local NuGet feed for source-built framework smoke tests; the app template defaults to the public Reactor preview unless `--MSUIReactorVersion` is supplied | `mur pack-local` |
+| `mur pack-local` / `mur clean-local` | Package / clean the local NuGet feed for source-built framework smoke tests; scaffolded apps default to the public Reactor preview unless `--reactor-version` is supplied | `mur pack-local` |
+| `mur templates status` | Report whether `dotnet new reactor` resolves (exit `0` available, `1` probe failed, `2` pack too old, `3` pack missing) | `mur templates status` |
+
+To *install* the template pack, use the Windows App SDK CLI rather than `mur`:
+`winapp new -t reactor -n MyApp` installs it on demand and scaffolds in one
+step, and `winapp new --list` installs it without scaffolding. `bootstrap.ps1`
+drives that same command, so a bootstrapped checkout can go straight to
+`dotnet new reactor`.
Beyond the subcommands there are four top-level options worth knowing:
`mur --create ` scaffolds a new Reactor project, `mur --skill`
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 905a78048..4cc6ee722 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -19,54 +19,92 @@ the rest of the docset elaborates.
> **Public preview package available.** Reactor ships `Microsoft.UI.Reactor`
-> `0.1.0-preview.16` on NuGet.org. The project template package is still
-> installed from source for now; `bootstrap.ps1` installs `mur`, packs/registers
-> the local `reactorapp` template, and stamps generated apps to reference the
-> public preview package by default. Broader signed distribution is tracked in
+> `0.1.0-preview.16` on NuGet.org, and the project templates ship in the
+> official Windows App SDK `dotnet new` pack
+> (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`) — so `dotnet new reactor`
+> works from a plain .NET SDK install, no source checkout required.
+> `bootstrap.ps1` is for *contributors*: it installs `mur`, packs source-built
+> framework snapshots, and registers those same templates. Broader signed
+> distribution is tracked in
> [spec 022](https://github.com/microsoft/microsoft-ui-reactor/blob/main/docs/specs/022-packaging-and-distribution.md).
Reactor is a declarative UI framework for building native Windows apps in pure C#.
No XAML, no data binding, no view models. You describe your UI as a function of
state and Reactor keeps the screen in sync.
-## Setup (one-time)
+## Setup
+
+If you just want to build an app, scaffold one and go — you do not need to
+clone this repo:
```powershell
-git clone https://github.com/microsoft/microsoft-ui-reactor.git
-cd microsoft-ui-reactor
-./bootstrap.ps1
+winget install Microsoft.WinAppCli
+winapp new -t reactor -n MyApp
+cd MyApp
+dotnet run
```
-That's it. `bootstrap.ps1` packs and installs `mur` as a `dotnet tool` global
-install (so it's on PATH cross-shell with no manual `$env:Path` edits), runs
-`mur pack-local` to produce local source-built framework snapshots and the
-matching `ProjectTemplates` nupkg, registers the `dotnet new reactorapp`
-template, and drops the Reactor agent plugin under `~/.claude/plugins/reactor`
-(symlink when allowed, copy otherwise). Apps created from that template reference
-`Microsoft.UI.Reactor` version `0.1.0-preview.16` from NuGet.org by default.
+`winapp` is the Windows App SDK CLI. `winapp new` installs the template pack on
+demand (and offers to update a stale one), so there is no separate install step
+and no version to pin.
+
+That gives you four starting points — `reactor` (blank), `reactor-mvu`
+(Model-View-Update via `UseReducer`), `reactor-navview` (`NavigationView` shell)
+and `reactor-tabview` (`TabView` shell). `winapp new --list` prints them all, and
+omitting `-t` picks one interactively.
-When it finishes you can immediately run:
+Prefer plain .NET tooling? Install the pack yourself, then scaffold with
+`dotnet new`:
```powershell
-dotnet new reactorapp -n MyApp
-cd MyApp
-dotnet run
+dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::
+dotnet new reactor -n MyApp
+```
+
+The pack is prerelease-only today, and `dotnet new install` resolves stable
+versions unless you pin one explicitly — which is the step `winapp new` removes.
+
+Scaffolded apps are **packaged** (single-project MSIX), so `dotnet run`
+registers a loose-layout package and launches the app with full package
+identity — the same thing F5 does in Visual Studio. That requires **Developer
+Mode** (Settings → System → For developers). See [Packaging](packaging.md) for
+the unpackaged alternative.
+
+## Contributor setup (one-time)
+
+Working *on* Reactor rather than with it? Clone and bootstrap:
+
+```powershell
+git clone https://github.com/microsoft/microsoft-ui-reactor.git
+cd microsoft-ui-reactor
+./bootstrap.ps1
```
+`bootstrap.ps1` packs and installs `mur` as a `dotnet tool` global
+install (so it's on PATH cross-shell with no manual `$env:Path` edits), runs
+`mur pack-local` to produce local source-built framework snapshots, installs the
+Windows App SDK template pack,
+and drops the Reactor agent plugin under `~/.claude/plugins/reactor`
+(symlink when allowed, copy otherwise).
+
### After `git pull`
-The source checkout changes — your local template package, CLI, plugin, and
-optional source-built framework snapshots do not, unless you repack them. Two
-options:
+The source checkout changes — your local framework snapshots, CLI and plugin
+do not, unless you repack them. Two options:
```powershell
-mur upgrade # repacks the framework + templates and refreshes plugin
-./bootstrap.ps1 # same, plus updates the `mur` global tool itself
+mur upgrade # repacks the framework and refreshes the plugin
+./bootstrap.ps1 # same, plus the `mur` global tool and the template pack
```
`mur upgrade` is the lightweight path. Re-run `bootstrap.ps1` when you want to
pick up CLI changes (a `mur` process can't replace its own binary mid-run).
+The `dotnet new reactor` templates are not repacked by either: they ship in the
+Windows App SDK pack, so `git pull` never invalidates them. `mur upgrade` only
+*checks* that they still resolve; `bootstrap.ps1` installs the pack when it is
+missing, and `winapp new --list --template-version latest` updates it.
+
### Verify the install
```powershell
@@ -74,17 +112,16 @@ mur doctor
```
Lists every dependency the rest of this guide assumes — .NET 10+ SDK, `mur` on
-PATH, current `local-nupkgs/` developer feed, the `reactorapp` template
+PATH, current `local-nupkgs/` developer feed, the `dotnet new reactor` template
registration, and the optional Claude plugin. Each line is PASS / WARN / FAIL
with a one-line remediation for anything broken.
> **What this gets you.** A globally-resolvable `mur` (via `~/.dotnet/tools`),
-> a locally installed `reactorapp` template that references
-> ` Version="0.1.0-preview.16" />`, a local NuGet feed at `/local-nupkgs/`
+> the `dotnet new reactor` templates, a local NuGet feed at
+> `/local-nupkgs/`
> for source-built smoke tests, and an agent plugin so AI assistants generate
> against the real factories (`mur --skill` / `mur --api` print the same
-> content). Run `mur upgrade` whenever you pull new template, CLI, plugin, or
+> content). Run `mur upgrade` whenever you pull new CLI, plugin, or
> framework changes.
> **Only need the framework package?** Reference the published
@@ -107,8 +144,8 @@ anything goes wrong.
| 2 | `git clone` + `cd` | Local source checkout |
| 3 | `dotnet pack src/Reactor.Cli` | `Microsoft.UI.Reactor.Cli..nupkg` in `local-nupkgs/` |
| 4 | `dotnet tool install -g` | `mur` resolvable cross-shell from `~/.dotnet/tools` |
-| 5 | `mur pack-local` | Source-built framework snapshots plus a local `ProjectTemplates` nupkg; generated apps default to the public Reactor preview |
-| 6 | `dotnet new uninstall` + `install` | `dotnet new reactorapp` template registered |
+| 5 | `mur pack-local` | Source-built framework snapshots in `local-nupkgs/` |
+| 6 | `dotnet new install` | `dotnet new reactor` templates registered |
| 7 | Symlink/copy `plugins/reactor` | Reactor agent kit under `~/.claude/plugins/reactor` (optional) |
| 8 | `mur doctor` | Verification that 1–7 all stuck |
@@ -168,18 +205,16 @@ $env:Path = "$env:USERPROFILE\.dotnet\tools;$env:Path"
New PowerShell windows pick up the user-PATH change on their own.
-**5. Pack local framework snapshots and project templates.** This produces the
-source-built `0.0.0-local` framework nupkgs for smoke tests plus the local
-`ProjectTemplates` nupkg that installs `dotnet new reactorapp`. The template's
-normal default references the public `Microsoft.UI.Reactor` `0.1.0-preview.16`
-package.
+**5. Pack local framework snapshots.** This produces the source-built
+`0.0.0-local` framework nupkgs so recipes and smoke tests in this clone resolve
+your working tree instead of the published package.
```powershell
mur pack-local
# Produces:
# local-nupkgs/Microsoft.UI.Reactor.0.0.0-local.nupkg
# local-nupkgs/Microsoft.UI.Reactor.Advanced.0.0.0-local.nupkg
-# local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg
+# local-nupkgs/Microsoft.UI.Reactor.Devtools.0.0.0-local.nupkg
```
If you'd rather not depend on the freshly-installed `mur`, you can invoke
@@ -190,15 +225,17 @@ dotnet run --project src/Reactor.Cli/Reactor.Cli.csproj `
-c Release "-p:Platform=$hostArch" -- pack-local
```
-**6. Install the `dotnet new reactorapp` template.** The template engine
-caches by package id, so a same-version repack can lose to the cached copy.
-Always uninstall first.
+**6. Install the `dotnet new reactor` templates.** These come from the Windows
+App SDK template pack on NuGet.org, not from this checkout:
```powershell
-dotnet new uninstall Microsoft.UI.Reactor.ProjectTemplates 2>$null
-dotnet new install local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg
+dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::
```
+The pack is prerelease-only today, so pin a version explicitly — a bare
+`dotnet new install` resolves stable versions only. `winapp new -t reactor`
+collapses both steps: it installs the newest pack on demand, then scaffolds.
+
**7. (Optional) Install the Reactor agent plugin.** If you use Claude Code
or another agent and want it to author Reactor code with the right
factories, drop the in-repo plugin folder into your agent's plugin path. A
@@ -244,42 +281,40 @@ the install must happen from a shell that isn't already running `mur`).
> with no arch-aware PATH munging, and `dotnet tool update -g` becomes the
> upgrade verb.
-> **Caveat:** The core framework package is public, but the `reactorapp` project-template
-> package is still source-installed. If `dotnet new reactorapp` is missing, run
-> `bootstrap.ps1` (or `mur upgrade` from an already bootstrapped checkout) to
-> repack and reinstall `Microsoft.UI.Reactor.ProjectTemplates` from
-> `local-nupkgs/`. The template installer caches by package id, so a same-version
-> repack can lose to the cached copy — `mur upgrade` handles this by running
-> `dotnet new uninstall` first.
+> **Caveat:** The Reactor templates ship in the Windows App SDK `dotnet new` pack
+> (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`), so `dotnet new reactor`
+> needs no source checkout. If it's missing, `winapp new -t reactor` installs the
+> pack on demand and scaffolds in one step. To install it without scaffolding, run
+> `dotnet new install Microsoft.WindowsAppSDK.WinUI.CSharp.Templates::` —
+> the pack is prerelease-only today, so the explicit version pin is required.
## Creating a Project
-With the template installed, scaffold a new app from anywhere on disk:
+With the templates installed, scaffold a new app from anywhere on disk:
```powershell
-dotnet new reactorapp -n MyApp
+dotnet new reactor -n MyApp
cd MyApp
dotnet run
```
The template wires up the `Microsoft.UI.Reactor` package reference, the
-WinUI 3 target framework, and a working `App.cs` that mounts a single
-Reactor component. No `App.xaml`, no `MainWindow.xaml.cs` — just one C#
-file.
+WinUI 3 target framework, MSIX packaging, and a working `App.cs` that mounts a
+single Reactor component. No `App.xaml`, no `MainWindow.xaml.cs` — just one C#
+file. Swap `reactor` for `reactor-mvu`, `reactor-navview` or `reactor-tabview`
+to start from a richer shell.
-By default that package reference is
-``.
For local framework smoke tests, generate with
-`dotnet new reactorapp -n MyLocalApp --MSUIReactorVersion 0.0.0-local` and run
+`dotnet new reactor -n MyLocalApp --reactor-version 0.0.0-local` and run
from inside the source checkout or another folder that has the local feed
configured.
> **Why a custom template?** A `dotnet new console` does not produce a WinUI
> app — it builds a console target with no UI thread, no `OutputType=WinExe`,
-> no WindowsAppSDK reference, and no `[STAThread]` entry point. `reactorapp`
-> sets all of those plus the Reactor package reference and a backdrop-aware
-> root component, so you get a window on first `dotnet run` instead of a
-> console-host stub.
+> no WindowsAppSDK reference, and no `[STAThread]` entry point. `reactor`
+> sets all of those plus the Reactor package reference, MSIX packaging and a
+> backdrop-aware root component, so you get a window on first `dotnet run`
+> instead of a console-host stub.
## Your First App
@@ -735,7 +770,7 @@ side effects, `UseRef` for non-rendering bookkeeping).
The dev menu needs **two** independent signals, and neither one is
`#if DEBUG`. First the build-time capability — the `Reactor.DevtoolsSupport`
-feature switch, which `dotnet new reactorapp` already sets in Debug
+feature switch, which `dotnet new reactor` already sets in Debug
configurations along with the `Microsoft.UI.Reactor.Devtools` package. Second a
session opt-in on the command line:
diff --git a/docs/guide/index.md b/docs/guide/index.md
index db51faf5a..a58d7b578 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -213,7 +213,7 @@ uniform Summary / Parameters / Returns / Discussion / Examples / See Also page.
## Minimal Project Setup
-The fastest path is [`dotnet new reactorapp`](getting-started.md), which
+The fastest path is [`dotnet new reactor`](getting-started.md), which
scaffolds all of this for you. To wire it up by hand instead, create a console
project and edit the `.csproj`:
diff --git a/docs/guide/packaging.md b/docs/guide/packaging.md
index 63d225684..d3d89de3d 100644
--- a/docs/guide/packaging.md
+++ b/docs/guide/packaging.md
@@ -4,9 +4,9 @@
A Microsoft.UI.Reactor (Reactor) app is a normal WinUI 3 / Windows App SDK executable —
`dotnet publish` produces the deployable artifact and the framework
itself adds nothing exotic to the project file. What you choose at
-publish time is the **shape** of that artifact: an unpackaged folder
-(the [`dotnet new reactorapp`](getting-started.md) default), a signed
-MSIX, a single-file bundle, or a Native AOT native binary — each
+publish time is the **shape** of that artifact: a packaged MSIX
+(the [`dotnet new reactor`](getting-started.md) default), an unpackaged
+folder, a single-file bundle, or a Native AOT native binary — each
combined with a `win-x64` or `win-arm64` runtime identifier. The
trade-offs are the same ones any WinUI 3 app faces; the
Reactor-specific notes on this page cover what changes when your
@@ -17,11 +17,19 @@ INPC walker).
| Publish shape | Key properties | Runtime identifier | What you get |
|---|---|---|---|
-| Unpackaged (template default) | `WindowsPackageType=None`, `WindowsAppSDKSelfContained=true` | `win-x64` / `win-arm64` | A folder with `MyApp.exe` and the WinUI 3 runtime alongside it. Run from anywhere; ship as a zip. |
+| Unpackaged | `WindowsPackageType=None`, `WindowsAppSDKSelfContained=true` | `win-x64` / `win-arm64` | A folder with `MyApp.exe` and the WinUI 3 runtime alongside it. Run from anywhere; ship as a zip. |
| MSIX | `WindowsPackageType=MSIX`, `GenerateAppxPackageOnBuild=true`, signed via `PackageCertificateThumbprint` or `PackageCertificateKeyFile` | `win-x64` / `win-arm64` | A signed `.msix`. Required for Microsoft Store; the cleanest sideload story for enterprise. |
| Single-file | `PublishSingleFile=true`, `IncludeNativeLibrariesForSelfExtract=true` | `win-x64` / `win-arm64` (must be set) | One `.exe` that self-extracts the WinUI runtime to `%TEMP%/.net/` on first launch. |
| Native AOT | `PublishAot=true`, `InvariantGlobalization=true` (recommended) | `win-x64` / `win-arm64` (required) | A native binary with no JIT, no `Assembly.GetTypes()`, no `Reflection.Emit`. Fastest cold start; trim-only. |
+A scaffolded app starts out **packaged**: `dotnet new reactor` emits a
+`Package.appxmanifest` and sets `EnableMsixTooling`, so `dotnet run`
+registers a loose-layout package and the app has identity from the
+first launch. The properties in the MSIX row above are the *publish*
+knobs for producing and signing a redistributable `.msix` — a separate
+concern the scaffold deliberately leaves off, since a fresh clone
+should build without a developer certificate.
+
The four shapes are not mutually exclusive — MSIX wraps any of the
three publish outputs, and AOT layers on top of either an unpackaged
folder or an MSIX. The decision is usually distribution-channel-first
@@ -31,8 +39,8 @@ folder or an MSIX. The decision is usually distribution-channel-first
## The unpackaged shape
-`dotnet new reactorapp` scaffolds an unpackaged WinUI 3 project — the
-shape every sample in this repo also uses:
+Prefer a zip-and-go folder over an MSIX? Set `WindowsPackageType=None`
+on a scaffolded project — the shape every sample in this repo uses:
```xml
@@ -106,7 +114,30 @@ shape adds three properties on top of the unpackaged CSPROJ:
```
`Package.appxmanifest` declares the package identity (Publisher,
-PackageFamilyName, capabilities, file-type associations). The
+PackageFamilyName, capabilities, file-type associations). Here is the
+identity block from this repo's own packaged selftest host, which is a
+real single-project MSIX app:
+
+```xml
+
+
+
+ Reactor Packaged Test Host
+ Microsoft.UI.Reactor
+ Images\StoreLogo.png
+
+
+
+
+
+```
+
+`Publisher` must match the subject of the signing certificate, and
+`Name` plus `Publisher` together derive the PackageFamilyName that
+Windows uses to identify the app. The
[WinUI 3 packaging docs](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/packaging/)
cover the manifest surface in full. The signing certificate is
either a Microsoft Store-issued cert (for Store submissions) or a
@@ -164,8 +195,8 @@ and any `System.Drawing.Common` / `TraceEvent` natives transitively
pulled in by Reactor) ship per-RID, which is why the runtime
identifier matters even for managed-only Reactor code. The repo's
sample apps default to `x64;ARM64`; the
-`reactorapp` template uses `x64;ARM64;X86`
-(X86 retained for parity with the WinUI 3 templates), but Reactor
+`dotnet new reactor` templates use `x86;x64;ARM64`
+(x86 retained for parity with the WinUI 3 templates), but Reactor
itself is only tested on x64 / ARM64.
## Native AOT
@@ -192,64 +223,21 @@ and a runtime identifier:
`dotnet publish -c Release -r win-x64` produces a native binary —
no `coreclr.dll`, no JIT, ~50 ms cold start versus ~250 ms for the
-JIT-based build on the same hardware. The project template gates
-the same shape behind a `NativeAot` parameter:
+JIT-based build on the same hardware.
+
+The `dotnet new reactor` templates do **not** enable AOT — they ship
+`PublishReadyToRun` + `PublishTrimmed` for non-Debug configurations
+instead. To go all the way to AOT, add the properties to the scaffolded
+CSPROJ yourself:
```xml
-
- WinExe
- net10.0-windows10.0.22621.0
- TargetFrameworkOverride-windows10.0.22621.0
-
- x64;ARM64;X86
- true
- None
-
- true
- 10.0.17763.0
- 10.0.17763.0
- enable
-
- Assets\AppIcon.ico
-
- $(NETCoreSdkPortableRuntimeIdentifier)
-
- true
- true
-
+
+ true
+ true
```
-Pass `dotnet new reactorapp --NativeAot true` to get the AOT-enabled
-variant. `InvariantGlobalization=true` is paired with `PublishAot`
+`InvariantGlobalization=true` is paired with `PublishAot`
because the alternative — shipping the full ICU data — pulls in
trim warnings that the AOT analyzer flags as actionable.
@@ -356,7 +344,7 @@ in Release too.
## Next Steps
- **[Dev Tooling](dev-tooling.md)** — Previous: the inner-loop side of the build pipeline (`mur pack-local`, `dotnet watch`, hot reload).
-- **[Getting Started](getting-started.md)** — Where the `dotnet new reactorapp` template that produces the unpackaged shape comes from.
+- **[Getting Started](getting-started.md)** — Where the `dotnet new reactor` templates that produce the packaged shape come from.
- **[Performance](performance.md)** — When you should reach for AOT (cold-start budgets, startup-perf benchmarks).
- **[Perf Instrumentation](perf-instrumentation.md)** — The ETW / EventPipe pipeline that survives AOT publish unchanged.
- **[Dev Tooling](dev-tooling.md)** — How the `Reactor.DevtoolsSupport` capability switch combines with `--devtools` activation.
diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md
index 9858e941c..e58dd5d0b 100644
--- a/docs/security/threat-model.md
+++ b/docs/security/threat-model.md
@@ -83,7 +83,6 @@ Three things to notice:
|---|---|---|---|---|
| `Microsoft.UI.Reactor..nupkg` | NuGet, MSIL only | No | Not yet (tracked separately) | Framework |
| `Microsoft.UI.Reactor..snupkg` | Symbols | No | n/a | |
-| `Microsoft.UI.Reactor.Templates.*.nupkg` | NuGet | No | Not yet | `dotnet new` templates |
| `reactor-skill-kit-.zip` | Zip | No (mur.exe is framework-dependent .NET 10) | Not yet | Contains `bin/{x64,arm64}/mur.exe`, `install-skill-kit.ps1` |
No MSI, no MSIX, no Appx, no Authenticode-signed bundle. **Codesigning of NuGet and `mur.exe` is a known compliance gap** — tracked under the BinSkim/SDL stream, not in this threat model.
diff --git a/plugins/reactor/agents/reactor-dev.agent.md b/plugins/reactor/agents/reactor-dev.agent.md
index d3f4c8bf0..dbb73ce88 100644
--- a/plugins/reactor/agents/reactor-dev.agent.md
+++ b/plugins/reactor/agents/reactor-dev.agent.md
@@ -6,21 +6,26 @@ user-invocable: true
## Process
-> ### ⚠️ ALWAYS start a new app with `dotnet new reactorapp`
+> ### ⚠️ ALWAYS start a new app with `dotnet new reactor`
>
> Unless the user has explicitly asked you to do something else (e.g. modify an existing project, write a single-file `#:package` script on purpose, or convert from XAML/MVVM into an existing tree), **your very first action on a new Reactor app is:**
>
> ```
-> dotnet new reactorapp -n
+> dotnet new reactor -n
> ```
>
> Run it from the workspace root. It produces a working `.csproj` and `App.cs` already wired against `Microsoft.UI.Reactor` (the `App.cs` carries its own `using` directives — there is no `GlobalUsings.cs`, and you should not add one). **Edit that. Do not hand-write a `.csproj` and `App.cs` from scratch** — that path consistently leads to invented API names (`UseElementFocus`, `AutomationLandmarkType.Complementary`, etc.), wasted `mur check` round-trips, and longer sessions.
>
-> If the template isn't installed yet, install it before scaffolding:
+> Richer starting points: `reactor-mvu` (Model-View-Update via `UseReducer`), `reactor-navview` (`NavigationView` shell), `reactor-tabview` (`TabView` shell).
+>
+> If the templates aren't installed yet, scaffold with the Windows App SDK CLI
+> instead — it installs the pack on demand, so there is no version to pin:
>
> ```
-> dotnet new install Microsoft.UI.Reactor.ProjectTemplates
+> winapp new -t reactor -n
> ```
+>
+> To install the pack without scaffolding, run `winapp new --list`.
You build Reactor apps in this rhythm: scaffold → understand requirements → draft component tree → write files in a batch → `mur check`.
@@ -31,7 +36,7 @@ Before continuing
Then for each task:
-1. **Scaffold first** (see the callout above). For a new app: `dotnet new reactorapp -n `. Skip this step *only* if the user has told you to write a single-file script, edit an existing project, or otherwise asked for a non-scaffolded shape.
+1. **Scaffold first** (see the callout above). For a new app: `dotnet new reactor -n `. Skip this step *only* if the user has told you to write a single-file script, edit an existing project, or otherwise asked for a non-scaffolded shape.
2. **Understand the task.** Note what the app needs to do. Don't guess at requirements.
3. **Draft.** Sketch the component tree, identify state, decide where each piece lives. If you know how you'd build the equivalent in React, you already know the shape — just translate to the C# spelling.
4. **Write the files in a batch.** Add models and child components in one stretch on top of the scaffolded `App.cs`. Don't stop and rebuild after each file — build once at the end.
diff --git a/plugins/reactor/skills/reactor-getting-started/SKILL.md b/plugins/reactor/skills/reactor-getting-started/SKILL.md
index 0b63051ef..671be5361 100644
--- a/plugins/reactor/skills/reactor-getting-started/SKILL.md
+++ b/plugins/reactor/skills/reactor-getting-started/SKILL.md
@@ -50,13 +50,13 @@ description: "Reactor essentials in one place — React-to-Reactor mental model,
## Starting a new app
-`dotnet new reactorapp -n ` scaffolds the canonical shape: `App.cs` (entry point + initial component, with the `using` block at the top) plus `.csproj`. See the anti-probe + `mur check` notes under "Use a `.csproj` …" below for what comes out of the scaffold.
+`dotnet new reactor -n ` scaffolds the canonical shape: `App.cs` (entry point + initial component, with the `using` block at the top) plus `.csproj`. Richer shells: `reactor-mvu`, `reactor-navview`, `reactor-tabview`. See the anti-probe + `mur check` notes under "Use a `.csproj` …" below for what comes out of the scaffold.
For a single-file `dotnet run App.cs` demo (no `.csproj`), prepend the file-level `#:package` / `#:property` headers — see `reactor-build-and-check`'s single-file-scripts section.
## Use a `.csproj` when you need …
-… multiple files, **analyzers** (single-file `.cs` builds don't load them), or shared project references. `dotnet new reactorapp` scaffolds the canonical csproj — you don't need to author one from scratch.
+… multiple files, **analyzers** (single-file `.cs` builds don't load them), or shared project references. `dotnet new reactor` scaffolds the canonical csproj — you don't need to author one from scratch.
`UseWinUI` MUST be `true`. **No XAML files of any kind.**
@@ -65,11 +65,11 @@ produced.** Reactor works in both shapes:
- **Unpackaged** — `None`, no `Package.appxmanifest`.
`None` wins over `EnableMsixTooling`, so a project carrying both still builds unpackaged. Runs
- from any folder, no MSIX registration. This is what `dotnet new reactorapp` produces today.
+ from any folder, no MSIX registration. Scaffold with `dotnet new reactor` and set this property.
- **Packaged** — has a `Package.appxmanifest`. Either omit `WindowsPackageType` entirely (the
default produces a packaged app) or set it to `MSIX`; pair it with `EnableMsixTooling=true` for
the single-project MSIX tooling. Launches with package identity, which is what identity-gated
- APIs require.
+ APIs require. This is what the `dotnet new reactor` family produces.
Neither is "the" Reactor shape, and neither is signalled by one property alone. **If a project has a
`Package.appxmanifest`, it is packaged on purpose — adding
@@ -78,9 +78,11 @@ still succeeds, so nothing surfaces the mistake.** Switch modes only when the us
the whole property *set*, not a single flag — the `packaging` guide has both shapes in full,
including the explicit-`MSIX` form.
-**After `dotnet new reactorapp -n `, the workspace contains `App.cs` (entry point + initial component) and `.csproj`, plus a `Properties/launchSettings.json` for F5 — and nothing else you need to touch.** Other Reactor templates scaffold more files (a packaged one adds MSIX packaging inputs); those are not source — leave them alone. There is no `Program.cs` and no `GlobalUsings.cs` — modify `App.cs` in place. `App.cs` has its own `using` directives at the top — see the *Required imports* section below — and that is where you add new namespaces (e.g. `using System.Linq;` when you reach for `.Select(...)`). Some templates enable implicit usings, so a namespace may already be in scope. Don't probe the `.csproj` after scaffolding unless you're adding a `PackageReference` or changing a property — `Restore succeeded.` in the scaffold stdout is the only confirmation you need.
+**After `dotnet new reactor -n `, the source you edit is `App.cs` (entry point + initial component) next to `.csproj`.** The packaged templates also scaffold MSIX packaging inputs — `Package.appxmanifest`, `app.manifest`, `Assets/`, `Properties/launchSettings.json`, `Properties/PublishProfiles/`; those are not source — leave them alone. There is no `Program.cs` and no `GlobalUsings.cs` — modify `App.cs` in place. `App.cs` has its own `using` directives at the top — see the *Required imports* section below — and that is where you add new namespaces (e.g. `using System.Linq;` when you reach for `.Select(...)`). Some templates enable implicit usings, so a namespace may already be in scope. Don't probe the `.csproj` after scaffolding unless you're adding a `PackageReference` or changing a property — `Restore succeeded.` in the scaffold stdout is the only confirmation you need.
-**The scaffolded csproj ships with `WindowsAppSDKSelfContained=true` and a Debug-only ItemGroup that adds `Microsoft.UI.Reactor.Devtools` + `Reactor.DevtoolsSupport=true`.** Together they make `dotnet watch run` (and the very rough, experimental Visual Studio embedded-preview extension) hot-reload safe and F5 (which passes `--devtools` from `Properties/launchSettings.json`) bring up the devtools menu. The VS extension is currently the roughest Reactor surface; do not present it as stable. Release builds drop the devtools package and host-config switch so trim / AOT analyzers stay quiet — see the `packaging` guide for the full rationale before flipping either knob.
+**Scaffolded apps are packaged, so `dotnet run` launches them with MSIX package identity** — the F5 equivalent. That needs **Developer Mode on** (Settings → System → For developers) to register the loose-layout package, and a concrete architecture (the template declares `x86;x64;ARM64`; AnyCPU is rejected).
+
+**The scaffolded csproj ships a Debug-only ItemGroup that adds `Microsoft.UI.Reactor.Devtools` + `Reactor.DevtoolsSupport=true`.** `Properties/launchSettings.json` carries three profiles: a default **Package** profile, an **Unpackaged** profile, and an **Unpackaged, Devtools** profile that passes `--devtools` to bring up the devtools menu. Pick the devtools profile in the launch dropdown when you want it. Release builds drop the devtools package and host-config switch so trim / AOT analyzers stay quiet — see the `packaging` guide for the full rationale before flipping either knob.
**Verify your edits with `mur check`** before declaring done. From the project directory: `mur check` (no arguments) runs `dotnet build` and emits one compressed line per diagnostic with a `→ try:` suggestion when the engine recognizes the mistake; `mur check --final` is the explicit "I am done iterating" sweep that emits the full diagnostic set including suppressed iteration-mode warnings. For anything more involved than the build/fix loop — strict-mode failures, custom diagnostic gating, MSBuild passthrough flags — load the `reactor-build-and-check` skill.
@@ -591,7 +593,7 @@ Nested `.Provide()` overrides the outer for its subtree only. If no provider is
> ⚠️ **Platform flag required when working *inside this repo* (selfhost)**: always build samples / in-repo projects with an explicit platform: `dotnet build -p:Platform=x64` (or `ARM64`). Omitting `-p:Platform=...` causes `WindowsAppSDKSelfContained` errors. This applies to `dotnet build`, `dotnet run`, and `mur check` invocations alike.
>
-> The `dotnet new reactorapp` template auto-resolves `RuntimeIdentifier` from the host SDK when `Platform`/`RuntimeIdentifier` aren't explicit, so consumer projects scaffolded outside the repo build with bare `dotnet build` / F5 — the rule above only applies in the selfhost tree.
+> The `dotnet new reactor` template auto-resolves `RuntimeIdentifier` from the host SDK when `Platform`/`RuntimeIdentifier` aren't explicit, so consumer projects scaffolded outside the repo build with bare `dotnet build` / F5 — the rule above only applies in the selfhost tree.
## Where the skill content comes from (and the api index)
diff --git a/src/Reactor.Advanced/README.md b/src/Reactor.Advanced/README.md
index 21d30188a..bc5f38cbf 100644
--- a/src/Reactor.Advanced/README.md
+++ b/src/Reactor.Advanced/README.md
@@ -83,7 +83,6 @@ When `radius` changes, the new `redrawKey` tells Reactor to invalidate the canva
- [`Microsoft.UI.Reactor`](https://www.nuget.org/packages/Microsoft.UI.Reactor) — the core declarative WinUI 3 framework (required).
- [`Microsoft.UI.Reactor.Devtools`](https://www.nuget.org/packages/Microsoft.UI.Reactor.Devtools) — optional developer-loop devtools host.
-- [`Microsoft.UI.Reactor.ProjectTemplates`](https://www.nuget.org/packages/Microsoft.UI.Reactor.ProjectTemplates) — `dotnet new` templates.
## Feedback & Contributing
diff --git a/src/Reactor.Cli/Doctor/DoctorCommand.cs b/src/Reactor.Cli/Doctor/DoctorCommand.cs
index f8a0ea295..e9752445f 100644
--- a/src/Reactor.Cli/Doctor/DoctorCommand.cs
+++ b/src/Reactor.Cli/Doctor/DoctorCommand.cs
@@ -4,7 +4,7 @@
// depends on. Prints a one-line PASS / WARN / FAIL per check and exits non-zero
// only when there are FAILs — WARNs (e.g. missing-template-enumeration or a
// stale-looking checkout) still exit 0 since the install is usable. Designed
-// to be the first thing a confused user runs after `dotnet new reactorapp`
+// to be the first thing a confused user runs after `dotnet new reactor`
// fails — every FAIL prints a copy-pasteable next step.
//
// Checks (in order):
@@ -16,14 +16,15 @@
// 4. local-nupkgs/Microsoft.UI.Reactor..nupkg present (framework)
// 5. local-nupkgs/Microsoft.UI.Reactor.Advanced..nupkg present
// (warn-only — opt-in Win2D canvas package)
-// 6. local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates..nupkg present
-// 7. `dotnet new` template list includes `reactorapp` (always runs — does
-// not depend on the repo checkout being found)
-// 8. Claude plugin at ~/.claude/plugins/reactor (informational only; not
+// 6. The Windows App SDK `dotnet new` template pack is registered, which is
+// what provides `dotnet new reactor` (always runs — does not depend on
+// the repo checkout being found)
+// 7. Claude plugin at ~/.claude/plugins/reactor (informational only; not
// every developer uses Claude Code)
using System.Diagnostics;
using Microsoft.UI.Reactor.Cli.Pack;
+using Microsoft.UI.Reactor.Cli.Templates;
namespace Microsoft.UI.Reactor.Cli.Doctor;
@@ -98,7 +99,6 @@ public static int Run(string[] args)
// DefaultLocalVersion is the literal "0.0.0-local" so this is purely defensive.
var advancedFileName = $"Microsoft.UI.Reactor.Advanced.{PackLocalCommand.DefaultLocalVersion}.nupkg";
var advancedNupkg = Path.Combine(feed, Path.GetFileName(advancedFileName));
- var templateNupkg = Path.Combine(feed, $"Microsoft.UI.Reactor.ProjectTemplates.{PackLocalCommand.DefaultLocalVersion}.nupkg");
if (!File.Exists(frameworkNupkg))
{
@@ -124,33 +124,56 @@ public static int Run(string[] args)
{
Pass("local Advanced nupkg", $"{Path.GetFileName(advancedNupkg)} ({FormatAge(File.GetLastWriteTimeUtc(advancedNupkg))})");
}
-
- if (!File.Exists(templateNupkg))
- {
- Fail("local template nupkg", $"missing {templateNupkg}. Run `mur pack-local`.");
- failures++;
- }
- else
- {
- Pass("local template nupkg", $"{Path.GetFileName(templateNupkg)}");
- }
}
- // 4. dotnet new reactorapp template
- var templates = ListInstalledTemplates();
- if (templates is null)
+ // 4. `dotnet new reactor` templates (Windows App SDK template pack).
+ // Probe the template short name, not just the package id: the pack
+ // shipped versions that predate the Reactor templates, so a package-id
+ // check reports PASS while `dotnet new reactor` fails.
+ var templatesAvailable = WinAppSdkTemplates.AreTemplatesAvailable();
+ if (templatesAvailable is null)
{
Warn("dotnet new template", "could not enumerate `dotnet new` templates");
warnings++;
}
- else if (templates.Any(t => t.IndexOf("reactorapp", StringComparison.OrdinalIgnoreCase) >= 0))
+ else if (templatesAvailable.Value)
{
- Pass("dotnet new template", "reactorapp registered");
+ var ver = WinAppSdkTemplates.GetInstalledVersion();
+ var detail = ver is null
+ ? $"`dotnet new {WinAppSdkTemplates.BlankShortName}` available"
+ : $"`dotnet new {WinAppSdkTemplates.BlankShortName}` available ({WinAppSdkTemplates.PackageId} {ver})";
+ Pass("dotnet new template", detail);
}
else
{
- Fail("dotnet new template", "reactorapp not registered. Run `mur upgrade` or `dotnet new install /local-nupkgs/Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg`.");
- failures++;
+ // The short name is absent — but is the pack there at all? A null
+ // here means the installed-package list could not be read, which is a
+ // probe failure, not evidence the pack is missing. Reporting "not
+ // registered" then sends the developer to reinstall something that
+ // may already be fine. Mirrors TemplatesCommand.StatusExitCode.
+ var packageInstalled = WinAppSdkTemplates.IsPackageInstalled();
+ if (packageInstalled is null)
+ {
+ Warn("dotnet new template",
+ $"`dotnet new {WinAppSdkTemplates.BlankShortName}` did not resolve, and the installed-package " +
+ "list could not be read — so whether the pack is present is unknown. Check with `mur templates status`.");
+ warnings++;
+ }
+ else if (packageInstalled.Value)
+ {
+ // Installed, but this version doesn't carry the Reactor templates.
+ // Distinct remediation from "not installed", so say so explicitly.
+ var ver = WinAppSdkTemplates.GetInstalledVersion() ?? "(unknown)";
+ Fail("dotnet new template",
+ $"{WinAppSdkTemplates.PackageId} {ver} is installed but does not provide `dotnet new {WinAppSdkTemplates.BlankShortName}`. " +
+ $"Update to a version that ships the Reactor templates: `winapp new --list --template-version latest`.");
+ failures++;
+ }
+ else
+ {
+ Fail("dotnet new template", $"{WinAppSdkTemplates.PackageId} not registered, so `dotnet new {WinAppSdkTemplates.BlankShortName}` is unavailable. Run `./bootstrap.ps1`, or install the pack with `winapp new --list`.");
+ failures++;
+ }
}
// 5. Claude plugin (informational only — many devs don't use it)
@@ -181,7 +204,7 @@ public static int Run(string[] args)
Console.WriteLine($" OK — {warnings} warning(s). Your install is functional.");
return 0;
}
- Console.WriteLine(" All checks passed. You're ready to `dotnet new reactorapp -n MyApp`.");
+ Console.WriteLine($" All checks passed. You're ready to `dotnet new {WinAppSdkTemplates.BlankShortName} -n MyApp`.");
return 0;
}
@@ -315,31 +338,6 @@ static bool IsGlobalToolInstalled()
}
}
- static List? ListInstalledTemplates()
- {
- try
- {
- var psi = new ProcessStartInfo("dotnet")
- {
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- ArgumentList = { "new", "list" },
- };
- using var proc = Process.Start(psi);
- if (proc is null) return null;
- var output = proc.StandardOutput.ReadToEnd();
- proc.WaitForExit();
- return output.Split('\n', StringSplitOptions.RemoveEmptyEntries)
- .Select(s => s.TrimEnd())
- .ToList();
- }
- catch
- {
- return null;
- }
- }
-
static string FormatAge(DateTime utc)
{
var age = DateTime.UtcNow - utc;
diff --git a/src/Reactor.Cli/Pack/CleanLocalCommand.cs b/src/Reactor.Cli/Pack/CleanLocalCommand.cs
index f95a7e053..24b48a3c1 100644
--- a/src/Reactor.Cli/Pack/CleanLocalCommand.cs
+++ b/src/Reactor.Cli/Pack/CleanLocalCommand.cs
@@ -12,7 +12,11 @@ namespace Microsoft.UI.Reactor.Cli.Pack;
public static class CleanLocalCommand
{
// Package IDs that `pack-local` produces — lowercase to match the NuGet
- // global-packages folder convention.
+ // global-packages folder convention. `projecttemplates` is retained even
+ // though the package was removed from this repo: dev machines bootstrapped
+ // before the removal still have a stale nupkg in local-nupkgs/ and an
+ // extracted copy in the global cache, and cleaning those up is exactly what
+ // this command is for.
internal static readonly string[] PackageIds =
[
"microsoft.ui.reactor",
@@ -21,7 +25,9 @@ public static class CleanLocalCommand
"microsoft.ui.reactor.projecttemplates",
];
- // Template package ID used by `dotnet new install`.
+ // Legacy `dotnet new` template package, removed from this repo in favour of
+ // the Windows App SDK pack (`dotnet new reactor`). Kept here so the command
+ // can still unregister a stale install left behind on a dev machine.
internal const string TemplatePackageId = "Microsoft.UI.Reactor.ProjectTemplates";
public static int Run(string[] args)
@@ -93,7 +99,8 @@ public static int Run(string[] args)
// 3. Clear the NuGet HTTP cache so stale metadata doesn't linger.
RunDotnet(repoRoot, "nuget", "locals", "http-cache", "--clear");
- // 4. Uninstall project templates (non-fatal).
+ // 4. Unregister the legacy `dotnet new reactorapp` template if a stale
+ // install is still present from before it was removed (non-fatal).
UninstallTemplates(repoRoot);
Console.WriteLine();
diff --git a/src/Reactor.Cli/Pack/PackLocalCommand.cs b/src/Reactor.Cli/Pack/PackLocalCommand.cs
index 3946acae3..046aabe9a 100644
--- a/src/Reactor.Cli/Pack/PackLocalCommand.cs
+++ b/src/Reactor.Cli/Pack/PackLocalCommand.cs
@@ -16,13 +16,6 @@
// --version package version stamped on the produced nupkgs
// (default 0.0.0-local).
// --configuration build configuration (default Debug).
-// --framework-version version the packed `reactorapp` template tells
-// generated apps to reference. Accepts an explicit
-// version, or `latest` to resolve the newest published
-// Microsoft.UI.Reactor from NuGet. Omitted → the
-// template's csproj default. `bootstrap.ps1` passes
-// `latest`, so a fresh clone scaffolds against the
-// current release without a manual version bump.
using System.Diagnostics;
using System.Net.Http;
@@ -38,7 +31,6 @@ public static int Run(string[] args)
{
var version = ParseFlag(args, "--version") ?? DefaultLocalVersion;
var configuration = ParseFlag(args, "--configuration") ?? "Debug";
- var frameworkVersionArg = ParseFlag(args, "--framework-version");
var repoRoot = FindRepoRoot();
if (repoRoot is null)
@@ -56,8 +48,6 @@ public static int Run(string[] args)
// when `mur pack-local` reports success but the consumer keeps resolving stale.
foreach (var stale in Directory.EnumerateFiles(feed, $"Microsoft.UI.Reactor.{version}.*nupkg"))
DeleteStaleNupkg(stale);
- foreach (var stale in Directory.EnumerateFiles(feed, $"Microsoft.UI.Reactor.ProjectTemplates.{version}.*nupkg"))
- DeleteStaleNupkg(stale);
foreach (var stale in Directory.EnumerateFiles(feed, $"Microsoft.UI.Reactor.Advanced.{version}.*nupkg"))
DeleteStaleNupkg(stale);
foreach (var stale in Directory.EnumerateFiles(feed, $"Microsoft.UI.Reactor.Devtools.{version}.*nupkg"))
@@ -101,28 +91,6 @@ public static int Run(string[] args)
return rc;
}
- // 4. Project templates — Microsoft.UI.Reactor.ProjectTemplates..nupkg.
- // Powers `dotnet new reactorapp -n MyApp` against this clone. Templates pack
- // is AnyCPU (no arch needed). The framework version the generated app
- // references is baked into template.json at pack time from
- // MicrosoftUIReactorVersion:
- // • no --framework-version → the csproj default (a real *public* package),
- // so a scaffold restores from NuGet.org and builds standalone.
- // • --framework-version → stamp . `bootstrap.ps1` passes `latest`,
- // which resolves the newest published package so the local scaffold
- // default tracks releases automatically instead of drifting behind them.
- // Either way it's a public NuGet version; pass `--MSUIReactorVersion
- // 0.0.0-local` to `dotnet new reactorapp` to instead consume the
- // source-built framework packed into this feed.
- var templateFrameworkVersion = ResolveTemplateFrameworkVersion(frameworkVersionArg);
- Console.WriteLine($"Packing Microsoft.UI.Reactor.ProjectTemplates {version} → {feed}");
- rc = RunPack(repoRoot, Path.Combine("tools", "Templates", "Microsoft.UI.Reactor.Templates.csproj"), configuration, version, feed, arch: null, frameworkVersion: templateFrameworkVersion);
- if (rc != 0)
- {
- Console.Error.WriteLine("templates pack failed.");
- return rc;
- }
-
// Bust NuGet's HTTP cache for our local source so the new build is picked up
// immediately on the next restore. Failure here is non-fatal but surfaced as
// a warning — stale caches are a common "why doesn't my change take effect"
@@ -168,7 +136,6 @@ public static int Run(string[] args)
"microsoft.ui.reactor",
"microsoft.ui.reactor.advanced",
"microsoft.ui.reactor.devtools",
- "microsoft.ui.reactor.projecttemplates",
})
{
var cached = Path.Combine(globalPackages, packageId, version);
@@ -187,8 +154,6 @@ public static int Run(string[] args)
}
}
- var templatesNupkg = Path.Combine(feed, $"Microsoft.UI.Reactor.ProjectTemplates.{version}.nupkg");
-
Console.WriteLine();
Console.WriteLine($"Done. Apps in this repo can now reference:");
Console.WriteLine($" #:package Microsoft.UI.Reactor@{version}");
@@ -197,10 +162,9 @@ public static int Run(string[] args)
Console.WriteLine($" ");
Console.WriteLine($" ");
Console.WriteLine();
- Console.WriteLine($"To use `dotnet new reactorapp` against this feed:");
- Console.WriteLine($" dotnet new install \"{templatesNupkg}\"");
- Console.WriteLine($" # then, from anywhere inside this clone (so nuget.config applies):");
- Console.WriteLine($" dotnet new reactorapp -n MyApp");
+ Console.WriteLine($"To scaffold an app against this feed:");
+ Console.WriteLine($" dotnet new reactor -n MyApp --reactor-version {version}");
+ Console.WriteLine($" # run from inside this clone so nuget.config applies.");
Console.WriteLine($"Outside the clone, copy nuget.config to your project parent or add the absolute");
Console.WriteLine($"path '{feed}' as a NuGet source on your machine.");
return 0;
@@ -266,7 +230,7 @@ void AddPath(string? path)
return paths;
}
- static int RunPack(string repoRoot, string projectRelative, string configuration, string version, string feed, string? arch, string? frameworkVersion = null)
+ static int RunPack(string repoRoot, string projectRelative, string configuration, string version, string feed, string? arch)
{
var psi = new ProcessStartInfo("dotnet")
{
@@ -279,10 +243,6 @@ static int RunPack(string repoRoot, string projectRelative, string configuration
psi.ArgumentList.Add("-v:m");
psi.ArgumentList.Add($"-c:{configuration}");
psi.ArgumentList.Add($"-p:Version={version}");
- // Stamp the framework version the generated template references (baked into
- // template.json by the templates csproj's BeforePack target). Supplied only
- // for the templates pack; null leaves the csproj default in place.
- if (frameworkVersion is not null) psi.ArgumentList.Add($"-p:MicrosoftUIReactorVersion={frameworkVersion}");
psi.ArgumentList.Add($"-o:{feed}");
if (arch is not null) psi.ArgumentList.Add($"-p:Platform={arch}");
@@ -318,59 +278,6 @@ static void DeleteStaleNupkg(string path)
return null;
}
- // Resolve the framework version to bake into the packed template's
- // , from the --framework-version flag value:
- // • null/empty → null (leave the csproj default in place; today's behavior).
- // • "latest" → newest published Microsoft.UI.Reactor on NuGet.org, or null
- // (warn + fall back to the csproj default) when it can't be
- // resolved. Best-effort so `bootstrap.ps1` on a flaky network
- // still produces a usable feed.
- // • otherwise → the literal version supplied.
- internal static string? ResolveTemplateFrameworkVersion(string? frameworkVersionArg)
- {
- if (string.IsNullOrWhiteSpace(frameworkVersionArg))
- return null;
-
- var trimmed = frameworkVersionArg.Trim();
- if (!trimmed.Equals("latest", StringComparison.OrdinalIgnoreCase))
- return trimmed;
-
- var latest = TryResolveLatestPublishedFrameworkVersion();
- if (latest is null)
- {
- Console.Error.WriteLine(
- "warning: could not resolve the latest published Microsoft.UI.Reactor version from NuGet; " +
- "scaffolded apps will reference the template's built-in default version. Re-run with network " +
- "access, or pass an explicit --framework-version .");
- return null;
- }
-
- Console.WriteLine($"Latest published Microsoft.UI.Reactor: {latest} (stamped into the template)");
- return latest;
- }
-
- // NuGet flat-container index for the published framework package. Lists every
- // published version (including prereleases) as a JSON string array.
- const string FrameworkFlatContainerIndexUrl =
- "https://api.nuget.org/v3-flatcontainer/microsoft.ui.reactor/index.json";
-
- static string? TryResolveLatestPublishedFrameworkVersion()
- {
- try
- {
- using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
- var json = http.GetStringAsync(FrameworkFlatContainerIndexUrl).GetAwaiter().GetResult();
- return SelectLatestVersion(ParseFlatContainerVersions(json));
- }
- catch (Exception ex)
- {
- Console.Error.WriteLine(
- $"warning: querying NuGet for the latest Microsoft.UI.Reactor version failed " +
- $"({ex.GetType().Name}: {ex.Message}).");
- return null;
- }
- }
-
// Extract the "versions" string array from a NuGet flat-container index.json.
// Returns an empty list for malformed / oddly-shaped payloads (best-effort).
internal static IReadOnlyList ParseFlatContainerVersions(string json)
diff --git a/src/Reactor.Cli/Program.cs b/src/Reactor.Cli/Program.cs
index 57a8356de..5ab44b1a1 100644
--- a/src/Reactor.Cli/Program.cs
+++ b/src/Reactor.Cli/Program.cs
@@ -84,6 +84,11 @@
return Microsoft.UI.Reactor.Cli.Pack.CleanLocalCommand.Run(args.Skip(1).ToArray());
}
+if (arg == "templates")
+{
+ return Microsoft.UI.Reactor.Cli.Templates.TemplatesCommand.Run(args.Skip(1).ToArray());
+}
+
if (arg == "doctor")
{
return Microsoft.UI.Reactor.Cli.Doctor.DoctorCommand.Run(args.Skip(1).ToArray());
@@ -133,8 +138,9 @@ void ShowHelp()
Console.WriteLine(" check [path] Build and emit one-line diagnostics with skill-file pointers");
Console.WriteLine(" pack-local Pack the in-source framework to /local-nupkgs/ as 0.0.0-local");
Console.WriteLine(" clean-local Remove local packages, NuGet cache entries, and templates");
- Console.WriteLine(" doctor Verify the install (SDK, mur, local feed, template, plugin)");
- Console.WriteLine(" upgrade Re-pack framework + templates and refresh plugin after `git pull`");
+ Console.WriteLine(" templates Report whether `dotnet new reactor` resolves (install: `winapp new`)");
+ Console.WriteLine(" doctor Verify the install (SDK, mur, local feed, templates, plugin)");
+ Console.WriteLine(" upgrade Re-pack the framework and refresh the plugin after `git pull`");
Console.WriteLine(" figma watch Poll a Figma file for design changes");
}
diff --git a/src/Reactor.Cli/Templates/TemplatesCommand.cs b/src/Reactor.Cli/Templates/TemplatesCommand.cs
new file mode 100644
index 000000000..96e275adc
--- /dev/null
+++ b/src/Reactor.Cli/Templates/TemplatesCommand.cs
@@ -0,0 +1,145 @@
+// `mur templates` — report whether the `dotnet new` template pack that provides
+// `dotnet new reactor` is usable.
+//
+// Reactor's app templates ship inside the Windows App SDK template pack
+// (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`) rather than being built
+// from this checkout.
+//
+// This command only *reports*. Installing is the Windows App SDK CLI's job —
+// `winapp new` installs the pack on demand and scaffolds in one step, and
+// `bootstrap.ps1` §5 drives it through `winapp new --list`. `mur templates
+// install` used to exist because `dotnet new install` has no --prerelease switch
+// and resolves stable-only, which fails while the pack is prerelease-only;
+// `winapp` handles that, so the installer is gone rather than duplicated.
+//
+// Subcommands:
+// status Report whether `dotnet new reactor` resolves. Exit codes are
+// load-bearing — see StatusExit.
+
+namespace Microsoft.UI.Reactor.Cli.Templates;
+
+public static class TemplatesCommand
+{
+ public static int Run(string[] args)
+ {
+ var sub = args.FirstOrDefault();
+
+ if (sub is null or "--help" or "-h" or "help")
+ {
+ ShowHelp();
+ return sub is null ? 1 : 0;
+ }
+
+ switch (sub)
+ {
+ case "status":
+ return Status();
+ case "install":
+ // Named explicitly rather than falling into "unknown subcommand":
+ // this command existed, `bootstrap.ps1` and the docs used to call
+ // it, and silently rejecting it as a typo would send someone
+ // looking for a misspelling instead of the replacement.
+ Console.Error.WriteLine(
+ "mur templates install has been removed. Scaffold with the Windows App SDK CLI instead:");
+ Console.Error.WriteLine($" winapp new -t {WinAppSdkTemplates.BlankShortName} -n MyApp");
+ Console.Error.WriteLine(
+ " It installs the template pack on demand. To install the pack without scaffolding:");
+ Console.Error.WriteLine(" winapp new --list");
+ return 1;
+ default:
+ Console.Error.WriteLine($"mur templates: unknown subcommand '{sub}'.");
+ Console.Error.WriteLine();
+ ShowHelp();
+ return 1;
+ }
+ }
+
+ static void ShowHelp()
+ {
+ Console.WriteLine("Usage: mur templates status");
+ Console.WriteLine();
+ Console.WriteLine($"Reports whether `dotnet new {WinAppSdkTemplates.BlankShortName}` resolves, i.e. whether");
+ Console.WriteLine($"{WinAppSdkTemplates.PackageId} is installed and carries the Reactor templates.");
+ Console.WriteLine();
+ Console.WriteLine("Exit codes:");
+ Console.WriteLine($" {StatusExit.Available} `dotnet new {WinAppSdkTemplates.BlankShortName}` resolves");
+ Console.WriteLine($" {StatusExit.ProbeFailed} the `dotnet new` template engine could not be enumerated");
+ Console.WriteLine($" {StatusExit.InstalledButUnusable} the pack is installed but does not carry the Reactor templates");
+ Console.WriteLine($" {StatusExit.NotInstalled} the pack is not installed");
+ Console.WriteLine();
+ Console.WriteLine("To install the pack, use the Windows App SDK CLI:");
+ Console.WriteLine($" winapp new -t {WinAppSdkTemplates.BlankShortName} -n MyApp # installs on demand, then scaffolds");
+ Console.WriteLine(" winapp new --list # installs on demand, scaffolds nothing");
+ }
+
+ ///
+ /// `mur templates status` exit codes. Three distinct situations that all mean
+ /// "cannot scaffold" but call for different remediation, so callers (bootstrap
+ /// especially) must not collapse them into one message.
+ ///
+ internal static class StatusExit
+ {
+ /// `dotnet new reactor` resolves.
+ public const int Available = 0;
+ /// The template engine could not be enumerated at all.
+ public const int ProbeFailed = 1;
+ /// The pack is installed but does not carry the Reactor templates.
+ public const int InstalledButUnusable = 2;
+ /// The pack is not installed.
+ public const int NotInstalled = 3;
+ }
+
+ ///
+ /// Maps the two probes to a code. Pure, so the
+ /// distinction bootstrap.ps1 branches on is testable without a machine.
+ ///
+ /// null when the template engine could not be enumerated.
+ /// null when the installed-package list could not be read.
+ internal static int StatusExitCode(bool? available, bool? packageInstalled)
+ {
+ if (available is null) return StatusExit.ProbeFailed;
+ if (available.Value) return StatusExit.Available;
+ // Absent short name: is the pack there at all? "Installed but too old" and
+ // "never installed" need different advice, and an unreadable package list
+ // is a probe failure rather than either.
+ if (packageInstalled is null) return StatusExit.ProbeFailed;
+ return packageInstalled.Value ? StatusExit.InstalledButUnusable : StatusExit.NotInstalled;
+ }
+
+ static int Status()
+ {
+ // Report the question that matters — "can I scaffold?" — not merely
+ // whether the package id appears in the installed list.
+ var available = WinAppSdkTemplates.AreTemplatesAvailable();
+ var packageInstalled = available == false ? WinAppSdkTemplates.IsPackageInstalled() : null;
+ var exitCode = StatusExitCode(available, packageInstalled);
+ var version = exitCode == StatusExit.ProbeFailed ? null : WinAppSdkTemplates.GetInstalledVersion();
+
+ switch (exitCode)
+ {
+ case StatusExit.Available:
+ Console.WriteLine(version is null
+ ? $"`dotnet new {WinAppSdkTemplates.BlankShortName}` is available."
+ : $"`dotnet new {WinAppSdkTemplates.BlankShortName}` is available ({WinAppSdkTemplates.PackageId} {version}).");
+ break;
+
+ case StatusExit.InstalledButUnusable:
+ Console.WriteLine(
+ $"{WinAppSdkTemplates.PackageId} {version ?? "(unknown)"} is installed, but it does not provide " +
+ $"`dotnet new {WinAppSdkTemplates.BlankShortName}`. Update it with `winapp new --list`.");
+ break;
+
+ case StatusExit.NotInstalled:
+ Console.WriteLine(
+ $"{WinAppSdkTemplates.PackageId} is NOT installed. Install it with `winapp new --list`, " +
+ $"or scaffold directly with `winapp new -t {WinAppSdkTemplates.BlankShortName} -n MyApp`.");
+ break;
+
+ default:
+ Console.Error.WriteLine("mur templates status: could not enumerate `dotnet new` templates.");
+ break;
+ }
+
+ return exitCode;
+ }
+}
diff --git a/src/Reactor.Cli/Templates/WinAppSdkTemplates.cs b/src/Reactor.Cli/Templates/WinAppSdkTemplates.cs
new file mode 100644
index 000000000..f3394fb4f
--- /dev/null
+++ b/src/Reactor.Cli/Templates/WinAppSdkTemplates.cs
@@ -0,0 +1,273 @@
+// The Windows App SDK `dotnet new` template pack, which is where Reactor's app
+// templates now live.
+//
+// Reactor used to ship its own `Microsoft.UI.Reactor.ProjectTemplates` pack
+// (`dotnet new reactorapp`, unpackaged). The Windows App SDK template pack now
+// carries first-class Reactor templates alongside the WinUI 3 XAML ones. The
+// legacy pack's source has been deleted from this repo and it is no longer
+// built or published — the versions already on NuGet.org are all that remain,
+// and they are deprecated.
+//
+// Two user-visible differences from the legacy `reactorapp` template:
+// • the short name is `reactor` (plus `reactor-mvu` / `reactor-navview` /
+// `reactor-tabview` for the richer shells), and
+// • scaffolded apps are **packaged** (single-project MSIX) rather than
+// unpackaged, so `dotnet run` launches them with package identity.
+//
+// This type only *probes* — it answers "is the pack installed?" and "will
+// `dotnet new reactor` resolve?" for `mur doctor` and `mur templates status`.
+// Installing is the Windows App SDK CLI's job: `winapp new` installs the pack
+// on demand and scaffolds in one step. Reactor used to carry its own installer
+// (`mur templates install`) because `dotnet new install` has no `--prerelease`
+// switch and resolves stable-only, which fails while the pack is prerelease-only;
+// working around that meant resolving versions off the NuGet flat container and
+// tiptoeing around `dotnet new install --force`, which uninstalls the existing
+// package *before* downloading the replacement. `winapp` handles all of it, so
+// that machinery is gone.
+
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.UI.Reactor.Cli.Templates;
+public static class WinAppSdkTemplates
+{
+ /// NuGet id of the template pack that ships the Reactor templates.
+ public const string PackageId = "Microsoft.WindowsAppSDK.WinUI.CSharp.Templates";
+
+ /// `dotnet new` short name of the blank Reactor template.
+ public const string BlankShortName = "reactor";
+
+ ///
+ /// The canonical `dotnet new` short name of each Reactor template — one per
+ /// template, for scaffold guidance.
+ ///
+ ///
+ /// Deliberately not every registered short name. Each template also carries a
+ /// winui--prefixed alias (and the blank one a reactor-blank
+ /// alias), so the pack registers nine names for four templates. Listing all
+ /// nine as "scaffold an app with" would be noise; callers that need to
+ /// recognise an arbitrary alias should match the listing, not this array.
+ ///
+ public static readonly string[] ShortNames =
+ [
+ "reactor",
+ "reactor-mvu",
+ "reactor-navview",
+ "reactor-tabview",
+ ];
+
+ ///
+ /// True when the template *package* is registered with the `dotnet new`
+ /// engine. Returns null when the installed-package list could not be
+ /// enumerated at all (no `dotnet` on PATH, engine error) so callers can
+ /// distinguish "definitely missing" from "couldn't tell".
+ ///
+ ///
+ /// This answers "is the pack installed?", NOT "can I run `dotnet new
+ /// reactor`?" — the pack shipped versions (e.g. 0.0.6-alpha) that predate
+ /// the Reactor templates, so it can be installed and still not provide
+ /// them. Use for the user-facing
+ /// question; this one exists to decide whether an install would be
+ /// replacing something.
+ ///
+ public static bool? IsPackageInstalled()
+ {
+ // `dotnet new uninstall` with no arguments lists installed template
+ // *packages* by id. `dotnet new list` only shows template short names,
+ // which can't tell our legacy `reactorapp` pack apart from the Windows
+ // App SDK one when both happen to be installed.
+ var output = RunCapture("new", "uninstall");
+ if (output is null) return null;
+ return InterpretPackageInstalledOutput(output);
+ }
+
+ ///
+ /// Whether this pack's id appears as a listed package in `dotnet new uninstall`
+ /// output. Split out (and internal) so the matching rule is testable.
+ ///
+ ///
+ /// Matches the id line exactly rather than searching the transcript: a
+ /// substring hit also fires on a longer id that merely contains ours, and on
+ /// the uninstall hint lines that echo the id. Either would report an old pack
+ /// as installed and send the caller to the wrong remediation.
+ ///
+ internal static bool InterpretPackageInstalledOutput(string output) =>
+ output
+ .Split('\n')
+ .Any(line => string.Equals(line.Trim(), PackageId, StringComparison.OrdinalIgnoreCase));
+
+ ///
+ /// True when `dotnet new reactor` will actually resolve — i.e. the blank
+ /// Reactor template short name is registered. Returns null when the
+ /// template engine could not be queried at all.
+ ///
+ ///
+ /// Checking the package id alone is a false PASS: `dotnet new list reactor`
+ /// reports "No templates found" against an installed-but-too-old pack, so a
+ /// package-only probe tells a developer they're ready to scaffold when the
+ /// very next command fails. Ask the engine the question the user cares about.
+ ///
+ public static bool? AreTemplatesAvailable()
+ {
+ // `dotnet new list ` exits 103 and prints "No templates found
+ // matching" when nothing matches. Any *other* non-zero exit is an engine
+ // or SDK failure, not an answer — reporting it as "definitely missing"
+ // sends `mur doctor` and bootstrap to the wrong remediation.
+ var (output, exitCode) = RunCaptureWithExit("new", "list", BlankShortName);
+ if (output is null) return null;
+ return InterpretTemplateListOutput(output, exitCode);
+ }
+
+ ///
+ /// Interprets `dotnet new list reactor` output. Split out (and internal) so
+ /// the rule is unit-testable without shelling out to the template engine.
+ ///
+ ///
+ /// The template engine's exit code. 0 is a listing; 103 is its documented
+ /// "no templates matched". Anything else is a failure we cannot interpret,
+ /// so the answer is null ("couldn't tell") rather than false.
+ ///
+ internal static bool? InterpretTemplateListOutput(string output, int exitCode)
+ {
+ // The negative marker is authoritative when the engine reported a
+ // no-match, and is checked first because the message repeats the search
+ // term (and prints a `dotnet new search reactor` hint), so a naive
+ // short-name match reports the template as present exactly when absent.
+ if (output.Contains("No templates found", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ // A non-zero exit without that marker is an engine/SDK error: say
+ // "couldn't tell" instead of inventing a definite answer from its stderr.
+ if (exitCode != 0 && exitCode != NoTemplatesFoundExitCode)
+ return null;
+
+ return MatchesBlankShortName(output);
+ }
+
+ ///
+ /// `dotnet new list` exit code for "no templates matched the input" — the one
+ /// non-zero result that is an answer rather than a failure.
+ ///
+ internal const int NoTemplatesFoundExitCode = 103;
+
+ ///
+ /// Whether the listing registers as a short name.
+ ///
+ internal static bool MatchesBlankShortName(string output)
+ {
+ // Match the short name as a whole token. A plain Contains (or a \b regex)
+ // also matches `reactor-mvu` and `winui-reactor`, because '-' is a word
+ // boundary — so a listing that has the richer shells but not the blank
+ // template would be read as success. Short names are comma-separated
+ // within a whitespace-delimited column, so require one of those delimiters
+ // on each side.
+ //
+ // Deliberately case-sensitive: the Template Name column carries the
+ // capitalised prose word ("Reactor MVU App"), which is a standalone token
+ // and would match case-insensitively even when the blank template is
+ // absent. Short names are lowercase and BlankShortName is a constant.
+ return Regex.IsMatch(output, BlankShortNameTokenPattern);
+ }
+
+ ///
+ /// as a whole token: preceded and followed by a
+ /// comma, whitespace, or the edge of the text.
+ ///
+ private static readonly string BlankShortNameTokenPattern =
+ @"(?
+ /// The installed version of the template pack, or null when it isn't
+ /// installed (or the listing couldn't be read).
+ ///
+ public static string? GetInstalledVersion()
+ {
+ var output = RunCapture("new", "uninstall");
+ return output is null ? null : InterpretInstalledVersionOutput(output);
+ }
+
+ ///
+ /// Pulls this pack's version out of `dotnet new uninstall` output. Split out
+ /// (and internal) so the parser is testable against captured CLI text without
+ /// invoking the template engine.
+ ///
+ internal static string? InterpretInstalledVersionOutput(string output)
+ {
+ // The listing indents each package id, then its metadata:
+ // Microsoft.WindowsAppSDK.WinUI.CSharp.Templates
+ // Version: 0.0.6-alpha
+ // Several packages can be listed, so match the id line exactly rather than
+ // by substring — other ids legitimately contain this one as a prefix.
+ var lines = output.Replace("\r\n", "\n").Split('\n');
+ for (var i = 0; i < lines.Length; i++)
+ {
+ if (!lines[i].Trim().Equals(PackageId, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ // The package id sits at one indent level and its metadata deeper, so
+ // a line indented no further than the header starts the *next* package.
+ // Stopping there matters: without it, a package with no `Version:` line
+ // borrows the next package's version, which is worse than reporting
+ // nothing — `mur doctor` would name a version that isn't installed.
+ var headerIndent = IndentOf(lines[i]);
+
+ for (var j = i + 1; j < lines.Length && j <= i + 4; j++)
+ {
+ if (lines[j].Trim().Length == 0) continue;
+ if (IndentOf(lines[j]) <= headerIndent) break;
+
+ var trimmed = lines[j].Trim();
+ if (trimmed.StartsWith("Version:", StringComparison.OrdinalIgnoreCase))
+ return trimmed["Version:".Length..].Trim();
+ }
+ return null;
+ }
+ return null;
+ }
+
+ static int IndentOf(string line) => line.Length - line.TrimStart().Length;
+
+ static string? RunCapture(params string[] arguments) => RunCaptureWithExit(arguments).Output;
+
+ ///
+ /// Runs `dotnet ` and returns its combined output
+ /// plus exit code. Output is null when the process could not be started.
+ ///
+ ///
+ /// The exit code is returned rather than swallowed because `dotnet new list`
+ /// uses it to distinguish "no templates matched" (103) from an engine
+ /// failure, and collapsing the two turns a probe failure into a confident
+ /// wrong answer. `dotnet new uninstall` meanwhile exits non-zero when nothing
+ /// is installed while still printing a usable listing, so callers that only
+ /// want the text keep ignoring it.
+ ///
+ static (string? Output, int ExitCode) RunCaptureWithExit(params string[] arguments)
+ {
+ var psi = new ProcessStartInfo("dotnet")
+ {
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+ foreach (var a in arguments) psi.ArgumentList.Add(a);
+
+ try
+ {
+ using var proc = Process.Start(psi);
+ if (proc is null) return (null, -1);
+ // Drain both pipes concurrently. Reading stdout to the end first lets
+ // `dotnet new` fill the unread stderr pipe and block before it exits —
+ // a deadlock, not a slow path. CheckCommand documents the same hazard.
+ var stdoutTask = proc.StandardOutput.ReadToEndAsync();
+ var stderrTask = proc.StandardError.ReadToEndAsync();
+ global::System.Threading.Tasks.Task.WaitAll(stdoutTask, stderrTask);
+ proc.WaitForExit();
+ return (stdoutTask.Result + stderrTask.Result, proc.ExitCode);
+ }
+ catch (Exception ex) when (ex is Win32Exception or InvalidOperationException or IOException)
+ {
+ return (null, -1);
+ }
+ }
+}
diff --git a/src/Reactor.Cli/Upgrade/UpgradeCommand.cs b/src/Reactor.Cli/Upgrade/UpgradeCommand.cs
index 507121422..8fbf8f419 100644
--- a/src/Reactor.Cli/Upgrade/UpgradeCommand.cs
+++ b/src/Reactor.Cli/Upgrade/UpgradeCommand.cs
@@ -1,10 +1,11 @@
// `mur upgrade` — refresh a Reactor developer install after `git pull`.
//
// Re-runs the source-side steps of bootstrap.ps1:
-// 1. Re-pack the framework + ProjectTemplates into local-nupkgs/
+// 1. Re-pack the framework packages into local-nupkgs/
// (delegates to `mur pack-local`).
-// 2. Reinstall the `dotnet new reactorapp` template (uninstall first so the
-// template engine drops its cached copy).
+// 2. Make sure the Windows App SDK `dotnet new` template pack (which ships
+// `dotnet new reactor`) is installed — self-healing for a checkout that
+// was bootstrapped before the templates moved there.
// 3. Refresh the Claude Code plugin install.
// 4. Rebuild + reinstall the Reactor VS preview extension (best-effort —
// skipped if VS / the VSIX-dev workload aren't installed, same probe
@@ -17,6 +18,7 @@
using System.Diagnostics;
using Microsoft.UI.Reactor.Cli.Pack;
+using Microsoft.UI.Reactor.Cli.Templates;
namespace Microsoft.UI.Reactor.Cli.Upgrade;
@@ -35,8 +37,8 @@ public static int Run(string[] args)
return 1;
}
- // 1. Re-pack framework + templates.
- Console.WriteLine("==> Repacking Microsoft.UI.Reactor + ProjectTemplates");
+ // 1. Re-pack framework packages.
+ Console.WriteLine("==> Repacking Microsoft.UI.Reactor packages");
var rc = PackLocalCommand.Run(Array.Empty());
if (rc != 0)
{
@@ -44,25 +46,30 @@ public static int Run(string[] args)
return rc;
}
- // 2. Reinstall the dotnet new template. Uninstall first so the template
- // engine drops the cached version by id (the installer otherwise wins
- // against a same-id repack — see getting-started.md caveat).
+ // 2. Report on the `dotnet new reactor` templates. They ship in the Windows
+ // App SDK template pack rather than being built from this checkout, so
+ // `git pull` never invalidates them and there is nothing to refresh here.
+ // Installing them is `winapp`'s job, so this only probes and points.
Console.WriteLine();
- Console.WriteLine("==> Reinstalling `dotnet new reactorapp` template");
- var feed = Path.Combine(repoRoot, "local-nupkgs");
- var templateNupkg = Path.Combine(feed, $"Microsoft.UI.Reactor.ProjectTemplates.{PackLocalCommand.DefaultLocalVersion}.nupkg");
- if (!File.Exists(templateNupkg))
+ Console.WriteLine($"==> Checking `dotnet new {WinAppSdkTemplates.BlankShortName}` templates ({WinAppSdkTemplates.PackageId})");
+ switch (WinAppSdkTemplates.AreTemplatesAvailable())
{
- Console.Error.WriteLine($"mur upgrade: template nupkg not found at {templateNupkg} after pack-local.");
- return 1;
- }
- // Uninstall is best-effort: non-zero exit just means it wasn't installed.
- RunDotnet(repoRoot, ignoreExitCode: true, "new", "uninstall", CleanLocalCommand.TemplatePackageId);
- rc = RunDotnet(repoRoot, ignoreExitCode: false, "new", "install", templateNupkg);
- if (rc != 0)
- {
- Console.Error.WriteLine("mur upgrade: template install failed.");
- return rc;
+ case true:
+ Console.WriteLine(
+ $" `dotnet new {WinAppSdkTemplates.BlankShortName}` is available " +
+ $"({WinAppSdkTemplates.PackageId} {WinAppSdkTemplates.GetInstalledVersion() ?? "(unknown)"}).");
+ break;
+ case false:
+ // Best-effort: a developer who scaffolds by hand shouldn't have
+ // `mur upgrade` fail over a template pack it no longer manages.
+ Console.Error.WriteLine(
+ $" `dotnet new {WinAppSdkTemplates.BlankShortName}` is not available. Install the pack with " +
+ $"`winapp new --list`, or scaffold directly with " +
+ $"`winapp new -t {WinAppSdkTemplates.BlankShortName} -n MyApp`.");
+ break;
+ default:
+ Console.Error.WriteLine(" Could not enumerate `dotnet new` templates; skipping the check.");
+ break;
}
// 3. Refresh Claude plugin (best-effort; not every user has Claude Code).
@@ -129,6 +136,7 @@ public static int Run(string[] args)
Console.WriteLine();
Console.WriteLine("Upgrade complete.");
Console.WriteLine();
+ var feed = Path.Join(repoRoot, "local-nupkgs");
Console.WriteLine(" To bump `mur` itself (which can't update its own running process), run:");
Console.WriteLine($" dotnet tool update -g --add-source \"{feed}\" Microsoft.UI.Reactor.Cli");
Console.WriteLine(" Or just re-run ./bootstrap.ps1 from the repo root.");
@@ -278,39 +286,6 @@ static void TryReinstallVsExtension(string repoRoot)
return null;
}
- static int RunDotnet(string workingDirectory, bool ignoreExitCode, params string[] arguments)
- {
- var psi = new ProcessStartInfo("dotnet")
- {
- UseShellExecute = false,
- WorkingDirectory = workingDirectory,
- };
- foreach (var a in arguments) psi.ArgumentList.Add(a);
-
- Process? proc;
- try
- {
- proc = Process.Start(psi);
- }
- catch (Exception ex)
- {
- if (ignoreExitCode) return 0;
- Console.Error.WriteLine($"mur upgrade: failed to start `dotnet {string.Join(' ', arguments)}`: {ex.Message}");
- Console.Error.WriteLine(" Verify .NET 10+ is installed and `dotnet` resolves on PATH.");
- return 1;
- }
- if (proc is null)
- {
- if (ignoreExitCode) return 0;
- Console.Error.WriteLine($"mur upgrade: `dotnet {string.Join(' ', arguments)}` did not start (Process.Start returned null).");
- return 1;
- }
- using (proc)
- {
- proc.WaitForExit();
- return ignoreExitCode ? 0 : proc.ExitCode;
- }
- }
static void CopyDirectory(string src, string dst)
{
diff --git a/src/Reactor.Devtools/README.md b/src/Reactor.Devtools/README.md
index fdf574840..eb12eb79d 100644
--- a/src/Reactor.Devtools/README.md
+++ b/src/Reactor.Devtools/README.md
@@ -71,7 +71,6 @@ The devtools surface is **developer-loop only** and ships with hard gates:
- [`Microsoft.UI.Reactor`](https://www.nuget.org/packages/Microsoft.UI.Reactor) — the core declarative WinUI 3 framework (required).
- [`Microsoft.UI.Reactor.Advanced`](https://www.nuget.org/packages/Microsoft.UI.Reactor.Advanced) — optional Win2D/graphics components.
-- [`Microsoft.UI.Reactor.ProjectTemplates`](https://www.nuget.org/packages/Microsoft.UI.Reactor.ProjectTemplates) — `dotnet new` templates.
## Feedback & Contributing
diff --git a/src/Reactor/README.md b/src/Reactor/README.md
index 42284b416..be926cb45 100644
--- a/src/Reactor/README.md
+++ b/src/Reactor/README.md
@@ -107,7 +107,6 @@ From here, the same model scales to real apps. The [getting started guide](https
- [`Microsoft.UI.Reactor.Advanced`](https://www.nuget.org/packages/Microsoft.UI.Reactor.Advanced) — optional components with heavier native/graphics dependencies (Win2D canvas, charts).
- [`Microsoft.UI.Reactor.Devtools`](https://www.nuget.org/packages/Microsoft.UI.Reactor.Devtools) — optional developer-loop devtools host (live tree inspection, hot reload, preview).
-- [`Microsoft.UI.Reactor.ProjectTemplates`](https://www.nuget.org/packages/Microsoft.UI.Reactor.ProjectTemplates) — `dotnet new` templates for scaffolding Reactor apps.
## Feedback & Contributing
diff --git a/tests/Reactor.DocPipeline.Tests/MinimalCsprojDocTests.cs b/tests/Reactor.DocPipeline.Tests/MinimalCsprojDocTests.cs
index 103e97780..47afa384c 100644
--- a/tests/Reactor.DocPipeline.Tests/MinimalCsprojDocTests.cs
+++ b/tests/Reactor.DocPipeline.Tests/MinimalCsprojDocTests.cs
@@ -105,27 +105,24 @@ public void Documented_csproj_declares_required_winui_properties(string element,
}
[Fact]
- public void Documented_package_id_matches_the_scaffolded_template()
+ public void Documented_package_id_is_the_framework_package()
{
- // Cross-check against the real `dotnet new reactorapp` template, which
- // the page names as the fast path. If the two ever disagree on which
- // package to reference, one of them is lying to the reader.
- var repoRoot = FindRepoRoot();
- var templateCsproj = global::System.IO.Path.Join(
- repoRoot, "tools", "Templates", "templates", "WinUIApp-CSharp", "Company.ReactorApp1.csproj");
-
- Assert.True(global::System.IO.File.Exists(templateCsproj),
- $"Expected the scaffolded template at {templateCsproj}; if it moved, update this test.");
+ // The page names `dotnet new reactor` as the fast path, but those
+ // templates now live in the Windows App SDK pack and are not present in
+ // this repo, so there is no in-repo csproj to cross-check against. Pin
+ // the next best invariant: the hand-authored block documents the real
+ // framework package id, which is what a reader copies.
+ var doc = global::System.Xml.Linq.XDocument.Parse(ExtractMinimalSetupCsproj());
- var scaffoldIds = global::System.Xml.Linq.XDocument.Load(templateCsproj)
- .Descendants("PackageReference")
+ var documentedIds = doc.Descendants("PackageReference")
.Select(p => p.Attribute("Include")?.Value)
+ .Where(id => id is not null)
.ToList();
- // Positive control: the scaffold really does carry package references,
- // so a miss below is a disagreement and not an empty parse.
- Assert.NotEmpty(scaffoldIds);
- Assert.Contains(PackageId, scaffoldIds);
+ // Positive control: the block really does carry package references, so a
+ // miss below is a disagreement and not an empty parse.
+ Assert.NotEmpty(documentedIds);
+ Assert.Contains(PackageId, documentedIds);
}
[Fact]
diff --git a/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs b/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs
deleted file mode 100644
index 9f229b402..000000000
--- a/tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs
+++ /dev/null
@@ -1,376 +0,0 @@
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Text;
-using Xunit;
-using Xunit.Sdk;
-
-namespace Microsoft.UI.Reactor.IntegrationTests.Packaging;
-
-[Collection(LocalPackageFeedCollection.Name)]
-public sealed class CreateTemplateTests : IDisposable
-{
- private readonly TemplatePackageTestFixture _fixture;
- private readonly string _tempRoot = Path.Combine(Path.GetTempPath(), $"reactor-template-smoke-{Guid.NewGuid():N}");
-
- public CreateTemplateTests(TemplatePackageTestFixture fixture)
- {
- _fixture = fixture;
- Directory.CreateDirectory(_tempRoot);
- }
-
- [Theory]
- [InlineData(false)]
- [InlineData(true)]
- public void CreateAndRunReactorappTemplate(bool useProgramMain)
- {
- var scenarioName = useProgramMain ? "program-main" : "top-level";
- var appDir = CreateDirectory($"generated-app-{scenarioName}");
- var projectName = CreateProjectName(useProgramMain);
-
- CreateNuGetConfig(appDir, _fixture.PackageSourceDir, _fixture.NugetPackagesDir, _fixture.CommandEnvironment);
-
- RunHelpers.RunDotnet(
- $"new reactorapp --debug:custom-hive \"{_fixture.TemplateHiveDir}\" --use-program-main {useProgramMain.ToString().ToLowerInvariant()} --name {projectName} --output \"{appDir}\" --force",
- _fixture.RepoRoot,
- _fixture.CommandEnvironment,
- timeoutMs: 180_000);
-
- var projectPath = Path.Combine(appDir, $"{projectName}.csproj");
- Assert.True(File.Exists(projectPath), $"Expected generated project at '{projectPath}'.");
-
- RunHelpers.RunDotnet(
- $"build -a {_fixture.RunArchitecture}",
- appDir,
- _fixture.CommandEnvironment,
- timeoutMs: 300_000);
-
- RunDotnetRun(
- appDir,
- projectName,
- _fixture.RunArchitecture,
- configuration: null,
- _fixture.CommandEnvironment,
- timeoutMs: 120_000);
-
- RunDotnetRun(
- appDir,
- projectName,
- _fixture.RunArchitecture,
- configuration: "Release",
- _fixture.CommandEnvironment,
- timeoutMs: 120_000);
- }
-
- public void Dispose()
- {
- try
- {
- if (Directory.Exists(_tempRoot))
- {
- Directory.Delete(_tempRoot, recursive: true);
- }
- }
- catch
- {
- // Best-effort cleanup for temporary smoke-test artifacts.
- }
- }
-
- private string CreateDirectory(string name)
- {
- var path = Path.Combine(_tempRoot, name);
- Directory.CreateDirectory(path);
- return path;
- }
-
- private static void CreateNuGetConfig(
- string workingDirectory,
- string packageSourceDir,
- string nugetPackagesDir,
- IReadOnlyDictionary environmentVariables)
- {
- RunHelpers.RunDotnet(
- $"new nugetconfig --output \"{workingDirectory}\" --force",
- workingDirectory,
- environmentVariables,
- timeoutMs: 30_000);
-
- var configPath = Path.Combine(workingDirectory, "nuget.config");
-
- RunHelpers.RunDotnet(
- $"nuget config set globalPackagesFolder \"{nugetPackagesDir}\" --configfile \"{configPath}\"",
- workingDirectory,
- environmentVariables,
- timeoutMs: 30_000);
-
- RunHelpers.RunDotnet(
- $"nuget add source \"{packageSourceDir}\" --name reactor-local --configfile \"{configPath}\"",
- workingDirectory,
- environmentVariables,
- timeoutMs: 30_000);
- }
-
- private static string CreateProjectName(bool useProgramMain)
- {
- var prefix = useProgramMain ? "ReactorProgMain" : "ReactorTopLevel";
- return $"{prefix}{Guid.NewGuid():N}".Substring(0, 28);
- }
-
- private static void RunDotnetRun(
- string workingDirectory,
- string projectName,
- string architecture,
- string? configuration,
- IReadOnlyDictionary environmentVariables,
- int timeoutMs)
- {
- var stdout = new StringBuilder();
- var stderr = new StringBuilder();
- var outputLock = new object();
- var sawChildProcess = false;
- string? lastUiDetails = null;
-
- var configurationArg = configuration is null ? string.Empty : $" -c {configuration}";
- using var process = RunHelpers.CreateProcess("dotnet", $"run{configurationArg} -a {architecture}", workingDirectory, environmentVariables);
- process.OutputDataReceived += (_, args) =>
- {
- if (args.Data is null)
- {
- return;
- }
-
- lock (outputLock)
- {
- stdout.AppendLine(args.Data);
- }
- };
- process.ErrorDataReceived += (_, args) =>
- {
- if (args.Data is null)
- {
- return;
- }
-
- lock (outputLock)
- {
- stderr.AppendLine(args.Data);
- }
- };
-
- if (!process.Start())
- {
- throw new XunitException("Failed to start 'dotnet run'.");
- }
-
- process.BeginOutputReadLine();
- process.BeginErrorReadLine();
-
- try
- {
- var deadline = DateTime.UtcNow + TimeSpan.FromMilliseconds(timeoutMs);
- while (DateTime.UtcNow < deadline)
- {
- if (process.HasExited)
- {
- break;
- }
-
- var launchedProcess = Process.GetProcessesByName(projectName)
- .FirstOrDefault(candidate => !candidate.HasExited);
- if (launchedProcess != null)
- {
- sawChildProcess = true;
- var uiState = UiaHelpers.FindUIALement(launchedProcess, "NameInput", out var uiDetails);
- lastUiDetails = uiDetails;
- launchedProcess.Dispose();
-
- if (uiState == UiaHelpers.UIAFindResult.Found)
- {
- return;
- }
-
- if (uiState == UiaHelpers.UIAFindResult.NotFound)
- {
- throw new XunitException(
- $"Generated app showed Reactor's render-error fallback instead of the expected template UI.{Environment.NewLine}" +
- $"Working directory: {workingDirectory}{Environment.NewLine}" +
- $"UI Automation details: {uiDetails}{Environment.NewLine}" +
- RunHelpers.FormatCommandOutput(stdout.ToString(), stderr.ToString()));
- }
- }
-
- Thread.Sleep(500);
- }
-
- if (process.HasExited)
- {
- process.WaitForExit();
- throw new XunitException(
- $"Command failed: dotnet run{configurationArg} -a {architecture}{Environment.NewLine}" +
- $"Exit code: {process.ExitCode}{Environment.NewLine}" +
- $"Working directory: {workingDirectory}{Environment.NewLine}" +
- RunHelpers.FormatCommandOutput(stdout.ToString(), stderr.ToString()));
- }
-
- throw new XunitException(
- $"Timed out waiting for '{projectName}.exe' to start from 'dotnet run{configurationArg} -a {architecture}'. " +
- $"Child process observed: {sawChildProcess}.{Environment.NewLine}" +
- $"UI Automation details: {lastUiDetails ?? "None captured."}{Environment.NewLine}" +
- $"Working directory: {workingDirectory}{Environment.NewLine}" +
- RunHelpers.FormatCommandOutput(stdout.ToString(), stderr.ToString()));
- }
- finally
- {
- RunHelpers.TryKillProcessTree(process);
- foreach (var launchedProcess in Process.GetProcessesByName(projectName))
- {
- try
- {
- if (!launchedProcess.HasExited)
- {
- launchedProcess.Kill(entireProcessTree: true);
- launchedProcess.WaitForExit(5_000);
- }
- }
- catch
- {
- // Best-effort cleanup for unique smoke-test app names.
- }
- finally
- {
- launchedProcess.Dispose();
- }
- }
- }
- }
-}
-
-public sealed class TemplatePackageTestFixture : IDisposable
-{
- private readonly string _tempRoot = Path.Combine(Path.GetTempPath(), $"reactor-template-packages-{Guid.NewGuid():N}");
-
- public TemplatePackageTestFixture()
- {
- Directory.CreateDirectory(_tempRoot);
-
- RepoRoot = FindRepoRoot();
- var packageSuffix = Guid.NewGuid().ToString("N")[..12];
- PackageVersion = $"0.0.0-template-smoke-{packageSuffix}";
- PackageSourceDir = CreateDirectory("packages");
- NugetPackagesDir = CreateDirectory("nuget-global-packages");
- var nugetHttpCacheDir = CreateDirectory("nuget-http-cache");
- var dotnetCliHomeDir = CreateDirectory("dotnet-home");
- TemplateHiveDir = CreateDirectory("template-hive");
- RunArchitecture = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
- CommandEnvironment = CreateCommandEnvironment(dotnetCliHomeDir, nugetHttpCacheDir);
-
- RunHelpers.RunDotnet(
- $"pack \"{Path.Combine(RepoRoot, "src", "Reactor", "Reactor.csproj")}\" --no-restore --configuration Release -o \"{PackageSourceDir}\" -p:Version={PackageVersion}",
- RepoRoot,
- CommandEnvironment,
- timeoutMs: 300_000);
- // The template's Debug-only ItemGroup also references Microsoft.UI.Reactor.Devtools
- // (so the devtools menu lights up in Debug + the VS embedded-preview extension works).
- // Pack it here too so the post-scaffold `dotnet build` finds the matching version on
- // the local feed instead of trying NuGet.org.
- RunHelpers.RunDotnet(
- $"pack \"{Path.Combine(RepoRoot, "src", "Reactor.Devtools", "Reactor.Devtools.csproj")}\" --no-restore --configuration Release -o \"{PackageSourceDir}\" -p:Version={PackageVersion}",
- RepoRoot,
- CommandEnvironment,
- timeoutMs: 300_000);
- // Track B (spec-062): Microsoft.UI.Reactor.Devtools now depends on
- // Microsoft.UI.Reactor.Advanced (its docking devtools use Advanced internals),
- // so the scaffolded app pulls Advanced transitively via its Debug ItemGroup.
- // Pack it here too so the post-scaffold `dotnet build` resolves the matching
- // smoke version from the local feed instead of falling back to NuGet.org
- // (which caused an NU1605 downgrade of Microsoft.UI.Reactor).
- RunHelpers.RunDotnet(
- $"pack \"{Path.Combine(RepoRoot, "src", "Reactor.Advanced", "Reactor.Advanced.csproj")}\" --no-restore --configuration Release -o \"{PackageSourceDir}\" -p:Version={PackageVersion}",
- RepoRoot,
- CommandEnvironment,
- timeoutMs: 300_000);
- RunHelpers.RunDotnet(
- $"pack \"{Path.Combine(RepoRoot, "tools", "Templates", "Microsoft.UI.Reactor.Templates.csproj")}\" --no-restore --configuration Release -o \"{PackageSourceDir}\" -p:Version={PackageVersion} -p:MicrosoftUIReactorVersion={PackageVersion} -p:Platform=AnyCPU",
- RepoRoot,
- CommandEnvironment,
- timeoutMs: 180_000);
-
- _ = FindPackage(PackageSourceDir, "Microsoft.UI.Reactor", PackageVersion);
- _ = FindPackage(PackageSourceDir, "Microsoft.UI.Reactor.Devtools", PackageVersion);
- _ = FindPackage(PackageSourceDir, "Microsoft.UI.Reactor.Advanced", PackageVersion);
- var templatePackage = FindPackage(PackageSourceDir, "Microsoft.UI.Reactor.ProjectTemplates", PackageVersion);
-
- RunHelpers.RunDotnet(
- $"new install --debug:custom-hive \"{TemplateHiveDir}\" \"{templatePackage}\"",
- RepoRoot,
- CommandEnvironment,
- timeoutMs: 120_000);
- }
-
- private static string FindPackage(string packageSourceDir, string packageId, string version)
- {
- var packagePath = Path.Combine(packageSourceDir, $"{packageId}.{version}.nupkg");
- Assert.True(File.Exists(packagePath), $"Expected package '{packagePath}' to exist.");
- return packagePath;
- }
-
- private static Dictionary CreateCommandEnvironment(string dotnetCliHomeDir, string nugetHttpCacheDir)
- {
- return new(StringComparer.OrdinalIgnoreCase)
- {
- ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "false",
- ["DOTNET_CLI_HOME"] = dotnetCliHomeDir,
- ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1",
- ["DOTNET_NOLOGO"] = "1",
- ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1",
- ["NUGET_HTTP_CACHE_PATH"] = nugetHttpCacheDir,
- };
- }
-
- public string RepoRoot { get; }
-
- public string PackageVersion { get; }
-
- public string PackageSourceDir { get; }
-
- public string NugetPackagesDir { get; }
-
- public string TemplateHiveDir { get; }
-
- public string RunArchitecture { get; }
-
- public IReadOnlyDictionary CommandEnvironment { get; }
-
- public void Dispose()
- {
- try
- {
- if (Directory.Exists(_tempRoot))
- {
- Directory.Delete(_tempRoot, recursive: true);
- }
- }
- catch
- {
- // Best-effort cleanup for shared package-setup artifacts.
- }
- }
-
- private string CreateDirectory(string name)
- {
- var path = Path.Combine(_tempRoot, name);
- Directory.CreateDirectory(path);
- return path;
- }
-
- private static string FindRepoRoot()
- {
- var dir = AppContext.BaseDirectory;
- while (dir != null && !File.Exists(Path.Combine(dir, "Reactor.slnx")))
- {
- dir = Path.GetDirectoryName(dir);
- }
-
- return dir ?? throw new DirectoryNotFoundException("Could not find repo root (Reactor.sln).");
- }
-}
diff --git a/tests/Reactor.IntegrationTests/Packaging/LocalPackageFeedCollection.cs b/tests/Reactor.IntegrationTests/Packaging/LocalPackageFeedCollection.cs
index 78d5b2387..5a0965713 100644
--- a/tests/Reactor.IntegrationTests/Packaging/LocalPackageFeedCollection.cs
+++ b/tests/Reactor.IntegrationTests/Packaging/LocalPackageFeedCollection.cs
@@ -13,7 +13,7 @@ namespace Microsoft.UI.Reactor.IntegrationTests.Packaging;
/// Sharing one fixture also means the ~2-minute pack happens once instead of per class.
///
[CollectionDefinition(Name, DisableParallelization = true)]
-public sealed class LocalPackageFeedCollection : ICollectionFixture
+public sealed class LocalPackageFeedCollection : ICollectionFixture
{
public const string Name = "LocalPackageFeed";
}
diff --git a/tests/Reactor.IntegrationTests/Packaging/LocalPackageFeedFixture.cs b/tests/Reactor.IntegrationTests/Packaging/LocalPackageFeedFixture.cs
new file mode 100644
index 000000000..0dfcf0b4c
--- /dev/null
+++ b/tests/Reactor.IntegrationTests/Packaging/LocalPackageFeedFixture.cs
@@ -0,0 +1,122 @@
+// Shared fixture that packs the Reactor framework packages into a throwaway
+// local NuGet feed, so consumer-facing integration tests can restore against a
+// real .nupkg instead of a project reference.
+//
+// It previously also packed and installed the in-repo
+// `Microsoft.UI.Reactor.ProjectTemplates` package and scaffolded from it. That
+// package was removed in favour of the Windows App SDK `dotnet new reactor`
+// templates, so the template-pack and template-hive plumbing is gone; what
+// remains is the local package feed itself.
+
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace Microsoft.UI.Reactor.IntegrationTests.Packaging;
+
+public sealed class LocalPackageFeedFixture : IDisposable
+{
+ private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"reactor-local-feed-{Guid.NewGuid():N}");
+
+ public LocalPackageFeedFixture()
+ {
+ Directory.CreateDirectory(_tempRoot);
+
+ RepoRoot = FindRepoRoot();
+ var packageSuffix = Guid.NewGuid().ToString("N")[..12];
+ PackageVersion = $"0.0.0-feed-smoke-{packageSuffix}";
+ PackageSourceDir = CreateDirectory("packages");
+ NugetPackagesDir = CreateDirectory("nuget-global-packages");
+ var nugetHttpCacheDir = CreateDirectory("nuget-http-cache");
+ var dotnetCliHomeDir = CreateDirectory("dotnet-home");
+ RunArchitecture = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
+ CommandEnvironment = CreateCommandEnvironment(dotnetCliHomeDir, nugetHttpCacheDir);
+
+ RunHelpers.RunDotnet(
+ $"pack \"{Path.Join(RepoRoot, "src", "Reactor", "Reactor.csproj")}\" --no-restore --configuration Release -o \"{PackageSourceDir}\" -p:Version={PackageVersion}",
+ RepoRoot,
+ CommandEnvironment,
+ timeoutMs: 300_000);
+ // Devtools depends on Advanced (spec-062), and consumers can pull both
+ // transitively, so pack all three at the same version to keep a restore
+ // from falling through to NuGet.org and hitting an NU1605 downgrade.
+ RunHelpers.RunDotnet(
+ $"pack \"{Path.Join(RepoRoot, "src", "Reactor.Devtools", "Reactor.Devtools.csproj")}\" --no-restore --configuration Release -o \"{PackageSourceDir}\" -p:Version={PackageVersion}",
+ RepoRoot,
+ CommandEnvironment,
+ timeoutMs: 300_000);
+ RunHelpers.RunDotnet(
+ $"pack \"{Path.Join(RepoRoot, "src", "Reactor.Advanced", "Reactor.Advanced.csproj")}\" --no-restore --configuration Release -o \"{PackageSourceDir}\" -p:Version={PackageVersion}",
+ RepoRoot,
+ CommandEnvironment,
+ timeoutMs: 300_000);
+
+ _ = FindPackage(PackageSourceDir, "Microsoft.UI.Reactor", PackageVersion);
+ _ = FindPackage(PackageSourceDir, "Microsoft.UI.Reactor.Devtools", PackageVersion);
+ _ = FindPackage(PackageSourceDir, "Microsoft.UI.Reactor.Advanced", PackageVersion);
+ }
+
+ private static string FindPackage(string packageSourceDir, string packageId, string version)
+ {
+ var packagePath = Path.Join(packageSourceDir, $"{packageId}.{version}.nupkg");
+ Assert.True(File.Exists(packagePath), $"Expected package '{packagePath}' to exist.");
+ return packagePath;
+ }
+
+ private static Dictionary CreateCommandEnvironment(string dotnetCliHomeDir, string nugetHttpCacheDir)
+ {
+ return new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "false",
+ ["DOTNET_CLI_HOME"] = dotnetCliHomeDir,
+ ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1",
+ ["DOTNET_NOLOGO"] = "1",
+ ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1",
+ ["NUGET_HTTP_CACHE_PATH"] = nugetHttpCacheDir,
+ };
+ }
+
+ public string RepoRoot { get; }
+
+ public string PackageVersion { get; }
+
+ public string PackageSourceDir { get; }
+
+ public string NugetPackagesDir { get; }
+
+ public string RunArchitecture { get; }
+
+ public IReadOnlyDictionary CommandEnvironment { get; }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(_tempRoot))
+ {
+ Directory.Delete(_tempRoot, recursive: true);
+ }
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ // Best-effort cleanup for shared package-setup artifacts.
+ }
+ }
+
+ private string CreateDirectory(string name)
+ {
+ var path = Path.Join(_tempRoot, name);
+ Directory.CreateDirectory(path);
+ return path;
+ }
+
+ private static string FindRepoRoot()
+ {
+ var dir = AppContext.BaseDirectory;
+ while (dir != null && !File.Exists(Path.Join(dir, "Reactor.slnx")))
+ {
+ dir = Path.GetDirectoryName(dir);
+ }
+
+ return dir ?? throw new DirectoryNotFoundException("Could not find repo root (Reactor.slnx).");
+ }
+}
diff --git a/tests/Reactor.IntegrationTests/Packaging/SourceMapPackageConsumerTests.cs b/tests/Reactor.IntegrationTests/Packaging/SourceMapPackageConsumerTests.cs
index bf2c12cd9..ba55fac4e 100644
--- a/tests/Reactor.IntegrationTests/Packaging/SourceMapPackageConsumerTests.cs
+++ b/tests/Reactor.IntegrationTests/Packaging/SourceMapPackageConsumerTests.cs
@@ -17,18 +17,17 @@ namespace Microsoft.UI.Reactor.IntegrationTests.Packaging;
/// Debug-only check passes if the generator is unconditionally on, and a Release-only
/// check passes if it is unconditionally off.
///
-/// Like its sibling this needs network access to
-/// restore the Windows App SDK, so it only runs where NuGet.org is reachable (CI's
-/// "Integration Tests" job). On a network-restricted machine both fail identically with
-/// NU1301 during restore.
+/// This needs network access to restore the Windows App SDK, so it only runs
+/// where NuGet.org is reachable (CI's "Integration Tests" job). On a
+/// network-restricted machine it fails with NU1301 during restore.
///
[Collection(LocalPackageFeedCollection.Name)]
public sealed class SourceMapPackageConsumerTests : IDisposable
{
- private readonly TemplatePackageTestFixture _fixture;
+ private readonly LocalPackageFeedFixture _fixture;
private readonly string _tempRoot = Path.Join(Path.GetTempPath(), $"reactor-sourcemap-pkg-{Guid.NewGuid():N}");
- public SourceMapPackageConsumerTests(TemplatePackageTestFixture fixture)
+ public SourceMapPackageConsumerTests(LocalPackageFeedFixture fixture)
{
_fixture = fixture;
Directory.CreateDirectory(_tempRoot);
diff --git a/tests/Reactor.PackagedTests.Host/Package.appxmanifest b/tests/Reactor.PackagedTests.Host/Package.appxmanifest
index 7987467f0..cdc294a33 100644
--- a/tests/Reactor.PackagedTests.Host/Package.appxmanifest
+++ b/tests/Reactor.PackagedTests.Host/Package.appxmanifest
@@ -8,6 +8,7 @@
+
+
diff --git a/tests/Reactor.Tests/PackLocalFrameworkVersionTests.cs b/tests/Reactor.Tests/PackLocalFrameworkVersionTests.cs
index 24250e392..127013315 100644
--- a/tests/Reactor.Tests/PackLocalFrameworkVersionTests.cs
+++ b/tests/Reactor.Tests/PackLocalFrameworkVersionTests.cs
@@ -126,24 +126,4 @@ public void ParseFlatContainerVersions_end_to_end_picks_latest()
Assert.Equal("0.1.0-preview.11", latest);
}
-
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- [InlineData(" ")]
- public void ResolveTemplateFrameworkVersion_returns_null_for_no_flag(string? arg)
- {
- // No --framework-version → leave the csproj default in place (no network).
- Assert.Null(PackLocalCommand.ResolveTemplateFrameworkVersion(arg));
- }
-
- [Fact]
- public void ResolveTemplateFrameworkVersion_passes_explicit_version_through()
- {
- // An explicit version is used verbatim and must not trigger a NuGet lookup.
- Assert.Equal("0.1.0-preview.7",
- PackLocalCommand.ResolveTemplateFrameworkVersion("0.1.0-preview.7"));
- Assert.Equal("0.1.0-preview.7",
- PackLocalCommand.ResolveTemplateFrameworkVersion(" 0.1.0-preview.7 "));
- }
}
diff --git a/tests/Reactor.Tests/TemplateMetadataTests.cs b/tests/Reactor.Tests/TemplateMetadataTests.cs
deleted file mode 100644
index 3f718f994..000000000
--- a/tests/Reactor.Tests/TemplateMetadataTests.cs
+++ /dev/null
@@ -1,202 +0,0 @@
-// Repository-content validation for project-template metadata.
-//
-// The bug this test was added against:
-// `tools/Templates/templates/WinUIApp-CSharp/.template.config/template.json`
-// shipped with `identity` = "Micrsoft.UI.Reactor.CSharp" (missing the
-// second 'o') from at least Phase 1 onward. The existing integration test
-// `CreateTemplateTests` did not catch this because it installs the
-// template into a per-test ephemeral hive via `--debug:custom-hive`,
-// where the misspelled identity is unique (no duplicates), so
-// `dotnet new reactorapp` resolves correctly inside the fresh hive.
-//
-// The typo only surfaces against the user's *real* template cache
-// (~/.templateengine/dotnetcli//templatecache.json), where every
-// harness run that does `dotnet new install ... --force` accumulates
-// duplicate entries for the same misspelled identity. Eventually the
-// `dotnet new reactorapp` short-name lookup finds more than one match
-// and throws "Sequence contains more than one matching element" with
-// exit code 70. The EC3 eval batch hit this 20/20 runs.
-//
-// What this test asserts:
-// - The template's `identity` and `groupIdentity` use the canonical
-// Microsoft.UI.Reactor brand namespace, not any spelling variant.
-// - The file contains no `Micrsoft` substring anywhere (catches the
-// same typo if it sneaks back in via copy-paste in a new symbol /
-// description / etc.).
-//
-// What this test deliberately does NOT do:
-// - Run `dotnet new install`. That path is covered by
-// `tests/Reactor.IntegrationTests/Packaging/CreateTemplateTests.cs`.
-// Content validation belongs in fast unit tests so a typo lights up
-// in seconds rather than minutes.
-
-using System.Text.Json;
-using Xunit;
-
-namespace Microsoft.UI.Reactor.Tests;
-
-public sealed class TemplateMetadataTests
-{
- static readonly string TemplateJsonPath = Path.Combine(
- "tools", "Templates", "templates", "WinUIApp-CSharp", ".template.config", "template.json");
-
- [Fact]
- public void Identity_is_canonical_brand_namespace()
- {
- var doc = LoadTemplateJson();
- var identity = doc.RootElement.GetProperty("identity").GetString();
- Assert.Equal("Microsoft.UI.Reactor.CSharp", identity);
- }
-
- [Fact]
- public void GroupIdentity_is_canonical_brand_namespace()
- {
- var doc = LoadTemplateJson();
- var groupIdentity = doc.RootElement.GetProperty("groupIdentity").GetString();
- Assert.Equal("Microsoft.UI.Reactor", groupIdentity);
- }
-
- [Fact]
- public void File_contains_no_brand_typos()
- {
- // Broad guard: catches any future typo of the same shape in any
- // field of the file. The exact-match assertions above are the
- // load-bearing checks; this is the belt-and-suspenders sweep.
- var (path, text) = ReadTemplateJson();
- Assert.False(
- text.Contains("Micrsoft", StringComparison.Ordinal),
- $"'{path}' contains the typo 'Micrsoft' (missing the second 'o'). " +
- $"Use 'Microsoft' everywhere — the identity/groupIdentity fields are load-bearing for `dotnet new`'s template-cache lookup.");
- }
-
- [Fact]
- public void ShortName_resolves_to_reactorapp()
- {
- // Anchors the public CLI command-name the agent docs (and the
- // wordpuzzle smoke pattern) depend on. Changing it is a breaking
- // change; this test surfaces an accidental rename.
- var doc = LoadTemplateJson();
- var shortName = doc.RootElement.GetProperty("shortName").GetString();
- Assert.Equal("reactorapp", shortName);
- }
-
- // ── Framework-version drift guard ──────────────────────────────────────
- //
- // The template bakes the framework version generated apps reference
- // (`MicrosoftUIReactorVersion` → template.json `MSUIReactorVersion`
- // defaultValue, via the csproj BeforePack target). That version used to be
- // a hardcoded csproj default that had to be hand-bumped every release — and
- // it silently drifted (stuck at preview.4 while the framework shipped
- // through preview.11), so published templates generated apps referencing an
- // ancient package (see issue #866). The fix makes the release workflow STAMP
- // the version it's publishing onto the templates pack. This test fails the
- // instant that automation is removed, so the drift can't silently return.
-
- [Fact]
- public void ReleaseWorkflow_stamps_framework_version_into_templates_pack()
- {
- // The release "Pack Templates" step must pass -p:MicrosoftUIReactorVersion
- // = the resolved release version, so the published ProjectTemplates
- // package references the framework version published in the same run.
- var (path, text) = ReadRepoFile(Path.Combine(".github", "workflows", "release.yml"));
- var step = ExtractYamlStep(text, "Pack Templates");
- Assert.False(step is null,
- $"'{path}' has no 'Pack Templates' step — the release template pack moved or was renamed.");
- Assert.Contains("Microsoft.UI.Reactor.Templates.csproj", step, StringComparison.Ordinal);
- Assert.True(
- step!.Contains("-p:MicrosoftUIReactorVersion=${{ steps.version.outputs.version }}", StringComparison.Ordinal),
- $"The 'Pack Templates' step in '{path}' must pass " +
- "'-p:MicrosoftUIReactorVersion=${{ steps.version.outputs.version }}' so the published " +
- "template references the framework version shipped in the same release run. Without it, " +
- "the baked reference falls back to the csproj default and drifts behind the framework (issue #866).");
- }
-
- [Fact]
- public void Bootstrap_packs_templates_with_latest_framework_version()
- {
- // The local side of the same fix: bootstrap.ps1 must pack the templates with
- // `--framework-version latest` so a fresh clone's `dotnet new reactorapp`
- // tracks the newest published package instead of a hand-maintained csproj
- // default. Fails the instant that wiring is dropped from either invocation
- // path (installed `mur` or the `dotnet run` fallback).
- var (path, text) = ReadRepoFile("bootstrap.ps1");
- var normalized = text.Replace("\r\n", "\n");
- var matches = global::System.Text.RegularExpressions.Regex.Matches(
- normalized, @"pack-local\s+--framework-version\s+latest");
- Assert.True(
- matches.Count >= 2,
- $"'{path}' must invoke `mur pack-local --framework-version latest` on both the installed-`mur` " +
- $"and `dotnet run` fallback paths so local scaffolds track the latest published framework " +
- $"(found {matches.Count}, expected >= 2). Dropping it re-introduces the drift fixed for issue #866.");
- }
-
- // Returns the text of the YAML step whose `name:` equals stepName (the slice
- // from that step's `- name:` line up to the next `- name:` line or EOF), or
- // null if no such step exists. Deliberately simple line scanning — enough to
- // scope a Contains assertion to one step without a YAML dependency.
- static string? ExtractYamlStep(string yaml, string stepName)
- {
- var lines = yaml.Replace("\r\n", "\n").Split('\n');
- int start = -1;
- for (int i = 0; i < lines.Length; i++)
- {
- var t = lines[i].TrimStart();
- if (t.StartsWith("- name:", StringComparison.Ordinal) &&
- t.Substring("- name:".Length).Trim() == stepName)
- {
- start = i;
- break;
- }
- }
- if (start < 0) return null;
-
- int end = lines.Length;
- for (int i = start + 1; i < lines.Length; i++)
- {
- if (lines[i].TrimStart().StartsWith("- name:", StringComparison.Ordinal))
- {
- end = i;
- break;
- }
- }
- return string.Join("\n", lines[start..end]);
- }
-
- static JsonDocument LoadTemplateJson()
- {
- var (_, text) = ReadTemplateJson();
- return JsonDocument.Parse(text, new JsonDocumentOptions
- {
- // template.json files in the wild use trailing commas; the
- // template engine tolerates them and so should our test.
- AllowTrailingCommas = true,
- CommentHandling = JsonCommentHandling.Skip,
- });
- }
-
- static (string path, string text) ReadTemplateJson()
- {
- var repoRoot = FindRepoRoot();
- var path = Path.Combine(repoRoot, TemplateJsonPath);
- Assert.True(File.Exists(path), $"Expected '{path}' to exist; template.json moved or removed?");
- return (path, File.ReadAllText(path));
- }
-
- static (string path, string text) ReadRepoFile(string repoRelativePath)
- {
- var path = Path.Combine(FindRepoRoot(), repoRelativePath);
- Assert.True(File.Exists(path), $"Expected '{path}' to exist; file moved or removed?");
- return (path, File.ReadAllText(path));
- }
-
- static string FindRepoRoot()
- {
- var dir = AppContext.BaseDirectory;
- while (dir != null && !File.Exists(Path.Combine(dir, "Reactor.slnx")))
- {
- dir = Path.GetDirectoryName(dir);
- }
- Assert.NotNull(dir);
- return dir!;
- }
-}
diff --git a/tests/Reactor.Tests/VersionSingleSourceTests.cs b/tests/Reactor.Tests/VersionSingleSourceTests.cs
index 68ec47d88..d597537e5 100644
--- a/tests/Reactor.Tests/VersionSingleSourceTests.cs
+++ b/tests/Reactor.Tests/VersionSingleSourceTests.cs
@@ -105,27 +105,6 @@ public void DirectoryBuildProps_defines_ReactorPublicVersion()
Assert.Matches(@"^\d+\.\d+\.\d+", match.Groups[1].Value.Trim());
}
- [Fact]
- public void TemplatesCsproj_fallback_derives_from_ReactorPublicVersion()
- {
- // The templates csproj's MicrosoftUIReactorVersion fallback default must
- // derive from $(ReactorPublicVersion), NOT carry its own literal — that
- // is what collapses the repo to ONE framework-version literal.
- var (path, text) = ReadRepoFile(Path.Join(
- "tools", "Templates", "Microsoft.UI.Reactor.Templates.csproj"));
-
- Assert.Matches(
- @"]*>\s*\$\(ReactorPublicVersion\)\s*",
- text);
-
- var literalFallback = global::System.Text.RegularExpressions.Regex.IsMatch(
- text, @"]*>\s*" + PreviewLiteralPattern);
- Assert.False(
- literalFallback,
- $"'{path}' hardcodes a preview version in . Derive the fallback from " +
- "$(ReactorPublicVersion) instead so the repo has exactly one framework-version literal.");
- }
-
// ── Guard 3: CI compiled-docs freshness gate is wired ─────────────────
//
// Widened for issue #1052. This guard used to pin a two-file
diff --git a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs
index f9d05678f..2dddf07df 100644
--- a/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs
+++ b/tests/Reactor.Tests/WinAppSDKReferenceGuardTests.cs
@@ -29,10 +29,12 @@ public class WinAppSDKReferenceGuardTests
// Repo-root-relative, '/'-separated csproj paths allowed to reference the
// Microsoft.WindowsAppSDK metapackage directly.
- private static readonly HashSet Allowlist = new(StringComparer.OrdinalIgnoreCase)
- {
- "tools/Templates/templates/WinUIApp-CSharp/Company.ReactorApp1.csproj",
- };
+ //
+ // Empty since the in-repo `dotnet new reactorapp` template was removed — it
+ // was the only project that pinned the metapackage itself, because a
+ // scaffolded consumer does not inherit this repo's central injection. The
+ // replacement templates live in the Windows App SDK pack, outside this repo.
+ private static readonly HashSet Allowlist = new(StringComparer.OrdinalIgnoreCase);
///
/// Source-level guard: no repo project may declare a direct
diff --git a/tests/Reactor.Tests/WinAppSdkTemplatesTests.cs b/tests/Reactor.Tests/WinAppSdkTemplatesTests.cs
new file mode 100644
index 000000000..d1a909f2f
--- /dev/null
+++ b/tests/Reactor.Tests/WinAppSdkTemplatesTests.cs
@@ -0,0 +1,599 @@
+// Unit coverage for the Windows App SDK `dotnet new` template pack helper
+// (Templates/WinAppSdkTemplates.cs).
+//
+// Reactor's app templates moved out of this repo's own
+// `Microsoft.UI.Reactor.ProjectTemplates` pack (`dotnet new reactorapp`) and
+// into `Microsoft.WindowsAppSDK.WinUI.CSharp.Templates` (`dotnet new reactor`).
+// bootstrap.ps1 §5, `mur upgrade`, and `mur doctor` all route through this
+// helper.
+//
+// The load-bearing piece worth unit-testing is version *selection*. The install
+// path can't just hand `dotnet new install` a bare package id: that command has
+// no `--prerelease` switch and resolves stable-only, so it fails outright while
+// the pack is publishing prereleases (it shipped 0.0.6-alpha before any stable).
+// So we resolve a version ourselves and pass `::`. These tests pin
+// the two rules that resolution has to get right:
+// 1. prefer a stable version when one exists, and
+// 2. otherwise fall back to the *newest* prerelease — numerically, not
+// lexically ("alpha.10" > "alpha.9").
+//
+// The network fetch and the `dotnet new install` invocation are not unit-tested
+// (they hit nuget.org and the template engine); `mur templates status` and the
+// bootstrap CI job cover those.
+
+using Microsoft.UI.Reactor.Cli.Templates;
+using Xunit;
+
+namespace Microsoft.UI.Reactor.Tests;
+
+public sealed class WinAppSdkTemplatesTests
+{
+ [Fact]
+ public void PackageId_is_the_windows_app_sdk_template_pack()
+ {
+ // Anchors the identity bootstrap.ps1 installs and `mur doctor` probes for.
+ // A rename here silently turns the doctor check into a permanent FAIL.
+ Assert.Equal("Microsoft.WindowsAppSDK.WinUI.CSharp.Templates", WinAppSdkTemplates.PackageId);
+ }
+
+ [Fact]
+ public void ShortNames_are_the_canonical_name_of_each_template_in_the_pack()
+ {
+ // One canonical name per template (microsoft/WindowsAppSDK#6620), NOT every
+ // registered short name: the shipped pack also registers a `winui-` alias
+ // for each, plus `reactor-blank`, so nine names cover four templates.
+ // Verified against the published 0.0.7-alpha listing.
+ Assert.Equal(
+ new[] { "reactor", "reactor-mvu", "reactor-navview", "reactor-tabview" },
+ WinAppSdkTemplates.ShortNames);
+ Assert.Equal("reactor", WinAppSdkTemplates.BlankShortName);
+ Assert.Contains(WinAppSdkTemplates.BlankShortName, WinAppSdkTemplates.ShortNames);
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ // ── Installed-version parsing ──────────────────────────────────────────
+
+ [Fact]
+ public void InterpretInstalledVersionOutput_reads_the_version_for_this_pack()
+ {
+ // Verbatim shape of `dotnet new uninstall` with two packs installed — the
+ // other pack's id is a PREFIX-adjacent name, which a substring match would
+ // confuse with ours.
+ const string listing = """
+ Currently installed items:
+ Microsoft.WindowsAppSDK.Templates
+ Version: 0.1.11-prerelease.100
+ Details:
+ Author: Microsoft
+ Microsoft.WindowsAppSDK.WinUI.CSharp.Templates
+ Version: 0.0.7-alpha
+ Details:
+ Author: Microsoft
+ """;
+
+ Assert.Equal("0.0.7-alpha", WinAppSdkTemplates.InterpretInstalledVersionOutput(listing));
+ }
+
+ [Fact]
+ public void InterpretInstalledVersionOutput_does_not_borrow_the_next_packages_version()
+ {
+ // A package with no `Version:` line followed closely by another package:
+ // a bare 4-line look-ahead walks straight into the neighbour and returns
+ // *its* version. Reporting a version that is not installed is worse than
+ // reporting none — `mur doctor` would name it in a PASS line.
+ const string adjacent = """
+ Currently installed items:
+ Microsoft.WindowsAppSDK.WinUI.CSharp.Templates
+ Microsoft.Other.Templates
+ Version: 9.9.9
+ """;
+
+ Assert.Null(WinAppSdkTemplates.InterpretInstalledVersionOutput(adjacent));
+
+ // Sanity: the same neighbour is still readable in its own right, so the
+ // guard stops the scan rather than breaking the parser.
+ const string normal = """
+ Currently installed items:
+ Microsoft.WindowsAppSDK.WinUI.CSharp.Templates
+ Version: 0.0.7-alpha
+ Microsoft.Other.Templates
+ Version: 9.9.9
+ """;
+ Assert.Equal("0.0.7-alpha", WinAppSdkTemplates.InterpretInstalledVersionOutput(normal));
+ }
+
+ [Fact]
+ public void InterpretInstalledVersionOutput_returns_null_when_this_pack_is_absent()
+ {
+ const string listing = """
+ Currently installed items:
+ Microsoft.WindowsAppSDK.Templates
+ Version: 0.1.11-prerelease.100
+ """;
+
+ Assert.Null(WinAppSdkTemplates.InterpretInstalledVersionOutput(listing));
+ }
+
+ [Fact]
+ public void InterpretInstalledVersionOutput_returns_null_when_no_version_line_follows()
+ {
+ // Malformed / truncated listing must not return a neighbouring package's version.
+ const string listing = """
+ Currently installed items:
+ Microsoft.WindowsAppSDK.WinUI.CSharp.Templates
+ Details:
+ Author: Microsoft
+ """;
+
+ Assert.Null(WinAppSdkTemplates.InterpretInstalledVersionOutput(listing));
+ }
+
+ [Fact]
+ public void InterpretTemplateListOutput_requires_the_exact_short_name()
+ {
+ // Three traps in one fixture, all from the real listing shape:
+ // • '-' is a word boundary, so `\breactor\b` / Contains("reactor") also
+ // matches `reactor-mvu` and the real `winui-reactor-mvu` alias;
+ // • the Template Name column carries the capitalised prose word
+ // "Reactor" as a standalone token, so a case-insensitive token match
+ // passes too;
+ // • so does the Tags column, where "Reactor" is slash-delimited.
+ // None of them means the blank `reactor` template is installed.
+ const string withoutBlank = """
+ These templates matched your input: 'reactor'
+
+ Template Name Short Name Language Tags
+ ----------------------------------------- ------------------------------------- -------- ------------------------------------------
+ Reactor MVU App (Experimental) reactor-mvu,winui-reactor-mvu [C#] Windows/WinUI/Desktop/Reactor/Experimental
+ """;
+
+ Assert.False(WinAppSdkTemplates.InterpretTemplateListOutput(withoutBlank, exitCode: 0));
+ }
+
+ [Fact]
+ public void InterpretTemplateListOutput_accepts_the_short_name_in_a_comma_list()
+ {
+ // Verbatim from the published 0.0.7-alpha pack, so the fixture is measured
+ // rather than imagined. Three things have to survive: the comma-separated
+ // alias column, the capitalised prose "Reactor" in the Template Name
+ // column, and "Reactor" again inside the slash-delimited Tags column.
+ const string withBlank = """
+ These templates matched your input: 'reactor'
+
+ Template Name Short Name Language Tags
+ ----------------------------------------- ------------------------------------- -------- ------------------------------------------
+ Reactor Blank App (Experimental) reactor,reactor-blank,winui-reactor [C#] Windows/WinUI/Desktop/Reactor/Experimental
+ Reactor MVU App (Experimental) reactor-mvu,winui-reactor-mvu [C#] Windows/WinUI/Desktop/Reactor/Experimental
+ """;
+
+ Assert.True(WinAppSdkTemplates.InterpretTemplateListOutput(withBlank, exitCode: 0));
+ }
+
+
+
+
+
+
+
+
+
+
+
+ [Fact]
+ public void Bootstrap_does_not_advertise_dotnet_new_reactor_when_templates_are_skipped()
+ {
+ // Two false promises to avoid: -SkipTemplates leaves the pack uninstalled,
+ // and a *verified-unavailable* pack (0.0.6-alpha predates the Reactor
+ // templates) is installed but cannot scaffold. Printing the command in
+ // either case hands the user something that fails immediately.
+ var (path, text) = ReadRepoFile("bootstrap.ps1");
+ var normalized = text.Replace("\r\n", "\n");
+ var next = normalized[normalized.LastIndexOf("Write-Host 'Next:'", StringComparison.Ordinal)..];
+ Assert.True(
+ next.Contains("if ($SkipTemplates)", StringComparison.Ordinal),
+ $"'{path}' must gate the `dotnet new reactor` next-step guidance on -SkipTemplates.");
+ Assert.True(
+ next.Contains("elseif (-not $templatesVerified)", StringComparison.Ordinal),
+ $"'{path}' must also gate that guidance on the step-5 verification result.");
+ }
+
+ [Fact]
+ public void Upgrade_reports_template_availability_without_installing()
+ {
+ // `mur upgrade` no longer installs the pack — `winapp` owns that — so the
+ // guard is that it still *probes* and points somewhere useful, rather than
+ // silently dropping the check or claiming to have refreshed anything.
+ var (path, text) = ReadRepoFile(global::System.IO.Path.Join("src", "Reactor.Cli", "Upgrade", "UpgradeCommand.cs"));
+ Assert.True(
+ text.Contains("AreTemplatesAvailable()", StringComparison.Ordinal),
+ $"'{path}' must still probe template availability during upgrade.");
+ Assert.True(
+ text.Contains("winapp new", StringComparison.Ordinal),
+ $"'{path}' must point at `winapp new` when the templates are missing.");
+ // The installer is gone; nothing here may call it back into existence.
+ Assert.DoesNotContain("WinAppSdkTemplates.Install", text, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Bootstrap_does_not_treat_an_unusable_pack_as_an_install_failure()
+ {
+ // Regression: a non-zero result for an installed-but-unusable pack (or an
+ // absent winapp) made bootstrap Fail before it ever reached the
+ // verification and the gated guidance below it. Exit 2 must fall through.
+ var (path, text) = ReadRepoFile("bootstrap.ps1");
+ Assert.True(
+ global::System.Text.RegularExpressions.Regex.IsMatch(
+ text.Replace("\r\n", "\n"),
+ @"\$templatesExit -ne 0 -and \$templatesExit -ne 2"),
+ $"'{path}' must let exit 2 through to the verification step.");
+ }
+
+ [Theory]
+ // Three distinct "cannot scaffold" situations that need different advice:
+ // telling someone their pack "predates the Reactor templates" when it is not
+ // installed at all — or when the probe itself failed — sends them to re-pin a
+ // version that was never the problem.
+ [InlineData(true, null, 0)] // resolves
+ [InlineData(null, null, 1)] // template engine unreadable
+ [InlineData(false, true, 2)] // pack present, short name absent → too old
+ [InlineData(false, false, 3)] // pack absent
+ [InlineData(false, null, 1)] // package list unreadable → probe failure, not "too old"
+ public void StatusExitCode_separates_unusable_missing_and_probe_failure(
+ bool? available, bool? packageInstalled, int expected)
+ {
+ Assert.Equal(expected, TemplatesCommand.StatusExitCode(available, packageInstalled));
+ }
+
+ [Fact]
+ public void Bootstrap_gives_different_advice_per_status_outcome()
+ {
+ // Regression: a single warning claiming "installed but too old" fired for
+ // all three, including "not installed" and "probe failed".
+ var (path, text) = ReadRepoFile("bootstrap.ps1");
+ var normalized = text.Replace("\r\n", "\n");
+ var start = normalized.IndexOf("$templatesVerified = ($statusExit -eq 0)", StringComparison.Ordinal);
+ Assert.True(start >= 0, $"'{path}' must derive $templatesVerified from the status exit code.");
+ var block = normalized[start..];
+
+ Assert.True(
+ global::System.Text.RegularExpressions.Regex.IsMatch(block, @"switch \(\$statusExit\)"),
+ $"'{path}' must branch on the status exit code rather than emitting one warning for every failure.");
+ Assert.Contains("is not installed", block, StringComparison.Ordinal);
+ Assert.Contains("Could not enumerate", block, StringComparison.Ordinal);
+ }
+
+
+
+
+
+ [Fact]
+ public void InterpretPackageInstalledOutput_matches_the_id_line_exactly()
+ {
+ // A substring search over the transcript also fires on a longer id that
+ // merely contains ours, and on the hint lines that echo it — reporting an
+ // old pack as installed and choosing the wrong remediation.
+ const string otherPackOnly = """
+ Currently installed items:
+ Microsoft.WindowsAppSDK.WinUI.CSharp.Templates.Extras
+ Version: 1.0.0
+ Uninstall command:
+ dotnet new uninstall Microsoft.WindowsAppSDK.WinUI.CSharp.Templates.Extras
+ """;
+ Assert.False(WinAppSdkTemplates.InterpretPackageInstalledOutput(otherPackOnly));
+
+ const string thisPack = """
+ Currently installed items:
+ Microsoft.WindowsAppSDK.WinUI.CSharp.Templates
+ Version: 0.0.7-alpha
+ """;
+ Assert.True(WinAppSdkTemplates.InterpretPackageInstalledOutput(thisPack));
+ }
+
+ [Fact]
+ public void Doctor_treats_an_unreadable_package_list_as_a_warning_not_a_missing_pack()
+ {
+ // `IsPackageInstalled()` returns null when the installed-package list
+ // could not be read. Comparing it to `true` alone sent that case to the
+ // "not registered" FAIL, telling the developer to reinstall a pack that
+ // may be perfectly fine — the same ProbeFailed distinction StatusExitCode
+ // already draws.
+ var (path, text) = ReadRepoFile(global::System.IO.Path.Join(
+ "src", "Reactor.Cli", "Doctor", "DoctorCommand.cs"));
+ Assert.True(
+ text.Contains("packageInstalled is null", StringComparison.Ordinal),
+ $"'{path}' must handle an unreadable installed-package list separately from a missing pack.");
+ Assert.False(
+ text.Contains("IsPackageInstalled() == true", StringComparison.Ordinal),
+ $"'{path}' must not collapse null (couldn't tell) into false (not installed).");
+ }
+
+ [Fact]
+ public void Bootstrap_invokes_the_resolved_winapp_path_not_a_bare_command()
+ {
+ // The CLI is also accepted via its app-execution alias, which exists on
+ // disk without always being resolvable through this process's PATH. A
+ // bare `winapp` therefore fails on exactly the machines the alias
+ // fallback exists to support.
+ var (path, text) = ReadRepoFile("bootstrap.ps1");
+ var normalized = text.Replace("\r\n", "\n");
+ Assert.True(
+ normalized.Contains("function Get-WinAppCliPath", StringComparison.Ordinal),
+ $"'{path}' must resolve the winapp CLI to a concrete path.");
+ Assert.True(
+ global::System.Text.RegularExpressions.Regex.IsMatch(normalized, @"&\s*\$winAppExe\s+@winAppNewArgs"),
+ $"'{path}' must invoke the resolved winapp path, not a bare `winapp`.");
+ }
+
+ // ── False-PASS guard: "pack installed" != "templates usable" ───────────
+ //
+ // Observed live during the de-stale merge: the machine had
+ // Microsoft.WindowsAppSDK.WinUI.CSharp.Templates 0.0.6-alpha installed —
+ // a version published *before* the Reactor templates were added. A
+ // package-id-only probe reported `mur doctor` PASS and `mur templates
+ // status` "installed", while `dotnet new list reactor` said
+ // "No templates found matching: 'reactor'." and exited 103.
+ //
+ // The trap is that the not-found message itself contains the search term
+ // ("...matching: 'reactor'" plus a "dotnet new search reactor" hint), so a
+ // naive `output.Contains("reactor")` returns true exactly when the template
+ // is missing. These pin the negative-marker-first rule.
+
+ [Fact]
+ public void InterpretTemplateListOutput_reports_unknown_for_an_engine_failure()
+ {
+ // `dotnet new list` exits 103 for "no templates matched" — an answer. Any
+ // other non-zero exit is an SDK/engine failure, and there is no reason to
+ // believe its stderr describes the template state. Reporting `false` there
+ // makes `mur doctor` fail and bootstrap advise a reinstall on the strength
+ // of an error it never parsed.
+ const string engineError = """
+ The command could not be loaded, possibly because of a missing SDK.
+ """;
+
+ Assert.Null(WinAppSdkTemplates.InterpretTemplateListOutput(engineError, exitCode: 1));
+
+ // The two interpretable outcomes still answer definitively.
+ Assert.False(WinAppSdkTemplates.InterpretTemplateListOutput(
+ "No templates found matching: 'reactor'.", WinAppSdkTemplates.NoTemplatesFoundExitCode));
+ Assert.True(WinAppSdkTemplates.InterpretTemplateListOutput(
+ "Reactor Blank App reactor,reactor-blank [C#]", exitCode: 0));
+ }
+
+ [Fact]
+ public void InterpretTemplateListOutput_trusts_the_not_found_marker_over_the_exit_code()
+ {
+ // The marker is authoritative when present: some SDKs have reported the
+ // no-match message with a non-103 exit, and that is still a real answer.
+ Assert.False(WinAppSdkTemplates.InterpretTemplateListOutput(
+ "No templates found matching: 'reactor'.", exitCode: 1));
+ }
+
+ [Fact]
+ public void InterpretTemplateListOutput_reports_missing_for_the_not_found_message()
+ {
+ // Verbatim output captured from `dotnet new list reactor` against an
+ // installed-but-too-old 0.0.6-alpha pack. Note it mentions "reactor"
+ // three times — a substring match on the short name would say "found".
+ const string notFound = """
+ No templates found matching: 'reactor'.
+
+ To search for the templates on NuGet.org, run:
+ dotnet new search reactor
+
+ For details on the exit code, refer to https://aka.ms/templating-exit-codes#103
+ """;
+
+ Assert.False(WinAppSdkTemplates.InterpretTemplateListOutput(notFound, WinAppSdkTemplates.NoTemplatesFoundExitCode));
+ }
+
+ [Fact]
+ public void InterpretTemplateListOutput_reports_available_for_a_real_listing()
+ {
+ // Shape of a real `dotnet new list reactor` table.
+ const string listing = """
+ These templates matched your input: 'reactor'
+
+ Template Name Short Name Language Tags
+ --------------------------------- ----------------------------- -------- -------------
+ Reactor Blank App (Experimental) reactor,reactor-blank [C#] Windows/WinUI
+ Reactor MVU App (Experimental) reactor-mvu [C#] Windows/WinUI
+ """;
+
+ Assert.True(WinAppSdkTemplates.InterpretTemplateListOutput(listing, exitCode: 0));
+ }
+
+ [Fact]
+ public void Doctor_probes_template_availability_not_just_package_presence()
+ {
+ // Source-level guard on the call site. The whole point of the fix is
+ // that DoctorCommand asks "can the user scaffold?" — if it reverts to
+ // the package-id probe for its PASS branch, the false PASS returns.
+ var (path, text) = ReadRepoFile(global::System.IO.Path.Join(
+ "src", "Reactor.Cli", "Doctor", "DoctorCommand.cs"));
+ Assert.Contains("AreTemplatesAvailable()", text, StringComparison.Ordinal);
+ }
+
+
+ // ── Bootstrap wiring guards ────────────────────────────────────────────
+ //
+ // Reactor's app templates live in the Windows App SDK `dotnet new` pack
+ // (`Microsoft.WindowsAppSDK.WinUI.CSharp.Templates`, short name `reactor`).
+ // The in-repo `Microsoft.UI.Reactor.ProjectTemplates` pack that used to
+ // provide `dotnet new reactorapp` has been deleted. These two tests pin both
+ // halves of that contract — the regression they guard is silent, since a
+ // bootstrap that quietly re-registered `reactorapp` would hand new
+ // developers an unpackaged template the docs no longer describe.
+
+ [Fact]
+ public void Bootstrap_installs_the_windows_app_sdk_template_pack()
+ {
+ var (path, text) = ReadRepoFile("bootstrap.ps1");
+ var normalized = text.Replace("\r\n", "\n");
+ Assert.Contains("Microsoft.WindowsAppSDK.WinUI.CSharp.Templates", text, StringComparison.Ordinal);
+ // `winapp new --list` installs the pack on demand; --use-defaults keeps an
+ // already-installed one and never prompts, which is bootstrap's
+ // install-if-missing (not reinstall) semantics on a non-interactive run.
+ Assert.True(
+ global::System.Text.RegularExpressions.Regex.IsMatch(
+ normalized, @"'new',\s*'--list',\s*'--use-defaults'"),
+ $"'{path}' must install the Reactor templates via `winapp new --list --use-defaults`, which " +
+ "installs the Windows App SDK template pack on demand. `dotnet new install` has no --prerelease " +
+ "switch and resolves stable-only, so installing the bare package id fails while the pack is " +
+ "prerelease-only — that is the whole reason this does not shell out to `dotnet new install`.");
+ // The in-repo installer is gone; bootstrap may not call it back.
+ Assert.False(
+ global::System.Text.RegularExpressions.Regex.IsMatch(normalized, @"templates',\s*'install'"),
+ $"'{path}' must not call the removed `mur templates install`.");
+ }
+
+ [Fact]
+ public void Bootstrap_does_not_install_the_removed_reactorapp_template()
+ {
+ // The in-repo ProjectTemplates pack was deleted; nothing in bootstrap may
+ // resurrect it by handing the id to `dotnet new install`.
+ var (path, text) = ReadRepoFile("bootstrap.ps1");
+ var normalized = text.Replace("\r\n", "\n");
+
+ // Strip comment lines: the step documents the migration in prose, and
+ // those mentions must not trip this guard.
+ var code = string.Join('\n', normalized
+ .Split('\n')
+ .Where(line => !line.TrimStart().StartsWith("#", StringComparison.Ordinal)));
+
+ Assert.False(
+ global::System.Text.RegularExpressions.Regex.IsMatch(
+ code, @"new\s+install.*Microsoft\.UI\.Reactor\.ProjectTemplates"),
+ $"'{path}' must not `dotnet new install` Microsoft.UI.Reactor.ProjectTemplates — that package was " +
+ "removed from this repo. Scaffolding goes through the Windows App SDK pack (`dotnet new reactor`).");
+ }
+
+ [Fact]
+ public void Repo_no_longer_ships_the_in_repo_template_package()
+ {
+ // The deletion itself. Assert on the tracked *source* rather than the
+ // directory: bin/obj under tools/Templates are gitignored, so a
+ // contributor who built the project before pulling this change still has
+ // the folder on disk. Checking Directory.Exists would fail for them while
+ // nothing is actually wrong.
+ var root = FindRoot();
+ foreach (var relative in new[]
+ {
+ global::System.IO.Path.Join("tools", "Templates", "Microsoft.UI.Reactor.Templates.csproj"),
+ global::System.IO.Path.Join("tools", "Templates", "templates", "WinUIApp-CSharp", ".template.config", "template.json"),
+ })
+ {
+ Assert.False(
+ global::System.IO.File.Exists(global::System.IO.Path.Join(root, relative)),
+ $"'{relative}' is back. The in-repo Microsoft.UI.Reactor.ProjectTemplates package was removed " +
+ "in favour of the Windows App SDK `dotnet new reactor` templates.");
+ }
+
+ var (relPath, release) = ReadRepoFile(global::System.IO.Path.Join(".github", "workflows", "release.yml"));
+ Assert.False(
+ release.Contains("Microsoft.UI.Reactor.Templates.csproj", StringComparison.Ordinal),
+ $"'{relPath}' packs the removed template project again.");
+ }
+
+ static (string path, string text) ReadRepoFile(string repoRelativePath)
+ {
+ var path = global::System.IO.Path.Join(FindRoot(), repoRelativePath);
+ Assert.True(global::System.IO.File.Exists(path), $"Expected '{path}' to exist; file moved or removed?");
+ return (path, global::System.IO.File.ReadAllText(path));
+ }
+
+ static string FindRoot()
+ {
+ var dir = AppContext.BaseDirectory;
+ while (dir != null && !global::System.IO.File.Exists(global::System.IO.Path.Join(dir, "Reactor.slnx")))
+ dir = global::System.IO.Path.GetDirectoryName(dir);
+ Assert.NotNull(dir);
+ return dir!;
+ }
+}
+
+// ── `mur templates install` argv parsing ──────────────────────────────────
+//
+// Only the branches that return *before* touching the machine are exercised:
+// help, unknown option, missing value, and a bare positional. A real install is
+// a global `dotnet new install`, which these tests must never trigger.
+//
+// Strict parsing is load-bearing: `--sorce ./pkgs` silently ignored would
+// install from the configured feeds instead of the folder the user named, and
+// the install would look successful.
+[Collection("ConsoleTests")]
+public sealed class TemplatesCommandArgvTests
+{
+ static (int ExitCode, string Stdout, string Stderr) Run(params string[] args)
+ {
+ var originalOut = Console.Out;
+ var originalError = Console.Error;
+ using var stdout = new global::System.IO.StringWriter();
+ using var stderr = new global::System.IO.StringWriter();
+ try
+ {
+ Console.SetOut(stdout);
+ Console.SetError(stderr);
+ var exitCode = TemplatesCommand.Run(args);
+ return (exitCode, stdout.ToString(), stderr.ToString());
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ Console.SetError(originalError);
+ }
+ }
+
+
+
+ [Fact]
+ public void Unknown_subcommand_and_no_subcommand_both_show_help()
+ {
+ var (missing, missingOut, _) = Run();
+ Assert.Equal(1, missing);
+ Assert.Contains("mur templates", missingOut, StringComparison.Ordinal);
+
+ var (unknown, _, unknownErr) = Run("instal");
+ Assert.Equal(1, unknown);
+ Assert.Contains("instal", unknownErr, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Removed_install_subcommand_names_its_replacement()
+ {
+ // `install` is handled explicitly rather than falling into "unknown
+ // subcommand": it existed, bootstrap and the docs called it, and anyone
+ // with it in muscle memory or a script needs to be sent to `winapp new`
+ // — not left hunting for a typo.
+ var (exitCode, _, stderr) = Run("install");
+
+ Assert.Equal(1, exitCode);
+ Assert.Contains("has been removed", stderr, StringComparison.Ordinal);
+ Assert.Contains("winapp new", stderr, StringComparison.Ordinal);
+ // A bare "unknown subcommand 'install'" would be the unhelpful outcome.
+ Assert.DoesNotContain("unknown subcommand", stderr, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Help_documents_the_status_exit_codes()
+ {
+ // bootstrap.ps1 branches on these, so they are contract, not cosmetics.
+ var (exitCode, stdout, _) = Run("--help");
+
+ Assert.Equal(0, exitCode);
+ Assert.Contains("Exit codes:", stdout, StringComparison.Ordinal);
+ Assert.Contains("winapp new", stdout, StringComparison.Ordinal);
+ // The removed installer must not be advertised as an option any more.
+ Assert.DoesNotContain("mur templates install", stdout, StringComparison.Ordinal);
+ }
+}
diff --git a/tests/vs_reactor/ci/BootstrapFeedResolver.Tests.ps1 b/tests/vs_reactor/ci/BootstrapFeedResolver.Tests.ps1
index 50eaa9927..b3e6dad3a 100644
--- a/tests/vs_reactor/ci/BootstrapFeedResolver.Tests.ps1
+++ b/tests/vs_reactor/ci/BootstrapFeedResolver.Tests.ps1
@@ -211,6 +211,7 @@ try {
Assert-Throws { Resolve-ReactorNuGetFeed -ExplicitConfig (Join-Path $tmp 'missing.config') } `
'missing explicit NuGet config is rejected'
+
$restoreArgs = Get-ReactorRestoreArguments `
-NuGetSource 'https://packagefeedproxy.microsoft.io/nuget/v3/index.json' `
-NpmRegistry 'https://packagefeedproxy.microsoft.io/npm'
diff --git a/tools/Templates/Microsoft.UI.Reactor.Templates.csproj b/tools/Templates/Microsoft.UI.Reactor.Templates.csproj
deleted file mode 100644
index 9d4152501..000000000
--- a/tools/Templates/Microsoft.UI.Reactor.Templates.csproj
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
- Template
- AnyCPU
- Microsoft.UI.Reactor.ProjectTemplates
- Project templates for Microsoft.UI.Reactor
- Microsoft
- Project templates for Microsoft.UI.Reactor
- © Microsoft Corporation. All rights reserved.
- Icon.png
- README.md
- MIT
- true
- dotnet-new;templates;WinUI;WinUI3;Windows App SDK;WinAppSDK;Windows;desktop;Reactor
- netstandard2.0
- true
- false
- content
- true
- $(NoWarn);NU5128
- 0.0.0-local
- $(ReactorPublicVersion)
-
-
-
-
-
-
-
-
-
- $([System.String]::Concat(%(RelativeDir), '%(Filename)%(Extension)'))
- $(IntermediateOutputPath)$([System.String]::Concat(%(RelativeDir), '%(Filename)%(Extension)'))
-
-
-
-
-
-
- _UpdateTemplateVersions
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tools/Templates/README.md b/tools/Templates/README.md
deleted file mode 100644
index 4b2811378..000000000
--- a/tools/Templates/README.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# Microsoft.UI.Reactor.ProjectTemplates
-
-**`dotnet new` templates for scaffolding [`Microsoft.UI.Reactor`](https://www.nuget.org/packages/Microsoft.UI.Reactor) apps — a ready-to-run WinUI 3 Reactor project in one command.**
-
-## About
-
-This package installs project templates for the .NET CLI and Visual Studio so you can create a new declarative WinUI 3 desktop app powered by Reactor without wiring up the project by hand.
-
-## How to Use
-
-Install the templates:
-
-```shell
-dotnet new install Microsoft.UI.Reactor.ProjectTemplates
-```
-
-Create a new Reactor app:
-
-```shell
-dotnet new reactorapp -n MyApp
-cd MyApp
-dotnet run -p:Platform=x64
-```
-
-This scaffolds a runnable WinUI 3 project that references `Microsoft.UI.Reactor` and the Windows App SDK, with a root component already wired up through `ReactorApp.Run`.
-
-### Template options
-
-- `--NativeAot` (bool, default `false`) — configure the project for Native AOT publishing.
-- `--UseProgramMain` (bool, default `false`) — generate an explicit `Program.Main` instead of top-level statements.
-- `--Framework ` (default `net10.0`) — choose the target framework.
-
-```shell
-# Example: a Native AOT-ready app with an explicit Main method
-dotnet new reactorapp -n MyApp --NativeAot --UseProgramMain
-```
-
-## Included Templates
-
-- **Microsoft WinUI Reactor App** (short name `reactorapp`) — a Windows WinUI 3 application using Reactor (C#).
-
-## Updating and uninstalling
-
-```shell
-# Update to the latest published templates
-dotnet new update
-
-# Remove the templates
-dotnet new uninstall Microsoft.UI.Reactor.ProjectTemplates
-```
-
-## Additional Documentation
-
-- [Getting started guide](https://github.com/microsoft/microsoft-ui-reactor/blob/main/docs/guide/getting-started.md)
-- [`dotnet new` documentation](https://learn.microsoft.com/dotnet/core/tools/dotnet-new)
-- [Custom templates for dotnet new](https://learn.microsoft.com/dotnet/core/tools/custom-templates)
-
-## Related Packages
-
-- [`Microsoft.UI.Reactor`](https://www.nuget.org/packages/Microsoft.UI.Reactor) — the core declarative WinUI 3 framework.
-- [`Microsoft.UI.Reactor.Advanced`](https://www.nuget.org/packages/Microsoft.UI.Reactor.Advanced) — optional Win2D/graphics components.
-- [`Microsoft.UI.Reactor.Devtools`](https://www.nuget.org/packages/Microsoft.UI.Reactor.Devtools) — optional developer-loop devtools host.
-
-## Feedback & Contributing
-
-These templates are part of the open-source Reactor project. File issues, ask questions, and contribute on [GitHub](https://github.com/microsoft/microsoft-ui-reactor). See [CONTRIBUTING.md](https://github.com/microsoft/microsoft-ui-reactor/blob/main/CONTRIBUTING.md) to get started.
-
-## Support Policy
-
-This package is currently released as a preview and is provided under the [MIT License](https://github.com/microsoft/microsoft-ui-reactor/blob/main/LICENSE). APIs may change between preview releases.
diff --git a/tools/Templates/templates/WinUIApp-CSharp/.template.config/dotnetcli.host.json b/tools/Templates/templates/WinUIApp-CSharp/.template.config/dotnetcli.host.json
deleted file mode 100644
index 375341020..000000000
--- a/tools/Templates/templates/WinUIApp-CSharp/.template.config/dotnetcli.host.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "$schema": "http://json.schemastore.org/dotnetcli.host",
- "symbolInfo": {
- "TargetFrameworkOverride": {
- "isHidden": "true",
- "longName": "target-framework-override",
- "shortName": ""
- },
- "Framework": {
- "longName": "framework"
- },
- "skipRestore": {
- "longName": "no-restore",
- "shortName": ""
- },
- "langVersion": {
- "longName": "langVersion",
- "shortName": ""
- },
- "UseProgramMain": {
- "longName": "use-program-main",
- "shortName": ""
- },
- "NativeAot": {
- "longName": "aot",
- "shortName": ""
- }
- },
- "usageExamples": [
- ""
- ]
-}
diff --git a/tools/Templates/templates/WinUIApp-CSharp/.template.config/ide.host.json b/tools/Templates/templates/WinUIApp-CSharp/.template.config/ide.host.json
deleted file mode 100644
index e3cc4ca00..000000000
--- a/tools/Templates/templates/WinUIApp-CSharp/.template.config/ide.host.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "$schema": "http://json.schemastore.org/ide.host",
- "tags": [
- {
- "type": "projectType",
- "add": [],
- "remove": [ "Common" ]
- }
- ],
- "symbolInfo": [
- {
- "id": "UseProgramMain",
- "isVisible": true,
- "PersistenceScope": "Shared",
- "PersistenceScopeName": "Microsoft"
- },
- {
- "id": "NativeAot",
- "isVisible": true
- }
- ]
-}
diff --git a/tools/Templates/templates/WinUIApp-CSharp/.template.config/template.json b/tools/Templates/templates/WinUIApp-CSharp/.template.config/template.json
deleted file mode 100644
index 843ba0aa3..000000000
--- a/tools/Templates/templates/WinUIApp-CSharp/.template.config/template.json
+++ /dev/null
@@ -1,132 +0,0 @@
-{
- "$schema": "http://json.schemastore.org/template",
- "author": "Microsoft",
- "classifications": [ "Windows", "WinUI" ],
- "identity": "Microsoft.UI.Reactor.CSharp",
- "groupIdentity": "Microsoft.UI.Reactor",
- "name": "Microsoft WinUI Reactor App",
- "description": "A Windows WinUI Application using Reactor",
- "shortName": "reactorapp",
- "tags": {
- "language": "C#",
- "type": "project"
- },
- "sourceName": "Company.ReactorApp1",
- "preferNameDirectory": true,
- "sources": [
- {
- "modifiers": [
- {
- "//": "Binary assets must be copied verbatim; the template engine otherwise runs sourceName / version substitution over their bytes.",
- "copyOnly": [ "Assets/**" ]
- }
- ]
- }
- ],
- "symbols": {
- "TargetFrameworkOverride": {
- "type": "parameter",
- "description": "Overrides the target framework",
- "replaces": "TargetFrameworkOverride",
- "datatype": "string",
- "defaultValue": "",
- "displayName": "Target framework override"
- },
- "Framework": {
- "type": "parameter",
- "description": "The target framework for the project.",
- "datatype": "choice",
- "choices": [
- {
- "choice": "net10.0",
- "description": "Target net10.0",
- "displayName": ".NET 10.0"
- }
- ],
- "replaces": "net10.0",
- "defaultValue": "net10.0",
- "displayName": "Framework"
- },
- "skipRestore": {
- "type": "parameter",
- "datatype": "bool",
- "description": "If specified, skips the automatic restore of the project on create.",
- "defaultValue": "false",
- "displayName": "Skip restore"
- },
- "UseProgramMain": {
- "type": "parameter",
- "datatype": "bool",
- "defaultValue": "false",
- "description": "Whether to generate an explicit Program class and Main method instead of top-level statements.",
- "displayName": "Do not use _top-level statements"
- },
- "NativeAot" : {
- "type": "parameter",
- "datatype": "bool",
- "defaultValue": "false",
- "displayName": "Enable _native AOT publish",
- "description": "Whether to enable the project for publishing as native AOT."
- },
- "HostIdentifier": {
- "type": "bind",
- "binding": "HostIdentifier"
- },
- "csharpFeature_TopLevelProgram": {
- "type": "computed",
- "value": "UseProgramMain != \"true\""
- },
- "MSUIReactorVersion": {
- "type": "parameter",
- "dataType": "string",
- "replaces": "MS_UI_REACTOR_VERSION",
- "defaultValue": "MS_UI_REACTOR_VERSION_VALUE"
- },
- },
- "primaryOutputs": [
- {
- "path": "Company.ReactorApp1.csproj"
- },
- {
- "condition": "(HostIdentifier != \"dotnetcli\" && HostIdentifier != \"dotnetcli-preview\")",
- "path": "App.cs"
- }
- ],
- "defaultName": "WinUIApp1",
- "postActions": [
- {
- "actionId": "B17581D1-C5C9-4489-8F0A-004BE667B814",
- "continueOnError": true,
- "description": "Update Microsoft.WindowsAppSDK to the latest stable version.",
- "args": {
- "referenceType": "package",
- "reference": "Microsoft.WindowsAppSDK"
- },
- "manualInstructions": [ { "text": "Run 'dotnet add package Microsoft.WindowsAppSDK'" } ]
- },
- {
- "id": "restore",
- "condition": "(!skipRestore)",
- "description": "Restore NuGet packages required by this project.",
- "manualInstructions": [
- {
- "text": "Run 'dotnet restore'"
- }
- ],
- "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
- "continueOnError": true,
- "args": { "files": ["Company.ReactorApp1.csproj"] }
- },
- {
- "id": "editor",
- "condition": "(HostIdentifier != \"dotnetcli\" && HostIdentifier != \"dotnetcli-preview\")",
- "description": "Opens App.cs in the editor",
- "manualInstructions": [],
- "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6",
- "args": {
- "files": "1"
- },
- "continueOnError": true
- }
- ]
-}
diff --git a/tools/Templates/templates/WinUIApp-CSharp/App.cs b/tools/Templates/templates/WinUIApp-CSharp/App.cs
deleted file mode 100644
index ef2b26ce7..000000000
--- a/tools/Templates/templates/WinUIApp-CSharp/App.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using Microsoft.UI.Reactor;
-using Microsoft.UI.Reactor.Core; // BackdropKind
-using Microsoft.UI.Reactor.Layout; // FlexDirection, FlexJustify, FlexAlign
-using Microsoft.UI.Xaml; // Thickness, HorizontalAlignment, VerticalAlignment
-using Microsoft.UI.Xaml.Controls; // Orientation, InfoBarSeverity, etc.
-using static Microsoft.UI.Reactor.Factories;
-
-#if (csharpFeature_TopLevelProgram)
-ReactorApp.Run("Company.ReactorApp1", width: 900, height: 600,
- icon: WindowIcon.FromPath("Assets/AppIcon.ico"));
-
-#else
-namespace Company.ReactorApp1;
-
-class Program
-{
- static void Main(string[] args)
- {
- ReactorApp.Run("Company.ReactorApp1", width: 900, height: 600,
- icon: WindowIcon.FromPath("Assets/AppIcon.ico"));
- }
-}
-
-#endif
-class App : Component
-{
- public override Element Render()
- {
- var (name, setName) = UseState("World");
-
- // The title bar's app mark is inherited from the window icon set via
- // ReactorApp.Run(icon:) above -- no need to restate it here. Call
- // .Icon(...) only to show something different, or .NoIcon() for none.
- var titleBar = TitleBar("Company.ReactorApp1")
- .Flex(shrink: 0);
-
- var body = Border(
- FlexColumn(
- Heading($"Hello, {name}!"),
- TextBox(name, setName, placeholderText: "Your name")
- .AutomationName("NameInput")
- ) with { RowGap = 16 }
- ).Padding(24).Flex(grow: 1, basis: 0);
-
- return FlexColumn(titleBar, body)
- .Backdrop(BackdropKind.Mica);
- }
-}
diff --git a/tools/Templates/templates/WinUIApp-CSharp/Assets/AppIcon.ico b/tools/Templates/templates/WinUIApp-CSharp/Assets/AppIcon.ico
deleted file mode 100644
index 09dea3793..000000000
Binary files a/tools/Templates/templates/WinUIApp-CSharp/Assets/AppIcon.ico and /dev/null differ
diff --git a/tools/Templates/templates/WinUIApp-CSharp/Company.ReactorApp1.csproj b/tools/Templates/templates/WinUIApp-CSharp/Company.ReactorApp1.csproj
deleted file mode 100644
index 4f40b52d4..000000000
--- a/tools/Templates/templates/WinUIApp-CSharp/Company.ReactorApp1.csproj
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
- WinExe
- net10.0-windows10.0.22621.0
- TargetFrameworkOverride-windows10.0.22621.0
-
- x64;ARM64;X86
- true
- None
-
- true
- 10.0.17763.0
- 10.0.17763.0
- enable
-
- Assets\AppIcon.ico
-
- $(NETCoreSdkPortableRuntimeIdentifier)
-
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tools/Templates/templates/WinUIApp-CSharp/Properties/launchSettings.json b/tools/Templates/templates/WinUIApp-CSharp/Properties/launchSettings.json
deleted file mode 100644
index a7aab3de1..000000000
--- a/tools/Templates/templates/WinUIApp-CSharp/Properties/launchSettings.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "//": [
- "Default F5 / `dotnet run` profile for the scaffolded Reactor app. The devtools",
- "profile pairs --devtools with the Debug-only `Microsoft.UI.Reactor.Devtools` package +",
- "Reactor.DevtoolsSupport host config option (see Company.ReactorApp1.csproj).",
- "Release builds exclude the devtools package, so the default profile does not pass",
- "--devtools. The Reactor Visual Studio embedded-preview extension (spec 056) launches",
- "with its own argv (`--devtools run --embed --embed-host-pid `) and does not",
- "consult this file."
- ],
- "profiles": {
- "Company.ReactorApp1": {
- "commandName": "Project",
- "nativeDebugging": false
- },
- "Company.ReactorApp1 Devtools": {
- "commandName": "Project",
- "commandLineArgs": "--devtools",
- "nativeDebugging": false
- }
- }
-}