feat: emit structured errors in JSON mode - #221
Merged
Conversation
When the global --json flag is active, command failures now emit exactly
one machine-parseable JSON object on stderr instead of chalk-colored prose,
so agents and scripts (jq pipelines, the Claude skill) can parse failures
the same way they parse success.
Shape: { error, code, status, details } where code is a stable enum
(AUTH_FAILED, NOT_FOUND, VALIDATION, API_ERROR, NETWORK, UNKNOWN) derived
from the existing error classification. Errors stay on stderr (stdout=data,
stderr=diagnostics) and exit codes are unchanged (1).
Both error paths are covered: the global handleCommandError and the api
command's custom catch/validation paths. Non-JSON output is byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Jul 23, 2026
# [2.19.0](v2.18.1...v2.19.0) (2026-07-23) ### Features * emit structured errors in JSON mode ([#221](#221)) ([45a0b87](45a0b87))
|
🎉 This PR is included in version 2.19.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
Intent
Add structured error output for --json mode in confluence-cli (roadmap item #1). Problem: success output was structured JSON (lib/output.js emitJson) but failures always printed chalk-colored prose to stderr even under --json, so agents/scripts (jq pipelines, the Claude skill) could parse success but not failure. Goal: when the global --json flag is active, any command failure emits exactly ONE machine-parseable JSON object on stderr (stdout stays empty) and nothing else, shape {error, code, status, details}. Decisions/tradeoffs: (1) errors stay on stderr to preserve the existing stdout=data / stderr=diagnostics contract the README/SKILL.md already teach; (2) exit codes are unchanged (1; the api command's pre-existing jq exit-2 is preserved); (3) code is a small stable enum derived from existing classification: AUTH_FAILED (401/403), NOT_FOUND (404), API_ERROR (other 4xx/5xx), NETWORK (connection/DNS/timeout error codes), VALIDATION (thrown Error with no HTTP response), UNKNOWN; (4) details carries the raw API response body or null. Both error paths are covered: the global handleCommandError in bin/confluence.js and the api command's custom catch plus its inline validation/read-only/jq sites in bin/commands/api.js. Non-JSON output is intentionally byte-identical (existing users see the same prose, including the read-only Tip line and jq messages). Deliberately did NOT refactor handleCommandError beyond structured emission and did NOT touch the markdown/storage conversion pipeline (that is a separate roadmap item). Added unit/integration tests (auth-failure, API-error-with-body, validation-error under --json, plus a non-json-prose-unchanged assertion, and real-binary api-command --json tests) and updated README JSON section and SKILL.md Error Patterns.
What Changed
{error, code, status, details}object on stderr for command failures in--jsonmode while keeping stdout empty and human-readable output unchanged.Risk Assessment
✅ Low: The follow-up accurately documents jq’s preserved exit status, and no remaining material issues were found in the reviewed scope.
Testing
After installing missing locked dependencies, all focused and full automated tests passed; real CLI runs confirmed single parseable JSON stderr objects, empty stdout, correct classifications/details and exit codes, while preserving human diagnostics, and generated dependencies were cleaned up.
Evidence: End-to-end CLI transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 3 issues found → auto-fixed (2) ✅
bin/confluence.js:69- Required: “any command failure emits exactly ONE…JSON object on stderr (stdout stays empty).” This handler is bypassed bygetConfig()andassertWritable(), which callprocess.exit()after prose output; missing config also writes guidance to stdout. Route these failures through structured handling or explicitly narrow the requirement.bin/confluence.js:69- Commander parsing andpreActionfailures never reach this handler. Missing arguments, unknown options, and unsupported--jsoncommands still emit prose, contradicting the required “any command failure” behavior. These should emit a singleVALIDATIONobject unless explicitly exempted.README.md:396- The new guarantee says failure leaves stdout empty, butcopy-tree --json --fail-on-errorandversions-purge --jsonemit success JSON to stdout before setting exit status 1 for partial failures. Decide whether these nonzero outcomes require structured stderr errors or are an intentional exception.🔧 Fix: Clarify structured JSON error coverage and exceptions
1 error still open:
README.md:396- Intent requires “the api command's pre-existing jq exit-2 is preserved,” but this says the API error path exits1; jq failures still calltrackAndExit(..., 2). Qualify this sentence and the matching SKILL.md text with the jq exception.🔧 Fix: Document preserved jq failure exit status
✅ Re-checked - no issues remain.
🔧 **Test** - 1 issue found → auto-fixed (6) ✅
bin/confluence.js:22- A supported global--jsoncommand bypasses structured emission:--json delete 123 --yeswith a read-only profile exits 1 and prints separate Error and Tip prose lines instead of exactly one{error, code, status, details}object.assertWritable()callsprocess.exit(1)beforewithClient()can route the failure throughhandleCommandError. Route this precondition through JSON-aware error handling and add a real-binary regression test.npm test -- --runInBand tests/json-output.test.js tests/api-command.test.js(initially blocked by absent dependencies, then passed afternpm ci --ignore-scripts)npm test -- --runInBandReal CLI against a deterministic local HTTP mock: global 401 and 404 paths, API 500 path, inline validation, network failure, jq compile failure, and missing jqReal CLI withCONFLUENCE_READ_ONLY=true:node bin/index.js --json delete 123 --yesReal CLI without JSON:node bin/index.js api /rest/api/content -f badfieldjq -e 'keys == ["code","details","error","status"]'over captured JSON stderr and zero-byte assertions over captured stdoutRemovednode_modules, stopped the mock server, and confirmed the worktree remained clean🔧 Fix: fix: route read-only failures through JSON handler
1 error still open:
lib/config.js:896- A supported--json spacesinvocation with no configuration exits 1 but writes setup instructions to stdout and non-JSON prose to stderr.getConfig()callsprocess.exit(1)beforehandleCommandError, violating the required single JSON stderr object and empty stdout contract. Route configuration failures through JSON-aware handling while preserving non-JSON output, and add a real-binary regression test.npm ci --ignore-scriptsnpm test -- --runInBand tests/json-output.test.js tests/api-command.test.js tests/read-only.test.js tests/with-client.test.jsnpm test -- --runInBandReal CLI checks for 401, 404, 500, validation, read-only, jq exit 2, network failure, and non-JSON proseCONFLUENCE_CONFIG_DIR=<empty> node bin/index.js --json spaces🔧 Fix: fix JSON output for configuration failures
3 issues (2 errors, 1 warning) still open:
bin/confluence.js:154-confluence --json infoand unsupported-command failures still emit Commander/plain prose rather than one structured JSON object, contrary to the required any-failure contract.lib/config.js:292- Malformed config files print recovery prose before the JSON payload, making stderr unparsable in both standard andapicommands.README.md:419- The exceptions paragraph incorrectly says read-only and configuration-loading failures remain prose, contradicting current behavior.npm ci --ignore-scriptsnpm test -- --runInBand tests/json-output.test.js tests/api-command.test.js tests/read-only.test.js tests/with-client.test.js tests/config.test.jsnpm test -- --runInBandnode /var/folders/mm/mzvsb9xn40vd3pqkp4y8n_sm0000gn/T/no-mistakes-evidence/01KY7CH68RKFRX3CZ9WNSQ9NHK/capture-cli-errors.js "$PWD"Real CLI checks against a controlled HTTP server for 401, 404, 503, network, validation, read-only, config, and jq failures🔧 Fix: fix JSON handling for usage and config errors
1 error still open:
lib/confluence-client.js:380- A supported--json info <display URL>failure logs the caught Axios error before the structured handler runs, producing a diagnostic stack plus JSON on stderr. The stream cannot be parsed as one JSON object. Suppress or route this intermediate diagnostic in JSON mode and add a real-binary regression test.npm ci --ignore-scriptsnpm test -- --runInBand tests/json-output.test.js tests/api-command.test.js tests/read-only.test.js tests/with-client.test.jsnpm test -- --runInBandReal-binary local-server checks for 401, 404, 500, network, read-only, usage, jq, and non-JSON prose pathsnode bin/index.js --json info http://127.0.0.1:43129/display/SPACE/TitleMissing-jq check using a restrictedPATH🔧 Fix: Suppress display URL diagnostics in JSON mode
2 issues (1 error, 1 warning) still open:
lib/confluence-client.js:293- A mode-0644 mTLS client key prints a permission warning before the structured NETWORK payload. Whole-stderr JSON.parse fails. Suppress this warning only in JSON mode and add a real-binary regression test.plugins/confluence/skills/confluence/SKILL.md:828- The bundled skill incorrectly says usage, unsupported-JSON, read-only, and configuration failures remain prose, although they now emit structured JSON.Initial focusednpm test -- --runInBand ...attempt (Jest unavailable)npm ci --ignore-scriptsnpm test -- --runInBand tests/display-url-errors.test.js tests/json-output.test.js tests/api-command.test.js tests/read-only.test.js tests/with-client.test.jsnpm test -- --runInBandReal-binary JSON and non-JSON display-URL failuresReal-binary JSON network failure with a mode-0644 mTLS keyrm -rf node_modules && git status --short --branch🔧 Fix: Suppress mTLS warning noise in JSON mode
2 errors still open:
lib/netrc.js:104- An unreadable.netrcemits a warning before the structured error. Consequently,JSON.parse(stderr)fails. Suppress this warning only in JSON mode and add a real-binary regression test.lib/config.js:65- An invalidCONFLUENCE_LINK_STYLEemits a fallback warning before a later structured error. Consequently,JSON.parse(stderr)fails. Gate this warning in JSON mode while preserving human output, with a regression test.npm test -- --runInBand tests/json-output.test.js tests/api-command.test.js tests/read-only.test.js tests/display-url-errors.test.js tests/mtls-errors.test.js tests/with-client.test.jsnpm test -- --runInBandReal CLI:confluence --json api /authagainst a local 401 serverReal CLI:confluence --json api /ok --jq '.['against a local serverReal CLI display-URL resolution failureReal CLI mTLS failure with a mode-0644 key, in JSON and human modesReal CLI unreadable-netrc and invalid-link-style failure probes🔧 Fix: Suppress configuration warnings in JSON mode
✅ Re-checked - no issues remain.
Initial focused Jest command (dependencies absent:jest: command not found)npm ci --ignore-scriptsnpm test -- --runInBand tests/json-output.test.js tests/api-command.test.js tests/read-only.test.js tests/with-client.test.js tests/display-url-errors.test.js tests/mtls-errors.test.js tests/config-warning-errors.test.jsnpm test -- --runInBandReal CLI checks for HTTP 401/500, network, read-only, link-style warning suppression, non-JSON diagnostics, and jq exit status 2Removednode_modulesand verified a clean worktree✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.