Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,13 @@ When adding COMMANDS sections in `src/help.ts`, use `chalk.bold()` for headers,

### Flag conventions
- All flags kebab-case: `--my-flag` (never camelCase)
- **No duplicate defaults in descriptions**: oclif appends `[default: N]` to `--help` automatically. Don't repeat it in `description`. Exceptions: falsy defaults (`0`, `false`) — oclif suppresses these, so keep them in the description. Behavioral phrases like `"defaults to current app"` are fine too.
- `--app`: `"The app ID or name (defaults to current app)"` (for commands with `resolveAppId`), `"The app ID (defaults to current app)"` (for commands without)
- `--limit`: `"Maximum number of results to return"` with `min: 1` (oclif shows `[default: N]` automatically, don't duplicate in description)
- `--limit`: `"Maximum number of results to return"` with `min: 1`
- `--duration`: Use `durationFlag` from `src/flags.ts`. `"Automatically exit after N seconds"`, alias `-D`.
- `--rewind`: Use `rewindFlag` from `src/flags.ts`. `"Number of messages to rewind when subscribing (default: 0)"`. Apply with `this.configureRewind(channelOptions, flags.rewind, flags, component, channelName)`.
- `--start`/`--end`: Use `timeRangeFlags` from `src/flags.ts` and parse with `parseTimestamp()` from `src/utils/time.ts`. Accepts ISO 8601, Unix ms, or relative (e.g., `"1h"`, `"30m"`, `"2d"`).
- `--direction`: `"Direction of message retrieval (default: backwards)"` or `"Direction of log retrieval"`, options `["backwards", "forwards"]`.
- `--direction`: `"Direction of message retrieval"` or `"Direction of log retrieval"`, options `["backwards", "forwards"]`.
- Channels use "publish", Rooms use "send" (matches SDK terminology)
- Command descriptions: imperative mood, sentence case, no trailing period (e.g., `"Subscribe to presence events on a channel"`)

Expand Down
88 changes: 48 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ FLAGS
--json Output in JSON format
--pretty-json Output in colorized JSON format
--token-only Output only the token string without any formatting or additional information
--ttl=<value> [default: 3600] Time to live in seconds (default: 3600, 1 hour)
--ttl=<value> [default: 3600] Time to live in seconds

DESCRIPTION
Create an Ably Token with capabilities
Expand Down Expand Up @@ -913,7 +913,7 @@ FLAGS
--json Output in JSON format
--pretty-json Output in colorized JSON format
--token-only Output only the token string without any formatting or additional information
--ttl=<value> [default: 3600] Time to live in seconds (default: 3600, 1 hour)
--ttl=<value> [default: 3600] Time to live in seconds

DESCRIPTION
Create an Ably JWT token with capabilities
Expand Down Expand Up @@ -1404,11 +1404,15 @@ DESCRIPTION
Manage annotations on Ably Pub/Sub channel messages

EXAMPLES
$ ably channels annotations publish my-channel "01234567890:0" "reactions:flag.v1" --name thumbsup
$ ably channels annotations publish my-channel "01234567890:0" "metrics:total.v1"

$ ably channels annotations publish my-channel "01234567890:0" "reactions:unique.v1" --name thumbsup

$ ably channels annotations subscribe my-channel

$ ably channels annotations get my-channel "01234567890:0"

$ ably channels annotations delete my-channel "01234567890:0" "receipts:flag.v1"
```

_See code: [src/commands/channels/annotations/index.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/annotations/index.ts)_
Expand Down Expand Up @@ -1439,13 +1443,15 @@ DESCRIPTION
Delete an annotation from a channel message

EXAMPLES
$ ably channels annotations delete my-channel "01234567890:0" "reactions:flag.v1" --name thumbsup
$ ably channels annotations delete my-channel "01234567890:0" "receipts:flag.v1"

$ ably channels annotations delete my-channel "01234567890:0" "categories:distinct.v1" --name important

$ ably channels annotations delete my-channel "01234567890:0" "reactions:multiple.v1" --name thumbsup
$ ably channels annotations delete my-channel "01234567890:0" "reactions:unique.v1" --name thumbsup

$ ably channels annotations delete my-channel "01234567890:0" "reactions:flag.v1" --json
$ ably channels annotations delete my-channel "01234567890:0" "rating:multiple.v1" --name stars

$ ably channels annotations delete my-channel "01234567890:0" "reactions:flag.v1" --pretty-json
$ ably channels annotations delete my-channel "01234567890:0" "receipts:flag.v1" --json
```

_See code: [src/commands/channels/annotations/delete.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/annotations/delete.ts)_
Expand Down Expand Up @@ -1512,15 +1518,17 @@ DESCRIPTION
Publish an annotation on a channel message

EXAMPLES
$ ably channels annotations publish my-channel "01234567890:0" "reactions:flag.v1" --name thumbsup
$ ably channels annotations publish my-channel "01234567890:0" "metrics:total.v1"

$ ably channels annotations publish my-channel "01234567890:0" "reactions:multiple.v1" --name thumbsup --count 3
$ ably channels annotations publish my-channel "01234567890:0" "receipts:flag.v1"

$ ably channels annotations publish my-channel "01234567890:0" "reactions:flag.v1" --data '{"key":"value"}'
$ ably channels annotations publish my-channel "01234567890:0" "categories:distinct.v1" --name important

$ ably channels annotations publish my-channel "01234567890:0" "reactions:flag.v1" --json
$ ably channels annotations publish my-channel "01234567890:0" "reactions:unique.v1" --name thumbsup

$ ably channels annotations publish my-channel "01234567890:0" "reactions:flag.v1" --pretty-json
$ ably channels annotations publish my-channel "01234567890:0" "rating:multiple.v1" --name stars --count 4

$ ably channels annotations publish my-channel "01234567890:0" "metrics:total.v1" --json
```

_See code: [src/commands/channels/annotations/publish.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/annotations/publish.ts)_
Expand Down Expand Up @@ -1555,11 +1563,11 @@ EXAMPLES

$ ably channels annotations subscribe my-channel --type "reactions:flag.v1"

$ ably channels annotations subscribe my-channel --json

$ ably channels annotations subscribe my-channel --pretty-json
$ ably channels annotations subscribe my-channel --type "metrics:total.v1"

$ ably channels annotations subscribe my-channel --duration 30

$ ably channels annotations subscribe my-channel --json
```

_See code: [src/commands/channels/annotations/subscribe.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/annotations/subscribe.ts)_
Expand Down Expand Up @@ -1701,8 +1709,8 @@ ARGUMENTS

FLAGS
-v, --verbose Output verbose logs
--cipher=<value> Decryption key for encrypted messages (AES-128)
--direction=<option> [default: backwards] Direction of message retrieval (default: backwards)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No need to show default twice in help methods

--cipher=<value> Decryption key for encrypted messages, uses AES-256 by default
--direction=<option> [default: backwards] Direction of message retrieval
<options: backwards|forwards>
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--json Output in JSON format
Expand All @@ -1726,7 +1734,7 @@ EXAMPLES

$ ably channels history my-channel --limit 100

$ ably channels history my-channel --direction forward
$ ably channels history my-channel --direction forwards
```

_See code: [src/commands/channels/history.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/channels/history.ts)_
Expand Down Expand Up @@ -2037,6 +2045,8 @@ EXAMPLES

$ 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
```

Expand All @@ -2056,8 +2066,8 @@ ARGUMENTS
MESSAGE The message to publish (JSON format or plain text)

FLAGS
-c, --count=<value> [default: 1] Number of messages to publish (default: 1)
-d, --delay=<value> [default: 40] Delay between messages in milliseconds (default: 40ms, max 25 msgs/sec)
-c, --count=<value> [default: 1] Number of messages to publish
-d, --delay=<value> [default: 40] Delay between messages in milliseconds (max 25 msgs/sec)
-e, --encoding=<value> The encoding for the message
-n, --name=<value> The event name (if not specified in the message JSON)
-v, --verbose Output verbose logs
Expand Down Expand Up @@ -2113,13 +2123,13 @@ ARGUMENTS
FLAGS
-D, --duration=<value> Automatically exit after N seconds
-v, --verbose Output verbose logs
--cipher-algorithm=<value> [default: aes] Encryption algorithm to use (default: aes)
--cipher-key=<value> Encryption key for decrypting messages (hex-encoded)
--cipher-key-length=<value> [default: 256] Length of encryption key in bits (default: 256)
--cipher-mode=<value> [default: cbc] Cipher mode to use (default: cbc)
--cipher-algorithm=<value> [default: aes] Encryption algorithm to use
--cipher-key=<value> Encryption key for decrypting messages (base64-encoded or hex-encoded)
--cipher-key-length=<value> [default: 256] Length of encryption key in bits
--cipher-mode=<value> [default: cbc] Cipher mode to use
--client-id=<value> Overrides any default client ID when using API authentication. Use "none" to
explicitly set no client ID. Not applicable when using token authentication.
--delta Enable delta compression for messages
--delta Enable delta compression (VCDIFF) to reduce message payload sizes
--json Output in JSON format
--pretty-json Output in colorized JSON format
--rewind=<value> Number of messages to rewind when subscribing (default: 0)
Expand Down Expand Up @@ -2393,13 +2403,13 @@ FLAGS
--channel-filter=<value> Channel filter pattern
--json Output in JSON format
--pretty-json Output in colorized JSON format
--request-mode=<option> [default: single] Request mode for the integration (default: single)
--request-mode=<option> [default: single] Request mode for the integration
<options: single|batch>
--rule-type=<option> (required) Type of integration (http, amqp, etc.)
<options: http|amqp|kinesis|firehose|pulsar|kafka|azure|azure-functions|mqtt|cloudmqtt>
--source-type=<option> (required) The event source type
<options: channel.message|channel.presence|channel.lifecycle|presence.message>
--status=<option> [default: enabled] Initial status of the integration (default: enabled)
--status=<option> [default: enabled] Initial status of the integration
<options: enabled|disabled>
--target-url=<value> Target URL for HTTP integrations

Expand Down Expand Up @@ -3589,19 +3599,19 @@ FLAGS
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--max-length=<value> [default: 10000] Maximum number of messages in the queue (default: 10000)
--max-length=<value> [default: 10000] Maximum number of messages in the queue (max: 10000)
--name=<value> (required) Name of the queue
--pretty-json Output in colorized JSON format
--region=<value> [default: us-east-1-a] Region for the queue (default: us-east-1-a)
--ttl=<value> [default: 60] Time to live for messages in seconds (default: 60)
--region=<value> [default: us-east-1-a] Region for the queue (e.g., us-east-1-a, eu-west-1-a)
--ttl=<value> [default: 60] Time to live for messages in seconds (max: 3600)

DESCRIPTION
Create a queue

EXAMPLES
$ ably queues create --name "my-queue"

$ ably queues create --name "my-queue" --ttl 3600 --max-length 100000
Copy link
Contributor Author

@sacOO7 sacOO7 Mar 23, 2026

Choose a reason for hiding this comment

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

We are showing max values for both ttl and max-length, so updated to

ably queues create --name "my-queue" --ttl 300 --max-length 5000

$ ably queues create --name "my-queue" --ttl 300 --max-length 5000

$ ably queues create --name "my-queue" --region "eu-west-1-a" --app "My App"

Expand Down Expand Up @@ -3807,7 +3817,7 @@ FLAGS
-v, --verbose Output verbose logs
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--json Output in JSON format
--order=<option> [default: newestFirst] Query direction: oldestFirst or newestFirst (default: newestFirst)
--order=<option> [default: newestFirst] Order of results: oldestFirst or newestFirst
<options: oldestFirst|newestFirst>
--pretty-json Output in colorized JSON format
--show-metadata Display message metadata if available
Expand Down Expand Up @@ -3988,8 +3998,8 @@ ARGUMENTS
TEXT The message text to send

FLAGS
-c, --count=<value> [default: 1] Number of messages to send (default: 1)
-d, --delay=<value> [default: 40] Delay between messages in milliseconds (default: 40ms, max 25 msgs/sec)
-c, --count=<value> [default: 1] Number of messages to send
-d, --delay=<value> [default: 40] Delay between messages in milliseconds (max 25 msgs/sec)
-v, --verbose Output verbose logs
--client-id=<value> Overrides any default client ID when using API authentication. Use "none" to explicitly set
no client ID. Not applicable when using token authentication.
Expand Down Expand Up @@ -4127,17 +4137,15 @@ Get current occupancy metrics for a room

```
USAGE
$ ably rooms occupancy get ROOM [-v] [--json | --pretty-json] [--client-id <value>]
$ ably rooms occupancy get ROOM [-v] [--json | --pretty-json]

ARGUMENTS
ROOM Room to get occupancy for

FLAGS
-v, --verbose Output verbose logs
--client-id=<value> Overrides any default client ID when using API authentication. Use "none" to explicitly set
no client ID. Not applicable when using token authentication.
--json Output in JSON format
--pretty-json Output in colorized JSON format
-v, --verbose Output verbose logs
--json Output in JSON format
--pretty-json Output in colorized JSON format

DESCRIPTION
Get current occupancy metrics for a room
Expand Down
27 changes: 27 additions & 0 deletions src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,33 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
this.isWebCliMode = isWebCliMode();
}

protected validateChannelName(
args: Record<string, unknown>,
flags: BaseFlags,
argv?: string[],
): void {
const name = (args.channel ?? args.channels ?? "") as string;
if (!name.trim()) {
this.fail("Channel name cannot be empty", flags, "parse");
}

if (argv) {
if (argv.length === 0) {
this.fail(
"At least one channel name is required",
flags,
"channelSubscribe",
);
}

for (const n of argv) {
if (!n.trim()) {
this.fail("Channel name cannot be empty", flags, "parse");
}
}
}
}

protected isAnonymousWebMode(): boolean {
// In web CLI mode, the server sets ABLY_ANONYMOUS_USER_MODE when no access token is available
return this.isWebCliMode && process.env.ABLY_ANONYMOUS_USER_MODE === "true";
Expand Down
38 changes: 35 additions & 3 deletions src/chat-base-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as Ably from "ably";
import { ChatClient, Room, RoomStatus } from "@ably/chat";
import { ChatClient, LogLevel, Room, RoomStatus } from "@ably/chat";

import { AblyBaseCommand } from "./base-command.js";
import { productApiFlags } from "./flags.js";
Expand All @@ -17,6 +17,33 @@ export abstract class ChatBaseCommand extends AblyBaseCommand {
private _chatClient: ChatClient | null = null;
private _cleanupTimeout: NodeJS.Timeout | undefined;

protected validateRoomName(
args: Record<string, unknown>,
flags: BaseFlags,
argv?: string[],
): void {
const name = (args.room ?? args.rooms ?? "") as string;
if (!name.trim()) {
this.fail("Room name cannot be empty", flags, "parse");
}

if (argv) {
if (argv.length === 0) {
this.fail(
"At least one room name is required",
flags,
"roomMessageSubscribe",
);
}

for (const n of argv) {
if (!n.trim()) {
this.fail("Room name cannot be empty", flags, "parse");
}
}
}
}

/**
* Override getClientOptions to disable binary protocol for Chat commands.
* The Chat API uses realtime.request() for REST calls, and binary (MsgPack)
Expand Down Expand Up @@ -103,8 +130,13 @@ export abstract class ChatBaseCommand extends AblyBaseCommand {
);
}

// Use the Ably client to create the Chat client
return (this._chatClient = new ChatClient(realtimeClient));
// Use the Ably client to create the Chat client.
// Suppress Chat SDK's console logger by default; enable it when --verbose is active
// so that additional diagnostic output is available on demand.
const chatOptions = flags.verbose
? { logLevel: LogLevel.Info }
: { logLevel: LogLevel.Silent };
return (this._chatClient = new ChatClient(realtimeClient, chatOptions));
}

protected setupRoomStatusHandler(
Expand Down
3 changes: 3 additions & 0 deletions src/commands/apps/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default class AppsCreateCommand extends ControlBaseCommand {

async run(): Promise<void> {
const { flags } = await this.parse(AppsCreateCommand);
if (!flags.name?.trim()) {
this.fail("App name cannot be empty", flags, "parse");
}

try {
const controlApi = this.createControlApi(flags);
Expand Down
3 changes: 3 additions & 0 deletions src/commands/apps/rules/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export default class RulesCreateCommand extends ControlBaseCommand {

async run(): Promise<void> {
const { flags } = await this.parse(RulesCreateCommand);
if (!flags.name?.trim()) {
this.fail("Rule name cannot be empty", flags, "parse");
}

const appId = await this.requireAppId(flags);

Expand Down
2 changes: 1 addition & 1 deletion src/commands/auth/issue-ably-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class IssueAblyTokenCommand extends AblyBaseCommand {

ttl: Flags.integer({
default: 3600, // 1 hour
description: "Time to live in seconds (default: 3600, 1 hour)",
description: "Time to live in seconds",
}),
};

Expand Down
2 changes: 1 addition & 1 deletion src/commands/auth/issue-jwt-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default class IssueJwtTokenCommand extends AblyBaseCommand {

ttl: Flags.integer({
default: 3600, // 1 hour
description: "Time to live in seconds (default: 3600, 1 hour)",
description: "Time to live in seconds",
}),
};

Expand Down
3 changes: 3 additions & 0 deletions src/commands/auth/keys/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export default class KeysCreateCommand extends ControlBaseCommand {

async run(): Promise<void> {
const { flags } = await this.parse(KeysCreateCommand);
if (!flags.name?.trim()) {
this.fail("Key name cannot be empty", flags, "parse");
}

const appId = await this.requireAppId(flags);

Expand Down
Loading
Loading