Skip to content

Conversation

@mattheworiordan
Copy link
Member

@mattheworiordan mattheworiordan commented Jan 6, 2026

Whilst working on adding the new push notification dashboards, I realised users are quite limited by what they can do with push today because:

  • They cannot make any changes from the dashboard (register devices, channel subsdcriptions, etc)
  • They cannot test a push notification without writing a lot of code (they need to build a client)
  • The CLI has ZERO coverage over the push APIs.

So I decided to just go ahead and add push support, and follow the new naming convention from @m-hulbert in
PDR-087: Updated CLI command hierarchy
, the new patterns / testing implemented by @AndyTWF in this repo. I think that's a big win for completeness of this CLI, and it will materially improve how users can use push in the new dashboards with the CLI, along with the new UI I have added (see recording of new dashboards with push notification inspector & testing tool mentioned in this Slack post).

Note that whilst this does work, the APNS and FCM APIs are broken right now, so I have a fix at https://github.com/ably/website/commit/8d632061042074c35098821075bbc2fa2b03e4cc which I am hoping will make it upstream in the coming week or so. It is part of a much larger set of improvements, including the new push dashboard overhaul work in these commits.

Here is the brief I used to execute on this work:


Executive Summary

This proposal defines a comprehensive set of CLI commands for managing and testing push notifications in Ably. The commands are designed to be consistent with existing CLI patterns and align with the new command hierarchy defined in PDR-087.


Investigation: Why Only APNs and Not FCM?

Current State

The CLI currently only supports setting APNs credentials via ably apps set-apns-p12, which uses the Control API endpoint POST /apps/{id}/pkcs12.

Root Cause

The FCM configuration was simply never implemented, despite the Control API fully supporting it.

The Control API supports both APNs and FCM configuration through the app PATCH endpoint with these fields:

Platform Control API Fields Dashboard Configuration
APNs apnsCertificate, apnsPrivateKey, apnsUseSandboxEndpoint .p12 upload or PEM paste
FCM fcmServiceAccount, fcmProjectId JSON service account upload

The CLI implemented a dedicated P12 upload command but never added equivalent FCM commands. This appears to be an oversight rather than a technical limitation.

Recommendation

Add FCM configuration support alongside the APNs commands in the new ably push command group.


Command Hierarchy

Following PDR-087's decision to move ably apps set-apns-p12 to ably push set-apns-p12, the full push command hierarchy will be:

ably push
├── config                    # Configuration management (APNs/FCM credentials)
│   ├── show                  # Show current push configuration status
│   ├── set-apns              # Set APNs credentials (certificate or token-based)
│   ├── set-fcm               # Set FCM credentials (service account JSON)
│   ├── clear-apns            # Remove APNs configuration
│   └── clear-fcm             # Remove FCM configuration
│
├── devices                   # Device registration management (SDK: push.admin.deviceRegistrations)
│   ├── save                  # Register new or update existing device
│   ├── get                   # Get device details
│   ├── list                  # List registered devices
│   ├── remove                # Remove a single device registration
│   └── remove-where          # Remove devices matching filter criteria
│
├── channels                  # Channel subscription management (SDK: push.admin.channelSubscriptions)
│   ├── save                  # Subscribe device/client to channel
│   ├── list                  # List channel subscriptions
│   ├── list-channels         # List channels with push subscriptions
│   ├── remove                # Remove a single subscription
│   └── remove-where          # Remove subscriptions matching filter criteria
│
├── publish                   # Send push notification to recipient(s)
└── batch-publish             # Batch publish (up to 10,000 notifications)

Naming Convention Note

The CLI uses shortened namespace names (devices and channels) rather than the full SDK names (deviceRegistrations and channelSubscriptions) for brevity. Typing ably push device-registrations remove-where repeatedly would be cumbersome for users. The help text for each command will reference the full SDK method name (e.g., "Maps to push.admin.deviceRegistrations.save()") to maintain clarity for developers familiar with the SDK.

Additionally, per PDR-087, push logs remain under ably logs push:

ably logs push
├── subscribe                 # Stream push notification logs (existing)
└── history                   # View historical push logs (existing)

Detailed Command Specifications

1. Configuration Commands (ably push config)

ably push config show

Show the current push notification configuration status for an app.

ably push config show [--app APP_ID]

Flags:

  • --app - App ID or name (uses current app if not specified)
  • --json - Output as JSON
  • --pretty-json - Output as formatted JSON

Output (text):

Push Notification Configuration for app "My App" (app-id-123)

APNs (iOS):
  Status: Configured
  Environment: Production
  Certificate: *.ably.io (expires 2025-12-31)

FCM (Android):
  Status: Configured
  Project ID: my-firebase-project
  Service Account: firebase-adminsdk@...

ably push config set-apns

Configure Apple Push Notification service credentials. Supports both certificate-based (.p12) and token-based (.p8) authentication.

# Certificate-based authentication (existing functionality, enhanced)
ably push config set-apns --certificate /path/to/cert.p12 [--password PASSWORD] [--sandbox]

# Token-based authentication (new)
ably push config set-apns --key-file /path/to/key.p8 --key-id KEY_ID --team-id TEAM_ID --topic BUNDLE_ID

Flags:

  • --app - App ID or name (uses current app if not specified)
  • --certificate - Path to .p12 certificate file
  • --password - Password for the .p12 certificate
  • --sandbox - Use APNs sandbox environment (default: production)
  • --key-file - Path to .p8 private key file (token auth)
  • --key-id - Key ID from Apple Developer (token auth)
  • --team-id - Team ID from Apple Developer (token auth)
  • --topic - App bundle identifier (token auth)
  • --json - Output as JSON

Examples:

# Upload P12 certificate for production
ably push config set-apns --certificate ./certs/push-prod.p12 --password "secret"

# Upload P12 certificate for sandbox testing
ably push config set-apns --certificate ./certs/push-dev.p12 --sandbox

# Configure token-based authentication
ably push config set-apns --key-file ./AuthKey_ABC123.p8 \
  --key-id ABC123 --team-id XYZ789 --topic com.myapp.bundle

ably push config set-fcm

Configure Firebase Cloud Messaging credentials.

ably push config set-fcm --service-account /path/to/service-account.json

Flags:

  • --app - App ID or name (uses current app if not specified)
  • --service-account - Path to Firebase service account JSON file
  • --json - Output as JSON

Examples:

# Upload FCM service account credentials
ably push config set-fcm --service-account ./firebase-service-account.json

# Configure for a specific app
ably push config set-fcm --app "My Production App" --service-account ./firebase-prod.json

ably push config clear-apns

Remove APNs configuration from an app.

ably push config clear-apns [--app APP_ID] [--force]

Flags:

  • --app - App ID or name
  • --force - Skip confirmation prompt
  • --json - Output as JSON

ably push config clear-fcm

Remove FCM configuration from an app.

ably push config clear-fcm [--app APP_ID] [--force]

Flags:

  • --app - App ID or name
  • --force - Skip confirmation prompt
  • --json - Output as JSON

2. Device Registration Commands (ably push devices)

Maps to SDK: push.admin.deviceRegistrations

ably push devices save

Register a new device or update an existing device registration.

# Register with inline parameters
ably push devices save --id DEVICE_ID --platform PLATFORM --form-factor FORM_FACTOR \
  --transport-type TRANSPORT --device-token TOKEN [--client-id CLIENT_ID]

# Register from JSON file
ably push devices save --data /path/to/device.json

# Register from inline JSON
ably push devices save --data '{"id":"device-123","platform":"android",...}'

Flags:

  • --app - App ID or name
  • --id - Device identifier (required unless using --data)
  • --platform - Platform: ios, android, or browser (required unless using --data)
  • --form-factor - Form factor: phone, tablet, desktop, tv, watch, car, embedded
  • --client-id - Optional client identifier to associate with the device
  • --transport-type - Push transport: apns, fcm, or web
  • --device-token - Device token for APNs/FCM (or registration token for FCM)
  • --metadata - Optional metadata as JSON string
  • --data - Full device details as JSON string or file path
  • --json - Output as JSON

Examples:

# Register an Android device
ably push devices save \
  --id "device-android-001" \
  --platform android \
  --form-factor phone \
  --transport-type fcm \
  --device-token "fcm-registration-token-here" \
  --client-id "user-456"

# Register from JSON file
ably push devices save --data ./device-registration.json

ably push devices get

Get detailed information about a specific device.

ably push devices get DEVICE_ID

Args:

  • DEVICE_ID - The device identifier (required)

Flags:

  • --app - App ID or name
  • --json - Output as JSON
  • --pretty-json - Output as formatted JSON

Output (text):

Device Details

Device ID:    abc123-device-uuid
Client ID:    user-456
Platform:     ios
Form Factor:  phone
State:        Active
Created:      2025-01-01 12:00:00
Updated:      2025-01-06 10:30:00

Push Recipient:
  Transport:    apns
  Device Token: ****...****abcd (redacted)

Metadata:
  app_version: 2.5.0
  os_version:  iOS 18.0

ably push devices list

List registered devices for the current app.

ably push devices list [--client-id CLIENT_ID] [--device-id DEVICE_ID] [--state STATE] [--limit N]

Flags:

  • --app - App ID or name
  • --client-id - Filter by client ID
  • --device-id - Filter by device ID
  • --state - Filter by state: ACTIVE, FAILING, or FAILED
  • --limit - Maximum number of results (default: 100, max: 1000)
  • --json - Output as JSON
  • --pretty-json - Output as formatted JSON

Output (text):

Registered Devices (3 found)

DEVICE ID                              PLATFORM   CLIENT ID      STATE      LAST ACTIVE
-----------------------------------------------------------------------------------------------
abc123-device-uuid                     ios        user-456       Active     2025-01-06 10:30:00
def456-device-uuid                     android    user-456       Active     2025-01-06 09:15:00
ghi789-device-uuid                     web        user-789       Active     2025-01-05 22:00:00

ably push devices remove

Remove a single device registration.

ably push devices remove DEVICE_ID [--force]

Args:

  • DEVICE_ID - The device identifier (required)

Flags:

  • --app - App ID or name
  • --force - Skip confirmation prompt
  • --json - Output as JSON

ably push devices remove-where

Remove devices matching filter criteria.

ably push devices remove-where --client-id CLIENT_ID [--force]
ably push devices remove-where --device-id DEVICE_ID [--force]

Flags:

  • --app - App ID or name
  • --client-id - Remove all devices for this client
  • --device-id - Remove specific device by ID
  • --force - Skip confirmation prompt
  • --json - Output as JSON

Note: At least one filter (--client-id or --device-id) must be specified.


3. Channel Subscription Commands (ably push channels)

Maps to SDK: push.admin.channelSubscriptions

ably push channels save

Subscribe a device or client to a push-enabled channel.

# Subscribe by device ID
ably push channels save --channel CHANNEL --device-id DEVICE_ID

# Subscribe by client ID
ably push channels save --channel CHANNEL --client-id CLIENT_ID

Flags:

  • --app - App ID or name
  • --channel - Channel name to subscribe to (required)
  • --device-id - Device ID to subscribe
  • --client-id - Client ID to subscribe (subscribes all client's devices)
  • --json - Output as JSON

Note: Either --device-id or --client-id must be specified.

Examples:

# Subscribe a specific device to alerts channel
ably push channels save --channel alerts --device-id abc123

# Subscribe all of a user's devices to notifications
ably push channels save --channel notifications --client-id user-456

ably push channels list

List push channel subscriptions.

ably push channels list --channel CHANNEL [--client-id CLIENT_ID] [--device-id DEVICE_ID]

Flags:

  • --app - App ID or name
  • --channel - Filter by channel name (required)
  • --client-id - Filter by client ID
  • --device-id - Filter by device ID
  • --limit - Maximum number of results (default: 100, max: 1000)
  • --json - Output as JSON

Output (text):

Push Subscriptions for channel "alerts" (3 found)

TYPE        ID
----------------------------------
device      abc123-device-uuid
device      def456-device-uuid
client      user-456

ably push channels list-channels

List all channels that have at least one push subscription.

ably push channels list-channels [--limit N]

Flags:

  • --app - App ID or name
  • --limit - Maximum number of results (default: 100, max: 1000)
  • --json - Output as JSON

Output (text):

Channels with Push Subscriptions (3 found)

CHANNEL
-----------------------------------
alerts
notifications
system-updates

ably push channels remove

Remove a single subscription.

ably push channels remove --channel CHANNEL --device-id DEVICE_ID
ably push channels remove --channel CHANNEL --client-id CLIENT_ID

Flags:

  • --app - App ID or name
  • --channel - Channel to unsubscribe from (required)
  • --device-id - Device ID to unsubscribe
  • --client-id - Client ID to unsubscribe
  • --force - Skip confirmation prompt
  • --json - Output as JSON

Note: Either --device-id or --client-id must be specified.


ably push channels remove-where

Remove subscriptions matching filter criteria.

ably push channels remove-where --channel CHANNEL [--device-id DEVICE_ID] [--client-id CLIENT_ID]

Flags:

  • --app - App ID or name
  • --channel - Channel to unsubscribe from (required)
  • --device-id - Filter by device ID
  • --client-id - Filter by client ID
  • --force - Skip confirmation prompt
  • --json - Output as JSON

Note: Channel is required. At least one additional filter (--device-id or --client-id) is recommended to avoid removing all subscriptions for a channel.


4. Publish Commands (ably push publish)

ably push publish

Publish a push notification directly to device(s) or client(s).

# Publish to a specific device
ably push publish --device-id DEVICE_ID --title "Title" --body "Message"

# Publish to all devices of a client
ably push publish --client-id CLIENT_ID --title "Title" --body "Message"

# Publish with full payload from file
ably push publish --device-id DEVICE_ID --payload /path/to/notification.json

# Publish with inline JSON payload
ably push publish --device-id DEVICE_ID --payload '{"notification":{"title":"Hi","body":"Hello"}}'

Flags:

  • --app - App ID or name
  • --device-id - Target device ID
  • --client-id - Target client ID (all their devices)
  • --title - Notification title
  • --body - Notification body
  • --sound - Notification sound (default, or filename)
  • --icon - Notification icon (Android/Web)
  • --badge - Badge count (iOS)
  • --data - Custom data payload (JSON string)
  • --payload - Full notification payload (JSON string or file path)
  • --collapse-key - Collapse key for notification grouping
  • --ttl - Time-to-live in seconds
  • --apns - APNs-specific overrides (JSON)
  • --fcm - FCM-specific overrides (JSON)
  • --web - Web Push-specific overrides (JSON)
  • --json - Output as JSON

Examples:

# Simple notification to a device
ably push publish --device-id abc123 --title "New Message" --body "You have a new message"

# Notification with custom data
ably push publish --client-id user-456 \
  --title "Order Update" \
  --body "Your order has shipped!" \
  --data '{"orderId":"12345","trackingUrl":"https://..."}'

# iOS-specific notification with badge
ably push publish --device-id abc123 \
  --title "New Message" \
  --body "You have 3 unread messages" \
  --badge 3 \
  --sound "notification.wav"

# Background/silent notification (iOS)
ably push publish --device-id abc123 \
  --payload '{"apns":{"aps":{"content-available":1},"apns-headers":{"apns-push-type":"background","apns-priority":"5"}}}'

# Full payload from file
ably push publish --device-id abc123 --payload ./notification.json

ably push batch-publish

Publish push notifications to multiple recipients in a single request.

ably push batch-publish --payload /path/to/batch.json
ably push batch-publish --payload '[{"recipient":{"deviceId":"abc"},"payload":{"notification":{"title":"Hi"}}}]'
ably push batch-publish --payload -  # Read from stdin

Flags:

  • --app - App ID or name
  • --payload - Batch payload (JSON array, file path, or - for stdin) - required
  • --json - Output as JSON

Batch Payload Format:

[
  {
    "recipient": { "deviceId": "abc123" },
    "payload": {
      "notification": { "title": "Hello", "body": "World" }
    }
  },
  {
    "recipient": { "clientId": "user-456" },
    "payload": {
      "notification": { "title": "Hi", "body": "There" },
      "data": { "key": "value" }
    }
  }
]

Limits:

  • Maximum 10,000 notifications per batch request

Output (text):

Batch Push Results

Total: 3
Successful: 2
Failed: 1

Failed Notifications:
  - Recipient: clientId=user-789
    Error: Device not registered (code: 40001)

API Mapping

Device Registrations (push.admin.deviceRegistrations)

CLI Command SDK Method API Endpoint Method
push devices save save() /push/deviceRegistrations POST
push devices get get() /push/deviceRegistrations/{deviceId} GET
push devices list list() /push/deviceRegistrations GET
push devices remove remove() /push/deviceRegistrations/{deviceId} DELETE
push devices remove-where removeWhere() /push/deviceRegistrations DELETE

Channel Subscriptions (push.admin.channelSubscriptions)

CLI Command SDK Method API Endpoint Method
push channels save save() /push/channelSubscriptions POST
push channels list list() /push/channelSubscriptions GET
push channels list-channels listChannels() /push/channels GET
push channels remove remove() /push/channelSubscriptions DELETE
push channels remove-where removeWhere() /push/channelSubscriptions DELETE

Publishing (push.admin)

CLI Command SDK Method API Endpoint Method
push publish publish() /push/publish POST
push batch-publish N/A (REST only) /push/batch/publish POST

Configuration (Control API)

CLI Command API Endpoint Method
push config set-apns (p12) /apps/{id}/pkcs12 POST
push config set-apns (pem/p8) /apps/{id} PATCH
push config set-fcm /apps/{id} PATCH
push config clear-apns /apps/{id} PATCH
push config clear-fcm /apps/{id} PATCH
push config show /apps/{id} GET

Note

Adds end-to-end push notification management to the CLI and cleans up legacy APNs handling.

  • New ably push suite: config (show/set-apns/set-fcm/clear-apns/clear-fcm), devices (save/get/list/remove/remove-where), channels (save/list/list-channels/remove/remove-where), publish, and batch-publish
  • Configuration: APNs via .p12 or token (.p8), FCM via service account JSON; plus show/clear commands
  • Removed: legacy apps set-apns-p12 command and example; replaced with push config set-apns
  • Docs/help updates: README expanded for new commands; argument label fixes (APPID, INTEGRATIONID, QUEUEID); added ably config path/show; clarified defaults and added flags (e.g., --sequence-numbers, --show-others, duration flags) across several commands

Written by Cursor Bugbot for commit 73ce711. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added push notification batch publishing capability.
    • Added push channel subscription management (save, list, remove).
    • Added push device registration management (save, list, get, remove).
    • Added push notification configuration commands for APNs and FCM.
    • Enhanced APNs P12 certificate upload handling.
  • Documentation

    • Updated CLI command signatures and argument names for clarity (e.g., [APPID], [INTEGRATIONID]).
    • Expanded command usage examples and descriptions.
    • Added comprehensive documentation for new push management commands.

✏️ Tip: You can customize this high-level summary in your review settings.

mattheworiordan and others added 11 commits January 6, 2026 16:05
Implement Stage 1 of push notification support with full device
registration management using the Ably SDK's push.admin.deviceRegistrations
API.

Commands added:
- push devices save: Register devices (Android/FCM, iOS/APNs, Web Push)
- push devices get: Retrieve device details by ID
- push devices list: List devices with filters (clientId, state, limit)
- push devices remove: Remove single device by ID
- push devices remove-where: Bulk remove by clientId/deviceId

Features:
- Full support for all three platforms: Android (FCM), iOS (APNs), browser (Web Push)
- Web Push requires --target-url, --p256dh-key, and --auth-secret flags
- JSON output mode with --json flag
- Confirmation prompts for destructive operations (--force to skip)
- Token redaction in get command for security

Includes 14 E2E tests hitting real Ably sandbox endpoints with fake
device tokens (verified to work for testing purposes).
Implement Stage 2 of push notification support with full channel
subscription management using the Ably SDK's push.admin.channelSubscriptions
API.

Commands added:
- push channels save: Subscribe device or client to a push channel
- push channels list: List subscriptions for a channel with filters
- push channels list-channels: List all channels with push subscriptions
- push channels remove: Remove single subscription
- push channels remove-where: Bulk remove subscriptions by filter

Features:
- Subscribe by device ID or client ID (not both)
- Filter subscriptions by deviceId or clientId
- JSON output mode with --json flag
- Confirmation prompts for destructive operations (--force to skip)
- Helpful error messages for push-not-enabled (40100) errors

Note: Full integration tests require a push-enabled channel namespace
which is not available in the standard test environment. Validation
tests verify CLI command structure and error handling.

Part of #124
Add push notification publishing commands:

- `push publish` - Publish notification to device or client
  - Supports --device-id or --client-id targeting
  - Simple flags: --title, --body, --sound, --icon, --badge
  - Custom data via --data, full control via --payload
  - Platform-specific overrides: --apns, --fcm, --web

- `push batch-publish` - Publish up to 10,000 notifications
  - Accepts JSON array via --payload (file, inline, or stdin)
  - Reports success/failure counts per notification
Implements Stage 4 of push notification support with commands to manage
Apple Push Notification service (APNs) credentials:

- `push config show` - Display current push notification configuration
- `push config set-apns` - Configure APNs with P12 certificate or P8 token auth
- `push config clear-apns` - Remove APNs configuration

Key implementation details:
- P12 certificate upload uses multipart/form-data with correct endpoint
- Support for both certificate-based (.p12) and token-based (.p8) auth
- Token auth requires key-id, team-id, and bundle-id flags
- Sandbox/production environment determined by certificate type (not flag)

Also fixes:
- Control API uploadApnsP12 now uses FormData/Blob for proper multipart upload
- apps:set-apns-p12 updated to use Buffer instead of base64 string
- Updated unit tests to use correct /v1/apps/{id}/pkcs12 endpoint
Implements Stage 5 of push notification support with commands to manage
Firebase Cloud Messaging (FCM) credentials:

- `push config set-fcm` - Configure FCM using a service account JSON file
  - Validates file exists and contains valid JSON
  - Validates type="service_account" and project_id fields
  - Displays project ID and client email on success
- `push config clear-fcm` - Remove FCM configuration
  - Confirms before clearing (skip with --force)
  - Detects and reports if FCM is not configured

Uses Control API PATCH /apps/{id} with fcmServiceAccount field.
Progress messages suppressed in JSON output mode.

Note: Ably API validates credentials with Google/Firebase before
persisting - test credentials will be accepted but not stored.
Push fixtures useful for real CLI testing
- Remove duplicate confirm() method from clear-apns.ts and clear-fcm.ts
- Use existing interactiveHelper.confirm() from base class instead
- Fix publish.ts and batch-publish.ts to exit(1) after JSON error output
- Ensures consistent error handling with proper exit codes across push commands
Add comprehensive unit tests for:
- push:devices:save (11 tests)
- push:devices:get (7 tests)
- push:devices:list (8 tests)
- push:devices:remove (5 tests)
- push:devices:remove-where (5 tests)

Tests mock the Ably SDK and verify command behavior,
flag validation, and error handling.

Also adds removeWhere and listChannels mock methods
to mock-ably-rest.ts helper.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive unit tests for:
- push:channels:save (9 tests)
- push:channels:list (8 tests)
- push:channels:remove (8 tests)
- push:channels:remove-where (6 tests)
- push:channels:list-channels (5 tests)

Tests mock the Ably SDK and verify command behavior,
flag validation, and error handling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive unit tests for:
- push:publish (15 tests) - single notification publishing
- push:batch-publish (13 tests) - batch notification publishing

Tests cover:
- Device and client ID targeting
- Notification payload construction
- Platform-specific overrides (APNs, FCM, Web)
- JSON payload parsing
- Validation of required flags
- Error handling for API failures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive unit tests for:
- push:config:set-apns - P12 certificate and P8 token-based auth (13 tests)
- push:config:clear-apns - clearing APNs configuration (7 tests)
- push:config:show - displaying push notification config status (10 tests)

Tests use nock to mock Control API endpoints (/v1/me, /v1/accounts/{id}/apps)
which are called by ControlBaseCommand before app-specific operations.

Note: set-fcm and clear-fcm tests already existed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Jan 6, 2026

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

Project Deployment Review Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Jan 9, 2026 1:05pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

Walkthrough

This PR introduces a comprehensive push notification management system to the Ably CLI, adding new command groups for batch publishing, device registrations, channel subscriptions, and configuration management. The API signature for uploading APNS P12 certificates changes from base64 string to Buffer, and the underlying App interface expands with new authentication and configuration fields.

Changes

Cohort / File(s) Summary
Push command infrastructure
src/commands/push/index.ts, src/commands/push/channels/index.ts, src/commands/push/config/index.ts, src/commands/push/devices/index.ts
New topic command groups extending BaseTopicCommand to organize push notification subcommands. Each defines topic names, command groups, descriptions, and usage examples.
Push batch publishing
src/commands/push/batch-publish.ts
New command for publishing push notifications in batches (up to 10,000 items). Validates recipient requirements (deviceId or clientId), supports JSON payloads from inline/file/stdin, and provides detailed result summaries with per-item error details.
Push channel subscriptions
src/commands/push/channels/list-channels.ts, src/commands/push/channels/list.ts, src/commands/push/channels/remove.ts, src/commands/push/channels/remove-where.ts, src/commands/push/channels/save.ts
New commands for managing push channel subscriptions: listing subscribed channels, retrieving subscriptions, adding/removing subscriptions with optional device-id or client-id filtering.
Push device management
src/commands/push/devices/list.ts, src/commands/push/devices/get.ts, src/commands/push/devices/remove.ts, src/commands/push/devices/remove-where.ts, src/commands/push/devices/save.ts
New commands for managing push device registrations: listing, retrieving, registering, and removing devices with platform-specific transport details (FCM, APNs, Web).
Push direct publish
src/commands/push/publish.ts
New command for publishing push notifications to individual devices or clients. Supports simple (title/body) and full payload modes with optional data, platform-specific overrides, and detailed recipient targeting.
Push configuration
src/commands/push/config/set-apns.ts, src/commands/push/config/set-fcm.ts, src/commands/push/config/clear-apns.ts, src/commands/push/config/clear-fcm.ts, src/commands/push/config/show.ts
New commands for managing APNs and FCM push configurations per app: setting certificate or token-based APNs, FCM service accounts, clearing credentials, and displaying current configuration status.
APNS P12 certificate upload
src/commands/apps/set-apns-p12.ts
Modified to read P12 certificate as a raw Buffer instead of base64 string; removed useForSandbox parameter from API call with contextual comment clarifying sandbox selection by certificate type.
Control API service
src/services/control-api.ts
Updated uploadApnsP12 signature: certificateData parameter now accepts Buffer instead of string; useForSandbox option removed. App interface extended with apnsAuthType, apnsUseSandboxEndpoint, and fcmProjectId fields. Multipart form-data upload implementation updated.
Unit tests
test/unit/commands/push/*/...test.ts, test/unit/commands/apps/set-apns-p12.test.ts
Comprehensive test suites for all new push commands and updated APNS command covering success paths, JSON output, validation errors, API error handling, and edge cases.
E2E tests
test/e2e/push/batch-publish-e2e.test.ts, test/e2e/push/channels-e2e.test.ts, test/e2e/push/devices-e2e.test.ts, test/e2e/push/publish-e2e.test.ts
End-to-end test suites verifying CLI integration with Ably API for batch publishing, channel subscriptions, device registrations, and direct push publishing scenarios.
Test fixtures and mocks
test/fixtures/push/..., test/helpers/mock-ably-rest.ts
Added test files for APNs private key (PEM format), FCM service account JSON, and README documentation. Extended mock Ably REST client with channelSubscriptions.listChannels, channelSubscriptions.removeWhere, and deviceRegistrations.removeWhere methods with pagination support.
Documentation
README.md
Updated command signatures and argument placeholders from legacy identifiers (ID, RULEID) to new public identifiers (APPID, INTEGRATIONID, QUEUEID, etc.). Clarified argument names, added new flags, reorganized command groupings for push-related features.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • test: add more unit tests #118: Modifies the same uploadApnsP12 API flow in src/commands/apps/set-apns-p12.ts, including the control-api signature and related unit tests that overlap with this PR's changes to certificate handling.
  • feat: add centralized mock helpers for Ably SDKs #121: Extends test/helpers/mock-ably-rest.ts with centralized mock helpers for push APIs (channelSubscriptions.listChannels, removeWhere, deviceRegistrations.removeWhere) that are directly utilized and built upon in this PR's test infrastructure.

Poem

🐰 A warren of push commands now hops into place,
Devices, channels, configs—organized with grace!
Batch notifications fly swiftly through the air,
While Apple and Firebase credentials find their lair.
From token to certificate, the CLI's now complete! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Push notification support' clearly summarizes the main change—adding comprehensive CLI support for Ably Push with multiple subcommands for devices, channels, configuration, and publishing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch push-notification-support

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • PDR-087: Request failed with status code 404

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.

Update APNs token-based auth to use correct field names per Control API:
- apnsPrivateKey → apnsSigningKey
- applePushKeyId → apnsSigningKeyId
- applePushTeamId → apnsIssuerKey
- applePushBundleId → apnsTopicHeader
- Add apnsAuthType: "token" to request payload

Update config show to use new response fields:
- Use apnsAuthType to determine APNs config status and auth type
- Use fcmProjectId to determine FCM config status
- Use apnsUseSandboxEndpoint with fallback to legacy field

Add App interface fields: apnsAuthType, apnsUseSandboxEndpoint, fcmProjectId

Ref: ably/website@8d632061 (feature/legacy-dashboard-quick-wins)

Note: Tests pass against mocks; E2E tests pending API deployment to staging
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

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: 5

Caution

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

⚠️ Outside diff range comments (1)
src/commands/apps/set-apns-p12.ts (1)

33-37: Flag behavior inconsistency creates misleading UX.

The --use-for-sandbox flag is still accepted and displayed in output (lines 73-77), but it's no longer passed to the API (line 61-63). This creates a confusing experience:

  1. User provides --use-for-sandbox
  2. CLI shows "Environment: Sandbox"
  3. But the API ignores this - certificate type determines environment

This means the output could be incorrect and misleading. For example, if a user provides a production certificate with --use-for-sandbox, the output says "Sandbox" but the API will use it for production.

Options:

  1. Remove the flag entirely (breaking change, but clearer)
  2. Update output to clarify: "Certificate type determines environment (not the flag)"
  3. Fetch the actual environment from the API response and display that

Also note: The flag is still in the examples (line 21) which should be updated accordingly.

Also applies to: 68-78

🤖 Fix all issues with AI Agents
In @src/commands/push/channels/list-channels.ts:
- Around line 79-92: The JSON error path in listChannelsCommand (check the
this.shouldOutputJson(flags) branch that logs this.formatJsonOutput({...})) logs
the error but doesn't terminate with a non-zero exit; add a call to this.exit(1)
immediately after the this.log(...) in that branch so the process returns a
failure code when --json is used, mirroring the pattern used in push/publish.ts
and push/batch-publish.ts.

In @src/commands/push/channels/remove-where.ts:
- Around line 114-133: The JSON-path in the catch block for the remove-where
command logs the error but doesn't terminate with a non-zero exit code; after
calling this.log(this.formatJsonOutput(...)) when this.shouldOutputJson(flags)
is true, call this.exit(1) (or process.exit(1) if this.exit isn’t available) so
the command exits with a non-zero status; locate the catch block that builds
errorMessage/errorCode and uses this.shouldOutputJson(flags) and
this.formatJsonOutput to add the exit call immediately after logging the JSON
error.

In @src/commands/push/devices/list.ts:
- Around line 136-148: The JSON error path in the devices list command uses
this.log(this.formatJsonOutput(...)) but doesn't terminate with a non-zero exit,
so update the branch inside shouldOutputJson(flags) to either call
this.jsonError(...) passing the same structured payload (error: errorMessage,
success: false) or keep the log call and immediately invoke this.exit(1) after
it; locate the logic around shouldOutputJson, formatJsonOutput and the current
error handling to apply one of these two fixes so JSON-mode failures return a
non-zero exit code.

In @src/commands/push/devices/remove.ts:
- Around line 77-101: The JSON error branch in the catch of the remove device
command logs the error via this.formatJsonOutput when
this.shouldOutputJson(flags) is true but does not exit with a non-zero code,
causing inconsistent exit behavior versus the non-JSON branch that calls
this.error(); modify the catch so after logging the JSON error (using
this.log(this.formatJsonOutput(...))) you invoke this.error(...) or otherwise
call process.exit(1) to ensure the command exits with code 1 on error; locate
the catch block around the remove device handler where errorMessage/errorCode
are computed and add the exit behavior for the JSON branch while preserving the
existing 40400-specific message semantics.

In @test/e2e/push/publish-e2e.test.ts:
- Around line 73-76: The test failure handler is being registered too late; move
the setupTestFailureHandler() call from the afterEach block into a beforeEach so
it registers before each test runs. Specifically, remove
setupTestFailureHandler() from the afterEach that currently contains
setupTestFailureHandler() and await cleanupTrackedResources(), add a
beforeEach(() => setupTestFailureHandler()) (or await if it returns a promise)
and leave await cleanupTrackedResources() in afterEach to continue cleanup after
each test. Ensure you reference setupTestFailureHandler, beforeEach, and
afterEach when applying the change.
🧹 Nitpick comments (20)
src/commands/push/batch-publish.ts (1)

7-240: Batch publish logic is sound; consider tightening result typing for maintainability

The overall flow (payload parsing from string/file/stdin, validation, 1–10k bound checks, REST call, and JSON vs human-readable summarization) looks correct and in line with the new push batch-publish semantics.

The only improvement I’d suggest is around typing and result processing:

  • response.items and r.error are treated as Record<string, unknown>, which forces a lot of loose casting and property access (error.message, error.code) that can be awkward under strict TS/ESLint.
  • Defining a dedicated result type (e.g. type BatchPublishResultItem = { error?: { message?: string; code?: string | number } }) and casting response.items once would make filters and logging safer and clearer.
  • As a small nit, you could compute successful/failed in a single reduce pass over results, but that’s purely optional.

Functionally this is fine; the above is a readability/typing refinement you can defer.

README.md (1)

837-3921: Push command documentation is thorough; address markdownlint fenced‑code language warnings

The new ably push and related sections (config/devices/channels/publish/batch-publish) are well-documented and match the command surfaces and flag semantics added in this PR.

However, markdownlint is flagging a number of fenced code blocks without a language (MD040), and several of those fences are in newly added/updated sections (for example around the ably push, ably push batch-publish, and ably config command docs where blocks start with plain instead ofsh or similar). This will keep lint failing.

I’d recommend updating the newly introduced fences to include a language (e.g. ```sh-session or ```bash for CLI examples, ```json where appropriate). If you’re generating this via oclif’s README tooling, it may be worth adjusting the generator config so future updates also include languages.

src/commands/push/publish.ts (1)

187-206: File path detection heuristic may misidentify valid JSON.

The check !payload.trim().startsWith("{") && !payload.trim().startsWith("[") will incorrectly treat a JSON string starting with whitespace-prefixed content or other valid JSON (like a quoted string "..." or a number) as a file path.

🔎 Suggested improvement

Consider checking file existence first or using a more explicit flag like --payload-file:

  private parsePayload(payload: string): Record<string, unknown> {
    let jsonString = payload;

    // Check if it's a file path
-   if (!payload.trim().startsWith("{") && !payload.trim().startsWith("[")) {
+   // Try parsing as JSON first; if it fails and looks like a path, try file
+   try {
+     return JSON.parse(payload) as Record<string, unknown>;
+   } catch {
      const filePath = path.resolve(payload);
-
      if (!fs.existsSync(filePath)) {
-       throw new Error(`File not found: ${filePath}`);
+       throw new Error(`Invalid JSON format or file not found: ${filePath}`);
      }
-
      jsonString = fs.readFileSync(filePath, "utf8");
+     try {
+       return JSON.parse(jsonString) as Record<string, unknown>;
+     } catch {
+       throw new Error("Invalid JSON format in payload file");
+     }
    }
-
-   try {
-     return JSON.parse(jsonString) as Record<string, unknown>;
-   } catch {
-     throw new Error("Invalid JSON format in --payload");
-   }
  }
test/unit/commands/push/channels/remove-where.test.ts (1)

92-112: Consider adding mutual exclusivity validation test.

The validation tests cover missing --channel and missing filter criteria. Per the pattern in publish.ts which enforces mutual exclusivity of --device-id and --client-id, consider adding a test to verify the command rejects both being specified simultaneously (if the command implements this validation).

it("should reject when both --device-id and --client-id are provided", async () => {
  const { error } = await runCommand(
    [
      "push:channels:remove-where",
      "--channel",
      "alerts",
      "--device-id",
      "device-1",
      "--client-id",
      "client-1",
      "--force",
    ],
    import.meta.url,
  );

  expect(error).toBeDefined();
  expect(error!.message).toMatch(/only one.*device-id.*client-id/i);
});
test/unit/commands/push/config/set-apns.test.ts (1)

121-128: JSON extraction approach is brittle.

The regex-based JSON extraction (stdout.match(/\{[\s\S]*\}/)) could break if stdout contains multiple JSON objects or nested structures. While the comment acknowledges mixed output, consider:

  • Parsing only the last line of stdout if JSON is always printed last
  • Using a more specific marker/delimiter for JSON output
  • Validating that only one JSON object is matched
🔎 More robust JSON extraction approach
-      // Extract JSON from output (command may log progress messages)
-      const jsonMatch = stdout.match(/\{[\s\S]*\}/);
-      expect(jsonMatch).not.toBeNull();
-      const output = JSON.parse(jsonMatch![0]);
+      // Extract JSON from output (command may log progress messages)
+      // JSON output should be on the last line
+      const lines = stdout.trim().split('\n');
+      const jsonLine = lines[lines.length - 1];
+      const output = JSON.parse(jsonLine);
src/commands/push/config/set-fcm.ts (1)

44-62: Type assertion unnecessary for required flag.

The flags["service-account"] is already typed as string since it's marked required: true. The as string cast on line 46 is redundant.

🔎 Proposed fix
         // Read and validate service account JSON
-        const serviceAccountPath = path.resolve(
-          flags["service-account"] as string,
-        );
+        const serviceAccountPath = path.resolve(flags["service-account"]);
test/e2e/push/publish-e2e.test.ts (1)

26-52: Test device cleanup could leave orphaned registration on beforeAll failure.

If the device registration in beforeAll succeeds but subsequent code fails, the device won't be cleaned up because afterAll only runs after beforeAll completes. Consider wrapping the registration in a try-catch that stores the device ID only on success.

This is a minor reliability concern for E2E test cleanup. The current approach is acceptable since the fake token and unique ID minimize collision risk.

src/commands/push/devices/list.ts (1)

124-130: Clarify pagination guidance for users.

The pagination hint says "Use --limit to show more" but doesn't explain that there's a hard 1000-item cap (Line 62) or how to retrieve additional pages beyond that limit. Users might expect --limit 2000 to work or need guidance on pagination tokens.

Consider enhancing the message:

🔎 Suggested message improvement
         if (devicesPage.hasNext()) {
           this.log(
             chalk.yellow(
-              `Showing first ${devices.length} devices. Use --limit to show more.`,
+              `Showing first ${devices.length} devices. Use --limit (max 1000) to retrieve more results. Pagination beyond 1000 items requires multiple API calls.`,
             ),
           );
         }
src/services/control-api.ts (1)

489-543: Refactor duplicated error handling logic.

The error handling in uploadApnsP12 (lines 521-542) duplicates the logic from the private request method (lines 570-611). This creates a maintenance burden—any future improvements to error messages or parsing would need to be applied in both places.

🔎 Proposed refactor to extract common error handling

Consider extracting the error response parsing into a shared private method:

private async parseErrorResponse(response: Response): Promise<string> {
  const responseBody = await response.text();
  let responseData: unknown = responseBody;
  try {
    responseData = JSON.parse(responseBody);
  } catch {
    /* Ignore parsing errors */
  }

  let errorMessage = `API request failed (${response.status} ${response.statusText})`;
  if (
    typeof responseData === "object" &&
    responseData !== null &&
    "message" in responseData &&
    typeof responseData.message === "string"
  ) {
    errorMessage += `: ${responseData.message}`;
  } else if (
    typeof responseData === "string" &&
    responseData.length < 100
  ) {
    errorMessage += `: ${responseData}`;
  }
  return errorMessage;
}

Then use it in both uploadApnsP12 and the request method:

     if (!response.ok) {
-      const responseBody = await response.text();
-      let responseData: unknown = responseBody;
-      try {
-        responseData = JSON.parse(responseBody);
-      } catch {
-        /* Ignore parsing errors */
-      }
-
-      let errorMessage = `API request failed (${response.status} ${response.statusText})`;
-      if (
-        typeof responseData === "object" &&
-        responseData !== null &&
-        "message" in responseData &&
-        typeof responseData.message === "string"
-      ) {
-        errorMessage += `: ${responseData.message}`;
-      }
+      const errorMessage = await this.parseErrorResponse(response);
       throw new Error(errorMessage);
     }
src/commands/push/config/show.ts (1)

44-45: Consider simplifying null/undefined checks.

The pattern !== null && !== undefined is verbose. Since both null and undefined are falsy in the context of checking if configuration exists, you could simplify these checks.

🔎 Suggested simplification
           apns: {
             configured:
-              app.apnsAuthType !== null && app.apnsAuthType !== undefined,
+              app.apnsAuthType != null,
             authType: app.apnsAuthType || null,

Or, if you want to be explicit about the configuration check:

-            app.apnsAuthType !== null && app.apnsAuthType !== undefined,
+            Boolean(app.apnsAuthType),

Note: Using != null (loose equality) checks for both null and undefined, which is one of the few acceptable uses of loose equality in TypeScript.

Also applies to: 67-68, 99-100

src/commands/push/channels/list-channels.ts (1)

77-77: Type assertion could be more precise.

The type assertion (error as { code?: number }).code works but doesn't provide type safety. Consider using a type guard or checking the property existence first.

🔎 Suggested improvement
     const errorMessage =
       error instanceof Error ? error.message : String(error);
-    const errorCode = (error as { code?: number }).code;
+    const errorCode = 
+      error && typeof error === 'object' && 'code' in error && typeof error.code === 'number'
+        ? error.code
+        : undefined;

Or define a helper type guard:

function hasErrorCode(error: unknown): error is { code: number } {
  return error != null && typeof error === 'object' && 'code' in error && typeof error.code === 'number';
}

const errorCode = hasErrorCode(error) ? error.code : undefined;
src/commands/push/config/clear-apns.ts (1)

78-86: Consider stronger typing for the app update payload.

The type cast to Record<string, unknown> on line 85 bypasses TypeScript's type checking and could mask potential issues with the updateApp API signature.

🔎 Suggested refactor to use proper typing
-        await api.updateApp(appId, {
-          apnsCertificate: null,
-          apnsPrivateKey: null,
-          applePushKeyId: null,
-          applePushTeamId: null,
-          applePushBundleId: null,
-          apnsUsesSandboxCert: false,
-        } as Record<string, unknown>);
+        await api.updateApp(appId, {
+          apnsCertificate: null,
+          apnsPrivateKey: null,
+          applePushKeyId: null,
+          applePushTeamId: null,
+          applePushBundleId: null,
+          apnsUsesSandboxCert: false,
+        });

If TypeScript complains, this indicates the updateApp method's type signature may need adjustment to accept nullable fields, which would be more correct than suppressing the type check.

src/commands/push/devices/save.ts (2)

164-183: Consider more robust file path detection.

The file path detection on line 168 using !data.trim().startsWith("{") could incorrectly treat a filename starting with { as inline JSON. While unlikely in practice, a more explicit check would be safer.

🔎 More robust file detection approach
  private parseDeviceData(data: string): Ably.DeviceDetails {
-    let jsonString = data;
-
-    // Check if it's a file path
-    if (!data.trim().startsWith("{")) {
+    let jsonString: string;
+    
+    // Try to parse as JSON first; if it fails and looks like a path, try reading as file
+    const trimmed = data.trim();
+    if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+      // Likely inline JSON
+      try {
+        return JSON.parse(trimmed) as Ably.DeviceDetails;
+      } catch {
+        throw new Error("Invalid JSON format in --data");
+      }
+    } else {
+      // Treat as file path
      const filePath = path.resolve(data);
      
      if (!fs.existsSync(filePath)) {
        throw new Error(`File not found: ${filePath}`);
      }
      
      jsonString = fs.readFileSync(filePath, "utf8");
+      try {
+        return JSON.parse(jsonString) as Ably.DeviceDetails;
+      } catch {
+        throw new Error(`Invalid JSON format in file: ${filePath}`);
+      }
    }
-
-    try {
-      return JSON.parse(jsonString) as Ably.DeviceDetails;
-    } catch {
-      throw new Error("Invalid JSON format in --data");
-    }
  }

This approach tries to parse JSON first, and only treats the input as a file path if it doesn't look like JSON, reducing ambiguity.


240-246: Enhance error message with parsing details.

When JSON parsing fails for metadata (line 243), the error message could be more helpful by including the underlying parse error, which would aid debugging for users.

🔎 Include parse error in message
    if (flags.metadata) {
      try {
        device.metadata = JSON.parse(flags.metadata as string);
-      } catch {
-        throw new Error("Invalid JSON format in --metadata");
+      } catch (error) {
+        const parseError = error instanceof Error ? error.message : String(error);
+        throw new Error(`Invalid JSON format in --metadata: ${parseError}`);
      }
    }

Similarly, consider enhancing the error messages in parseDeviceData (lines 180-182) to include parse errors.

src/commands/push/channels/remove.ts (1)

116-139: Missing exit after JSON error output.

In the error handler, when outputting JSON errors (lines 122-131), the code continues to the else block (lines 132-138) which calls this.error(). Since this.error() exits the process, this won't cause issues in practice, but the logic suggests JSON output should return early to avoid the else branch entirely.

🔎 Suggested improvement for clarity
       if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonOutput(
             {
               error: errorMessage,
               code: errorCode,
               success: false,
             },
             flags,
           ),
         );
+        return;
       } else {
         if (errorCode === 40400) {
           this.error(`Subscription not found`);
         } else {
           this.error(`Error removing subscription: ${errorMessage}`);
         }
       }
src/commands/push/channels/save.ts (1)

100-111: Missing early return after JSON error output.

Similar to the remove command, when outputting JSON errors (lines 101-110), the code continues to the else block which calls this.error(). While this works because this.error() exits, adding an early return after JSON output improves code clarity and prevents accidental fall-through if the error method changes.

🔎 Suggested improvement for clarity
       if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonOutput(
             {
               error: errorMessage,
               code: errorCode,
               success: false,
             },
             flags,
           ),
         );
+        return;
       } else {
         if (errorCode === 40100) {
src/commands/push/channels/list.ts (2)

100-106: Consider handling subscriptions with neither deviceId nor clientId.

The table output logic assumes each subscription has either deviceId or clientId (lines 101-105). If the API returns a subscription with neither field populated, no row would be displayed, potentially confusing users.

🔎 Suggested defensive handling
         // List subscriptions
         for (const sub of subscriptions) {
           if (sub.deviceId) {
             this.log(`${"device".padEnd(12)}${sub.deviceId}`);
           } else if (sub.clientId) {
             this.log(`${"client".padEnd(12)}${sub.clientId}`);
+          } else {
+            this.log(`${"unknown".padEnd(12)}${chalk.dim("(no ID)")}`);
           }
         }

113-124: Missing early return after JSON error output.

When outputting JSON errors, add an early return to prevent fall-through to the else branch (similar pattern seen in other commands).

🔎 Suggested improvement
       if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonOutput(
             {
               error: errorMessage,
               code: errorCode,
               success: false,
             },
             flags,
           ),
         );
+        return;
       } else {
         this.error(`Error listing subscriptions: ${errorMessage}`);
       }
src/commands/push/devices/get.ts (2)

107-113: Token redaction could expose short tokens.

The redaction logic only applies when token.length > 8 (line 110). For tokens with 8 or fewer characters, no redaction occurs and they could be fully exposed. While very short tokens are unlikely in production, consider applying redaction for all non-empty tokens or logging a warning for unexpectedly short tokens.

🔎 Suggested improvement for comprehensive redaction
           const recipient = device.push.recipient as Record<string, unknown>;
           const token = recipient.registrationToken || recipient.deviceToken;
 
-          if (typeof token === "string" && token.length > 8) {
+          if (typeof token === "string" && token.length > 0) {
+            if (token.length <= 8) {
+              const redacted = "***";
+              this.log(`  ${chalk.dim("Token:")}        ${redacted} (redacted)`);
+            } else {
               const redacted = `${token.slice(0, 4)}...${token.slice(-4)}`;
               this.log(`  ${chalk.dim("Token:")}        ${redacted} (redacted)`);
+            }
           }
         }

129-140: Missing early return after JSON error output.

Add an early return after JSON error output for consistency with error handling patterns.

🔎 Suggested improvement
       if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonOutput(
             {
               error: errorMessage,
               code: errorCode,
               success: false,
             },
             flags,
           ),
         );
+        return;
       } else {
         if (errorCode === 40400) {
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9b37916 and a565487.

⛔ Files ignored due to path filters (2)
  • test/fixtures/push/test-apns-cert.pem is excluded by !**/*.pem
  • test/fixtures/push/test-apns-private-key.pem is excluded by !**/*.pem
📒 Files selected for processing (49)
  • README.md
  • src/commands/apps/set-apns-p12.ts
  • src/commands/push/batch-publish.ts
  • src/commands/push/channels/index.ts
  • src/commands/push/channels/list-channels.ts
  • src/commands/push/channels/list.ts
  • src/commands/push/channels/remove-where.ts
  • src/commands/push/channels/remove.ts
  • src/commands/push/channels/save.ts
  • src/commands/push/config/clear-apns.ts
  • src/commands/push/config/clear-fcm.ts
  • src/commands/push/config/index.ts
  • src/commands/push/config/set-apns.ts
  • src/commands/push/config/set-fcm.ts
  • src/commands/push/config/show.ts
  • src/commands/push/devices/get.ts
  • src/commands/push/devices/index.ts
  • src/commands/push/devices/list.ts
  • src/commands/push/devices/remove-where.ts
  • src/commands/push/devices/remove.ts
  • src/commands/push/devices/save.ts
  • src/commands/push/index.ts
  • src/commands/push/publish.ts
  • src/services/control-api.ts
  • test/e2e/push/channels-e2e.test.ts
  • test/e2e/push/devices-e2e.test.ts
  • test/e2e/push/publish-e2e.test.ts
  • test/fixtures/push/README.md
  • test/fixtures/push/test-apns-key.p8
  • test/fixtures/push/test-fcm-service-account.json
  • test/helpers/mock-ably-rest.ts
  • test/unit/commands/apps/set-apns-p12.test.ts
  • test/unit/commands/push/batch-publish.test.ts
  • test/unit/commands/push/channels/list-channels.test.ts
  • test/unit/commands/push/channels/list.test.ts
  • test/unit/commands/push/channels/remove-where.test.ts
  • test/unit/commands/push/channels/remove.test.ts
  • test/unit/commands/push/channels/save.test.ts
  • test/unit/commands/push/config/clear-apns.test.ts
  • test/unit/commands/push/config/clear-fcm.test.ts
  • test/unit/commands/push/config/set-apns.test.ts
  • test/unit/commands/push/config/set-fcm.test.ts
  • test/unit/commands/push/config/show.test.ts
  • test/unit/commands/push/devices/get.test.ts
  • test/unit/commands/push/devices/list.test.ts
  • test/unit/commands/push/devices/remove-where.test.ts
  • test/unit/commands/push/devices/remove.test.ts
  • test/unit/commands/push/devices/save.test.ts
  • test/unit/commands/push/publish.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.test.ts

📄 CodeRabbit inference engine (.cursor/rules/AI-Assistance.mdc)

**/*.test.ts: When testing, mock the Ably SDK properly by stubbing the constructor (e.g., sinon.stub(Ably, 'Rest').returns(mockClient)).
Always ensure resources are properly closed/cleaned up in tests (e.g., call await client.close() and sinon.restore() in afterEach).

Files:

  • test/unit/commands/push/devices/get.test.ts
  • test/unit/commands/apps/set-apns-p12.test.ts
  • test/unit/commands/push/channels/save.test.ts
  • test/unit/commands/push/channels/remove-where.test.ts
  • test/unit/commands/push/devices/list.test.ts
  • test/unit/commands/push/config/set-fcm.test.ts
  • test/unit/commands/push/config/set-apns.test.ts
  • test/unit/commands/push/channels/list-channels.test.ts
  • test/unit/commands/push/config/show.test.ts
  • test/unit/commands/push/devices/remove.test.ts
  • test/unit/commands/push/devices/save.test.ts
  • test/unit/commands/push/channels/list.test.ts
  • test/e2e/push/devices-e2e.test.ts
  • test/e2e/push/publish-e2e.test.ts
  • test/unit/commands/push/config/clear-fcm.test.ts
  • test/unit/commands/push/batch-publish.test.ts
  • test/unit/commands/push/publish.test.ts
  • test/unit/commands/push/devices/remove-where.test.ts
  • test/e2e/push/channels-e2e.test.ts
  • test/unit/commands/push/channels/remove.test.ts
  • test/unit/commands/push/config/clear-apns.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/Development.mdc)

**/*.{ts,tsx}: Use TypeScript and follow best practice naming conventions
ESLint is used to ensure all code adheres best practices; ensure you check that all code passes the linter before concluding your work.

Files:

  • test/unit/commands/push/devices/get.test.ts
  • test/unit/commands/apps/set-apns-p12.test.ts
  • test/unit/commands/push/channels/save.test.ts
  • src/commands/push/channels/remove-where.ts
  • test/unit/commands/push/channels/remove-where.test.ts
  • test/unit/commands/push/devices/list.test.ts
  • src/commands/push/config/clear-apns.ts
  • src/commands/push/config/index.ts
  • test/unit/commands/push/config/set-fcm.test.ts
  • test/unit/commands/push/config/set-apns.test.ts
  • test/unit/commands/push/channels/list-channels.test.ts
  • test/unit/commands/push/config/show.test.ts
  • test/unit/commands/push/devices/remove.test.ts
  • test/unit/commands/push/devices/save.test.ts
  • src/commands/push/batch-publish.ts
  • test/unit/commands/push/channels/list.test.ts
  • src/commands/push/channels/save.ts
  • src/commands/push/channels/list.ts
  • src/commands/push/channels/index.ts
  • test/e2e/push/devices-e2e.test.ts
  • test/e2e/push/publish-e2e.test.ts
  • src/commands/push/devices/get.ts
  • src/commands/push/config/set-fcm.ts
  • src/commands/push/devices/index.ts
  • src/commands/push/devices/remove.ts
  • src/commands/push/devices/list.ts
  • src/commands/push/devices/remove-where.ts
  • src/commands/push/channels/list-channels.ts
  • test/unit/commands/push/config/clear-fcm.test.ts
  • test/unit/commands/push/batch-publish.test.ts
  • test/unit/commands/push/publish.test.ts
  • src/commands/push/config/set-apns.ts
  • src/commands/push/config/show.ts
  • src/commands/push/devices/save.ts
  • test/unit/commands/push/devices/remove-where.test.ts
  • src/commands/push/config/clear-fcm.ts
  • src/commands/push/publish.ts
  • test/helpers/mock-ably-rest.ts
  • src/commands/push/index.ts
  • test/e2e/push/channels-e2e.test.ts
  • test/unit/commands/push/channels/remove.test.ts
  • src/services/control-api.ts
  • src/commands/push/channels/remove.ts
  • src/commands/apps/set-apns-p12.ts
  • test/unit/commands/push/config/clear-apns.test.ts
{test/**/*.{ts,js},**/*.{test,spec}.{ts,js}}

📄 CodeRabbit inference engine (.cursor/rules/Development.mdc)

When new features are added or changes made, tests must be updated or added, and it is your responsibility to ensure the tests pass before deeming your work complete.

Files:

  • test/unit/commands/push/devices/get.test.ts
  • test/unit/commands/apps/set-apns-p12.test.ts
  • test/unit/commands/push/channels/save.test.ts
  • test/unit/commands/push/channels/remove-where.test.ts
  • test/unit/commands/push/devices/list.test.ts
  • test/unit/commands/push/config/set-fcm.test.ts
  • test/unit/commands/push/config/set-apns.test.ts
  • test/unit/commands/push/channels/list-channels.test.ts
  • test/unit/commands/push/config/show.test.ts
  • test/unit/commands/push/devices/remove.test.ts
  • test/unit/commands/push/devices/save.test.ts
  • test/unit/commands/push/channels/list.test.ts
  • test/e2e/push/devices-e2e.test.ts
  • test/e2e/push/publish-e2e.test.ts
  • test/unit/commands/push/config/clear-fcm.test.ts
  • test/unit/commands/push/batch-publish.test.ts
  • test/unit/commands/push/publish.test.ts
  • test/unit/commands/push/devices/remove-where.test.ts
  • test/helpers/mock-ably-rest.ts
  • test/e2e/push/channels-e2e.test.ts
  • test/unit/commands/push/channels/remove.test.ts
  • test/unit/commands/push/config/clear-apns.test.ts
src/commands/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/AI-Assistance.mdc)

src/commands/**/*.ts: Commands should follow the oclif structure: export default class MyCommand extends Command { ... }
Command should support auth via the standard auth helper (import { getAuth } from '../../helpers/auth') and use it in the run method.
Use try/catch for error handling in commands, and provide user-friendly error messages (e.g., handle error.code === 40100 for authentication failures).
Do not use direct HTTP requests (e.g., fetch) for data plane APIs; use the Ably SDK instead.
Do not use hardcoded endpoint URLs; use configuration values (e.g., flags.controlHost, config.controlHost) when constructing URLs.
Do not use non-standard command structures (e.g., export default class { async execute(args) { ... } }); always use the oclif Command class.

Files:

  • src/commands/push/channels/remove-where.ts
  • src/commands/push/config/clear-apns.ts
  • src/commands/push/config/index.ts
  • src/commands/push/batch-publish.ts
  • src/commands/push/channels/save.ts
  • src/commands/push/channels/list.ts
  • src/commands/push/channels/index.ts
  • src/commands/push/devices/get.ts
  • src/commands/push/config/set-fcm.ts
  • src/commands/push/devices/index.ts
  • src/commands/push/devices/remove.ts
  • src/commands/push/devices/list.ts
  • src/commands/push/devices/remove-where.ts
  • src/commands/push/channels/list-channels.ts
  • src/commands/push/config/set-apns.ts
  • src/commands/push/config/show.ts
  • src/commands/push/devices/save.ts
  • src/commands/push/config/clear-fcm.ts
  • src/commands/push/publish.ts
  • src/commands/push/index.ts
  • src/commands/push/channels/remove.ts
  • src/commands/apps/set-apns-p12.ts
src/commands/**/*.{ts,js}

📄 CodeRabbit inference engine (.cursor/rules/Development.mdc)

Follow oclif framework best practices as described in the oclif documentation.

Files:

  • src/commands/push/channels/remove-where.ts
  • src/commands/push/config/clear-apns.ts
  • src/commands/push/config/index.ts
  • src/commands/push/batch-publish.ts
  • src/commands/push/channels/save.ts
  • src/commands/push/channels/list.ts
  • src/commands/push/channels/index.ts
  • src/commands/push/devices/get.ts
  • src/commands/push/config/set-fcm.ts
  • src/commands/push/devices/index.ts
  • src/commands/push/devices/remove.ts
  • src/commands/push/devices/list.ts
  • src/commands/push/devices/remove-where.ts
  • src/commands/push/channels/list-channels.ts
  • src/commands/push/config/set-apns.ts
  • src/commands/push/config/show.ts
  • src/commands/push/devices/save.ts
  • src/commands/push/config/clear-fcm.ts
  • src/commands/push/publish.ts
  • src/commands/push/index.ts
  • src/commands/push/channels/remove.ts
  • src/commands/apps/set-apns-p12.ts
{README.md,docs/**,.cursor/rules/**}

📄 CodeRabbit inference engine (.cursor/rules/Development.mdc)

Update all relevant documentation, including README.md, files within the docs/ directory, and files within the .cursor/rules/ directory if the changes impact requirements, structure, testing procedures, development guidelines, Ably usage, or project structure.

Files:

  • README.md
🧬 Code graph analysis (17)
test/unit/commands/push/devices/get.test.ts (4)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/mock-ably-rest.ts (1)
  • getMockAblyRest (247-252)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/channels/save.test.ts (3)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/channels/remove-where.test.ts (3)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/devices/list.test.ts (4)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/mock-ably-rest.ts (1)
  • getMockAblyRest (247-252)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/channels/list-channels.test.ts (3)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/config/show.test.ts (4)
test/helpers/mock-config-manager.ts (1)
  • getMockConfigManager (476-483)
test/root-hooks.ts (1)
  • afterEach (13-61)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/devices/remove.test.ts (4)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/mock-ably-rest.ts (1)
  • getMockAblyRest (247-252)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/devices/save.test.ts (4)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/mock-ably-rest.ts (1)
  • getMockAblyRest (247-252)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/channels/list.test.ts (3)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/e2e/push/devices-e2e.test.ts (3)
test/helpers/e2e-test-helper.ts (7)
  • SHOULD_SKIP_E2E (18-19)
  • forceExit (637-639)
  • createAblyClient (68-97)
  • resetTestTracking (800-803)
  • setupTestFailureHandler (791-795)
  • cleanupTrackedResources (645-669)
  • E2E_API_KEY (17-17)
test/root-hooks.ts (3)
  • afterAll (63-69)
  • beforeEach (8-11)
  • afterEach (13-61)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/e2e/push/publish-e2e.test.ts (3)
test/helpers/e2e-test-helper.ts (7)
  • SHOULD_SKIP_E2E (18-19)
  • forceExit (637-639)
  • createAblyClient (68-97)
  • resetTestTracking (800-803)
  • setupTestFailureHandler (791-795)
  • cleanupTrackedResources (645-669)
  • E2E_API_KEY (17-17)
test/root-hooks.ts (3)
  • afterAll (63-69)
  • beforeEach (8-11)
  • afterEach (13-61)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/unit/commands/push/config/clear-fcm.test.ts (4)
test/helpers/mock-config-manager.ts (1)
  • getMockConfigManager (476-483)
test/root-hooks.ts (1)
  • afterEach (13-61)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/batch-publish.test.ts (3)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/publish.test.ts (3)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
test/unit/commands/push/devices/remove-where.test.ts (4)
test/root-hooks.ts (1)
  • beforeEach (8-11)
test/helpers/mock-ably-rest.ts (1)
  • getMockAblyRest (247-252)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
src/services/control-api.ts (1)
src/utils/version.ts (1)
  • getCliVersion (13-15)
test/unit/commands/push/config/clear-apns.test.ts (4)
test/helpers/mock-config-manager.ts (1)
  • getMockConfigManager (476-483)
test/root-hooks.ts (1)
  • afterEach (13-61)
test/helpers/command-helpers.ts (1)
  • runCommand (67-83)
test/helpers/cli-runner.ts (1)
  • stdout (125-127)
🪛 Checkov (3.2.334)
test/fixtures/push/test-fcm-service-account.json

[medium] 5-6: Private Key

(CKV_SECRET_13)

🪛 Gitleaks (8.30.0)
test/unit/commands/push/config/set-apns.test.ts

[high] 51-51: Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.

(private-key)

🪛 markdownlint-cli2 (0.18.1)
README.md

841-841: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2391-2391: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2428-2428: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2465-2465: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2749-2749: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3431-3431: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3461-3461: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3497-3497: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3520-3520: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3562-3562: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3598-3598: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3638-3638: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3676-3676: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3712-3712: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3737-3737: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3774-3774: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3811-3811: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3855-3855: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3891-3891: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3927-3927: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3954-3954: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


3992-3992: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4037-4037: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4078-4078: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4114-4114: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4173-4173: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4228-4228: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4254-4254: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4295-4295: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4335-4335: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4371-4371: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4400-4400: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4441-4441: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4464-4464: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4521-4521: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4542-4542: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4587-4587: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


4728-4728: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: auth-tests
  • GitHub Check: ui-tests
  • GitHub Check: session-tests
  • GitHub Check: e2e-cli
  • GitHub Check: Cursor Bugbot
  • GitHub Check: test

Comment on lines 79 to 92
if (this.shouldOutputJson(flags)) {
this.log(
this.formatJsonOutput(
{
error: errorMessage,
code: errorCode,
success: false,
},
flags,
),
);
} else {
this.error(`Error listing channels: ${errorMessage}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if this pattern is consistent across push commands
rg -n 'shouldOutputJson' --type=ts -g 'src/commands/push/**/*.ts' -A10 -B2 | head -100

Repository: ably/ably-cli

Length of output: 170


🏁 Script executed:

# Also look at the specific file mentioned for comparison
cat -n src/commands/push/devices/list.ts | grep -A15 -B5 "shouldOutputJson"

Repository: ably/ably-cli

Length of output: 1546


🏁 Script executed:

# Search for how this.error() is used in the codebase to understand the pattern
rg -n 'this\.error\(' --type=ts -g 'src/commands/**/*.ts' -B3 | head -80

Repository: ably/ably-cli

Length of output: 4561


🏁 Script executed:

# Search for error handling in JSON mode across all push commands
rg -n 'this\.shouldOutputJson' --type=ts -g 'src/commands/push/**/*.ts' -A15 | grep -E "(shouldOutputJson|this\.error|this\.exit)" | head -60

Repository: ably/ably-cli

Length of output: 4921


🏁 Script executed:

# Look at push/publish.ts to see how it handles errors in JSON mode
sed -n '160,185p' src/commands/push/publish.ts

Repository: ably/ably-cli

Length of output: 754


🏁 Script executed:

# Check if there's a pattern in channels commands for list-channels
ls -la src/commands/push/channels/

Repository: ably/ably-cli

Length of output: 530


Add this.exit(1) after logging JSON errors to ensure proper exit code.

When --json is used and an error occurs, the command logs JSON but doesn't call this.exit(1), causing the process to exit with code 0 instead of a non-zero error code. This pattern is inconsistent: push/publish.ts and push/batch-publish.ts correctly call this.exit(1) after logging JSON errors, but many other push commands (including list-channels.ts, devices/list.ts, channels/list.ts, remove commands, etc.) omit this call. Align all push commands to follow the pattern used in publish.ts by adding this.exit(1) in the JSON error path.

🤖 Prompt for AI Agents
In @src/commands/push/channels/list-channels.ts around lines 79 - 92, The JSON
error path in listChannelsCommand (check the this.shouldOutputJson(flags) branch
that logs this.formatJsonOutput({...})) logs the error but doesn't terminate
with a non-zero exit; add a call to this.exit(1) immediately after the
this.log(...) in that branch so the process returns a failure code when --json
is used, mirroring the pattern used in push/publish.ts and
push/batch-publish.ts.

Comment on lines 114 to 133
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorCode = (error as { code?: number }).code;

if (this.shouldOutputJson(flags)) {
this.log(
this.formatJsonOutput(
{
error: errorMessage,
code: errorCode,
success: false,
},
flags,
),
);
} else {
this.error(`Error removing subscriptions: ${errorMessage}`);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing non-zero exit code in JSON error output.

When an error occurs in JSON mode, the command logs the error but doesn't exit with a non-zero code. This could cause scripts relying on exit codes to incorrectly assume success.

🔎 Proposed fix
       if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonOutput(
             {
               error: errorMessage,
               code: errorCode,
               success: false,
             },
             flags,
           ),
         );
+        this.exit(1);
       } else {
         this.error(`Error removing subscriptions: ${errorMessage}`);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorCode = (error as { code?: number }).code;
if (this.shouldOutputJson(flags)) {
this.log(
this.formatJsonOutput(
{
error: errorMessage,
code: errorCode,
success: false,
},
flags,
),
);
} else {
this.error(`Error removing subscriptions: ${errorMessage}`);
}
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorCode = (error as { code?: number }).code;
if (this.shouldOutputJson(flags)) {
this.log(
this.formatJsonOutput(
{
error: errorMessage,
code: errorCode,
success: false,
},
flags,
),
);
this.exit(1);
} else {
this.error(`Error removing subscriptions: ${errorMessage}`);
}
}
🤖 Prompt for AI Agents
In @src/commands/push/channels/remove-where.ts around lines 114 - 133, The
JSON-path in the catch block for the remove-where command logs the error but
doesn't terminate with a non-zero exit code; after calling
this.log(this.formatJsonOutput(...)) when this.shouldOutputJson(flags) is true,
call this.exit(1) (or process.exit(1) if this.exit isn’t available) so the
command exits with a non-zero status; locate the catch block that builds
errorMessage/errorCode and uses this.shouldOutputJson(flags) and
this.formatJsonOutput to add the exit call immediately after logging the JSON
error.

Comment on lines 136 to 148
if (this.shouldOutputJson(flags)) {
this.log(
this.formatJsonOutput(
{
error: errorMessage,
success: false,
},
flags,
),
);
} else {
this.error(`Error listing devices: ${errorMessage}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for JSON error output patterns in other commands
rg -n -A5 -B5 'shouldOutputJson.*flags.*error' --type=ts -g 'src/commands/**/*.ts'

Repository: ably/ably-cli

Length of output: 39


🏁 Script executed:

cat -n src/commands/push/devices/list.ts | head -160 | tail -40

Repository: ably/ably-cli

Length of output: 1325


🏁 Script executed:

#!/bin/bash
# Search for error handling patterns in commands
rg -n 'this\.error\(' --type=ts -g 'src/commands/**/*.ts' | head -20

Repository: ably/ably-cli

Length of output: 1735


🏁 Script executed:

cat -n src/commands/push/devices/list.ts | sed -n '120,160p'

Repository: ably/ably-cli

Length of output: 1001


🏁 Script executed:

#!/bin/bash
# Search for commands that check shouldOutputJson to understand error handling patterns
rg -n -B10 'shouldOutputJson' --type=ts -g 'src/commands/**/*.ts' | grep -A10 -B10 'catch\|error'

Repository: ably/ably-cli

Length of output: 50370


🏁 Script executed:

#!/bin/bash
# Search specifically for error handling in try-catch blocks where JSON output might be relevant
rg -n -A15 'catch\s*\(' --type=ts -g 'src/commands/**/*.ts' | head -100

Repository: ably/ably-cli

Length of output: 6321


🏁 Script executed:

#!/bin/bash
# Check if there's a base Command class that defines shouldOutputJson and how it should be used
find . -name '*.ts' -type f | xargs grep -l 'shouldOutputJson' | grep -v node_modules | head -5

Repository: ably/ably-cli

Length of output: 212


🏁 Script executed:

cat -n src/base-command.ts | head -100

Repository: ably/ably-cli

Length of output: 4024


🏁 Script executed:

#!/bin/bash
# Search for jsonError method in base command
rg -n 'jsonError' --type=ts -g 'src/**/*.ts' | head -20

Repository: ably/ably-cli

Length of output: 1669


🏁 Script executed:

cat -n src/commands/integrations/list.ts | sed -n '110,130p'

Repository: ably/ably-cli

Length of output: 692


🏁 Script executed:

cat -n src/commands/push/devices/list.ts | sed -n '132,150p'

Repository: ably/ably-cli

Length of output: 628


🏁 Script executed:

#!/bin/bash
# Search for jsonError method definition in base command
rg -n 'jsonError' --type=ts -A10 'src/base-command.ts' | head -50

Repository: ably/ably-cli

Length of output: 446


🏁 Script executed:

#!/bin/bash
# Search for jsonError in interactive-base-command as well
rg -n 'jsonError\(' --type=ts -A5 'src/interactive-base-command.ts'

Repository: ably/ably-cli

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search more broadly for where jsonError is defined
find ./src -name '*.ts' -type f | xargs grep -l 'jsonError.*=' | head -10

Repository: ably/ably-cli

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check another command to confirm the pattern - push/publish.ts
cat -n src/commands/push/publish.ts | sed -n '158,180p'

Repository: ably/ably-cli

Length of output: 864


🏁 Script executed:

#!/bin/bash
# Let's also check push/batch-publish.ts to see the pattern
cat -n src/commands/push/batch-publish.ts | sed -n '134,160p'

Repository: ably/ably-cli

Length of output: 943


Call this.exit(1) when logging JSON errors or use this.jsonError() helper instead.

When --json is used and an error occurs, the command logs JSON output via this.log() but doesn't exit with a non-zero code. This will cause automated tooling relying on exit codes to incorrectly treat the error as success.

Other commands in the codebase follow one of two patterns:

  • Use this.jsonError() helper (e.g., src/commands/integrations/list.ts), which automatically exits with code 1
  • Use this.log() followed by explicit this.exit(1) (e.g., src/commands/push/publish.ts, src/commands/push/batch-publish.ts)
Current code (lines 136-148)
      if (this.shouldOutputJson(flags)) {
        this.log(
          this.formatJsonOutput(
            {
              error: errorMessage,
              success: false,
            },
            flags,
          ),
        );
      } else {
        this.error(`Error listing devices: ${errorMessage}`);
      }

Either replace with this.jsonError() or add this.exit(1) after the JSON log.

🤖 Prompt for AI Agents
In @src/commands/push/devices/list.ts around lines 136 - 148, The JSON error
path in the devices list command uses this.log(this.formatJsonOutput(...)) but
doesn't terminate with a non-zero exit, so update the branch inside
shouldOutputJson(flags) to either call this.jsonError(...) passing the same
structured payload (error: errorMessage, success: false) or keep the log call
and immediately invoke this.exit(1) after it; locate the logic around
shouldOutputJson, formatJsonOutput and the current error handling to apply one
of these two fixes so JSON-mode failures return a non-zero exit code.

Comment on lines 77 to 101
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorCode = (error as { code?: number }).code;

if (this.shouldOutputJson(flags)) {
this.log(
this.formatJsonOutput(
{
error: errorMessage,
code: errorCode,
success: false,
},
flags,
),
);
} else {
if (errorCode === 40400) {
this.error(`Device not found: ${args.deviceId}`);
} else {
this.error(`Error removing device: ${errorMessage}`);
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find all push commands to see error handling patterns
fd --type f 'remove\.ts$' src/commands/ | head -20

Repository: ably/ably-cli

Length of output: 177


🏁 Script executed:

#!/bin/bash
# Check other push commands for error handling patterns
find src/commands/push -name "*.ts" -type f | head -10

Repository: ably/ably-cli

Length of output: 432


🏁 Script executed:

#!/bin/bash
# Search for error handling in other commands that use JSON output
rg "shouldOutputJson" src/commands/ -A 10 -B 2 | head -100

Repository: ably/ably-cli

Length of output: 5953


🏁 Script executed:

#!/bin/bash
# Look at the specific file to understand the context better
wc -l src/commands/push/devices/remove.ts

Repository: ably/ably-cli

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Read the entire remove.ts file to understand full context
cat -n src/commands/push/devices/remove.ts

Repository: ably/ably-cli

Length of output: 3599


🏁 Script executed:

#!/bin/bash
# Look for test files for the remove command
find . -path "*/test*" -o -path "*/spec*" | grep -i "remove" | grep -i "device" | head -10

Repository: ably/ably-cli

Length of output: 160


🏁 Script executed:

#!/bin/bash
# Search for test files more broadly
fd "remove\.test\." src/commands/push/devices/ 2>/dev/null || fd "remove\.spec\." src/commands/push/devices/ 2>/dev/null || find . -type f -name "*remove*.test.*" | grep push | grep device

Repository: ably/ably-cli

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if test directory exists and find push device tests
find . -type d -name "__tests__" -o -type d -name "test" -o -type d -name "tests" | head -5

Repository: ably/ably-cli

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Look for test files in common locations
ls -la src/commands/push/devices/ | grep -E "\.(test|spec)"

Repository: ably/ably-cli

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Search test directory for device remove tests
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) | grep -i device | head -10

Repository: ably/ably-cli

Length of output: 336


🏁 Script executed:

#!/bin/bash
# Check the base-command to see how error handling is intended
wc -l src/base-command.ts

Repository: ably/ably-cli

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Look at relevant parts of base-command
head -150 src/base-command.ts

Repository: ably/ably-cli

Length of output: 5269


🏁 Script executed:

#!/bin/bash
# Check the test file for remove.ts to see how exit codes are tested
cat -n test/unit/commands/push/devices/remove.test.ts | head -150

Repository: ably/ably-cli

Length of output: 3299


🏁 Script executed:

#!/bin/bash
# Look at the full test file
wc -l test/unit/commands/push/devices/remove.test.ts

Repository: ably/ably-cli

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Check if tests verify exit codes for JSON error scenarios
rg "exit" test/unit/commands/push/devices/remove.test.ts -B 3 -A 3

Repository: ably/ably-cli

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check how error testing is done in the test file
rg "json.*error|error.*json" test/unit/commands/push/devices/remove.test.ts -B 5 -A 5

Repository: ably/ably-cli

Length of output: 39


🏁 Script executed:

#!/bin/bash
# View the entire test file
cat -n test/unit/commands/push/devices/remove.test.ts

Repository: ably/ably-cli

Length of output: 3299


🏁 Script executed:

#!/bin/bash
# Check if other command tests check JSON error scenarios
rg "json.*error|error.*json|--json" test/unit/commands/push/ -l | head -5

Repository: ably/ably-cli

Length of output: 286


🏁 Script executed:

#!/bin/bash
# Look at save.ts test to see if it tests JSON error handling
cat -n test/unit/commands/push/devices/save.test.ts | grep -A 20 "json"

Repository: ably/ably-cli

Length of output: 1412


🏁 Script executed:

#!/bin/bash
# Check if there are any tests that verify exit codes with JSON output
rg "exit\(" test/unit/commands/ -B 3 -A 3 | head -50

Repository: ably/ably-cli

Length of output: 641


🏁 Script executed:

#!/bin/bash
# Look for how other commands handle error exit codes in JSON mode
rg "this\.exit\(" src/commands/ -B 5 -A 2 | head -80

Repository: ably/ably-cli

Length of output: 2608


🏁 Script executed:

#!/bin/bash
# Check interactive-base-command for error method behavior
rg "error\(" src/interactive-base-command.ts -B 3 -A 5

Repository: ably/ably-cli

Length of output: 1028


Add exit(1) in JSON error handling to match error exit codes in non-JSON mode.

The command inconsistently handles errors between JSON and non-JSON output modes. In JSON mode (lines 82-92), the code logs the error but exits with code 0 due to implicit return. The non-JSON path correctly calls this.error() which exits with code 1. This pattern is already established in other push commands (publish.ts, batch-publish.ts).

🔎 Required fix
       if (this.shouldOutputJson(flags)) {
         this.log(
           this.formatJsonOutput(
             {
               error: errorMessage,
               code: errorCode,
               success: false,
             },
             flags,
           ),
         );
+        this.exit(1);
       } else {
🤖 Prompt for AI Agents
In @src/commands/push/devices/remove.ts around lines 77 - 101, The JSON error
branch in the catch of the remove device command logs the error via
this.formatJsonOutput when this.shouldOutputJson(flags) is true but does not
exit with a non-zero code, causing inconsistent exit behavior versus the
non-JSON branch that calls this.error(); modify the catch so after logging the
JSON error (using this.log(this.formatJsonOutput(...))) you invoke
this.error(...) or otherwise call process.exit(1) to ensure the command exits
with code 1 on error; locate the catch block around the remove device handler
where errorMessage/errorCode are computed and add the exit behavior for the JSON
branch while preserving the existing 40400-specific message semantics.

Comment on lines +73 to +76
afterEach(async () => {
setupTestFailureHandler();
await cleanupTrackedResources();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

setupTestFailureHandler should be called in beforeEach, not afterEach.

The setupTestFailureHandler function registers a handler for test failures using onTestFailed. Calling it in afterEach means the handler is registered after the test has already run, which defeats its purpose. It should be called in beforeEach to capture failures during test execution.

🔎 Proposed fix
   beforeEach(() => {
     resetTestTracking();
+    setupTestFailureHandler();
   });

   afterEach(async () => {
-    setupTestFailureHandler();
     await cleanupTrackedResources();
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
afterEach(async () => {
setupTestFailureHandler();
await cleanupTrackedResources();
});
beforeEach(() => {
resetTestTracking();
setupTestFailureHandler();
});
afterEach(async () => {
await cleanupTrackedResources();
});
🤖 Prompt for AI Agents
In @test/e2e/push/publish-e2e.test.ts around lines 73 - 76, The test failure
handler is being registered too late; move the setupTestFailureHandler() call
from the afterEach block into a beforeEach so it registers before each test
runs. Specifically, remove setupTestFailureHandler() from the afterEach that
currently contains setupTestFailureHandler() and await
cleanupTrackedResources(), add a beforeEach(() => setupTestFailureHandler()) (or
await if it returns a promise) and leave await cleanupTrackedResources() in
afterEach to continue cleanup after each test. Ensure you reference
setupTestFailureHandler, beforeEach, and afterEach when applying the change.

Fixes three issues identified in PR review comments:

1. Remove --use-for-sandbox flag from set-apns-p12 command
   (PR #126 comment: flag had no API effect, showed misleading output)
   - Sandbox vs Production is now determined by certificate type
   - Removed flag definition, examples, and conditional output

2. Fix clear-apns to detect token-based auth configuration
   (PR #126 comment: detection used old fields, missed token auth)
   - Now uses apnsAuthType field for detection (consistent with show.ts)
   - Clears both token-based and certificate-based auth fields

3. Add this.exit(1) for JSON mode errors in push commands
   (PR #126 comment: JSON errors exited with code 0)
   - Fixed: devices/get, list, save, remove, remove-where
   - Fixed: channels/list, list-channels, save, remove, remove-where

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add --use-sandbox flag to APNs authentication in `ably push config set-apns`.
Users must specify the environment for both token and certificate auth
(defaults to production).

- Token auth (.p8): apnsUseSandboxEndpoint sent with every request
- Certificate auth (.p12): apnsUseSandboxEndpoint sent with every request

Users are responsible for matching the endpoint with their certificate
type (development cert = sandbox, distribution cert = production).

Also removes deprecated `ably apps set-apns-p12` command - all push
configuration should be under `ably push` namespace.

Ref: PDR-088, WEB-3698
- Skip confirmation prompts in clear-apns and clear-fcm when --json flag
  is used, preventing automation hangs
- Add null check in batch-publish validateBatchItems() before accessing
  item.recipient to handle malformed input arrays
this.error(`Certificate file not found: ${certificatePath}`);
}

this.log(`Uploading APNs P12 certificate for app ${appId}...`);
Copy link

Choose a reason for hiding this comment

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

JSON output corrupted by unconditional logging in set-apns

Medium Severity

The progress messages at lines 111 and 161 are logged unconditionally with this.log() before JSON output is produced. When users run the command with --json flag, these text messages will be prepended to the JSON output, breaking machine-readable parsing. Compare with set-fcm.ts (lines 78-82) which correctly wraps the progress message in if (!this.shouldOutputJson(flags)).

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a known CLI issue that we're going to address

…flicts

- clear-fcm: Output proper JSON when FCM is not configured (was outputting
  colored text which corrupted JSON output)
- clear-apns: Suppress progress message in JSON mode to avoid corrupting
  JSON output
- publish: Validate that notification flags (--sound, --icon, --badge,
  --data, --collapse-key, --ttl, --apns, --fcm, --web) are not used with
  --payload, preventing silent flag ignorance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… APNs detection

Issue 1: Flag naming conflict
- Renamed --client-id to --recipient-client-id in all push commands to avoid
  collision with the global --client-id authentication flag
- Affected commands: publish, channels/save, channels/remove, channels/remove-where,
  channels/list, devices/save, devices/list, devices/remove-where
- Updated all corresponding unit tests

Issue 2: APNs detection ignores legacy configuration
- Expanded APNs configuration detection to check both new (apnsAuthType) and
  legacy (apnsUsesSandboxCert) fields
- Apps configured before Control API update may have apnsUsesSandboxCert set
  without apnsAuthType
- Affected commands: clear-apns, show
Copy link
Contributor

@AndyTWF AndyTWF left a comment

Choose a reason for hiding this comment

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

Few bits - mostly using utility functions etc

const errorCode = (error as { code?: number }).code;

if (this.shouldOutputJson(flags)) {
this.log(
Copy link
Contributor

Choose a reason for hiding this comment

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

We have this.jsonError which handles the formatting and throwing of errors in a way oclif likes


static override examples = [
// List all subscriptions for a channel
"$ ably push channels list --channel alerts",
Copy link
Contributor

Choose a reason for hiding this comment

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

I would have expected to see ably push channels list return a list of channels with push subscriptions, and then another one like ably push channels get that returns the details for one channel

// Confirm deletion unless --force is used
if (!flags.force && !this.shouldOutputJson(flags)) {
const { default: inquirer } = await import("inquirer");
const { confirmed } = await inquirer.prompt([
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a standardised confirmation util now in prompt-confirmation.ts so would be good to use/upgrade that

// Confirm deletion unless --force is used
if (!flags.force && !this.shouldOutputJson(flags)) {
const { default: inquirer } = await import("inquirer");
const { confirmed } = await inquirer.prompt([
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto re confirmation utils (I wont comment on future examples)


// Confirm unless --force is used or in JSON mode (JSON mode should not prompt)
if (!flags.force && !this.shouldOutputJson(flags)) {
const confirmed = await this.interactiveHelper.confirm(
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like we have 3 different ways of confirming (2 in this PR):

  • The existing prompt-confirmation helper in utils
  • Some commands in this PR import inquirer
  • This command uses interactiveHelper

Can we pick one?

// Require at least one filter criterion
if (!flags["recipient-client-id"] && !flags["device-id"]) {
if (this.shouldOutputJson(flags)) {
this.log(
Copy link
Contributor

Choose a reason for hiding this comment

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

Using this.jsonError here will solve the issue that cursor found

const response = await rest.request(
"POST",
"/push/batch/publish",
2, // API version
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be 4 now

});

describe("push channels list-channels", () => {
it.skipIf(SHOULD_SKIP_E2E)(
Copy link
Contributor

Choose a reason for hiding this comment

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

vitest supports describe.skipIf, which we can do just once on the outermost describe

// Note: Tests that require push-enabled channels are not included here
// because the test environment doesn't have push configured for channels.
// The validation tests above verify the CLI command structure is correct.
// Full integration tests would require a push-enabled namespace configuration.
Copy link
Contributor

Choose a reason for hiding this comment

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

It should be quite trivial to use the control API in a test to create the prerequisite namespace? We should definitely have this tested

describe("push:channels:list-channels command", () => {
beforeEach(() => {
const mock = getMockAblyRest();
mock.push.admin.channelSubscriptions.listChannels.mockReset();
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to do this cleanup on a per-file basis, for unit tests they're cleared up in an afterEach in test/unit/setup.ts

@mattheworiordan
Copy link
Member Author

Thanks for all the great feedback @AndyTWF 👍
will address shortly

Add "Discover Patterns Before Implementing" section to help AI agents
and contributors find and follow existing codebase conventions before
implementing common functionality.

This addresses review feedback where existing utilities (jsonError,
promptForConfirmation) were not used because their existence wasn't
discovered before implementation.
- Use this.jsonError() instead of manual JSON error formatting
- Use promptForConfirmation utility instead of inquirer imports
- Change batch-publish API version from 2 to 4
- Use describe.skipIf in E2E tests instead of it.skipIf on each test
- Remove unnecessary mockReset() from unit test beforeEach hooks
@mattheworiordan
Copy link
Member Author

@AndyTWF I was honestly quite annoyed that there were so many obvious problems with this PR that I created.

I decided to revisit my coding/agent rules and prompts, and then reflect on the rules for this repo too, as I think these mistakes are obvious (thanks for pointing them out, I should have known about the obvious problems 😢) and should be avoid for future contributors too. So I've add c3e5099 to CLAUDE.md, which I hope will help, and then addressed your feedback.

Shout if you think I've missed anything in the "lessons" for CLAUDE or otherwise.

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.

3 participants