From da8af0dd588847307d319e659e32029b4e20f4a1 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 26 Mar 2026 14:51:03 +0530 Subject: [PATCH 1/2] Implemented json output for subscribe commands/sub-commands --- src/base-command.ts | 3 ++- src/chat-base-command.ts | 22 +++++++++++++++++++ .../channels/annotations/subscribe.ts | 11 ++++++++++ src/commands/channels/occupancy/subscribe.ts | 11 ++++++++++ src/commands/channels/presence/enter.ts | 3 ++- src/commands/channels/presence/subscribe.ts | 11 ++++++++++ src/commands/channels/presence/update.ts | 3 ++- src/commands/channels/subscribe.ts | 11 ++++++++++ .../logs/channel-lifecycle/subscribe.ts | 11 ++++++++++ .../logs/connection-lifecycle/subscribe.ts | 11 ++++++++++ src/commands/logs/push/subscribe.ts | 11 ++++++++++ src/commands/logs/subscribe.ts | 11 ++++++++++ .../rooms/messages/reactions/subscribe.ts | 1 + src/commands/rooms/messages/subscribe.ts | 1 + src/commands/rooms/occupancy/subscribe.ts | 1 + src/commands/rooms/presence/subscribe.ts | 17 +++----------- src/commands/rooms/reactions/subscribe.ts | 1 + src/commands/rooms/typing/subscribe.ts | 1 + src/commands/spaces/cursors/set.ts | 3 ++- src/commands/spaces/cursors/subscribe.ts | 11 ++++++++++ src/commands/spaces/locations/set.ts | 3 ++- src/commands/spaces/locations/subscribe.ts | 11 ++++++++++ src/commands/spaces/locks/acquire.ts | 3 ++- src/commands/spaces/locks/get-all.ts | 2 +- src/commands/spaces/locks/get.ts | 2 +- src/commands/spaces/locks/subscribe.ts | 11 ++++++++++ src/commands/spaces/members/enter.ts | 3 ++- src/commands/spaces/members/subscribe.ts | 11 ++++++++++ src/commands/spaces/occupancy/subscribe.ts | 11 ++++++++++ src/commands/spaces/subscribe.ts | 11 ++++++++++ src/utils/json-status.ts | 15 +++++++++++++ .../commands/spaces/members/subscribe.test.ts | 8 ++++++- test/unit/commands/spaces/subscribe.test.ts | 8 ++++++- 33 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 src/utils/json-status.ts diff --git a/src/base-command.ts b/src/base-command.ts index 52bacace..4c7cb5c0 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -19,6 +19,7 @@ import { buildJsonRecord, formatWarning, } from "./utils/output.js"; +import { JsonStatusType } from "./utils/json-status.js"; import { getCliVersion } from "./utils/version.js"; import Spaces from "@ably/spaces"; import { ChatClient } from "@ably/chat"; @@ -1621,7 +1622,7 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand { if (exitReason === "timeout" && !isTestMode()) { const message = "Duration elapsed – command finished cleanly."; if (this.shouldOutputJson(flags)) { - this.logJsonStatus("complete", message, flags); + this.logJsonStatus(JsonStatusType.Complete, message, flags); } else { this.log(message); } diff --git a/src/chat-base-command.ts b/src/chat-base-command.ts index 0fb13404..c7a26126 100644 --- a/src/chat-base-command.ts +++ b/src/chat-base-command.ts @@ -5,6 +5,7 @@ import { AblyBaseCommand } from "./base-command.js"; import { productApiFlags } from "./flags.js"; import { BaseFlags } from "./types/cli.js"; +import { JsonStatusType } from "./utils/json-status.js"; import { formatSuccess, formatListening, @@ -119,8 +120,17 @@ export abstract class ChatBaseCommand extends AblyBaseCommand { roomName: string; successMessage?: string; listeningMessage?: string; + subscribingMessage?: string; }, ): void { + if (options.subscribingMessage) { + this.logJsonStatus( + JsonStatusType.Subscribing, + options.subscribingMessage, + flags as BaseFlags, + ); + } + room.onStatusChange((statusChange) => { let reason: Error | null | string | undefined; if (statusChange.current === RoomStatus.Failed) { @@ -144,12 +154,24 @@ export abstract class ChatBaseCommand extends AblyBaseCommand { this.log(formatListening(options.listeningMessage)); } } + if (options.listeningMessage) { + this.logJsonStatus( + JsonStatusType.Listening, + options.listeningMessage, + flags as BaseFlags, + ); + } break; } case RoomStatus.Detached: { if (!this.shouldOutputJson(flags)) { this.log(formatWarning("Disconnected from Ably")); } + this.logJsonStatus( + JsonStatusType.Disconnected, + "Disconnected from Ably.", + flags as BaseFlags, + ); break; } case RoomStatus.Failed: { diff --git a/src/commands/channels/annotations/subscribe.ts b/src/commands/channels/annotations/subscribe.ts index e58457a4..40826e09 100644 --- a/src/commands/channels/annotations/subscribe.ts +++ b/src/commands/channels/annotations/subscribe.ts @@ -8,6 +8,7 @@ import { productApiFlags, rewindFlag, } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatAnnotationsOutput, formatListening, @@ -91,6 +92,11 @@ export default class ChannelsAnnotationsSubscribe extends AblyBaseCommand { ), ); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to annotations on channel: ${channelName}.`, + flags, + ); this.setupChannelStateLogging(channel, flags, { includeUserFriendlyMessages: true, @@ -172,6 +178,11 @@ export default class ChannelsAnnotationsSubscribe extends AblyBaseCommand { this.log(formatListening("Listening for annotations.")); this.log(""); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for annotations.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/channels/occupancy/subscribe.ts b/src/commands/channels/occupancy/subscribe.ts index 7e393e16..5bb5e9d7 100644 --- a/src/commands/channels/occupancy/subscribe.ts +++ b/src/commands/channels/occupancy/subscribe.ts @@ -2,6 +2,7 @@ import { Args } from "@oclif/core"; import * as Ably from "ably"; import { AblyBaseCommand } from "../../../base-command.js"; import { clientIdFlag, durationFlag, productApiFlags } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatListening, formatProgress, @@ -85,6 +86,11 @@ export default class ChannelsOccupancySubscribe extends AblyBaseCommand { ), ); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to occupancy events on channel: ${channelName}.`, + flags, + ); await channel.subscribe(occupancyEventName, (message: Ably.Message) => { const timestamp = formatMessageTimestamp(message.timestamp); @@ -127,6 +133,11 @@ export default class ChannelsOccupancySubscribe extends AblyBaseCommand { ); this.log(formatListening("Listening for occupancy events.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for occupancy events.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/channels/presence/enter.ts b/src/commands/channels/presence/enter.ts index 85372a70..4f8e1c21 100644 --- a/src/commands/channels/presence/enter.ts +++ b/src/commands/channels/presence/enter.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import { AblyBaseCommand } from "../../../base-command.js"; import { clientIdFlag, durationFlag, productApiFlags } from "../../../flags.js"; import { isJsonData } from "../../../utils/json-formatter.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatClientId, formatEventType, @@ -207,7 +208,7 @@ export default class ChannelsPresenceEnter extends AblyBaseCommand { } this.logJsonStatus( - "holding", + JsonStatusType.Holding, "Holding presence. Press Ctrl+C to exit.", flags, ); diff --git a/src/commands/channels/presence/subscribe.ts b/src/commands/channels/presence/subscribe.ts index 91c408dd..14c0beca 100644 --- a/src/commands/channels/presence/subscribe.ts +++ b/src/commands/channels/presence/subscribe.ts @@ -2,6 +2,7 @@ import { Args } from "@oclif/core"; import * as Ably from "ably"; import { AblyBaseCommand } from "../../../base-command.js"; import { clientIdFlag, durationFlag, productApiFlags } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatListening, formatProgress, @@ -78,6 +79,11 @@ export default class ChannelsPresenceSubscribe extends AblyBaseCommand { ), ); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to presence events on channel: ${channelName}.`, + flags, + ); await channel.presence.subscribe( (presenceMessage: Ably.PresenceMessage) => { @@ -126,6 +132,11 @@ export default class ChannelsPresenceSubscribe extends AblyBaseCommand { this.log(formatListening("Listening for presence events.")); this.log(""); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for presence events.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/channels/presence/update.ts b/src/commands/channels/presence/update.ts index fe188067..0afe7ced 100644 --- a/src/commands/channels/presence/update.ts +++ b/src/commands/channels/presence/update.ts @@ -2,6 +2,7 @@ import { Args, Flags } from "@oclif/core"; import * as Ably from "ably"; import { AblyBaseCommand } from "../../../base-command.js"; import { clientIdFlag, durationFlag, productApiFlags } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatClientId, formatLabel, @@ -133,7 +134,7 @@ export default class ChannelsPresenceUpdate extends AblyBaseCommand { } this.logJsonStatus( - "holding", + JsonStatusType.Holding, "Holding presence. Press Ctrl+C to exit.", flags, ); diff --git a/src/commands/channels/subscribe.ts b/src/commands/channels/subscribe.ts index 7990b87a..9ee88129 100644 --- a/src/commands/channels/subscribe.ts +++ b/src/commands/channels/subscribe.ts @@ -7,6 +7,7 @@ import { productApiFlags, rewindFlag, } from "../../flags.js"; +import { JsonStatusType } from "../../utils/json-status.js"; import { formatListening, formatProgress, @@ -171,6 +172,11 @@ export default class ChannelsSubscribe extends AblyBaseCommand { ), ); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to channel: ${channel.name}.`, + flags, + ); // Set up channel state logging this.setupChannelStateLogging(channel, flags, { @@ -268,6 +274,11 @@ export default class ChannelsSubscribe extends AblyBaseCommand { this.log(formatListening("Listening for messages.")); this.log(""); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for messages.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/logs/channel-lifecycle/subscribe.ts b/src/commands/logs/channel-lifecycle/subscribe.ts index eba343e0..9e2e90b7 100644 --- a/src/commands/logs/channel-lifecycle/subscribe.ts +++ b/src/commands/logs/channel-lifecycle/subscribe.ts @@ -9,6 +9,7 @@ import { rewindFlag, } from "../../../flags.js"; import { formatMessageData } from "../../../utils/json-formatter.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatLabel, formatListening, @@ -79,6 +80,11 @@ export default class LogsChannelLifecycleSubscribe extends AblyBaseCommand { "subscribing", `Subscribing to ${channelName}...`, ); + this.logJsonStatus( + JsonStatusType.Subscribing, + "Subscribing to channel lifecycle logs.", + flags, + ); // Subscribe to the channel await channel.subscribe((message) => { const timestamp = formatMessageTimestamp(message.timestamp); @@ -136,6 +142,11 @@ export default class LogsChannelLifecycleSubscribe extends AblyBaseCommand { this.log(formatListening("Listening for channel lifecycle logs.")); this.log(""); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for channel lifecycle logs.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/logs/connection-lifecycle/subscribe.ts b/src/commands/logs/connection-lifecycle/subscribe.ts index 47770f9c..f4962389 100644 --- a/src/commands/logs/connection-lifecycle/subscribe.ts +++ b/src/commands/logs/connection-lifecycle/subscribe.ts @@ -7,6 +7,7 @@ import { productApiFlags, rewindFlag, } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatEventType, formatListening, @@ -79,6 +80,11 @@ export default class LogsConnectionLifecycleSubscribe extends AblyBaseCommand { `Subscribing to connection lifecycle logs`, { channel: logsChannelName }, ); + this.logJsonStatus( + JsonStatusType.Subscribing, + "Subscribing to connection lifecycle logs.", + flags, + ); // Subscribe to connection lifecycle logs await channel.subscribe((message: Ably.Message) => { @@ -129,6 +135,11 @@ export default class LogsConnectionLifecycleSubscribe extends AblyBaseCommand { formatListening("Listening for connection lifecycle log events."), ); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for connection lifecycle log events.", + flags, + ); // Wait until the user interrupts or the optional duration elapses await this.waitAndTrackCleanup(flags, "logs", flags.duration); diff --git a/src/commands/logs/push/subscribe.ts b/src/commands/logs/push/subscribe.ts index b7eb86ce..77ad904d 100644 --- a/src/commands/logs/push/subscribe.ts +++ b/src/commands/logs/push/subscribe.ts @@ -9,6 +9,7 @@ import { rewindFlag, } from "../../../flags.js"; import { formatMessageData } from "../../../utils/json-formatter.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatListening, formatResource, @@ -77,6 +78,11 @@ export default class LogsPushSubscribe extends AblyBaseCommand { "subscribing", `Subscribing to ${channelName}...`, ); + this.logJsonStatus( + JsonStatusType.Subscribing, + "Subscribing to push logs.", + flags, + ); // Subscribe to the channel await channel.subscribe((message) => { @@ -158,6 +164,11 @@ export default class LogsPushSubscribe extends AblyBaseCommand { this.log(formatListening("Listening for push logs.")); this.log(""); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for push logs.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/logs/subscribe.ts b/src/commands/logs/subscribe.ts index 7cd74560..f66f7072 100644 --- a/src/commands/logs/subscribe.ts +++ b/src/commands/logs/subscribe.ts @@ -8,6 +8,7 @@ import { productApiFlags, rewindFlag, } from "../../flags.js"; +import { JsonStatusType } from "../../utils/json-status.js"; import { formatEventType, formatListening, @@ -110,6 +111,11 @@ export default class LogsSubscribe extends AblyBaseCommand { `Subscribing to log events: ${logTypes.join(", ")}`, { logTypes, channel: logsChannelName }, ); + this.logJsonStatus( + JsonStatusType.Subscribing, + "Subscribing to app logs.", + flags, + ); // Subscribe to specified log types const subscribePromises: Promise[] = []; @@ -169,6 +175,11 @@ export default class LogsSubscribe extends AblyBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatListening("Listening for log events.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for log events.", + flags, + ); // Wait until the user interrupts or the optional duration elapses await this.waitAndTrackCleanup(flags, "logs", flags.duration); diff --git a/src/commands/rooms/messages/reactions/subscribe.ts b/src/commands/rooms/messages/reactions/subscribe.ts index c487f341..be479de8 100644 --- a/src/commands/rooms/messages/reactions/subscribe.ts +++ b/src/commands/rooms/messages/reactions/subscribe.ts @@ -114,6 +114,7 @@ export default class MessagesReactionsSubscribe extends ChatBaseCommand { roomName: room, successMessage: "Connected to Ably.", listeningMessage: `Listening for message reactions in room ${formatResource(room)}.`, + subscribingMessage: `Subscribing to message reactions in room: ${room}.`, }); // Attach to the room diff --git a/src/commands/rooms/messages/subscribe.ts b/src/commands/rooms/messages/subscribe.ts index ef6d6eaf..c84c598c 100644 --- a/src/commands/rooms/messages/subscribe.ts +++ b/src/commands/rooms/messages/subscribe.ts @@ -169,6 +169,7 @@ export default class MessagesSubscribe extends ChatBaseCommand { roomName, successMessage: `Subscribed to room: ${formatResource(roomName)}.`, listeningMessage: "Listening for messages.", + subscribingMessage: `Subscribing to room: ${roomName}.`, }); // Attach to the room diff --git a/src/commands/rooms/occupancy/subscribe.ts b/src/commands/rooms/occupancy/subscribe.ts index 3cbc77e4..7063f9c9 100644 --- a/src/commands/rooms/occupancy/subscribe.ts +++ b/src/commands/rooms/occupancy/subscribe.ts @@ -95,6 +95,7 @@ export default class RoomsOccupancySubscribe extends ChatBaseCommand { roomName: this.roomName!, successMessage: `Subscribed to occupancy in room: ${formatResource(this.roomName!)}.`, listeningMessage: "Listening for occupancy updates.", + subscribingMessage: `Subscribing to occupancy in room: ${this.roomName!}.`, }); // Attach to the room diff --git a/src/commands/rooms/presence/subscribe.ts b/src/commands/rooms/presence/subscribe.ts index 067dae8e..bfb84e6b 100644 --- a/src/commands/rooms/presence/subscribe.ts +++ b/src/commands/rooms/presence/subscribe.ts @@ -8,11 +8,9 @@ import { formatClientId, formatEventType, formatLabel, - formatListening, formatMessageTimestamp, formatProgress, formatResource, - formatSuccess, formatTimestamp, } from "../../../utils/output.js"; @@ -75,8 +73,9 @@ export default class RoomsPresenceSubscribe extends ChatBaseCommand { this.setupRoomStatusHandler(currentRoom, flags, { roomName: this.roomName!, - successMessage: `Connected to room: ${formatResource(this.roomName!)}.`, - listeningMessage: undefined, + successMessage: `Subscribed to presence in room: ${formatResource(this.roomName!)}.`, + listeningMessage: "Listening for presence events.", + subscribingMessage: `Subscribing to presence in room: ${this.roomName!}.`, }); await currentRoom.attach(); @@ -150,16 +149,6 @@ export default class RoomsPresenceSubscribe extends ChatBaseCommand { "Listening for presence events. Press Ctrl+C to exit.", ); - if (!this.shouldOutputJson(flags)) { - this.log( - formatSuccess( - `Subscribed to presence in room: ${formatResource(this.roomName!)}.`, - ), - ); - this.log(formatListening("Listening for presence events.")); - this.log(""); - } - // Wait until the user interrupts or the optional duration elapses await this.waitAndTrackCleanup(flags, "presence", flags.duration); } catch (error) { diff --git a/src/commands/rooms/reactions/subscribe.ts b/src/commands/rooms/reactions/subscribe.ts index 97f5d709..7d2cd314 100644 --- a/src/commands/rooms/reactions/subscribe.ts +++ b/src/commands/rooms/reactions/subscribe.ts @@ -92,6 +92,7 @@ export default class RoomsReactionsSubscribe extends ChatBaseCommand { roomName, successMessage: `Subscribed to reactions in room: ${formatResource(roomName)}.`, listeningMessage: "Listening for reactions.", + subscribingMessage: `Subscribing to reactions in room: ${roomName}.`, }); // Attach to the room diff --git a/src/commands/rooms/typing/subscribe.ts b/src/commands/rooms/typing/subscribe.ts index 9d9a66c1..d94d92d2 100644 --- a/src/commands/rooms/typing/subscribe.ts +++ b/src/commands/rooms/typing/subscribe.ts @@ -73,6 +73,7 @@ export default class TypingSubscribe extends ChatBaseCommand { roomName, successMessage: `Subscribed to typing in room: ${formatResource(roomName)}.`, listeningMessage: "Listening for typing indicators.", + subscribingMessage: `Subscribing to typing in room: ${roomName}.`, }); // Set up typing indicators diff --git a/src/commands/spaces/cursors/set.ts b/src/commands/spaces/cursors/set.ts index 54855a6c..9ad6dcc3 100644 --- a/src/commands/spaces/cursors/set.ts +++ b/src/commands/spaces/cursors/set.ts @@ -2,6 +2,7 @@ import type { CursorData, CursorPosition } from "@ably/spaces"; import { Args, Flags } from "@oclif/core"; import { errorMessage } from "../../../utils/errors.js"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatListening, @@ -263,7 +264,7 @@ export default class SpacesCursorsSet extends SpacesBaseCommand { } this.logJsonStatus( - "holding", + JsonStatusType.Holding, "Holding cursor. Press Ctrl+C to exit.", flags, ); diff --git a/src/commands/spaces/cursors/subscribe.ts b/src/commands/spaces/cursors/subscribe.ts index 83b9d4ad..1aef957f 100644 --- a/src/commands/spaces/cursors/subscribe.ts +++ b/src/commands/spaces/cursors/subscribe.ts @@ -1,6 +1,7 @@ import { type CursorUpdate } from "@ably/spaces"; import { Args } from "@oclif/core"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatListening, @@ -45,6 +46,11 @@ export default class SpacesCursorsSubscribe extends SpacesBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatProgress("Subscribing to cursor updates")); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to cursor updates in space: ${spaceName}.`, + flags, + ); await this.initializeSpace(flags, spaceName, { enterSpace: false }); @@ -106,6 +112,11 @@ export default class SpacesCursorsSubscribe extends SpacesBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatListening("Listening for cursor movements.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for cursor movements.", + flags, + ); await this.waitAndTrackCleanup(flags, "cursor", flags.duration); } catch (error) { diff --git a/src/commands/spaces/locations/set.ts b/src/commands/spaces/locations/set.ts index 6a2ae8c8..424cada3 100644 --- a/src/commands/spaces/locations/set.ts +++ b/src/commands/spaces/locations/set.ts @@ -1,6 +1,7 @@ import { Args, Flags } from "@oclif/core"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatSuccess, @@ -77,7 +78,7 @@ export default class SpacesLocationsSet extends SpacesBaseCommand { } this.logJsonStatus( - "holding", + JsonStatusType.Holding, "Holding location. Press Ctrl+C to exit.", flags, ); diff --git a/src/commands/spaces/locations/subscribe.ts b/src/commands/spaces/locations/subscribe.ts index 608cd82b..3eeecdea 100644 --- a/src/commands/spaces/locations/subscribe.ts +++ b/src/commands/spaces/locations/subscribe.ts @@ -3,6 +3,7 @@ import { Args } from "@oclif/core"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatListening, formatProgress, @@ -42,12 +43,22 @@ export default class SpacesLocationsSubscribe extends SpacesBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatProgress("Subscribing to location updates")); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to location updates in space: ${spaceName}.`, + flags, + ); await this.initializeSpace(flags, spaceName, { enterSpace: false }); if (!this.shouldOutputJson(flags)) { this.log(formatListening("Listening for location updates.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for location updates.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/spaces/locks/acquire.ts b/src/commands/spaces/locks/acquire.ts index 02e89663..e3a9cf03 100644 --- a/src/commands/spaces/locks/acquire.ts +++ b/src/commands/spaces/locks/acquire.ts @@ -3,6 +3,7 @@ import { Args, Flags } from "@oclif/core"; import { errorMessage } from "../../../utils/errors.js"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatSuccess, @@ -135,7 +136,7 @@ export default class SpacesLocksAcquire extends SpacesBaseCommand { } this.logJsonStatus( - "holding", + JsonStatusType.Holding, "Holding lock. Press Ctrl+C to exit.", flags, ); diff --git a/src/commands/spaces/locks/get-all.ts b/src/commands/spaces/locks/get-all.ts index 06b7ce10..0f60de3c 100644 --- a/src/commands/spaces/locks/get-all.ts +++ b/src/commands/spaces/locks/get-all.ts @@ -43,7 +43,7 @@ export default class SpacesLocksGetAll extends SpacesBaseCommand { try { await this.initializeSpace(flags, spaceName, { - enterSpace: false, + enterSpace: true, setupConnectionLogging: false, }); diff --git a/src/commands/spaces/locks/get.ts b/src/commands/spaces/locks/get.ts index 23b357ad..8bca0f39 100644 --- a/src/commands/spaces/locks/get.ts +++ b/src/commands/spaces/locks/get.ts @@ -40,7 +40,7 @@ export default class SpacesLocksGet extends SpacesBaseCommand { try { await this.initializeSpace(flags, spaceName, { - enterSpace: false, + enterSpace: true, setupConnectionLogging: false, }); diff --git a/src/commands/spaces/locks/subscribe.ts b/src/commands/spaces/locks/subscribe.ts index 9f36bde8..933fc1d0 100644 --- a/src/commands/spaces/locks/subscribe.ts +++ b/src/commands/spaces/locks/subscribe.ts @@ -3,6 +3,7 @@ import { Args } from "@oclif/core"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatListening, formatMessageTimestamp, @@ -47,6 +48,11 @@ export default class SpacesLocksSubscribe extends SpacesBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatProgress("Subscribing to lock events")); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to lock events in space: ${spaceName}.`, + flags, + ); await this.initializeSpace(flags, spaceName, { enterSpace: false }); @@ -84,6 +90,11 @@ export default class SpacesLocksSubscribe extends SpacesBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatListening("Listening for lock events.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for lock events.", + flags, + ); await this.waitAndTrackCleanup(flags, "lock", flags.duration); } catch (error) { diff --git a/src/commands/spaces/members/enter.ts b/src/commands/spaces/members/enter.ts index 04bd383f..b6a348d6 100644 --- a/src/commands/spaces/members/enter.ts +++ b/src/commands/spaces/members/enter.ts @@ -2,6 +2,7 @@ import type { ProfileData } from "@ably/spaces"; import { Args, Flags } from "@oclif/core"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; import { formatSuccess, @@ -91,7 +92,7 @@ export default class SpacesMembersEnter extends SpacesBaseCommand { } this.logJsonStatus( - "holding", + JsonStatusType.Holding, "Holding presence. Press Ctrl+C to exit.", flags, ); diff --git a/src/commands/spaces/members/subscribe.ts b/src/commands/spaces/members/subscribe.ts index 67a786a9..81e36c0d 100644 --- a/src/commands/spaces/members/subscribe.ts +++ b/src/commands/spaces/members/subscribe.ts @@ -3,6 +3,7 @@ import { Args } from "@oclif/core"; import { productApiFlags, clientIdFlag, durationFlag } from "../../../flags.js"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatListening, formatMessageTimestamp, @@ -55,12 +56,22 @@ export default class SpacesMembersSubscribe extends SpacesBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatProgress("Subscribing to member updates")); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to member updates in space: ${spaceName}.`, + flags, + ); await this.initializeSpace(flags, spaceName, { enterSpace: false }); if (!this.shouldOutputJson(flags)) { this.log(formatListening("Listening for member events.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for member events.", + flags, + ); // Subscribe to member presence events this.logCliEvent( diff --git a/src/commands/spaces/occupancy/subscribe.ts b/src/commands/spaces/occupancy/subscribe.ts index f8de63b3..26ce8338 100644 --- a/src/commands/spaces/occupancy/subscribe.ts +++ b/src/commands/spaces/occupancy/subscribe.ts @@ -3,6 +3,7 @@ import * as Ably from "ably"; import { AblyBaseCommand } from "../../../base-command.js"; import { clientIdFlag, durationFlag, productApiFlags } from "../../../flags.js"; +import { JsonStatusType } from "../../../utils/json-status.js"; import { formatEventType, formatLabel, @@ -80,6 +81,11 @@ export default class SpacesOccupancySubscribe extends AblyBaseCommand { ), ); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to occupancy events on space: ${spaceName}.`, + flags, + ); await channel.subscribe(occupancyEventName, (message: Ably.Message) => { const timestamp = formatMessageTimestamp(message.timestamp); @@ -139,6 +145,11 @@ export default class SpacesOccupancySubscribe extends AblyBaseCommand { ); this.log(formatListening("Listening for occupancy events.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for occupancy events.", + flags, + ); this.logCliEvent( flags, diff --git a/src/commands/spaces/subscribe.ts b/src/commands/spaces/subscribe.ts index 32f471fa..c41e8606 100644 --- a/src/commands/spaces/subscribe.ts +++ b/src/commands/spaces/subscribe.ts @@ -3,6 +3,7 @@ import { Args } from "@oclif/core"; import { productApiFlags, clientIdFlag, durationFlag } from "../../flags.js"; import { SpacesBaseCommand } from "../../spaces-base-command.js"; +import { JsonStatusType } from "../../utils/json-status.js"; import { formatCountLabel, formatListening, @@ -63,6 +64,11 @@ export default class SpacesSubscribe extends SpacesBaseCommand { if (!this.shouldOutputJson(flags)) { this.log(formatProgress("Subscribing to space updates")); } + this.logJsonStatus( + JsonStatusType.Subscribing, + `Subscribing to space updates in space: ${spaceName}.`, + flags, + ); await this.initializeSpace(flags, spaceName, { enterSpace: false }); @@ -106,6 +112,11 @@ export default class SpacesSubscribe extends SpacesBaseCommand { ); this.log(formatListening("Listening for space updates.")); } + this.logJsonStatus( + JsonStatusType.Listening, + "Listening for space updates.", + flags, + ); this.logCliEvent( flags, diff --git a/src/utils/json-status.ts b/src/utils/json-status.ts new file mode 100644 index 00000000..67edb149 --- /dev/null +++ b/src/utils/json-status.ts @@ -0,0 +1,15 @@ +/** Standardized status values for `logJsonStatus` in subscribe and hold commands. */ +export enum JsonStatusType { + /** Command is establishing connection / attaching to resource */ + Subscribing = "subscribing", + /** Successfully attached and actively listening for events */ + Listening = "listening", + /** Command is holding state (presence, lock, cursor) until Ctrl+C */ + Holding = "holding", + /** Duration elapsed or command finished cleanly */ + Complete = "complete", + /** Non-fatal warning (disconnection, retry, partial failure) */ + Warning = "warning", + /** Disconnected from Ably, will attempt to reconnect */ + Disconnected = "disconnected", +} diff --git a/test/unit/commands/spaces/members/subscribe.test.ts b/test/unit/commands/spaces/members/subscribe.test.ts index 602ace38..ada6093c 100644 --- a/test/unit/commands/spaces/members/subscribe.test.ts +++ b/test/unit/commands/spaces/members/subscribe.test.ts @@ -102,7 +102,13 @@ describe("spaces:members:subscribe command", () => { import.meta.url, ); - const result = JSON.parse(stdout); + const lines = stdout.trim().split("\n").filter(Boolean); + const eventLine = lines.find((l) => { + const parsed = JSON.parse(l); + return parsed.type === "event"; + }); + expect(eventLine).toBeDefined(); + const result = JSON.parse(eventLine!); expect(result.type).toBe("event"); expect(result.member).toBeDefined(); expect(result.member.clientId).toBe("user-1"); diff --git a/test/unit/commands/spaces/subscribe.test.ts b/test/unit/commands/spaces/subscribe.test.ts index 6aacc796..6513104c 100644 --- a/test/unit/commands/spaces/subscribe.test.ts +++ b/test/unit/commands/spaces/subscribe.test.ts @@ -96,7 +96,13 @@ describe("spaces:subscribe command", () => { import.meta.url, ); - const result = JSON.parse(stdout); + const lines = stdout.trim().split("\n").filter(Boolean); + const eventLine = lines.find((l) => { + const parsed = JSON.parse(l); + return parsed.type === "event"; + }); + expect(eventLine).toBeDefined(); + const result = JSON.parse(eventLine!); expect(result.type).toBe("event"); expect(result.space).toBeDefined(); expect(result.space.members).toBeDefined(); From e456aa8c91bb1dc2d8bf88d5027180433ca03d0d Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 26 Mar 2026 15:12:01 +0530 Subject: [PATCH 2/2] Update CLAUDE.md/claude skills JSON conventions --- .claude/skills/ably-codebase-review/SKILL.md | 8 +++- .../ably-new-command/references/patterns.md | 46 ++++++++++++++----- .claude/skills/ably-review/SKILL.md | 4 +- AGENTS.md | 4 +- src/commands/spaces/locks/get-all.ts | 2 +- src/commands/spaces/locks/get.ts | 2 +- 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/.claude/skills/ably-codebase-review/SKILL.md b/.claude/skills/ably-codebase-review/SKILL.md index dbe59626..63088b2a 100644 --- a/.claude/skills/ably-codebase-review/SKILL.md +++ b/.claude/skills/ably-codebase-review/SKILL.md @@ -154,7 +154,9 @@ Launch these agents **in parallel**. Each agent gets a focused mandate and uses 4. Cross-reference: every leaf command should appear in both the `logJsonResult`/`logJsonEvent` list and the `shouldOutputJson` list 5. **Read** streaming commands to verify they use `logJsonEvent`, one-shot commands use `logJsonResult` 6. **Read** each `logJsonResult`/`logJsonEvent` call and verify data is nested under a domain key — singular for events/single items (e.g., `{message: ...}`, `{cursor: ...}`), plural for collections (e.g., `{cursors: [...]}`, `{rules: [...]}`). Top-level envelope fields are `type`, `command`, `success` only. Metadata like `total`, `timestamp`, `appId` may sit alongside the domain key. -7. **Check** hold commands (set, enter, acquire) emit `logJsonStatus("holding", ...)` after `logJsonResult` — this signals to JSON consumers that the command is alive and waiting for Ctrl+C / `--duration` +7. **Check** hold commands (set, enter, acquire) emit `logJsonStatus(JsonStatusType.Holding, ...)` after `logJsonResult` +8. **Check** subscribe commands emit `logJsonStatus(JsonStatusType.Subscribing, ...)` when connecting and `logJsonStatus(JsonStatusType.Listening, ...)` when attached. Room subscribe commands should pass `subscribingMessage` and `listeningMessage` to `setupRoomStatusHandler`. +9. **Grep** for `logJsonStatus("` — all calls must use `JsonStatusType` enum, not raw strings **Reasoning guidance:** - Commands that ONLY have human output (no JSON path) are deviations @@ -162,7 +164,9 @@ Launch these agents **in parallel**. Each agent gets a focused mandate and uses - Topic index commands (showing help) don't need JSON output - Data spread at the top level without a domain key is a deviation — nest under a singular or plural domain noun - Metadata fields (`total`, `timestamp`, `hasMore`, `appId`) alongside the domain key are acceptable — they describe the result, not the domain objects -- Hold commands missing `logJsonStatus` after `logJsonResult` are deviations — JSON consumers need the hold signal +- Hold commands missing `logJsonStatus` after `logJsonResult` are deviations +- Subscribe commands missing `logJsonStatus(Subscribing/Listening)` are deviations +- Raw string arguments to `logJsonStatus` are deviations — use `JsonStatusType` enum ### Agent 6: Test Pattern Sweep diff --git a/.claude/skills/ably-new-command/references/patterns.md b/.claude/skills/ably-new-command/references/patterns.md index b3be44e1..c4a51a97 100644 --- a/.claude/skills/ably-new-command/references/patterns.md +++ b/.claude/skills/ably-new-command/references/patterns.md @@ -28,6 +28,8 @@ static override flags = { ``` ```typescript +import { JsonStatusType } from "../../utils/json-status.js"; + async run(): Promise { const { args, flags } = await this.parse(MySubscribeCommand); @@ -47,13 +49,8 @@ async run(): Promise { if (!this.shouldOutputJson(flags)) { this.log(formatProgress("Attaching to channel: " + formatResource(args.channel))); } - - channel.once("attached", () => { - if (!this.shouldOutputJson(flags)) { - this.log(formatSuccess("Attached to channel: " + formatResource(args.channel) + ".")); - this.log(formatListening("Listening for events.")); - } - }); + // JSON subscribe status — logJsonStatus has built-in shouldOutputJson guard + this.logJsonStatus(JsonStatusType.Subscribing, `Subscribing to channel: ${args.channel}.`, flags); let sequenceCounter = 0; await channel.subscribe((message) => { @@ -89,10 +86,35 @@ async run(): Promise { } }); + if (!this.shouldOutputJson(flags)) { + this.log(formatSuccess("Subscribed to channel: " + formatResource(args.channel) + ".")); + this.log(formatListening("Listening for events.")); + } + // JSON listening status — emitted after successful attach + this.logJsonStatus(JsonStatusType.Listening, "Listening for events.", flags); + await this.waitAndTrackCleanup(flags, "subscribe", flags.duration); } ``` +**JSON status lifecycle for subscribe commands:** +All subscribe commands must emit two JSON status messages so consumers can distinguish "connecting" from "ready": +```jsonl +{"type":"status","command":"channels:subscribe","status":"subscribing","message":"Subscribing to channel: my-channel."} +{"type":"status","command":"channels:subscribe","status":"listening","message":"Listening for events."} +{"type":"event","command":"channels:subscribe","message":{...}} +``` + +For **room commands** (extending `ChatBaseCommand`), pass `subscribingMessage` and `listeningMessage` to `setupRoomStatusHandler` — the handler emits both statuses automatically: +```typescript +this.setupRoomStatusHandler(room, flags, { + roomName, + successMessage: `Subscribed to room: ${formatResource(roomName)}.`, + listeningMessage: "Listening for messages.", + subscribingMessage: `Subscribing to room: ${roomName}.`, +}); +``` + --- ## Publish/Send Pattern @@ -669,7 +691,7 @@ Commands must behave strictly according to their documented purpose — no unint **Set / enter / acquire commands** — hold state until Ctrl+C / `--duration`: - Enter space (manual: `enterSpace: false` + `space.enter()` + `markAsEntered()`), perform operation, output confirmation, then hold with `waitAndTrackCleanup` -- Emit `formatListening("Holding .")` (human) and `logJsonStatus("holding", ...)` (JSON) +- Emit `formatListening("Holding .")` (human) and `logJsonStatus(JsonStatusType.Holding, ...)` (JSON) - **NOT subscribe** to other events — that is what subscribe commands are for **Side-effect rules:** @@ -700,7 +722,7 @@ this.markAsEntered(); await this.space!.locations.set(location); // output result... this.log(formatListening("Holding location.")); -this.logJsonStatus("holding", "Holding location. Press Ctrl+C to exit.", flags); +this.logJsonStatus(JsonStatusType.Holding, "Holding location. Press Ctrl+C to exit.", flags); await this.waitAndTrackCleanup(flags, "location", flags.duration); ``` @@ -747,9 +769,11 @@ Metadata fields (`total`, `timestamp`, `hasMore`, `appId`) may sit alongside the ### Hold status for long-running commands (logJsonStatus) -Long-running commands that hold state (e.g. `spaces members enter`, `spaces locations set`, `spaces locks acquire`, `spaces cursors set`) must emit a status line after the result so JSON consumers know the command is alive and waiting: +Long-running commands that hold state (e.g. `spaces members enter`, `spaces locations set`, `spaces locks acquire`, `spaces cursors set`) must emit a status line after the result so JSON consumers know the command is alive and waiting. Always use the `JsonStatusType` enum from `src/utils/json-status.ts` — never pass raw strings: ```typescript +import { JsonStatusType } from "../../../utils/json-status.js"; + // After the result output: if (this.shouldOutputJson(flags)) { this.logJsonResult({ member: formatMemberOutput(self!) }, flags); @@ -760,7 +784,7 @@ if (this.shouldOutputJson(flags)) { } // logJsonStatus has built-in shouldOutputJson guard — no outer if needed -this.logJsonStatus("holding", "Holding presence. Press Ctrl+C to exit.", flags); +this.logJsonStatus(JsonStatusType.Holding, "Holding presence. Press Ctrl+C to exit.", flags); await this.waitAndTrackCleanup(flags, "member", flags.duration); ``` diff --git a/.claude/skills/ably-review/SKILL.md b/.claude/skills/ably-review/SKILL.md index 6fd54073..936c62d1 100644 --- a/.claude/skills/ably-review/SKILL.md +++ b/.claude/skills/ably-review/SKILL.md @@ -119,7 +119,9 @@ For each changed command file, run the relevant checks. Spawn agents for paralle 3. **Grep** for `shouldOutputJson` — verify human output is guarded 4. **Read** the file to verify streaming commands use `logJsonEvent` and one-shot commands use `logJsonResult` 5. **Read** `logJsonResult`/`logJsonEvent` call sites and check data is nested under a domain key (singular for events/single items, plural for collections) — not spread at top level. Top-level envelope fields are `type`, `command`, `success` only. Metadata like `total`, `timestamp`, `appId` may sit alongside the domain key. -6. **Check** hold commands (set, enter, acquire) emit `logJsonStatus("holding", ...)` after `logJsonResult` — this signals to JSON consumers that the command is alive and waiting for Ctrl+C / `--duration` +6. **Check** hold commands (set, enter, acquire) emit `logJsonStatus(JsonStatusType.Holding, ...)` after `logJsonResult` +7. **Check** subscribe commands emit `logJsonStatus(JsonStatusType.Subscribing, ...)` when connecting and `logJsonStatus(JsonStatusType.Listening, ...)` when attached. Room subscribe commands should pass `subscribingMessage` and `listeningMessage` to `setupRoomStatusHandler`. +8. **Grep** for `logJsonStatus("` — all calls must use `JsonStatusType` enum, not raw strings **Control API helper check (grep — for Control API commands only):** 1. **Grep** for `resolveAppId` — should use `requireAppId` instead (encapsulates null check and `fail()`) diff --git a/AGENTS.md b/AGENTS.md index 0766eb79..4fc594d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -224,7 +224,9 @@ All output helpers use the `format` prefix and are exported from `src/utils/outp - **Pagination next hint**: `buildPaginationNext(hasMore, lastTimestamp?)` — returns `{ hint, start? }` for JSON output when `hasMore` is true. Pass `lastTimestamp` only for history commands (which have `--start`). - **JSON guard**: All human-readable output (progress, success, listening messages) must be wrapped in `if (!this.shouldOutputJson(flags))` so it doesn't pollute `--json` output. Only JSON payloads should be emitted when `--json` is active. - **JSON envelope**: Use `this.logJsonResult(data, flags)` for one-shot results, `this.logJsonEvent(data, flags)` for streaming events, and `this.logJsonStatus(status, message, flags)` for hold/status signals in long-running commands. The envelope adds top-level fields (`type`, `command`, `success?`). Nest domain data under a **domain key** (see "JSON data nesting convention" below). Do NOT add ad-hoc `success: true/false` — the envelope handles it. `--json` produces compact single-line output (NDJSON for streaming). `--pretty-json` is unchanged. -- **JSON hold status**: Long-running hold commands (e.g. `spaces members enter`, `spaces locations set`, `spaces locks acquire`, `spaces cursors set`) must emit a `logJsonStatus("holding", "Holding . Press Ctrl+C to exit.", flags)` line after the result. This tells LLM agents and scripts that the command is alive and waiting. `logJsonStatus` has a built-in `shouldOutputJson` guard — no outer `if` needed. +- **JSON status types**: Use the `JsonStatusType` enum from `src/utils/json-status.ts` for all `logJsonStatus` calls — never pass raw strings. Available values: `Subscribing`, `Listening`, `Holding`, `Complete`, `Warning`, `Disconnected`. `logJsonStatus` has a built-in `shouldOutputJson` guard — no outer `if` needed. Two command-type-specific patterns: + - **Hold commands** (set, enter, acquire): emit `JsonStatusType.Holding` after the result. + - **Subscribe commands**: emit `JsonStatusType.Subscribing` when connecting, `JsonStatusType.Listening` when attached. Room commands pass `subscribingMessage`/`listeningMessage` to `setupRoomStatusHandler`; channel/space/log commands call `logJsonStatus` directly. See `references/patterns.md` for templates. - **JSON errors**: Use `this.fail(error, flags, component, context?)` as the single error funnel in command `run()` methods. It logs the CLI event, preserves structured error data (Ably codes, HTTP status), emits JSON error envelope when `--json` is active, and calls `this.error()` for human-readable output. Returns `never` — no `return;` needed after calling it. Do NOT call `this.error()` directly — it is an internal implementation detail of `fail`. - **History output**: Use `[index] [timestamp]` on the same line as a heading: `` `${formatIndex(index + 1)} ${formatTimestamp(timestamp)}` ``, then fields indented below. This is distinct from **get-all output** which uses `[index]` alone on its own line. See `references/patterns.md` "History results" and "One-shot results" for both patterns. diff --git a/src/commands/spaces/locks/get-all.ts b/src/commands/spaces/locks/get-all.ts index 0f60de3c..06b7ce10 100644 --- a/src/commands/spaces/locks/get-all.ts +++ b/src/commands/spaces/locks/get-all.ts @@ -43,7 +43,7 @@ export default class SpacesLocksGetAll extends SpacesBaseCommand { try { await this.initializeSpace(flags, spaceName, { - enterSpace: true, + enterSpace: false, setupConnectionLogging: false, }); diff --git a/src/commands/spaces/locks/get.ts b/src/commands/spaces/locks/get.ts index 8bca0f39..23b357ad 100644 --- a/src/commands/spaces/locks/get.ts +++ b/src/commands/spaces/locks/get.ts @@ -40,7 +40,7 @@ export default class SpacesLocksGet extends SpacesBaseCommand { try { await this.initializeSpace(flags, spaceName, { - enterSpace: true, + enterSpace: false, setupConnectionLogging: false, });