feat(cli): JSON output for switch and version - #10
Merged
Merged
Conversation
Give clients a structured answer for the one command that had none: version --json prints {version, commit, date} through the shared JSON writer, and the prose output is unchanged when the flag is absent.
The report-specific writeJSON helper in info.go becomes the one JSON writer the package uses, so catalog list/show and version all emit with SetEscapeHTML(false) and the same indentation; the duplicate encodeJSON in catalog.go is gone.
Every run that reaches the switch handler now prints exactly one JSON document on stdout, whatever the outcome, and the exit code is unchanged. The pair (outcome, writeStatus) is fixed - sent/sent, dry-run/none, refused/none, failed/unknown - so a client can tell a dry run from a send, which the exit code alone cannot, and can validate the two against each other. The text path is untouched: a refusal still exits 2 with the full template and its closing promise. A JSON run that failed returns a renderedError instead, which message() renders as nothing and exitCode() unwraps, so the report is not printed twice. Cobra-level errors stay prose on stderr with exit 1, because --json has not been parsed when they occur. assumed reports what happened rather than what was asked for: performSwitch records that the policy actually assumed a model, so a bypassed write whose tool then failed still says so on the one outcome where an unverified value may have reached the monitor. Refusal.explanation becomes Refusal.Explanation so the document can pair the reason code with the sentence a human would have seen, rather than keeping a second copy of the wording.
Each entry of inputs[] in catalog list --json and catalog show --json now has a label field, from catalog.Input.Label() - the human name monmux itself prints in success and refusal messages, so a client can show what monmux shows without inventing a spelling of its own. info --json's enabledInputs stays a list of names: every enabled input is recorded by construction, so a client that needs labels joins on the catalog entry.
New docs/json.md covers every --json command field by field: the outcome/writeStatus pair and how it maps to the exit code, which fields are stable and which are prose, what redaction does to a document, the three shapes the version fields take across a release, a make go-build and a bare go build, and the scope of the switch guarantee - one document for every run that reaches the handler, with cobra-level errors staying prose on stderr, so a client never reads exit 1 with no parsable stdout as a refusal. The flag table now lists switch and version among the --json commands, the README usage block shows it, troubleshooting says the reason code is refusal.reason, security.md counts switch --json among the outputs that redact, and both the AGENTS.md documentation table and the adversarial-review skill - routing table and contract-drift checklist alike - point at the new page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds structured output to the two commands that lacked it, so a client — the GNOME Shell extension being built against
monmux — can read a switch's result instead of parsing prose, and writes the contract down.
monmux version --json—{"version", "commit", "date"}, through the same JSON writerinfo --jsonuses. The proseoutput is unchanged when the flag is absent.
monmux switch --json— one document on stdout for every outcome, with the exit code unchanged. The pairoutcome/writeStatusis fixed:sent/sent(exit 0),dry-run/none(exit 0),refused/none(exit 2),failed/unknown(exit 1). The exit code stays the authority for "the write status is unknown"; the document is theauthority for telling a dry run from a real send, which the exit code cannot. A client validates the two against each
other and treats any other combination as a protocol error, so a dry run can never be announced as a completed switch.
catalog list --json/catalog show --json— each entry ofinputs[]gainslabel, the human name monmux itselfprints (
DisplayPort,HDMI 2,USB-C).info --json'senabledInputsstays a list of names: a client that wantslabels joins on the catalog entry, where every enabled input is recorded by construction.
docs/json.md— the whole contract, field by field: what is stable and what is prose, what redaction does to adocument, the three shapes the
versionfields take across a release, amake go-buildand a barego build, and thescope of the switch guarantee.
What is not weakened
No fail-closed invariant moves. The text path is untouched: a refusal still exits 2 and still ends with
No DDC write was performed.A--jsonrun that failed returns arenderedErrorthatmessage()renders as nothing andexitCode()unwraps, so the report is not printed twice and the codes are exactly the ones the text path uses.--jsonadds no way to name a VCP code or a value; the bytes still come from the compiled-in catalog. Redaction is unchanged:
command,display.handleandrefusal.detectedare masked unless--show-serial.assumedreports what happened rather than what was asked for — it istrueexactly when the policy assumed a model,including a bypassed write whose tool then failed, which is the one outcome where an unverified value may have reached a
monitor.
Scope of the guarantee
One document for every run that reaches the
switchhandler, including the argument errors it raises itself. Errors cobraraises before it — wrong argument count, unknown flag, bad flag value — stay prose on stderr with exit 1, as for every
command, because
--jsonhas not been parsed when they occur. A client reads "exit 1 with nothing parsable on stdout" as afailure whose text is stderr, never as a refusal.
Pull request checklist
mainbranchmake checklocally before creating the commit and it has run successfullyWIPcommits in this PRAlso run on every commit:
make go-test,make go-vet-cross,make go-lint-cross(0 issues for bothGOOS=linuxandGOOS=darwin).Monitor writes
internal/catalog/models.yaml, the evidence is in the entry, and the Summary above says which modeland which input it enables — n/a, the catalog is untouched
Verification was read-only throughout:
monmux version --json,monmux switch dp --dry-run --jsonand a refusal(
monmux switch hdmi9 --json, exit 2). Nothing was written to a monitor. The new tests drive the fake backend only.Type of changes