Add aspire installs command; drop install table from aspire doctor#17461
Add aspire installs command; drop install table from aspire doctor#17461radical wants to merge 6 commits into
aspire installs command; drop install table from aspire doctor#17461Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17461Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17461" |
This comment was marked as outdated.
This comment was marked as outdated.
Move CLI install enumeration and per-install metadata reporting out of `aspire doctor` into a new `aspire installs` command surface, and align the install-metadata vocabulary on `source` everywhere so the CLI, sidecar spec, installer scripts, and documentation use the same term. `aspire installs list` reports every discovered Aspire CLI install and every orphan hive in a vertical, color-aware layout. `--format json` emits the same rows as a stable contract documented in `docs/specs/cli-output-formats.md`. `aspire installs --self --format json` replaces the hidden `aspire doctor --self` endpoint that install discovery uses to cross-check a peer install's reported metadata. `aspire doctor` no longer ships an install table or `--self` flag; it now reports environment checks only. Install discovery still discovers peers, but the peer probe now invokes `aspire installs --self`. The sidecar spec moves to `docs/specs/install-sources.md`; the `InstallSidecarReader`, `InstallationInfo`, scripts, homebrew template, localhive helpers, and tests all read and write `source` (formerly `route`). Homebrew installs are tagged with the explicit `homebrew` source value in the sidecar and user-facing output. The WinGet first-run install sidecar probe now runs before `aspire installs list` instead of `aspire doctor`, so PR users still get a fresh sidecar populated on the first invocation after `winget install Microsoft.Aspire`. On Windows, the shared `PathLookupHelper` preserves the executable casing recorded on disk after PATHEXT resolution, so `aspire.exe` does not render as `aspire.EXE` just because PATHEXT contains `.EXE`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The install-source sidecar's `source` field is the wire-level schema value: the release script, PR installer, dotnet-tool packaging, and the Homebrew cask all write the literal `brew` string (matching the cask's `postflight` block and the actual Homebrew CLI binary name). `BundleService.ComputeDefaultExtractDir`, `ParseInstallSource`, and the installer test theory rows consume the same wire value. The user-facing surfaces in `aspire installs list` — `GetInstallKind`, `GetCleanupHint`, `GetManagedBy`, and the matching test expectations — render the friendlier `homebrew` / `Homebrew` label because that's what users recognise. Match on `"brew"` at the wire layer, translate to `"homebrew"` at the display layer. The schema stays minimal, the cask sidecar stays a literal one-liner, and users still see the conventional spelling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…xt factory
WingetFirstRunProbe is the writer that lets a winget portable install identify
itself as such on first run — winget has no install hook, so the running CLI
self-stamps `.aspire-install.json` after consulting Windows ARP. It was being
invoked from two unrelated consumer sites:
- InstallsCommand.ListCommand.ExecuteAsync, before discovery reads the sidecar
- BundleService.GetBundleExtractDirForCurrentProcess, before the extract dir
is computed from the sidecar source
Each consumer paid the same try/catch cost and each leaked installer-specific
knowledge into a layer that should be installer-agnostic ("commands shouldn't
depend on any specific installer"). Worse, the factory that builds
CliExecutionContext also reads the sidecar (via GetUsersAspirePath ->
CliPathHelper.GetAspireHomeDirectory) but ran *before* either consumer fired
the probe — so a fresh winget install on its first invocation derived its
Aspire home with the sidecar still unstamped.
Hoist the invocation into the CliExecutionContext DI singleton factory in
Program.cs, immediately before BuildCliExecutionContext. The factory body now
runs the probe once per CLI invocation, before any downstream code reads the
sidecar. BundleService drops its optional WingetFirstRunProbe? parameter and
InstallsCommand drops its ctor parameter, field, and explicit call site.
Tests:
- WindowsRegistryReaderTests (new, 7 tests, Windows-only) exercises the real
HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall hive: matching
entry returns true; wrong identifier / missing target / different target /
surrounded by noise all return false; path comparison is case-insensitive;
empty processPath returns false. Each test creates a uniquely-named
`Microsoft.Aspire.Tests.<guid>` subkey and deletes it in a using; a static
sweeper removes leftovers from any crashed prior run before tests start.
- WingetStartupProbeTests (new, 2 tests) pins the new TryRunWingetFirstRunProbe
contract: it resolves the probe from DI and invokes Run with the binary
directory of Environment.ProcessPath, and any IWindowsRegistryReader
exception is swallowed so CLI startup is never broken by a probe failure.
TryRunWingetFirstRunProbe is internal for testing; the factory's use of it
remains a one-line check at code review.
Net: -54/+50 production lines; +279 test lines; all existing tests still pass
(1366/1366 in the Acquisition + Commands + Bundles surface on macOS).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
53d1e36 to
102af0d
Compare
There was a problem hiding this comment.
Pull request overview
This PR splits Aspire CLI installation enumeration out of aspire doctor into a dedicated aspire installs command, leaving doctor focused on environment checks only. It also completes a broad terminology rename from “install route” to “install source” across CLI code, scripts, tests, and docs.
Changes:
- Add new
aspire installscommand (including hidden--self --format jsonpeer-probe surface) and supporting hive enumeration. - Remove installation-table output and
--selfsupport fromaspire doctor, updating JSON contracts and resource strings accordingly. - Hoist WinGet first-run sidecar stamping into CLI startup (
CliExecutionContextfactory) and fix Windows PATH casing preservation inPathLookupHelper.
Reviewed changes
Copilot reviewed 74 out of 75 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Hosting.Tests/PathLookupHelperTests.cs | Adds Windows-only regression test for preserving filesystem casing when resolving PATHEXT hits. |
| tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | Registers InstallsCommand and HiveEnumerator in the CLI test DI container. |
| tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs | Renames route→source helpers/tests for Aspire home directory derivation. |
| tests/Aspire.Cli.Tests/Packaging/TemporaryNuGetConfigTests.cs | Updates cross-reference to renamed macOS firmlink test name. |
| tests/Aspire.Cli.Tests/Configuration/DotNetBasedAppHostServerChannelResolutionTests.cs | Updates terminology in test doc comment (route→source). |
| tests/Aspire.Cli.Tests/Commands/SetupCommandTests.cs | Renames route→source in test names/comments for setup default path behavior. |
| tests/Aspire.Cli.Tests/Commands/InstallsCommandTests.cs | New unit tests covering aspire installs (list output and hidden --self JSON). |
| tests/Aspire.Cli.Tests/Commands/DoctorCommandTests.cs | Updates doctor tests to assert installations are no longer included and --self is rejected. |
| tests/Aspire.Cli.Tests/BundleServiceComputeDefaultExtractDirTests.cs | Updates comments/terminology for bundle extraction layout selection (route→source). |
| tests/Aspire.Cli.Tests/Bundles/BundleServiceCrossSourceExtractionTests.cs | Renames cross-route extraction matrix test to cross-source and updates homebrew naming. |
| tests/Aspire.Cli.Tests/Acquisition/WingetStartupProbeTests.cs | New tests pinning startup-time WinGet sidecar probe behavior and failure swallowing. |
| tests/Aspire.Cli.Tests/Acquisition/WindowsRegistryReaderTests.cs | New Windows-only round-trip tests against HKCU Uninstall for WinGet detection. |
| tests/Aspire.Cli.Tests/Acquisition/PeerInstallProbeTests.cs | Updates peer probe to call installs --self --format json and accept bare-array JSON shape. |
| tests/Aspire.Cli.Tests/Acquisition/InstallSidecarReaderTests.cs | Updates spec reference and enum expectations (brew→Homebrew) plus “future-source” wording. |
| tests/Aspire.Cli.Tests/Acquisition/InstallationDiscoveryDiscoverAllTests.cs | Updates expectations/messages and property names (route→source) for discovery behavior. |
| tests/Aspire.Acquisition.Tests/Scripts/VerifyCliArchivePowerShellTests.cs | Updates user-facing verifier assertions to “install-source sidecar” wording. |
| tests/Aspire.Acquisition.Tests/Scripts/ReleaseScriptShellTests.cs | Updates dry-run sidecar messaging assertions (route→source). |
| tests/Aspire.Acquisition.Tests/Scripts/ReleaseScriptPowerShellTests.cs | Updates -WhatIf sidecar messaging assertions (route→source). |
| tests/Aspire.Acquisition.Tests/Scripts/PRScriptToolModeTests.cs | Updates tool-mode assertions to ensure no “source sidecar” messaging appears. |
| tests/Aspire.Acquisition.Tests/Scripts/PRScriptShellTests.cs | Updates PR-script dry-run tests and sidecar messaging (route→source). |
| tests/Aspire.Acquisition.Tests/Scripts/PRScriptPowerShellTests.cs | Updates PR-script WhatIf tests and sidecar messaging (route→source). |
| tests/Aspire.Acquisition.Tests/Scripts/PRScriptInstallerModeTests.cs | Updates installer-mode expectations and wording (“Homebrew” command naming). |
| tests/Aspire.Acquisition.Tests/Scripts/PRScriptInstallE2ETests.cs | Updates E2E assertion wording for the sidecar presence (route→source). |
| tests/Aspire.Acquisition.Tests/Scripts/LocalHiveScriptFunctionTests.cs | Adds guard test ensuring localhive scripts don’t reference removed aspire info. |
| tests/Aspire.Acquisition.Tests/Scripts/Common/FakeArchiveHelper.cs | Updates spec reference in archive helper docs (install-routes→install-sources). |
| src/Shared/PathLookupHelper.cs | Returns filesystem-cased executable paths on Windows instead of inheriting PATHEXT casing. |
| src/Aspire.Cli/Utils/EnvironmentChecker/EnvironmentCheckResult.cs | Removes installations from doctor JSON response model. |
| src/Aspire.Cli/Utils/CliPathHelper.cs | Renames route→source for Aspire home directory selection helper. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.cs.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.de.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.es.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.fr.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.it.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ja.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ko.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pl.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pt-BR.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ru.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.tr.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hans.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hant.xlf | Removes installation-table-related localized resources from doctor XLF. |
| src/Aspire.Cli/Resources/DoctorCommandStrings.resx | Removes installation-table string resources now that doctor no longer renders installs. |
| src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs | Removes generated resource accessors for deleted installation-table strings. |
| src/Aspire.Cli/Program.cs | Runs WinGet first-run probe during CLI startup and registers InstallsCommand/HiveEnumerator. |
| src/Aspire.Cli/JsonSourceGenerationContext.cs | Adds source-gen support for InstallationInfo[] and List<InstallListItem>. |
| src/Aspire.Cli/Commands/SetupCommand.cs | Updates comments for source-aware vs source-independent extract/install path behavior. |
| src/Aspire.Cli/Commands/RootCommand.cs | Wires InstallsCommand into the CLI root command. |
| src/Aspire.Cli/Commands/InstallsCommand.cs | Implements new aspire installs command and JSON/list outputs including orphan hives. |
| src/Aspire.Cli/Commands/InstallationInfoOutput.cs | Removes doctor-oriented discovery output; keeps safe self-description helper for installs self-probe. |
| src/Aspire.Cli/Commands/DoctorCommand.cs | Removes install enumeration/self-probe and limits output to environment checks. |
| src/Aspire.Cli/CliExecutionContext.cs | Updates docs/comments to reflect source-specific Aspire home selection behavior. |
| src/Aspire.Cli/Bundles/BundleService.cs | Removes WinGet probe call from bundle extraction path and updates spec reference. |
| src/Aspire.Cli/Acquisition/WingetFirstRunProbe.cs | Updates documentation wording to “install-source sidecar”. |
| src/Aspire.Cli/Acquisition/PeerInstallProbe.cs | Switches peer self-describe call to installs --self --format json and supports new JSON shape. |
| src/Aspire.Cli/Acquisition/IPeerInstallProbe.cs | Updates contract docs for the new peer-probe invocation surface. |
| src/Aspire.Cli/Acquisition/InstallSource.cs | Renames enum member to Homebrew while keeping the wire string as brew. |
| src/Aspire.Cli/Acquisition/InstallSidecarReader.cs | Updates comments/spec reference to install-sources. |
| src/Aspire.Cli/Acquisition/InstallationInfo.cs | Renames JSON field route→source and updates contract docs to installs surfaces. |
| src/Aspire.Cli/Acquisition/InstallationDiscovery.cs | Renames/threads Source through discovery and updates messaging to install-source terminology. |
| src/Aspire.Cli/Acquisition/InstallationCandidateSources.cs | Updates comment wording for installs discovery. |
| src/Aspire.Cli/Acquisition/IInstallSidecarReader.cs | Updates sidecar reader docs to install-source terminology and removes obsolete command references. |
| src/Aspire.Cli/Acquisition/IInstallationDiscovery.cs | Updates discovery interface docs for aspire installs and new peer-probe surface. |
| src/Aspire.Cli/Acquisition/HiveEnumerator.cs | Adds enumeration of hives used by aspire installs list to show orphan hives. |
| localhive.sh | Updates sidecar stamping comments/spec reference to install-sources terminology. |
| localhive.ps1 | Updates sidecar stamping and archive validation messaging to install-sources terminology. |
| eng/scripts/verify-cli-tool-nupkg.ps1 | Updates comment wording around sidecar source expectations. |
| eng/scripts/verify-cli-archive.ps1 | Updates archive sidecar validation messaging to install-sources terminology. |
| eng/scripts/get-aspire-cli.sh | Updates installer sidecar messaging to “source sidecar” and spec reference. |
| eng/scripts/get-aspire-cli.ps1 | Updates installer sidecar messaging to “source sidecar” and spec reference. |
| eng/scripts/get-aspire-cli-pr.sh | Renames PR sidecar writer function and updates comments/messages to install-sources terminology. |
| eng/scripts/get-aspire-cli-pr.ps1 | Renames PR sidecar writer function and updates WhatIf messaging to “source sidecar”. |
| eng/clipack/Common.projitems | Updates build-time guard/error text to “install sources” terminology and new spec reference. |
| docs/specs/install-sources.md | Renames/updates the sidecar specification from install-routes to install-sources. |
| docs/specs/cli-output-formats.md | Documents aspire installs list --format json and clarifies hidden installs --self contract. |
| docs/dogfooding-pull-requests.md | Updates wording around Homebrew uninstall behavior and shared prefixes (route→source). |
Files not reviewed (1)
- src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs: Language not supported
`HiveEnumerator.HasHive` and `GetHivePath` previously fed the channel string straight into `Path.Combine(<hivesRoot>, channel)` and `new DirectoryInfo(...)`. The channel value reaches this surface via peer `installs --self` JSON, which is read from `InstallationInfo. FromJsonElement` without any shape validation. A peer reporting `".."`, an absolute path, or a string containing invalid path characters could therefore make `aspire installs list` display a path outside the hives root, or throw `ArgumentException` from the `DirectoryInfo` constructor. Run the channel through `IdentityChannelReader.IsValidChannel` — the same allow-list the build pipeline uses on the producing side (`stable`, `staging`, `daily`, `local`, `pr-<digits>`) — and treat any non-matching value as "no hive". `GetHivePath` now returns `string?` to match; the only caller already short-circuits via `HasHive` so the nullable return is sink-compatible. `GetHives()` is intentionally left untouched: it enumerates whatever directories exist under the hives root so an orphan hive from an older install script or a future channel name still surfaces in `installs list`.
mitchdenny
left a comment
There was a problem hiding this comment.
Reviewed the install-discovery split. Three related findings, all on InstallsCommand.cs — the status field in the new installs list --format json contract is currently overloaded for non-Ok rows, which both complicates programmatic consumption and forces a couple of magic-string duplications.
PR Testing ReportPR Information
CLI Version Verification
Test Scenarios ExecutedScenario 1:
|
| # | Scenario | Status |
|---|---|---|
| 1 | installs list (human) |
✅ |
| 2 | installs list --format json |
✅ |
| 3 | installs --self --format json (peer probe) |
✅ |
| 4 | doctor (env-only) |
✅ |
| 4b | doctor --format json (no installations key) |
✅ |
| 5 | doctor --self rejected |
✅ |
| 6 | installs help fallback |
✅ |
Overall Result
✅ PR VERIFIED
The new aspire installs command works, the peer-probe contract emits the renamed source field in the expected array shape, and aspire doctor has been correctly stripped of install enumeration.
Observations
- Could not exercise a non-Ok install row in this happy-install environment, so the
status: "failed: <reason>"concatenation flagged in the code review remains untested at runtime. aspire installs(bare) returns exit code 1 when displaying help — minor UX nit (see Scenario 6).
Notes on coverage gaps
- Windows-only behaviors not exercised: the
PathLookupHelperPATHEXT casing fix and theWingetFirstRunProbestartup hoist are Windows-specific. Verified covered by the new unit tests (WindowsRegistryReaderTests,WingetStartupProbeTests,PathLookupHelperTests) but not end-to-end on Windows here. - Orphan hive listing not exercised because no extra hives exist in the isolated test $HOME. Covered by
InstallsCommandTests.
`GetInstallStatus` was concatenating `$"{install.Status}: {install.StatusReason}"`
for non-Ok rows, so the JSON `status` field became `"failed: <reason>"` and the
human "list" output printed the reason twice — once in the `Status` line, once
in the `Reason` line — because `InstallListItem.StatusReason` is already wired
to `install.StatusReason` separately.
Keep `status` enum-shaped (`active|shadowed|notOnPath|failed|notProbed|no install found`)
so programmatic consumers can `switch` on it without splitting on `": "`, and let
`statusReason` carry the human-readable message alone. The human-format `Reason`
field already renders only when `StatusReason` is non-empty, so the on-screen
duplication disappears for free once `status` stops embedding the reason.
Also extract the literal `"no install found"` into `OrphanHiveStatus` const so
the orphan-row constructor and the `GetSortRank` switch share a single source
of truth — otherwise a rename of one without the other would silently demote
orphan rows out of their last-sorted bucket into the `_ => 3` fallback.
Update `docs/specs/cli-output-formats.md` to drop the `failed: <reason>` /
`notProbed: <reason>` shapes from the documented `status` set, call out that
`statusReason` is where the message lives, and add a failed-row example.
|
Thanks for the thorough PR-test pass @mitchdenny. All three inline comments addressed in aa0d308:
On Scenario 6 (bare CI green, ready for re-review. |
PR Testing ReportPR Information
CLI Version Verification
Install path: Changes Analyzed (per PR body)
Test Scenarios ExecutedS1: Install + version verification — ✅ Passed
S2:
|
| Scenario | Coverage | Status |
|---|---|---|
| S1: Install + version verification | Happy | ✅ |
S2: installs list (text) |
Happy | ✅ |
S3: installs list --format json |
Happy | ✅ |
S4: doctor (no install table, no --self) |
Happy | ✅ |
S5: installs --self --format json |
Happy | ✅ |
S6: aspire new aspire-empty smoke |
Happy | ✅ |
U1: doctor --self (flag removed) |
Unhappy | ✅ |
U2: installs list --format bogus |
Unhappy | ✅ |
| U3: orphan hive surfaces | Boundary | ✅ |
Overall Result
✅ PR VERIFIED — all 9 scenarios pass. The new aspire installs surface, the JSON contract, and the aspire doctor slim-down all behave exactly as the PR description states.
Observations / Recommendations
- Exit codes for unknown args / invalid enum values are 0. Pre-existing System.CommandLine behavior, not introduced by this PR. Scripts that previously branched on
aspire doctor --selffailing won't observe a non-zero exit when they get migrated. - Orphan hive detection is AspireHome-scoped. A PR install can't see orphan hives in
~/.aspire/hives/, and vice-versa. The PR description's example assumes a single AspireHome; consider one sentence indocs/specs/install-sources.md(if not already) calling out that orphan rows are per-AspireHome. - First-run welcome animation interleaves with
installs listoutput untilASPIRE_NON_INTERACTIVE=1is set. Existing first-run UX; not a PR-introduced issue.
Evidence
Logs saved under <testDir>/evidence/:
s2-installs-list.txt,s2b-installs-help.txt,s2c-installs-list-help.txts3-installs-list-json.txts4-doctor.txt,s4b-doctor-help.txts5-installs-self-json.txts6-new.txtu1-doctor-self.txt,u2-installs-bogus-format.txtu3-orphan-hive-json.txt,u3-orphan-hive-text.txt
PR Testing Report — Windows x64Platform: Windows 11 / x64 / PowerShell 7. Scenarios were chosen to exercise the Windows-only surfaces this PR adds (winget probe + Setup
CLI Version Verification
Scenario Results
Detailed FindingsScenario 7 —
|
Description
Aspire CLI install enumeration lived inside
aspire doctoralongside theenvironment checks. That mixed two unrelated concerns into one command, and
the install table couldn't be inspected without running the full
prerequisite check pass each time.
A new
aspire installscommand surface owns CLI install enumeration.aspire doctornow reports environment checks only.User-facing usage
List every discovered CLI install plus any orphan package hives:
The vertical, color-aware layout shows each install's
Status,Channel,binary
Path,Hivepath, and the reason if a row is in a non-okstate.For tooling,
--format jsonemits the contract documented indocs/specs/cli-output-formats.md:[ { "id": "stable", "kind": "homebrew", "channel": "stable", "path": "/opt/homebrew/Caskroom/aspire/13.2.0/aspire", "status": "active", "managedBy": "homebrew" }, { "id": "pr-17400", "kind": "orphan-hive", "channel": "pr-17400", "hive": "/home/user/.aspire/hives/pr-17400", "status": "no install found", "statusReason": "No discovered install reports this hive's channel." } ]Changes
The diff is large (75 files, ~1700 net) but most of it is mechanical. By area:
aspire installscommand (new) —src/Aspire.Cli/Commands/InstallsCommand.csis new (~330 lines). The hidden--self --format jsonpeer-probe endpoint is documented as an internal cross-version contract indocs/specs/cli-output-formats.md. Peer-probe wiring moves fromaspire doctor --selftoaspire installs --self.aspire doctorshrinks to env checks — install-table rendering removed fromDoctorCommand.cs;DoctorCommand.csno longer takes the--selfflag.DoctorCommandStrings.{resx,Designer.cs}and all 13.xlftranslations are deleted (~900 lines net deletion).route→source— ~30 files. Code, tests, installer scripts (get-aspire-cli{,-pr}.{sh,ps1},eng/homebrew/aspire.rb.template,localhive.{sh,ps1}), and docs (docs/specs/install-routes.md→install-sources.md). On-disk wire strings are unchanged —script/pr/winget/brew/dotnet-tool/localhiveall stay literal. Only C# identifiers (e.g.InstallSource.Brew→InstallSource.Homebrew), filenames, and prose were renamed. Sidecar field name remainssource(it always was).WingetFirstRunProbe.Runinvocation moved fromBundleService+InstallsCommand.ListCommandinto theCliExecutionContextDI factory inProgram.cs. Same behavior, single call site;InstallsCommandandBundleServiceare now installer-agnostic.InstallsCommandTestscovers the new command surface.WindowsRegistryReaderTests(Windows-only) round-trips against the realHKCU\…\Uninstallhive withMicrosoft.Aspire.Tests.<guid>-prefixed entries that are deleted in ausingand swept by a static initializer on each test run.WingetStartupProbeTestspins the new DI-factory invocation contract and the swallow-on-failure behavior.PathLookupHelperon Windows preserves on-disk executable casing afterPATHEXTresolution (soaspire.exedoes not render asaspire.EXEjust becausePATHEXTcontains.EXE).Checklist
<remarks />and<code />elements on your triple slash comments?