Skip to content

Commit 3f54bc1

Browse files
committed
Created plan to format message subscribe and history in CLI commands
1 parent 910f9c4 commit 3f54bc1

1 file changed

Lines changed: 347 additions & 0 deletions

File tree

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
# Implementation Plan: Consistent Message Output for channels subscribe & history
2+
3+
## Original Request
4+
5+
Currently when running `bin/run.js channels subscribe test`, the output is:
6+
```
7+
Using: Account=Free account (DIBHRw) • App=Sandbox (jy3uew) • Key=Root (jy3uew.oZJBOA)
8+
9+
Attaching to channel: test...
10+
Successfully attached to channel: test
11+
✓ Subscribed to channel: test.
12+
Listening for messages. Press Ctrl+C to exit.
13+
[2026-03-06T05:13:09.160Z] Channel: test | Event: (none)
14+
Data: hello
15+
```
16+
17+
Fields shown are `Channel`, `Event`, and `Data`.
18+
19+
When running `bin/run.js channels history test`, the output is:
20+
```
21+
[1] 2026-03-06T05:19:36.130Z
22+
Event: (none)
23+
Client ID: ably-cli-d6d6be45
24+
Data:
25+
hello
26+
```
27+
28+
The output is inconsistent between subscribe and history. We want to show exactly the same fields with the same format for both commands. The important fields are: `event`, `channel`, `id`, `clientId`, `data`, `timestamp`, and `serial`. All of these fields should be consistently available for both commands.
29+
30+
## Problem
31+
32+
The `channels subscribe` and `channels history` commands display messages in inconsistent formats:
33+
34+
**Subscribe** (current):
35+
```
36+
[2026-03-06T05:13:09.160Z] Channel: test | Event: (none)
37+
Data: hello
38+
```
39+
- Shows Channel and Event on one line, Data on next line
40+
- Missing fields: `id`, `clientId`, `serial`
41+
42+
**History** (current):
43+
```
44+
[1] 2026-03-06T05:19:36.130Z
45+
Event: (none)
46+
Client ID: ably-cli-d6d6be45
47+
Data:
48+
hello
49+
```
50+
- Starts with arbitrary index `[1]` — not useful, not a message field
51+
- Timestamp on same line as index, not in `[brackets]` like subscribe
52+
- Missing fields: `channel`, `id`, `serial`
53+
- Data value is on a separate line from the `Data:` label (inconsistent with subscribe where simple data is inline)
54+
55+
**Subscribe JSON** (current `--json`):
56+
```json
57+
{
58+
"channel": "test",
59+
"clientId": "...",
60+
"connectionId": "...",
61+
"data": "hello",
62+
"encoding": "...",
63+
"event": "(none)",
64+
"id": "msg-123",
65+
"timestamp": "2026-03-06T05:13:09.160Z"
66+
}
67+
```
68+
- Missing: `serial`
69+
70+
**History JSON** (current `--json`):
71+
```json
72+
{
73+
"messages": [
74+
{
75+
"id": "msg-1",
76+
"name": "test-event",
77+
"data": { "text": "Hello world" },
78+
"timestamp": 1700000000000,
79+
"clientId": "client-1",
80+
"connectionId": "conn-1"
81+
}
82+
]
83+
}
84+
```
85+
- Missing: `channel`, `serial`
86+
- Uses `name` instead of `event`
87+
- Timestamp is raw milliseconds, not ISO string
88+
89+
## Target Format
90+
91+
Both commands must display **exactly the same fields in the same format**. Required fields:
92+
`timestamp`, `channel`, `event`, `id`, `clientId`, `serial`, `data`
93+
94+
### Human-readable output
95+
96+
Each field on its own line, all at the same level. Coloring follows CLAUDE.md conventions:
97+
- **Secondary labels**: `chalk.dim("Label:")` — for all field names
98+
- **Resource names** (channel): `resource(name)` — cyan
99+
- **Event types**: `chalk.yellow(eventType)`
100+
- **Client IDs**: `chalk.blue(clientId)`
101+
- **Data**: For simple values, inline on same line as label. For JSON objects/arrays, label on its own line then formatted JSON below.
102+
103+
Single message:
104+
```
105+
Timestamp: 2026-03-06T05:13:09.160Z
106+
Channel: test
107+
Event: greeting
108+
ID: msg-123
109+
Client ID: publisher-client
110+
Serial: 01826232064561-001@e]GBiqkIkBnR52:001
111+
Data: hello world
112+
```
113+
114+
For JSON data:
115+
```
116+
Timestamp: 2026-03-06T05:13:09.160Z
117+
Channel: test
118+
Event: greeting
119+
ID: msg-123
120+
Client ID: publisher-client
121+
Serial: 01826232064561-001@e]GBiqkIkBnR52:001
122+
Data:
123+
{
124+
"text": "hello world"
125+
}
126+
```
127+
128+
Multiple messages (history or subscribe stream), separated by blank lines:
129+
```
130+
Timestamp: 2026-03-06T05:13:09.160Z
131+
Channel: test
132+
Event: greeting
133+
ID: msg-123
134+
Client ID: publisher-client
135+
Serial: 01826232064561-001@e]GBiqkIkBnR52:001
136+
Data: hello world
137+
138+
Timestamp: 2026-03-06T05:13:10.200Z
139+
Channel: test
140+
Event: update
141+
ID: msg-124
142+
Client ID: another-client
143+
Serial: 01826232064562-001@e]GBiqkIkBnR52:002
144+
Data:
145+
{
146+
"status": "active"
147+
}
148+
```
149+
150+
### JSON output (--json flag)
151+
152+
Both commands must include all required fields with consistent naming and formatting.
153+
154+
**Subscribe JSON** — emits one JSON object per message (streaming, one at a time):
155+
```json
156+
{
157+
"timestamp": "2026-03-06T05:13:09.160Z",
158+
"channel": "test",
159+
"event": "greeting",
160+
"id": "msg-123",
161+
"clientId": "publisher-client",
162+
"serial": "01826232064561-001@e]GBiqkIkBnR52:001",
163+
"data": "hello world"
164+
}
165+
```
166+
167+
Changes from current:
168+
- Add `serial` field (from `message.serial`)
169+
- Remove `connectionId` and `encoding` (not in required fields)
170+
171+
**History JSON** — emits an array of message objects:
172+
```json
173+
[
174+
{
175+
"timestamp": "2023-11-14T22:13:20.000Z",
176+
"channel": "test",
177+
"event": "test-event",
178+
"id": "msg-1",
179+
"clientId": "client-1",
180+
"serial": "01826232064561-001@e]GBiqkIkBnR52:001",
181+
"data": { "text": "Hello world" }
182+
},
183+
{
184+
"timestamp": "2023-11-14T22:13:21.000Z",
185+
"channel": "test",
186+
"event": "another-event",
187+
"id": "msg-2",
188+
"clientId": "client-2",
189+
"serial": "01826232064562-001@e]GBiqkIkBnR52:002",
190+
"data": "Plain text message"
191+
}
192+
]
193+
```
194+
195+
Changes from current:
196+
- Output a plain JSON array instead of `{ messages: [...] }` wrapper
197+
- Add `channel` field (from args)
198+
- Add `serial` field (from `message.serial`)
199+
- Rename `name``event` for consistency
200+
- Convert `timestamp` from raw milliseconds to ISO 8601 string
201+
- Remove `connectionId` (not in required fields)
202+
203+
### Design decisions
204+
205+
- **Each field on its own line** — consistent, scannable, easy to grep
206+
- **Timestamp is a regular field**`Timestamp:` label like all others, not a special `[brackets]` header
207+
- **No indentation** — all fields at the same level, no nesting
208+
- **Field order**: Timestamp → Channel → Event → ID → Client ID → Serial → Data
209+
- **Missing values**: `(none)` for missing event name. Omit fields entirely if not available (e.g. if `clientId` is undefined, don't show the Client ID line). Exception: Event always shown, using `(none)` as fallback.
210+
- **No index numbers**: History currently shows `[1]`, `[2]` — remove. Timestamp + serial provide ordering.
211+
- **Coloring**: Applied per CLAUDE.md conventions listed above. No unnecessary coloring on plain values (id, serial, data strings).
212+
- **Blank line separator**: Between messages in multi-message output.
213+
- **JSON consistency**: Both commands use the same field names (`event` not `name`), same timestamp format (ISO 8601), same field set.
214+
- **History JSON is a plain array**: No `{ messages: [...] }` wrapper — just the array directly, which is simpler and more consistent with subscribe's per-message objects.
215+
216+
## Implementation Steps
217+
218+
### 1. Add `formatMessagesOutput` helper to `src/utils/output.ts`
219+
220+
Create a single shared function that accepts an array of messages:
221+
222+
```typescript
223+
export interface MessageDisplayFields {
224+
channel: string;
225+
clientId?: string;
226+
data: unknown;
227+
event: string;
228+
id?: string;
229+
serial?: string;
230+
sequencePrefix?: string;
231+
timestamp: string;
232+
}
233+
234+
/**
235+
* Format an array of messages for human-readable console output.
236+
* Each message shows all fields on separate lines, messages separated by blank lines.
237+
* Returns "No messages found." for empty arrays.
238+
*/
239+
export function formatMessagesOutput(messages: MessageDisplayFields[]): string
240+
```
241+
242+
The function:
243+
- Returns `"No messages found."` for empty arrays
244+
- For each message, builds lines:
245+
- `${chalk.dim("Timestamp:")} ${timestamp}` + optional sequencePrefix
246+
- `${chalk.dim("Channel:")} ${resource(channel)}`
247+
- `${chalk.dim("Event:")} ${chalk.yellow(event)}`
248+
- (if id) `${chalk.dim("ID:")} ${id}`
249+
- (if clientId) `${chalk.dim("Client ID:")} ${chalk.blue(clientId)}`
250+
- (if serial) `${chalk.dim("Serial:")} ${serial}`
251+
- Data: `${chalk.dim("Data:")} ${value}` for simple, or `${chalk.dim("Data:")}` + formatted JSON block on next lines
252+
- Joins messages with `\n\n` (blank line separator)
253+
254+
### 2. Add `toMessageJson` helper to `src/utils/output.ts`
255+
256+
Create a single helper that normalizes one message into the consistent JSON shape. For arrays, callers simply use `.map(toMessageJson)`:
257+
258+
```typescript
259+
/**
260+
* Convert a single MessageDisplayFields to a plain object for JSON output.
261+
* Includes all required fields, omits undefined optional fields.
262+
*
263+
* Usage:
264+
* Single message (subscribe): toMessageJson(msg)
265+
* Array of messages (history): messages.map(toMessageJson)
266+
*/
267+
export function toMessageJson(msg: MessageDisplayFields): Record<string, unknown>
268+
```
269+
270+
Returns:
271+
```typescript
272+
{
273+
timestamp: msg.timestamp,
274+
channel: msg.channel,
275+
event: msg.event,
276+
...(msg.id ? { id: msg.id } : {}),
277+
...(msg.clientId ? { clientId: msg.clientId } : {}),
278+
...(msg.serial ? { serial: msg.serial } : {}),
279+
data: msg.data,
280+
}
281+
```
282+
283+
### 3. Update `src/commands/channels/subscribe.ts`
284+
285+
- Import `formatMessagesOutput`, `toMessageJson`
286+
- Remove imports of `formatJson`, `isJsonData`, `formatTimestamp`
287+
- Add `serial` to the message fields (from `message.serial`)
288+
- Build a `MessageDisplayFields` object from the Ably message
289+
- Human output: `this.log(formatMessagesOutput([msgFields]))`
290+
- JSON output: `this.log(this.formatJsonOutput(toMessageJson(msgFields), flags))`
291+
- Remove `connectionId` and `encoding` from JSON output
292+
293+
### 4. Update `src/commands/channels/history.ts`
294+
295+
- Import `formatMessagesOutput`, `toMessageJson` from output utils
296+
- Build a `MessageDisplayFields[]` array from history results:
297+
- `channel` from `args.channel`
298+
- `event` from `message.name || "(none)"`
299+
- `serial` from `message.serial`
300+
- `timestamp` as ISO string (convert from milliseconds)
301+
- Human output: `this.log(formatMessagesOutput(displayMessages))`
302+
- The "No messages found" case is handled by `formatMessagesOutput` returning the appropriate string
303+
- Remove the `[index]` prefix, the for-loop, and the "Found N messages" header
304+
- JSON output: `this.log(this.formatJsonOutput(displayMessages.map(toMessageJson), flags))`
305+
- Output a plain array instead of `{ messages: [...] }` wrapper
306+
307+
### 5. Update `.claude/CLAUDE.md`
308+
309+
Add a "Message display" subsection under "CLI Output & Flag Conventions":
310+
311+
```markdown
312+
### Message display (channels subscribe, channels history, etc.)
313+
- Use `formatMessagesOutput()` from `src/utils/output.ts` for all message rendering
314+
- Use `toMessageJson()` for consistent JSON output shape; for arrays use `.map(toMessageJson)`
315+
- Each field on its own line, no indentation — all fields at the same level
316+
- Field order: Timestamp → Channel → Event → ID → Client ID → Serial → Data
317+
- Omit optional fields (ID, Client ID, Serial) if the value is undefined/null
318+
- Event always shown; use `(none)` when message has no event name
319+
- Data: inline for simple values, block for JSON objects/arrays
320+
- Multiple messages separated by blank lines
321+
- JSON output uses consistent field names (`event` not `name`), ISO 8601 timestamps
322+
```
323+
324+
### 6. Update tests
325+
326+
- `test/unit/commands/channels/subscribe.test.ts`:
327+
- Update assertion for "Event: test-event" (still present)
328+
- Add checks for new fields: "Timestamp:", "Channel:", "ID:", "Client ID:", "Serial:"
329+
- Update JSON test if needed (serial field added, connectionId/encoding removed)
330+
331+
- `test/unit/commands/channels/history.test.ts`:
332+
- Remove assertion for `[1]` index format
333+
- Update to match new field layout: "Timestamp:", "Channel:", "Event:", "ID:", "Serial:"
334+
- Update JSON test: now expects a plain array instead of `{ messages: [...] }`, with `event` instead of `name`, ISO timestamp instead of milliseconds
335+
336+
### 7. Verify E2E compatibility
337+
338+
The E2E test (`channel-subscribe-e2e.test.ts`) only checks `output.includes("Subscribe E2E Test")`unaffected by format changes.
339+
340+
## Files Changed
341+
342+
1. `src/utils/output.ts`Add `formatMessagesOutput`, `toMessageJson`, and `MessageDisplayFields`
343+
2. `src/commands/channels/subscribe.ts`Use `formatMessagesOutput([msg])` + `toMessageJson(msg)`, add serial
344+
3. `src/commands/channels/history.ts`Use `formatMessagesOutput(messages)` + `messages.map(toMessageJson)`, add channel/serial, remove index, plain array JSON
345+
4. `.claude/CLAUDE.md`Document message display conventions
346+
5. `test/unit/commands/channels/subscribe.test.ts`Update assertions for new format
347+
6. `test/unit/commands/channels/history.test.ts`Update assertions for new format + JSON structure

0 commit comments

Comments
 (0)