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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .claude/skills/ably-new-command/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ If the topic/subtopic doesn't exist yet, you also need an index file at `src/com
**Product API commands** (channels, rooms, spaces, presence, pub/sub):
```typescript
import { Args, Flags } from "@oclif/core";
import * as Ably from "ably";
import type * as Ably from "@ably/pubsub-server";
import chalk from "chalk";

import { AblyBaseCommand } from "../../base-command.js";
Expand Down Expand Up @@ -139,6 +139,8 @@ static flags = {

**When to include `clientIdFlag`:** Add `...clientIdFlag` to commands where client identity affects the operation: subscribe, publish, enter, set, acquire, update, delete, append, annotate. The reason is that users may want to test auth scenarios — e.g., "can client B update client A's message?" — so they need the ability to set their client ID. Do NOT add to read-only queries (get, get-all, history, occupancy get) — Ably capabilities are operation-based, not clientId-based, so client identity is irrelevant for pure reads.

**Target client IDs are not identity:** the base command treats `--client-id` as the client ID the CLI acts as only when the command declares it via the shared `clientIdFlag` object. When a command's `--client-id` names a *target* or *filter* (a push recipient, a token's subject), declare a command-local `Flags.string()` instead — never spread `clientIdFlag` for that — so the value can't become the CLI's own identity. `test/unit/base/client-id-target-guard.test.ts` enforces this for every command.

For history commands, also use `timeRangeFlags`:
```typescript
import { productApiFlags, timeRangeFlags } from "../../flags.js";
Expand Down Expand Up @@ -466,13 +468,13 @@ See the "Keeping Skills Up to Date" section in `CLAUDE.md` for the full list of
- [ ] Non-JSON data output uses multi-line labeled blocks (see `patterns.md` "Human-Readable Output Format"), not tables or custom grids
- [ ] Non-JSON output exposes all available SDK fields (same data as JSON mode, omitting only null/empty values)
- [ ] SDK types imported directly (`import type { CursorUpdate } from "@ably/spaces"`) — no local interface redefinitions of SDK types (display interfaces in `src/utils/` are fine)
- [ ] Field coverage checked against SDK type definitions (`node_modules/ably/ably.d.ts`, `node_modules/@ably/spaces/dist/mjs/types.d.ts`)
- [ ] Field coverage checked against SDK type definitions (`node_modules/@ably/pubsub-core/types.d.ts`, re-exported by `@ably/pubsub-server`; `node_modules/@ably/spaces/dist/mjs/types.d.ts`)
- [ ] Subscribe commands do NOT fetch initial state — they only listen for new events (use `get-all` for current state)
- [ ] Resource names use `formatResource(name)`, never quoted
- [ ] JSON output uses `logJsonResult()` (one-shot) or `logJsonEvent()` (streaming), not direct `formatJsonRecord()`
- [ ] JSON data nested under a domain key (singular for events/single items, plural for collections) — not spread at top level (see `patterns.md` "JSON Data Nesting Convention")
- [ ] `room.attach()` / `space.enter()` only called when the SDK method requires a realtime connection (subscribe, send, presence) — NOT for REST mutations (update, delete, annotate, history)
- [ ] Subscribe/enter commands use `this.waitAndTrackCleanup(flags, component, flags.duration)` (not `waitUntilInterruptedOrTimeout`)
- [ ] Subscribe/enter commands use `this.waitAndTrackCleanup(flags, component, flags.duration)` (not `waitUntilInterruptedOrTimeout`) and call `this.setupConnectionStateLogging(client, flags)` — together they fail the command (non-zero exit) when the connection fails or keeps dropping, instead of exiting 0
- [ ] Error handling uses `this.fail()` exclusively, not `this.error()` or `this.log(chalk.red(...))`
- [ ] Component strings are camelCase: single-word lowercase (`"room"`, `"auth"`), multi-word camelCase (`"channelPublish"`, `"roomPresenceSubscribe"`)
- [ ] At least one `--json` example in `static examples`
Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/ably-new-command/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,18 +615,20 @@ Attributes: {"priority": "high"}

| SDK | Type file | Key types |
|-----|-----------|-----------|
| `ably` | `node_modules/ably/ably.d.ts` | `Message`, `PresenceMessage`, `ChannelStateChange`, `ConnectionStateChange` |
| `@ably/pubsub-server` | `node_modules/@ably/pubsub-core/types.d.ts` (re-exported) | `PubSubRealtimeClient`, `PubSubHttpClient`, `Message`, `PresenceMessage`, `ChannelStateChange`, `ConnectionStateChange` |
| `@ably/spaces` | `node_modules/@ably/spaces/dist/mjs/types.d.ts` | `SpaceMember`, `CursorUpdate`, `CursorPosition`, `CursorData`, `Lock`, `ProfileData` |
| `@ably/chat` | `node_modules/@ably/chat/dist/chat/core/*.d.ts` | `Message` (chat), `PresenceMember`, `OccupancyEvent`, `Reaction` |

**Use SDK source code as the source of truth** for method behavior — whether a method requires prior state (e.g., `space.enter()`), what side effects it has, what it actually returns. When in doubt, read the implementation:

| SDK | Source directory | Key files |
|-----|-----------------|-----------|
| `ably` | `node_modules/ably/` | Realtime, REST, channels, presence |
| `@ably/pubsub-server` | `node_modules/@ably/pubsub-core/build/` (its runtime) | Realtime, HTTP, channels, presence |
| `@ably/spaces` | `node_modules/@ably/spaces/dist/mjs/` | `Space.js`, `Members.js`, `Locations.js`, `Cursors.js`, `Locks.js` |
| `@ably/chat` | `node_modules/@ably/chat/dist/chat/core/` | Rooms, messages, presence, reactions |

**Never construct Pub/Sub clients directly or import `ably`.** CLI code imports Pub/Sub types with `import type * as Ably from "@ably/pubsub-server"` and gets clients from `createAblyRestClient()` / `createAblyRealtimeClient()`, which build them with `@ably/pubsub-server`'s `createHttpClient` / `createRealtimeClient`, so the CLI always connects as a server. `@ably/chat` (`createChatClient`) and `@ably/spaces` (`createSpacesClient`) wrap that same realtime client; `ably` v2 is not installed at all.

**Import SDK types directly** — never redefine SDK interfaces locally. If `@ably/spaces` exports `CursorPosition`, import it:
```typescript
// WRONG — local redefinition duplicates SDK type
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ this.error() ← oclif exit (ONLY inside fail, nowhere else)
- **Never use `this.error()` directly** — it is an internal implementation detail of `this.fail()`.
- **`requireAppId`** returns `Promise<string>` (not nullable) — calls `this.fail()` internally if no app found.
- **`runControlCommand<T>`** returns `Promise<T>` (not nullable) — calls `this.fail()` internally on error.
- **Error hints**: `fail()` appends a CLI-specific hint from `src/utils/errors.ts` if one exists for the Ably error code. Hints must only contain actionable CLI advice (e.g., "run `ably login`"), not restate the upstream error message (which is already shown). When adding new error codes, **fetch** https://ably.com/docs/platform/errors/codes using WebFetch to get the official description — do NOT rely on memory or assumptions about what an error code means.
- **Error hints**: `fail()` appends a CLI-specific hint from `src/utils/errors.ts` if one exists for the Ably error code. A hint may be a function of `HintContext` (token auth or not, whether the command declares `--client-id`) so it never suggests a flag the command lacks. Hints must only contain actionable CLI advice (e.g., "run `ably login`"), not restate the upstream error message (which is already shown). When adding new error codes, **fetch** https://ably.com/docs/platform/errors/codes using WebFetch to get the official description — do NOT rely on memory or assumptions about what an error code means.
- **`extractErrorInfo(error)`** (`src/utils/errors.ts`) — extracts `{ message, code?, statusCode?, href? }` from an unknown error value. Use in commands that report per-item errors inline (batch publish results, connection test summaries) rather than exiting via `this.fail()`.

### Additional output patterns (direct chalk, not helpers)
Expand Down Expand Up @@ -326,9 +326,10 @@ When adding COMMANDS sections in `src/help.ts`, use `chalk.bold()` for headers,
- Spaces: https://ably.com/docs/spaces and API ref at https://sdk.ably.com/builds/ably/spaces/main/typedoc/index.html
- Control API: https://ably.com/docs/account/control-api and ref at https://ably.com/docs/api/control-api
- Platform: https://ably.com/docs/platform
- **Pub/Sub packages**: the CLI never imports `ably`, and depends on `@ably/pubsub-server` only. Types come from it too (`import type * as Ably from "@ably/pubsub-server"`; clients are `Ably.PubSubRealtimeClient` / `Ably.PubSubHttpClient`). Clients are constructed only by `createAblyRestClient()` / `createAblyRealtimeClient()` in the base command, with `createHttpClient` / `createRealtimeClient` from `@ably/pubsub-server`, so the CLI always connects as a server. Chat and Spaces are built on the same core: create them with `createChatClient(realtime, options)` and `createSpacesClient(realtime)`, passing the CLI's own realtime client. Nothing in the dependency tree uses `ably` v2.
- The CLI uses Ably SDKs for all data plane commands. When an API exists in the data plane REST API but has no corresponding SDK method, use the Pub/Sub SDK's request method.
- The Control API has no official SDK, so raw HTTP requests are used.
- **SDK packages (`node_modules/ably/`, `node_modules/@ably/spaces/`, `node_modules/@ably/chat/`) are the local source of truth** for types and method behavior. Type definitions (e.g., `ably.d.ts`, `types.d.ts`) tell you what fields exist; source code (e.g., `Space.js`, `Members.js`) tells you how methods behave (side effects, prerequisites like `space.enter()`). When in doubt, read the implementation — not just the types. See `references/patterns.md` "Field display rules" in the `ably-new-command` skill for the full path table and import conventions.
- **SDK packages (`node_modules/@ably/pubsub-core/` (the core behind `@ably/pubsub-server`), `node_modules/@ably/spaces/`, `node_modules/@ably/chat/`) are the local source of truth** for types and method behavior. Type definitions (e.g., `@ably/pubsub-core/types.d.ts`, `types.d.ts`) tell you what fields exist; source code (e.g., `Space.js`, `Members.js`) tells you how methods behave (side effects, prerequisites like `space.enter()`). When in doubt, read the implementation — not just the types. See `references/patterns.md` "Field display rules" in the `ably-new-command` skill for the full path table and import conventions.

## Development Standards

Expand Down
1 change: 1 addition & 0 deletions docs/Environment-Variables/General-Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ These environment variables are most commonly used during development as well as
| `ABLY_TOKEN` | Authentication | Token/JWT for data plane commands | None |
| `ABLY_ACCESS_TOKEN` | Authentication | Access token for Control API commands | None |
| `ABLY_APP_ID` | App Selection | Default app for `--app` flag | None |
| `ABLY_CLIENT_ID` | Configuration | Client ID the CLI acts as | Generated once per install |
| `ABLY_CLI_CONFIG_DIR` | Configuration | Custom config directory | `~/.ably` |
| `ABLY_HISTORY_FILE` | Configuration | Custom history file location | `~/.ably/history` |
| `ABLY_CLI_DEFAULT_DURATION` | Behavior | Auto-exit long-running commands (seconds) | None (forever) |
Expand Down
4 changes: 3 additions & 1 deletion docs/Project-Structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ This document outlines the directory structure of the Ably CLI project.
│ │ ├── command_not_found/ # Fuzzy-match suggestions for unknown commands
│ │ └── init/ # CLI initialization
│ ├── services/ # Business logic
│ │ ├── client-identity.ts # Resolves the client ID the CLI acts as
│ │ ├── config-manager.ts # CLI configuration (accounts, apps, API keys)
│ │ ├── control-api.ts # Ably Control API HTTP client
│ │ ├── history-manager.ts # Interactive mode command history persistence
Expand All @@ -73,8 +74,9 @@ This document outlines the directory structure of the Ably CLI project.
│ └── utils/
│ ├── channel-rule-display.ts # Channel rule human-readable display
│ ├── chat-constants.ts # Shared Chat SDK constants (REACTION_TYPE_MAP)
│ ├── connection-health.ts # Detects dead connections (failed, reconnect loops)
│ ├── server-url.ts # Local server URL parsing (parseServerUrl, formatServerUrl)
│ ├── errors.ts # Error utilities (errorMessage)
│ ├── errors.ts # Error utilities (errorMessage, errorWithReason, hints)
│ ├── interrupt-feedback.ts # Ctrl+C feedback messages
│ ├── json-formatter.ts # JSON output formatting (formatJson, formatMessageData)
│ ├── key-display.ts # Key capability formatting
Expand Down
4 changes: 2 additions & 2 deletions docs/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ Call generators at describe-block level. Write `"functionality"` and `"error han

| File | Provides |
|------|----------|
| `mock-ably-realtime.ts` | Mock `Ably.Realtime` — channels, presence, connection events |
| `mock-ably-rest.ts` | Mock `Ably.Rest` — REST channel operations, request() |
| `mock-ably-realtime.ts` | Mock `PubSubRealtimeClient` — channels, presence, connection events |
| `mock-ably-rest.ts` | Mock `PubSubHttpClient` — REST channel operations, request() |
| `mock-ably-chat.ts` | Mock Chat SDK — rooms, messages, typing, reactions |
| `mock-ably-spaces.ts` | Mock Spaces SDK — members, cursors, locations, locks |

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,16 @@
}
},
"dependencies": {
"@ably/chat": "^1.3.1",
"@ably/spaces": "^0.5.2",
"@ably/chat": "0.0.0-canary.20261001T1209.783b80e6",
"@ably/pubsub-server": "3.0.0-canary.20261001T0958.2d5f47d6",
"@ably/spaces": "0.0.0-canary.20261001T1221.ca08adce",
"@inquirer/prompts": "^8.4.2",
"@oclif/core": "^4.11.1",
"@oclif/plugin-autocomplete": "^3.2.48",
"@oclif/plugin-warn-if-update-available": "^3.1.63",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"ably": "^2.21.0",
"chalk": "^5.6.2",
"cli-table3": "^0.6.5",
"color-json": "^3.0.5",
Expand Down
Loading
Loading