Skip to content

[DX-960] Add automatic pagination support to list and history commands#170

Draft
umair-ably wants to merge 3 commits intoDX-919-pushfrom
DX-960-pagination
Draft

[DX-960] Add automatic pagination support to list and history commands#170
umair-ably wants to merge 3 commits intoDX-919-pushfrom
DX-960-pagination

Conversation

@umair-ably
Copy link
Contributor

@umair-ably umair-ably commented Mar 13, 2026

Summary

  • Introduces a shared pagination utility (src/utils/pagination.ts) with collectPaginatedResults, collectHttpPaginatedResults, and collectFilteredPaginatedResults that automatically follow cursor-based pages until the user's --limit is satisfied
  • Retrofits all 16 list/history commands to use automatic pagination instead of returning only the first page of results
  • Adds hasMore to JSON output envelopes so consumers can detect truncation programmatically
  • Shows a limit warning only when there genuinely are more results (previously shown whenever count == limit)

Motivation

Previously, commands like ably channels list --limit 200 silently returned only the first page (typically 100 items) even when more existed. Users had no way to get all results without scripting the SDK directly. Now the CLI transparently fetches as many pages as needed to honour --limit.

Review strategy

Order What to review Files What to look for
1 Pagination utility (core logic) src/utils/pagination.ts, test/unit/utils/pagination.test.ts Correctness of page-walking loop, truncation, hasMore edge cases, filtered variant with maxPages safety cap
2 SDK paginated commands src/commands/channels/history.ts, src/commands/logs/history.ts, src/commands/logs/connection-lifecycle/history.ts, src/commands/logs/push/history.ts, src/commands/rooms/messages/history.ts Consistent integration pattern: collectPaginatedResults(firstPage, flags.limit), multi-page warning, hasMore in JSON
3 HTTP paginated commands src/commands/channels/list.ts, src/commands/push/channels/list-channels.ts, src/commands/push/channels/list.ts, src/commands/push/devices/list.ts Same pattern but using collectHttpPaginatedResults
4 Filtered pagination (rooms/spaces) src/commands/rooms/list.ts, src/commands/spaces/list.ts Uses collectFilteredPaginatedResults because client-side filtering (chat channels, spaces) means raw items != final items
5 Control API commands (no cursor pagination) src/commands/apps/list.ts, src/commands/apps/rules/list.ts, src/commands/auth/keys/list.ts, src/commands/integrations/list.ts, src/commands/queues/list.ts Simple client-side slice with --limit flag added; no pagination utility needed since Control API returns all results
6 Test updates test/unit/commands/*/..., test/helpers/mock-ably-rest.ts, test/helpers/mock-ably-chat.ts Mock helpers now return proper PaginatedResult shape with hasNext()/next(); tests verify hasMore and limit behaviour
7 Output helpers & docs src/utils/output.ts, README.md, .claude/skills/... formatLimitWarning only shown when hasMore is true; README auto-regenerated

Test plan

  • pnpm test:unit passes — all pagination and command tests green
  • Verify ably channels list --limit 5 on an app with >5 active channels returns exactly 5 and shows the limit warning
  • Verify ably channels history <channel> --limit 200 fetches multiple pages transparently
  • Verify --json output includes "hasMore": true/false
  • Verify rooms/spaces list correctly deduplicates across pages

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

New Features

  • Added --limit option to list commands (apps, rules, auth keys, integrations, queues, channels, rooms, and spaces) with a default of 100 results per request.
  • JSON responses for paginated commands now include a hasMore field indicating whether additional results are available.

Improvements

  • Enhanced pagination handling across history and list commands for more efficient data retrieval.
  • Added warnings when results are truncated due to limit settings to alert users of additional available data.

- Guard deprecation warning in set-apns-p12 for JSON mode
- Use formatWarning() and formatClientId() in push channels list
- Validate --data flag parses to a JSON object in push devices save
- Validate all JSON flags (--recipient, --payload, --data, --apns, --fcm, --web) parse to objects in push publish
- Let E2E test setup failures propagate instead of silently skipping
@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Mar 13, 2026 1:46am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4cd0bc36-1c60-4408-abcf-b59683ce0d10

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Introduces comprehensive pagination support across the Ably CLI by adding a new pagination utility module (src/utils/pagination.ts) with helpers for consuming paginated results, adding limit flags to list commands (default 100), and updating 15+ history and list commands to collect results across pages, include hasMore metadata in JSON output, and emit warnings when multiple pages are consumed.

Changes

Cohort / File(s) Summary
Pagination Core Utilities
src/utils/pagination.ts, test/unit/utils/pagination.test.ts
New pagination module exporting collectPaginatedResults, collectHttpPaginatedResults, and collectFilteredPaginatedResults functions to aggregate items across paginated responses; includes comprehensive test coverage for multi-page collection, filtering, and edge cases.
Test Mock Infrastructure
test/helpers/mock-ably-chat.ts, test/helpers/mock-ably-rest.ts
Added history mock to MockRoomMessages and new createMockPaginatedResult helper to simulate paginated API responses across all test scenarios.
List Commands with Limit Flags
src/commands/apps/list.ts, src/commands/apps/rules/list.ts, src/commands/auth/keys/list.ts, src/commands/integrations/list.ts, src/commands/queues/list.ts
Added limit flag (default 100) to each command; computes hasMore by comparing total results to limit; includes hasMore in JSON output and emits formatLimitWarning in non-JSON output when results exceed limit.
History Commands with Pagination
src/commands/channels/history.ts, src/commands/logs/connection-lifecycle/history.ts, src/commands/logs/history.ts, src/commands/logs/push/history.ts, src/commands/rooms/messages/history.ts
Integrated collectPaginatedResults to extract messages, hasMore, and pagesConsumed; emits warning when pagesConsumed > 1 and not JSON output; includes hasMore in JSON results and conditionally logs limit warning only when more data exists.
List Commands with Pagination Helpers
src/commands/channels/list.ts, src/commands/push/channels/list-channels.ts, src/commands/push/channels/list.ts, src/commands/push/devices/list.ts
Replaced direct item extraction with collectPaginatedResults or collectHttpPaginatedResults; logs warning for multi-page fetches; includes hasMore in JSON output; conditionalizes limit warning to only display when additional results exist.
Filtered Pagination Commands
src/commands/rooms/list.ts, src/commands/spaces/list.ts
Integrated collectFilteredPaginatedResults for client-side deduplication/filtering; implements seenItems tracking to prevent duplicates; caps API request sizing to optimize pagination; includes hasMore in JSON and conditional warnings.
Test Updates: History/Connection
test/unit/commands/channels/history.test.ts, test/unit/commands/logs/connection-lifecycle/history.test.ts, test/unit/commands/logs/history.test.ts, test/unit/commands/logs/push/history.test.ts, test/unit/commands/rooms/messages/history.test.ts
Wrapped mocked history responses with createMockPaginatedResult to simulate paginated API structure across all test cases.
Test Updates: List Commands
test/unit/commands/channels/list.test.ts, test/unit/commands/push/channels/list-channels.test.ts, test/unit/commands/push/channels/list.ts, test/unit/commands/push/devices/list.test.ts, test/unit/commands/rooms/list.test.ts, test/unit/commands/rooms/messages.test.ts, test/unit/commands/rooms/messages/history.test.ts, test/unit/commands/spaces/list.test.ts
Replaced direct items arrays in mocks with createMockPaginatedResult wrapper; introduced helper functions (e.g., createMockChatChannelItems) to generate deterministic test data.
Documentation
.claude/skills/ably-new-command/references/patterns.md, README.md
Updated pattern documentation with pagination examples and hasMore handling; added --limit <value> flag documentation to multiple list commands in README with default values and descriptions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • denissellu
  • jamiehenson

Poem

🐰 With pages galore, we now can bound,
Across paginated results, safe and sound!
hasMore flags gleam, limits all neat,
Pagination helpers make the code complete! 🎯

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'DX-960 Add automatic pagination support to list and history commands' clearly summarizes the main change: adding pagination functionality to list and history commands throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch DX-960-pagination
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@umair-ably umair-ably changed the base branch from main to DX-919-push March 13, 2026 01:00
@umair-ably umair-ably requested a review from Copilot March 13, 2026 01:00
@umair-ably
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@umair-ably umair-ably changed the title [DX-960] Adds Pagination Support Add automatic pagination support to list and history commands Mar 13, 2026
@umair-ably umair-ably changed the title Add automatic pagination support to list and history commands [DX-960] Add automatic pagination support to list and history commands Mar 13, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds shared pagination helpers and wires them into multiple CLI list/history commands so outputs respect --limit while correctly indicating when additional results exist.

Changes:

  • Introduces collectPaginatedResults, collectHttpPaginatedResults, and collectFilteredPaginatedResults utilities plus unit tests.
  • Updates multiple commands to fetch additional pages when needed and to emit hasMore (and optional warnings about extra page fetches).
  • Updates test mocks and command unit tests to return PaginatedResult-like objects.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/utils/pagination.ts Adds generic pagination collection helpers used by commands.
src/commands/channels/list.ts Uses HTTP pagination collector for /channels enumeration; returns hasMore.
src/commands/channels/history.ts Uses pagination collector for channel history; returns hasMore and limit warnings.
src/commands/logs/history.ts Uses pagination collector for app log history; returns hasMore.
src/commands/logs/push/history.ts Uses pagination collector for push log history; returns hasMore.
src/commands/logs/connection-lifecycle/history.ts Uses pagination collector for connection lifecycle logs; returns hasMore.
src/commands/push/devices/list.ts Uses pagination collector for device registrations list; returns hasMore.
src/commands/push/channels/list.ts Uses pagination collector for channel subscriptions list; returns hasMore.
src/commands/push/channels/list-channels.ts Uses pagination collector for listChannels; returns hasMore.
src/commands/rooms/messages/history.ts Uses pagination collector for chat message history; returns hasMore.
src/commands/rooms/list.ts Uses filtered pagination + dedupe for chat rooms enumeration; returns hasMore.
src/commands/spaces/list.ts Uses filtered pagination + dedupe for spaces enumeration; returns hasMore.
src/commands/apps/list.ts Adds --limit and hasMore for apps listing (client-side slice).
src/commands/apps/rules/list.ts Adds --limit and hasMore for namespaces/rules listing (client-side slice).
src/commands/auth/keys/list.ts Adds --limit and hasMore for keys listing (client-side slice).
src/commands/integrations/list.ts Adds --limit and hasMore for integrations listing (client-side slice).
src/commands/queues/list.ts Adds --limit and hasMore for queues listing (client-side slice).
test/helpers/mock-ably-rest.ts Adds createMockPaginatedResult and updates default mocks to return paginated-like results.
test/helpers/mock-ably-chat.ts Adds a history mock on room messages.
test/unit/utils/pagination.test.ts New unit tests for pagination utilities.
test/unit/commands/channels/list.test.ts Updates mocks to use paginated-like results.
test/unit/commands/channels/history.test.ts Updates mocks to use paginated-like results.
test/unit/commands/logs/history.test.ts Updates mocks to use paginated-like results and simulates hasMore.
test/unit/commands/logs/push/history.test.ts Updates mocks to use paginated-like results.
test/unit/commands/logs/connection-lifecycle/history.test.ts Updates mocks to use paginated-like results.
test/unit/commands/push/devices/list.test.ts Updates mocks to use paginated-like results.
test/unit/commands/push/channels/list.test.ts Updates mocks to use paginated-like results.
test/unit/commands/push/channels/list-channels.test.ts Updates mocks to use paginated-like results.
test/unit/commands/rooms/messages.test.ts Updates history mocks to use paginated-like results.
test/unit/commands/rooms/messages/history.test.ts Updates history mocks to use paginated-like results.
test/unit/commands/rooms/list.test.ts Updates mocks to use paginated-like results.
test/unit/commands/spaces/list.test.ts Updates mocks to use paginated-like results.
README.md Documents new --limit flags for control-plane list commands.
.claude/skills/ably-new-command/references/patterns.md Documents new pagination patterns for new commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/spaces/list.ts (1)

129-143: ⚠️ Potential issue | 🟠 Major

Don't print a false negative when hasMore is already true.

With the new pagination path, collectFilteredPaginatedResults() can stop early and still return hasMore: true. If that happens before any matching spaces are found, Line 141 still prints No active spaces found. and exits even though later pages may contain matches. Gate this branch on !hasMore, or raise the scan cap for this command.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/list.ts` around lines 129 - 143, The command prints "No
active spaces found." even when pagination indicates more pages exist; update
the early-exit condition in the spaces list flow to only log and return when
there are genuinely no further pages to scan. Specifically, in the branch that
checks limitedSpaces.length === 0, also require !hasMore (from the
collectFilteredPaginatedResults result) before logging and returning;
alternatively, if intended, raise the scan cap used by
collectFilteredPaginatedResults so it scans further, but the minimal fix is to
gate the no-results branch on !hasMore to avoid false negatives.
🧹 Nitpick comments (12)
test/helpers/mock-ably-chat.ts (1)

240-244: Consider using createMockPaginatedResult for consistency.

The inline paginated result mock is missing isLast(), first(), and current() methods that createMockPaginatedResult in test/helpers/mock-ably-rest.ts provides. While the current shape works for the pagination utilities (which only use items, hasNext(), next()), using the shared helper ensures interface consistency and prevents potential test failures if other code expects the full shape.

♻️ Proposed refactor using createMockPaginatedResult

Add the import at the top of the file:

import { createMockPaginatedResult } from "./mock-ably-rest.js";

Then update the mock:

-    history: vi.fn().mockResolvedValue({
-      items: [],
-      hasNext: () => false,
-      next: async () => null,
-    }),
+    history: vi.fn().mockResolvedValue(createMockPaginatedResult([])),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/helpers/mock-ably-chat.ts` around lines 240 - 244, Replace the ad-hoc
history mock with the shared helper to ensure full paginated result shape:
import and use createMockPaginatedResult from mock-ably-rest and return
createMockPaginatedResult([]) from the history mock (the current
vi.fn().mockResolvedValue({...}) block). This ensures methods like isLast(),
first(), and current() are present alongside items, hasNext(), and next(),
keeping the test utilities and expected interfaces consistent.
test/helpers/mock-ably-rest.ts (1)

254-280: Make paginated test helper support arbitrary page chains.

createMockPaginatedResult currently accepts only one explicit nextPage, which makes it hard to model 3+ non-empty pages in tests. Expanding this helper to accept a page list would improve coverage for deeper pagination flows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/helpers/mock-ably-rest.ts` around lines 254 - 280, The helper
createMockPaginatedResult currently only models a single nextPage; change its
API to accept an array of pages (e.g., items: T[], pages?: Array<{items: T[];
hasNext?: boolean}>) or a single pages: Array<T[]> so you can build an arbitrary
chain, then construct the linked results for each page so next() returns the
following page's result or null, hasNext()/isLast() reflect whether a subsequent
page exists, and first()/current() still resolve to the appropriate result;
implement this by mapping the input pages into result objects up front and
wiring each result.next to return the next mapped result (or null) to support 3+
non-empty pages while leaving the public method names
(createMockPaginatedResult, next, hasNext, isLast, first, current) unchanged.
test/unit/commands/push/devices/list.test.ts (1)

70-85: Add assertions for pagination metadata and a multi-page case.

The test now uses paginated mocks, but it doesn’t assert the new pagination contract (hasMore) or exercise the multi-page branch (pagesConsumed > 1 warning path). Adding those assertions will prevent regressions in the new pagination behavior.

💡 Minimal assertion update
       const result = JSON.parse(stdout);
       expect(result).toHaveProperty("type", "result");
       expect(result).toHaveProperty("success", true);
       expect(result).toHaveProperty("devices");
+      expect(result).toHaveProperty("hasMore", false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/commands/push/devices/list.test.ts` around lines 70 - 85, Update
the "should output JSON when requested" test to also assert the pagination
contract and add a new multi-page scenario: when using getMockAblyRest() /
mock.push.admin.deviceRegistrations.list with createMockPaginatedResult, assert
the parsed JSON contains the pagination metadata (e.g.,
result.pagination.hasMore is present and correct) and keep devices assertions;
add a separate test that returns a multi-page paginated mock (simulate
pagesConsumed > 1) and assert the multi-page warning path by capturing
runCommand's stderr (or combined output) contains the expected warning message
about pagesConsumed > 1. Ensure you reference the same symbols: runCommand,
getMockAblyRest, mock.push.admin.deviceRegistrations.list, and
createMockPaginatedResult.
src/commands/apps/rules/list.ts (1)

59-62: Apply pagination at the service layer if the REST API supports it.

The listNamespaces() method currently accepts only appId and fetches all namespaces, then the command truncates locally at line 61. If the /apps/{appId}/namespaces endpoint supports query parameters for pagination, update listNamespaces() to accept and pass a limit parameter, reducing unnecessary over-fetch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/apps/rules/list.ts` around lines 59 - 62, The command is
fetching all namespaces then slicing locally; instead, update the service method
listNamespaces to accept a limit (and optionally offset/page) and pass that
through to the REST call so the server paginates (e.g.,
/apps/{appId}/namespaces?limit=...). Change the call site in this file to pass
flags.limit into controlApi.listNamespaces(appId, flags.limit) and update the
control API implementation (the listNamespaces function) to accept the new
parameter and include it as a query parameter in the HTTP request; also update
any other callers of listNamespaces to provide a limit or default it to
undefined so behavior remains backward-compatible.
.claude/skills/ably-new-command/references/patterns.md (1)

162-175: Prefer stderr in the command templates for warning output.

The examples currently log warnings with this.log(...). For command patterns, use this.logToStderr(...) for warnings so generated commands follow current output-channel conventions.

♻️ Suggested template adjustment
-      this.log(formatWarning(`Fetched ${pagesConsumed} pages to retrieve ${messages.length} results. Each page incurs additional API requests.`));
+      this.logToStderr(formatWarning(`Fetched ${pagesConsumed} pages to retrieve ${messages.length} results. Each page incurs additional API requests.`));

@@
-        if (warning) this.log(warning);
+        if (warning) this.logToStderr(warning);

@@
-      this.log(formatWarning(`Fetched ${pagesConsumed} pages to retrieve ${items.length} results. Each page incurs additional API requests.`));
+      this.logToStderr(formatWarning(`Fetched ${pagesConsumed} pages to retrieve ${items.length} results. Each page incurs additional API requests.`));

@@
-        if (warning) this.log(warning);
+        if (warning) this.logToStderr(warning);
Based on learnings: In the ably/ably-cli TypeScript (oclif) codebase, warning messages should use `this.logToStderr(formatWarning("..."))` and be guarded from `--json` output paths.

Also applies to: 367-385

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/ably-new-command/references/patterns.md around lines 162 -
175, The warning outputs currently use this.log(...) even when they should go to
stderr; update the code so any warning text (the formatWarning(...) call when
pagesConsumed > 1 and the formatLimitWarning(...) call when hasMore) is emitted
with this.logToStderr(...) and only when !this.shouldOutputJson(flags); keep the
json path using this.logJsonResult({ messages, hasMore }, flags) unchanged and
ensure you reference the existing symbols pagesConsumed,
shouldOutputJson(flags), formatWarning, formatLimitWarning, hasMore,
logJsonResult, and replace this.log(...) for warnings with
this.logToStderr(...).
src/commands/channels/list.ts (1)

107-113: Use this.logToStderr() for warning output.

Consistent with other commands, warning messages should go to stderr.

♻️ Proposed fix
       if (pagesConsumed > 1 && !this.shouldOutputJson(flags)) {
-        this.log(
+        this.logToStderr(
           formatWarning(
             `Fetched ${pagesConsumed} pages to retrieve ${channels.length} results. Each page incurs additional API requests.`,
           ),
         );
       }

Based on learnings: "Always use this.logToStderr(formatWarning('...')) for warning messages."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/channels/list.ts` around lines 107 - 113, The warning currently
uses this.log(...) in the conditional that checks pagesConsumed and
shouldOutputJson; replace the this.log call with
this.logToStderr(formatWarning(...)) so warnings are written to stderr
consistently (keep the same message using pagesConsumed and channels.length and
leave the surrounding conditional intact). Update the occurrence where
formatWarning is passed to this.log in the channels listing logic to call
this.logToStderr instead, ensuring behavior matches other commands that route
warnings to stderr.
src/commands/logs/push/history.ts (1)

70-76: Use this.logToStderr() for warning output.

Same issue as other history commands - warning messages should go to stderr.

♻️ Proposed fix
       if (pagesConsumed > 1 && !this.shouldOutputJson(flags)) {
-        this.log(
+        this.logToStderr(
           formatWarning(
             `Fetched ${pagesConsumed} pages to retrieve ${messages.length} results. Each page incurs additional API requests.`,
           ),
         );
       }

Based on learnings: "Always use this.logToStderr(formatWarning('...')) for warning messages."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/logs/push/history.ts` around lines 70 - 76, The warning
currently uses this.log(...) which sends output to stdout; change it to
this.logToStderr(...) so warnings go to stderr. In the block that checks
pagesConsumed > 1 && !this.shouldOutputJson(flags), replace the call
this.log(formatWarning(...)) with this.logToStderr(formatWarning(...)) while
preserving the same message content (use pagesConsumed and messages.length as
before) so behavior is identical except the output stream.
src/commands/rooms/list.ts (2)

124-130: Use this.logToStderr() for warning output.

Consistent with other commands, warning messages should go to stderr.

♻️ Proposed fix
       if (pagesConsumed > 1 && !this.shouldOutputJson(flags)) {
-        this.log(
+        this.logToStderr(
           formatWarning(
             `Fetched ${pagesConsumed} pages to retrieve ${limitedRooms.length} results. Each page incurs additional API requests.`,
           ),
         );
       }

Based on learnings: "Always use this.logToStderr(formatWarning('...')) for warning messages."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/rooms/list.ts` around lines 124 - 130, The warning currently
uses this.log(...) in the rooms listing code (check pagesConsumed,
shouldOutputJson and the formatWarning call); change it to use
this.logToStderr(formatWarning(...)) so the warning is emitted to stderr
consistently with other commands and only when pagesConsumed > 1 &&
!this.shouldOutputJson(flags). Replace the this.log(...) call with
this.logToStderr(...) while keeping the same message text and conditional logic.

110-121: Filter predicate has side effects - consider documenting or refactoring.

The filter function mutates channel.channelId and channel.room (lines 118-119). While this works because JavaScript passes objects by reference, side effects in filter predicates are unconventional and can be surprising. The pattern is functional but worth a brief inline comment explaining the intentional mutation for room name normalization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/rooms/list.ts` around lines 110 - 121, The predicate passed to
filter mutates the RoomItem (it overwrites channel.channelId and channel.room)
which is surprising; either move the normalization out of the filter or document
the intent. Fix by turning the predicate into a pure boolean test (use
channelId/includes and regex match, check seenRooms) and then perform the
mutation in a separate mapping step (e.g., after filtering, set
channel.channelId/channel.room to the extracted roomName), or if you keep the
in-filter mutation, add a concise inline comment above the predicate explaining
that mutation of channel.channelId and channel.room is intentional for room name
normalization (references: RoomItem, seenRooms, channel.channelId,
channel.room).
src/commands/logs/connection-lifecycle/history.ts (1)

69-75: Use this.logToStderr() for warning output.

Same issue as other history commands - warning messages should go to stderr.

♻️ Proposed fix
       if (pagesConsumed > 1 && !this.shouldOutputJson(flags)) {
-        this.log(
+        this.logToStderr(
           formatWarning(
             `Fetched ${pagesConsumed} pages to retrieve ${messages.length} results. Each page incurs additional API requests.`,
           ),
         );
       }

Based on learnings: "Always use this.logToStderr(formatWarning('...')) for warning messages."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/logs/connection-lifecycle/history.ts` around lines 69 - 75, The
warning is currently emitted with this.log(...) which writes to stdout; change
it to this.logToStderr(...) so warnings go to stderr. Specifically, in the block
that checks pagesConsumed > 1 and !this.shouldOutputJson(flags), replace the
this.log(formatWarning(...)) call with this.logToStderr(formatWarning(...))—keep
the same formatWarning(...) payload and leave the pagesConsumed, messages.length
and shouldOutputJson(flags) logic untouched.
src/commands/logs/history.ts (1)

69-75: Use this.logToStderr() for warning output.

Per project conventions, warning messages should be written to stderr, not stdout. This prevents warnings from polluting piped output.

♻️ Proposed fix
       if (pagesConsumed > 1 && !this.shouldOutputJson(flags)) {
-        this.log(
+        this.logToStderr(
           formatWarning(
             `Fetched ${pagesConsumed} pages to retrieve ${messages.length} results. Each page incurs additional API requests.`,
           ),
         );
       }

Based on learnings: "In the ably/ably-cli TypeScript (oclif) codebase, do not use this.warn() directly in command implementations. Always use this.logToStderr(formatWarning('...')) for warning messages."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/logs/history.ts` around lines 69 - 75, The current warning uses
this.log(...) which writes to stdout; change it to write to stderr by calling
this.logToStderr(...) instead when emitting the warning (the conditional that
checks pagesConsumed > 1 and !this.shouldOutputJson(flags)); replace the
this.log(formatWarning(...)) call with this.logToStderr(formatWarning(...)) so
warnings use stderr per project convention, keeping the same formatWarning text
and preserving the pagesConsumed / messages.length values.
test/unit/commands/spaces/list.test.ts (1)

50-53: Add a real multi-page fixture for spaces:list.

Every mocked response here is still a single page, so the new cross-page behavior in src/commands/spaces/list.ts never runs. A two-page fixture with duplicates split across pages would exercise seenSpaces, hasMore, and the multi-page warning path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/commands/spaces/list.test.ts` around lines 50 - 53, The test
currently only returns a single-page response via mock.request with
createMockPaginatedResult(createMockSpaceChannelItems()), so the new multi-page
logic in src/commands/spaces/list.ts (seenSpaces, hasMore and the multi-page
warning path) never executes; update the test to mock multiple paginated
responses by using mock.request.mockResolvedValueOnce for the first page
(hasMore: true) and mockResolvedValueOnce for the second page (hasMore: false),
crafting the two page fixtures with createMockPaginatedResult and
createMockSpaceChannelItems variants such that at least one space ID appears on
both pages to exercise deduplication and trigger the multi-page warning path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/apps/rules/list.ts`:
- Around line 117-124: The warning is being written to stdout with this.log(...)
inside the hasMore block; change it to emit to stderr and avoid JSON mode by
using this.logToStderr(...) and wrapping the call so it only runs when not in
JSON output mode (respecting the command's JSON flag handling). Update the
branch that builds warning via formatLimitWarning(namespaces.length,
flags.limit, "channel rules") to call this.logToStderr(...) instead of
this.log(...) and ensure the warning is skipped when output is JSON.

In `@src/commands/auth/keys/list.ts`:
- Around line 111-114: The limit warning is being printed to stdout via
this.log(...) in the hasMore branch; change it to emit on stderr using
this.logToStderr and wrap the warning in the standard warning formatter. Replace
the this.log(warning) call with
this.logToStderr(formatWarning(formatLimitWarning(keys.length, flags.limit,
"keys"))) so the warning produced by formatLimitWarning(...) is sent to stderr
using formatWarning(...).

In `@src/commands/channels/history.ts`:
- Around line 94-100: Replace the two warning prints that currently call
this.log(formatWarning(...)) with this.logToStderr(formatWarning(...));
specifically change the warning inside the pagesConsumed > 1 &&
!this.shouldOutputJson(flags) branch (the one that mentions fetched pages and
messages) and the other similar warning later in the file so warnings go to
stderr while preserving the existing shouldOutputJson gating and the same
formatWarning text.

In `@src/commands/push/channels/list-channels.ts`:
- Around line 53-59: The warning message is currently sent to stdout via
this.log(...) in the pagesConsumed check; update the call to
this.logToStderr(formatWarning(...)) so warnings are emitted to stderr instead.
Locate the block that checks "if (pagesConsumed > 1 &&
!this.shouldOutputJson(flags))" and replace the this.log(...) invocation with
this.logToStderr(...), and make the same replacement in the other warning branch
referenced (the similar block around the 81-88 range) that also uses
formatWarning and shouldOutputJson(flags).

In `@src/commands/push/channels/list.ts`:
- Around line 75-81: The pagination warning currently uses
this.log(formatWarning(...)) which should write to stderr; replace the calls
where pagesConsumed is checked (the block that calls this.log with
formatWarning) with this.logToStderr(formatWarning(...)) and do the same for the
other similar warning block (the second occurrence that also checks
pagesConsumed / shouldOutputJson). Ensure you keep the use of
shouldOutputJson(flags), formatWarning(...), and the existing message text
unchanged while swapping this.log to this.logToStderr.

In `@src/commands/push/devices/list.ts`:
- Around line 71-77: The warning currently written with
this.log(formatWarning(...)) (e.g., the pagesConsumed/devices message) is going
to stdout; change it to this.logToStderr(formatWarning(...)) while keeping the
existing guard that checks !this.shouldOutputJson(flags) so JSON output isn't
polluted; update the other similar warning block (the second occurrence guarded
by shouldOutputJson) the same way and ensure you reference the same
formatWarning(...) call and variables pagesConsumed and devices to locate the
spots.

In `@src/commands/rooms/messages/history.ts`:
- Around line 143-149: The warning currently uses this.log(...) which writes to
stdout and can contaminate --json output; change calls that emit
formatWarning(...) (e.g., the block checking pagesConsumed > 1 with
!this.shouldOutputJson(flags) and the similar block later around the other
warning) to use this.logToStderr(formatWarning(...)) instead of this.log(...).
Locate usages by searching for formatWarning(...) and the pagesConsumed /
shouldOutputJson(flags) checks and replace the this.log invocation with
this.logToStderr to route warnings to stderr.

In `@src/commands/spaces/list.ts`:
- Around line 118-123: The warning is being written to stdout with this.log(...)
which mixes diagnostics into command output; change the call inside the
pagesConsumed > 1 && !this.shouldOutputJson(flags) block to use
this.logToStderr(formatWarning(...)) instead so the formatted warning is sent to
stderr. Update the invocation that currently logs the message built with
formatWarning(`Fetched ${pagesConsumed} pages to retrieve
${limitedSpaces.length} results...`) leaving the condition and message intact.
Ensure you only replace this.log with this.logToStderr in that block so JSON
output paths (shouldOutputJson) remain unchanged.
- Around line 104-114: The filter currently treats IDs like "orders::$spacey" as
spaces because it uses includes("::$space") and /^(.+?)::\$space.*$/; update the
check to ensure "$space" is a full namespace segment by using a stricter regex
(e.g. test for ::\$space followed by end or another ::) before extracting the
prefix; in the block around SpaceItem/channel handling (references: SpaceItem,
seenSpaces, channel.channelId, spaceNameMatch) replace the includes and the
existing match with a single regex test/extract (e.g. /^(.+?)::\$space(?:$|::)/)
so only true "::\$space" segments are accepted, then proceed to add to
seenSpaces and set channel.channelId/channel.spaceName as before.

In `@src/utils/pagination.ts`:
- Around line 24-27: The function collectPaginatedResults currently accepts any
integer for the limit parameter which allows non-positive values and leads to
invalid slice semantics; add a guard at the start of collectPaginatedResults
that handles limit <= 0 (either by throwing a clear ArgumentError or by treating
it as 0 and returning an empty PaginationResult) and ensure you clamp or
normalize limit with something like maxLimit = Math.max(0, limit) before any
slice/truncation logic; apply the same guard/normalization to the other
pagination helper in this file that also accepts a limit (the helper referenced
at lines 69-74) so no negative or zero limits produce incorrect slice(0, -n)
behavior.
- Around line 91-95: The hasMore boolean currently includes pagesConsumed >=
maxPages which can return true even when no more pages exist; change the hasMore
calculation to only consider actual evidence of more results (items.length >
limit or currentPage?.hasNext()) — remove the pagesConsumed >= maxPages clause
from hasMore (symbols: hasMore, items, limit, currentPage.hasNext(),
pagesConsumed, maxPages, truncated) so hasMore is only true when there are extra
items or the currentPage reports a next page.

---

Outside diff comments:
In `@src/commands/spaces/list.ts`:
- Around line 129-143: The command prints "No active spaces found." even when
pagination indicates more pages exist; update the early-exit condition in the
spaces list flow to only log and return when there are genuinely no further
pages to scan. Specifically, in the branch that checks limitedSpaces.length ===
0, also require !hasMore (from the collectFilteredPaginatedResults result)
before logging and returning; alternatively, if intended, raise the scan cap
used by collectFilteredPaginatedResults so it scans further, but the minimal fix
is to gate the no-results branch on !hasMore to avoid false negatives.

---

Nitpick comments:
In @.claude/skills/ably-new-command/references/patterns.md:
- Around line 162-175: The warning outputs currently use this.log(...) even when
they should go to stderr; update the code so any warning text (the
formatWarning(...) call when pagesConsumed > 1 and the formatLimitWarning(...)
call when hasMore) is emitted with this.logToStderr(...) and only when
!this.shouldOutputJson(flags); keep the json path using this.logJsonResult({
messages, hasMore }, flags) unchanged and ensure you reference the existing
symbols pagesConsumed, shouldOutputJson(flags), formatWarning,
formatLimitWarning, hasMore, logJsonResult, and replace this.log(...) for
warnings with this.logToStderr(...).

In `@src/commands/apps/rules/list.ts`:
- Around line 59-62: The command is fetching all namespaces then slicing
locally; instead, update the service method listNamespaces to accept a limit
(and optionally offset/page) and pass that through to the REST call so the
server paginates (e.g., /apps/{appId}/namespaces?limit=...). Change the call
site in this file to pass flags.limit into controlApi.listNamespaces(appId,
flags.limit) and update the control API implementation (the listNamespaces
function) to accept the new parameter and include it as a query parameter in the
HTTP request; also update any other callers of listNamespaces to provide a limit
or default it to undefined so behavior remains backward-compatible.

In `@src/commands/channels/list.ts`:
- Around line 107-113: The warning currently uses this.log(...) in the
conditional that checks pagesConsumed and shouldOutputJson; replace the this.log
call with this.logToStderr(formatWarning(...)) so warnings are written to stderr
consistently (keep the same message using pagesConsumed and channels.length and
leave the surrounding conditional intact). Update the occurrence where
formatWarning is passed to this.log in the channels listing logic to call
this.logToStderr instead, ensuring behavior matches other commands that route
warnings to stderr.

In `@src/commands/logs/connection-lifecycle/history.ts`:
- Around line 69-75: The warning is currently emitted with this.log(...) which
writes to stdout; change it to this.logToStderr(...) so warnings go to stderr.
Specifically, in the block that checks pagesConsumed > 1 and
!this.shouldOutputJson(flags), replace the this.log(formatWarning(...)) call
with this.logToStderr(formatWarning(...))—keep the same formatWarning(...)
payload and leave the pagesConsumed, messages.length and shouldOutputJson(flags)
logic untouched.

In `@src/commands/logs/history.ts`:
- Around line 69-75: The current warning uses this.log(...) which writes to
stdout; change it to write to stderr by calling this.logToStderr(...) instead
when emitting the warning (the conditional that checks pagesConsumed > 1 and
!this.shouldOutputJson(flags)); replace the this.log(formatWarning(...)) call
with this.logToStderr(formatWarning(...)) so warnings use stderr per project
convention, keeping the same formatWarning text and preserving the pagesConsumed
/ messages.length values.

In `@src/commands/logs/push/history.ts`:
- Around line 70-76: The warning currently uses this.log(...) which sends output
to stdout; change it to this.logToStderr(...) so warnings go to stderr. In the
block that checks pagesConsumed > 1 && !this.shouldOutputJson(flags), replace
the call this.log(formatWarning(...)) with this.logToStderr(formatWarning(...))
while preserving the same message content (use pagesConsumed and messages.length
as before) so behavior is identical except the output stream.

In `@src/commands/rooms/list.ts`:
- Around line 124-130: The warning currently uses this.log(...) in the rooms
listing code (check pagesConsumed, shouldOutputJson and the formatWarning call);
change it to use this.logToStderr(formatWarning(...)) so the warning is emitted
to stderr consistently with other commands and only when pagesConsumed > 1 &&
!this.shouldOutputJson(flags). Replace the this.log(...) call with
this.logToStderr(...) while keeping the same message text and conditional logic.
- Around line 110-121: The predicate passed to filter mutates the RoomItem (it
overwrites channel.channelId and channel.room) which is surprising; either move
the normalization out of the filter or document the intent. Fix by turning the
predicate into a pure boolean test (use channelId/includes and regex match,
check seenRooms) and then perform the mutation in a separate mapping step (e.g.,
after filtering, set channel.channelId/channel.room to the extracted roomName),
or if you keep the in-filter mutation, add a concise inline comment above the
predicate explaining that mutation of channel.channelId and channel.room is
intentional for room name normalization (references: RoomItem, seenRooms,
channel.channelId, channel.room).

In `@test/helpers/mock-ably-chat.ts`:
- Around line 240-244: Replace the ad-hoc history mock with the shared helper to
ensure full paginated result shape: import and use createMockPaginatedResult
from mock-ably-rest and return createMockPaginatedResult([]) from the history
mock (the current vi.fn().mockResolvedValue({...}) block). This ensures methods
like isLast(), first(), and current() are present alongside items, hasNext(),
and next(), keeping the test utilities and expected interfaces consistent.

In `@test/helpers/mock-ably-rest.ts`:
- Around line 254-280: The helper createMockPaginatedResult currently only
models a single nextPage; change its API to accept an array of pages (e.g.,
items: T[], pages?: Array<{items: T[]; hasNext?: boolean}>) or a single pages:
Array<T[]> so you can build an arbitrary chain, then construct the linked
results for each page so next() returns the following page's result or null,
hasNext()/isLast() reflect whether a subsequent page exists, and
first()/current() still resolve to the appropriate result; implement this by
mapping the input pages into result objects up front and wiring each result.next
to return the next mapped result (or null) to support 3+ non-empty pages while
leaving the public method names (createMockPaginatedResult, next, hasNext,
isLast, first, current) unchanged.

In `@test/unit/commands/push/devices/list.test.ts`:
- Around line 70-85: Update the "should output JSON when requested" test to also
assert the pagination contract and add a new multi-page scenario: when using
getMockAblyRest() / mock.push.admin.deviceRegistrations.list with
createMockPaginatedResult, assert the parsed JSON contains the pagination
metadata (e.g., result.pagination.hasMore is present and correct) and keep
devices assertions; add a separate test that returns a multi-page paginated mock
(simulate pagesConsumed > 1) and assert the multi-page warning path by capturing
runCommand's stderr (or combined output) contains the expected warning message
about pagesConsumed > 1. Ensure you reference the same symbols: runCommand,
getMockAblyRest, mock.push.admin.deviceRegistrations.list, and
createMockPaginatedResult.

In `@test/unit/commands/spaces/list.test.ts`:
- Around line 50-53: The test currently only returns a single-page response via
mock.request with createMockPaginatedResult(createMockSpaceChannelItems()), so
the new multi-page logic in src/commands/spaces/list.ts (seenSpaces, hasMore and
the multi-page warning path) never executes; update the test to mock multiple
paginated responses by using mock.request.mockResolvedValueOnce for the first
page (hasMore: true) and mockResolvedValueOnce for the second page (hasMore:
false), crafting the two page fixtures with createMockPaginatedResult and
createMockSpaceChannelItems variants such that at least one space ID appears on
both pages to exercise deduplication and trigger the multi-page warning path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2d80ce26-d6d5-4260-b00c-404ee2a3205d

📥 Commits

Reviewing files that changed from the base of the PR and between 9dee531 and 5b1d40e.

📒 Files selected for processing (34)
  • .claude/skills/ably-new-command/references/patterns.md
  • README.md
  • src/commands/apps/list.ts
  • src/commands/apps/rules/list.ts
  • src/commands/auth/keys/list.ts
  • src/commands/channels/history.ts
  • src/commands/channels/list.ts
  • src/commands/integrations/list.ts
  • src/commands/logs/connection-lifecycle/history.ts
  • src/commands/logs/history.ts
  • src/commands/logs/push/history.ts
  • src/commands/push/channels/list-channels.ts
  • src/commands/push/channels/list.ts
  • src/commands/push/devices/list.ts
  • src/commands/queues/list.ts
  • src/commands/rooms/list.ts
  • src/commands/rooms/messages/history.ts
  • src/commands/spaces/list.ts
  • src/utils/pagination.ts
  • test/helpers/mock-ably-chat.ts
  • test/helpers/mock-ably-rest.ts
  • test/unit/commands/channels/history.test.ts
  • test/unit/commands/channels/list.test.ts
  • test/unit/commands/logs/connection-lifecycle/history.test.ts
  • test/unit/commands/logs/history.test.ts
  • test/unit/commands/logs/push/history.test.ts
  • test/unit/commands/push/channels/list-channels.test.ts
  • test/unit/commands/push/channels/list.test.ts
  • test/unit/commands/push/devices/list.test.ts
  • test/unit/commands/rooms/list.test.ts
  • test/unit/commands/rooms/messages.test.ts
  • test/unit/commands/rooms/messages/history.test.ts
  • test/unit/commands/spaces/list.test.ts
  • test/unit/utils/pagination.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants