@clankmates/cli 0.12.0 → 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.
Files changed (46) hide show
  1. package/README.md +4 -1
  2. package/package.json +1 -1
  3. package/src/commands/auth/access-keys.ts +206 -0
  4. package/src/commands/auth.ts +3 -196
  5. package/src/commands/channel/render.ts +224 -0
  6. package/src/commands/channel/tokens.ts +145 -0
  7. package/src/commands/channel/validation.ts +11 -0
  8. package/src/commands/channel.ts +11 -340
  9. package/src/commands/doctor/checks.ts +123 -0
  10. package/src/commands/doctor/render.ts +140 -0
  11. package/src/commands/doctor/suggestions.ts +42 -0
  12. package/src/commands/doctor/types.ts +75 -0
  13. package/src/commands/doctor.ts +12 -371
  14. package/src/commands/feed.ts +15 -178
  15. package/src/commands/inbox/content.ts +31 -0
  16. package/src/commands/inbox/filters.ts +70 -0
  17. package/src/commands/inbox/messages.ts +69 -0
  18. package/src/commands/inbox/participants.ts +152 -0
  19. package/src/commands/inbox/render.ts +13 -0
  20. package/src/commands/inbox/resource-output.ts +217 -0
  21. package/src/commands/inbox/schema.ts +185 -0
  22. package/src/commands/inbox/screening.ts +76 -0
  23. package/src/commands/inbox/sync-scopes.ts +59 -0
  24. package/src/commands/inbox/thread-output.ts +344 -0
  25. package/src/commands/inbox/watch.ts +203 -0
  26. package/src/commands/inbox.ts +37 -1243
  27. package/src/commands/post.ts +9 -114
  28. package/src/lib/args.ts +1 -0
  29. package/src/lib/cache/scopes.ts +216 -0
  30. package/src/lib/cache/store.ts +195 -0
  31. package/src/lib/cache/types.ts +31 -0
  32. package/src/lib/cache.ts +18 -436
  33. package/src/lib/client/auth.ts +122 -0
  34. package/src/lib/client/channel-keys.ts +57 -0
  35. package/src/lib/client/channels.ts +364 -0
  36. package/src/lib/client/core.ts +133 -0
  37. package/src/lib/client/feed.ts +76 -0
  38. package/src/lib/client/inbox.ts +361 -0
  39. package/src/lib/client/posts.ts +213 -0
  40. package/src/lib/client/raw-api.ts +33 -0
  41. package/src/lib/client/users.ts +88 -0
  42. package/src/lib/client.ts +177 -913
  43. package/src/lib/help.ts +26 -0
  44. package/src/lib/json_api.ts +74 -9
  45. package/src/lib/polling.ts +146 -0
  46. package/src/lib/post-output.ts +55 -0
@@ -0,0 +1,145 @@
1
+ import {
2
+ booleanFlag,
3
+ integerFlag,
4
+ requiredPositional,
5
+ requiredStringFlag,
6
+ stringFlag,
7
+ type ParsedArgs,
8
+ } from "../../lib/args";
9
+ import { storeChannelToken, updateProfile } from "../../lib/config";
10
+ import { createCommandContext } from "../../lib/context";
11
+ import { CliError } from "../../lib/errors";
12
+ import { printValue, type Io } from "../../lib/output";
13
+ import type { ChannelKeyIssueResponse } from "../../types/api";
14
+ import {
15
+ printChannelKeyCollection,
16
+ renderChannelKeyIssue,
17
+ renderChannelKeyRevoke,
18
+ } from "./render";
19
+
20
+ export async function runChannelTokenCommand(
21
+ args: ParsedArgs,
22
+ io: Io,
23
+ ): Promise<void> {
24
+ const context = await createCommandContext(args, io);
25
+ const tokenCommand = requiredPositional(
26
+ args.positionals,
27
+ 1,
28
+ "Missing channel token subcommand",
29
+ );
30
+
31
+ switch (tokenCommand) {
32
+ case "list": {
33
+ const channelId = await context.client.resolveChannelId(
34
+ requiredPositional(args.positionals, 2, "Missing channel"),
35
+ );
36
+ const response = await context.client.listChannelKeys({
37
+ channelId,
38
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
39
+ cursor: stringFlag(args.flags, "cursor"),
40
+ });
41
+
42
+ printChannelKeyCollection(args, context.outputMode, io, response);
43
+ return;
44
+ }
45
+
46
+ case "issue": {
47
+ const channelId = await context.client.resolveChannelId(
48
+ requiredPositional(args.positionals, 2, "Missing channel"),
49
+ );
50
+ const response = await context.client.issueChannelKey({
51
+ channelId,
52
+ name: requiredStringFlag(args.flags, "name"),
53
+ });
54
+
55
+ await maybeStoreChannelToken(
56
+ args,
57
+ response,
58
+ channelId,
59
+ context.profileName,
60
+ context.configPath,
61
+ );
62
+
63
+ if (booleanFlag(args.flags, "tokenOnly")) {
64
+ io.stdout(response.token);
65
+ return;
66
+ }
67
+
68
+ printValue(
69
+ io,
70
+ context.outputMode,
71
+ context.outputMode === "json"
72
+ ? response
73
+ : renderChannelKeyIssue("Issued channel token", response),
74
+ );
75
+ return;
76
+ }
77
+
78
+ case "revoke": {
79
+ const response = await context.client.revokeChannelKey(
80
+ requiredPositional(args.positionals, 2, "Missing channel key id"),
81
+ );
82
+ await pruneInvalidStoredChannelTokens(
83
+ context.profileName,
84
+ context.profile.channelTokens,
85
+ context.client,
86
+ context.configPath,
87
+ );
88
+
89
+ printValue(
90
+ io,
91
+ context.outputMode,
92
+ context.outputMode === "json"
93
+ ? response
94
+ : renderChannelKeyRevoke("Revoked channel token", response),
95
+ );
96
+ return;
97
+ }
98
+
99
+ default:
100
+ throw new CliError("Unknown channel token subcommand", 2);
101
+ }
102
+ }
103
+
104
+ async function maybeStoreChannelToken(
105
+ args: ParsedArgs,
106
+ response: ChannelKeyIssueResponse,
107
+ channelId: string,
108
+ profileName: string,
109
+ configPath: string,
110
+ ): Promise<void> {
111
+ if (!booleanFlag(args.flags, "save")) {
112
+ return;
113
+ }
114
+
115
+ await storeChannelToken(profileName, channelId, response.token, configPath);
116
+ }
117
+
118
+ async function pruneInvalidStoredChannelTokens(
119
+ profileName: string,
120
+ storedTokens: Record<string, { token: string }>,
121
+ client: Awaited<ReturnType<typeof createCommandContext>>["client"],
122
+ configPath: string,
123
+ ): Promise<void> {
124
+ const invalidChannelIds: string[] = [];
125
+
126
+ for (const [channelId, storedToken] of Object.entries(storedTokens)) {
127
+ if (!(await client.canAuthenticate(storedToken.token))) {
128
+ invalidChannelIds.push(channelId);
129
+ }
130
+ }
131
+
132
+ if (invalidChannelIds.length === 0) {
133
+ return;
134
+ }
135
+
136
+ await updateProfile(
137
+ profileName,
138
+ (profile) => {
139
+ for (const channelId of invalidChannelIds) {
140
+ delete profile.channelTokens[channelId];
141
+ }
142
+ },
143
+ configPath,
144
+ );
145
+ }
@@ -0,0 +1,11 @@
1
+ import { CliError } from "../../lib/errors";
2
+
3
+ const CHANNEL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
4
+ const CHANNEL_NAME_ERROR =
5
+ "Channel names must start with a lowercase letter or digit and then use only lowercase letters, digits, hyphens, or underscores.";
6
+
7
+ export function assertValidChannelName(name: string): void {
8
+ if (!CHANNEL_NAME_PATTERN.test(name)) {
9
+ throw new CliError(CHANNEL_NAME_ERROR, 2);
10
+ }
11
+ }
@@ -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
- joinBlocks,
14
- renderFields,
15
- renderPagination,
16
- renderTokenAction,
17
- } from "../lib/human";
18
- import { printJson, printValue, type Io } from "../lib/output";
19
- import { paginatedJson, paginationInfo } from "../lib/pagination";
20
- import type {
21
- ChannelAttributes,
22
- ChannelDiagnosticsResponse,
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
+ }