From 0eaff9303b97859920fc376de668774f125c5440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Sat, 19 Sep 2026 10:34:17 +0200 Subject: [PATCH 1/7] docs: propose structured CLI output contract --- doc/specs/#184 - Structured CLI Output.md | 162 ++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 doc/specs/#184 - Structured CLI Output.md diff --git a/doc/specs/#184 - Structured CLI Output.md b/doc/specs/#184 - Structured CLI Output.md new file mode 100644 index 0000000000..200ab00571 --- /dev/null +++ b/doc/specs/#184 - Structured CLI Output.md @@ -0,0 +1,162 @@ +--- +author: Przemysław Kłys +created on: 2026-09-19 +last updated: 2026-09-19 +issue id: 184 +--- + +# Structured CLI output + +For [#184](https://github.com/microsoft/winget-cli/issues/184) + +## Abstract + +Add an opt-in JSON output mode to commands that return package data, starting with `list` and the non-mutating form of `upgrade`. Define the data contract before shipping it, and put formatting in the reporter so later commands do not each invent their own JSON path. + +## Inspiration + +The PowerShell module and COM API already provide structured package data, but callers of `winget.exe` in other shells and tools still have to parse localized, width-dependent tables. [#6346](https://github.com/microsoft/winget-cli/pull/6346) demonstrates the first use case, while its review identified two things to settle first: a stable shape aligned with the PowerShell objects, and a design that scales beyond two commands. + +This proposal is for CLI consumers; it does not replace the PowerShell module or COM API. PowerShell remains the richer option when an application can use it directly. + +## Solution Design + +### Scope and rollout + +Introduce `--format json` behind an experimental feature setting. The first supported commands are `winget list` and `winget upgrade` when `upgrade` is only listing available updates. The default table output does not change. Until a command has a defined schema and a typed result, it must reject `--format json` as unsupported, rather than return an empty document that looks like a successful query. Mutating `upgrade` modes such as `--all` remain unsupported in this first slice. + +The schema is reviewed with the first slice and stored with the client. The experimental gate gives room to correct the contract before it becomes stable; once stabilized, additions should be optional and existing field meanings should remain unchanged. Subsequent commands can adopt the same output machinery with command-specific result types and schemas. + +### Data contract + +One invocation writes one UTF-8 JSON document to stdout, followed by a newline. The envelope has a schema version, command name, package array, and error array. The initial proposal uses the PowerShell object's names and types for overlapping package fields. Envelope names are CLI-specific; they are not PowerShell package properties. + +```json +{ + "schemaVersion": "1.0", + "command": "list", + "packages": [ + { + "Name": "Example App", + "Id": "Example.App", + "InstalledVersion": "1.0.0", + "AvailableVersions": ["1.1.0"], + "IsUpdateAvailable": true, + "Source": "winget" + } + ], + "errors": [] +} +``` + +`Name`, `Id`, `InstalledVersion`, `AvailableVersions`, `IsUpdateAvailable`, and `Source` correspond to `PSInstalledCatalogPackage` properties. `AvailableVersions` contains the versions known from the matched source; it must not be populated by splitting or interpreting a rendered table cell. `Source` is the actual source name when known, even when the human table omits its Source column. Use `null` when the source cannot be established, not an empty string that could be mistaken for a hidden table column. An empty successful result uses `"packages": []` and `"errors": []`. + +The exact schema version and field casing are proposed here for review, not an assertion that the current implementation already emits this shape. The JSON schema should define required versus nullable fields, version string semantics, whether all available versions can be returned without extra catalog work, and whether `upgrade` needs any additional fields before implementation is finalized. Do not silently substitute only the selected upgrade candidate for `AvailableVersions` if PowerShell exposes more. + +Failures should be typed objects, for example: + +```json +{ + "schemaVersion": "1.0", + "command": "list", + "packages": [], + "errors": [ + { "code": "0x8A15000F", "message": "Source data is missing.", "source": "winget" } + ] +} +``` + +The example code and message are illustrative, not a new error mapping. `code` should carry the actual HRESULT in a fixed representation. Preserve the command's exit code. A partial source failure may return packages from healthy sources and an error entry for the failed source; a command-level failure still produces a parseable document when the reporter has been initialized. Catastrophic startup failures before argument parsing may remain outside this guarantee and should be documented separately. + +### Reporter and table ownership + +Select an output mode once in `Reporter`. Workflows pass typed results, warnings, and errors to it. The reporter serializes a command result to JSON, or renders the existing human output. Plain decorative strings are not JSON data and are suppressed from JSON stdout. `TableOutput` can consume typed rows and keep presentation choices such as column hiding and truncation on the text side; it must not be the source of truth for serialized fields. This avoids a new format branch at every output call site. + +The JSON document should be finalized once per invocation, including empty and error cases. Progress, settings warnings, source notices, and diagnostic logs must not precede or follow it on stdout. Diagnostics can use stderr where appropriate; typed warnings that callers need should be represented in the schema rather than mixed into the JSON stream. Tests should parse stdout as one document, not search for a JSON substring. + +Add a privacy-conscious telemetry event for use of the JSON mode, with command and success/failure category. Do not record package names, IDs, source contents, or the JSON payload. Existing telemetry controls continue to apply. + +### Other surfaces and validation + +No manifest fields, manifest JSON schemas, COM IDL interfaces, PowerShell cmdlets or output objects, WinGet Configuration resources, or group policies change in this proposal. The CLI JSON schema is separate from package manifests and from `winget export`'s existing interchange format; it is not a new manifest schema version. No `winget-create`, `winget-cli-restsource`, or `winget-pkgs` validation-pipeline change is required for the first slice. + +The new `experimentalFeatures` key defaults to false. The final key name should follow the repository's feature naming convention; one possible settings shape is: + +```json +{ + "experimentalFeatures": { + "structuredOutput": true + } +} +``` + +| Setting | `--format` | Result | +| --- | --- | --- | +| Off | Omitted | Existing text output | +| On | Omitted | Existing text output | +| On | `json` on supported command | JSON document | +| Off | `json` | Experimental-feature error | +| On | `json` on unsupported mode | Explicit unsupported-mode error | + +The settings flag permits the CLI argument; it does not make JSON the default format. `--no-vt` changes only terminal decoration, not JSON data. `--disable-interactivity` and noninteractive hosts use the same JSON contract. If a query would require a prompt, the command should fail with a typed error rather than interleave a prompt with JSON. `--help` keeps its normal help output, as it describes the command rather than executing a package query. + +| Command or mode | Interactive | `--no-vt` | Noninteractive | JSON behavior | +| --- | --- | --- | --- | --- | +| `list` | Supported | Supported | Supported | Installed package result | +| `upgrade` (listing) | Supported | Supported | Supported | Available-update result | +| `upgrade --all` or package execution | Not in first slice | Not in first slice | Not in first slice | Explicit unsupported-mode error | +| Other commands | Not in first slice | Not in first slice | Not in first slice | Explicit unsupported-command error | +| Empty result | Supported | Supported | Supported | Empty arrays, valid document | +| Source-open failure | Supported | Supported | Supported | Typed error, valid document where reporter is active | + +### Implementation and test boundary + +The first implementation PR should contain the CLI JSON schema and focused tests for schema validation, known source provenance on single- and multi-source systems, null/empty distinctions, package version fields, empty results, partial and command-level errors, exit codes, help, and unsupported execution modes. Existing table behavior must remain covered. The generic reporter path should be demonstrated by both `list` and listing-only `upgrade`, without duplicating serialization branches in each workflow. + +## UI/UX Design + +With the feature enabled, `winget list --format json` prints the single document shown above. `winget upgrade --format json` uses the same envelope with `"command": "upgrade"` and the agreed package schema. Without `--format json`, users see the current localized table. `--format json` on an unsupported command or mutating mode reports an explicit error and nonzero exit code; it must not silently perform an operation or present table text as JSON. + +## Capabilities + +### Accessibility + +The existing text presentation remains the default, including its accessibility behavior. JSON is intended for tools and is not a replacement for readable command output. + +### Security + +JSON values must be escaped by a serializer, never assembled by string concatenation. The format option does not widen source access or bypass policy and agreement checks. Telemetry excludes package and source data. + +### Reliability + +One parseable document, explicit empty results, typed failures, and exit-code preservation give callers a reliable contract. Source provenance must come from the data model, not table-column visibility. + +### Compatibility + +No existing default output or PowerShell/COM contract changes. The option is experimental while the schema is under review. Published schema versions need an explicit compatibility policy before the gate is removed. + +### Performance, Power, and Efficiency + +The JSON path should not open each package again merely to recreate display data. Returning every available version may require catalog access beyond today's table; measure that cost and settle the field semantics before freezing the schema. + +## Potential Issues + +- The PowerShell object model and CLI query semantics are related but not identical. A field name should not imply stronger data than the CLI can obtain without extra source work. +- A partial result with source errors needs an unambiguous exit-code policy; consumers should inspect both the process status and `errors`. +- Some failures occur before the reporter can select JSON mode. The documented guarantee must have a clear startup boundary. +- A generic reporter change could alter text output accidentally. Keep table regression tests alongside JSON tests. + +## Deprecation Path + +No existing feature is deprecated. The human table stays the default, and this proposal does not replace the PowerShell module, COM API, or `winget export` format. + +## Future considerations + +After the first schema and reporter path are agreed, `search`, `show`, and other result-producing commands can add their own typed results and schemas. Mutating commands may eventually expose structured progress and final results, but that is a separate contract and is not implied by this first slice. + +## Resources + +- [Structured output tracking issue #184](https://github.com/microsoft/winget-cli/issues/184) +- [Initial list/upgrade implementation PR #6346](https://github.com/microsoft/winget-cli/pull/6346) +- [PowerShell installed package object](https://github.com/microsoft/winget-cli/blob/master/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs) +- [Reporter](https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerCLICore/ExecutionReporter.h) and [TableOutput](https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerCLICore/TableOutput.h) From f4efc7a5297e9d8ca8d035673c432421eb14e869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Sat, 19 Sep 2026 11:02:48 +0200 Subject: [PATCH 2/7] Tidy structured output spec wording --- doc/specs/#184 - Structured CLI Output.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/specs/#184 - Structured CLI Output.md b/doc/specs/#184 - Structured CLI Output.md index 200ab00571..31d7f8db98 100644 --- a/doc/specs/#184 - Structured CLI Output.md +++ b/doc/specs/#184 - Structured CLI Output.md @@ -1,5 +1,5 @@ --- -author: Przemysław Kłys +author: Przemysław Kłys created on: 2026-09-19 last updated: 2026-09-19 issue id: 184 @@ -66,7 +66,7 @@ Failures should be typed objects, for example: } ``` -The example code and message are illustrative, not a new error mapping. `code` should carry the actual HRESULT in a fixed representation. Preserve the command's exit code. A partial source failure may return packages from healthy sources and an error entry for the failed source; a command-level failure still produces a parseable document when the reporter has been initialized. Catastrophic startup failures before argument parsing may remain outside this guarantee and should be documented separately. +The example code and message are illustrative, not a new error mapping. `code` should carry the actual HRESULT in a fixed representation. Preserve the command's exit code. A partial source failure may return packages from healthy sources and an error entry for the failed source; a command-level failure still produces a machine-readable document when the reporter has been initialized. Catastrophic startup failures before argument parsing may remain outside this guarantee and should be documented separately. ### Reporter and table ownership @@ -129,7 +129,7 @@ JSON values must be escaped by a serializer, never assembled by string concatena ### Reliability -One parseable document, explicit empty results, typed failures, and exit-code preservation give callers a reliable contract. Source provenance must come from the data model, not table-column visibility. +One machine-readable document, explicit empty results, typed failures, and exit-code preservation give callers a reliable contract. Source provenance must come from the data model, not table-column visibility. ### Compatibility From 44d53528e32aea69b55545776d0550cfd16c1f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Sun, 20 Sep 2026 16:56:02 +0200 Subject: [PATCH 3/7] docs: clarify structured output contract --- doc/specs/#184 - Structured CLI Output.md | 224 +++++++++++++++++----- 1 file changed, 171 insertions(+), 53 deletions(-) diff --git a/doc/specs/#184 - Structured CLI Output.md b/doc/specs/#184 - Structured CLI Output.md index 31d7f8db98..6036a003ad 100644 --- a/doc/specs/#184 - Structured CLI Output.md +++ b/doc/specs/#184 - Structured CLI Output.md @@ -1,7 +1,7 @@ --- -author: Przemysław Kłys +author: Przemysław Kłys @PrzemyslawKlys created on: 2026-09-19 -last updated: 2026-09-19 +last updated: 2026-09-20 issue id: 184 --- @@ -11,7 +11,7 @@ For [#184](https://github.com/microsoft/winget-cli/issues/184) ## Abstract -Add an opt-in JSON output mode to commands that return package data, starting with `list` and the non-mutating form of `upgrade`. Define the data contract before shipping it, and put formatting in the reporter so later commands do not each invent their own JSON path. +Add an opt-in JSON output mode to commands that return package data, starting with `list` and the non-mutating form of `upgrade`. Define the contract before shipping it, and keep serialization in the reporter so later commands do not each invent their own JSON path. ## Inspiration @@ -23,64 +23,157 @@ This proposal is for CLI consumers; it does not replace the PowerShell module or ### Scope and rollout -Introduce `--format json` behind an experimental feature setting. The first supported commands are `winget list` and `winget upgrade` when `upgrade` is only listing available updates. The default table output does not change. Until a command has a defined schema and a typed result, it must reject `--format json` as unsupported, rather than return an empty document that looks like a successful query. Mutating `upgrade` modes such as `--all` remain unsupported in this first slice. +Introduce `--format json` behind an experimental feature setting. The first supported operations are installed-package listing and available-upgrade listing. The default table output does not change. Until an operation has a defined schema and typed result, it must reject `--format json` rather than return an empty document that looks successful. Mutating upgrade modes remain unsupported in this first slice. -The schema is reviewed with the first slice and stored with the client. The experimental gate gives room to correct the contract before it becomes stable; once stabilized, additions should be optional and existing field meanings should remain unchanged. Subsequent commands can adopt the same output machinery with command-specific result types and schemas. +The schemas are reviewed with the first implementation and stored with the client. The experimental gate allows the contract to change before stabilization. Once stable, new optional fields increment the schema minor version; removing a field, changing its type, or changing its meaning requires a new major version. Consumers must ignore fields they do not understand within a supported major version. -### Data contract +### Proposed decisions -One invocation writes one UTF-8 JSON document to stdout, followed by a newline. The envelope has a schema version, command name, package array, and error array. The initial proposal uses the PowerShell object's names and types for overlapping package fields. Envelope names are CLI-specific; they are not PowerShell package properties. +| Area | Proposal | Reason | +| --- | --- | --- | +| Common shape | A lower camel case envelope with a command-specific `result` object | Later commands can add their own result types without growing one universal object | +| Package fields | Keep PowerShell property names and types where the concepts overlap | Scripts see the same package vocabulary across CLI and PowerShell | +| Upgrade candidate | Add nullable `UpgradeVersion` alongside `AvailableVersions` | The full version list does not identify the version WinGet selected after applying command rules | +| Incomplete output | Include `result.truncated`, typed `warnings`, and typed `errors` | Automation can distinguish a complete empty result from an incomplete query | +| Schema identity | Versioned JSON Schema files with stable identifiers | Consumers can validate the exact contract they received | +| Partial failure | Return usable data, include the errors, and use a nonzero process exit code | Callers can use partial data without mistaking it for a complete success | + +These are the proposed contract decisions for design review. They are not claims about what the implementation in #6346 currently emits. + +### Common envelope + +One invocation writes one UTF-8 JSON document without a byte order mark to stdout, followed by a newline. Every supported operation uses the same envelope: ```json { + "$schema": "https://aka.ms/winget-cli-output.list.1.0.schema.json", "schemaVersion": "1.0", "command": "list", - "packages": [ - { - "Name": "Example App", - "Id": "Example.App", - "InstalledVersion": "1.0.0", - "AvailableVersions": ["1.1.0"], - "IsUpdateAvailable": true, - "Source": "winget" - } - ], + "mode": "installed", + "result": { + "packages": [], + "truncated": false + }, + "warnings": [], "errors": [] } ``` -`Name`, `Id`, `InstalledVersion`, `AvailableVersions`, `IsUpdateAvailable`, and `Source` correspond to `PSInstalledCatalogPackage` properties. `AvailableVersions` contains the versions known from the matched source; it must not be populated by splitting or interpreting a rendered table cell. `Source` is the actual source name when known, even when the human table omits its Source column. Use `null` when the source cannot be established, not an empty string that could be mistaken for a hidden table column. An empty successful result uses `"packages": []` and `"errors": []`. +`command` is the command entered by the caller. `mode` identifies the normalized read operation: + +| Invocation | `command` | `mode` | +| --- | --- | --- | +| `winget list --format json` | `list` | `installed` | +| `winget list --upgrade-available --format json` | `list` | `availableUpgrades` | +| `winget upgrade --format json` | `upgrade` | `availableUpgrades` | -The exact schema version and field casing are proposed here for review, not an assertion that the current implementation already emits this shape. The JSON schema should define required versus nullable fields, version string semantics, whether all available versions can be returned without extra catalog work, and whether `upgrade` needs any additional fields before implementation is finalized. Do not silently substitute only the selected upgrade candidate for `AvailableVersions` if PowerShell exposes more. +The envelope fields, `result`, `warnings`, and `errors` are always present. An empty successful query returns empty arrays and `result.truncated` set to `false`. -Failures should be typed objects, for example: +### Package result + +The installed-package result uses the PowerShell object's property names and types where the concepts overlap. `UpgradeVersion` is CLI-specific because a list of all known versions does not show which version the command selected. ```json { + "$schema": "https://aka.ms/winget-cli-output.list.1.0.schema.json", "schemaVersion": "1.0", "command": "list", - "packages": [], + "mode": "installed", + "result": { + "packages": [ + { + "Name": "Example App", + "Id": "Example.App", + "InstalledVersion": "1.0.0", + "AvailableVersions": ["1.1.0", "1.0.0"], + "IsUpdateAvailable": true, + "UpgradeVersion": "1.1.0", + "Source": "winget" + } + ], + "truncated": false + }, + "warnings": [], + "errors": [] +} +``` + +`Name`, `Id`, `InstalledVersion`, `AvailableVersions`, `IsUpdateAvailable`, and `Source` correspond to `PSInstalledCatalogPackage` properties. Their rules are: + +- Version values are opaque strings. Consumers must not assume semantic version syntax. +- `AvailableVersions` contains all versions known from the matched source. It must not be created by splitting or interpreting a rendered table cell. +- `IsUpdateAvailable` means that the matched source contains a newer version than `InstalledVersion`. It does not by itself promise that policy, pins, or the current command mode allow an upgrade. +- `UpgradeVersion` is the version selected by the current command after its filters and pin behavior are applied. It is `null` when no actionable candidate is selected. +- `Source` is the actual source name when known, even when the human table hides its Source column. It is `null` when the source cannot be established. +- More than one row may have the same `Id` because WinGet can find multiple installed entries. Consumers must not use `Id` alone as an array key. +- The package order follows the command's existing default or requested sort behavior. Consumers should not treat position as package identity. + +Returning all available versions may require catalog access beyond today's table. The implementation must measure that cost and must not reopen each package only to recreate display data. If the full list cannot be produced within an acceptable cost, the schema must be revised during the experimental phase rather than changing the meaning of `AvailableVersions`. + +### Warnings, errors, and process status + +Warnings describe a result that is usable but needs context. Warning codes are stable, non-localized identifiers; messages are localized diagnostic text. For example, the `warnings` array can contain: + +```json +[ + { + "code": "SearchTruncated", + "message": "More results are available than were returned.", + "source": null + } +] +``` + +`result.truncated` is `true` whenever a source or result limit prevented the command from returning the complete matching set. The warning explains the condition to a person, while the boolean gives automation a direct completeness check. Other conditions that change or omit results, such as packages skipped because an installed version is unknown, must use typed warnings rather than text written around the JSON document. + +Errors use the same document rather than replacing it with table output: + +```json +{ + "$schema": "https://aka.ms/winget-cli-output.list.1.0.schema.json", + "schemaVersion": "1.0", + "command": "list", + "mode": "installed", + "result": { + "packages": [], + "truncated": false + }, + "warnings": [], "errors": [ - { "code": "0x8A15000F", "message": "Source data is missing.", "source": "winget" } + { + "code": "0x8A15000F", + "message": "Source data is missing.", + "source": "winget" + } ] } ``` -The example code and message are illustrative, not a new error mapping. `code` should carry the actual HRESULT in a fixed representation. Preserve the command's exit code. A partial source failure may return packages from healthy sources and an error entry for the failed source; a command-level failure still produces a machine-readable document when the reporter has been initialized. Catastrophic startup failures before argument parsing may remain outside this guarantee and should be documented separately. +The example code and message are illustrative, not a new error mapping. Error `code` contains the actual HRESULT as `0x` followed by eight uppercase hexadecimal digits. `message` is localized and must not be parsed. `source` is the source name when the error belongs to a source and `null` for command-level failures. + +A partial source failure may return packages from healthy sources and error entries for failed sources. An empty `errors` array means that the command completed without an execution error. A non-empty `errors` array always produces a nonzero process exit code; for partial results, the first error is the primary error and supplies the process HRESULT. Warnings alone do not change the exit code. + +Once argument parsing recognizes `--format json`, feature-gate, option-validation, source, and execution failures must use the JSON envelope. Failures before argument parsing can select JSON mode, such as process startup failure or an invalid command line that cannot be parsed, remain outside this guarantee. `--help` keeps normal help output because it describes the command rather than executing a package query. An unsupported value such as `--format xml` also uses the normal argument error because JSON mode was not selected. ### Reporter and table ownership -Select an output mode once in `Reporter`. Workflows pass typed results, warnings, and errors to it. The reporter serializes a command result to JSON, or renders the existing human output. Plain decorative strings are not JSON data and are suppressed from JSON stdout. `TableOutput` can consume typed rows and keep presentation choices such as column hiding and truncation on the text side; it must not be the source of truth for serialized fields. This avoids a new format branch at every output call site. +Select the output mode once in `Reporter`. Workflows pass typed results, warnings, and errors to it. The reporter serializes the command result to JSON or renders the existing human output. Plain decorative strings are not JSON data and are suppressed from JSON stdout. `TableOutput` can consume typed rows and keep presentation choices such as column hiding and truncation on the text side; it must not be the source of truth for serialized fields. -The JSON document should be finalized once per invocation, including empty and error cases. Progress, settings warnings, source notices, and diagnostic logs must not precede or follow it on stdout. Diagnostics can use stderr where appropriate; typed warnings that callers need should be represented in the schema rather than mixed into the JSON stream. Tests should parse stdout as one document, not search for a JSON substring. +The reporter finalizes the JSON document once per invocation, including empty and error cases. Progress, settings warnings, source notices, and diagnostic logs must not precede or follow it on stdout. Diagnostics may use stderr, but stderr is not part of the JSON schema and callers must not parse it as structured output. Any warning needed by a caller belongs in the envelope. -Add a privacy-conscious telemetry event for use of the JSON mode, with command and success/failure category. Do not record package names, IDs, source contents, or the JSON payload. Existing telemetry controls continue to apply. +Add a privacy-conscious telemetry event for use of JSON mode, with the command, normalized mode, schema major version, and success, warning, partial-failure, or failure category. Do not record package names, IDs, source contents, warning or error messages, or the JSON payload. Existing telemetry controls continue to apply. -### Other surfaces and validation +### Schema files and compatibility + +Store the output schemas under `schemas/JSON/cli-output/` and use JSON Schema draft 2020-12. Each supported command and schema version has a stable `$id`, exposed through the document's `$schema` property. The first files are the list 1.0 schema and upgrade 1.0 schema; shared envelope, package, warning, and error definitions may use referenced schema files in the same folder. + +The schemas define every required field, nullable value, enumeration, and string format described here. Envelope, result, package, warning, and error objects allow additional properties so a consumer that supports the same major version can ignore later optional fields. A minor schema version may add optional fields or new warning codes. A major version is required to remove a field, make an optional field required, change a type, rename a value, or change existing semantics. -No manifest fields, manifest JSON schemas, COM IDL interfaces, PowerShell cmdlets or output objects, WinGet Configuration resources, or group policies change in this proposal. The CLI JSON schema is separate from package manifests and from `winget export`'s existing interchange format; it is not a new manifest schema version. No `winget-create`, `winget-cli-restsource`, or `winget-pkgs` validation-pipeline change is required for the first slice. +This schema is separate from package manifests and from `winget export`'s interchange format. It does not change manifest schema versions or validation in `winget-pkgs`, `winget-create`, or `winget-cli-restsource`. -The new `experimentalFeatures` key defaults to false. The final key name should follow the repository's feature naming convention; one possible settings shape is: +### Settings and supported modes + +The new `experimentalFeatures` key defaults to false. The final key name follows the repository's feature naming convention; the proposed settings shape is: ```json { @@ -94,28 +187,52 @@ The new `experimentalFeatures` key defaults to false. The final key name should | --- | --- | --- | | Off | Omitted | Existing text output | | On | Omitted | Existing text output | -| On | `json` on supported command | JSON document | -| Off | `json` | Experimental-feature error | -| On | `json` on unsupported mode | Explicit unsupported-mode error | +| On | `json` on a supported operation | JSON document | +| Off | `json` | JSON feature-gate error and nonzero exit code | +| On | `json` on an unsupported operation | JSON unsupported-operation error and nonzero exit code | + +The settings flag permits the argument; it does not make JSON the default format. `--no-vt` changes only terminal decoration, not JSON data. `--disable-interactivity` and noninteractive hosts use the same contract. If a query would require a prompt, the command fails with a typed error rather than interleaving a prompt with JSON. `--silent` is installer behavior, not a listing modifier, so it is outside the first slice. + +| Command or mode | First-slice behavior | +| --- | --- | +| `list` and filters that only narrow or sort the query | Supported as `mode: installed` | +| `list --upgrade-available` | Supported as `mode: availableUpgrades` | +| `upgrade` with no package argument | Supported as `mode: availableUpgrades` | +| `--count` on a supported query | Supported; `result.truncated` reports an incomplete set | +| `--include-unknown` on an available-upgrades query | Supported; the installed version remains an opaque version string | +| `list --details` | Unsupported until a details result schema exists | +| `--include-pinned` | Unsupported until the result models pin type and upgrade eligibility | +| `--silent` | Unsupported because it selects installer behavior rather than a read-only listing | +| Targeted upgrade or any option that can execute an upgrade | Unsupported in the first slice | +| `upgrade --all` | Unsupported in the first slice | +| Other commands | Unsupported until they have a typed result and schema | +| Empty result | Supported; empty arrays and a valid document | +| Source-open or source-search failure | Typed error and valid document after JSON mode is selected | + +### Other surfaces and validation -The settings flag permits the CLI argument; it does not make JSON the default format. `--no-vt` changes only terminal decoration, not JSON data. `--disable-interactivity` and noninteractive hosts use the same JSON contract. If a query would require a prompt, the command should fail with a typed error rather than interleave a prompt with JSON. `--help` keeps its normal help output, as it describes the command rather than executing a package query. +No manifest fields, manifest JSON schemas, COM IDL interfaces, PowerShell cmdlets or output objects, WinGet Configuration resources, or group policies change in this proposal. No `winget-create`, `winget-cli-restsource`, or `winget-pkgs` validation-pipeline change is required for the first slice. -| Command or mode | Interactive | `--no-vt` | Noninteractive | JSON behavior | -| --- | --- | --- | --- | --- | -| `list` | Supported | Supported | Supported | Installed package result | -| `upgrade` (listing) | Supported | Supported | Supported | Available-update result | -| `upgrade --all` or package execution | Not in first slice | Not in first slice | Not in first slice | Explicit unsupported-mode error | -| Other commands | Not in first slice | Not in first slice | Not in first slice | Explicit unsupported-command error | -| Empty result | Supported | Supported | Supported | Empty arrays, valid document | -| Source-open failure | Supported | Supported | Supported | Typed error, valid document where reporter is active | +The first implementation PR must include the versioned CLI output schemas and focused tests for: -### Implementation and test boundary +- schema validation for success, warning, partial-failure, and command-failure documents; +- single- and multi-source provenance, including `Source: null`; +- all version fields and selected upgrade behavior; +- duplicate package IDs and multiple installed entries; +- empty results and truncated results; +- skipped unknown installed versions and partial source failures; +- process exit codes for warnings, partial failures, and command failures; +- feature-disabled, unsupported-format, unsupported-command, and unsupported-mode behavior; +- `--help`, `--no-vt`, interactive, and noninteractive execution; +- existing text-table behavior. -The first implementation PR should contain the CLI JSON schema and focused tests for schema validation, known source provenance on single- and multi-source systems, null/empty distinctions, package version fields, empty results, partial and command-level errors, exit codes, help, and unsupported execution modes. Existing table behavior must remain covered. The generic reporter path should be demonstrated by both `list` and listing-only `upgrade`, without duplicating serialization branches in each workflow. +The generic reporter path must be demonstrated by `list`, `list --upgrade-available`, and listing-only `upgrade` without duplicating serialization branches in their workflows. ## UI/UX Design -With the feature enabled, `winget list --format json` prints the single document shown above. `winget upgrade --format json` uses the same envelope with `"command": "upgrade"` and the agreed package schema. Without `--format json`, users see the current localized table. `--format json` on an unsupported command or mutating mode reports an explicit error and nonzero exit code; it must not silently perform an operation or present table text as JSON. +With the feature enabled, `winget list --format json` prints the installed-package document shown above. `winget list --upgrade-available --format json` and `winget upgrade --format json` return the available-upgrades result with the command and mode values defined above. Without `--format json`, users see the current localized table. + +An unsupported command or mutating mode reports a JSON error and a nonzero exit code after JSON mode has been selected. It must not perform an operation, return an empty success document, or present table text as JSON. ## Capabilities @@ -125,34 +242,35 @@ The existing text presentation remains the default, including its accessibility ### Security -JSON values must be escaped by a serializer, never assembled by string concatenation. The format option does not widen source access or bypass policy and agreement checks. Telemetry excludes package and source data. +JSON values must be escaped by a serializer, never assembled by string concatenation. The format option does not widen source access or bypass policy and agreement checks. Telemetry excludes package, source, warning, error-message, and payload data. ### Reliability -One machine-readable document, explicit empty results, typed failures, and exit-code preservation give callers a reliable contract. Source provenance must come from the data model, not table-column visibility. +One machine-readable document, explicit empty and truncated results, typed warnings and failures, and a defined exit-code policy give callers a reliable contract. Source provenance comes from the data model, not table-column visibility. ### Compatibility -No existing default output or PowerShell/COM contract changes. The option is experimental while the schema is under review. Published schema versions need an explicit compatibility policy before the gate is removed. +No existing default output or PowerShell/COM contract changes. The option remains experimental while the schemas are under review. After stabilization, consumers use the schema major version as the compatibility boundary and ignore unknown optional fields. ### Performance, Power, and Efficiency -The JSON path should not open each package again merely to recreate display data. Returning every available version may require catalog access beyond today's table; measure that cost and settle the field semantics before freezing the schema. +The JSON path reuses the typed package data collected by the workflow. It must not render a table and parse it back, and it should not open every package again only to populate display-equivalent fields. The first implementation measures the cost of collecting all available versions before the 1.0 schema is stabilized. ## Potential Issues -- The PowerShell object model and CLI query semantics are related but not identical. A field name should not imply stronger data than the CLI can obtain without extra source work. -- A partial result with source errors needs an unambiguous exit-code policy; consumers should inspect both the process status and `errors`. -- Some failures occur before the reporter can select JSON mode. The documented guarantee must have a clear startup boundary. +- The PowerShell object model and CLI query semantics are related but not identical. CLI-only fields must be named explicitly rather than changing the meaning of a PowerShell field. +- Returning every available version may add source work compared with the current table. The experimental contract may need revision if measurement shows unacceptable cost. +- Partial results are deliberately nonzero. Consumers that accept them must parse the document even when the process reports failure. +- Some failures occur before JSON mode can be selected. The guarantee begins when argument parsing recognizes `--format json`. - A generic reporter change could alter text output accidentally. Keep table regression tests alongside JSON tests. ## Deprecation Path No existing feature is deprecated. The human table stays the default, and this proposal does not replace the PowerShell module, COM API, or `winget export` format. -## Future considerations +## Future Considerations -After the first schema and reporter path are agreed, `search`, `show`, and other result-producing commands can add their own typed results and schemas. Mutating commands may eventually expose structured progress and final results, but that is a separate contract and is not implied by this first slice. +After the first schemas and reporter path are agreed, `search`, `show`, and other result-producing commands can add their own typed results. Pin-aware output needs a stable model for pin type and upgrade eligibility before `--include-pinned` is supported. Mutating commands may eventually expose structured progress and final results, but that is a separate contract and is not implied by this first slice. ## Resources From 793f06429f73a2aaf1eb81fbb913038bdf965625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Sun, 20 Sep 2026 17:00:52 +0200 Subject: [PATCH 4/7] docs: use linked author identity --- doc/specs/#184 - Structured CLI Output.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/specs/#184 - Structured CLI Output.md b/doc/specs/#184 - Structured CLI Output.md index 6036a003ad..b31b383194 100644 --- a/doc/specs/#184 - Structured CLI Output.md +++ b/doc/specs/#184 - Structured CLI Output.md @@ -1,5 +1,5 @@ --- -author: Przemysław Kłys @PrzemyslawKlys +author: Przemysław Kłys https://github.com/PrzemyslawKlys created on: 2026-09-19 last updated: 2026-09-20 issue id: 184 From 82462c15036e78ffa6363c2e18a4b7861913506e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Sun, 20 Sep 2026 17:04:55 +0200 Subject: [PATCH 5/7] docs: accept spec author name --- .github/actions/spelling/expect.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 41ffa13e88..4eb3b7a030 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -287,6 +287,7 @@ jsoncpp JToken JValue Kaido +Klys KNOWNFOLDERID kool ktf @@ -460,6 +461,7 @@ PRODUCTICON propkey PROPVARIANT proxystub +Przemyslaw psapi pscustomobject pseudocode From 059a570ed30570251224e1639929cbfce99ee5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Sun, 20 Sep 2026 17:12:11 +0200 Subject: [PATCH 6/7] docs: define unsupported JSON envelope --- doc/specs/#184 - Structured CLI Output.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/specs/#184 - Structured CLI Output.md b/doc/specs/#184 - Structured CLI Output.md index b31b383194..3fb0bb5b3f 100644 --- a/doc/specs/#184 - Structured CLI Output.md +++ b/doc/specs/#184 - Structured CLI Output.md @@ -69,6 +69,8 @@ One invocation writes one UTF-8 JSON document without a byte order mark to stdou The envelope fields, `result`, `warnings`, and `errors` are always present. An empty successful query returns empty arrays and `result.truncated` set to `false`. +If JSON was selected but the command or mode is unsupported, there is no normalized read operation or command result to describe. That failure uses the versioned error-envelope schema with `mode: null` and `result: null`. Supported `list` and `upgrade` operations continue to use their command schema even when they fail before returning packages; their typed result remains present and contains an empty package array. + ### Package result The installed-package result uses the PowerShell object's property names and types where the concepts overlap. `UpgradeVersion` is CLI-specific because a list of all known versions does not show which version the command selected. @@ -165,7 +167,7 @@ Add a privacy-conscious telemetry event for use of JSON mode, with the command, ### Schema files and compatibility -Store the output schemas under `schemas/JSON/cli-output/` and use JSON Schema draft 2020-12. Each supported command and schema version has a stable `$id`, exposed through the document's `$schema` property. The first files are the list 1.0 schema and upgrade 1.0 schema; shared envelope, package, warning, and error definitions may use referenced schema files in the same folder. +Store the output schemas under `schemas/JSON/cli-output/` and use JSON Schema draft 2020-12. Each supported command and schema version has a stable `$id`, exposed through the document's `$schema` property. The first files are the list 1.0 schema, upgrade 1.0 schema, and error-envelope 1.0 schema; shared envelope, package, warning, and error definitions may use referenced schema files in the same folder. The schemas define every required field, nullable value, enumeration, and string format described here. Envelope, result, package, warning, and error objects allow additional properties so a consumer that supports the same major version can ignore later optional fields. A minor schema version may add optional fields or new warning codes. A major version is required to remove a field, make an optional field required, change a type, rename a value, or change existing semantics. From 004937829573b8f25440c8a6da9ae6e0ffe4d94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Wed, 7 Oct 2026 18:57:44 +0200 Subject: [PATCH 7/7] Revise structured output contract after maintainer feedback --- doc/specs/#184 - Structured CLI Output.md | 288 ++++++++++------------ 1 file changed, 134 insertions(+), 154 deletions(-) diff --git a/doc/specs/#184 - Structured CLI Output.md b/doc/specs/#184 - Structured CLI Output.md index 3fb0bb5b3f..3a5bfca3d6 100644 --- a/doc/specs/#184 - Structured CLI Output.md +++ b/doc/specs/#184 - Structured CLI Output.md @@ -1,7 +1,7 @@ --- -author: Przemysław Kłys https://github.com/PrzemyslawKlys +author: Przemysław Kłys https://github.com/PrzemyslawKlys, OpenAI Codex created on: 2026-09-19 -last updated: 2026-09-20 +last updated: 2026-10-07 issue id: 184 --- @@ -11,272 +11,252 @@ For [#184](https://github.com/microsoft/winget-cli/issues/184) ## Abstract -Add an opt-in JSON output mode to commands that return package data, starting with `list` and the non-mutating form of `upgrade`. Define the contract before shipping it, and keep serialization in the reporter so later commands do not each invent their own JSON path. +Add an opt-in JSON output mode to commands that return package data, starting with `list` and the non-mutating form of `upgrade`. Define a shared invocation envelope and preserve the package relationships available to the client. The reporter owns serialization so later commands can use the same mechanism. ## Inspiration -The PowerShell module and COM API already provide structured package data, but callers of `winget.exe` in other shells and tools still have to parse localized, width-dependent tables. [#6346](https://github.com/microsoft/winget-cli/pull/6346) demonstrates the first use case, while its review identified two things to settle first: a stable shape aligned with the PowerShell objects, and a design that scales beyond two commands. +The COM API and PowerShell module already provide structured package data. Callers of `winget.exe` in other shells and tools still have to parse localized, width-dependent tables. [#6346](https://github.com/microsoft/winget-cli/pull/6346) demonstrates that use case, but its table-derived model loses source information and does not scale to other commands. -This proposal is for CLI consumers; it does not replace the PowerShell module or COM API. PowerShell remains the richer option when an application can use it directly. +This proposal adds another public contract to maintain. COM remains the preferred API for applications that can use it directly, and PowerShell remains available to scripts that can take that dependency. The CLI use case is automation that invokes the executable without introducing either runtime. The specification needs maintainer agreement before the implementation in [#6536](https://github.com/microsoft/winget-cli/pull/6536) is updated or stabilized. ## Solution Design ### Scope and rollout -Introduce `--format json` behind an experimental feature setting. The first supported operations are installed-package listing and available-upgrade listing. The default table output does not change. Until an operation has a defined schema and typed result, it must reject `--format json` rather than return an empty document that looks successful. Mutating upgrade modes remain unsupported in this first slice. +Introduce `--format json` behind the `structuredOutput` experimental setting. Installed-package listing and available-upgrade listing are the first supported operations. Mutating operations and commands without a result schema return a structured error once JSON mode has been selected. Existing table output remains the default. -The schemas are reviewed with the first implementation and stored with the client. The experimental gate allows the contract to change before stabilization. Once stable, new optional fields increment the schema minor version; removing a field, changing its type, or changing its meaning requires a new major version. Consumers must ignore fields they do not understand within a supported major version. - -### Proposed decisions - -| Area | Proposal | Reason | -| --- | --- | --- | -| Common shape | A lower camel case envelope with a command-specific `result` object | Later commands can add their own result types without growing one universal object | -| Package fields | Keep PowerShell property names and types where the concepts overlap | Scripts see the same package vocabulary across CLI and PowerShell | -| Upgrade candidate | Add nullable `UpgradeVersion` alongside `AvailableVersions` | The full version list does not identify the version WinGet selected after applying command rules | -| Incomplete output | Include `result.truncated`, typed `warnings`, and typed `errors` | Automation can distinguish a complete empty result from an incomplete query | -| Schema identity | Versioned JSON Schema files with stable identifiers | Consumers can validate the exact contract they received | -| Partial failure | Return usable data, include the errors, and use a nonzero process exit code | Callers can use partial data without mistaking it for a complete success | - -These are the proposed contract decisions for design review. They are not claims about what the implementation in #6346 currently emits. +The experimental gate permits revisions while the contract is reviewed. Before stabilization, the implementation must demonstrate the shared reporter path across `list`, `list --upgrade-available`, and listing-only `upgrade`. ### Common envelope -One invocation writes one UTF-8 JSON document without a byte order mark to stdout, followed by a newline. Every supported operation uses the same envelope: +Every invocation in JSON mode writes one compact UTF-8 JSON document without a byte order mark to stdout, followed by a newline. Examples below use indentation for readability. ```json { - "$schema": "https://aka.ms/winget-cli-output.list.1.0.schema.json", - "schemaVersion": "1.0", + "$schema": "https://aka.ms/winget-cli-output.list.1.31.schema.json", + "schemaVersion": "1.31", "command": "list", - "mode": "installed", - "result": { - "packages": [], - "truncated": false - }, + "arguments": {"format": ["json"]}, + "result": {"packages": [], "truncated": false}, "warnings": [], - "errors": [] + "error": null } ``` -`command` is the command entered by the caller. `mode` identifies the normalized read operation: +The common envelope schema defines and requires `$schema`, `schemaVersion`, `command`, `arguments`, `result`, `warnings`, and `error`. Each command schema references that definition and supplies its result type. A generic error-envelope schema uses the same required fields with `result: null` for operations without a supported result type. -| Invocation | `command` | `mode` | -| --- | --- | --- | -| `winget list --format json` | `list` | `installed` | -| `winget list --upgrade-available --format json` | `list` | `availableUpgrades` | -| `winget upgrade --format json` | `upgrade` | `availableUpgrades` | +`command` is the resolved command name, including parent command names when applicable. `arguments` records accepted arguments under their canonical long names without leading dashes. Values are arrays of strings; flags are empty arrays. Positional arguments use their parser-defined names. Aliases are normalized, repeated values retain their order, and implicit defaults are omitted. Sensitive arguments must be redacted rather than echoed. This object is output data and must not be copied into telemetry. -The envelope fields, `result`, `warnings`, and `errors` are always present. An empty successful query returns empty arrays and `result.truncated` set to `false`. +For example, `list --upgrade-available --format json` has command `list` and arguments `{"upgrade-available":[],"format":["json"]}`. Listing-only `upgrade --format json` has command `upgrade` and arguments `{"format":["json"]}`. No separate `mode` field is needed. -If JSON was selected but the command or mode is unsupported, there is no normalized read operation or command result to describe. That failure uses the versioned error-envelope schema with `mode: null` and `result: null`. Supported `list` and `upgrade` operations continue to use their command schema even when they fail before returning packages; their typed result remains present and contains an empty package array. +An empty successful query has empty package and warning arrays, `truncated: false`, and `error: null`. A supported command that fails before producing data retains its typed empty result. An unsupported operation uses the generic envelope with `result: null`. ### Package result -The installed-package result uses the PowerShell object's property names and types where the concepts overlap. `UpgradeVersion` is CLI-specific because a list of all known versions does not show which version the command selected. +Serialize the graph represented by `ICompositePackage`, `IPackage`, and `PackageVersionKey`, rather than rendered table rows. An installed result has its installed package and zero or more available packages, each with its own source and version collection. Preserve version channels and source identity; do not merge versions from different sources into one string array. ```json { - "$schema": "https://aka.ms/winget-cli-output.list.1.0.schema.json", - "schemaVersion": "1.0", - "command": "list", - "mode": "installed", - "result": { - "packages": [ - { + "packages": [ + { + "Name": "Example App", + "Id": "Example.App", + "InstalledPackage": { "Name": "Example App", "Id": "Example.App", - "InstalledVersion": "1.0.0", - "AvailableVersions": ["1.1.0", "1.0.0"], - "IsUpdateAvailable": true, - "UpgradeVersion": "1.1.0", - "Source": "winget" + "Versions": [{"Version": "1.0.0", "Channel": ""}] + }, + "InstalledSource": { + "Name": "winget", + "Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe" + }, + "AvailablePackages": [ + { + "Name": "Example App", + "Id": "Example.App", + "Source": { + "Name": "winget", + "Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe" + }, + "Versions": [ + {"Version": "1.1.0", "Channel": ""}, + {"Version": "1.0.0", "Channel": ""} + ] + } + ], + "UpgradeVersion": {"Version": "1.1.0", "Channel": ""}, + "UpgradeSource": { + "Name": "winget", + "Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe" } - ], - "truncated": false - }, - "warnings": [], - "errors": [] + } + ], + "truncated": false } ``` -`Name`, `Id`, `InstalledVersion`, `AvailableVersions`, `IsUpdateAvailable`, and `Source` correspond to `PSInstalledCatalogPackage` properties. Their rules are: +- `InstalledPackage` comes from `GetInstalled()`. Its identity may differ from the correlated result, for example an installed product code matched to a catalog package ID. +- `InstalledSource` identifies the source the package was last installed from, when known. An installed-packages inventory source is not evidence of the original catalog. Return `null` when that provenance is unavailable. +- `AvailablePackages` comes from `GetAvailable()`, retaining each package, its source, and its `GetVersionKeys()` collection. Each version keeps its opaque version string and channel. Consumers must not assume semantic version syntax. +- `UpgradeVersion` is the candidate selected after command filters and pin behavior. `UpgradeSource` identifies its source. Both are `null` when no actionable candidate is selected. They may differ from the last installed version and `InstalledSource`. +- Source objects contain the configured name and stable identifier when known; unavailable values are `null`. Sources remain visible even when a table hides a repeated Source column. They exclude credentials and authentication headers. +- Multiple installed entries can share an `Id`, and available packages can represent that ID in different sources. Neither IDs nor array positions are unique package identities. +- Package order follows the command's existing sort behavior. Version collections retain the model's descending order, with each channel kept alongside its version. -- Version values are opaque strings. Consumers must not assume semantic version syntax. -- `AvailableVersions` contains all versions known from the matched source. It must not be created by splitting or interpreting a rendered table cell. -- `IsUpdateAvailable` means that the matched source contains a newer version than `InstalledVersion`. It does not by itself promise that policy, pins, or the current command mode allow an upgrade. -- `UpgradeVersion` is the version selected by the current command after its filters and pin behavior are applied. It is `null` when no actionable candidate is selected. -- `Source` is the actual source name when known, even when the human table hides its Source column. It is `null` when the source cannot be established. -- More than one row may have the same `Id` because WinGet can find multiple installed entries. Consumers must not use `Id` alone as an array key. -- The package order follows the command's existing default or requested sort behavior. Consumers should not treat position as package identity. +This projects package relationships and version keys without promising every internal property or downloading every manifest. A future details result can add metadata through the same graph. PowerShell property names are retained where their meaning matches, but its current flattened shape does not limit the CLI contract. -Returning all available versions may require catalog access beyond today's table. The implementation must measure that cost and must not reopen each package only to recreate display data. If the full list cannot be produced within an acceptable cost, the schema must be revised during the experimental phase rather than changing the meaning of `AvailableVersions`. +`result.truncated` reports truncation in the source search, for example a source-reported result limit. Terminal width and hidden table columns do not affect serialized data. Ordinary table presentation therefore leaves this value `false`; a warning accompanies actual search truncation. -### Warnings, errors, and process status +### Warnings, invocation error, and process status -Warnings describe a result that is usable but needs context. Warning codes are stable, non-localized identifiers; messages are localized diagnostic text. For example, the `warnings` array can contain: +`warnings` is an array of contextual diagnostics. Reuse stable resource string identifiers for existing diagnostics where possible. Messages remain localized and must not be parsed. A diagnostic may include source context and an HRESULT without becoming the invocation's primary error: ```json -[ - { - "code": "SearchTruncated", - "message": "More results are available than were returned.", - "source": null - } -] +{ + "code": "SourceOpenFailed", + "message": "Source data is missing.", + "source": "winget", + "hresult": -1978335217 +} ``` -`result.truncated` is `true` whenever a source or result limit prevented the command from returning the complete matching set. The warning explains the condition to a person, while the boolean gives automation a direct completeness check. Other conditions that change or omit results, such as packages skipped because an installed version is unknown, must use typed warnings rather than text written around the JSON document. +The example is illustrative; use the existing diagnostic identifier and actual HRESULT. The warning schema accepts a string `code`, rather than a closed enumeration requiring edits for every new diagnostic. A published code's meaning remains stable. `source` and `hresult` are nullable. -Errors use the same document rather than replacing it with table output: +`error` is either `null` or one object describing the invocation failure. It contains the actual HRESULT as a signed 32-bit JSON integer and its localized message, without per-source context: ```json -{ - "$schema": "https://aka.ms/winget-cli-output.list.1.0.schema.json", - "schemaVersion": "1.0", - "command": "list", - "mode": "installed", - "result": { - "packages": [], - "truncated": false - }, - "warnings": [], - "errors": [ - { - "code": "0x8A15000F", - "message": "Source data is missing.", - "source": "winget" - } - ] -} +{"hresult": -1978335217, "message": "Source data is missing."} ``` -The example code and message are illustrative, not a new error mapping. Error `code` contains the actual HRESULT as `0x` followed by eight uppercase hexadecimal digits. `message` is localized and must not be parsed. `source` is the source name when the error belongs to a source and `null` for command-level failures. +There is one invocation HRESULT. Its 32-bit value agrees with the process exit code; hosts displaying unsigned exit codes may show a different decimal representation of the same bits. Success uses `error: null` and exit code zero. -A partial source failure may return packages from healthy sources and error entries for failed sources. An empty `errors` array means that the command completed without an execution error. A non-empty `errors` array always produces a nonzero process exit code; for partial results, the first error is the primary error and supplies the process HRESULT. Warnings alone do not change the exit code. +A partial source failure can retain packages from healthy sources and contextual diagnostics for failed sources. The invocation has a non-null `error` and a nonzero exit code indicating incomplete execution. The command's existing error selection supplies that HRESULT; array order does not select it. Ordinary warnings, such as a stale source that remains usable, retain their existing command status semantics. -Once argument parsing recognizes `--format json`, feature-gate, option-validation, source, and execution failures must use the JSON envelope. Failures before argument parsing can select JSON mode, such as process startup failure or an invalid command line that cannot be parsed, remain outside this guarantee. `--help` keeps normal help output because it describes the command rather than executing a package query. An unsupported value such as `--format xml` also uses the normal argument error because JSON mode was not selected. +Once parsing recognizes `--format json`, feature-gate, unsupported-command, option-validation, source, and execution failures use the common envelope. Unsupported operations must not execute, emit table text, or return an empty success document. Startup failures and command lines that cannot be parsed far enough to select JSON remain outside this guarantee. `--help` keeps normal help output. An unsupported format such as `--format xml` keeps the normal argument error because JSON was not selected. ### Reporter and table ownership -Select the output mode once in `Reporter`. Workflows pass typed results, warnings, and errors to it. The reporter serializes the command result to JSON or renders the existing human output. Plain decorative strings are not JSON data and are suppressed from JSON stdout. `TableOutput` can consume typed rows and keep presentation choices such as column hiding and truncation on the text side; it must not be the source of truth for serialized fields. +Select JSON mode once in `Reporter`. Workflows supply typed package results and diagnostics. The reporter owns the final document, shared envelope, and invocation error. Plain decorative strings are suppressed on JSON stdout. `TableOutput` keeps hidden columns, width truncation, and decoration on the text path. -The reporter finalizes the JSON document once per invocation, including empty and error cases. Progress, settings warnings, source notices, and diagnostic logs must not precede or follow it on stdout. Diagnostics may use stderr, but stderr is not part of the JSON schema and callers must not parse it as structured output. Any warning needed by a caller belongs in the envelope. +The reporter finalizes once per invocation, including empty and failure cases. Progress, settings warnings, source notices, and logs must not surround the document on stdout. Diagnostics can still use stderr, but automation obtains structured diagnostics from the envelope. -Add a privacy-conscious telemetry event for use of JSON mode, with the command, normalized mode, schema major version, and success, warning, partial-failure, or failure category. Do not record package names, IDs, source contents, warning or error messages, or the JSON payload. Existing telemetry controls continue to apply. +Extend existing command telemetry and its summary with the selected output format and schema version. Do not add a separate event. Existing command and completion information provides invocation context. Do not record package data, source contents, supplied argument values, messages, or the JSON document. Existing telemetry controls continue to apply. ### Schema files and compatibility -Store the output schemas under `schemas/JSON/cli-output/` and use JSON Schema draft 2020-12. Each supported command and schema version has a stable `$id`, exposed through the document's `$schema` property. The first files are the list 1.0 schema, upgrade 1.0 schema, and error-envelope 1.0 schema; shared envelope, package, warning, and error definitions may use referenced schema files in the same folder. +Store JSON Schema draft 2020-12 definitions under `schemas/JSON/cli-output/`. Define the envelope once and reference it from list, upgrade, and generic error-envelope schemas. Share package, source, version-key, diagnostic, and invocation-error definitions across commands. + +Begin at the client version shipping the contract, proposed here as `1.31`, rather than introducing an unrelated `1.0` sequence. Select the final version with the implementation's shipping client. Each version has stable identifiers exposed through `$schema` and `schemaVersion`. Example URLs require registration before stabilization. -The schemas define every required field, nullable value, enumeration, and string format described here. Envelope, result, package, warning, and error objects allow additional properties so a consumer that supports the same major version can ignore later optional fields. A minor schema version may add optional fields or new warning codes. A major version is required to remove a field, make an optional field required, change a type, rename a value, or change existing semantics. +Schemas specify required fields and types. Objects permit additional properties, and consumers ignore optional fields they do not recognize. Compatible releases can add optional fields and diagnostic codes. Stable 1.X releases do not remove or rename fields, change their types, or change their meaning. An incompatible replacement needs a separately agreed contract and migration plan; it cannot silently replace an existing schema identifier. -This schema is separate from package manifests and from `winget export`'s interchange format. It does not change manifest schema versions or validation in `winget-pkgs`, `winget-create`, or `winget-cli-restsource`. +This applies to CLI output. It does not change manifest schemas, `winget export`, or the schema acceptance policy in `winget-pkgs`. -### Settings and supported modes +### Settings and supported operations -The new `experimentalFeatures` key defaults to false. The final key name follows the repository's feature naming convention; the proposed settings shape is: +The `structuredOutput` experimental setting defaults to false: ```json -{ - "experimentalFeatures": { - "structuredOutput": true - } -} +{"experimentalFeatures": {"structuredOutput": true}} ``` | Setting | `--format` | Result | | --- | --- | --- | | Off | Omitted | Existing text output | | On | Omitted | Existing text output | -| On | `json` on a supported operation | JSON document | +| On | `json` on a supported operation | JSON result document | | Off | `json` | JSON feature-gate error and nonzero exit code | -| On | `json` on an unsupported operation | JSON unsupported-operation error and nonzero exit code | +| On | `json` on an unsupported operation | JSON error envelope and nonzero exit code | -The settings flag permits the argument; it does not make JSON the default format. `--no-vt` changes only terminal decoration, not JSON data. `--disable-interactivity` and noninteractive hosts use the same contract. If a query would require a prompt, the command fails with a typed error rather than interleaving a prompt with JSON. `--silent` is installer behavior, not a listing modifier, so it is outside the first slice. +The setting permits the argument without changing the default format. JSON is compact; a separate pretty-print option is outside this first slice. `--no-vt` does not change JSON data. Interactive and `--disable-interactivity` callers receive the same contract. A required prompt produces a structured error rather than interleaving prompts with JSON. Agreement acceptance flags remain available where the command supports them. -| Command or mode | First-slice behavior | +| Command or condition | First-slice behavior | | --- | --- | -| `list` and filters that only narrow or sort the query | Supported as `mode: installed` | -| `list --upgrade-available` | Supported as `mode: availableUpgrades` | -| `upgrade` with no package argument | Supported as `mode: availableUpgrades` | -| `--count` on a supported query | Supported; `result.truncated` reports an incomplete set | -| `--include-unknown` on an available-upgrades query | Supported; the installed version remains an opaque version string | -| `list --details` | Unsupported until a details result schema exists | -| `--include-pinned` | Unsupported until the result models pin type and upgrade eligibility | -| `--silent` | Unsupported because it selects installer behavior rather than a read-only listing | -| Targeted upgrade or any option that can execute an upgrade | Unsupported in the first slice | -| `upgrade --all` | Unsupported in the first slice | -| Other commands | Unsupported until they have a typed result and schema | -| Empty result | Supported; empty arrays and a valid document | -| Source-open or source-search failure | Typed error and valid document after JSON mode is selected | +| `list` and supported query or sort filters | Installed-package graph | +| `list --upgrade-available` | Package graph for the available-upgrade query | +| `upgrade` with no package argument | Package graph for the available-upgrade query | +| `--count` on a supported query | Report search truncation when the source indicates it | +| `--include-unknown` on an available-upgrade query | Preserve the installed version's opaque value | +| `list --details` | Structured error until a details schema exists | +| `--include-pinned` | Structured error until pin type and upgrade eligibility are modeled | +| `--silent`, targeted upgrade, or any mutating upgrade option | Structured error; no operation | +| Other commands | Structured error until a typed result and schema exist | +| Empty result | Typed empty result, valid envelope, normal command status | +| Source-open or source-search failure | Valid envelope, contextual diagnostics, invocation error | ### Other surfaces and validation -No manifest fields, manifest JSON schemas, COM IDL interfaces, PowerShell cmdlets or output objects, WinGet Configuration resources, or group policies change in this proposal. No `winget-create`, `winget-cli-restsource`, or `winget-pkgs` validation-pipeline change is required for the first slice. +No manifest fields, COM IDL interfaces, PowerShell cmdlets or objects, Configuration resources, or group policies change. No changes are needed in `winget-create`, `winget-cli-restsource`, or the `winget-pkgs` validation pipeline. COM and PowerShell retain their existing interactivity and data contracts. -The first implementation PR must include the versioned CLI output schemas and focused tests for: +The implementation includes schema validation and focused behavior tests for: -- schema validation for success, warning, partial-failure, and command-failure documents; -- single- and multi-source provenance, including `Source: null`; -- all version fields and selected upgrade behavior; -- duplicate package IDs and multiple installed entries; -- empty results and truncated results; -- skipped unknown installed versions and partial source failures; -- process exit codes for warnings, partial failures, and command failures; -- feature-disabled, unsupported-format, unsupported-command, and unsupported-mode behavior; +- the shared envelope across success, warnings, partial failure, and invocation failure; +- differing installed and upgrade provenance, unknown provenance, and single-source column hiding; +- multiple available packages with distinct sources and version channels; +- duplicate installed IDs and existing command sorting; +- empty results, source search truncation, and table width independence; +- actual invocation HRESULTs, contextual source diagnostics, and process exit codes; +- canonical arguments, omitted defaults, and sensitive-value redaction; +- feature-disabled, unsupported-command, unsupported-format, and unsupported-operation behavior; - `--help`, `--no-vt`, interactive, and noninteractive execution; -- existing text-table behavior. +- unchanged existing text output. -The generic reporter path must be demonstrated by `list`, `list --upgrade-available`, and listing-only `upgrade` without duplicating serialization branches in their workflows. +Demonstrate the shared reporter path across all three supported invocations without duplicating serialization in workflows. Collecting version keys must not download every manifest. ## UI/UX Design -With the feature enabled, `winget list --format json` prints the installed-package document shown above. `winget list --upgrade-available --format json` and `winget upgrade --format json` return the available-upgrades result with the command and mode values defined above. Without `--format json`, users see the current localized table. +With the feature enabled, an empty successful list writes this single line: + +```json +{"$schema":"https://aka.ms/winget-cli-output.list.1.31.schema.json","schemaVersion":"1.31","command":"list","arguments":{"format":["json"]},"result":{"packages":[],"truncated":false},"warnings":[],"error":null} +``` -An unsupported command or mutating mode reports a JSON error and a nonzero exit code after JSON mode has been selected. It must not perform an operation, return an empty success document, or present table text as JSON. +Available-upgrade listings use their parsed command and arguments in the same envelope. Unsupported operations return a JSON error and a nonzero exit code without performing the operation. Omitting `--format json` retains the localized table. ## Capabilities ### Accessibility -The existing text presentation remains the default, including its accessibility behavior. JSON is intended for tools and is not a replacement for readable command output. +Readable text output and its accessibility behavior remain the default. Compact JSON is intended for tools; callers can format it with their existing JSON tools when reading it directly. ### Security -JSON values must be escaped by a serializer, never assembled by string concatenation. The format option does not widen source access or bypass policy and agreement checks. Telemetry excludes package, source, warning, error-message, and payload data. +Use a JSON serializer to escape values. Source access, trust, policy, and agreement checks still apply. Redact sensitive arguments and omit credentials from source objects. Telemetry excludes argument values and output data. ### Reliability -One machine-readable document, explicit empty and truncated results, typed warnings and failures, and a defined exit-code policy give callers a reliable contract. Source provenance comes from the data model, not table-column visibility. +One document describes the invocation, result, contextual diagnostics, and final failure. Source and version relationships come from the package model, so presentation choices cannot discard them. ### Compatibility -No existing default output or PowerShell/COM contract changes. The option remains experimental while the schemas are under review. After stabilization, consumers use the schema major version as the compatibility boundary and ignore unknown optional fields. +The option remains experimental until the schemas are agreed. Existing default output, COM, and PowerShell contracts do not change. Stable output schemas retain their defined meanings and accept additive optional fields. ### Performance, Power, and Efficiency -The JSON path reuses the typed package data collected by the workflow. It must not render a table and parse it back, and it should not open every package again only to populate display-equivalent fields. The first implementation measures the cost of collecting all available versions before the 1.0 schema is stabilized. +Reuse the package graph and version keys available to the command. Do not render and parse a table or download every manifest. Measure the extra cost of enumerating available versions before stabilization. ## Potential Issues -- The PowerShell object model and CLI query semantics are related but not identical. CLI-only fields must be named explicitly rather than changing the meaning of a PowerShell field. -- Returning every available version may add source work compared with the current table. The experimental contract may need revision if measurement shows unacceptable cost. -- Partial results are deliberately nonzero. Consumers that accept them must parse the document even when the process reports failure. -- Some failures occur before JSON mode can be selected. The guarantee begins when argument parsing recognizes `--format json`. -- A generic reporter change could alter text output accidentally. Keep table regression tests alongside JSON tests. +- A CLI output format adds a public contract alongside COM and PowerShell. Maintainers must agree that its executable-only use case justifies that ongoing cost. +- Package relationships are richer than the current PowerShell projection. Naming alignment must not flatten them or change existing property meanings. +- Enumerating available version keys may add source work. Measure it in the experimental implementation. +- Consumers using partial results must parse the document even when the process exits nonzero. +- Failures before parsing can select JSON remain unstructured. +- Reporter changes can affect text output; retain existing behavior tests. ## Deprecation Path -No existing feature is deprecated. The human table stays the default, and this proposal does not replace the PowerShell module, COM API, or `winget export` format. +No feature is deprecated. Tables remain the default, and COM, PowerShell, and `winget export` keep their existing contracts. ## Future Considerations -After the first schemas and reporter path are agreed, `search`, `show`, and other result-producing commands can add their own typed results. Pin-aware output needs a stable model for pin type and upgrade eligibility before `--include-pinned` is supported. Mutating commands may eventually expose structured progress and final results, but that is a separate contract and is not implied by this first slice. +After the envelope and package model are agreed, other query commands can define result schemas. A details result, pin-aware query data, pretty printing, and structured mutating operations need their own reviewed contracts. They are outside this first slice. ## Resources -- [Structured output tracking issue #184](https://github.com/microsoft/winget-cli/issues/184) -- [Initial list/upgrade implementation PR #6346](https://github.com/microsoft/winget-cli/pull/6346) +- [Tracking issue #184](https://github.com/microsoft/winget-cli/issues/184) +- [Initial implementation #6346](https://github.com/microsoft/winget-cli/pull/6346) and [experimental implementation #6536](https://github.com/microsoft/winget-cli/pull/6536) +- [Package graph and version interfaces](https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerRepositoryCore/Public/winget/RepositorySearch.h) - [PowerShell installed package object](https://github.com/microsoft/winget-cli/blob/master/src/PowerShell/Microsoft.WinGet.Client.Engine/PSObjects/PSInstalledCatalogPackage.cs) - [Reporter](https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerCLICore/ExecutionReporter.h) and [TableOutput](https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerCLICore/TableOutput.h) +- [Existing command telemetry](https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp)