Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .claude/skills/ably-new-command/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,34 @@ static override flags = {
};
```

For history and list commands, use pagination utilities:
```typescript
import {
buildPaginationNext,
collectPaginatedResults,
formatPaginationWarning,
} from "../../utils/pagination.js";

// In run():
const { items, hasMore, pagesConsumed } = await collectPaginatedResults(firstPage, flags.limit);
const paginationWarning = formatPaginationWarning(pagesConsumed, items.length, true); // true for history (billable)
if (paginationWarning && !this.shouldOutputJson(flags)) {
this.log(paginationWarning);
}
// For JSON output:
const next = buildPaginationNext(hasMore, lastTimestamp); // lastTimestamp only for history commands
this.logJsonResult({ [domainKey]: items, hasMore, ...(next && { next }) }, flags);
```

For list commands that need client-side filtering (e.g., rooms/spaces with prefix), use `collectFilteredPaginatedResults`:
```typescript
import { collectFilteredPaginatedResults } from "../../utils/pagination.js";

const { items, hasMore, pagesConsumed } = await collectFilteredPaginatedResults(
firstPage, flags.limit, (item) => item.name.startsWith(prefix),
);
```

### Command metadata

```typescript
Expand Down
60 changes: 58 additions & 2 deletions .claude/skills/ably-new-command/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ See `src/commands/rooms/messages/update.ts` and `src/commands/rooms/messages/del
## History Pattern

```typescript
import { collectPaginatedResults, formatPaginationWarning } from "../../utils/pagination.js";

async run(): Promise<void> {
const { args, flags } = await this.parse(MyHistoryCommand);

Expand All @@ -222,14 +224,24 @@ async run(): Promise<void> {
};

const history = await channel.history(historyParams);
const messages = history.items;
const { items: messages, hasMore, pagesConsumed } = await collectPaginatedResults(history, flags.limit);

const paginationWarning = formatPaginationWarning(pagesConsumed, messages.length);
if (paginationWarning && !this.shouldOutputJson(flags)) {
this.log(paginationWarning);
}

if (this.shouldOutputJson(flags)) {
// Plural domain key for collections, optional metadata alongside
this.logJsonResult({ messages, total: messages.length }, flags);
this.logJsonResult({ messages, hasMore, total: messages.length }, flags);
} else {
this.log(formatSuccess(`Found ${messages.length} messages.`));
// Display each message using multi-line labeled blocks

if (hasMore) {
const warning = formatLimitWarning(messages.length, flags.limit, "messages");
if (warning) this.log(warning);
}
}
} catch (error) {
this.fail(error, flags, "history", { channel: args.channel });
Expand Down Expand Up @@ -407,11 +419,55 @@ async run(): Promise<void> {
}
```

**Product API list with pagination** (e.g., `push devices list`, `channels list`) β€” use `collectPaginatedResults`:
```typescript
import { buildPaginationNext, collectPaginatedResults, formatPaginationWarning } from "../../utils/pagination.js";

async run(): Promise<void> {
const { flags } = await this.parse(MyListCommand);

try {
const rest = await this.createAblyRestClient(flags);
if (!rest) return;

const firstPage = await rest.someResource.list({ limit: flags.limit });
const { items, hasMore, pagesConsumed } = await collectPaginatedResults(firstPage, flags.limit);

const paginationWarning = formatPaginationWarning(pagesConsumed, items.length);
if (paginationWarning && !this.shouldOutputJson(flags)) {
this.log(paginationWarning);
}

if (this.shouldOutputJson(flags)) {
const next = buildPaginationNext(hasMore);
this.logJsonResult({ items, hasMore, ...(next && { next }) }, flags);
} else {
this.log(`Found ${items.length} items:\n`);
for (const item of items) {
this.log(formatHeading(`Item ID: ${item.id}`));
this.log(` ${formatLabel("Type")} ${item.type}`);
this.log("");
}

if (hasMore) {
const warning = formatLimitWarning(items.length, flags.limit, "items");
if (warning) this.log(warning);
}
}
} catch (error) {
this.fail(error, flags, "listItems");
}
}
```

Key conventions for list output:
- `formatResource()` is for inline resource name references, not for record headings
- `formatHeading()` is for record heading lines that act as visual separators between multi-field records
- `formatLabel(text)` for field labels in detail lines (automatically appends `:`)
- `formatSuccess()` is not used in list commands β€” it's for confirming an action completed
- `formatLimitWarning()` should only be shown when `hasMore` is true β€” it means there are more results beyond the limit
- Always include `hasMore` and `next` in JSON output for paginated commands. `next` provides continuation hints (and `start` timestamp for history commands)
- Use `collectPaginatedResults()` for SDK paginated results and `collectFilteredPaginatedResults()` when a client-side filter is applied across pages

---

Expand Down
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,11 @@ All output helpers use the `format` prefix and are exported from `src/utils/outp
- **Headings**: `formatHeading("Record ID: " + id)` β€” bold, for record headings in list output.
- **Index**: `formatIndex(n)` β€” dim bracketed number `[n]`, for history/list ordering.
- **Count labels**: `formatCountLabel(n, "message")` β€” cyan count + pluralized label.
- **Limit warnings**: `formatLimitWarning(count, limit, "items")` β€” yellow warning if results truncated.
- **Limit warnings**: `formatLimitWarning(count, limit, "items")` β€” yellow warning if results truncated. Only show when `hasMore === true`.
- **Pagination collection**: `collectPaginatedResults(firstPage, limit)` β€” walks cursor-based pages until `limit` items are collected. Returns `{ items, hasMore, pagesConsumed }`. Use for both SDK and HTTP paginated commands.
- **Filtered pagination**: `collectFilteredPaginatedResults(firstPage, limit, filter, maxPages?)` β€” same as above but applies a client-side filter. Use for rooms/spaces list where channels need prefix filtering. `maxPages` (default: 20) prevents runaway requests.
- **Pagination warning**: `formatPaginationWarning(pagesConsumed, itemCount, isBillable?)` β€” shows "Fetched N pages" when `pagesConsumed > 1`. Pass `isBillable: true` for history commands (billable API calls). Guard with `!this.shouldOutputJson(flags)`.
- **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 and `this.logJsonEvent(data, flags)` for streaming events. The envelope adds three 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 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`.
Expand Down Expand Up @@ -294,7 +298,7 @@ When adding COMMANDS sections in `src/help.ts`, use `chalk.bold()` for headers,
### Flag conventions
- All flags kebab-case: `--my-flag` (never camelCase)
- `--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 (default: N)"`
- `--limit`: `"Maximum number of results to return"` with `min: 1` (oclif shows `[default: N]` automatically, don't duplicate in description)
- `--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"`).
Expand Down
81 changes: 43 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,12 +543,13 @@ List all apps in the current account

```
USAGE
$ ably apps list [-v] [--json | --pretty-json]
$ ably apps list [-v] [--json | --pretty-json] [--limit <value>]

FLAGS
-v, --verbose Output verbose logs
--json Output in JSON format
--pretty-json Output in colorized JSON format
-v, --verbose Output verbose logs
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
List all apps in the current account
Expand Down Expand Up @@ -676,13 +677,14 @@ List channel rules for an app

```
USAGE
$ ably apps rules list [-v] [--json | --pretty-json] [--app <value>]
$ ably apps rules list [-v] [--json | --pretty-json] [--app <value>] [--limit <value>]

FLAGS
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--pretty-json Output in colorized JSON format
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
List channel rules for an app
Expand Down Expand Up @@ -1063,13 +1065,14 @@ List all keys in the app

```
USAGE
$ ably auth keys list [-v] [--json | --pretty-json] [--app <value>]
$ ably auth keys list [-v] [--json | --pretty-json] [--app <value>] [--limit <value>]

FLAGS
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--pretty-json Output in colorized JSON format
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
List all keys in the app
Expand Down Expand Up @@ -1453,7 +1456,7 @@ ARGUMENTS
FLAGS
-v, --verbose Output verbose logs
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
Expand Down Expand Up @@ -1694,7 +1697,7 @@ FLAGS
<options: backwards|forwards>
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--json Output in JSON format
--limit=<value> [default: 50] Maximum number of results to return (default: 50)
--limit=<value> [default: 50] Maximum number of results to return
--pretty-json Output in colorized JSON format
--start=<value> Start time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")

Expand Down Expand Up @@ -1757,7 +1760,7 @@ FLAGS
-p, --prefix=<value> Filter channels by prefix
-v, --verbose Output verbose logs
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
Expand Down Expand Up @@ -2407,13 +2410,14 @@ List all integrations

```
USAGE
$ ably integrations list [-v] [--json | --pretty-json] [--app <value>]
$ ably integrations list [-v] [--json | --pretty-json] [--app <value>] [--limit <value>]

FLAGS
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--pretty-json Output in colorized JSON format
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
List all integrations
Expand Down Expand Up @@ -2609,7 +2613,7 @@ FLAGS
<options: backwards|forwards>
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format
--start=<value> Start time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")

Expand Down Expand Up @@ -2682,7 +2686,7 @@ FLAGS
<options: backwards|forwards>
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format
--start=<value> Start time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")

Expand Down Expand Up @@ -2743,7 +2747,7 @@ FLAGS
<options: backwards|forwards>
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format
--start=<value> Start time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")

Expand Down Expand Up @@ -2933,7 +2937,7 @@ FLAGS
--client-id=<value> Filter by client ID
--device-id=<value> Filter by device ID
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
Expand All @@ -2960,7 +2964,7 @@ USAGE
FLAGS
-v, --verbose Output verbose logs
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
Expand Down Expand Up @@ -3298,7 +3302,7 @@ FLAGS
--client-id=<value> Filter by client ID
--device-id=<value> Filter by device ID
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format
--state=<option> Filter by device state
<options: ACTIVE|FAILING|FAILED>
Expand Down Expand Up @@ -3570,13 +3574,14 @@ List all queues

```
USAGE
$ ably queues list [-v] [--json | --pretty-json] [--app <value>]
$ ably queues list [-v] [--json | --pretty-json] [--app <value>] [--limit <value>]

FLAGS
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--pretty-json Output in colorized JSON format
-v, --verbose Output verbose logs
--app=<value> The app ID or name (defaults to current app)
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
List all queues
Expand Down Expand Up @@ -3632,7 +3637,7 @@ FLAGS
-p, --prefix=<value> Filter rooms by prefix
-v, --verbose Output verbose logs
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
Expand Down Expand Up @@ -3723,7 +3728,7 @@ ARGUMENTS
ROOM The room to get message history from

FLAGS
-l, --limit=<value> [default: 50] Maximum number of results to return (default: 50)
-l, --limit=<value> [default: 50] Maximum number of results to return
-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
Expand Down Expand Up @@ -4548,7 +4553,7 @@ FLAGS
-p, --prefix=<value> Filter spaces by prefix
-v, --verbose Output verbose logs
--json Output in JSON format
--limit=<value> [default: 100] Maximum number of results to return (default: 100)
--limit=<value> [default: 100] Maximum number of results to return
--pretty-json Output in colorized JSON format

DESCRIPTION
Expand Down Expand Up @@ -4978,7 +4983,7 @@ FLAGS
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--interval=<value> [default: 6] Polling interval in seconds (only used with --live)
--json Output in JSON format
--limit=<value> [default: 10] Maximum number of results to return (default: 10)
--limit=<value> [default: 10] Maximum number of results to return
--live Subscribe to live stats updates (uses minute interval)
--pretty-json Output in colorized JSON format
--start=<value> Start time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
Expand Down Expand Up @@ -5028,7 +5033,7 @@ FLAGS
--end=<value> End time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
--interval=<value> [default: 6] Polling interval in seconds (only used with --live)
--json Output in JSON format
--limit=<value> [default: 10] Maximum number of results to return (default: 10)
--limit=<value> [default: 10] Maximum number of results to return
--live Subscribe to live stats updates (uses minute interval)
--pretty-json Output in colorized JSON format
--start=<value> Start time as ISO 8601, Unix ms, or relative (e.g., "1h", "30m", "2d")
Expand Down
1 change: 1 addition & 0 deletions docs/Project-Structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ This document outlines the directory structure of the Ably CLI project.
β”‚ β”œβ”€β”€ history.ts # History query parameter builder
β”‚ β”œβ”€β”€ message.ts # Message interpolation ({{.Count}}, {{.Timestamp}})
β”‚ β”œβ”€β”€ output.ts # Output helpers (progress, success, resource, etc.)
β”‚ β”œβ”€β”€ pagination.ts # Generic pagination utilities (collectPaginatedResults, collectFilteredPaginatedResults)
β”‚ β”œβ”€β”€ prompt-confirmation.ts # Y/N confirmation prompts
β”‚ β”œβ”€β”€ readline-helper.ts # Readline utilities for interactive mode
β”‚ β”œβ”€β”€ sigint-exit.ts # SIGINT/Ctrl+C handling (exit code 130)
Expand Down
Loading
Loading