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
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
53 changes: 29 additions & 24 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 (default: 100)
--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 (default: 100)
--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 (default: 100)
--pretty-json Output in colorized JSON format

DESCRIPTION
List all keys in the app
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 (default: 100)
--pretty-json Output in colorized JSON format

DESCRIPTION
List all integrations
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 (default: 100)
--pretty-json Output in colorized JSON format

DESCRIPTION
List all queues
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
17 changes: 15 additions & 2 deletions src/commands/apps/list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Flags } from "@oclif/core";
import chalk from "chalk";

import { ControlBaseCommand } from "../../control-base-command.js";
import { formatLimitWarning } from "../../utils/output.js";

export default class AppsList extends ControlBaseCommand {
static override description = "List all apps in the current account";
Expand All @@ -13,6 +15,10 @@ export default class AppsList extends ControlBaseCommand {

static override flags = {
...ControlBaseCommand.globalFlags,
limit: Flags.integer({
default: 100,
description: "Maximum number of results to return (default: 100)",
}),
};

async run(): Promise<void> {
Expand All @@ -21,7 +27,9 @@ export default class AppsList extends ControlBaseCommand {
await this.runControlCommand(
flags,
async (controlApi) => {
const apps = await controlApi.listApps();
const allApps = await controlApi.listApps();
const hasMore = allApps.length > flags.limit;
const apps = allApps.slice(0, flags.limit);

// Get current app ID from config
const currentAppId = this.configManager.getCurrentAppId();
Expand All @@ -33,7 +41,7 @@ export default class AppsList extends ControlBaseCommand {
isCurrent: app.id === currentAppId,
}));

this.logJsonResult({ apps: appsWithCurrentFlag }, flags);
this.logJsonResult({ apps: appsWithCurrentFlag, hasMore }, flags);
return;
}

Expand Down Expand Up @@ -77,6 +85,11 @@ export default class AppsList extends ControlBaseCommand {

this.log(""); // Add a blank line between apps
}

if (hasMore) {
const warning = formatLimitWarning(apps.length, flags.limit, "apps");
if (warning) this.log(warning);
}
},
"Error listing apps",
);
Expand Down
24 changes: 22 additions & 2 deletions src/commands/apps/rules/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import type { Namespace } from "../../../services/control-api.js";

import { ControlBaseCommand } from "../../../control-base-command.js";
import { formatChannelRuleDetails } from "../../../utils/channel-rule-display.js";
import { formatCountLabel, formatHeading } from "../../../utils/output.js";
import {
formatCountLabel,
formatHeading,
formatLimitWarning,
} from "../../../utils/output.js";

interface ChannelRuleOutput {
authenticated: boolean;
Expand Down Expand Up @@ -40,6 +44,10 @@ export default class RulesListCommand extends ControlBaseCommand {
description: "The app ID or name (defaults to current app)",
required: false,
}),
limit: Flags.integer({
default: 100,
description: "Maximum number of results to return (default: 100)",
}),
};

async run(): Promise<void> {
Expand All @@ -48,12 +56,15 @@ export default class RulesListCommand extends ControlBaseCommand {

try {
const controlApi = this.createControlApi(flags);
const namespaces = await controlApi.listNamespaces(appId);
const allNamespaces = await controlApi.listNamespaces(appId);
const hasMore = allNamespaces.length > flags.limit;
const namespaces = allNamespaces.slice(0, flags.limit);

if (this.shouldOutputJson(flags)) {
this.logJsonResult(
{
appId,
hasMore,
rules: namespaces.map(
(rule: Namespace): ChannelRuleOutput => ({
authenticated: rule.authenticated || false,
Expand Down Expand Up @@ -102,6 +113,15 @@ export default class RulesListCommand extends ControlBaseCommand {

this.log(""); // Add a blank line between rules
});

if (hasMore) {
const warning = formatLimitWarning(
namespaces.length,
flags.limit,
"channel rules",
);
if (warning) this.log(warning);
}
}
} catch (error) {
this.fail(error, flags, "ruleList", { appId });
Expand Down
15 changes: 14 additions & 1 deletion src/commands/auth/keys/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { formatCapabilities } from "../../../utils/key-display.js";
import {
formatHeading,
formatLabel,
formatLimitWarning,
formatResource,
} from "../../../utils/output.js";

Expand All @@ -25,6 +26,10 @@ export default class KeysListCommand extends ControlBaseCommand {
description: "The app ID or name (defaults to current app)",
env: "ABLY_APP_ID",
}),
limit: Flags.integer({
default: 100,
description: "Maximum number of results to return (default: 100)",
}),
};

async run(): Promise<void> {
Expand All @@ -39,7 +44,9 @@ export default class KeysListCommand extends ControlBaseCommand {

try {
const controlApi = this.createControlApi(flags);
const keys = await controlApi.listKeys(appId);
const allKeys = await controlApi.listKeys(appId);
const hasMore = allKeys.length > flags.limit;
const keys = allKeys.slice(0, flags.limit);

// Get the current key name for highlighting (app_id.key_Id)
const currentKeyId = this.configManager.getKeyId(appId);
Expand All @@ -63,6 +70,7 @@ export default class KeysListCommand extends ControlBaseCommand {
this.logJsonResult(
{
appId,
hasMore,
keys: keysWithCurrent,
},
flags,
Expand Down Expand Up @@ -100,6 +108,11 @@ export default class KeysListCommand extends ControlBaseCommand {

this.log("");
}

if (hasMore) {
const warning = formatLimitWarning(keys.length, flags.limit, "keys");
if (warning) this.log(warning);
}
}
} catch (error) {
this.fail(error, flags, "keyList", { appId });
Expand Down
Loading
Loading