@clankmates/cli 0.11.1 → 0.13.0
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 +7 -3
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +4 -3
- package/src/commands/auth/access-keys.ts +206 -0
- package/src/commands/auth.ts +3 -196
- package/src/commands/channel/render.ts +224 -0
- package/src/commands/channel/tokens.ts +145 -0
- package/src/commands/channel/validation.ts +11 -0
- package/src/commands/channel.ts +11 -340
- package/src/commands/doctor/checks.ts +123 -0
- package/src/commands/doctor/render.ts +140 -0
- package/src/commands/doctor/suggestions.ts +42 -0
- package/src/commands/doctor/types.ts +75 -0
- package/src/commands/doctor.ts +12 -371
- package/src/commands/feed.ts +19 -178
- package/src/commands/inbox/content.ts +31 -0
- package/src/commands/inbox/filters.ts +70 -0
- package/src/commands/inbox/messages.ts +69 -0
- package/src/commands/inbox/participants.ts +152 -0
- package/src/commands/inbox/render.ts +13 -0
- package/src/commands/inbox/resource-output.ts +217 -0
- package/src/commands/inbox/schema.ts +185 -0
- package/src/commands/inbox/screening.ts +76 -0
- package/src/commands/inbox/sync-scopes.ts +59 -0
- package/src/commands/inbox/thread-output.ts +344 -0
- package/src/commands/inbox/watch.ts +203 -0
- package/src/commands/inbox.ts +58 -1220
- package/src/commands/post.ts +24 -116
- package/src/lib/args.ts +8 -0
- package/src/lib/cache/scopes.ts +216 -0
- package/src/lib/cache/store.ts +195 -0
- package/src/lib/cache/types.ts +31 -0
- package/src/lib/cache.ts +18 -382
- package/src/lib/client/auth.ts +122 -0
- package/src/lib/client/channel-keys.ts +57 -0
- package/src/lib/client/channels.ts +364 -0
- package/src/lib/client/core.ts +133 -0
- package/src/lib/client/feed.ts +76 -0
- package/src/lib/client/inbox.ts +361 -0
- package/src/lib/client/posts.ts +213 -0
- package/src/lib/client/raw-api.ts +33 -0
- package/src/lib/client/users.ts +88 -0
- package/src/lib/client.ts +197 -894
- package/src/lib/help.ts +66 -9
- package/src/lib/json_api.ts +74 -9
- package/src/lib/pagination.ts +5 -0
- package/src/lib/polling.ts +146 -0
- package/src/lib/post-output.ts +55 -0
- package/src/types/api.ts +1 -0
package/src/commands/channel.ts
CHANGED
|
@@ -6,32 +6,20 @@ import {
|
|
|
6
6
|
stringFlag,
|
|
7
7
|
type ParsedArgs,
|
|
8
8
|
} from "../lib/args";
|
|
9
|
-
import { storeChannelToken, updateProfile } from "../lib/config";
|
|
10
9
|
import { createCommandContext } from "../lib/context";
|
|
11
10
|
import { CliError } from "../lib/errors";
|
|
11
|
+
import { printValue, type Io } from "../lib/output";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ChannelKeyAttributes,
|
|
24
|
-
ChannelKeyIssueResponse,
|
|
25
|
-
ChannelKeyRevokeResponse,
|
|
26
|
-
ChannelPinResponse,
|
|
27
|
-
ChannelPublicationResponse,
|
|
28
|
-
IdResponse,
|
|
29
|
-
ShareTokenResponse,
|
|
30
|
-
} from "../types/api";
|
|
31
|
-
|
|
32
|
-
const CHANNEL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
33
|
-
const CHANNEL_NAME_ERROR =
|
|
34
|
-
"Channel names must start with a lowercase letter or digit and then use only lowercase letters, digits, hyphens, or underscores.";
|
|
13
|
+
formatChannelDiagnostics,
|
|
14
|
+
formatChannelRecord,
|
|
15
|
+
printChannelCollection,
|
|
16
|
+
renderChannelPinAction,
|
|
17
|
+
renderChannelPublicationAction,
|
|
18
|
+
renderIdAction,
|
|
19
|
+
renderShareToken,
|
|
20
|
+
} from "./channel/render";
|
|
21
|
+
import { runChannelTokenCommand } from "./channel/tokens";
|
|
22
|
+
import { assertValidChannelName } from "./channel/validation";
|
|
35
23
|
|
|
36
24
|
export async function runChannelCommand(
|
|
37
25
|
args: ParsedArgs,
|
|
@@ -305,320 +293,3 @@ export async function runChannelCommand(
|
|
|
305
293
|
throw new CliError("Unknown channel subcommand", 2);
|
|
306
294
|
}
|
|
307
295
|
}
|
|
308
|
-
|
|
309
|
-
async function runChannelTokenCommand(
|
|
310
|
-
args: ParsedArgs,
|
|
311
|
-
io: Io,
|
|
312
|
-
): Promise<void> {
|
|
313
|
-
const context = await createCommandContext(args, io);
|
|
314
|
-
const tokenCommand = requiredPositional(
|
|
315
|
-
args.positionals,
|
|
316
|
-
1,
|
|
317
|
-
"Missing channel token subcommand",
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
switch (tokenCommand) {
|
|
321
|
-
case "list": {
|
|
322
|
-
const channelId = await context.client.resolveChannelId(
|
|
323
|
-
requiredPositional(args.positionals, 2, "Missing channel"),
|
|
324
|
-
);
|
|
325
|
-
const response = await context.client.listChannelKeys({
|
|
326
|
-
channelId,
|
|
327
|
-
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
328
|
-
cursor: stringFlag(args.flags, "cursor"),
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
if (context.outputMode === "json") {
|
|
332
|
-
printJson(
|
|
333
|
-
io,
|
|
334
|
-
paginatedJson(args, {
|
|
335
|
-
items: response.items,
|
|
336
|
-
nextCursor: response.nextCursor,
|
|
337
|
-
}),
|
|
338
|
-
);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
printValue(
|
|
343
|
-
io,
|
|
344
|
-
context.outputMode,
|
|
345
|
-
response.items.map((item) => formatChannelKeyRow(item)),
|
|
346
|
-
);
|
|
347
|
-
const pagination = paginationInfo(args, response.nextCursor);
|
|
348
|
-
const message = renderPagination(
|
|
349
|
-
pagination?.nextCursor,
|
|
350
|
-
pagination?.nextCommand,
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
if (message) {
|
|
354
|
-
io.stdout(message);
|
|
355
|
-
}
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
case "issue": {
|
|
360
|
-
const channelId = await context.client.resolveChannelId(
|
|
361
|
-
requiredPositional(args.positionals, 2, "Missing channel"),
|
|
362
|
-
);
|
|
363
|
-
const response = await context.client.issueChannelKey({
|
|
364
|
-
channelId,
|
|
365
|
-
name: requiredStringFlag(args.flags, "name"),
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
await maybeStoreChannelToken(
|
|
369
|
-
args,
|
|
370
|
-
response,
|
|
371
|
-
channelId,
|
|
372
|
-
context.profileName,
|
|
373
|
-
context.configPath,
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
if (booleanFlag(args.flags, "tokenOnly")) {
|
|
377
|
-
io.stdout(response.token);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
printValue(
|
|
382
|
-
io,
|
|
383
|
-
context.outputMode,
|
|
384
|
-
context.outputMode === "json"
|
|
385
|
-
? response
|
|
386
|
-
: renderChannelKeyIssue("Issued channel token", response),
|
|
387
|
-
);
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
case "revoke": {
|
|
392
|
-
const response = await context.client.revokeChannelKey(
|
|
393
|
-
requiredPositional(args.positionals, 2, "Missing channel key id"),
|
|
394
|
-
);
|
|
395
|
-
await pruneInvalidStoredChannelTokens(
|
|
396
|
-
context.profileName,
|
|
397
|
-
context.profile.channelTokens,
|
|
398
|
-
context.client,
|
|
399
|
-
context.configPath,
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
printValue(
|
|
403
|
-
io,
|
|
404
|
-
context.outputMode,
|
|
405
|
-
context.outputMode === "json"
|
|
406
|
-
? response
|
|
407
|
-
: renderChannelKeyRevoke("Revoked channel token", response),
|
|
408
|
-
);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
default:
|
|
413
|
-
throw new CliError("Unknown channel token subcommand", 2);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function maybeStoreChannelToken(
|
|
418
|
-
args: ParsedArgs,
|
|
419
|
-
response: ChannelKeyIssueResponse,
|
|
420
|
-
channelId: string,
|
|
421
|
-
profileName: string,
|
|
422
|
-
configPath: string,
|
|
423
|
-
): Promise<void> {
|
|
424
|
-
if (!booleanFlag(args.flags, "save")) {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
await storeChannelToken(profileName, channelId, response.token, configPath);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function printChannelCollection(
|
|
432
|
-
args: ParsedArgs,
|
|
433
|
-
outputMode: "json" | "table",
|
|
434
|
-
io: Io,
|
|
435
|
-
response: {
|
|
436
|
-
items: Array<{ id: string; attributes: ChannelAttributes }>;
|
|
437
|
-
nextCursor?: string;
|
|
438
|
-
},
|
|
439
|
-
): void {
|
|
440
|
-
if (outputMode === "json") {
|
|
441
|
-
printJson(
|
|
442
|
-
io,
|
|
443
|
-
paginatedJson(args, {
|
|
444
|
-
items: response.items,
|
|
445
|
-
nextCursor: response.nextCursor,
|
|
446
|
-
}),
|
|
447
|
-
);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
printValue(
|
|
452
|
-
io,
|
|
453
|
-
outputMode,
|
|
454
|
-
response.items.map((item) => formatChannelRow(item)),
|
|
455
|
-
);
|
|
456
|
-
const pagination = paginationInfo(args, response.nextCursor);
|
|
457
|
-
const message = renderPagination(
|
|
458
|
-
pagination?.nextCursor,
|
|
459
|
-
pagination?.nextCommand,
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
if (message) {
|
|
463
|
-
io.stdout(message);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function formatChannelRecord(channel: { id: string; attributes: ChannelAttributes }) {
|
|
468
|
-
return {
|
|
469
|
-
id: channel.id,
|
|
470
|
-
name: channel.attributes.name,
|
|
471
|
-
visibility: channel.attributes.visibility,
|
|
472
|
-
publiclyListed: channel.attributes.publicly_listed ?? false,
|
|
473
|
-
description: channel.attributes.description ?? "",
|
|
474
|
-
postingPausedUntil: channel.attributes.posting_paused_until ?? "",
|
|
475
|
-
pinnedPostId: channel.attributes.pinned_post_id ?? "",
|
|
476
|
-
insertedAt: channel.attributes.inserted_at ?? "",
|
|
477
|
-
updatedAt: channel.attributes.updated_at ?? "",
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function formatChannelDiagnostics(diagnostics: ChannelDiagnosticsResponse) {
|
|
482
|
-
return {
|
|
483
|
-
channelId: diagnostics.channel_id,
|
|
484
|
-
channelName: diagnostics.channel_name,
|
|
485
|
-
channelDescription: diagnostics.channel_description ?? "",
|
|
486
|
-
stateLabels: diagnostics.state_labels.join(", "),
|
|
487
|
-
activePublishKeyCount: diagnostics.active_publish_key_count,
|
|
488
|
-
lastPostedAt: diagnostics.last_posted_at ?? "",
|
|
489
|
-
postingPausedUntil: diagnostics.posting_paused_until ?? "",
|
|
490
|
-
latestBlockedWriteAt: diagnostics.latest_blocked_write_at ?? "",
|
|
491
|
-
latestBlockedWriteReason:
|
|
492
|
-
diagnostics.latest_blocked_write_reason_label ??
|
|
493
|
-
diagnostics.latest_blocked_write_reason ??
|
|
494
|
-
"",
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function formatChannelRow(channel: { id: string; attributes: ChannelAttributes }) {
|
|
499
|
-
return {
|
|
500
|
-
id: channel.id,
|
|
501
|
-
name: channel.attributes.name,
|
|
502
|
-
visibility: channel.attributes.visibility,
|
|
503
|
-
publiclyListed: channel.attributes.publicly_listed ?? false,
|
|
504
|
-
pinnedPostId: channel.attributes.pinned_post_id ?? "",
|
|
505
|
-
description: channel.attributes.description ?? "",
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function formatChannelKeyRow(item: { id: string; attributes: ChannelKeyAttributes }) {
|
|
510
|
-
return {
|
|
511
|
-
id: item.id,
|
|
512
|
-
name: item.attributes.name ?? "",
|
|
513
|
-
expiresAt: item.attributes.expires_at ?? "",
|
|
514
|
-
revokedAt: item.attributes.revoked_at ?? "",
|
|
515
|
-
issuedAt: item.attributes.inserted_at ?? "",
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function renderShareToken(title: string, response: ShareTokenResponse): string {
|
|
520
|
-
return joinBlocks([
|
|
521
|
-
title,
|
|
522
|
-
renderFields([["Token", response.token]]),
|
|
523
|
-
]);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function renderIdAction(title: string, response: IdResponse): string {
|
|
527
|
-
return joinBlocks([
|
|
528
|
-
title,
|
|
529
|
-
renderFields([["ID", response.id]]),
|
|
530
|
-
]);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function renderChannelPublicationAction(
|
|
534
|
-
title: string,
|
|
535
|
-
response: ChannelPublicationResponse,
|
|
536
|
-
): string {
|
|
537
|
-
return joinBlocks([
|
|
538
|
-
title,
|
|
539
|
-
renderFields([
|
|
540
|
-
["ID", response.id],
|
|
541
|
-
["Name", response.name],
|
|
542
|
-
["Publicly listed", response.publicly_listed],
|
|
543
|
-
]),
|
|
544
|
-
]);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function renderChannelPinAction(
|
|
548
|
-
title: string,
|
|
549
|
-
response: ChannelPinResponse,
|
|
550
|
-
): string {
|
|
551
|
-
const channel = Array.isArray(response.data)
|
|
552
|
-
? response.data[0]
|
|
553
|
-
: response.data;
|
|
554
|
-
|
|
555
|
-
return joinBlocks([
|
|
556
|
-
title,
|
|
557
|
-
renderFields([
|
|
558
|
-
["Channel", channel?.attributes.name ?? ""],
|
|
559
|
-
["Pinned post", channel?.attributes.pinned_post_id ?? ""],
|
|
560
|
-
]),
|
|
561
|
-
]);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function renderChannelKeyIssue(
|
|
565
|
-
title: string,
|
|
566
|
-
response: ChannelKeyIssueResponse,
|
|
567
|
-
): string {
|
|
568
|
-
return renderTokenAction({
|
|
569
|
-
title,
|
|
570
|
-
id: response.id,
|
|
571
|
-
name: response.name,
|
|
572
|
-
token: response.token,
|
|
573
|
-
issuedAt: response.issued_at,
|
|
574
|
-
expiresAt: response.expires_at,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function renderChannelKeyRevoke(
|
|
579
|
-
title: string,
|
|
580
|
-
response: ChannelKeyRevokeResponse,
|
|
581
|
-
): string {
|
|
582
|
-
return joinBlocks([
|
|
583
|
-
title,
|
|
584
|
-
renderFields([
|
|
585
|
-
["ID", response.id],
|
|
586
|
-
["Name", response.name],
|
|
587
|
-
]),
|
|
588
|
-
]);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async function pruneInvalidStoredChannelTokens(
|
|
592
|
-
profileName: string,
|
|
593
|
-
storedTokens: Record<string, { token: string }>,
|
|
594
|
-
client: Awaited<ReturnType<typeof createCommandContext>>["client"],
|
|
595
|
-
configPath: string,
|
|
596
|
-
): Promise<void> {
|
|
597
|
-
const invalidChannelIds: string[] = [];
|
|
598
|
-
|
|
599
|
-
for (const [channelId, storedToken] of Object.entries(storedTokens)) {
|
|
600
|
-
if (!(await client.canAuthenticate(storedToken.token))) {
|
|
601
|
-
invalidChannelIds.push(channelId);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
if (invalidChannelIds.length === 0) {
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
await updateProfile(
|
|
610
|
-
profileName,
|
|
611
|
-
(profile) => {
|
|
612
|
-
for (const channelId of invalidChannelIds) {
|
|
613
|
-
delete profile.channelTokens[channelId];
|
|
614
|
-
}
|
|
615
|
-
},
|
|
616
|
-
configPath,
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function assertValidChannelName(name: string): void {
|
|
621
|
-
if (!CHANNEL_NAME_PATTERN.test(name)) {
|
|
622
|
-
throw new CliError(CHANNEL_NAME_ERROR, 2);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ChannelDiagnostics,
|
|
5
|
+
ChannelDiagnosticsResult,
|
|
6
|
+
ChannelResolution,
|
|
7
|
+
CheckResult,
|
|
8
|
+
DoctorContext,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
export async function checkConfigPath(configPath: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
await access(configPath);
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function resolveRequestedChannel(
|
|
25
|
+
context: DoctorContext,
|
|
26
|
+
channel: string,
|
|
27
|
+
): Promise<ChannelResolution> {
|
|
28
|
+
try {
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
channelId: await context.client.resolveChannelId(channel),
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: (error as Error).message,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runOptionalCheck(
|
|
42
|
+
token: string | undefined,
|
|
43
|
+
operation: (token: string) => Promise<unknown>,
|
|
44
|
+
missingMessage: string,
|
|
45
|
+
): Promise<CheckResult> {
|
|
46
|
+
if (!token) {
|
|
47
|
+
return { ok: false, error: missingMessage };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return runCheck(() => operation(token));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runCheck(
|
|
54
|
+
operation: () => Promise<unknown>,
|
|
55
|
+
): Promise<CheckResult> {
|
|
56
|
+
try {
|
|
57
|
+
await operation();
|
|
58
|
+
return { ok: true };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return { ok: false, error: (error as Error).message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function maybeFetchChannelDiagnostics(input: {
|
|
65
|
+
context: DoctorContext;
|
|
66
|
+
requestedChannel?: string;
|
|
67
|
+
channelResolution: ChannelResolution;
|
|
68
|
+
ownerReadReady: boolean;
|
|
69
|
+
}): Promise<ChannelDiagnosticsResult> {
|
|
70
|
+
if (!input.requestedChannel) {
|
|
71
|
+
return { ok: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!input.channelResolution.ok || !input.channelResolution.channelId) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: "Channel diagnostics require a resolved channel.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!input.ownerReadReady) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: "Channel diagnostics require an owner-read token.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
value: await input.context.client.getChannelDiagnostics(
|
|
92
|
+
input.channelResolution.channelId,
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: (error as Error).message,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatChannelSummary(
|
|
104
|
+
diagnostics: ChannelDiagnostics | undefined,
|
|
105
|
+
): string {
|
|
106
|
+
if (!diagnostics || diagnostics.state_labels.length === 0) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return diagnostics.state_labels.join(", ");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatChannelDiagnosticsDetail(
|
|
114
|
+
diagnostics: ChannelDiagnostics | undefined,
|
|
115
|
+
): string {
|
|
116
|
+
const summary = formatChannelSummary(diagnostics);
|
|
117
|
+
|
|
118
|
+
if (!summary) {
|
|
119
|
+
return "Channel diagnostics are unavailable.";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `Channel diagnostics loaded successfully: ${summary}.`;
|
|
123
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatTimestamp,
|
|
3
|
+
joinBlocks,
|
|
4
|
+
renderBullets,
|
|
5
|
+
renderFields,
|
|
6
|
+
renderSection,
|
|
7
|
+
} from "../../lib/human";
|
|
8
|
+
import type { DoctorCheck, DoctorReport } from "./types";
|
|
9
|
+
|
|
10
|
+
export function renderDoctorReport(report: DoctorReport): string {
|
|
11
|
+
return joinBlocks([
|
|
12
|
+
renderSection(
|
|
13
|
+
"Status",
|
|
14
|
+
renderFields([
|
|
15
|
+
["Result", report.status],
|
|
16
|
+
["Summary", report.summary],
|
|
17
|
+
["Profile", report.profile],
|
|
18
|
+
["Base URL", report.baseUrl],
|
|
19
|
+
["Config", report.configPath],
|
|
20
|
+
["CLI version", report.cliVersion],
|
|
21
|
+
]),
|
|
22
|
+
),
|
|
23
|
+
renderSection(
|
|
24
|
+
"Auth",
|
|
25
|
+
renderFields([
|
|
26
|
+
[
|
|
27
|
+
"Master token",
|
|
28
|
+
renderTokenStatus(
|
|
29
|
+
report.hasMasterToken,
|
|
30
|
+
report.masterTokenOk,
|
|
31
|
+
report.masterTokenSource,
|
|
32
|
+
report.masterTokenError,
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"Read-only token",
|
|
37
|
+
renderTokenStatus(
|
|
38
|
+
report.hasReadOnlyToken,
|
|
39
|
+
report.readOnlyTokenOk,
|
|
40
|
+
report.readOnlyTokenSource,
|
|
41
|
+
report.readOnlyTokenError,
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"Owner-read token",
|
|
46
|
+
renderTokenStatus(
|
|
47
|
+
report.ownerReadTokenAvailable,
|
|
48
|
+
report.ownerReadTokenOk,
|
|
49
|
+
report.ownerReadTokenSource,
|
|
50
|
+
report.ownerReadTokenError,
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
["Stored channel tokens", report.storedChannelTokens],
|
|
54
|
+
]),
|
|
55
|
+
),
|
|
56
|
+
report.channel
|
|
57
|
+
? renderSection(
|
|
58
|
+
"Channel",
|
|
59
|
+
renderFields([
|
|
60
|
+
["Requested", report.channel],
|
|
61
|
+
["Resolved ID", report.channelId],
|
|
62
|
+
[
|
|
63
|
+
"Resolution",
|
|
64
|
+
report.channelResolutionOk ? "ok" : report.channelResolutionError,
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"Publish token",
|
|
68
|
+
report.publishTokenAvailable
|
|
69
|
+
? `available (${report.publishTokenSource})`
|
|
70
|
+
: "missing",
|
|
71
|
+
],
|
|
72
|
+
["Publish ready", report.publishReady],
|
|
73
|
+
[
|
|
74
|
+
"Diagnostics",
|
|
75
|
+
report.channelDiagnosticsAvailable
|
|
76
|
+
? report.channelSummary
|
|
77
|
+
: report.channelDiagnosticsError,
|
|
78
|
+
],
|
|
79
|
+
["Active publish keys", report.channelActivePublishKeyCount],
|
|
80
|
+
[
|
|
81
|
+
"Last posted",
|
|
82
|
+
report.channelLastPostedAt
|
|
83
|
+
? formatTimestamp(report.channelLastPostedAt)
|
|
84
|
+
: undefined,
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"Paused until",
|
|
88
|
+
report.channelPostingPausedUntil
|
|
89
|
+
? formatTimestamp(report.channelPostingPausedUntil)
|
|
90
|
+
: undefined,
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"Latest blocked write",
|
|
94
|
+
report.channelLatestBlockedWriteAt
|
|
95
|
+
? formatTimestamp(report.channelLatestBlockedWriteAt)
|
|
96
|
+
: undefined,
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"Latest blocked reason",
|
|
100
|
+
report.channelLatestBlockedWriteReasonLabel ||
|
|
101
|
+
report.channelLatestBlockedWriteReason ||
|
|
102
|
+
undefined,
|
|
103
|
+
],
|
|
104
|
+
]),
|
|
105
|
+
)
|
|
106
|
+
: undefined,
|
|
107
|
+
renderSection("Checks", renderChecks(report.checks)),
|
|
108
|
+
report.suggestions.length > 0
|
|
109
|
+
? renderSection("Suggestions", renderBullets(report.suggestions))
|
|
110
|
+
: undefined,
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderTokenStatus(
|
|
115
|
+
configured: boolean,
|
|
116
|
+
ok: boolean,
|
|
117
|
+
source: string,
|
|
118
|
+
error: string,
|
|
119
|
+
): string {
|
|
120
|
+
if (ok) {
|
|
121
|
+
return `ok (${source})`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (configured) {
|
|
125
|
+
return `failed (${source}): ${error}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return error || "missing";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderChecks(checks: DoctorCheck[]): string {
|
|
132
|
+
const rows = checks.map((check) => {
|
|
133
|
+
const status = check.ok ? "ok" : check.required ? "fail" : "warn";
|
|
134
|
+
const source = check.source ? ` [${check.source}]` : "";
|
|
135
|
+
|
|
136
|
+
return `${status.padEnd(4)} ${check.name}${source}\n ${check.detail}`;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return rows.join("\n");
|
|
140
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function buildSuggestions(input: {
|
|
2
|
+
configFileExists: boolean;
|
|
3
|
+
openApiOk: boolean;
|
|
4
|
+
ownerReadReady: boolean;
|
|
5
|
+
requestedChannel?: string;
|
|
6
|
+
channelResolutionOk: boolean;
|
|
7
|
+
publishReady: boolean;
|
|
8
|
+
channelDiagnosticsOk: boolean;
|
|
9
|
+
}): string[] {
|
|
10
|
+
const suggestions: string[] = [];
|
|
11
|
+
|
|
12
|
+
if (!input.configFileExists) {
|
|
13
|
+
suggestions.push("Run `clankm config init` to create local config.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!input.openApiOk) {
|
|
17
|
+
suggestions.push("Check `CLANKMATES_BASE_URL` or `--base-url`, then retry `clankm doctor --json`.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!input.ownerReadReady) {
|
|
21
|
+
suggestions.push("Configure a read-only or master token for owner reads with `clankm auth login ...`.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (input.requestedChannel && !input.channelResolutionOk) {
|
|
25
|
+
suggestions.push("Use a channel UUID or configure an owner-read token so channel names can be resolved.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (input.requestedChannel && !input.publishReady) {
|
|
29
|
+
suggestions.push("Provide `--channel-token`, `CLANKMATES_CHANNEL_TOKEN`, `CLANKMATES_CHANNEL_TOKENS_JSON`, `CLANKMATES_CHANNEL_TOKENS_FILE`, or a master token for publish.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
input.requestedChannel &&
|
|
34
|
+
input.channelResolutionOk &&
|
|
35
|
+
input.ownerReadReady &&
|
|
36
|
+
!input.channelDiagnosticsOk
|
|
37
|
+
) {
|
|
38
|
+
suggestions.push("Retry the channel diagnostics with an owner-read token that can read the requested channel.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return suggestions;
|
|
42
|
+
}
|