diff --git a/docs/dogfooding-pull-requests.md b/docs/dogfooding-pull-requests.md index dafd90fe7d1..12cf09188fd 100644 --- a/docs/dogfooding-pull-requests.md +++ b/docs/dogfooding-pull-requests.md @@ -329,9 +329,9 @@ The file is scoped to the solution directory and only affects projects under it. The Homebrew cask (`eng/homebrew/aspire.rb.template`) installs Aspire entirely inside the Caskroom version directory — `brew uninstall aspire` removes -the binary and the route sidecar end-to-end. The cask intentionally carries -no `zap` stanza, because `~/.aspire/` is a shared prefix with the script-route -and PR-route installers and a brew-driven recursive delete would clobber state +the binary and the source sidecar end-to-end. The cask intentionally carries +no `zap` stanza, because `~/.aspire/` is a shared prefix with the script-source +and PR-source installers and a brew-driven recursive delete would clobber state those installers still own. If you installed via the Homebrew cask before this change, you may have a @@ -343,7 +343,7 @@ Clean it up manually once after upgrading the cask: rm -rf ~/.aspire/installs/brew-stable ``` -NuGet hives under `~/.aspire/hives/` and any script-route or PR-route +NuGet hives under `~/.aspire/hives/` and any script-source or PR-source binaries under `~/.aspire/bin/` and `~/.aspire/dogfood/` are not touched by the cask in either direction; manage those with the steps above. diff --git a/docs/specs/cli-output-formats.md b/docs/specs/cli-output-formats.md index bbe2393303a..121517bb0fa 100644 --- a/docs/specs/cli-output-formats.md +++ b/docs/specs/cli-output-formats.md @@ -474,6 +474,74 @@ The JSON form includes secret values. Do not redirect it to logs or files unless `status` is one of `pass`, `warning`, or `fail`. Individual checks can include `details`, `fix`, `link`, or command-specific `metadata`. +### `aspire --info` + +`aspire --info --format json` emits a JSON object combining the running CLI's version, its identity channel, and the list of Aspire CLI installs and orphan package hives that the running CLI can discover: + +```json +{ + "version": "13.2.0", + "channel": "stable", + "installs": [ + { + "id": "script", + "kind": "script", + "path": "/home/user/.aspire/bin/aspire", + "canonicalPath": "/home/user/.aspire/bin/aspire", + "version": "13.2.0", + "channel": "stable", + "source": "script", + "hive": "/home/user/.aspire/hives/stable", + "pathStatus": "active", + "status": "ok" + }, + { + "id": "pr-17400", + "kind": "orphan-hive", + "channel": "pr-17400", + "hive": "/home/user/.aspire/hives/pr-17400", + "pathStatus": "notOnPath", + "status": "no install found", + "statusReason": "No discovered install reports this hive's channel." + }, + { + "id": "stable-2", + "kind": "homebrew", + "path": "/opt/homebrew/Caskroom/aspire/13.2.0/aspire", + "canonicalPath": "/opt/homebrew/Caskroom/aspire/13.2.0/aspire", + "version": "13.2.0", + "channel": "stable", + "source": "brew", + "pathStatus": "shadowed", + "status": "ok", + "managedBy": "homebrew" + }, + { + "id": "pr-17500", + "kind": "pr", + "path": "/home/user/.aspire/dogfood/pr-17500/bin/aspire", + "canonicalPath": "/home/user/.aspire/dogfood/pr-17500/bin/aspire", + "channel": "pr-17500", + "source": "pr", + "pathStatus": "notOnPath", + "status": "failed", + "statusReason": "Peer probe exited with code 137 (SIGKILL)." + } + ] +} +``` + +Each entry in `installs[*]` is an `InstallationInfo` row — the same record shape used by `aspire --info --self --format json` (described below). The two status axes are orthogonal: + +- `status` is the lifecycle: `ok` (usable), `failed` (probe attempted but did not return usable data), `notProbed` (listed but not executed because required install metadata was missing or invalid), or `no install found` (orphan-hive directories with no matching binary). +- `pathStatus` is the PATH-axis: `active` (first `aspire` resolved from `$PATH`), `shadowed` (on `$PATH` but masked by an earlier entry), or `notOnPath` (not discovered via `$PATH`). + +Programmatic consumers should `switch` on each axis independently. The `statusReason` field carries a human-readable explanation for non-`ok` rows; the `status` value itself stays enum-shaped. Nullable fields are omitted from JSON when absent (e.g., orphan-hive rows omit `path`, `canonicalPath`, `version`, `source`, `managedBy`). + +The top-level `channel` is the running CLI's identity (`local`, `stable`, `staging`, `daily`, or `pr-`). It is omitted from the JSON output when the running binary has missing or invalid `AspireCliChannel` assembly metadata; the command still exits 0 so users can use `aspire --info` to diagnose a broken binary. The `installs` array is always present, possibly empty. + +`aspire --info --self --format json` is a hidden modifier used by the install-discovery peer-probe path so a newer CLI can ask a peer CLI to describe itself. Its output is a single-element bare array whose element is an `InstallationInfo` row — the same record shape as `installs[*]` above. The `--self` row populates per-binary fields (`path`, `canonicalPath`, `version`, `channel`, `source`, `pathStatus`, `status`) for the running binary; the aggregate-only fields (`id`, `kind`, `hive`, `managedBy`) are omitted from a `--self` response because they are properties of the enumeration, not of the binary. This is the internal cross-version contract between Aspire CLI builds and is not a stable surface for external tooling; it may change without notice. + ### `aspire config info` `aspire config info --json` is a hidden tooling command that emits configuration paths, feature metadata, settings schemas, and advertised CLI capabilities: diff --git a/docs/specs/install-routes.md b/docs/specs/install-sources.md similarity index 56% rename from docs/specs/install-routes.md rename to docs/specs/install-sources.md index d2b8f2c8c72..690327b7cd3 100644 --- a/docs/specs/install-routes.md +++ b/docs/specs/install-sources.md @@ -1,8 +1,8 @@ -# Aspire CLI install-route sidecar +# Aspire CLI install-source sidecar > Pairs with `docs/specs/bundle.md` (bundle extraction layout) and `docs/ci/native-cli-packaging.md` (how archives are produced). -The CLI binary identifies its install route by reading a single +The CLI binary identifies its install source by reading a single `.aspire-install.json` sidecar that lives next to the binary. The sidecar's `source` field selects the extract-dir shape used by `BundleService` and, for portable installs, the Aspire home used for hives and local state. @@ -13,10 +13,10 @@ portable installs, the Aspire home used for hives and local state. (`/.aspire-install.json`) and contains exactly one field: ```json -{ "source": "" } +{ "source": "" } ``` -| `source` value | Install route | +| `source` value | Install source | |----------------|--------------------------------------------------------| | `brew` | Homebrew cask | | `winget` | WinGet portable manifest | @@ -37,26 +37,26 @@ install. `script` and `localhive` use the parent of `bin`; `pr` uses the parent of `dogfood/pr-/bin`. Package-manager installs and sidecar-less binaries keep the default Aspire home (`ASPIRE_HOME` when set, otherwise `$HOME/.aspire`). -## Per-route authorship +## Per-source authorship -**The shared per-RID CLI archives (`aspire-cli--*.zip` / `.tar.gz`) ship sidecar-free.** Those archives are reused across brew, winget, the release script, and the PR script — none of them owns the route label. Each route writes its own sidecar at install time. +**The shared per-RID CLI archives (`aspire-cli--*.zip` / `.tar.gz`) ship sidecar-free.** Those archives are reused across Homebrew, WinGet, the release script, and the PR script — none of them owns the source label. Each source writes its own sidecar at install time. -| Route | Archive shape | Sidecar writer | +| Source | Archive shape | Sidecar writer | |-------------|----------------------------------------|---------------------------------------------------------------------| -| brew | shared per-RID tarball | cask `postflight` block in `eng/homebrew/aspire.rb.template` | -| winget | shared per-RID zip | CLI first-run probe (`WingetFirstRunProbe`) — uses the WinGet portable ARP registry entry to confirm the running binary was placed by winget, then stamps the sidecar | +| Homebrew | shared per-RID tarball | cask `postflight` block in `eng/homebrew/aspire.rb.template` | +| winget | shared per-RID zip | CLI sidecar back-fill (`WingetSidecarBackfill`) — uses the WinGet portable ARP registry entry to confirm the running binary was placed by winget, then stamps the sidecar | | script | shared per-RID archive | `eng/scripts/get-aspire-cli.{sh,ps1}` (post-extraction) | | PR script | shared per-RID archive | `eng/scripts/get-aspire-cli-pr.{sh,ps1}` (post-extraction) | -| dotnet-tool | route-exclusive nupkg | payload-embedded (staged by `Aspire.Cli.csproj` `_PreparePreBuiltCliBinaryForPackTool`) | -| localhive | local-only (no shared archive) | `localhive.{sh,ps1}` writes the sidecar after copying the CLI binary into `/bin/`. When `--output PATH` is used, the sidecar is written inside the output dir, which is appropriate because localhive archives are route-exclusive (only consumed as localhive installs). | +| dotnet-tool | source-exclusive nupkg | payload-embedded (staged by `Aspire.Cli.csproj` `_PreparePreBuiltCliBinaryForPackTool`) | +| localhive | local-only (no shared archive) | `localhive.{sh,ps1}` writes the sidecar after copying the CLI binary into `/bin/`. When `--output PATH` is used, the sidecar is written inside the output dir, which is appropriate because localhive archives are source-exclusive (only consumed as localhive installs). | -The dotnet-tool nupkg is the one exception that payload-embeds the sidecar: the nupkg is route-exclusive (only `dotnet tool install` consumes it), so the embedded sidecar cannot leak into another route's prefix. +The dotnet-tool nupkg is the one exception that payload-embeds the sidecar: the nupkg is source-exclusive (only `dotnet tool install` consumes it), so the embedded sidecar cannot leak into another source's prefix. ## Why no payload-embed in shared archives -Until PR 16817 the per-RID archives baked `{"source":"brew"}` (osx-*) and `{"source":"winget"}` (win-*) into the archive root via an MSBuild target. Because the osx-* tarball is also consumed by `get-aspire-cli-pr.sh`, the smuggled `brew` sidecar landed in the script-route prefix at `/dogfood/pr-/bin/.aspire-install.json`, and `BundleService` then selected `binaryDir` (the `brew` flat-layout case) as the extract dir — producing `/dogfood/pr-/bin/versions//` instead of `/dogfood/pr-/versions//`. +Until PR 16817 the per-RID archives baked source sidecars (`osx-*` as Homebrew, `win-*` as WinGet) into the archive root via an MSBuild target. Because the `osx-*` tarball is also consumed by `get-aspire-cli-pr.sh`, the smuggled `brew` sidecar landed in the script-source prefix at `/dogfood/pr-/bin/.aspire-install.json`, and `BundleService` then selected `binaryDir` (the `brew` flat-layout case) as the extract dir — producing `/dogfood/pr-/bin/versions//` instead of `/dogfood/pr-/versions//`. -Removing the MSBuild target and moving each route to author its own sidecar at install time makes the per-RID archive route-agnostic and prevents the leak by construction. +Removing the MSBuild target and moving each source to author its own sidecar at install time makes the per-RID archive source-agnostic and prevents the leak by construction. ## Producer-side invariants (build / CI) @@ -67,10 +67,10 @@ Two mechanical checks guard the contract: ## Reader-side invariants (runtime) -`BundleService.ComputeDefaultExtractDir` is the single point of truth for layout selection. It performs no path-shape detection: layout is a pure function of the sidecar `source` value (or the fallback when the sidecar is absent, unreadable, malformed, or has an unknown `source`). Unknown `source` values fall back to the default Aspire home so unrecognized installs do not try to write next to the CLI binary. Coverage lives in `tests/Aspire.Cli.Tests/Bundles/BundleServiceCrossRouteExtractionTests.cs` as a theory over (source × prefix-shape) rows, including the cross-route case where a `brew` sidecar lands under a script-style prefix. +`BundleService.ComputeDefaultExtractDir` is the single point of truth for layout selection. It performs no path-shape detection: layout is a pure function of the sidecar `source` value (or the fallback when the sidecar is absent, unreadable, malformed, or has an unknown `source`). Unknown `source` values fall back to the default Aspire home so unrecognized installs do not try to write next to the CLI binary. Coverage lives in `tests/Aspire.Cli.Tests/Bundles/BundleServiceCrossSourceExtractionTests.cs` as a theory over (source × prefix-shape) rows, including the cross-source case where a `brew` sidecar lands under a script-style prefix. -`CliPathHelper.GetAspireHomeDirectory` is the single point of truth for Aspire-home selection. It reads the same sidecar but only changes home for Aspire-owned portable routes (`script`, `pr`, and `localhive`); package-manager routes use the user-profile home because their install roots are package-manager-owned. Coverage lives in `tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs`. +`CliPathHelper.GetAspireHomeDirectory` is the single point of truth for Aspire-home selection. It reads the same sidecar but only changes home for Aspire-owned portable sources (`script`, `pr`, and `localhive`); package-manager sources use the user-profile home because their install roots are package-manager-owned. Coverage lives in `tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs`. -> **Discovery scope (dotnet-tool route).** Install discovery walks the default `dotnet tool install -g` location at `~/.dotnet/tools/.store/aspire.cli` only. Custom `--tool-path` installs are not discovered today: the dotnet CLI has no machine-wide registry of arbitrary `--tool-path` installs to enumerate, and walking the filesystem would balloon the cost of `aspire doctor`. Users with a custom-`--tool-path` install can confirm it directly with `/aspire doctor --self`. +> **Discovery scope (dotnet-tool source).** Install discovery walks the default `dotnet tool install -g` location at `~/.dotnet/tools/.store/aspire.cli` only. Custom `--tool-path` installs are not discovered today: the dotnet CLI has no machine-wide registry of arbitrary `--tool-path` installs to enumerate, and walking the filesystem would balloon the cost of `aspire --info`. Users with a custom-`--tool-path` install can confirm it directly with `/aspire --info --self`. -For read-only install discovery (`aspire doctor --format json`), sidecar existence is the trust signal for peer probing. A candidate with any readable sidecar is probed even when `source` is not in the known route table; the raw `source` string is surfaced as the installation `route` so future package-manager routes can appear before this consumer updates. Sidecar-less, unreadable, or malformed candidates are listed without executing the binary. +For read-only install discovery (`aspire --info --format json`), sidecar existence is the trust signal for peer probing. A candidate with any readable sidecar is probed even when `source` is not in the known source table; the raw `source` string is surfaced as the installation `source` so future package-manager sources can appear before this consumer updates. Sidecar-less, unreadable, or malformed candidates are listed without executing the binary. diff --git a/eng/clipack/Common.projitems b/eng/clipack/Common.projitems index d9bfc4e9dab..049c8403def 100644 --- a/eng/clipack/Common.projitems +++ b/eng/clipack/Common.projitems @@ -42,12 +42,12 @@