diff --git a/README.md b/README.md index 22f37a3c..b14de2a1 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,8 @@ $ ably-interactive * [`ably channels occupancy subscribe CHANNEL`](#ably-channels-occupancy-subscribe-channel) * [`ably channels presence`](#ably-channels-presence) * [`ably channels presence enter CHANNEL`](#ably-channels-presence-enter-channel) -* [`ably channels presence get-all CHANNEL`](#ably-channels-presence-get-all-channel) +* [`ably channels presence get CHANNEL`](#ably-channels-presence-get-channel) * [`ably channels presence subscribe CHANNEL`](#ably-channels-presence-subscribe-channel) -* [`ably channels presence update CHANNEL`](#ably-channels-presence-update-channel) * [`ably channels publish CHANNEL MESSAGE`](#ably-channels-publish-channel-message) * [`ably channels subscribe CHANNELS`](#ably-channels-subscribe-channels) * [`ably channels update CHANNEL SERIAL MESSAGE`](#ably-channels-update-channel-serial-message) @@ -1896,7 +1895,7 @@ _See code: [src/commands/channels/presence.ts](https://github.com/ably/ably-cli/ ## `ably channels presence enter CHANNEL` -Enter presence on a channel and listen for presence events +Enter presence on a channel and remains present until terminated. ``` USAGE @@ -1918,9 +1917,11 @@ FLAGS --show-others Show other presence events while present (default: false) DESCRIPTION - Enter presence on a channel and listen for presence events + Enter presence on a channel and remains present until terminated. EXAMPLES + $ ably channels presence enter my-channel + $ ably channels presence enter my-channel --client-id "client123" $ ably channels presence enter my-channel --client-id "client123" --data '{"name":"John","status":"online"}' @@ -1932,19 +1933,17 @@ EXAMPLES $ ably channels presence enter my-channel --pretty-json $ ably channels presence enter my-channel --duration 30 - - $ ABLY_API_KEY="YOUR_API_KEY" ably channels presence enter my-channel ``` _See code: [src/commands/channels/presence/enter.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/presence/enter.ts)_ -## `ably channels presence get-all CHANNEL` +## `ably channels presence get CHANNEL` Get all current presence members on a channel -``` +```text USAGE - $ ably channels presence get-all CHANNEL [-v] [--json | --pretty-json] [--limit ] + $ ably channels presence get CHANNEL [-v] [--json | --pretty-json] [--limit ] ARGUMENTS CHANNEL Channel name to get presence members for @@ -1959,16 +1958,16 @@ DESCRIPTION Get all current presence members on a channel EXAMPLES - $ ably channels presence get-all my-channel + $ ably channels presence get my-channel - $ ably channels presence get-all my-channel --limit 50 + $ ably channels presence get my-channel --limit 50 - $ ably channels presence get-all my-channel --json + $ ably channels presence get my-channel --json - $ ably channels presence get-all my-channel --pretty-json + $ ably channels presence get my-channel --pretty-json ``` -_See code: [src/commands/channels/presence/get-all.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/presence/get-all.ts)_ +_See code: [src/commands/channels/presence/get.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/presence/get.ts)_ ## `ably channels presence subscribe CHANNEL` @@ -2008,40 +2007,6 @@ EXAMPLES _See code: [src/commands/channels/presence/subscribe.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/presence/subscribe.ts)_ -## `ably channels presence update CHANNEL` - -Update presence data on a channel - -``` -USAGE - $ ably channels presence update CHANNEL --data [-v] [--json | --pretty-json] [--client-id ] [-D - ] - -ARGUMENTS - CHANNEL Channel to update presence on - -FLAGS - -D, --duration= Automatically exit after N seconds - -v, --verbose Output verbose logs - --client-id= Overrides any default client ID when using API authentication. Use "none" to explicitly set - no client ID. Not applicable when using token authentication. - --data= (required) JSON data to associate with the presence update - --json Output in JSON format - --pretty-json Output in colorized JSON format - -DESCRIPTION - Update presence data on a channel - -EXAMPLES - $ ably channels presence update my-channel --data '{"status":"away"}' - - $ ably channels presence update my-channel --data '{"status":"busy"}' --json - - $ ably channels presence update my-channel --data '{"status":"online"}' --duration 60 -``` - -_See code: [src/commands/channels/presence/update.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/presence/update.ts)_ - ## `ably channels publish CHANNEL MESSAGE` Publish a message to an Ably channel diff --git a/src/commands/channels/presence/enter.ts b/src/commands/channels/presence/enter.ts index 85372a70..7455127d 100644 --- a/src/commands/channels/presence/enter.ts +++ b/src/commands/channels/presence/enter.ts @@ -25,16 +25,16 @@ export default class ChannelsPresenceEnter extends AblyBaseCommand { }; static override description = - "Enter presence on a channel and listen for presence events"; + "Enter presence on a channel and remains present until terminated."; static override examples = [ + "$ ably channels presence enter my-channel", '$ ably channels presence enter my-channel --client-id "client123"', '$ ably channels presence enter my-channel --client-id "client123" --data \'{"name":"John","status":"online"}\'', "$ ably channels presence enter my-channel --show-others", "$ ably channels presence enter my-channel --json", "$ ably channels presence enter my-channel --pretty-json", "$ ably channels presence enter my-channel --duration 30", - '$ ABLY_API_KEY="YOUR_API_KEY" ably channels presence enter my-channel', ]; static override flags = { diff --git a/src/commands/channels/presence/get-all.ts b/src/commands/channels/presence/get.ts similarity index 92% rename from src/commands/channels/presence/get-all.ts rename to src/commands/channels/presence/get.ts index d234e2d3..df3c61d4 100644 --- a/src/commands/channels/presence/get-all.ts +++ b/src/commands/channels/presence/get.ts @@ -20,7 +20,7 @@ import { formatPaginationLog, } from "../../../utils/pagination.js"; -export default class ChannelsPresenceGetAll extends AblyBaseCommand { +export default class ChannelsPresenceGet extends AblyBaseCommand { static override args = { channel: Args.string({ description: "Channel name to get presence members for", @@ -31,10 +31,10 @@ export default class ChannelsPresenceGetAll extends AblyBaseCommand { static override description = "Get all current presence members on a channel"; static override examples = [ - "$ ably channels presence get-all my-channel", - "$ ably channels presence get-all my-channel --limit 50", - "$ ably channels presence get-all my-channel --json", - "$ ably channels presence get-all my-channel --pretty-json", + "$ ably channels presence get my-channel", + "$ ably channels presence get my-channel --limit 50", + "$ ably channels presence get my-channel --json", + "$ ably channels presence get my-channel --pretty-json", ]; static override flags = { @@ -47,7 +47,7 @@ export default class ChannelsPresenceGetAll extends AblyBaseCommand { }; async run(): Promise { - const { args, flags } = await this.parse(ChannelsPresenceGetAll); + const { args, flags } = await this.parse(ChannelsPresenceGet); try { const client = await this.createAblyRestClient(flags); @@ -154,7 +154,7 @@ export default class ChannelsPresenceGetAll extends AblyBaseCommand { } } } catch (error) { - this.fail(error, flags, "presenceGetAll", { + this.fail(error, flags, "presenceGet", { channel: args.channel, }); } diff --git a/src/commands/channels/presence/update.ts b/src/commands/channels/presence/update.ts deleted file mode 100644 index fe188067..00000000 --- a/src/commands/channels/presence/update.ts +++ /dev/null @@ -1,163 +0,0 @@ -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 { - formatClientId, - formatLabel, - formatListening, - formatProgress, - formatResource, - formatSuccess, -} from "../../../utils/output.js"; - -export default class ChannelsPresenceUpdate extends AblyBaseCommand { - static override args = { - channel: Args.string({ - description: "Channel to update presence on", - required: true, - }), - }; - - static override description = "Update presence data on a channel"; - - static override examples = [ - '$ ably channels presence update my-channel --data \'{"status":"away"}\'', - '$ ably channels presence update my-channel --data \'{"status":"busy"}\' --json', - '$ ably channels presence update my-channel --data \'{"status":"busy"}\' --pretty-json', - '$ ably channels presence update my-channel --data \'{"status":"online"}\' --duration 60', - ]; - - static override flags = { - ...productApiFlags, - ...clientIdFlag, - data: Flags.string({ - description: "JSON data to associate with the presence update", - required: true, - }), - ...durationFlag, - }; - - private client: Ably.Realtime | null = null; - private channel: Ably.RealtimeChannel | null = null; - - async run(): Promise { - const { args, flags } = await this.parse(ChannelsPresenceUpdate); - - try { - this.client = await this.createAblyRealtimeClient(flags); - if (!this.client) return; - - const client = this.client; - const { channel: channelName } = args; - - const data = this.parseJsonFlag(flags.data, "data", flags); - - this.channel = client.channels.get(channelName); - - this.setupConnectionStateLogging(client, flags, { - includeUserFriendlyMessages: true, - }); - this.setupChannelStateLogging(this.channel, flags, { - includeUserFriendlyMessages: true, - }); - - if (!this.shouldOutputJson(flags)) { - this.log( - formatProgress( - `Entering and updating presence on channel: ${formatResource(channelName)}`, - ), - ); - } - - // Enter first (required before update) - this.logCliEvent( - flags, - "presence", - "entering", - `Entering presence on channel ${channelName}`, - { channel: channelName, clientId: client.auth.clientId }, - ); - await this.channel.presence.enter(data); - this.logCliEvent( - flags, - "presence", - "entered", - `Entered presence on channel ${channelName}`, - { channel: channelName, clientId: client.auth.clientId }, - ); - - // Update presence data - this.logCliEvent( - flags, - "presence", - "updating", - `Updating presence data on channel ${channelName}`, - { channel: channelName, data }, - ); - await this.channel.presence.update(data); - this.logCliEvent( - flags, - "presence", - "updated", - `Updated presence data on channel ${channelName}`, - { channel: channelName, clientId: client.auth.clientId, data }, - ); - - if (this.shouldOutputJson(flags)) { - this.logJsonResult( - { - presenceMessage: { - action: "update", - channel: channelName, - clientId: client.auth.clientId, - connectionId: client.connection.id, - data, - timestamp: new Date().toISOString(), - }, - }, - flags, - ); - } else { - this.log( - formatSuccess( - `Updated presence on channel: ${formatResource(channelName)}.`, - ), - ); - this.log( - `${formatLabel("Client ID")} ${formatClientId(client.auth.clientId)}`, - ); - this.log(`${formatLabel("Connection ID")} ${client.connection.id}`); - this.log(`${formatLabel("Data")} ${JSON.stringify(data)}`); - this.log(formatListening("Holding presence.")); - } - - this.logJsonStatus( - "holding", - "Holding presence. Press Ctrl+C to exit.", - flags, - ); - - await this.waitAndTrackCleanup(flags, "presence", flags.duration); - } catch (error) { - this.fail(error, flags, "presenceUpdate", { - channel: args.channel, - }); - } - } - - async finally(err: Error | undefined): Promise { - if (this.channel && this.client) { - try { - await Promise.race([ - this.channel.presence.leave(), - new Promise((resolve) => setTimeout(resolve, 2000)), - ]); - } catch { - // Ignore cleanup errors - } - } - - await super.finally(err); - } -} diff --git a/test/unit/commands/channels/presence/get-all.test.ts b/test/unit/commands/channels/presence/get.test.ts similarity index 87% rename from test/unit/commands/channels/presence/get-all.test.ts rename to test/unit/commands/channels/presence/get.test.ts index 60c08be4..a13dc7f5 100644 --- a/test/unit/commands/channels/presence/get-all.test.ts +++ b/test/unit/commands/channels/presence/get.test.ts @@ -31,7 +31,7 @@ const mockPresenceMembers = [ }, ]; -describe("channels:presence:get-all command", () => { +describe("channels:presence:get command", () => { beforeEach(() => { const mock = getMockAblyRest(); const channel = mock.channels._getChannel("test-channel"); @@ -41,11 +41,11 @@ describe("channels:presence:get-all command", () => { ); }); - standardHelpTests("channels:presence:get-all", import.meta.url); - standardArgValidationTests("channels:presence:get-all", import.meta.url, { + standardHelpTests("channels:presence:get", import.meta.url); + standardArgValidationTests("channels:presence:get", import.meta.url, { requiredArgs: ["test-channel"], }); - standardFlagTests("channels:presence:get-all", import.meta.url, [ + standardFlagTests("channels:presence:get", import.meta.url, [ "--limit", "--json", "--pretty-json", @@ -57,7 +57,7 @@ describe("channels:presence:get-all command", () => { const channel = mock.channels._getChannel("test-channel"); const { stdout } = await runCommand( - ["channels:presence:get-all", "test-channel"], + ["channels:presence:get", "test-channel"], import.meta.url, ); @@ -75,7 +75,7 @@ describe("channels:presence:get-all command", () => { channel.presence.get.mockResolvedValue(createMockPaginatedResult([])); const { stderr } = await runCommand( - ["channels:presence:get-all", "test-channel"], + ["channels:presence:get", "test-channel"], import.meta.url, ); @@ -84,7 +84,7 @@ describe("channels:presence:get-all command", () => { it("should output JSON with presenceMembers array", async () => { const { stdout } = await runCommand( - ["channels:presence:get-all", "test-channel", "--json"], + ["channels:presence:get", "test-channel", "--json"], import.meta.url, ); @@ -104,7 +104,7 @@ describe("channels:presence:get-all command", () => { it("should display member data when present", async () => { const { stdout } = await runCommand( - ["channels:presence:get-all", "test-channel"], + ["channels:presence:get", "test-channel"], import.meta.url, ); @@ -118,7 +118,7 @@ describe("channels:presence:get-all command", () => { const channel = mock.channels._getChannel("test-channel"); await runCommand( - ["channels:presence:get-all", "test-channel", "--limit", "50"], + ["channels:presence:get", "test-channel", "--limit", "50"], import.meta.url, ); @@ -144,7 +144,7 @@ describe("channels:presence:get-all command", () => { ); const { stdout } = await runCommand( - ["channels:presence:get-all", "test-channel", "--limit", "1", "--json"], + ["channels:presence:get", "test-channel", "--limit", "1", "--json"], import.meta.url, ); @@ -162,7 +162,7 @@ describe("channels:presence:get-all command", () => { channel.presence.get.mockRejectedValue(new Error("API error")); const { error } = await runCommand( - ["channels:presence:get-all", "test-channel"], + ["channels:presence:get", "test-channel"], import.meta.url, ); @@ -180,7 +180,7 @@ describe("channels:presence:get-all command", () => { ); const { error } = await runCommand( - ["channels:presence:get-all", "nonexistent-channel"], + ["channels:presence:get", "nonexistent-channel"], import.meta.url, ); diff --git a/test/unit/commands/channels/presence/update.test.ts b/test/unit/commands/channels/presence/update.test.ts deleted file mode 100644 index cdf7ebca..00000000 --- a/test/unit/commands/channels/presence/update.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { runCommand } from "@oclif/test"; -import { getMockAblyRealtime } from "../../../../helpers/mock-ably-realtime.js"; -import { - standardHelpTests, - standardArgValidationTests, - standardFlagTests, -} from "../../../../helpers/standard-tests.js"; - -describe("channels:presence:update command", () => { - beforeEach(() => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("test-channel"); - - // Configure connection.once to immediately call callback for 'connected' - mock.connection.once.mockImplementation( - (event: string, callback: () => void) => { - if (event === "connected") { - callback(); - } - }, - ); - - // Configure channel.once to immediately call callback for 'attached' - channel.once.mockImplementation((event: string, callback: () => void) => { - if (event === "attached") { - channel.state = "attached"; - callback(); - } - }); - }); - - standardHelpTests("channels:presence:update", import.meta.url); - standardArgValidationTests("channels:presence:update", import.meta.url, { - requiredArgs: ["test-channel"], - }); - standardFlagTests("channels:presence:update", import.meta.url, [ - "--data", - "--json", - "--duration", - "--client-id", - ]); - - describe("functionality", () => { - it("should enter and update presence with data", async () => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("test-channel"); - - const { stdout } = await runCommand( - [ - "channels:presence:update", - "test-channel", - "--data", - '{"status":"away"}', - ], - import.meta.url, - ); - - // Should show progress and successful update - expect(stdout).toContain("Entering and updating presence on channel"); - expect(stdout).toContain("Updated"); - expect(stdout).toContain("test-channel"); - // Verify enter then update were called - expect(channel.presence.enter).toHaveBeenCalled(); - expect(channel.presence.update).toHaveBeenCalledWith({ - status: "away", - }); - }); - - it("should show labeled output in human mode", async () => { - const { stdout } = await runCommand( - [ - "channels:presence:update", - "test-channel", - "--data", - '{"status":"away"}', - ], - import.meta.url, - ); - - expect(stdout).toContain("Client ID"); - expect(stdout).toContain("Connection ID"); - expect(stdout).toContain("Data"); - expect(stdout).toContain("Holding presence"); - }); - - it("should output JSON with presenceMessage domain key", async () => { - const { stdout } = await runCommand( - [ - "channels:presence:update", - "test-channel", - "--data", - '{"status":"away"}', - "--json", - ], - import.meta.url, - ); - - const lines = stdout.trim().split("\n").filter(Boolean); - expect(lines.length).toBeGreaterThanOrEqual(1); - - const result = JSON.parse(lines[0]); - expect(result.type).toBe("result"); - expect(result.presenceMessage).toBeDefined(); - expect(result.presenceMessage.action).toBe("update"); - expect(result.presenceMessage.channel).toBe("test-channel"); - expect(result.presenceMessage.clientId).toBeDefined(); - expect(result.presenceMessage.connectionId).toBeDefined(); - expect(result.presenceMessage.data).toEqual({ status: "away" }); - }); - - it("should emit hold status in JSON mode", async () => { - const { stdout } = await runCommand( - [ - "channels:presence:update", - "test-channel", - "--data", - '{"status":"away"}', - "--json", - ], - import.meta.url, - ); - - const lines = stdout.trim().split("\n").filter(Boolean); - expect(lines.length).toBeGreaterThanOrEqual(2); - - const status = JSON.parse(lines[1]); - expect(status.type).toBe("status"); - expect(status.status).toBe("holding"); - expect(status.message).toContain("Holding presence"); - }); - - it("should handle invalid JSON data gracefully", async () => { - const { error } = await runCommand( - [ - "channels:presence:update", - "test-channel", - "--data", - "not-valid-json", - ], - import.meta.url, - ); - - expect(error).toBeDefined(); - expect(error?.message).toMatch(/invalid|json/i); - }); - }); - - describe("error handling", () => { - it("should handle presence enter errors", async () => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("test-channel"); - channel.presence.enter.mockRejectedValue( - new Error("Presence enter failed"), - ); - - const { error } = await runCommand( - [ - "channels:presence:update", - "test-channel", - "--data", - '{"status":"away"}', - ], - import.meta.url, - ); - - expect(error).toBeDefined(); - }); - - it("should handle presence update errors", async () => { - const mock = getMockAblyRealtime(); - const channel = mock.channels._getChannel("test-channel"); - channel.presence.update.mockRejectedValue( - new Error("Presence update failed"), - ); - - const { error } = await runCommand( - [ - "channels:presence:update", - "test-channel", - "--data", - '{"status":"away"}', - ], - import.meta.url, - ); - - expect(error).toBeDefined(); - }); - }); -});