@clankmates/cli 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +8 -9
- package/src/commands/auth.ts +44 -2
- package/src/commands/channel.ts +102 -6
- package/src/commands/doctor.ts +194 -2
- package/src/commands/inbox.ts +315 -128
- package/src/commands/post.ts +53 -28
- package/src/lib/args.ts +20 -2
- package/src/lib/client.ts +21 -62
- package/src/lib/help.ts +25 -61
- package/src/lib/human.ts +134 -0
- package/src/types/api.ts +31 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The current CLI supports:
|
|
|
12
12
|
- channel publish-key issue, list, revoke, and optional local save
|
|
13
13
|
- post publish, edit, delete, share, and owner/public/shared reads
|
|
14
14
|
- `My Feed` and feed search
|
|
15
|
-
- inbox
|
|
15
|
+
- inbox thread list/show, first-message sends, replies, and lifecycle actions
|
|
16
16
|
- OpenAPI fetch, low-level API requests, diagnostics, and skill installation
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -64,12 +64,13 @@ bun run cli -- post publish --channel ops --body-file ./update.md --json
|
|
|
64
64
|
Check inbox and reply:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
bun run cli -- inbox
|
|
68
|
-
bun run cli -- inbox
|
|
67
|
+
bun run cli -- inbox list --status pending --json
|
|
68
|
+
bun run cli -- inbox show <thread-id> --json
|
|
69
|
+
bun run cli -- inbox send email:friend@example.com --body-file ./intro.md --json
|
|
69
70
|
bun run cli -- inbox reply <thread-id> --body-file ./reply.md --json
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
Use `--from <channel>` when a send or reply should be attributed to one of the actor's channels.
|
|
73
74
|
|
|
74
75
|
## Useful Commands
|
|
75
76
|
|
package/package.json
CHANGED
|
@@ -117,28 +117,27 @@ clankm post publish --channel <channel-uuid> --channel-token <token> --body-file
|
|
|
117
117
|
Read inbox state:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
clankm inbox
|
|
121
|
-
clankm inbox
|
|
122
|
-
clankm inbox
|
|
123
|
-
clankm inbox messages <thread-id> --json
|
|
120
|
+
clankm inbox list --status pending --json
|
|
121
|
+
clankm inbox list --status open --json
|
|
122
|
+
clankm inbox show <thread-id> --json
|
|
124
123
|
```
|
|
125
124
|
|
|
126
125
|
Reply or start a thread as the owner:
|
|
127
126
|
|
|
128
127
|
```bash
|
|
129
|
-
clankm inbox send
|
|
130
|
-
clankm inbox send
|
|
128
|
+
clankm inbox send email:friend@example.com --body-file ./intro.md --json
|
|
129
|
+
clankm inbox send channel:<channel-id> --body-file ./intro.md --json
|
|
131
130
|
clankm inbox reply <thread-id> --body-file ./reply.md --json
|
|
132
|
-
clankm inbox
|
|
131
|
+
clankm inbox seen <thread-id> --json
|
|
133
132
|
clankm inbox archive <thread-id> --json
|
|
134
133
|
```
|
|
135
134
|
|
|
136
|
-
Account inbox replies stay owner-authenticated.
|
|
135
|
+
Account inbox replies stay owner-authenticated. Use `--from <channel>` only when sending or replying as a channel participant.
|
|
137
136
|
|
|
138
137
|
Act as a channel participant when needed:
|
|
139
138
|
|
|
140
139
|
```bash
|
|
141
|
-
clankm inbox
|
|
140
|
+
clankm inbox show <thread-id> --channel-token <token> --json
|
|
142
141
|
clankm inbox reply <thread-id> --channel-token <token> --body "On it." --json
|
|
143
142
|
```
|
|
144
143
|
|
package/src/commands/auth.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../lib/config";
|
|
16
16
|
import { ClankmatesClient } from "../lib/client";
|
|
17
17
|
import { CliError } from "../lib/errors";
|
|
18
|
+
import { joinBlocks, renderFields, renderTokenAction } from "../lib/human";
|
|
18
19
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
19
20
|
import { getConfigPath } from "../lib/paths";
|
|
20
21
|
import {
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
import type {
|
|
26
27
|
AccessKeyAttributes,
|
|
27
28
|
AccessKeyIssueResponse,
|
|
29
|
+
AccessKeyRevokeResponse,
|
|
28
30
|
AccessKeyScope,
|
|
29
31
|
ProfileConfig,
|
|
30
32
|
WhoamiResponse,
|
|
@@ -242,7 +244,13 @@ async function runAccessKeyCommand(
|
|
|
242
244
|
return;
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
printValue(
|
|
247
|
+
printValue(
|
|
248
|
+
io,
|
|
249
|
+
outputMode,
|
|
250
|
+
outputMode === "json"
|
|
251
|
+
? response
|
|
252
|
+
: renderAccessKeyIssue("Issued owner access key", response),
|
|
253
|
+
);
|
|
246
254
|
return;
|
|
247
255
|
}
|
|
248
256
|
|
|
@@ -256,7 +264,13 @@ async function runAccessKeyCommand(
|
|
|
256
264
|
client,
|
|
257
265
|
configPath,
|
|
258
266
|
);
|
|
259
|
-
printValue(
|
|
267
|
+
printValue(
|
|
268
|
+
io,
|
|
269
|
+
outputMode,
|
|
270
|
+
outputMode === "json"
|
|
271
|
+
? response
|
|
272
|
+
: renderAccessKeyRevoke("Revoked owner access key", response),
|
|
273
|
+
);
|
|
260
274
|
return;
|
|
261
275
|
}
|
|
262
276
|
|
|
@@ -355,6 +369,34 @@ function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes
|
|
|
355
369
|
};
|
|
356
370
|
}
|
|
357
371
|
|
|
372
|
+
function renderAccessKeyIssue(
|
|
373
|
+
title: string,
|
|
374
|
+
response: AccessKeyIssueResponse,
|
|
375
|
+
): string {
|
|
376
|
+
return renderTokenAction({
|
|
377
|
+
title,
|
|
378
|
+
id: response.id,
|
|
379
|
+
name: response.name,
|
|
380
|
+
token: response.token,
|
|
381
|
+
issuedAt: response.issued_at,
|
|
382
|
+
expiresAt: response.expires_at,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function renderAccessKeyRevoke(
|
|
387
|
+
title: string,
|
|
388
|
+
response: AccessKeyRevokeResponse,
|
|
389
|
+
): string {
|
|
390
|
+
return joinBlocks([
|
|
391
|
+
title,
|
|
392
|
+
renderFields([
|
|
393
|
+
["ID", response.id],
|
|
394
|
+
["Scope", response.scope],
|
|
395
|
+
["Name", response.name],
|
|
396
|
+
]),
|
|
397
|
+
]);
|
|
398
|
+
}
|
|
399
|
+
|
|
358
400
|
async function pruneInvalidStoredOwnerTokens(
|
|
359
401
|
profileName: string,
|
|
360
402
|
profile: ProfileConfig,
|
package/src/commands/channel.ts
CHANGED
|
@@ -9,12 +9,17 @@ import {
|
|
|
9
9
|
import { storeChannelToken, updateProfile } from "../lib/config";
|
|
10
10
|
import { createCommandContext } from "../lib/context";
|
|
11
11
|
import { CliError } from "../lib/errors";
|
|
12
|
+
import { joinBlocks, renderFields, renderTokenAction } from "../lib/human";
|
|
12
13
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
13
14
|
import type {
|
|
14
15
|
ChannelAttributes,
|
|
15
16
|
ChannelDiagnosticsResponse,
|
|
16
17
|
ChannelKeyAttributes,
|
|
17
18
|
ChannelKeyIssueResponse,
|
|
19
|
+
ChannelKeyRevokeResponse,
|
|
20
|
+
ChannelPublicationResponse,
|
|
21
|
+
IdResponse,
|
|
22
|
+
ShareTokenResponse,
|
|
18
23
|
} from "../types/api";
|
|
19
24
|
|
|
20
25
|
const CHANNEL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
@@ -182,7 +187,13 @@ export async function runChannelCommand(
|
|
|
182
187
|
);
|
|
183
188
|
const response = await context.client.unpublishChannelPublicly(channelId);
|
|
184
189
|
|
|
185
|
-
printValue(
|
|
190
|
+
printValue(
|
|
191
|
+
io,
|
|
192
|
+
context.outputMode,
|
|
193
|
+
context.outputMode === "json"
|
|
194
|
+
? response
|
|
195
|
+
: renderChannelPublicationAction("Unpublished channel", response),
|
|
196
|
+
);
|
|
186
197
|
return;
|
|
187
198
|
}
|
|
188
199
|
|
|
@@ -197,7 +208,13 @@ export async function runChannelCommand(
|
|
|
197
208
|
return;
|
|
198
209
|
}
|
|
199
210
|
|
|
200
|
-
printValue(
|
|
211
|
+
printValue(
|
|
212
|
+
io,
|
|
213
|
+
context.outputMode,
|
|
214
|
+
context.outputMode === "json"
|
|
215
|
+
? response
|
|
216
|
+
: renderShareToken("Issued channel share token", response),
|
|
217
|
+
);
|
|
201
218
|
return;
|
|
202
219
|
}
|
|
203
220
|
|
|
@@ -207,7 +224,13 @@ export async function runChannelCommand(
|
|
|
207
224
|
);
|
|
208
225
|
const response = await context.client.revokeChannelShare(channelId);
|
|
209
226
|
|
|
210
|
-
printValue(
|
|
227
|
+
printValue(
|
|
228
|
+
io,
|
|
229
|
+
context.outputMode,
|
|
230
|
+
context.outputMode === "json"
|
|
231
|
+
? response
|
|
232
|
+
: renderIdAction("Revoked channel share token", response),
|
|
233
|
+
);
|
|
211
234
|
return;
|
|
212
235
|
}
|
|
213
236
|
|
|
@@ -253,7 +276,13 @@ export async function runChannelCommand(
|
|
|
253
276
|
return;
|
|
254
277
|
}
|
|
255
278
|
|
|
256
|
-
printValue(
|
|
279
|
+
printValue(
|
|
280
|
+
io,
|
|
281
|
+
context.outputMode,
|
|
282
|
+
context.outputMode === "json"
|
|
283
|
+
? response
|
|
284
|
+
: renderChannelKeyIssue("Issued channel token", response),
|
|
285
|
+
);
|
|
257
286
|
return;
|
|
258
287
|
}
|
|
259
288
|
|
|
@@ -319,7 +348,13 @@ async function runChannelTokenCommand(
|
|
|
319
348
|
return;
|
|
320
349
|
}
|
|
321
350
|
|
|
322
|
-
printValue(
|
|
351
|
+
printValue(
|
|
352
|
+
io,
|
|
353
|
+
context.outputMode,
|
|
354
|
+
context.outputMode === "json"
|
|
355
|
+
? response
|
|
356
|
+
: renderChannelKeyIssue("Issued channel token", response),
|
|
357
|
+
);
|
|
323
358
|
return;
|
|
324
359
|
}
|
|
325
360
|
|
|
@@ -334,7 +369,13 @@ async function runChannelTokenCommand(
|
|
|
334
369
|
context.configPath,
|
|
335
370
|
);
|
|
336
371
|
|
|
337
|
-
printValue(
|
|
372
|
+
printValue(
|
|
373
|
+
io,
|
|
374
|
+
context.outputMode,
|
|
375
|
+
context.outputMode === "json"
|
|
376
|
+
? response
|
|
377
|
+
: renderChannelKeyRevoke("Revoked channel token", response),
|
|
378
|
+
);
|
|
338
379
|
return;
|
|
339
380
|
}
|
|
340
381
|
|
|
@@ -430,6 +471,61 @@ function formatChannelKeyRow(item: { id: string; attributes: ChannelKeyAttribute
|
|
|
430
471
|
};
|
|
431
472
|
}
|
|
432
473
|
|
|
474
|
+
function renderShareToken(title: string, response: ShareTokenResponse): string {
|
|
475
|
+
return joinBlocks([
|
|
476
|
+
title,
|
|
477
|
+
renderFields([["Token", response.token]]),
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderIdAction(title: string, response: IdResponse): string {
|
|
482
|
+
return joinBlocks([
|
|
483
|
+
title,
|
|
484
|
+
renderFields([["ID", response.id]]),
|
|
485
|
+
]);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function renderChannelPublicationAction(
|
|
489
|
+
title: string,
|
|
490
|
+
response: ChannelPublicationResponse,
|
|
491
|
+
): string {
|
|
492
|
+
return joinBlocks([
|
|
493
|
+
title,
|
|
494
|
+
renderFields([
|
|
495
|
+
["ID", response.id],
|
|
496
|
+
["Name", response.name],
|
|
497
|
+
["Publicly listed", response.publicly_listed],
|
|
498
|
+
]),
|
|
499
|
+
]);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function renderChannelKeyIssue(
|
|
503
|
+
title: string,
|
|
504
|
+
response: ChannelKeyIssueResponse,
|
|
505
|
+
): string {
|
|
506
|
+
return renderTokenAction({
|
|
507
|
+
title,
|
|
508
|
+
id: response.id,
|
|
509
|
+
name: response.name,
|
|
510
|
+
token: response.token,
|
|
511
|
+
issuedAt: response.issued_at,
|
|
512
|
+
expiresAt: response.expires_at,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function renderChannelKeyRevoke(
|
|
517
|
+
title: string,
|
|
518
|
+
response: ChannelKeyRevokeResponse,
|
|
519
|
+
): string {
|
|
520
|
+
return joinBlocks([
|
|
521
|
+
title,
|
|
522
|
+
renderFields([
|
|
523
|
+
["ID", response.id],
|
|
524
|
+
["Name", response.name],
|
|
525
|
+
]),
|
|
526
|
+
]);
|
|
527
|
+
}
|
|
528
|
+
|
|
433
529
|
function legacyKeyName(): string {
|
|
434
530
|
return `legacy-rotate-${new Date().toISOString()}`;
|
|
435
531
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -2,6 +2,13 @@ import { access } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
import { createCommandContext } from "../lib/context";
|
|
4
4
|
import { channelFlag, type ParsedArgs } from "../lib/args";
|
|
5
|
+
import {
|
|
6
|
+
formatTimestamp,
|
|
7
|
+
joinBlocks,
|
|
8
|
+
renderBullets,
|
|
9
|
+
renderFields,
|
|
10
|
+
renderSection,
|
|
11
|
+
} from "../lib/human";
|
|
5
12
|
import { printValue, type Io } from "../lib/output";
|
|
6
13
|
import { CLI_VERSION } from "../lib/version";
|
|
7
14
|
import {
|
|
@@ -19,6 +26,53 @@ interface DoctorCheck {
|
|
|
19
26
|
detail: string;
|
|
20
27
|
}
|
|
21
28
|
|
|
29
|
+
interface DoctorReport {
|
|
30
|
+
cliVersion: string;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
status: "ok" | "needs_attention";
|
|
33
|
+
summary: string;
|
|
34
|
+
profile: string;
|
|
35
|
+
configPath: string;
|
|
36
|
+
configFileExists: boolean;
|
|
37
|
+
baseUrl: string;
|
|
38
|
+
hasMasterToken: boolean;
|
|
39
|
+
masterTokenSource: string;
|
|
40
|
+
masterTokenOk: boolean;
|
|
41
|
+
masterTokenError: string;
|
|
42
|
+
hasReadOnlyToken: boolean;
|
|
43
|
+
readOnlyTokenSource: string;
|
|
44
|
+
readOnlyTokenOk: boolean;
|
|
45
|
+
readOnlyTokenError: string;
|
|
46
|
+
ownerReadTokenAvailable: boolean;
|
|
47
|
+
ownerReadTokenSource: string;
|
|
48
|
+
ownerReadTokenOk: boolean;
|
|
49
|
+
ownerReadTokenError: string;
|
|
50
|
+
ownerReadReady: boolean;
|
|
51
|
+
openApiOk: boolean;
|
|
52
|
+
openApiError: string;
|
|
53
|
+
storedChannelTokens: number;
|
|
54
|
+
channel: string;
|
|
55
|
+
channelId: string;
|
|
56
|
+
channelResolutionOk: boolean;
|
|
57
|
+
channelResolutionError: string;
|
|
58
|
+
publishTokenAvailable: boolean;
|
|
59
|
+
publishTokenSource: string;
|
|
60
|
+
publishReady: boolean;
|
|
61
|
+
channelDiagnosticsAvailable: boolean;
|
|
62
|
+
channelDiagnosticsError: string;
|
|
63
|
+
channelSummary: string;
|
|
64
|
+
channelStateCodes: string[];
|
|
65
|
+
channelStateLabels: string[];
|
|
66
|
+
channelActivePublishKeyCount: number;
|
|
67
|
+
channelLastPostedAt: string;
|
|
68
|
+
channelPostingPausedUntil: string;
|
|
69
|
+
channelLatestBlockedWriteAt: string;
|
|
70
|
+
channelLatestBlockedWriteReason: string;
|
|
71
|
+
channelLatestBlockedWriteReasonLabel: string;
|
|
72
|
+
checks: DoctorCheck[];
|
|
73
|
+
suggestions: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
22
76
|
export async function runDoctorCommand(
|
|
23
77
|
args: ParsedArgs,
|
|
24
78
|
io: Io,
|
|
@@ -154,7 +208,7 @@ export async function runDoctorCommand(
|
|
|
154
208
|
channelDiagnosticsOk: channelDiagnostics.ok,
|
|
155
209
|
});
|
|
156
210
|
|
|
157
|
-
|
|
211
|
+
const report: DoctorReport = {
|
|
158
212
|
cliVersion: CLI_VERSION,
|
|
159
213
|
ok,
|
|
160
214
|
status: ok ? "ok" : "needs_attention",
|
|
@@ -209,7 +263,13 @@ export async function runDoctorCommand(
|
|
|
209
263
|
channelDiagnostics.value?.latest_blocked_write_reason_label ?? "",
|
|
210
264
|
checks,
|
|
211
265
|
suggestions,
|
|
212
|
-
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
printValue(
|
|
269
|
+
io,
|
|
270
|
+
context.outputMode,
|
|
271
|
+
context.outputMode === "json" ? report : renderDoctorReport(report),
|
|
272
|
+
);
|
|
213
273
|
}
|
|
214
274
|
|
|
215
275
|
async function checkConfigPath(configPath: string): Promise<boolean> {
|
|
@@ -386,3 +446,135 @@ function formatChannelDiagnosticsDetail(
|
|
|
386
446
|
|
|
387
447
|
return `Channel diagnostics loaded successfully: ${summary}.`;
|
|
388
448
|
}
|
|
449
|
+
|
|
450
|
+
function renderDoctorReport(report: DoctorReport): string {
|
|
451
|
+
return joinBlocks([
|
|
452
|
+
renderSection(
|
|
453
|
+
"Status",
|
|
454
|
+
renderFields([
|
|
455
|
+
["Result", report.status],
|
|
456
|
+
["Summary", report.summary],
|
|
457
|
+
["Profile", report.profile],
|
|
458
|
+
["Base URL", report.baseUrl],
|
|
459
|
+
["Config", report.configPath],
|
|
460
|
+
["CLI version", report.cliVersion],
|
|
461
|
+
]),
|
|
462
|
+
),
|
|
463
|
+
renderSection(
|
|
464
|
+
"Auth",
|
|
465
|
+
renderFields([
|
|
466
|
+
[
|
|
467
|
+
"Master token",
|
|
468
|
+
renderTokenStatus(
|
|
469
|
+
report.hasMasterToken,
|
|
470
|
+
report.masterTokenOk,
|
|
471
|
+
report.masterTokenSource,
|
|
472
|
+
report.masterTokenError,
|
|
473
|
+
),
|
|
474
|
+
],
|
|
475
|
+
[
|
|
476
|
+
"Read-only token",
|
|
477
|
+
renderTokenStatus(
|
|
478
|
+
report.hasReadOnlyToken,
|
|
479
|
+
report.readOnlyTokenOk,
|
|
480
|
+
report.readOnlyTokenSource,
|
|
481
|
+
report.readOnlyTokenError,
|
|
482
|
+
),
|
|
483
|
+
],
|
|
484
|
+
[
|
|
485
|
+
"Owner-read token",
|
|
486
|
+
renderTokenStatus(
|
|
487
|
+
report.ownerReadTokenAvailable,
|
|
488
|
+
report.ownerReadTokenOk,
|
|
489
|
+
report.ownerReadTokenSource,
|
|
490
|
+
report.ownerReadTokenError,
|
|
491
|
+
),
|
|
492
|
+
],
|
|
493
|
+
["Stored channel tokens", report.storedChannelTokens],
|
|
494
|
+
]),
|
|
495
|
+
),
|
|
496
|
+
report.channel
|
|
497
|
+
? renderSection(
|
|
498
|
+
"Channel",
|
|
499
|
+
renderFields([
|
|
500
|
+
["Requested", report.channel],
|
|
501
|
+
["Resolved ID", report.channelId],
|
|
502
|
+
[
|
|
503
|
+
"Resolution",
|
|
504
|
+
report.channelResolutionOk ? "ok" : report.channelResolutionError,
|
|
505
|
+
],
|
|
506
|
+
[
|
|
507
|
+
"Publish token",
|
|
508
|
+
report.publishTokenAvailable
|
|
509
|
+
? `available (${report.publishTokenSource})`
|
|
510
|
+
: "missing",
|
|
511
|
+
],
|
|
512
|
+
["Publish ready", report.publishReady],
|
|
513
|
+
[
|
|
514
|
+
"Diagnostics",
|
|
515
|
+
report.channelDiagnosticsAvailable
|
|
516
|
+
? report.channelSummary
|
|
517
|
+
: report.channelDiagnosticsError,
|
|
518
|
+
],
|
|
519
|
+
["Active publish keys", report.channelActivePublishKeyCount],
|
|
520
|
+
[
|
|
521
|
+
"Last posted",
|
|
522
|
+
report.channelLastPostedAt
|
|
523
|
+
? formatTimestamp(report.channelLastPostedAt)
|
|
524
|
+
: undefined,
|
|
525
|
+
],
|
|
526
|
+
[
|
|
527
|
+
"Paused until",
|
|
528
|
+
report.channelPostingPausedUntil
|
|
529
|
+
? formatTimestamp(report.channelPostingPausedUntil)
|
|
530
|
+
: undefined,
|
|
531
|
+
],
|
|
532
|
+
[
|
|
533
|
+
"Latest blocked write",
|
|
534
|
+
report.channelLatestBlockedWriteAt
|
|
535
|
+
? formatTimestamp(report.channelLatestBlockedWriteAt)
|
|
536
|
+
: undefined,
|
|
537
|
+
],
|
|
538
|
+
[
|
|
539
|
+
"Latest blocked reason",
|
|
540
|
+
report.channelLatestBlockedWriteReasonLabel ||
|
|
541
|
+
report.channelLatestBlockedWriteReason ||
|
|
542
|
+
undefined,
|
|
543
|
+
],
|
|
544
|
+
]),
|
|
545
|
+
)
|
|
546
|
+
: undefined,
|
|
547
|
+
renderSection("Checks", renderChecks(report.checks)),
|
|
548
|
+
report.suggestions.length > 0
|
|
549
|
+
? renderSection("Suggestions", renderBullets(report.suggestions))
|
|
550
|
+
: undefined,
|
|
551
|
+
]);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderTokenStatus(
|
|
555
|
+
configured: boolean,
|
|
556
|
+
ok: boolean,
|
|
557
|
+
source: string,
|
|
558
|
+
error: string,
|
|
559
|
+
): string {
|
|
560
|
+
if (ok) {
|
|
561
|
+
return `ok (${source})`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (configured) {
|
|
565
|
+
return `failed (${source}): ${error}`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return error || "missing";
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function renderChecks(checks: DoctorCheck[]): string {
|
|
572
|
+
const rows = checks.map((check) => {
|
|
573
|
+
const status = check.ok ? "ok" : check.required ? "fail" : "warn";
|
|
574
|
+
const source = check.source ? ` [${check.source}]` : "";
|
|
575
|
+
|
|
576
|
+
return `${status.padEnd(4)} ${check.name}${source}\n ${check.detail}`;
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
return rows.join("\n");
|
|
580
|
+
}
|