-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathbootstrap.ps1
More file actions
704 lines (635 loc) · 33.8 KB
/
Copy pathbootstrap.ps1
File metadata and controls
704 lines (635 loc) · 33.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
#requires -Version 5.1
<#
.SYNOPSIS
One-command setup for a Microsoft.UI.Reactor source checkout.
.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.
Idempotent — safe to re-run after `git pull` to refresh everything.
For a less heavyweight refresh (mur stays put), run `mur upgrade`.
.PARAMETER SkipPlugin
Skip installing the Claude Code plugin under ~/.claude/plugins.
.PARAMETER SkipMurInstall
Build and pack the CLI but don't run `dotnet tool install/update`.
Useful for CI or for users who manage tool installs externally.
.PARAMETER SkipVsExtension
Skip building / installing the Reactor Visual Studio embedded-preview
extension (`src/vs-reactor`). Default behavior is to detect VS via
vswhere and install the VSIX into the highest-version instance if
that instance has the 'Visual Studio extension development' workload.
Passing this flag (or not having VS / the workload installed) silently
skips this step — the rest of the bootstrap still completes.
.PARAMETER Configuration
Build configuration for the CLI nupkg. Default: Release.
.PARAMETER InstallWinAppSdk
Install the Windows App Runtime 2.0 via winget without prompting.
Useful for CI / one-shot dev-box automation. Mutually exclusive with
-NoWinAppSdk. The framework defaults to self-contained, so the
runtime is only required for framework-dependent deployment.
.PARAMETER NoWinAppSdk
Skip the Windows App Runtime 2.0 prompt silently. Useful for
non-interactive scripts that explicitly don't want the runtime
installed. Mutually exclusive with -InstallWinAppSdk.
.PARAMETER SkipWinAppCli
Skip installing the winapp CLI (`Microsoft.WinAppCli`). The CLI is the
UI automation driver used by the cross-process E2E tests in
`tests/Reactor.AppTests`; bootstrap installs it via winget by default so
those tests can run locally. Pass this flag if you don't run the E2E
suite or manage the CLI yourself. The install is best-effort either way —
a missing winget only warns, it never fails the bootstrap.
.PARAMETER NpmRegistry
Override the npm registry used by GitHub.Copilot.SDK to acquire its native
CLI. When omitted, bootstrap uses a packagefeedproxy.microsoft.io registry
already configured in NPM_CONFIG_REGISTRY or ~/.npmrc; otherwise the SDK's
public registry default is unchanged.
.PARAMETER NuGetConfig
Use an explicit NuGet.Config for bootstrap restores. When omitted, bootstrap
uses a packagefeedproxy.microsoft.io source already present in the user's
NuGet.Config if it is reachable; otherwise the repo's public config remains
in effect.
.PARAMETER Verbose
Common parameter (enabled by [CmdletBinding]). Surfaces extra
diagnostic output at every decision point: detected SDK list,
winget probe exit codes, host arch / pack args, global-tool
install-vs-update branch, template uninstall decision, plugin
symlink-vs-copy fallback, etc. Use this when bootstrap fails or
behaves unexpectedly and you want to see *why* a branch was taken.
.EXAMPLE
./bootstrap.ps1
Full bootstrap (prompts before installing WindowsAppRuntime).
.EXAMPLE
./bootstrap.ps1 -SkipPlugin
Skip the Claude plugin step.
.EXAMPLE
./bootstrap.ps1 -InstallWinAppSdk -SkipPlugin
Non-interactive: install everything (incl. WindowsAppRuntime) and
skip the agent plugin. Suitable for CI / fresh-dev-box automation.
.EXAMPLE
./bootstrap.ps1 -Verbose
Print extra `VERBOSE:` diagnostics at every decision point. Useful
for debugging install failures or unexpected branch behavior.
#>
[CmdletBinding()]
param(
[switch]$SkipPlugin,
[switch]$SkipMurInstall,
[switch]$SkipVsExtension,
[string]$Configuration = 'Release',
[switch]$InstallWinAppSdk,
[switch]$NoWinAppSdk,
[switch]$SkipWinAppCli,
[string]$NpmRegistry,
[string]$NuGetConfig
)
if ($InstallWinAppSdk -and $NoWinAppSdk) {
Write-Host ''
Write-Host "ERROR: -InstallWinAppSdk and -NoWinAppSdk are mutually exclusive." -ForegroundColor Red
exit 1
}
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
Set-Location $repoRoot
$feedResolver = Join-Path $repoRoot 'tools\BootstrapFeedResolver.ps1'
if (-not (Test-Path -LiteralPath $feedResolver -PathType Leaf)) {
Write-Host "ERROR: Missing bootstrap feed resolver: $feedResolver" -ForegroundColor Red
exit 1
}
. $feedResolver
# -Verbose is a common parameter (CmdletBinding); $VerbosePreference flips to
# 'Continue' automatically when the caller passes it. We expose a tiny helper
# so verbose blocks can short-circuit (e.g. skipping expensive `Out-String`
# captures) when the flag is off.
$script:VerboseOn = ($VerbosePreference -ne 'SilentlyContinue')
function Write-Dbg($msg) {
Write-Verbose $msg
}
function Write-Step($msg) {
Write-Host ''
Write-Host "==> $msg" -ForegroundColor Cyan
}
function Write-Ok($msg) {
Write-Host " [ok] $msg" -ForegroundColor Green
}
function Fail($msg) {
Write-Host ''
Write-Host "ERROR: $msg" -ForegroundColor Red
exit 1
}
# Install a winget package and refresh $env:Path so the freshly-installed tool
# is resolvable in this same shell. Hard-fails if winget itself is missing —
# that's an OS-level prerequisite this script doesn't try to repair.
function Install-WithWinget {
param(
[Parameter(Mandatory)][string]$Id,
[string]$Reason = $Id
)
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Fail "Need to install '$Reason' but winget is not on PATH. Install App Installer from the Microsoft Store, then re-run ./bootstrap.ps1."
}
Write-Host " Installing $Reason via winget ($Id)..." -ForegroundColor Yellow
Write-Dbg "winget install --id $Id --accept-source-agreements --accept-package-agreements --silent --disable-interactivity"
& winget install --id $Id --accept-source-agreements --accept-package-agreements --silent --disable-interactivity
Write-Dbg "winget exit code: $LASTEXITCODE"
if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne -1978335189) {
# -1978335189 = APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE (already installed / up-to-date)
Fail "winget install $Id failed (exit $LASTEXITCODE). Install $Reason manually and re-run ./bootstrap.ps1."
}
# winget edits the Machine + User PATH but the current process keeps its
# original. Rebuild $env:Path from the registry so subsequent commands in
# this script can find the freshly-installed binaries.
$env:Path = (
[Environment]::GetEnvironmentVariable('Path', 'Machine'),
[Environment]::GetEnvironmentVariable('Path', 'User')
) -join ';'
Write-Dbg "Refreshed `$env:Path from registry (Machine + User scopes)"
}
# ---------------------------------------------------------------------------
# 1. Pre-flight
# ---------------------------------------------------------------------------
Write-Step 'Pre-flight checks'
Write-Dbg "PowerShell $($PSVersionTable.PSVersion); OS arch $env:PROCESSOR_ARCHITECTURE; repo $repoRoot"
function Test-DotnetSdk10 {
if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { return $false }
foreach ($line in (& dotnet --list-sdks)) {
Write-Dbg " dotnet SDK: $line"
if ($line -match '^(\d+)\.' -and [int]$Matches[1] -ge 10) { return $true }
}
return $false
}
if (-not (Test-DotnetSdk10)) {
if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) {
Write-Host " [info] dotnet not found on PATH." -ForegroundColor Yellow
} else {
Write-Host " [info] dotnet present but no .NET 10+ SDK detected. Installed:" -ForegroundColor Yellow
& dotnet --list-sdks | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow }
}
Install-WithWinget -Id 'Microsoft.DotNet.SDK.10' -Reason '.NET 10 SDK'
if (-not (Test-DotnetSdk10)) {
Fail '.NET 10 SDK install reported success but `dotnet --list-sdks` still does not show a 10.x entry. Open a new shell and re-run ./bootstrap.ps1.'
}
}
Write-Ok ".NET SDK present"
$hostArch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'ARM64' } else { 'x64' }
$copilotPlatform = if ($hostArch -eq 'ARM64') { 'win32-arm64' } else { 'win32-x64' }
$npmSelection = Resolve-ReactorNpmRegistry -ExplicitRegistry $NpmRegistry
if ($npmSelection -and -not $npmSelection.Explicit) {
Write-Dbg "Testing unauthenticated npm tarball access: $($npmSelection.Registry) ($copilotPlatform)"
if (-not (Test-ReactorNpmRegistryAccess -Registry $npmSelection.Registry -Platform $copilotPlatform)) {
Write-Host " [warn] Configured npm proxy is not reachable; falling back to GitHub.Copilot.SDK's public registry." -ForegroundColor Yellow
$npmSelection = $null
}
}
if ($npmSelection) {
$npmSelectionKind = if ($npmSelection.Explicit) { 'explicit override' } else { 'user configuration' }
Write-Ok "npm registry selected from ${npmSelectionKind}: $($npmSelection.Registry)"
} else {
Write-Dbg 'No reachable configured npm proxy detected; GitHub.Copilot.SDK will use its public registry default'
}
$nugetSelection = Resolve-ReactorNuGetFeed -ExplicitConfig $NuGetConfig
if ($nugetSelection -and -not $nugetSelection.Explicit) {
Write-Dbg "Testing NuGet source access: $($nugetSelection.Source)"
if (-not (Test-ReactorNuGetSourceAccess -Source $nugetSelection.Source)) {
Write-Host " [warn] Configured NuGet proxy cannot provide GitHub.Copilot.SDK; falling back to the repo's public NuGet config." -ForegroundColor Yellow
$nugetSelection = $null
}
}
$effectiveNuGetConfig = if ($nugetSelection -and $nugetSelection.Explicit) { $nugetSelection.ConfigPath } else { $null }
$effectiveNuGetSource = if ($nugetSelection -and -not $nugetSelection.Explicit) { $nugetSelection.Source } else { $null }
if ($nugetSelection) {
if ($nugetSelection.Explicit) {
Write-Ok "NuGet config selected by explicit override: $effectiveNuGetConfig"
} else {
Write-Ok "NuGet proxy selected from user configuration: $effectiveNuGetSource"
}
} else {
Write-Dbg 'No reachable configured NuGet proxy detected; using the repo nuget.config'
}
function Get-ResolvedDotnetSdkVersion {
$sdkVersion = (& dotnet --version 2>$null | Select-Object -First 1)
$rc = $LASTEXITCODE
$global:LASTEXITCODE = 0
if ($rc -ne 0 -or [string]::IsNullOrWhiteSpace($sdkVersion)) { return $null }
return $sdkVersion.Trim()
}
function Get-VsExtensionSkipReason {
param([Parameter(Mandatory)]$VsInstance)
$sdkVersion = Get-ResolvedDotnetSdkVersion
if (-not $sdkVersion -or $sdkVersion -notmatch '^(\d+)\.(\d+)\.(\d+)') { return $null }
$sdkMajor = [int]$Matches[1]
$sdkPatch = [int]$Matches[3]
$sdkRequiresMsBuild18 = ($sdkMajor -gt 10) -or ($sdkMajor -eq 10 -and $sdkPatch -ge 300)
if (-not $sdkRequiresMsBuild18) { return $null }
$vsVersion = [string]$VsInstance.installationVersion
if ($vsVersion -notmatch '^(\d+)\.') { return $null }
$vsMajor = [int]$Matches[1]
if ($vsMajor -ge 18) { return $null }
return "Visual Studio $vsVersion uses MSBuild 17.x, but this checkout resolves .NET SDK $sdkVersion, which requires MSBuild 18+ for desktop VSIX builds."
}
# Windows App SDK runtime — recommended for samples, perf-tests, and any
# project that omits WindowsAppSDKSelfContained=true.
#
# 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
# proofs (tests/aot_trim_proof/*) keep =true explicitly so their build
# output stays a standalone deployable.
#
# Net effect: the user needs the WindowsAppRuntime 2.0 install matching
# our WindowsAppSDKVersion=2.1.3 to run most things in this repo. (Framework-
# dependent projects reference the Microsoft.WindowsAppSDK.WinUI sub-package
# rather than the full metapackage, but still bind that same machine-wide runtime.)
#
# So we prompt by default. `-InstallWinAppSdk` to force-install,
# `-InstallWinAppSdk:$false` to skip the prompt non-interactively.
function Test-WindowsAppRuntime20 {
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Dbg "winget not on PATH; skipping WindowsAppRuntime probe"
return $true # nothing we can check without winget
}
# --accept-source-agreements is needed even for `list` on a winget that
# hasn't been used before (e.g. a fresh CI runner). Without it, winget
# prompts for msstore terms and fails on a non-interactive shell with
# exit -1978335166.
Write-Dbg "winget list --id Microsoft.WindowsAppRuntime.2.0 --exact --accept-source-agreements"
if ($script:VerboseOn) {
& winget list --id Microsoft.WindowsAppRuntime.2.0 --exact --accept-source-agreements 2>&1 |
ForEach-Object { Write-Dbg " winget> $_" }
} else {
& winget list --id Microsoft.WindowsAppRuntime.2.0 --exact --accept-source-agreements 2>$null | Out-Null
}
$rc = $LASTEXITCODE
Write-Dbg "winget list exit code: $rc"
$global:LASTEXITCODE = 0 # don't let winget's status leak out of the probe
return ($rc -eq 0)
}
if (-not (Test-WindowsAppRuntime20)) {
if ($InstallWinAppSdk) {
Install-WithWinget -Id 'Microsoft.WindowsAppRuntime.2.0' -Reason 'Windows App Runtime 2.0'
} elseif ($NoWinAppSdk) {
Write-Host ' [skip] Windows App Runtime 2.0 not installed (skipped per -NoWinAppSdk).' -ForegroundColor Yellow
} else {
Write-Host ''
Write-Host ' Windows App Runtime 2.0 is not installed on this machine.' -ForegroundColor Yellow
Write-Host ' The Reactor repo defaults to WindowsAppSDKSelfContained=false, so most'
Write-Host ' samples, perf benches, and apps in this repo need the machine-wide runtime'
Write-Host ' installed to launch. Skip only if you know your projects override'
Write-Host ' WindowsAppSDKSelfContained=true to bundle their own copy.'
$answer = Read-Host ' Install Windows App Runtime 2.0 via winget now? [y/N]'
if ($answer -match '^[Yy]') {
Install-WithWinget -Id 'Microsoft.WindowsAppRuntime.2.0' -Reason 'Windows App Runtime 2.0'
} else {
Write-Host " Skipped. Re-run later with: winget install Microsoft.WindowsAppRuntime.2.0" -ForegroundColor Cyan
}
}
} else {
Write-Ok 'Windows App Runtime 2.0 installed'
}
# ---------------------------------------------------------------------------
# 1.6 winapp CLI (E2E UI test driver)
# ---------------------------------------------------------------------------
# The cross-process E2E tests in tests/Reactor.AppTests drive the running app
# through the `winapp ui` CLI (Microsoft.WinAppCli) instead of WinAppDriver.
# WinAppUi.ResolveWinAppExe() resolves it from %LOCALAPPDATA%\Microsoft\
# WindowsApps\winapp.exe (the alias winget's MSIX install drops) or `winapp`
# on PATH. Install it best-effort so `dotnet test tests/Reactor.AppTests`
# 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 }
$alias = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\winapp.exe'
Write-Dbg "winapp not on PATH; probing alias $alias"
return (Test-Path $alias)
}
if ($SkipWinAppCli) {
Write-Host ' [skip] winapp CLI install skipped (per -SkipWinAppCli).' -ForegroundColor Yellow
} elseif (Test-WinAppCli) {
Write-Ok 'winapp CLI present'
} elseif (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Host ' [warn] winapp CLI not found and winget is unavailable to install it.' -ForegroundColor Yellow
Write-Host ' E2E tests (tests/Reactor.AppTests) need it. Install manually with:' -ForegroundColor Yellow
Write-Host ' winget install Microsoft.WinAppCli (or grab winappcli-x64.zip from the WinAppCli releases)' -ForegroundColor Cyan
} else {
Write-Host ' Installing winapp CLI via winget (Microsoft.WinAppCli)...' -ForegroundColor Yellow
Write-Dbg 'winget install --id Microsoft.WinAppCli --accept-source-agreements --accept-package-agreements --silent --disable-interactivity'
& winget install --id Microsoft.WinAppCli --accept-source-agreements --accept-package-agreements --silent --disable-interactivity
$rc = $LASTEXITCODE
Write-Dbg "winget exit code: $rc"
$global:LASTEXITCODE = 0
# -1978335189 = already installed / up-to-date.
if ($rc -ne 0 -and $rc -ne -1978335189) {
Write-Host " [warn] winget install Microsoft.WinAppCli reported exit $rc. Install it manually to run the E2E suite." -ForegroundColor Yellow
} else {
# winget edits Machine + User PATH but not the live process; rebuild it
# so a follow-on E2E run in this same shell can resolve winapp.
$env:Path = (
[Environment]::GetEnvironmentVariable('Path', 'Machine'),
[Environment]::GetEnvironmentVariable('Path', 'User')
) -join ';'
if (Test-WinAppCli) {
Write-Ok 'winapp CLI installed'
} else {
Write-Host ' [warn] winapp CLI install reported success but the binary is not yet resolvable.' -ForegroundColor Yellow
Write-Host ' Open a new shell before running the E2E suite.' -ForegroundColor Cyan
}
}
}
# ---------------------------------------------------------------------------
# 2. Pack `mur` as a global-tool nupkg
# ---------------------------------------------------------------------------
Write-Step "Packing mur (Reactor CLI) for dotnet global tool install"
$feed = Join-Path $repoRoot 'local-nupkgs'
New-Item -ItemType Directory -Path $feed -Force | Out-Null
Write-Dbg "Local nupkg feed: $feed"
# Match host arch so the embed-resource step (which runs the SignaturesGen
# apphost) succeeds. The packed IL itself is platform-portable.
Write-Dbg "Host arch resolved to: $hostArch"
$cliPackArgs = @(
'pack',
(Join-Path $repoRoot 'src\Reactor.Cli\Reactor.Cli.csproj'),
'-c', $Configuration,
"-p:Platform=$hostArch",
'-o', $feed,
'--nologo', '-v:m'
)
$cliPackArgs += Get-ReactorRestoreArguments `
-NuGetConfig $effectiveNuGetConfig `
-NuGetSource $effectiveNuGetSource `
-NpmRegistry $(if ($npmSelection) { $npmSelection.Registry } else { $null })
Write-Dbg "dotnet $($cliPackArgs -join ' ')"
$cliPackExit = 0
Invoke-ReactorWithRestoreEnvironment `
-NuGetConfig $effectiveNuGetConfig `
-NuGetSource $effectiveNuGetSource `
-ExitCode ([ref]$cliPackExit) `
-Action {
& dotnet @cliPackArgs
}
if ($cliPackExit -ne 0) { Fail 'dotnet pack failed for Reactor.Cli' }
Write-Ok "Packed Microsoft.UI.Reactor.Cli -> $feed"
# ---------------------------------------------------------------------------
# 3. Install / update the global tool
# ---------------------------------------------------------------------------
if ($SkipMurInstall) {
Write-Host ''
Write-Host ' Skipping `dotnet tool install` (per -SkipMurInstall).' -ForegroundColor Yellow
} else {
Write-Step 'Installing mur as a dotnet global tool'
$existing = & dotnet tool list -g 2>$null | Select-String -SimpleMatch 'microsoft.ui.reactor.cli'
$toolArgs = Get-ReactorToolArguments `
-Feed $feed `
-NuGetConfig $effectiveNuGetConfig `
-NuGetSource $effectiveNuGetSource
if ($existing) {
Write-Dbg "Existing global tool detected ($($existing.Line.Trim())); using 'dotnet tool update'"
& dotnet tool update @toolArgs
} else {
Write-Dbg "No existing global tool; using 'dotnet tool install'"
& dotnet tool install @toolArgs
}
if ($LASTEXITCODE -ne 0) { Fail '`dotnet tool install/update` failed for Microsoft.UI.Reactor.Cli' }
# Make ~/.dotnet/tools visible to the rest of this script even if this is
# the first global tool the user has ever installed (dotnet adds it to the
# User PATH but not the current process).
$dotnetTools = Join-Path $env:USERPROFILE '.dotnet\tools'
if (Test-Path $dotnetTools) {
$pathParts = $env:Path -split ';'
if ($pathParts -notcontains $dotnetTools) {
Write-Dbg "Prepending $dotnetTools to current-process PATH"
$env:Path = "$dotnetTools;$env:Path"
} else {
Write-Dbg "$dotnetTools already on current-process PATH"
}
}
Write-Ok "mur installed as global tool (also on this shell's PATH)"
}
# ---------------------------------------------------------------------------
# 4. Pack the in-source framework + templates via the freshly-installed mur
# ---------------------------------------------------------------------------
Write-Step 'Packing local Microsoft.UI.Reactor + ProjectTemplates (`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 <PackageReference>, 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.
$packLocalExit = 0
Invoke-ReactorWithRestoreEnvironment `
-NuGetConfig $effectiveNuGetConfig `
-NuGetSource $effectiveNuGetSource `
-ExitCode ([ref]$packLocalExit) `
-Action {
$murResolved = Get-Command mur -ErrorAction SilentlyContinue
if ($murResolved) {
Write-Dbg "Using installed mur at $($murResolved.Source)"
& mur pack-local --framework-version latest
} else {
Write-Dbg "mur not on PATH; falling back to 'dotnet run' against Reactor.Cli source"
$murRestoreArgs = Get-ReactorRestoreArguments `
-NuGetConfig $effectiveNuGetConfig `
-NuGetSource $effectiveNuGetSource `
-NpmRegistry $(if ($npmSelection) { $npmSelection.Registry } else { $null })
& dotnet run `
--project (Join-Path $repoRoot 'src\Reactor.Cli\Reactor.Cli.csproj') `
-c $Configuration `
"-p:Platform=$hostArch" `
--nologo `
@murRestoreArgs `
-- pack-local --framework-version latest
}
}
if ($packLocalExit -ne 0) { Fail 'mur pack-local failed' }
# ---------------------------------------------------------------------------
# 5. Install the `dotnet new reactorapp` template
# ---------------------------------------------------------------------------
Write-Step 'Installing `dotnet new reactorapp` template'
$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"
}
# 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" }
}
$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
} else {
& dotnet new uninstall Microsoft.UI.Reactor.ProjectTemplates | Out-Null
}
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)
# ---------------------------------------------------------------------------
if ($SkipPlugin) {
Write-Host ''
Write-Host ' Skipping Claude plugin install (per -SkipPlugin).' -ForegroundColor Yellow
} else {
Write-Step 'Installing Reactor plugin for Claude Code'
$pluginSrc = Join-Path $repoRoot 'plugins\reactor'
$pluginDst = Join-Path $env:USERPROFILE '.claude\plugins\reactor'
Write-Dbg "Plugin src: $pluginSrc"
Write-Dbg "Plugin dst: $pluginDst"
if (-not (Test-Path $pluginSrc)) {
Write-Host " [skip] $pluginSrc not present in this checkout" -ForegroundColor Yellow
} else {
New-Item -ItemType Directory -Path (Split-Path $pluginDst) -Force | Out-Null
if (Test-Path $pluginDst) {
Write-Dbg "Removing existing $pluginDst before re-linking"
Remove-Item $pluginDst -Recurse -Force
}
# Prefer symlink so plugin edits in the checkout are immediately visible.
# Falls back to copy when symlink creation is unprivileged (Developer
# Mode off + non-admin shell).
$linked = $false
try {
New-Item -ItemType SymbolicLink -Path $pluginDst -Target $pluginSrc -ErrorAction Stop | Out-Null
$linked = $true
} catch {
Write-Dbg "Symlink failed: $($_.Exception.Message); falling back to copy"
}
if ($linked) {
Write-Ok "Symlinked $pluginDst -> $pluginSrc"
} else {
Copy-Item $pluginSrc $pluginDst -Recurse -Force
Write-Ok "Copied $pluginSrc -> $pluginDst (re-run bootstrap or `mur upgrade` to refresh)"
}
}
}
# ---------------------------------------------------------------------------
# 7. Reactor Visual Studio embedded-preview extension (optional)
# ---------------------------------------------------------------------------
# Spec 056: ships a VSIX that hosts the live Reactor preview inside a VS tool
# window. Building the VSIX needs the 'Visual Studio extension development'
# workload (desktop MSBuild + VSSDK targets). We probe via vswhere and skip
# silently when VS is absent or the workload isn't installed — the rest of
# the bootstrap (mur, framework nupkgs, template, plugin) doesn't depend on
# the extension, so this step never blocks the install.
if ($SkipVsExtension) {
Write-Host ''
Write-Host ' Skipping VS extension install (per -SkipVsExtension).' -ForegroundColor Yellow
} else {
Write-Step 'Building + installing Reactor Visual Studio extension (VSIX)'
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
if (-not (Test-Path -LiteralPath $vswhere)) {
Write-Host ' [skip] vswhere.exe not found — Visual Studio is not installed. The framework + mur + template + plugin are still ready.' -ForegroundColor Yellow
Write-Host " To install the preview extension later, install VS 2022/2026 with the 'Visual Studio extension development' workload, then re-run ./bootstrap.ps1."
} else {
# `-requires Microsoft.VisualStudio.Workload.VisualStudioExtension` filters to
# instances that have the VSIX-development workload (desktop MSBuild + VSSDK
# targets). Without it, Build-Vsix.ps1 fails with "Desktop MSBuild was not
# found." or VSSDK target errors. We want to detect the absence here and emit
# a friendly skip note instead of a stack of red MSBuild errors.
Write-Dbg "Probing vswhere for VS instances with the VSIX-dev workload"
$instancesJson = & $vswhere -all -prerelease `
-requires 'Microsoft.VisualStudio.Workload.VisualStudioExtension' `
-format json 2>$null
$instances = if ($instancesJson) { $instancesJson | ConvertFrom-Json } else { @() }
if (-not $instances -or $instances.Count -eq 0) {
Write-Host " [skip] Visual Studio is installed but no instance has the 'Visual Studio extension development' workload." -ForegroundColor Yellow
Write-Host " Add it from the Visual Studio Installer (Modify -> Workloads) and re-run ./bootstrap.ps1."
} else {
$target = $instances | Sort-Object installationVersion -Descending | Select-Object -First 1
Write-Dbg "Target VS: $($target.displayName) ($($target.instanceId), $($target.installationVersion))"
$reinstall = Join-Path $repoRoot 'src\vs-reactor\Reinstall-Vsix.ps1'
if (-not (Test-Path -LiteralPath $reinstall)) {
Write-Host " [skip] $reinstall not present in this checkout (older branch?). Pull `main` to get the VS extension." -ForegroundColor Yellow
} else {
$skipReason = Get-VsExtensionSkipReason $target
if ($skipReason) {
Write-Host " [skip] $skipReason" -ForegroundColor Yellow
Write-Host " Install Visual Studio 2026 / MSBuild 18+ to build and install the optional Reactor Preview VSIX from bootstrap."
} else {
# Reinstall-Vsix.ps1 chains Build-Vsix.ps1 (desktop MSBuild build) then
# the VSIXInstaller against the per-user data dir, plus a synchronous
# `devenv /updateconfiguration` to merge the pkgdef so menus appear on
# the next launch. -VsInstanceId pins to the same instance we probed.
$powerShellExe = (Get-Process -Id $PID).Path
& $powerShellExe -NoLogo -NoProfile -ExecutionPolicy Bypass -File $reinstall -Configuration $Configuration -VsInstanceId $target.instanceId
$vsixExit = $LASTEXITCODE
# Reinstall-Vsix.ps1's exit-code contract (see its .OUTPUTS):
# 3 == VSIX installed but `devenv /updateconfiguration`
# timed out. Don't claim [ok] for that, and don't fail
# either — the extension is installed and usable.
if ($vsixExit -eq 3) {
Write-Host ''
Write-Host " [warn] VS extension installed into $($target.displayName), but 'devenv /updateconfiguration' did not complete. Launch VS once to finish the menu merge; if View -> Other Windows -> Reactor Preview is missing, re-run src\vs-reactor\Reinstall-Vsix.ps1." -ForegroundColor Yellow
} elseif ($vsixExit -ne 0) {
# Don't fail the whole bootstrap — the rest of the install is
# usable without the VS extension. Surface the failure clearly
# so users debugging install issues see it.
Write-Host ''
Write-Host " [warn] VS extension install reported a non-zero exit code ($vsixExit). The rest of the bootstrap completed; re-run src\vs-reactor\Reinstall-Vsix.ps1 directly to retry." -ForegroundColor Yellow
} else {
Write-Ok "VS extension installed into $($target.displayName) — launch VS, then View -> Other Windows -> Reactor Preview."
}
# Write-Host does NOT clear $LASTEXITCODE, so without this
# the child's non-zero code survives to the end of the
# script and fails callers that check it (issue #1074:
# bootstrap.yml's `if ($LASTEXITCODE -ne 0) { throw }`).
$global:LASTEXITCODE = 0
}
}
}
}
}
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
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'
Write-Host ''
Write-Host 'Other useful commands:'
Write-Host ' mur doctor verify your install'
Write-Host ' mur upgrade refresh local packages + plugin after `git pull`'
Write-Host ' mur --help full command list'
Write-Host ''
Write-Host 'Run the E2E UI tests (needs the winapp CLI installed above):'
Write-Host " dotnet test tests/Reactor.AppTests -c Debug -p:Platform=$hostArch"
Write-Host ''
Write-Host 'Visual Studio preview (if VS installed): View -> Other Windows -> Reactor Preview'
# Every hard failure above exits via Fail (exit 1), so reaching here means
# success. Say so explicitly: bootstrap.yml runs this script *in-process* and
# checks $LASTEXITCODE on the next line, and $LASTEXITCODE is a global — so a
# code left behind by any best-effort step that shelled out to a native command
# survives to that check (issue #1074). Measured: in-process, a leaked 7 is
# still readable afterwards; `pwsh -File` returns 0 on fall-through and does
# not reproduce it. This line makes the outcome independent of which one the
# caller used.
exit 0