@clankmates/cli 0.7.1 → 0.9.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.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ booleanFlag,
2
3
  integerFlag,
3
4
  requiredPositional,
4
5
  stringFlag,
@@ -16,7 +17,9 @@ import {
16
17
  renderSection,
17
18
  shortId,
18
19
  } from "../lib/human";
20
+ import { resolveJsonInput } from "../lib/json-input";
19
21
  import { printJson, printValue, type Io } from "../lib/output";
22
+ import { paginatedJson, paginationInfo } from "../lib/pagination";
20
23
  import type {
21
24
  ExternalEmailIntakeAttributes,
22
25
  InboxRecipient,
@@ -44,7 +47,7 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
44
47
  channelToken,
45
48
  });
46
49
 
47
- await printThreadCollection(context, io, response, channelToken);
50
+ await printThreadCollection(args, context, io, response, channelToken);
48
51
  return;
49
52
  }
50
53
 
@@ -70,12 +73,12 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
70
73
  io,
71
74
  context.outputMode,
72
75
  context.outputMode === "json"
73
- ? {
76
+ ? paginatedJson(args, {
74
77
  thread,
75
78
  messages: messages.items,
76
79
  nextCursor: messages.nextCursor,
77
- }
78
- : renderThreadWithMessages(thread, messages, publicUsers),
80
+ })
81
+ : renderThreadWithMessages(args, thread, messages, publicUsers),
79
82
  );
80
83
  return;
81
84
  }
@@ -93,11 +96,11 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
93
96
  io,
94
97
  context.outputMode,
95
98
  context.outputMode === "json"
96
- ? {
99
+ ? paginatedJson(args, {
97
100
  items: response.items,
98
101
  nextCursor: response.nextCursor,
99
- }
100
- : renderAttachmentCollection(response),
102
+ })
103
+ : renderAttachmentCollection(args, response),
101
104
  );
102
105
  return;
103
106
  }
@@ -107,16 +110,20 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
107
110
  return;
108
111
  }
109
112
 
113
+ case "schema": {
114
+ await runSchemaCommand(context, args, io);
115
+ return;
116
+ }
117
+
110
118
  case "send": {
119
+ const content = await resolveMessageContent(args);
111
120
  const thread = await context.client.createThread({
112
121
  recipient: await parseRecipient(
113
122
  context,
114
123
  requiredPositional(args.positionals, 1, "Missing recipient"),
115
124
  ),
116
- body: (await resolveBodyInput({
117
- flags: args.flags,
118
- requireBody: true,
119
- }))!,
125
+ body: content.body,
126
+ payload: content.payload,
120
127
  from: await resolveSender(context, args),
121
128
  contextPostId: stringFlag(args.flags, "contextPostId"),
122
129
  channelToken: stringFlag(args.flags, "channelToken"),
@@ -135,12 +142,11 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
135
142
  case "reply": {
136
143
  const threadId = requiredPositional(args.positionals, 1, "Missing thread id");
137
144
  const channelToken = stringFlag(args.flags, "channelToken");
145
+ const content = await resolveMessageContent(args);
138
146
  const thread = await context.client.appendThreadMessage({
139
147
  threadId,
140
- body: (await resolveBodyInput({
141
- flags: args.flags,
142
- requireBody: true,
143
- }))!,
148
+ body: content.body,
149
+ payload: content.payload,
144
150
  from: await resolveSender(context, args),
145
151
  contextPostId: stringFlag(args.flags, "contextPostId"),
146
152
  channelToken,
@@ -229,6 +235,154 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
229
235
  }
230
236
  }
231
237
 
238
+ async function resolveMessageContent(args: ParsedArgs): Promise<{
239
+ body?: string;
240
+ payload?: Record<string, unknown>;
241
+ }> {
242
+ if (booleanFlag(args.flags, "stdin") && booleanFlag(args.flags, "payloadStdin")) {
243
+ throw new CliError("Use only one of `--stdin` or `--payload-stdin`", 2);
244
+ }
245
+
246
+ const body = await resolveBodyInput({ flags: args.flags });
247
+ const payload = await resolveJsonInput({
248
+ flags: args.flags,
249
+ inlineKey: "payload",
250
+ fileKey: "payloadFile",
251
+ stdinKey: "payloadStdin",
252
+ label: "Payload",
253
+ });
254
+
255
+ if (body === undefined && payload === undefined) {
256
+ throw new CliError(
257
+ "Provide at least one of `--body`, `--body-file`, `--stdin`, `--payload`, `--payload-file`, or `--payload-stdin`",
258
+ 2,
259
+ );
260
+ }
261
+
262
+ return { body, payload };
263
+ }
264
+
265
+ async function runSchemaCommand(
266
+ context: CommandContext,
267
+ args: ParsedArgs,
268
+ io: Io,
269
+ ): Promise<void> {
270
+ const subcommand = args.positionals[1];
271
+
272
+ switch (subcommand) {
273
+ case "show": {
274
+ const target = requiredPositional(
275
+ args.positionals,
276
+ 2,
277
+ "Missing schema target",
278
+ );
279
+ const schema = await getPublicInboxSchema(context, target);
280
+ printSchemaResource(context, io, schema);
281
+ return;
282
+ }
283
+
284
+ case "set": {
285
+ const scope = requiredPositional(
286
+ args.positionals,
287
+ 2,
288
+ "Missing schema scope",
289
+ );
290
+ const inboxSchema = await requiredInboxSchema(args);
291
+
292
+ if (scope === "account") {
293
+ printSchemaResource(
294
+ context,
295
+ io,
296
+ await context.client.setAccountInboxSchema(inboxSchema),
297
+ "Updated account inbox schema",
298
+ );
299
+ return;
300
+ }
301
+
302
+ if (scope === "channel") {
303
+ const channelRef = requiredPositional(
304
+ args.positionals,
305
+ 3,
306
+ "Missing channel name or id",
307
+ );
308
+ const channelId = await context.client.resolveChannelId(channelRef);
309
+ printSchemaResource(
310
+ context,
311
+ io,
312
+ await context.client.setChannelInboxSchema({
313
+ channelId,
314
+ inboxSchema,
315
+ }),
316
+ "Updated channel inbox schema",
317
+ );
318
+ return;
319
+ }
320
+
321
+ throw new CliError("Schema scope must be `account` or `channel`", 2);
322
+ }
323
+
324
+ case "remove": {
325
+ const scope = requiredPositional(
326
+ args.positionals,
327
+ 2,
328
+ "Missing schema scope",
329
+ );
330
+
331
+ if (scope === "account") {
332
+ printSchemaResource(
333
+ context,
334
+ io,
335
+ await context.client.removeAccountInboxSchema(),
336
+ "Removed account inbox schema",
337
+ );
338
+ return;
339
+ }
340
+
341
+ if (scope === "channel") {
342
+ const channelRef = requiredPositional(
343
+ args.positionals,
344
+ 3,
345
+ "Missing channel name or id",
346
+ );
347
+ const channelId = await context.client.resolveChannelId(channelRef);
348
+ printSchemaResource(
349
+ context,
350
+ io,
351
+ await context.client.removeChannelInboxSchema(channelId),
352
+ "Removed channel inbox schema",
353
+ );
354
+ return;
355
+ }
356
+
357
+ throw new CliError("Schema scope must be `account` or `channel`", 2);
358
+ }
359
+
360
+ default:
361
+ throw new CliError("Unknown inbox schema subcommand", 2);
362
+ }
363
+ }
364
+
365
+ async function requiredInboxSchema(
366
+ args: ParsedArgs,
367
+ ): Promise<Record<string, unknown>> {
368
+ const schema = await resolveJsonInput({
369
+ flags: args.flags,
370
+ inlineKey: "schema",
371
+ fileKey: "schemaFile",
372
+ stdinKey: "schemaStdin",
373
+ label: "Inbox schema",
374
+ });
375
+
376
+ if (!schema) {
377
+ throw new CliError(
378
+ "Provide exactly one of `--schema`, `--schema-file`, or `--schema-stdin`",
379
+ 2,
380
+ );
381
+ }
382
+
383
+ return schema;
384
+ }
385
+
232
386
  async function runScreeningCommand(
233
387
  context: CommandContext,
234
388
  args: ParsedArgs,
@@ -245,7 +399,7 @@ async function runScreeningCommand(
245
399
  channelToken,
246
400
  });
247
401
 
248
- printEmailIntakeCollection(context, io, response);
402
+ printEmailIntakeCollection(args, context, io, response);
249
403
  return;
250
404
  }
251
405
 
@@ -256,7 +410,7 @@ async function runScreeningCommand(
256
410
  channelToken,
257
411
  });
258
412
 
259
- printEmailIntakeCollection(context, io, response);
413
+ printEmailIntakeCollection(args, context, io, response);
260
414
  return;
261
415
  }
262
416
 
@@ -461,7 +615,28 @@ function looksLikeEmailAddress(value: string): boolean {
461
615
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
462
616
  }
463
617
 
618
+ async function getPublicInboxSchema(
619
+ context: CommandContext,
620
+ target: string,
621
+ ) {
622
+ const handleChannel = parseHandleChannel(target);
623
+
624
+ if (handleChannel) {
625
+ return context.client.getPublicChannelInboxSchema(
626
+ handleChannel.ownerHandle,
627
+ handleChannel.channelName,
628
+ );
629
+ }
630
+
631
+ if (target.startsWith("@")) {
632
+ return context.client.getPublicAccountInboxSchema(target);
633
+ }
634
+
635
+ throw new CliError("Schema target must be `@handle` or `@handle/channel`", 2);
636
+ }
637
+
464
638
  async function printThreadCollection(
639
+ args: ParsedArgs,
465
640
  context: CommandContext,
466
641
  io: Io,
467
642
  response: {
@@ -471,10 +646,13 @@ async function printThreadCollection(
471
646
  channelToken?: string,
472
647
  ): Promise<void> {
473
648
  if (context.outputMode === "json") {
474
- printJson(io, {
475
- items: response.items,
476
- nextCursor: response.nextCursor,
477
- });
649
+ printJson(
650
+ io,
651
+ paginatedJson(args, {
652
+ items: response.items,
653
+ nextCursor: response.nextCursor,
654
+ }),
655
+ );
478
656
  return Promise.resolve();
479
657
  }
480
658
 
@@ -501,9 +679,19 @@ async function printThreadCollection(
501
679
  expiresAt: item.attributes.expires_at ?? "",
502
680
  })),
503
681
  );
682
+ const pagination = paginationInfo(args, response.nextCursor);
683
+ const message = renderPagination(
684
+ pagination?.nextCursor,
685
+ pagination?.nextCommand,
686
+ );
687
+
688
+ if (message) {
689
+ io.stdout(message);
690
+ }
504
691
  }
505
692
 
506
693
  function renderThreadWithMessages(
694
+ args: ParsedArgs,
507
695
  thread: { id: string; attributes: ThreadAttributes },
508
696
  messages: {
509
697
  items: Array<{ id: string; attributes: MessageAttributes }>;
@@ -518,6 +706,7 @@ function renderThreadWithMessages(
518
706
  : messages.items
519
707
  .map((message) => renderMessage(message, publicUsers))
520
708
  .join("\n\n");
709
+ const pagination = paginationInfo(args, messages.nextCursor);
521
710
 
522
711
  return joinBlocks([
523
712
  `Thread ${thread.id}`,
@@ -579,7 +768,7 @@ function renderThreadWithMessages(
579
768
  ]),
580
769
  ),
581
770
  renderSection("Messages", messageBlocks),
582
- renderPagination(messages.nextCursor),
771
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
583
772
  ]);
584
773
  }
585
774
 
@@ -601,11 +790,70 @@ function renderMessage(
601
790
  const contextPost = attrs.context_post_id
602
791
  ? `Context post: ${attrs.context_post_id}\n`
603
792
  : "";
793
+ const payload = attrs.payload
794
+ ? `\n\nPayload\n${indent(JSON.stringify(attrs.payload, null, 2))}`
795
+ : "";
604
796
 
605
- return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}`;
797
+ return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}${payload}`;
798
+ }
799
+
800
+ function printSchemaResource(
801
+ context: CommandContext,
802
+ io: Io,
803
+ resource: {
804
+ id: string;
805
+ attributes: {
806
+ public_handle?: string | null;
807
+ name?: string | null;
808
+ inbox_schema?: Record<string, unknown> | null;
809
+ inbox_schema_hash?: string | null;
810
+ inbox_schema_updated_at?: string | null;
811
+ };
812
+ },
813
+ action?: string,
814
+ ): void {
815
+ printValue(
816
+ io,
817
+ context.outputMode,
818
+ context.outputMode === "json"
819
+ ? resource
820
+ : renderSchemaResource(resource, action),
821
+ );
822
+ }
823
+
824
+ function renderSchemaResource(
825
+ resource: {
826
+ id: string;
827
+ attributes: {
828
+ public_handle?: string | null;
829
+ name?: string | null;
830
+ inbox_schema?: Record<string, unknown> | null;
831
+ inbox_schema_hash?: string | null;
832
+ inbox_schema_updated_at?: string | null;
833
+ };
834
+ },
835
+ action?: string,
836
+ ): string {
837
+ const attrs = resource.attributes;
838
+ const schema = attrs.inbox_schema
839
+ ? JSON.stringify(attrs.inbox_schema, null, 2)
840
+ : "No inbox schema.";
841
+
842
+ return joinBlocks([
843
+ action,
844
+ `Inbox schema ${resource.id}`,
845
+ renderFields([
846
+ ["Handle", attrs.public_handle],
847
+ ["Channel", attrs.name],
848
+ ["Hash", attrs.inbox_schema_hash],
849
+ ["Updated", formatTimestamp(attrs.inbox_schema_updated_at)],
850
+ ]),
851
+ renderSection("Schema", indent(schema)),
852
+ ]);
606
853
  }
607
854
 
608
855
  function printEmailIntakeCollection(
856
+ args: ParsedArgs,
609
857
  context: CommandContext,
610
858
  io: Io,
611
859
  response: {
@@ -617,11 +865,11 @@ function printEmailIntakeCollection(
617
865
  io,
618
866
  context.outputMode,
619
867
  context.outputMode === "json"
620
- ? {
868
+ ? paginatedJson(args, {
621
869
  items: response.items,
622
870
  nextCursor: response.nextCursor,
623
- }
624
- : renderEmailIntakeCollection(response),
871
+ })
872
+ : renderEmailIntakeCollection(args, response),
625
873
  );
626
874
  }
627
875
 
@@ -640,16 +888,23 @@ function printEmailIntakeAction(
640
888
  );
641
889
  }
642
890
 
643
- function renderEmailIntakeCollection(response: {
644
- items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
645
- nextCursor?: string;
646
- }): string {
891
+ function renderEmailIntakeCollection(
892
+ args: ParsedArgs,
893
+ response: {
894
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
895
+ nextCursor?: string;
896
+ },
897
+ ): string {
647
898
  const body =
648
899
  response.items.length === 0
649
900
  ? "No email intakes."
650
901
  : response.items.map((intake) => renderEmailIntake(intake)).join("\n\n");
651
902
 
652
- return joinBlocks([body, renderPagination(response.nextCursor)]);
903
+ const pagination = paginationInfo(args, response.nextCursor);
904
+ return joinBlocks([
905
+ body,
906
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
907
+ ]);
653
908
  }
654
909
 
655
910
  function renderEmailIntake(intake: {
@@ -678,10 +933,13 @@ function renderEmailIntake(intake: {
678
933
  ]);
679
934
  }
680
935
 
681
- function renderAttachmentCollection(response: {
682
- items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
683
- nextCursor?: string;
684
- }): string {
936
+ function renderAttachmentCollection(
937
+ args: ParsedArgs,
938
+ response: {
939
+ items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
940
+ nextCursor?: string;
941
+ },
942
+ ): string {
685
943
  const body =
686
944
  response.items.length === 0
687
945
  ? "No attachments."
@@ -701,7 +959,11 @@ function renderAttachmentCollection(response: {
701
959
  })
702
960
  .join("\n\n");
703
961
 
704
- return joinBlocks([body, renderPagination(response.nextCursor)]);
962
+ const pagination = paginationInfo(args, response.nextCursor);
963
+ return joinBlocks([
964
+ body,
965
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
966
+ ]);
705
967
  }
706
968
 
707
969
  function renderThreadAction(
@@ -10,12 +10,14 @@ import { resolveBodyInput } from "../lib/body-input";
10
10
  import { createCommandContext } from "../lib/context";
11
11
  import { CliError } from "../lib/errors";
12
12
  import {
13
- formatTimestamp,
14
- joinBlocks,
15
13
  renderBodyBlock,
16
14
  renderFields,
15
+ formatTimestamp,
16
+ joinBlocks,
17
+ renderPagination,
17
18
  } from "../lib/human";
18
19
  import { printJson, printValue, type Io } from "../lib/output";
20
+ import { paginatedJson, paginationInfo } from "../lib/pagination";
19
21
  import type { PostAttributes, ShareTokenResponse } from "../types/api";
20
22
 
21
23
  export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
@@ -56,7 +58,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
56
58
  cursor: stringFlag(args.flags, "cursor"),
57
59
  });
58
60
 
59
- printPostCollection(context.outputMode, io, response);
61
+ printPostCollection(args, context.outputMode, io, response);
60
62
  return;
61
63
  }
62
64
 
@@ -76,7 +78,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
76
78
  cursor: stringFlag(args.flags, "cursor"),
77
79
  });
78
80
 
79
- printPostCollection(context.outputMode, io, response);
81
+ printPostCollection(args, context.outputMode, io, response);
80
82
  return;
81
83
  }
82
84
 
@@ -87,7 +89,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
87
89
  cursor: stringFlag(args.flags, "cursor"),
88
90
  });
89
91
 
90
- printPostCollection(context.outputMode, io, response);
92
+ printPostCollection(args, context.outputMode, io, response);
91
93
  return;
92
94
  }
93
95
 
@@ -218,6 +220,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
218
220
  }
219
221
 
220
222
  function printPostCollection(
223
+ args: ParsedArgs,
221
224
  outputMode: "json" | "table",
222
225
  io: Io,
223
226
  response: {
@@ -226,10 +229,13 @@ function printPostCollection(
226
229
  },
227
230
  ): void {
228
231
  if (outputMode === "json") {
229
- printJson(io, {
230
- items: response.items,
231
- nextCursor: response.nextCursor,
232
- });
232
+ printJson(
233
+ io,
234
+ paginatedJson(args, {
235
+ items: response.items,
236
+ nextCursor: response.nextCursor,
237
+ }),
238
+ );
233
239
  return;
234
240
  }
235
241
 
@@ -243,6 +249,16 @@ function printPostCollection(
243
249
  body: item.attributes.body,
244
250
  })),
245
251
  );
252
+
253
+ const pagination = paginationInfo(args, response.nextCursor);
254
+ const message = renderPagination(
255
+ pagination?.nextCursor,
256
+ pagination?.nextCommand,
257
+ );
258
+
259
+ if (message) {
260
+ io.stdout(message);
261
+ }
246
262
  }
247
263
 
248
264
  function renderPostDetail(
package/src/lib/args.ts CHANGED
@@ -35,6 +35,16 @@ const CLI_OPTIONS = {
35
35
  bodyFile: { type: "string" },
36
36
  "body-file": { type: "string" },
37
37
  stdin: { type: "boolean" },
38
+ payload: { type: "string" },
39
+ payloadFile: { type: "string" },
40
+ "payload-file": { type: "string" },
41
+ payloadStdin: { type: "boolean" },
42
+ "payload-stdin": { type: "boolean" },
43
+ schema: { type: "string" },
44
+ schemaFile: { type: "string" },
45
+ "schema-file": { type: "string" },
46
+ schemaStdin: { type: "boolean" },
47
+ "schema-stdin": { type: "boolean" },
38
48
  limit: { type: "string" },
39
49
  cursor: { type: "string" },
40
50
  status: { type: "string" },