Skip to content

[DX-793] [DX-938] Standardize JSON output with envelope format, fail() error handling, and format helpers#157

Open
umair-ably wants to merge 7 commits intomainfrom
dx-793-json-envelope
Open

[DX-793] [DX-938] Standardize JSON output with envelope format, fail() error handling, and format helpers#157
umair-ably wants to merge 7 commits intomainfrom
dx-793-json-envelope

Conversation

@umair-ably
Copy link
Contributor

@umair-ably umair-ably commented Mar 9, 2026

Review Strategy

This PR has 7 commits organized for efficient review:

Commit Files Lines How to review
1. Add buildJsonRecord utility and CommandError class 4 ~160 Read carefully — new infrastructure everything else depends on
2. Add JSON envelope and fail() to base classes and services 8 ~545 Read carefully — framework methods (fail(), logJsonResult, logJsonEvent)
3. Migrate commands to JSON envelope, fail(), and format helpers 91 ~4,300 Skim — same mechanical pattern across all commands. Spot-check 2-3 files, the rest follow the same shape
4. Add NDJSON test helpers and update unit tests 44 ~1,800 Skim — test infrastructure + updated assertions
5. Fix e2e tests for JSON envelope format 2 ~33 Quick glance
6. Update documentation and skills 5 ~200 Read — docs and skill pattern updates
7. Migrate placeholder topic commands to BaseTopicCommand 43 ~480 Quick glance — mechanical migration of 18 placeholder/topic files to BaseTopicCommand, plus 22 missing --json examples and 3 other small fixes

Recommended order: Commits 1 → 2 → spot-check a few files in 3 → 6 → 4 → 5 → 7

Problem

The --json and --pretty-json flags had several issues that made output difficult to consume programmatically:

  1. --json always pretty-printedformatJsonOutput() used 2-space indent even for --json, producing multi-line output incompatible with line-based streaming.
  2. No metadata envelope — Raw data objects with no context about the source command or record type.
  3. Verbose logs mixed with datalogCliEvent() records interspersed with data records, with no way to filter.
  4. Inconsistent error/success shapes — Some commands included success: true, some didn't. Error formats varied.
  5. Non-JSON text in JSON mode — Some code paths emitted human-readable text when --json was active, breaking jq pipelines.
  6. Fragile error handling — Commands used this.error() directly with inconsistent patterns. Easy to forget return; after error calls, leading to code continuing after errors.

Breaking Changes

If you are parsing --json output programmatically, note these changes:

Output format

  • --json is now compact single-line (was pretty-printed with 2-space indent). Each record is one line, compatible with NDJSON/JSON Lines. --pretty-json is unchanged.

Envelope wrapper

  • Every JSON record now includes type ("result", "event", "error", "log") and command fields. Existing data fields are preserved but wrapped in this envelope.

Renamed fields

To avoid collision with the envelope's type field, several data fields have been renamed:

Command Before After
auth:issue-jwt-token type: "jwt" tokenType: "jwt"
spaces:cursors:subscribe type: "cursor_update" eventType: "cursor_update"
spaces:members:subscribe type: "member_update" eventType: "member_update"
spaces:locks:subscribe type: "lock_event" eventType: "lock_event"
spaces:locations:subscribe type: "location_update" / "locations_snapshot" eventType: "location_update" / "locations_snapshot"
rooms:*:subscribe type: "initialSnapshot" eventType: "initialSnapshot"
Various subscribe commands type: <event_action> eventType: <event_action>

Removed fields

  • Manual success: true / success: false fields have been removed from individual command data. The envelope now manages success automatically (true for "result" type, false for "error" type).

Changes

1. Compact --json output (NDJSON for streaming)

--json now produces compact single-line output, compatible with NDJSON/JSON Lines. --pretty-json is unchanged.

Flag Before After
--json Pretty-printed (2-space indent) Compact single-line
--pretty-json Colorized multi-line Unchanged

2. Flat envelope with type + command discriminator

Every JSON record includes:

  • type"result", "event", "error", or "log"
  • command — the oclif command ID (e.g., "channels:subscribe")
  • success — boolean, present only on "result" and "error" types

3. fail() error handling with CommandError

All commands now use this.fail(error, flags, component) as the single error funnel. It returns never, so TypeScript enforces no code runs after it — eliminating the "forgotten return;" bug class. Errors are wrapped in CommandError which preserves Ably error codes and HTTP status codes.

4. Fixed non-JSON text leaking into --json mode

Human-readable messages (e.g., stats unsubscribe) are now suppressed when JSON output is active.

5. Improved test coverage

  • Tests that previously passed vacuously on empty output now assert jsonLines.length > 0 before validating content.
  • New test/helpers/ndjson.ts with parseNdjsonLines(), parseLogLines(), and filterByType() helpers.

6. Migrated placeholder topic commands to BaseTopicCommand and added --json examples

18 placeholder/topic files in rooms/, spaces/, and config/ that extended Command or AblyBaseCommand directly have been migrated to BaseTopicCommand for consistency with all other topic commands. Added missing --json examples to 22 leaf commands.

Output Examples

One-shot result (ably channels list --json):

{"type":"result","command":"channels:list","success":true,"channels":[{"name":"my-channel"}],"total":1}

Streaming events (ably channels subscribe test --json):

{"type":"event","command":"channels:subscribe","channel":"test","data":"hello","timestamp":"2025-04-11T12:00:01.000Z"}
{"type":"event","command":"channels:subscribe","channel":"test","data":"world","timestamp":"2025-04-11T12:00:02.000Z"}

Error (ably channels publish nonexistent hello --json):

{"type":"error","command":"channels:publish","success":false,"error":"Channel not found"}

Verbose log (ably channels subscribe test --json --verbose):

{"type":"log","command":"channels:subscribe","component":"subscribe","event":"channelAttached","message":"Attached to channel test"}
{"type":"event","command":"channels:subscribe","channel":"test","data":"hello","timestamp":"2025-04-11T12:00:01.000Z"}

Industry Precedent

CLI --json behavior Streaming format
Docker --format json → compact single-line NDJSON with type, action, status fields
GitHub CLI Auto-detects TTY: pretty in terminal, compact when piped N/A
Stripe CLI --format JSON for machine-readable Had same bug: non-JSON text mixed with JSON

References: Docker events, CLI JSON guide, NDJSON spec

Key Methods

Method Purpose
buildJsonRecord(type, command, data) Low-level: builds {type, command, success?, ...data} envelope
this.logJsonResult(data, flags) Emits a "result" envelope to stdout
this.logJsonEvent(data, flags) Emits an "event" envelope to stdout
this.fail(error, flags, component) Single error funnel: logs event, emits JSON error envelope, exits. Returns never
CommandError Error class preserving Ably error codes and HTTP status
logCliEvent(flags, component, event, message) Emits type: "log" envelope (verbose only)

Migration Scope

  • Core infrastructure: buildJsonRecord() utility, CommandError class, fail() method returning never
  • Base classes: logJsonResult/logJsonEvent helpers on AblyBaseCommand, updated ControlBaseCommand, ChatBaseCommand, SpacesBaseCommand, StatsBaseCommand
  • ~91 commands migrated to use fail(), logJsonResult/logJsonEvent, and format helpers
  • 18 placeholder/topic files migrated from Command/AblyBaseCommand to BaseTopicCommand
  • 22 commands received missing --json examples
  • StatsDisplay updated to use logToStderr instead of console.log, includes envelope fields
  • Test helpers: New test/helpers/ndjson.ts with captureJsonLogs(), parseNdjsonLines(), filterByType()

@vercel
Copy link

vercel bot commented Mar 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Mar 11, 2026 2:29am

Request Review

@umair-ably umair-ably changed the base branch from main to new-command-skill March 9, 2026 16:07
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Walkthrough

This PR refactors JSON output handling to use envelope-based formatting via formatJsonRecord(type, data, flags), replacing direct formatJsonOutput calls across ~60+ command files. New utilities buildJsonRecord and NDJSON test helpers are introduced. Success flags are removed from payloads, and some event fields are renamed to eventType/logType for clarity.

Changes

Cohort / File(s) Summary
Core JSON Output Infrastructure
src/base-command.ts, src/utils/output.ts, src/stats-base-command.ts, docs/AGENTS.md, docs/Project-Structure.md
Introduced formatJsonRecord() method and buildJsonRecord() utility to wrap JSON output with envelope structure (type, command, success). Refactored stats display to use envelope-based formatting with new command and prettyJson options. Updated documentation to reflect envelope-based JSON strategy.
Test Helpers & Utilities
test/helpers/ndjson.ts, test/unit/helpers/ndjson.test.ts, test/unit/utils/output.test.ts, test/unit/base/base-command-enhanced.test.ts, test/unit/base/auth-info-display.test.ts
Added NDJSON parsing helpers (parseNdjsonLines, parseLogLines, filterByType). Enhanced base command tests with testFormatJsonRecord() method. Added unit tests for buildJsonRecord() and NDJSON utilities. Updated test signatures to align with new JSON record formatting.
Accounts Commands
src/commands/accounts/current.ts, src/commands/accounts/list.ts, src/commands/accounts/login.ts, src/commands/accounts/logout.ts, src/commands/accounts/switch.ts
Replaced formatJsonOutput with formatJsonRecord("result", ...) for success paths. Removed explicit success: true/false fields from all JSON payloads. Updated error handling to use envelope-based formatting.
Apps Commands
src/commands/apps/create.ts, src/commands/apps/current.ts, src/commands/apps/delete.ts, src/commands/apps/list.ts, src/commands/apps/set-apns-p12.ts, src/commands/apps/update.ts, src/commands/apps/channel-rules/*
Unified JSON output to use formatJsonRecord("result", ...) for all success paths. Removed success flags from payloads. Updated nested channel-rules commands (create, delete, list, update) with consistent envelope formatting.
Auth Commands
src/commands/auth/issue-ably-token.ts, src/commands/auth/issue-jwt-token.ts, src/commands/auth/revoke-token.ts, src/commands/auth/keys/*
Replaced formatJsonOutput with formatJsonRecord("result", ...) across all key management and token commands. Removed success fields from payloads. Updated error payloads in get, list, create, and revoke operations.
Channels Commands
src/commands/channels/batch-publish.ts, src/commands/channels/history.ts, src/commands/channels/list.ts, src/commands/channels/publish.ts, src/commands/channels/subscribe.ts, src/commands/channels/occupancy/*, src/commands/channels/presence/*
Unified JSON output to formatJsonRecord("result", ...) for history and list; use formatJsonRecord("event", ...) for streaming events. Removed success flags. Presence commands updated with envelope-based formatting for both events and results.
Config Commands
src/commands/config/path.ts, src/commands/config/show.ts
Replaced formatJsonOutput with formatJsonRecord("result", ...). Removed success fields from all JSON output paths.
Connections & Integrations
src/commands/connections/test.ts, src/commands/integrations/create.ts, src/commands/integrations/delete.ts, src/commands/integrations/get.ts, src/commands/integrations/list.ts, src/commands/integrations/update.ts
Unified JSON output to envelope format with formatJsonRecord("result", ...). Removed success fields. Updated Target field rendering in non-JSON output to use direct JSON stringification instead of formatJsonOutput.
Logs Commands
src/commands/logs/history.ts, src/commands/logs/subscribe.ts, src/commands/logs/channel-lifecycle/subscribe.ts, src/commands/logs/connection-lifecycle/history.ts, src/commands/logs/connection-lifecycle/subscribe.ts, src/commands/logs/push/history.ts, src/commands/logs/push/subscribe.ts
Replaced formatJsonOutput with formatJsonRecord("result", ...) for history and formatJsonRecord("event", ...) for streaming events. Removed success flags. Updated envelope structure for all log output types.
Queues & Rooms Commands
src/commands/queues/create.ts, src/commands/queues/delete.ts, src/commands/queues/list.ts, src/commands/rooms/list.ts, src/commands/rooms/messages/history.ts, src/commands/rooms/messages/send.ts, src/commands/rooms/messages/subscribe.ts
Replaced formatJsonOutput with formatJsonRecord("result", ...) for results and formatJsonRecord("event", ...) for streaming. Removed success flags from payloads. Updated message reactions to remove inline interface and use envelope formatting directly.
Spaces Commands
src/commands/spaces/list.ts, src/commands/spaces/cursors/*.ts, src/commands/spaces/locks/*.ts, src/commands/spaces/locations/*.ts, src/commands/spaces/members/*.ts, src/commands/spaces/reactions/*.ts
Systematically replaced formatJsonOutput with formatJsonRecord("result", ...) or formatJsonRecord("event", ...) across all subcommands. Renamed type to eventType in event payloads (cursors, locations, members, reactions). Removed success flags from all JSON outputs.
Support & Bench Commands
src/commands/support/ask.ts, src/commands/bench/publisher.ts, src/commands/bench/subscriber.ts
Replaced formatJsonOutput with formatJsonRecord("result", ...). Removed success field from JSON payloads.
Test Updates: Command Tests
test/unit/commands/auth/issue-jwt-token.test.ts, test/unit/commands/channels/occupancy/subscribe.test.ts, test/unit/commands/channels/subscribe.test.ts, test/unit/commands/logs/*/subscribe.test.ts, test/unit/commands/queues/list.test.ts, test/unit/commands/rooms/messages.test.ts, test/unit/commands/rooms/messages/reactions/subscribe.test.ts, test/unit/commands/rooms/occupancy/subscribe.test.ts, test/unit/commands/rooms/presence/*.test.ts, test/unit/commands/rooms/reactions/subscribe.test.ts, test/unit/commands/rooms/typing/subscribe.test.ts, test/unit/commands/spaces/*/test.ts
Updated test assertions to use NDJSON parsing via parseLogLines/parseNdjsonLines. Added validation of JSON envelope structure (type, command, success fields). Updated expected field names to match new envelope format. Extended tests to verify JSON output for streaming and result commands.
Test Updates: Service Tests
test/unit/services/stats-display.test.ts, test/unit/commands/spaces/cursors/set.test.ts
Updated assertions to parse JSON envelope instead of raw JSON. Modified constructor calls to include command option. Updated expected output structure to validate envelope fields (type, command, success).

Sequence Diagram(s)

The conditions for generating sequence diagrams are not met. The changes primarily involve refactoring JSON output formatting across many command files rather than introducing new feature interactions or significantly modifying control flow between 3+ distinct components. Most changes are localized to individual command implementations and follow a consistent pattern (formatting function replacement).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR systematically refactors JSON output across 80+ files with similar but not identical patterns. While most changes follow the same transformation (replacing formatJsonOutput with formatJsonRecord), the high number of modified files, the introduction of new utility functions with specific envelope behavior, and the need to verify consistency across varied command implementations require careful review. Test updates add moderate complexity by validating the new envelope structure across numerous scenarios.

Possibly related PRs

  • [DX-928] Reduce Duplication  #153: Introduces centralized error/flag handlers (handleCommandError, parseJsonFlag) and timestamp utilities in the same infrastructure files (src/base-command.ts, src/utils/output.ts), making the JSON output refactoring changes directly related at the code level.
  • fix: various tidy ups, inconsistency fixes #101: Modifies JSON output and error handling in src/base-command.ts with overlapping goals to standardize JSON emission patterns across commands.

Suggested reviewers

  • mattheworiordan
  • denissellu
  • sacOO7

Poem

🐰 A JSON envelope wraps each result tight,
Type and command make the format bright,
No more success flags cluttering the way,
NDJSON streams flow smooth as day,
From rooms to spaces, a unified delight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: standardizing JSON output with envelope format, fail() error handling, and format helpers. This directly reflects the core objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dx-793-json-envelope

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@umair-ably umair-ably requested a review from Copilot March 9, 2026 16:11
@umair-ably
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR standardizes Ably CLI’s machine-readable output by making --json emit compact NDJSON-friendly records and introducing a consistent JSON envelope (type, command, success?) across commands, events, logs, and errors.

Changes:

  • Add a typed JSON envelope builder (buildJsonRecord) and a command-level wrapper (formatJsonRecord) so --json is compact and --pretty-json remains pretty/colorized.
  • Migrate many commands from ad-hoc formatJsonOutput() payloads to formatJsonRecord() and rename conflicting payload fields (e.g., typeeventType / logType).
  • Add NDJSON parsing helpers for tests and update/extend unit tests to assert non-empty JSON output and envelope shape.

Reviewed changes

Copilot reviewed 115 out of 115 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/unit/utils/output.test.ts Adds unit tests for buildJsonRecord envelope rules.
test/unit/services/stats-display.test.ts Updates stats display JSON test to assert envelope fields.
test/unit/helpers/ndjson.test.ts Adds tests for NDJSON/log-line parsing helpers.
test/unit/commands/spaces/locks/subscribe.test.ts Adds JSON envelope assertions for initial locks snapshot output.
test/unit/commands/spaces/locks/get.test.ts Adds JSON envelope assertions for lock get output.
test/unit/commands/spaces/locks/get-all.test.ts Adds JSON envelope assertions for lock list output.
test/unit/commands/spaces/locations/subscribe.test.ts Adds JSON envelope assertions for initial locations snapshot output.
test/unit/commands/spaces/locations/get-all.test.ts Adds JSON envelope assertions for location list output.
test/unit/commands/spaces/cursors/subscribe.test.ts Adds JSON envelope assertions for cursor update events.
test/unit/commands/spaces/cursors/set.test.ts Switches JSON assertions to parse structured output.
test/unit/commands/spaces/cursors/get-all.test.ts Adds JSON envelope assertions for cursor list output.
test/unit/commands/rooms/typing/subscribe.test.ts Uses parseLogLines to assert event envelopes in captured logs.
test/unit/commands/rooms/reactions/subscribe.test.ts Uses parseLogLines to assert event envelopes in captured logs.
test/unit/commands/rooms/presence/subscribe.test.ts Uses parseLogLines; asserts eventType and envelope fields.
test/unit/commands/rooms/presence/enter.test.ts Uses parseLogLines; asserts eventType and envelope fields.
test/unit/commands/rooms/occupancy/subscribe.test.ts Uses parseLogLines; asserts envelope + eventType for snapshots.
test/unit/commands/rooms/messages/reactions/subscribe.test.ts Uses parseLogLines; asserts event envelopes for reaction summaries.
test/unit/commands/rooms/messages.test.ts Adds/updates tests to assert result/event envelopes for --json flows.
test/unit/commands/queues/list.test.ts Updates error-path assertions to parse and validate error envelope.
test/unit/commands/logs/subscribe.test.ts Adds JSON envelope assertions for log event streaming.
test/unit/commands/logs/push/subscribe.test.ts Adds JSON envelope assertions for push log events.
test/unit/commands/logs/connection-lifecycle/subscribe.test.ts Adds JSON envelope assertions for connection lifecycle log events.
test/unit/commands/logs/channel-lifecycle/subscribe.test.ts Adds JSON envelope assertions for channel lifecycle log events.
test/unit/commands/channels/subscribe.test.ts Adds JSON envelope assertions for channel message events.
test/unit/commands/channels/occupancy/subscribe.test.ts Adds JSON envelope assertions for occupancy events.
test/unit/commands/auth/issue-jwt-token.test.ts Updates assertions to avoid colliding with envelope type field.
test/unit/base/base-command-enhanced.test.ts Adds tests for compact --json output and formatJsonRecord wrapping.
test/unit/base/auth-info-display.test.ts Updates test helper to match shouldHideAccountInfo() signature change.
test/helpers/ndjson.ts Introduces shared NDJSON/log parsing helpers for tests.
src/utils/output.ts Adds JsonRecordType and buildJsonRecord() envelope builder.
src/stats-base-command.ts Passes command ID/pretty-json to stats display; suppresses non-JSON unsubscribe text.
src/services/stats-display.ts Emits enveloped JSON records for stats; supports pretty printing in JSON mode.
src/commands/support/ask.ts Switches JSON output to formatJsonRecord("result", ...).
src/commands/spaces/members/subscribe.ts Migrates JSON outputs to envelope; renames event payload typeeventType; standardizes jsonError usage.
src/commands/spaces/members/enter.ts Migrates JSON outputs to envelope; renames event payload typeeventType; standardizes jsonError usage.
src/commands/spaces/locks/subscribe.ts Migrates JSON outputs to envelope; renames event payload typeeventType; standardizes jsonError usage.
src/commands/spaces/locks/get.ts Migrates JSON outputs to envelope for lock get paths.
src/commands/spaces/locks/get-all.ts Migrates JSON outputs to envelope for lock list paths.
src/commands/spaces/locks/acquire.ts Migrates JSON outputs to envelope for acquire + error paths.
src/commands/spaces/locations/subscribe.ts Migrates JSON outputs to envelope; renames event payload typeeventType; standardizes jsonError usage.
src/commands/spaces/locations/set.ts Migrates JSON outputs to envelope for set + event paths; standardizes jsonError usage.
src/commands/spaces/locations/get-all.ts Migrates JSON outputs to envelope for list paths; standardizes jsonError usage.
src/commands/spaces/list.ts Migrates JSON outputs to envelope for list paths.
src/commands/spaces/cursors/subscribe.ts Migrates JSON outputs to envelope; renames event payload typeeventType; standardizes jsonError usage.
src/commands/spaces/cursors/set.ts Migrates JSON outputs to envelope; standardizes jsonError usage.
src/commands/spaces/cursors/get-all.ts Migrates JSON outputs to envelope for connection + list paths.
src/commands/rooms/typing/subscribe.ts Migrates JSON event output to formatJsonRecord("event", ...).
src/commands/rooms/reactions/subscribe.ts Migrates JSON event output to formatJsonRecord("event", ...).
src/commands/rooms/reactions/send.ts Migrates JSON result/error output to envelope.
src/commands/rooms/presence/subscribe.ts Renames payload typeeventType; migrates JSON event output to envelope.
src/commands/rooms/presence/enter.ts Renames payload typeeventType; migrates JSON event output to envelope.
src/commands/rooms/occupancy/subscribe.ts Renames payload typeeventType; migrates JSON event output to envelope.
src/commands/rooms/occupancy/get.ts Migrates JSON result output to envelope.
src/commands/rooms/messages/subscribe.ts Migrates JSON event output to envelope; standardizes jsonError usage.
src/commands/rooms/messages/send.ts Migrates JSON result/error output to envelope; standardizes jsonError usage.
src/commands/rooms/messages/reactions/subscribe.ts Renames payload typeeventType; migrates JSON event output to envelope.
src/commands/rooms/messages/reactions/send.ts Migrates JSON result/error output to envelope; removes now-unneeded result interface.
src/commands/rooms/messages/reactions/remove.ts Migrates JSON result output to envelope; removes now-unneeded result interface.
src/commands/rooms/messages/history.ts Migrates JSON progress/result output to envelope.
src/commands/rooms/list.ts Migrates JSON list output to envelope.
src/commands/queues/list.ts Migrates JSON list/error output to envelope.
src/commands/queues/delete.ts Migrates JSON result/error output to envelope.
src/commands/queues/create.ts Migrates JSON result output to envelope.
src/commands/logs/subscribe.ts Renames payload typelogType; migrates JSON event output to envelope.
src/commands/logs/push/subscribe.ts Migrates JSON event output to envelope.
src/commands/logs/push/history.ts Migrates JSON result/error output to envelope.
src/commands/logs/history.ts Migrates JSON result/error output to envelope.
src/commands/logs/connection-lifecycle/subscribe.ts Migrates JSON event output to envelope.
src/commands/logs/connection-lifecycle/history.ts Migrates JSON result/error output to envelope.
src/commands/logs/channel-lifecycle/subscribe.ts Migrates JSON event output to envelope.
src/commands/integrations/update.ts Migrates JSON result output to envelope; uses JSON.stringify for human-readable target printing.
src/commands/integrations/list.ts Migrates JSON result output to envelope; uses JSON.stringify for human-readable target printing.
src/commands/integrations/get.ts Migrates JSON result output to envelope; uses JSON.stringify for human-readable target printing.
src/commands/integrations/delete.ts Migrates JSON result/error output to envelope.
src/commands/integrations/create.ts Migrates JSON result output to envelope; uses JSON.stringify for human-readable target printing.
src/commands/connections/test.ts Migrates JSON result output to envelope.
src/commands/config/show.ts Migrates JSON output to envelope for config show paths.
src/commands/config/path.ts Migrates JSON result output to envelope.
src/commands/channels/subscribe.ts Migrates JSON event/error output to envelope.
src/commands/channels/publish.ts Migrates JSON result/error output to envelope.
src/commands/channels/presence/subscribe.ts Migrates JSON event output to envelope.
src/commands/channels/presence/enter.ts Migrates JSON event/result/error output to envelope.
src/commands/channels/occupancy/subscribe.ts Migrates JSON event output to envelope.
src/commands/channels/occupancy/get.ts Migrates JSON result/error output to envelope.
src/commands/channels/list.ts Migrates JSON result/error output to envelope.
src/commands/channels/history.ts Migrates JSON result/error output to envelope.
src/commands/channels/batch-publish.ts Migrates JSON result/error output to envelope; uses JSON.stringify for human-readable response printing.
src/commands/bench/subscriber.ts Migrates JSON result output to envelope.
src/commands/bench/publisher.ts Migrates JSON result output to envelope.
src/commands/auth/revoke-token.ts Migrates JSON result/error output to envelope.
src/commands/auth/keys/revoke.ts Migrates JSON result/error output to envelope.
src/commands/auth/keys/list.ts Migrates JSON result/error output to envelope.
src/commands/auth/keys/get.ts Migrates JSON result/error output to envelope.
src/commands/auth/keys/current.ts Migrates JSON result output to envelope.
src/commands/auth/keys/create.ts Migrates JSON result/error output to envelope.
src/commands/auth/issue-jwt-token.ts Migrates JSON result output to envelope; renames token type field to avoid collision.
src/commands/auth/issue-ably-token.ts Migrates JSON result output to envelope.
src/commands/apps/update.ts Migrates JSON result/error output to envelope.
src/commands/apps/set-apns-p12.ts Migrates JSON result/error output to envelope.
src/commands/apps/list.ts Migrates JSON list output to envelope.
src/commands/apps/delete.ts Migrates JSON result/error output to envelope.
src/commands/apps/current.ts Migrates JSON result output to envelope.
src/commands/apps/create.ts Migrates JSON result/error output to envelope.
src/commands/apps/channel-rules/update.ts Migrates JSON result/error output to envelope.
src/commands/apps/channel-rules/list.ts Migrates JSON result/error output to envelope.
src/commands/apps/channel-rules/delete.ts Migrates JSON result/error output to envelope.
src/commands/apps/channel-rules/create.ts Migrates JSON result/error output to envelope.
src/commands/accounts/switch.ts Migrates JSON result/error output to envelope.
src/commands/accounts/logout.ts Migrates JSON result/error output to envelope.
src/commands/accounts/login.ts Migrates JSON result/error output to envelope.
src/commands/accounts/list.ts Migrates JSON result/error output to envelope.
src/commands/accounts/current.ts Migrates JSON result output to envelope in multiple modes/paths.
src/base-command.ts Implements compact --json, adds formatJsonRecord, updates verbose JSON logs, and changes account info hiding signature.
docs/Project-Structure.md Documents new NDJSON helper in test helpers.
AGENTS.md Updates contributor guidance to require formatJsonRecord JSON envelope output.
Comments suppressed due to low confidence (2)

src/commands/config/show.ts:35

  • In JSON mode when the config file is missing, the code outputs formatJsonRecord("result", { error: ... }) and then exits with code 1. Because result envelopes default to success: true, this produces a contradictory record (type: result + success: true + error: ...) for a failing command. Use an error envelope instead (e.g. formatJsonRecord("error", ...) or jsonError(...)) so success:false and type:"error" match the exit code.
    src/commands/accounts/current.ts:191
  • In the web-CLI error path, the JSON output uses formatJsonRecord("result", { error: ... }). Since result records default to success: true, this yields an inconsistent shape for a failed operation (type: result + success: true + error). Consider emitting an error record (and likely exiting non-zero) so consumers can reliably treat type:"error"/success:false as failure.
    } catch (error) {
      // If we can't get account details, show an error message
      if (this.shouldOutputJson(flags)) {
        this.log(
          this.formatJsonRecord(
            "result",
            {
              error: errorMessage(error),
              mode: "web-cli",
            },
            flags,
          ),
        );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (12)
src/commands/spaces/cursors/set.ts (1)

136-160: ⚠️ Potential issue | 🟠 Major

Keep the --data.position validation JSON-safe.

The earlier parse failures now return JSON envelopes, but the invalid-position branch still goes through this.error(). Under --json, that leaks human-readable output and breaks the NDJSON contract for malformed cursor payloads.

🐛 Proposed fix
       if (
         !cursorData.position ||
         typeof (cursorData.position as Record<string, unknown>).x !==
           "number" ||
         typeof (cursorData.position as Record<string, unknown>).y !== "number"
       ) {
-        this.error(
-          'Invalid cursor position in --data. Expected format: {"position":{"x":number,"y":number}}',
-        );
+        const errorMsg =
+          'Invalid cursor position in --data. Expected format: {"position":{"x":number,"y":number}}';
+        if (this.shouldOutputJson(flags)) {
+          this.jsonError({ error: errorMsg }, flags);
+        } else {
+          this.error(errorMsg);
+        }
         return;
       }
As per coding guidelines, "Wrap all human-readable output (progress, success, listening) in `if (!this.shouldOutputJson(flags))` to prevent polluting `--json` output".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/cursors/set.ts` around lines 136 - 160, The invalid
cursor position branch uses this.error(...) which emits human-readable output
even when --json/NDJSON is requested; update the validation handling around
cursorData.position so that if (this.shouldOutputJson(flags)) you call
this.jsonError({ error: 'Invalid cursor position ...' }, flags) and otherwise
emit the human-readable this.error(...) (or wrap the this.error call in if
(!this.shouldOutputJson(flags))). Reference cursorData, shouldOutputJson(flags),
jsonError, and the existing invalid cursor position message when making the
change.
src/commands/spaces/locks/subscribe.ts (1)

95-117: ⚠️ Potential issue | 🟠 Major

Emit an empty result record when there are no locks.

In JSON mode this branch produces no output at all for a successful empty snapshot. That makes ably spaces locks subscribe --json indistinguishable from a stalled command for machine consumers, especially with --duration 0.

🐛 Proposed fix
-      if (locks.length === 0) {
-        if (!this.shouldOutputJson(flags)) {
-          this.log(
-            chalk.yellow("No locks are currently active in this space."),
-          );
-        }
-      } else if (this.shouldOutputJson(flags)) {
+      if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonRecord(
             "result",
             {
               locks: locks.map((lock) => ({
@@
             flags,
           ),
         );
+      } else if (locks.length === 0) {
+        this.log(
+          chalk.yellow("No locks are currently active in this space."),
+        );
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locks/subscribe.ts` around lines 95 - 117, When in JSON
mode the code currently emits nothing for an empty snapshot (locks.length ===
0), making `ably spaces locks subscribe --json` produce no output; change the
empty-branch to also emit a JSON "result" record by calling
this.formatJsonRecord("result", { locks: [], spaceName, status: "connected" },
flags) (i.e., when locks.length === 0 and this.shouldOutputJson(flags) is true),
so consumers always receive a deterministically empty result; update the block
around locks, this.shouldOutputJson(flags), and this.formatJsonRecord usages in
subscribe.ts accordingly.
src/commands/apps/set-apns-p12.ts (1)

62-66: ⚠️ Potential issue | 🔴 Critical

Guard the progress log in JSON mode.

Line 62 emits human-readable progress text before the JSON envelope on Line 79, so ably apps set-apns-p12 ... --json will write non-JSON data to stdout and break machine parsing.

Proposed fix
-      this.log(
-        formatProgress(
-          `Uploading APNS P12 certificate for app ${formatResource(args.id)}`,
-        ),
-      );
+      if (!this.shouldOutputJson(flags)) {
+        this.log(
+          formatProgress(
+            `Uploading APNS P12 certificate for app ${formatResource(args.id)}`,
+          ),
+        );
+      }

As per coding guidelines, 'Wrap all human-readable output (progress, success, listening) in if (!this.shouldOutputJson(flags)) to prevent polluting --json output'.

Also applies to: 78-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/apps/set-apns-p12.ts` around lines 62 - 66, Wrap the
human-readable progress and success logs so they are suppressed when JSON output
is requested: guard the this.log(formatProgress(...)) call that uses
formatProgress and formatResource with if (!this.shouldOutputJson(flags)) and do
the same for the subsequent human-readable logs around lines that refer to the
APNS upload success; use the command's shouldOutputJson(flags) check to ensure
only the JSON envelope is written when --json is passed.
src/commands/apps/current.ts (1)

188-205: ⚠️ Potential issue | 🟠 Major

Don't emit a successful result from the error fallback.

This branch only runs when controlApi.getApp(appId) throws, but JSON mode still emits a "result" envelope. That hides the failure from machine consumers and reports success for a degraded response. Emit an "error" envelope here, or add an explicit partial-failure signal if this fallback is intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/apps/current.ts` around lines 188 - 205, The JSON fallback
currently emits a successful "result" envelope when controlApi.getApp(appId)
fails; change this to emit an "error" envelope (or an explicit partial-failure
indicator) so machine consumers see the failure. Locate the block using
this.shouldOutputJson(flags) and this.formatJsonRecord(...) where appId and
keyId are used, and replace the "result" type with "error" (and include an
appropriate error object/message from the caught failure or a clear
partial-failure description) while preserving flags formatting.
src/commands/support/ask.ts (1)

59-64: ⚠️ Potential issue | 🟠 Major

Guard the warning/error text when --json is enabled.

The "No previous conversation found..." warning and the fallback "Failed to get a response..." message still write plain text to stdout in JSON mode. That will corrupt the NDJSON stream before/alongside the envelope record. This catch path should stay on handleCommandError(...), and any warning should either be suppressed or emitted as a JSON log/event record instead.

As per coding guidelines, "Wrap all human-readable output (progress, success, listening) in if (!this.shouldOutputJson(flags)) to prevent polluting --json output" and "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission".

Also applies to: 75-85, 139-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/support/ask.ts` around lines 59 - 64, Guard all human-readable
console output paths in ask command by wrapping direct this.log/chalk calls
(e.g., the "No previous conversation found..." message and the "Failed to get a
response..." fallback) in if (!this.shouldOutputJson(flags)) so they are
suppressed in JSON mode; in catch blocks replace raw logging with
this.handleCommandError(err, flags) to emit structured JSON errors/events
instead of plain text; ensure spinner.stop() remains but any user-facing
messages are either suppressed under JSON or emitted via handleCommandError so
NDJSON output is not corrupted.
src/commands/auth/issue-jwt-token.ts (1)

74-83: ⚠️ Potential issue | 🟠 Major

Keep validation failures on the same JSON error path.

--json now returns a "result" record here, but the bad API key / bad capability JSON branches and the catch block still exit via this.error(...). Those cases won't emit an "error" envelope, so the output format changes between success and failure.

As per coding guidelines, "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission".

Also applies to: 125-141, 155-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/auth/issue-jwt-token.ts` around lines 74 - 83, Replace direct
calls to this.error(...) in validation branches with calls to
this.handleCommandError(...) so failures emit the consistent JSON error
envelope; specifically, when keyId/keySecret are missing (the branch using
this.error("Invalid API key format...")) and when capability JSON parsing fails
(the catch that currently does this.error(`Invalid capability JSON:
${errorMessage(error)}`)), construct an Error with the same message (or wrap the
original error) and pass it to this.handleCommandError. Apply the same change to
the other validation locations noted (around the blocks handling
flags.capability and the API key checks at the ranges corresponding to lines
125-141 and 155-156) so all validation and parse failures use
this.handleCommandError instead of this.error.
src/commands/accounts/current.ts (1)

179-191: ⚠️ Potential issue | 🟠 Major

Error response should use "error" type or jsonError(), not "result".

The error handling in web CLI mode wraps the error in a "result" envelope, but according to the PR's envelope semantics, errors should use the "error" type (which includes success: false). Using "result" for error responses breaks the contract that allows consumers to reliably distinguish success from failure by checking the type field.

🐛 Proposed fix
       } catch (error) {
         // If we can't get account details, show an error message
         if (this.shouldOutputJson(flags)) {
-          this.log(
-            this.formatJsonRecord(
-              "result",
-              {
-                error: errorMessage(error),
-                mode: "web-cli",
-              },
-              flags,
-            ),
-          );
+          this.jsonError(
+            {
+              error: errorMessage(error),
+              mode: "web-cli",
+            },
+            flags,
+          );
         } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/accounts/current.ts` around lines 179 - 191, The error handling
in the catch block currently emits an envelope with type "result" via
formatJsonRecord, which violates the envelope semantics for failures; update the
catch to emit an error envelope instead (either call the jsonError() helper with
the error message and mode "web-cli", or call formatJsonRecord with type "error"
and include success: false and the error details) so that
shouldOutputJson(flags) produces a proper error response; locate the catch in
the command's execution flow where shouldOutputJson, formatJsonRecord, and
errorMessage(error) are used and replace the "result"-typed response with
jsonError(...) or an "error" envelope.
src/commands/spaces/members/subscribe.ts (1)

83-100: ⚠️ Potential issue | 🟠 Major

Emit the initial result even when the space is empty.

In JSON mode, members.length === 0 currently logs nothing, so consumers never get the readiness snapshot for an empty space. Please always emit the initial result envelope with members: [].

Suggested fix
-      if (members.length === 0) {
-        if (!this.shouldOutputJson(flags)) {
-          this.log(
-            chalk.yellow("No members are currently present in this space."),
-          );
-        }
-      } else if (this.shouldOutputJson(flags)) {
+      if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonRecord(
             "result",
             {
               members: initialMembers,
               spaceName,
               status: "connected",
             },
             flags,
           ),
         );
+      } else if (members.length === 0) {
+        this.log(
+          chalk.yellow("No members are currently present in this space."),
+        );
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/members/subscribe.ts` around lines 83 - 100, The code
currently skips emitting the initial JSON "result" when members.length === 0, so
JSON consumers never receive the readiness snapshot; update the conditional in
the subscribe flow to always call this.formatJsonRecord(...) and this.log(...)
with { members: initialMembers, spaceName, status: "connected" } when
this.shouldOutputJson(flags) is true (even inside the members.length === 0
branch) — locate the block around the members.length check and the methods
shouldOutputJson, formatJsonRecord, and variables initialMembers/spaceName and
ensure the JSON emission happens unconditionally for JSON mode while preserving
the existing non-JSON yellow log for empty spaces.
src/commands/integrations/update.ts (1)

44-48: ⚠️ Potential issue | 🟠 Major

--status is accepted but never sent to updateRule().

status is declared and shown in the examples, but updatePayload explicitly excludes it and never copies flags.status. As written, ably integrations update ... --status disabled silently leaves the rule status unchanged.

Suggested fix
-      const updatePayload: Partial<Omit<PartialRuleData, "status">> = {
+      const updatePayload: Partial<PartialRuleData> = {
+        ...(flags.status && {
+          status: flags.status as PartialRuleData["status"],
+        }),
         ...(flags["request-mode"] && { requestMode: flags["request-mode"] }),
         ...(flags.source && { source: JSON.parse(flags.source) }),
         ...(flags.target && {

Also applies to: 80-90, 105-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/integrations/update.ts` around lines 44 - 48, The flags.status
value is parsed but never forwarded to the update call; modify the code that
builds updatePayload (and the branch near where updateRule(ruleId,
updatePayload) is called) to include status when provided — e.g., if
(flags.status) updatePayload.status = flags.status — so that the status string
("enabled"/"disabled") is sent to updateRule; ensure you add this conditional
assignment in the same function that creates updatePayload and before calling
updateRule.
src/commands/spaces/cursors/get-all.ts (1)

80-91: ⚠️ Potential issue | 🟠 Major

Don't emit two successful result records for one get-all invocation.

Lines 80-91 emit a result as soon as the connection is up, and Lines 236-252 emit another result with the actual cursor payload. For a one-shot command, that makes the stream ambiguous because consumers can treat the first record as the final answer. Make the connection notice a log/event record or suppress it in JSON mode.

Also applies to: 236-252

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/cursors/get-all.ts` around lines 80 - 91, The code
currently emits a JSON "result" record when the realtime connection is
established (using this.formatJsonRecord with "result" around the
this.realtimeClient!.connection.id / spaceName payload) and then emits another
"result" later with the actual cursor payload; change the first emission so it
does not produce a final "result" in JSON mode — either emit it as a non-final
"log" or "event" JSON record (e.g. use "log" instead of "result") or skip
emitting anything when this.shouldOutputJson(flags) is true, ensuring only the
cursor payload (the later this.formatJsonRecord call) uses "result"; apply the
same adjustment to the analogous emission at the later block (lines ~236-252) so
the command emits exactly one "result" for the final payload.
src/commands/accounts/login.ts (1)

87-113: ⚠️ Potential issue | 🟠 Major

--json still falls back to interactive stdout output.

If token is omitted, this path still opens a browser and writes prompt/fallback text to stdout. That breaks the NDJSON contract and makes ably accounts login --json unsafe for machine consumption. Please either require a token in JSON mode or move every prompt/browser fallback to stderr and skip browser launch. As per coding guidelines "Wrap all human-readable output (progress, success, listening) in if (!this.shouldOutputJson(flags)) to prevent polluting --json output".

Also applies to: 401-409, 472-483

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/accounts/login.ts` around lines 87 - 113, The current
interactive fallback in accounts login leaks human-readable output and opens a
browser even when --json is set; update the logic in the token acquisition block
so that if this.shouldOutputJson(flags) is true and args.token is not provided
the command fails fast with a JSON-safe error (i.e., throw or return an error
instead of prompting), and ensure all calls that emit human-readable text or
launch browsers (this.log messages around formatProgress and the
this.openBrowser(obtainTokenPath) call and the fallback `Please visit` message)
are executed only when !this.shouldOutputJson(flags); also apply the same guards
to the similar interactive blocks referenced (the blocks around lines handling
401-409 and 472-483) and use promptForToken() only when interactive output is
allowed.
src/commands/integrations/create.ts (1)

173-175: ⚠️ Potential issue | 🟡 Minor

Missing JSON error handling in catch block.

The catch block at line 173-175 uses this.error() directly without checking shouldOutputJson(flags). This means JSON-mode users will receive a thrown error rather than a JSON error envelope, which is inconsistent with other commands in this PR.

As per coding guidelines: "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission."

🛠️ Proposed fix to add JSON error handling
     } catch (error) {
-      this.error(`Error creating integration: ${errorMessage(error)}`);
+      if (this.shouldOutputJson(flags)) {
+        this.jsonError(
+          {
+            error: `Error creating integration: ${errorMessage(error)}`,
+          },
+          flags,
+        );
+        return;
+      }
+      this.error(`Error creating integration: ${errorMessage(error)}`);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/integrations/create.ts` around lines 173 - 175, The catch block
currently calls this.error(`Error creating integration: ${errorMessage(error)}`)
directly; replace that direct throw with a call to
this.handleCommandError(error, flags) so JSON-mode consumers get a proper JSON
error envelope and non-JSON behavior remains consistent—locate the try/catch in
createIntegration command (the catch that references errorMessage(error)) and
switch to using this.handleCommandError(error, flags) (or, if needed, pass the
extracted message alongside the original error) and remove the direct this.error
invocation; also ensure shouldOutputJson(flags) is not used directly here since
handleCommandError handles JSON branching.
🧹 Nitpick comments (14)
src/commands/spaces/locks/acquire.ts (1)

143-153: Route lock-acquire failures through handleCommandError().

This is still a catch block, so emitting JSON directly here bypasses the shared failure path and makes this command an exception to the standardized error handling.

As per coding guidelines, "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locks/acquire.ts` around lines 143 - 153, The catch block
manually emits JSON and calls this.error; instead route failures through the
shared handler by replacing the manual error flow with a call to
this.handleCommandError(error, { flags, context: { lockId }, logEvent: { name:
"lock", action: "acquireFailed", message: `Failed to acquire lock:
${errorMessage(error)}` } }); keep the existing logCliEvent usage only if
handleCommandError does not already log; otherwise remove the manual
this.logCliEvent/jsonError/this.error branches so all error output and JSON
emission for the acquire lock code path go through handleCommandError.
src/commands/queues/delete.ts (1)

112-118: Use the shared command error path here too.

This catch block sidesteps the centralized handler the PR is standardizing, so delete failures can drift from the shared JSON/error/exit behavior.

As per coding guidelines, "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/queues/delete.ts` around lines 112 - 118, Replace the manual
catch logic that creates errorMsg and branches on
this.shouldOutputJson()/this.jsonError()/this.error() with a call to the
centralized handler: call this.handleCommandError(error, flags) (passing the
original error object and flags) inside the catch. Remove the errorMsg
construction and the conditional json/error paths; keep references to
errorMessage(), shouldOutputJson(), jsonError(), and error() only for locating
the block and ensure the raw error is forwarded to handleCommandError.
src/commands/spaces/locks/subscribe.ts (1)

216-223: Prefer handleCommandError() in this catch block too.

This is one of the touched error paths, but it still hand-rolls JSON vs human output instead of using the centralized helper the PR is introducing. Routing through this.handleCommandError(error, flags, "lock", { spaceName }) will keep this command aligned with the new envelope/error contract.

As per coding guidelines, "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locks/subscribe.ts` around lines 216 - 223, The catch
block currently builds an errorMsg, logs via this.logCliEvent and conditionally
calls this.jsonError/console output; replace that manual handling by invoking
the centralized helper this.handleCommandError(error, flags, "lock", { spaceName
}) so the command follows the new envelope/error contract — remove the manual
this.logCliEvent and this.jsonError/shouldOutputJson branching in this catch,
and ensure you pass the existing spaceName variable into the metadata argument.
src/commands/config/show.ts (1)

26-34: Consider using "error" type for the missing config file case.

When the config file doesn't exist, this outputs a "result" envelope containing an error field. Since this is an error condition (followed by this.exit(1)), consider using formatJsonRecord("error", ...) instead for consistency with the envelope type semantics where type: "error" indicates failure.

However, if the intent is to have a uniform "result" envelope with success/error info embedded in the payload, this is acceptable.

💡 Optional: Use "error" type for error condition
       if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonRecord(
-            "result",
+            "error",
             { error: "Config file does not exist", path: configPath },
             flags,
           ),
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/config/show.ts` around lines 26 - 34, The JSON output for the
missing config file is using formatJsonRecord("result", ...) but this is an
error path (it then calls this.exit(1)); change the envelope type to "error" by
calling formatJsonRecord("error", { error: "Config file does not exist", path:
configPath }, flags) so the output semantics match the failure; update the call
near shouldOutputJson(...) / this.exit(1) and ensure any downstream consumers
expect type: "error".
src/commands/apps/channel-rules/list.ts (1)

108-120: Consider using handleCommandError() for consistent error handling.

The catch block manually handles JSON vs non-JSON error output. As per coding guidelines, commands should use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission, which centralizes this logic.

♻️ Proposed refactor
     } catch (error) {
-      if (this.shouldOutputJson(flags)) {
-        this.jsonError(
-          {
-            appId,
-            error: errorMessage(error),
-            status: "error",
-          },
-          flags,
-        );
-      } else {
-        this.error(`Error listing channel rules: ${errorMessage(error)}`);
-      }
+      this.handleCommandError(error, flags, "channel-rules", { appId });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/apps/channel-rules/list.ts` around lines 108 - 120, The catch
block in the list command manually inspects shouldOutputJson() and calls
jsonError()/this.error() with errorMessage(), which duplicates centralized
logic; replace that manual handling by calling this.handleCommandError(error,
flags) inside the catch so JSON vs non-JSON output and formatting are handled
consistently; update the catch in src/commands/apps/channel-rules/list.ts (the
block currently using shouldOutputJson(), jsonError(), errorMessage(), and
this.error()) to invoke this.handleCommandError(...) instead.
src/commands/integrations/get.ts (1)

69-71: Consider adding JSON error handling for consistency.

The catch block uses this.error() directly, which outputs a non-JSON error message even when --json flag is used. This breaks the JSON stream for machine consumers. As per coding guidelines, use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission.

♻️ Proposed refactor
     } catch (error) {
-      this.error(`Error getting integration rule: ${errorMessage(error)}`);
+      this.handleCommandError(error, flags, "integrations", { ruleId: args.ruleId });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/integrations/get.ts` around lines 69 - 71, Replace the direct
call to this.error(...) in the catch block of the get integration command with
this.handleCommandError(...) so errors are emitted in the CLI's JSON-aware
format; specifically, in the catch of the function in
src/commands/integrations/get.ts (where it currently calls this.error(`Error
getting integration rule: ${errorMessage(error)}`)), call
this.handleCommandError(error, 'Error getting integration rule') (or at minimum
this.handleCommandError(error)) and remove the direct this.error(...) usage so
JSON consumers receive the standardized error payload.
src/commands/auth/keys/list.ts (1)

118-129: Consider using handleCommandError() for consistent error handling.

Similar to other commands in this PR, the error handling could use handleCommandError() for consistency across the codebase. This aligns with the coding guidelines recommendation for catch blocks.

As per coding guidelines: "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission."

♻️ Suggested refactor
     } catch (error) {
-      if (this.shouldOutputJson(flags)) {
-        this.jsonError(
-          {
-            appId,
-            error: errorMessage(error),
-          },
-          flags,
-        );
-      } else {
-        this.error(`Error listing keys: ${errorMessage(error)}`);
-      }
+      this.handleCommandError(error, flags, "keys", { appId });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/auth/keys/list.ts` around lines 118 - 129, Replace the manual
catch logic that calls this.jsonError/this.error and errorMessage with a single
call to the shared handler: use this.handleCommandError(error, flags, { appId })
inside the catch so the command uses the centralized error flow
(handleCommandError) and preserves the appId metadata for JSON output; remove
the conditional on this.shouldOutputJson and the direct calls to this.jsonError
and this.error in the catch block.
src/commands/integrations/delete.ts (1)

112-118: Consider using handleCommandError() for consistent error handling.

The coding guidelines recommend using this.handleCommandError() in catch blocks for consistent error handling and JSON error emission. The current implementation manually checks shouldOutputJson and calls jsonError/this.error() separately.

As per coding guidelines: "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission."

♻️ Suggested refactor
     } catch (error) {
-      const errorMsg = `Error deleting integration: ${errorMessage(error)}`;
-      if (this.shouldOutputJson(flags)) {
-        this.jsonError({ error: errorMsg }, flags);
-      } else {
-        this.error(errorMsg);
-      }
+      this.handleCommandError(error, flags, "integration", {
+        integrationId: args.integrationId,
+      });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/integrations/delete.ts` around lines 112 - 118, The catch block
currently builds errorMsg and manually branches on shouldOutputJson/ jsonError/
this.error; replace that manual handling by calling
this.handleCommandError(error, flags) so the command uses the centralized error
path; remove the errorMsg/stringification logic in the catch and pass the
original error (or the error thrown) to this.handleCommandError (preserving
errorMessage usage elsewhere if needed) to ensure consistent JSON emission and
logging.
src/commands/rooms/messages/history.ts (1)

164-170: History output should use [index] timestamp format per guidelines.

The coding guidelines specify that history commands should use ${chalk.dim([${index + 1}])} ${formatTimestamp(timestamp)} ordering format for history output.

As per coding guidelines for src/commands/**/history*.{ts,tsx}: "Use [index] timestamp ordering format: ${chalk.dim([${index + 1}])} ${formatTimestamp(timestamp)} for history output."

♻️ Suggested format change
-          for (const message of messagesInOrder) {
+          for (const [index, message] of messagesInOrder.entries()) {
             // Format message with timestamp, author and content
             const timestamp = formatMessageTimestamp(message.timestamp);
             const author = message.clientId || "Unknown";

             this.log(
-              `${formatTimestamp(timestamp)} ${chalk.blue(`${author}:`)} ${message.text}`,
+              `${chalk.dim(`[${index + 1}]`)} ${formatTimestamp(timestamp)} ${chalk.blue(`${author}:`)} ${message.text}`,
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/rooms/messages/history.ts` around lines 164 - 170, The history
output loop currently logs `${formatTimestamp(timestamp)}
${chalk.blue(`${author}:`)} ${message.text}`; change it to include the 1-based
index prefix and use the required ordering by prepending `${chalk.dim(\`[${index
+ 1}]\`)} ${formatTimestamp(timestamp)}` before the rest of the message. Update
the loop over messagesInOrder in the same block (where formatMessageTimestamp,
formatTimestamp and this.log are used) to provide an index (e.g., use a for loop
with an index or entries()) and log `${chalk.dim(\`[${index + 1}]\`)}
${formatTimestamp(timestamp)} ${chalk.blue(`${author}:`)} ${message.text}` so it
follows the `${chalk.dim(`[${index + 1}]`)} ${formatTimestamp(timestamp)}`
guideline.
src/commands/spaces/locations/get-all.ts (1)

272-285: Use the centralized error helper in both catches.

The success path now follows the shared record formatter, but these catch blocks still special-case jsonError()/this.error() locally. Routing both through this.handleCommandError() keeps error envelopes and exit handling aligned with the rest of this PR. As per coding guidelines "Use this.handleCommandError() in catch blocks for consistent error handling and JSON error emission".

Also applies to: 286-291

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locations/get-all.ts` around lines 272 - 285, The catch
blocks currently call this.jsonError(...) / this.error(...) directly and use
errorMessage(error); replace those calls with a single call to
this.handleCommandError(error, flags) so errors go through the centralized
handler (preserving JSON envelope and exit behavior). Locate the catches that
reference this.shouldOutputJson, this.jsonError, this.error and
errorMessage(error) (both occurrences around the get-all flow) and swap them to
call this.handleCommandError(error, flags) instead, removing the special-case
local JSON/error handling.
test/helpers/ndjson.ts (1)

9-15: Consider adding error handling for malformed JSON lines.

parseNdjsonLines will throw if any line contains invalid JSON, while parseLogLines silently skips invalid lines. This inconsistency could cause test failures that are hard to diagnose when stdout contains unexpected non-JSON content (e.g., warning messages, progress indicators).

♻️ Proposed fix to add resilient parsing
 export function parseNdjsonLines(stdout: string): Record<string, unknown>[] {
-  return stdout
+  const results: Record<string, unknown>[] = [];
+  for (const line of stdout
     .trim()
     .split("\n")
-    .filter(Boolean)
-    .map((line) => JSON.parse(line));
+    .filter(Boolean)) {
+    try {
+      results.push(JSON.parse(line));
+    } catch {
+      // skip non-JSON lines (warnings, progress messages, etc.)
+    }
+  }
+  return results;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/helpers/ndjson.ts` around lines 9 - 15, parseNdjsonLines currently
throws on the first malformed JSON line; change it to be resilient like
parseLogLines by attempting to JSON.parse each non-empty line inside a try/catch
and skipping (or optionally collecting) lines that fail to parse so one bad
stdout line doesn't break tests; update the implementation of parseNdjsonLines
to iterate lines, try JSON.parse(line) in a try block, push successfully parsed
objects to the result array and ignore parse errors (or optionally log them),
preserving the function signature and behavior for valid lines.
src/commands/spaces/locks/get.ts (1)

56-60: Minor semantic consideration: "Lock not found" as a result vs error.

The "Lock not found" case uses formatJsonRecord("result", { error: "Lock not found", lockId }, ...). Having an error field inside a "result" envelope may be semantically confusing to consumers—they might expect errors only in "error" type envelopes.

Consider using { lock: null, lockId } to indicate a successful lookup that found nothing, or use this.jsonError(...) if this should be treated as an error condition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locks/get.ts` around lines 56 - 60, The current "Lock not
found" response uses this.formatJsonRecord("result", { error: "Lock not found",
lockId }, ...), which mixes an error field into a "result" envelope; change it
to return a clear "no-result" payload by using this.formatJsonRecord("result", {
lock: null, lockId }, flags) to indicate a successful lookup with no lock found,
or if this should be treated as an actual error, replace the call with
this.jsonError("Lock not found", { lockId }) so errors go through the error
envelope; update the code in the block that currently calls
this.formatJsonRecord with { error: ... } accordingly.
src/commands/spaces/locations/subscribe.ts (1)

183-187: Consider removing redundant status field from error payloads.

The status: "error" field in the jsonError payload is redundant since the jsonError method already wraps the payload with a type: "error" envelope and success: false. This applies to error calls at lines 185, 269, and 296.

Suggested change
         if (this.shouldOutputJson(flags)) {
           this.jsonError(
-            { error: errorMsg, spaceName, status: "error" },
+            { error: errorMsg, spaceName },
             flags,
           );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locations/subscribe.ts` around lines 183 - 187, Remove
the redundant status property from the error payloads that are passed to
this.jsonError: locate the calls where this.shouldOutputJson(flags) is true and
this.jsonError(...) is invoked (the call sites referencing spaceName and
errorMsg in subscribe command), and drop the explicit status: "error" key from
the payload objects so the envelope produced by this.jsonError (type: "error" /
success: false) is relied upon instead; specifically update the jsonError calls
in the subscribe handler (the invocations next to this.shouldOutputJson and any
other jsonError usages in the same function) to send only the error details
(e.g., { error: errorMsg, spaceName }) without the status field.
src/base-command.ts (1)

1004-1013: Minor: data spread could override base envelope fields.

The ...data spread at the end of logEntry allows caller-provided fields to override the base fields (component, event, message, timestamp). If this is intentional to allow flexibility, consider adding a brief comment. If not, spread data first to let base fields take precedence.

💡 Option to prevent override (if unintentional)
      const logEntry = {
+       ...data,
        component,
        event,
        message,
        timestamp: new Date().toISOString(),
-       ...data,
      };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/base-command.ts` around lines 1004 - 1013, The current JSON envelope
creation (in the isJsonMode branch building logEntry used by
this.formatJsonRecord and this.log) spreads ...data last which lets
caller-provided fields override base fields (component, event, message,
timestamp); change the object construction to spread ...data first (e.g., {
...data, component, event, message, timestamp }) so the base envelope fields
take precedence, or if overriding is intentional add a brief comment above the
logEntry construction documenting that behavior; update references around
logEntry, isJsonMode, formatJsonRecord, and flags accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 01e55c6c-ddba-4ef0-91ed-d732f61c4251

📥 Commits

Reviewing files that changed from the base of the PR and between 831b59c and 020a262.

📒 Files selected for processing (115)
  • AGENTS.md
  • docs/Project-Structure.md
  • src/base-command.ts
  • src/commands/accounts/current.ts
  • src/commands/accounts/list.ts
  • src/commands/accounts/login.ts
  • src/commands/accounts/logout.ts
  • src/commands/accounts/switch.ts
  • src/commands/apps/channel-rules/create.ts
  • src/commands/apps/channel-rules/delete.ts
  • src/commands/apps/channel-rules/list.ts
  • src/commands/apps/channel-rules/update.ts
  • src/commands/apps/create.ts
  • src/commands/apps/current.ts
  • src/commands/apps/delete.ts
  • src/commands/apps/list.ts
  • src/commands/apps/set-apns-p12.ts
  • src/commands/apps/update.ts
  • src/commands/auth/issue-ably-token.ts
  • src/commands/auth/issue-jwt-token.ts
  • src/commands/auth/keys/create.ts
  • src/commands/auth/keys/current.ts
  • src/commands/auth/keys/get.ts
  • src/commands/auth/keys/list.ts
  • src/commands/auth/keys/revoke.ts
  • src/commands/auth/revoke-token.ts
  • src/commands/bench/publisher.ts
  • src/commands/bench/subscriber.ts
  • src/commands/channels/batch-publish.ts
  • src/commands/channels/history.ts
  • src/commands/channels/list.ts
  • src/commands/channels/occupancy/get.ts
  • src/commands/channels/occupancy/subscribe.ts
  • src/commands/channels/presence/enter.ts
  • src/commands/channels/presence/subscribe.ts
  • src/commands/channels/publish.ts
  • src/commands/channels/subscribe.ts
  • src/commands/config/path.ts
  • src/commands/config/show.ts
  • src/commands/connections/test.ts
  • src/commands/integrations/create.ts
  • src/commands/integrations/delete.ts
  • src/commands/integrations/get.ts
  • src/commands/integrations/list.ts
  • src/commands/integrations/update.ts
  • src/commands/logs/channel-lifecycle/subscribe.ts
  • src/commands/logs/connection-lifecycle/history.ts
  • src/commands/logs/connection-lifecycle/subscribe.ts
  • src/commands/logs/history.ts
  • src/commands/logs/push/history.ts
  • src/commands/logs/push/subscribe.ts
  • src/commands/logs/subscribe.ts
  • src/commands/queues/create.ts
  • src/commands/queues/delete.ts
  • src/commands/queues/list.ts
  • src/commands/rooms/list.ts
  • src/commands/rooms/messages/history.ts
  • src/commands/rooms/messages/reactions/remove.ts
  • src/commands/rooms/messages/reactions/send.ts
  • src/commands/rooms/messages/reactions/subscribe.ts
  • src/commands/rooms/messages/send.ts
  • src/commands/rooms/messages/subscribe.ts
  • src/commands/rooms/occupancy/get.ts
  • src/commands/rooms/occupancy/subscribe.ts
  • src/commands/rooms/presence/enter.ts
  • src/commands/rooms/presence/subscribe.ts
  • src/commands/rooms/reactions/send.ts
  • src/commands/rooms/reactions/subscribe.ts
  • src/commands/rooms/typing/subscribe.ts
  • src/commands/spaces/cursors/get-all.ts
  • src/commands/spaces/cursors/set.ts
  • src/commands/spaces/cursors/subscribe.ts
  • src/commands/spaces/list.ts
  • src/commands/spaces/locations/get-all.ts
  • src/commands/spaces/locations/set.ts
  • src/commands/spaces/locations/subscribe.ts
  • src/commands/spaces/locks/acquire.ts
  • src/commands/spaces/locks/get-all.ts
  • src/commands/spaces/locks/get.ts
  • src/commands/spaces/locks/subscribe.ts
  • src/commands/spaces/members/enter.ts
  • src/commands/spaces/members/subscribe.ts
  • src/commands/support/ask.ts
  • src/services/stats-display.ts
  • src/stats-base-command.ts
  • src/utils/output.ts
  • test/helpers/ndjson.ts
  • test/unit/base/auth-info-display.test.ts
  • test/unit/base/base-command-enhanced.test.ts
  • test/unit/commands/auth/issue-jwt-token.test.ts
  • test/unit/commands/channels/occupancy/subscribe.test.ts
  • test/unit/commands/channels/subscribe.test.ts
  • test/unit/commands/logs/channel-lifecycle/subscribe.test.ts
  • test/unit/commands/logs/connection-lifecycle/subscribe.test.ts
  • test/unit/commands/logs/push/subscribe.test.ts
  • test/unit/commands/logs/subscribe.test.ts
  • test/unit/commands/queues/list.test.ts
  • test/unit/commands/rooms/messages.test.ts
  • test/unit/commands/rooms/messages/reactions/subscribe.test.ts
  • test/unit/commands/rooms/occupancy/subscribe.test.ts
  • test/unit/commands/rooms/presence/enter.test.ts
  • test/unit/commands/rooms/presence/subscribe.test.ts
  • test/unit/commands/rooms/reactions/subscribe.test.ts
  • test/unit/commands/rooms/typing/subscribe.test.ts
  • test/unit/commands/spaces/cursors/get-all.test.ts
  • test/unit/commands/spaces/cursors/set.test.ts
  • test/unit/commands/spaces/cursors/subscribe.test.ts
  • test/unit/commands/spaces/locations/get-all.test.ts
  • test/unit/commands/spaces/locations/subscribe.test.ts
  • test/unit/commands/spaces/locks/get-all.test.ts
  • test/unit/commands/spaces/locks/get.test.ts
  • test/unit/commands/spaces/locks/subscribe.test.ts
  • test/unit/helpers/ndjson.test.ts
  • test/unit/services/stats-display.test.ts
  • test/unit/utils/output.test.ts

Copy link
Contributor

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking PR changes against data classes/properties/fields in node-modules/@ably to make sure it;s usage across project is consistent. Shared review document for the same 👍

@umair-ably umair-ably force-pushed the dx-793-json-envelope branch from e72157f to 36f4f4a Compare March 11, 2026 00:53
@umair-ably umair-ably changed the base branch from new-command-skill to main March 11, 2026 00:54
@umair-ably umair-ably changed the title [DX-793] Fix JSON output [DX-793] Standardize JSON output with envelope format, fail() error handling, and format helpers Mar 11, 2026
@umair-ably umair-ably force-pushed the dx-793-json-envelope branch from 2a96237 to e222ee6 Compare March 11, 2026 01:29
@umair-ably umair-ably force-pushed the dx-793-json-envelope branch from e222ee6 to b1e4f35 Compare March 11, 2026 01:32
…dd --json examples

Migrate 14 placeholder/index files from extending Command to
BaseTopicCommand for consistency with other topic commands.
Add missing --json examples to 5 leaf commands.
@umair-ably umair-ably force-pushed the dx-793-json-envelope branch from b569d4f to bd92ea3 Compare March 11, 2026 02:29
@umair-ably umair-ably marked this pull request as ready for review March 11, 2026 02:41
@umair-ably umair-ably requested review from AndyTWF and sacOO7 March 11, 2026 02:41
@umair-ably umair-ably mentioned this pull request Mar 11, 2026
@umair-ably umair-ably changed the title [DX-793] Standardize JSON output with envelope format, fail() error handling, and format helpers [DX-793] [DX-938] Standardize JSON output with envelope format, fail() error handling, and format helpers Mar 11, 2026
Copy link
Contributor

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR looks good overall, almost all of the review comments are well addressed 👍
Found very few actionable items =>

  1. Missing eventType on reaction summary events
  2. Spaces Lock subscribe vs get field inconsistency
    Will share review doc for the same 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants