|
| 1 | +# **Proposal to Surface Sponsorship Information in the CLI** |
| 2 | + |
| 3 | +- Author: [@kalebfik](https://github.com/kalebfik) |
| 4 | +- GitHub Issue: [10703](https://github.com/NuGet/NuGetGallery/issues/10703) |
| 5 | +- [Accompanying Technical Implementation Spec](https://github.com/NuGet/Home/blob/dev-kalebfika-sponsorTechSpec/accepted/2026/implementing-sponsorship-in-cli.md) |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +This spec proposes surfacing sponsorship links in the command line using the command `dotnet package list --sponsor`, so that users are able to see which packages they use are looking for funding. |
| 10 | +This feature is opt-in and reports sponsorship information throughout a project. |
| 11 | + |
| 12 | +## Motivation |
| 13 | + |
| 14 | +Package maintainers can already add sponsorship links (GitHub Sponsors, Patreon, Open Collective, etc.) to their packages on nuget.org. [See the package sponsorship documentation for more information](https://learn.microsoft.com/en-us/nuget/nuget-org/package-sponsorship-on-nuget-org). |
| 15 | +Developers only see this today if they visit the package's nuget.org page. |
| 16 | +As a result, consumers who primarily use the command line to navigate their packages may not know that the dependencies they rely on are seeking support. |
| 17 | +The maintainers of those packages receive limited visibility for sponsorship links they may have provided on the website. |
| 18 | + |
| 19 | +The CLI is an appropriate place to add sponsorship information because there is already a way to review current dependencies using `dotnet package list`. |
| 20 | +This is the most relevant workflow to implement sponsorship information. |
| 21 | + |
| 22 | +## Goals |
| 23 | + |
| 24 | +- Make sponsorship information discoverable through the CLI using `dotnet package list --sponsor` |
| 25 | +- Help consumers and organizations identify packages seeking financial support |
| 26 | +- Increase visibility for sponsorship links provided by package maintainers |
| 27 | +- Keep the experience explicit and separate from restore |
| 28 | + |
| 29 | +## Non-Goals |
| 30 | + |
| 31 | +- Process sponsorship payments |
| 32 | +- Management of sponsorship links from the CLI |
| 33 | +- Display sponsorship features during restore |
| 34 | +- Validation of sponsorship links |
| 35 | +- Ranking or prioritizing sponsorship links from the CLI |
| 36 | + |
| 37 | +**Why not restore?** |
| 38 | + |
| 39 | +Restore is a process that is used frequently during builds and other background IDE operations. |
| 40 | +Sponsorship information is not required to resolve builds or install packages, so retrieving that information during the restore process could add network costs to a performance-sensitive operation without providing adequate justification for the extra cost. |
| 41 | +Keeping it opt-in through the `--sponsor` flag ensures that it is retrieved only when the user is explicitly asking for it. |
| 42 | + |
| 43 | +**Who this affects** |
| 44 | + |
| 45 | +- **Package consumers** get a way to discover, from the command line, which of their dependencies are seeking sponsorship — without having to visit nuget.org for every package individually. |
| 46 | +- **Package authors** who've already added sponsorship links to their packages get more visibility for those links, since consumers can now see them as part of a workflow (`dotnet package list --sponsor`), not just on the package's web page. |
| 47 | +- **Repository/org admins** have an easier way to see sponsorship status alongside the other package information they already manage day-to-day (versions, licenses, vulnerabilities). |
| 48 | + |
| 49 | +## Explanation |
| 50 | + |
| 51 | +### Functional Explanation |
| 52 | + |
| 53 | +**Proposed Experience** |
| 54 | + |
| 55 | +When a user runs `dotnet package list --sponsor`, output is grouped by project and the source used, and includes sponsorship links for each matching package: |
| 56 | + |
| 57 | +`dotnet package list --sponsor` output: |
| 58 | + |
| 59 | +```text |
| 60 | +// sample response |
| 61 | +Project 'Contoso.App' has the following sponsorable packages |
| 62 | +Top-level Package Sponsor |
| 63 | +> Contoso.Tools Source: https://api.example.org/v3/index.json |
| 64 | + https://github.com/sponsors/contoso |
| 65 | +Transitive Package |
| 66 | +> Contoso.Utility Source: https://api.example.org/v3/index.json |
| 67 | + https://github.com/sponsors/contoso |
| 68 | + Source: https://www.myget.org/F/contoso/api/v3/index.json |
| 69 | + https://buymeacoffee.com/contoso |
| 70 | +``` |
| 71 | + |
| 72 | +Sponsorship information is applied to the package ID rather than the version of a package. |
| 73 | +Therefore, sponsorship information remains the same for a package regardless of which package version is published, as long as those links have not changed. |
| 74 | +When a package has multiple sponsorship links, the CLI will display the links in the order returned by nuget.org. |
| 75 | + |
| 76 | +Both top-level and transitive packages will be included by default when using `--sponsor`. |
| 77 | +This behavior will be documented in the command's help description. |
| 78 | + |
| 79 | +The command also supports `dotnet package list --sponsor --format json` and will produce output such as: |
| 80 | + |
| 81 | +```json |
| 82 | +{ |
| 83 | + "version": 1, |
| 84 | + "parameters": "--sponsor --format json", |
| 85 | + "sources": [ |
| 86 | + "https://api.example.org/v3/index.json", |
| 87 | + "https://www.myget.org/F/contoso/api/v3/index.json" |
| 88 | + ], |
| 89 | + "packages": [ |
| 90 | + { |
| 91 | + "id": "Contoso.Tools", |
| 92 | + "projects": [ |
| 93 | + { |
| 94 | + "path": "/path/to/Contoso.App.csproj", |
| 95 | + "relationship": "topLevel" |
| 96 | + } |
| 97 | + ], |
| 98 | + "sponsorships": [ |
| 99 | + { |
| 100 | + "sources": [ |
| 101 | + "https://api.example.org/v3/index.json" |
| 102 | + ], |
| 103 | + "urls": [ |
| 104 | + "https://github.com/sponsors/contoso" |
| 105 | + ] |
| 106 | + } |
| 107 | + ] |
| 108 | + }, |
| 109 | + { |
| 110 | + "id": "Contoso.Utility", |
| 111 | + "projects": [ |
| 112 | + { |
| 113 | + "path": "/path/to/Contoso.App.csproj", |
| 114 | + "relationship": "transitive" |
| 115 | + } |
| 116 | + ], |
| 117 | + "sponsorships": [ |
| 118 | + { |
| 119 | + "sources": [ |
| 120 | + "https://api.example.org/v3/index.json" |
| 121 | + ], |
| 122 | + "urls": [ |
| 123 | + "https://github.com/sponsors/contoso" |
| 124 | + ] |
| 125 | + }, |
| 126 | + { |
| 127 | + "sources": [ |
| 128 | + "https://www.myget.org/F/contoso/api/v3/index.json" |
| 129 | + ], |
| 130 | + "urls": [ |
| 131 | + "https://buymeacoffee.com/contoso" |
| 132 | + ] |
| 133 | + } |
| 134 | + ] |
| 135 | + } |
| 136 | + ] |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +Each package source is listed once, with the projects where it is used and its top-level or transitive relationships recorded. |
| 141 | +If multiple sources return the same ordered URL list, the JSON represents the list once and includes all sources in the same `sponsorships` entry. |
| 142 | +If multiple sources return different URL lists, they are represented in separate `sponsorships` entries. |
| 143 | + |
| 144 | +**No Sponsorship Details Returned** |
| 145 | + |
| 146 | +The command selects enabled package sources from NuGet configuration. |
| 147 | +When a source is specified using `--source <SOURCE>`, only that source is queried for sponsorship information. |
| 148 | +A successful Registration response with a missing or empty `sponsorshipUrls` field is treated as a successful empty result. |
| 149 | + |
| 150 | +Console output includes only sources that return one or more sponsorship URLs. |
| 151 | +`https://api.nuget.org/v3/index.json` will be recommended when it is not part of the user's configured sources. |
| 152 | +If none of the selected sources return sponsorship details for a project, the CLI displays: |
| 153 | + |
| 154 | +```text |
| 155 | +// sample response |
| 156 | +No sponsorship details were returned using the following package sources: |
| 157 | + https://api.example.org/v3/index.json |
| 158 | +
|
| 159 | +Consider specifying an additional package source that provides sponsorship metadata, for example: `--source https://api.nuget.org/v3/index.json`. |
| 160 | +``` |
| 161 | + |
| 162 | +A source that does not support sponsorships will have a separate message: |
| 163 | + |
| 164 | +```text |
| 165 | +These sources do not provide sponsorship support: |
| 166 | + C:/Program Files (x86)/Microsoft SDKs/NuGetPackages/ |
| 167 | +``` |
| 168 | + |
| 169 | +The JSON output includes every selected source in the top-level `sources` list, which includes supported and unsupported sources, along with local sources. |
| 170 | +Only packages that have at least one sponsorship URL will appear in `packages`. |
| 171 | +For an empty report, `packages` is empty, and `problems` contains one warning per empty or unsupported source. |
| 172 | + |
| 173 | +```json |
| 174 | +{ |
| 175 | + "version": 1, |
| 176 | + "parameters": "--sponsor --format json", |
| 177 | + "sources": [ |
| 178 | + "https://api.example.org/v3/index.json", |
| 179 | + "C:/Program Files (x86)/Microsoft SDKs/NuGetPackages/" |
| 180 | + ], |
| 181 | + "problems": [ |
| 182 | + { |
| 183 | + "level": "warning", |
| 184 | + "text": "There are no sponsorship details found at https://api.example.org/v3/index.json" |
| 185 | + }, |
| 186 | + { |
| 187 | + "level": "warning", |
| 188 | + "text": "This source does not provide sponsorship support: C:/Program Files (x86)/Microsoft SDKs/NuGetPackages/" |
| 189 | + } |
| 190 | + ], |
| 191 | + "packages": [] |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +**Package Sources, Package Source Mapping (PSM), and `--source`** |
| 196 | + |
| 197 | +A package source advertises sponsorship support through a new version of the Registration resource. |
| 198 | +The exact Registration version will be finalized with the server implementation. |
| 199 | + |
| 200 | +```json |
| 201 | +{ |
| 202 | + "@id": "https://api.nuget.org/v3/registration5-semver1/", |
| 203 | + "@type": "RegistrationsBaseUrl/{NEW_VERSION}", |
| 204 | + "comment": "Base URL of Azure storage where NuGet package registration info is stored. This base URL does not include SemVer 2.0.0 packages." |
| 205 | +} |
| 206 | +``` |
| 207 | + |
| 208 | +Registration requests may disclose package IDs to package sources; therefore, sponsorship reporting will honor Package Source Mapping (PSM). |
| 209 | +When PSM is disabled, each package is queried against all configured sources. |
| 210 | +When PSM is enabled, the client queries only sources that are mapped to that package. |
| 211 | + |
| 212 | +The command displays an informational message when using the command with PSM enabled: |
| 213 | + |
| 214 | +```text |
| 215 | +Package Source Mapping is enabled. Sponsorship details will be requested from sources mapped to packages. |
| 216 | +``` |
| 217 | + |
| 218 | +Users will encounter an error if PSM is combined with `--source`. |
| 219 | +The command stops before making any sponsorship requests, and the CLI will produce an error message: |
| 220 | + |
| 221 | +```text |
| 222 | +Package Source Mapping is enabled and cannot be combined with `--source` for sponsorship reporting. |
| 223 | +``` |
| 224 | + |
| 225 | +PSM is intended to ensure each package is accessed only through package sources that are explicitly mapped in a user's configuration. |
| 226 | +Using `--source` replaces the configured source selection and could select a source that is not represented by those mappings. |
| 227 | +Instead of bypassing or ignoring PSM, the command fails before making any requests. |
| 228 | +A user must add the desired source and package mappings to NuGet configuration or use a configuration without PSM. |
| 229 | + |
| 230 | +A mapped source that is successfully queried but returns an empty `sponsorshipUrls` property produces the same empty-source behavior present in the JSON above. |
| 231 | +This includes sources that do not propagate sponsorship metadata from an upstream source. |
| 232 | + |
| 233 | +| Scenario | Who this represents | Behavior | |
| 234 | +|---|---|---| |
| 235 | +| **NuGet.org only; no PSM** | A developer using the default public NuGet ecosystem. | Query every top-level and transitive package ID against NuGet.org. | |
| 236 | +| **NuGet.org and other sources; no PSM** | A developer or organization using public and private feeds. | Query every package ID against each selected sponsorship-capable source; retain unsupported selected sources as warnings in JSON. Group results by source. | |
| 237 | +| **Only private or third-party sources; no PSM** | A developer or organization not using NuGet.org directly. | Query every package ID against each selected sponsorship-capable source. Report other selected sources as unsupported. | |
| 238 | +| **PSM enabled** | An organization restricting package IDs to configured sources. | Query each package ID only against its mapped, sponsorship-capable configured sources. | |
| 239 | +| **PSM enabled with `--source`** | A user attempting to override configured source selection while mappings are active. | Fail before any sponsorship request and explain that PSM cannot be combined with `--source`. | |
| 240 | + |
| 241 | +### Technical Explanation |
| 242 | + |
| 243 | +The CLI will retrieve sponsorship information through the Registration API, similar to how other package metadata is passed to the CLI. |
| 244 | +The difference is that sponsorship information will be scoped by package ID only, whereas other metadata is scoped by package ID and version. |
| 245 | + |
| 246 | +A successful nuget.org response with one or more URLs will appear in the report, while packages with no sponsorship URLs will be absent. |
| 247 | + |
| 248 | +The client will display the URLs exactly as each source returns them. |
| 249 | +There is no validation, ranking, or prioritization done on the client side. |
| 250 | + |
| 251 | +A package source will indicate sponsorship support through a `metadata.sponsorshipUrls` field on the Registration index root: |
| 252 | + |
| 253 | +**Example registration root entry with `metadata` and `sponsorshipUrls` added:** |
| 254 | + |
| 255 | +```jsonc |
| 256 | +{ |
| 257 | + "@id": "https://api.nuget.org/v3/registration5-gz-semver2/contoso.webapi.client/index.json", |
| 258 | + "@type": [ |
| 259 | + "catalog:CatalogRoot", |
| 260 | + "PackageRegistration", |
| 261 | + "catalog:Permalink" |
| 262 | + ], |
| 263 | + "commitId": "afa91af1-9505-41b8-ad75-eab8e613db14", |
| 264 | + "commitTimeStamp": "2026-04-10T00:15:25.1492389+00:00", |
| 265 | + "count": 2, |
| 266 | + // ** Start of proposal ** // |
| 267 | + "metadata": { |
| 268 | + "sponsorshipUrls": [ |
| 269 | + "https://github.com/sponsors/contoso" |
| 270 | + ] |
| 271 | + } |
| 272 | + // ** End of proposal ** // |
| 273 | +} |
| 274 | +``` |
| 275 | + |
| 276 | +- **Scope:** solution-wide across all projects. |
| 277 | +- **Transitive packages:** Included by default; when using `dotnet package list --help`, the description will state that transitive packages are included by default. |
| 278 | +- **Multiple sponsorship URLs:** The CLI displays sponsorship URLs in the order received from the package source. |
| 279 | +- **nuget.org URL limit:** nuget.org currently enforces a maximum of 10 URLs per package. |
| 280 | +- **Empty state:** A missing, null, or empty `sponsorshipUrls` value is treated as a successful empty result. |
| 281 | + |
| 282 | +## Drawbacks |
| 283 | + |
| 284 | +- At this time, nuget.org is the only source that supports sponsorship details in the Registration index root. |
| 285 | + - Other sources will produce a message saying that source does not support sponsorship reporting. |
| 286 | + - Sponsorship details from an Azure Artifacts upstream source are not shown unless that source exposes those details through its own Registration response. |
| 287 | + |
| 288 | +## Rationale and Alternatives |
| 289 | + |
| 290 | +Why `dotnet package list --sponsor`? Why not `dotnet package sponsor` or `dotnet package fund`? |
| 291 | + |
| 292 | +```bash |
| 293 | +dotnet package sponsor |
| 294 | +``` |
| 295 | + |
| 296 | +This approach treats sponsorships similarly to npm's `npm fund` command (see the appendix). |
| 297 | + |
| 298 | +A dedicated command could provide a clearer and more focused intent while also supporting future interactive experiences. |
| 299 | +That said, the intent for the proposed experience is to report package metadata, not to provide actions like selecting a provider or processing a sponsorship. |
| 300 | +If sponsorship grows into a bigger feature, a dedicated command could be revisited. |
| 301 | + |
| 302 | +## Prior Art/Related |
| 303 | + |
| 304 | +- [**Package sponsorships on nuget.org**](https://learn.microsoft.com/en-us/nuget/nuget-org/package-sponsorship-on-nuget-org): Package sponsorships can already be configured on nuget.org. |
| 305 | +- [**Companion server spec**](https://devdiv.visualstudio.com/DevDiv/_git/NuGet.Services/pullrequest/763096?_a=files&iteration=2&base=1): Proposes implementation for supporting package ID-level metadata in the Registration API (internal link). |
| 306 | +- **[`npm fund`](https://docs.npmjs.com/cli/v10/commands/npm-fund/)**: `npm fund` provides precedent for this pattern. The `funding` field lives in package metadata, and npm's guidance suggests keeping funding links at the package or author level. |
| 307 | + npm notes that funding information can be noisy in the CLI and that stale information could be problematic. |
| 308 | +- **`--deprecated`/`--vulnerable`**: precedent for opt-in report-style information that consumers already use and understand, informing this proposal's output/UX conventions. |
| 309 | +- **PM UI's existing project/license/report-abuse links**: precedent for Phase 2 — PM UI already shows package-supplied links to consumers using an established, low-risk pattern. |
| 310 | +- **[`nuget/home#14739`](https://github.com/nuget/home/issues/14739)**: Open issue for a PM UI sponsorship button. |
| 311 | + |
| 312 | +## Unresolved Questions |
| 313 | + |
| 314 | +None at this time. |
| 315 | + |
| 316 | +## Future Possibilities |
| 317 | + |
| 318 | +- **Multiple package sources:** Other sources adopt the new Registration resource version and can report sponsorship details. |
| 319 | +- **Dedicated sponsorship command:** Introduce a `dotnet package sponsor` or `dotnet package fund` command if sponsorships develop into a larger workflow. |
| 320 | +- **Interactive CLI experience:** Allow users to navigate a package's sponsorship links using their arrow keys and open one in their native browser. |
| 321 | +- **Filter top-level and transitive packages.** |
| 322 | +- **VS Package Manager UI hyperlinks** for sponsorship links, directly addressing [nuget/home#14739](https://github.com/nuget/home/issues/14739). |
| 323 | +- **AI agent integration:** Make sponsorship information available to package-management agents. See [Issue 14738](https://github.com/NuGet/Home/issues/14738). |
| 324 | + |
| 325 | +## Appendix |
| 326 | + |
| 327 | +### npm Fund Comparison |
| 328 | + |
| 329 | +npm provides a dedicated `npm fund` command that helps developers discover funding opportunities for packages they depend on. |
| 330 | +Package authors specify funding information directly in their package metadata using the `funding` field. |
| 331 | + |
| 332 | +```json |
| 333 | +{ |
| 334 | + "funding": { |
| 335 | + "type": "individual", |
| 336 | + "url": "http://example/donate" |
| 337 | + }, |
| 338 | + |
| 339 | + "funding": { |
| 340 | + "type": "patreon", |
| 341 | + "url": "https://domain/my-account" |
| 342 | + } |
| 343 | +} |
| 344 | +``` |
| 345 | + |
| 346 | +When a package containing funding metadata is installed, npm displays a summary message: |
| 347 | + |
| 348 | +```text |
| 349 | +Added 342 packages in 4s |
| 350 | +
|
| 351 | +3 packages are looking for funding |
| 352 | + run `npm fund` for details |
| 353 | +``` |
| 354 | + |
| 355 | +After running `npm fund`, users receive this output: |
| 356 | + |
| 357 | +```text |
| 358 | +my-project |
| 359 | +├── https://domain/sponsors/user |
| 360 | +│ └── express@4.21.2 |
| 361 | +├── https://domain/user |
| 362 | +│ └── @babel/core@7.28.0 |
| 363 | +└── https://domain/sponsors/user |
| 364 | + └── p-limit@7.1.1 |
| 365 | +``` |
| 366 | + |
| 367 | +**Characteristics of npm's approach** |
| 368 | + |
| 369 | +- Funding information is stored within package metadata. |
| 370 | +- Funding information is distributed through the package ecosystem. |
| 371 | +- Funding discovery has a dedicated command surface (`npm fund`). |
| 372 | + |
| 373 | +**Comparison with Proposed Approach** |
| 374 | + |
| 375 | +npm: Funding information is directly embedded in package metadata and distributed throughout the npm ecosystem. |
| 376 | + |
| 377 | +```bash |
| 378 | +npm fund |
| 379 | +``` |
| 380 | + |
| 381 | +Dedicated funding workflow. |
| 382 | + |
| 383 | +NuGet: Sponsorship information is retrieved from the selected package source and surfaced through client tooling. |
| 384 | + |
| 385 | +```bash |
| 386 | +dotnet package list --sponsor |
| 387 | +``` |
| 388 | + |
| 389 | +Report-style experience aligned with existing commands. |
| 390 | + |
| 391 | +**Why not mirror npm exactly?** |
| 392 | + |
| 393 | +1. Sponsorship information needs to be queried from the package source rather than embedded within the package. |
| 394 | +2. Sponsorship discovery follows existing NuGet resolution behavior. |
| 395 | +3. Restore output remains unchanged to avoid additional noise. |
| 396 | +4. Users explicitly opt into sponsorship discovery instead of receiving sponsorship messaging during restore operations. |
0 commit comments