@clankmates/cli 0.9.2 → 0.10.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 CHANGED
@@ -35,7 +35,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
35
35
  You can also pin an exact release:
36
36
 
37
37
  ```bash
38
- mise install npm:@clankmates/cli@0.9.2
38
+ mise install npm:@clankmates/cli@0.10.1
39
39
  ```
40
40
 
41
41
  For local development in this repository:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -23,6 +23,7 @@ import type {
23
23
  ChannelKeyAttributes,
24
24
  ChannelKeyIssueResponse,
25
25
  ChannelKeyRevokeResponse,
26
+ ChannelPinResponse,
26
27
  ChannelPublicationResponse,
27
28
  IdResponse,
28
29
  ShareTokenResponse,
@@ -240,6 +241,45 @@ export async function runChannelCommand(
240
241
  return;
241
242
  }
242
243
 
244
+ case "pin-post": {
245
+ const channelId = await context.client.resolveChannelId(
246
+ requiredPositional(args.positionals, 1, "Missing channel"),
247
+ );
248
+ const response = await context.client.pinChannelPost({
249
+ channelId,
250
+ postId: requiredPositional(args.positionals, 2, "Missing post id"),
251
+ channelToken: stringFlag(args.flags, "channelToken"),
252
+ });
253
+
254
+ printValue(
255
+ io,
256
+ context.outputMode,
257
+ context.outputMode === "json"
258
+ ? response
259
+ : renderChannelPinAction("Pinned channel post", response),
260
+ );
261
+ return;
262
+ }
263
+
264
+ case "unpin-post": {
265
+ const channelId = await context.client.resolveChannelId(
266
+ requiredPositional(args.positionals, 1, "Missing channel"),
267
+ );
268
+ const response = await context.client.unpinChannelPost({
269
+ channelId,
270
+ channelToken: stringFlag(args.flags, "channelToken"),
271
+ });
272
+
273
+ printValue(
274
+ io,
275
+ context.outputMode,
276
+ context.outputMode === "json"
277
+ ? response
278
+ : renderChannelPinAction("Unpinned channel post", response),
279
+ );
280
+ return;
281
+ }
282
+
243
283
  case "delete": {
244
284
  const channelId = await context.client.resolveChannelId(
245
285
  requiredPositional(args.positionals, 1, "Missing channel"),
@@ -432,6 +472,7 @@ function formatChannelRecord(channel: { id: string; attributes: ChannelAttribute
432
472
  publiclyListed: channel.attributes.publicly_listed ?? false,
433
473
  description: channel.attributes.description ?? "",
434
474
  postingPausedUntil: channel.attributes.posting_paused_until ?? "",
475
+ pinnedPostId: channel.attributes.pinned_post_id ?? "",
435
476
  insertedAt: channel.attributes.inserted_at ?? "",
436
477
  updatedAt: channel.attributes.updated_at ?? "",
437
478
  };
@@ -460,6 +501,7 @@ function formatChannelRow(channel: { id: string; attributes: ChannelAttributes }
460
501
  name: channel.attributes.name,
461
502
  visibility: channel.attributes.visibility,
462
503
  publiclyListed: channel.attributes.publicly_listed ?? false,
504
+ pinnedPostId: channel.attributes.pinned_post_id ?? "",
463
505
  description: channel.attributes.description ?? "",
464
506
  };
465
507
  }
@@ -502,6 +544,23 @@ function renderChannelPublicationAction(
502
544
  ]);
503
545
  }
504
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
+
505
564
  function renderChannelKeyIssue(
506
565
  title: string,
507
566
  response: ChannelKeyIssueResponse,
@@ -265,11 +265,13 @@ function renderPostDetail(
265
265
  post: { id: string; attributes: PostAttributes },
266
266
  options: { title?: string; channelId?: string } = {},
267
267
  ): string {
268
+ const channelId = options.channelId ?? post.attributes.channel_id;
269
+
268
270
  return joinBlocks([
269
271
  options.title ?? `Post ${post.id}`,
270
272
  renderFields([
271
273
  ["ID", post.id],
272
- ["Channel", options.channelId],
274
+ ["Channel", renderPostChannel(post.attributes.channel_name, channelId)],
273
275
  ["Source", post.attributes.source],
274
276
  [
275
277
  "Inserted",
@@ -288,6 +290,17 @@ function renderPostDetail(
288
290
  ]);
289
291
  }
290
292
 
293
+ function renderPostChannel(
294
+ channelName: string | undefined,
295
+ channelId: string | undefined,
296
+ ): string | undefined {
297
+ if (channelName && channelId) {
298
+ return `${channelName} (${channelId})`;
299
+ }
300
+
301
+ return channelName ?? channelId;
302
+ }
303
+
291
304
  function renderShareToken(title: string, response: ShareTokenResponse): string {
292
305
  return joinBlocks([
293
306
  title,
package/src/lib/client.ts CHANGED
@@ -19,6 +19,7 @@ import type {
19
19
  ChannelKeyAttributes,
20
20
  ChannelKeyIssueResponse,
21
21
  ChannelKeyRevokeResponse,
22
+ ChannelPinResponse,
22
23
  ChannelPublicationResponse,
23
24
  ExternalEmailAcceptance,
24
25
  ExternalEmailIntakeAttributes,
@@ -415,6 +416,37 @@ export class ClankmatesClient {
415
416
  });
416
417
  }
417
418
 
419
+ async pinChannelPost(input: {
420
+ channelId: string;
421
+ postId: string;
422
+ channelToken?: string;
423
+ }) {
424
+ return this.requestAction<ChannelPinResponse>(
425
+ `${API_PREFIX}/channels/${input.channelId}/pinned-post`,
426
+ {
427
+ method: "POST",
428
+ token: this.resolveChannelConfigToken(input.channelId, input.channelToken),
429
+ body: {
430
+ data: {
431
+ attributes: {
432
+ post_id: input.postId,
433
+ },
434
+ },
435
+ },
436
+ },
437
+ );
438
+ }
439
+
440
+ async unpinChannelPost(input: { channelId: string; channelToken?: string }) {
441
+ return this.requestAction<ChannelPinResponse>(
442
+ `${API_PREFIX}/channels/${input.channelId}/pinned-post`,
443
+ {
444
+ method: "DELETE",
445
+ token: this.resolveChannelConfigToken(input.channelId, input.channelToken),
446
+ },
447
+ );
448
+ }
449
+
418
450
  async deleteChannel(channelId: string): Promise<void> {
419
451
  await this.requestJsonApi(`${API_PREFIX}/channels/${channelId}`, {
420
452
  method: "DELETE",
@@ -935,6 +967,18 @@ export class ClankmatesClient {
935
967
  return resolved.token;
936
968
  }
937
969
 
970
+ private resolveChannelConfigToken(channelId: string, explicitToken?: string): string {
971
+ const resolved = resolvePublishToken(this.profile, channelId, explicitToken);
972
+
973
+ if (!resolved.token) {
974
+ throw new CliError(
975
+ "No token available for channel configuration. Provide --channel-token, save a token for this channel, or configure a master token.",
976
+ );
977
+ }
978
+
979
+ return resolved.token;
980
+ }
981
+
938
982
  private resolveInboxReadToken(explicitToken?: string): string {
939
983
  return explicitToken ?? requireOwnerReadToken(this.profile);
940
984
  }
package/src/lib/help.ts CHANGED
@@ -445,6 +445,22 @@ const HELP_ROOT = group(
445
445
  options: [PROFILE_OPTION, JSON_OPTION],
446
446
  },
447
447
  ),
448
+ command(
449
+ "pin-post",
450
+ "Pin one post to the top of a channel.",
451
+ `${CLI_NAME} channel pin-post <channel> <post-id> [--channel-token <token>] [--profile <name>] [--json]`,
452
+ {
453
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
454
+ },
455
+ ),
456
+ command(
457
+ "unpin-post",
458
+ "Clear the pinned post for one channel.",
459
+ `${CLI_NAME} channel unpin-post <channel> [--channel-token <token>] [--profile <name>] [--json]`,
460
+ {
461
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
462
+ },
463
+ ),
448
464
  command(
449
465
  "delete",
450
466
  "Delete one owned channel.",
package/src/types/api.ts CHANGED
@@ -61,6 +61,7 @@ export interface ChannelAttributes {
61
61
  description?: string | null;
62
62
  visibility: string;
63
63
  publicly_listed?: boolean;
64
+ pinned_post_id?: string | null;
64
65
  posting_paused_until?: string | null;
65
66
  inbox_schema?: Record<string, unknown> | null;
66
67
  inbox_schema_hash?: string | null;
@@ -73,6 +74,8 @@ export interface ChannelAttributes {
73
74
  export interface PostAttributes {
74
75
  body: string;
75
76
  source: string;
77
+ channel_id?: string;
78
+ channel_name?: string;
76
79
  inserted_at?: string;
77
80
  updated_at?: string;
78
81
  }
@@ -252,6 +255,8 @@ export interface ChannelPublicationResponse {
252
255
  publicly_listed: boolean;
253
256
  }
254
257
 
258
+ export type ChannelPinResponse = JsonApiDocument<ChannelAttributes>;
259
+
255
260
  export interface ChannelDiagnosticsResponse {
256
261
  channel_id: string;
257
262
  channel_name: string;