@clankmates/cli 0.6.0 → 0.6.2
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 +14 -0
- package/package.json +1 -1
- 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 +296 -43
- package/src/commands/post.ts +53 -28
- package/src/lib/client.ts +17 -0
- package/src/lib/human.ts +134 -0
- package/src/types/api.ts +9 -1
package/README.md
CHANGED
|
@@ -24,6 +24,20 @@ clankm auth --help
|
|
|
24
24
|
clankm help channel token
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
If you install through mise with `npm:@clankmates/cli = "latest"` and a new
|
|
28
|
+
release does not appear after `mise upgrade`, refresh mise's remote-version
|
|
29
|
+
cache for that invocation:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You can also pin an exact release:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
mise install npm:@clankmates/cli@0.6.2
|
|
39
|
+
```
|
|
40
|
+
|
|
27
41
|
For local development in this repository:
|
|
28
42
|
|
|
29
43
|
```bash
|
package/package.json
CHANGED
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
|
+
}
|
package/src/commands/inbox.ts
CHANGED
|
@@ -7,6 +7,15 @@ import {
|
|
|
7
7
|
import { resolveBodyInput } from "../lib/body-input";
|
|
8
8
|
import { createCommandContext, type CommandContext } from "../lib/context";
|
|
9
9
|
import { CliError } from "../lib/errors";
|
|
10
|
+
import {
|
|
11
|
+
formatTimestamp,
|
|
12
|
+
indent,
|
|
13
|
+
joinBlocks,
|
|
14
|
+
renderFields,
|
|
15
|
+
renderPagination,
|
|
16
|
+
renderSection,
|
|
17
|
+
shortId,
|
|
18
|
+
} from "../lib/human";
|
|
10
19
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
11
20
|
import type {
|
|
12
21
|
InboxRecipient,
|
|
@@ -15,6 +24,7 @@ import type {
|
|
|
15
24
|
MessageAttributes,
|
|
16
25
|
ThreadAttributes,
|
|
17
26
|
ThreadStatusFilter,
|
|
27
|
+
WhoamiActor,
|
|
18
28
|
} from "../types/api";
|
|
19
29
|
|
|
20
30
|
export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
@@ -23,15 +33,16 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
23
33
|
|
|
24
34
|
switch (subcommand) {
|
|
25
35
|
case "list": {
|
|
36
|
+
const channelToken = stringFlag(args.flags, "channelToken");
|
|
26
37
|
const response = await context.client.listInboxThreads({
|
|
27
38
|
status: parseStatusFilter(stringFlag(args.flags, "status")),
|
|
28
39
|
mailbox: parseMailboxFilter(stringFlag(args.flags, "mailbox")),
|
|
29
40
|
limit: integerFlag(args.flags, "limit", { label: "--limit" }),
|
|
30
41
|
cursor: stringFlag(args.flags, "cursor"),
|
|
31
|
-
channelToken
|
|
42
|
+
channelToken,
|
|
32
43
|
});
|
|
33
44
|
|
|
34
|
-
printThreadCollection(context, io, response);
|
|
45
|
+
await printThreadCollection(context, io, response, channelToken);
|
|
35
46
|
return;
|
|
36
47
|
}
|
|
37
48
|
|
|
@@ -45,6 +56,13 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
45
56
|
cursor: stringFlag(args.flags, "cursor"),
|
|
46
57
|
channelToken,
|
|
47
58
|
});
|
|
59
|
+
const ownerIds = ownerIdsForThreadDisplay(thread, messages.items);
|
|
60
|
+
const publicUsers =
|
|
61
|
+
context.outputMode === "json" || ownerIds.length === 0
|
|
62
|
+
? new Map<string, string>()
|
|
63
|
+
: publicUserHandlesById(
|
|
64
|
+
(await context.client.listPublicUsersById(ownerIds)).items,
|
|
65
|
+
);
|
|
48
66
|
|
|
49
67
|
printValue(
|
|
50
68
|
io,
|
|
@@ -55,7 +73,7 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
55
73
|
messages: messages.items,
|
|
56
74
|
nextCursor: messages.nextCursor,
|
|
57
75
|
}
|
|
58
|
-
:
|
|
76
|
+
: renderThreadWithMessages(thread, messages, publicUsers),
|
|
59
77
|
);
|
|
60
78
|
return;
|
|
61
79
|
}
|
|
@@ -77,7 +95,9 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
77
95
|
printValue(
|
|
78
96
|
io,
|
|
79
97
|
context.outputMode,
|
|
80
|
-
context.outputMode === "json"
|
|
98
|
+
context.outputMode === "json"
|
|
99
|
+
? thread
|
|
100
|
+
: renderThreadAction("Created thread", thread),
|
|
81
101
|
);
|
|
82
102
|
return;
|
|
83
103
|
}
|
|
@@ -99,7 +119,9 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
99
119
|
printValue(
|
|
100
120
|
io,
|
|
101
121
|
context.outputMode,
|
|
102
|
-
context.outputMode === "json"
|
|
122
|
+
context.outputMode === "json"
|
|
123
|
+
? thread
|
|
124
|
+
: renderThreadAction("Replied to thread", thread),
|
|
103
125
|
);
|
|
104
126
|
return;
|
|
105
127
|
}
|
|
@@ -114,7 +136,9 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
114
136
|
printValue(
|
|
115
137
|
io,
|
|
116
138
|
context.outputMode,
|
|
117
|
-
context.outputMode === "json"
|
|
139
|
+
context.outputMode === "json"
|
|
140
|
+
? thread
|
|
141
|
+
: renderThreadAction("Marked thread as seen", thread),
|
|
118
142
|
);
|
|
119
143
|
return;
|
|
120
144
|
}
|
|
@@ -129,7 +153,9 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
129
153
|
printValue(
|
|
130
154
|
io,
|
|
131
155
|
context.outputMode,
|
|
132
|
-
context.outputMode === "json"
|
|
156
|
+
context.outputMode === "json"
|
|
157
|
+
? thread
|
|
158
|
+
: renderThreadAction("Archived thread", thread),
|
|
133
159
|
);
|
|
134
160
|
return;
|
|
135
161
|
}
|
|
@@ -144,7 +170,9 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
144
170
|
printValue(
|
|
145
171
|
io,
|
|
146
172
|
context.outputMode,
|
|
147
|
-
context.outputMode === "json"
|
|
173
|
+
context.outputMode === "json"
|
|
174
|
+
? thread
|
|
175
|
+
: renderThreadAction("Resolved thread", thread),
|
|
148
176
|
);
|
|
149
177
|
return;
|
|
150
178
|
}
|
|
@@ -159,7 +187,9 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
159
187
|
printValue(
|
|
160
188
|
io,
|
|
161
189
|
context.outputMode,
|
|
162
|
-
context.outputMode === "json"
|
|
190
|
+
context.outputMode === "json"
|
|
191
|
+
? thread
|
|
192
|
+
: renderThreadAction("Blocked thread", thread),
|
|
163
193
|
);
|
|
164
194
|
return;
|
|
165
195
|
}
|
|
@@ -336,27 +366,39 @@ function parseHandleChannel(
|
|
|
336
366
|
return { ownerHandle, channelName };
|
|
337
367
|
}
|
|
338
368
|
|
|
339
|
-
function printThreadCollection(
|
|
369
|
+
async function printThreadCollection(
|
|
340
370
|
context: CommandContext,
|
|
341
371
|
io: Io,
|
|
342
372
|
response: {
|
|
343
373
|
items: Array<{ id: string; attributes: ThreadAttributes }>;
|
|
344
374
|
nextCursor?: string;
|
|
345
375
|
},
|
|
346
|
-
|
|
376
|
+
channelToken?: string,
|
|
377
|
+
): Promise<void> {
|
|
347
378
|
if (context.outputMode === "json") {
|
|
348
379
|
printJson(io, {
|
|
349
380
|
items: response.items,
|
|
350
381
|
nextCursor: response.nextCursor,
|
|
351
382
|
});
|
|
352
|
-
return;
|
|
383
|
+
return Promise.resolve();
|
|
353
384
|
}
|
|
354
385
|
|
|
386
|
+
const actor = (await context.client.whoami(channelToken)).actor;
|
|
387
|
+
const peers = response.items.map((item) => threadPeer(item.attributes, actor));
|
|
388
|
+
const ownerIds = ownerIdsForThreadList(peers);
|
|
389
|
+
const publicUsers =
|
|
390
|
+
ownerIds.length === 0
|
|
391
|
+
? new Map<string, string>()
|
|
392
|
+
: publicUserHandlesById(
|
|
393
|
+
(await context.client.listPublicUsersById(ownerIds)).items,
|
|
394
|
+
);
|
|
395
|
+
|
|
355
396
|
printValue(
|
|
356
397
|
io,
|
|
357
398
|
context.outputMode,
|
|
358
|
-
response.items.map((item) => ({
|
|
399
|
+
response.items.map((item, index) => ({
|
|
359
400
|
id: item.id,
|
|
401
|
+
with: formatThreadPeer(peers[index], publicUsers),
|
|
360
402
|
mailboxType: item.attributes.mailbox_type,
|
|
361
403
|
status: item.attributes.status,
|
|
362
404
|
lastMessageAt: item.attributes.last_message_at ?? "",
|
|
@@ -366,43 +408,254 @@ function printThreadCollection(
|
|
|
366
408
|
);
|
|
367
409
|
}
|
|
368
410
|
|
|
369
|
-
function
|
|
411
|
+
function renderThreadWithMessages(
|
|
370
412
|
thread: { id: string; attributes: ThreadAttributes },
|
|
371
413
|
messages: {
|
|
372
414
|
items: Array<{ id: string; attributes: MessageAttributes }>;
|
|
373
415
|
nextCursor?: string;
|
|
374
416
|
},
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
417
|
+
publicUsers: Map<string, string>,
|
|
418
|
+
): string {
|
|
419
|
+
const attrs = thread.attributes;
|
|
420
|
+
const messageBlocks =
|
|
421
|
+
messages.items.length === 0
|
|
422
|
+
? "No messages."
|
|
423
|
+
: messages.items
|
|
424
|
+
.map((message) => renderMessage(message, publicUsers))
|
|
425
|
+
.join("\n\n");
|
|
426
|
+
|
|
427
|
+
return joinBlocks([
|
|
428
|
+
`Thread ${thread.id}`,
|
|
429
|
+
renderFields([
|
|
430
|
+
["Mailbox", attrs.mailbox_type],
|
|
431
|
+
["Status", attrs.status],
|
|
432
|
+
["Last message", formatTimestamp(attrs.last_message_at)],
|
|
433
|
+
["Opened", formatTimestamp(attrs.opened_at)],
|
|
434
|
+
[
|
|
435
|
+
"Expires",
|
|
436
|
+
attrs.expires_at ? formatTimestamp(attrs.expires_at) : undefined,
|
|
437
|
+
],
|
|
438
|
+
]),
|
|
439
|
+
renderSection(
|
|
440
|
+
"Participants",
|
|
441
|
+
renderFields([
|
|
442
|
+
["A owner", formatUserActor(attrs.participant_a_owner_id, publicUsers)],
|
|
443
|
+
["A channel", formatActor("channel", attrs.participant_a_channel_id)],
|
|
444
|
+
["A seen", formatTimestamp(attrs.participant_a_seen_at)],
|
|
445
|
+
[
|
|
446
|
+
"A archived",
|
|
447
|
+
attrs.participant_a_archived_at
|
|
448
|
+
? formatTimestamp(attrs.participant_a_archived_at)
|
|
449
|
+
: undefined,
|
|
450
|
+
],
|
|
451
|
+
[
|
|
452
|
+
"A blocked",
|
|
453
|
+
attrs.participant_a_blocked_at
|
|
454
|
+
? formatTimestamp(attrs.participant_a_blocked_at)
|
|
455
|
+
: undefined,
|
|
456
|
+
],
|
|
457
|
+
[
|
|
458
|
+
"A resolved",
|
|
459
|
+
attrs.participant_a_resolved_at
|
|
460
|
+
? formatTimestamp(attrs.participant_a_resolved_at)
|
|
461
|
+
: undefined,
|
|
462
|
+
],
|
|
463
|
+
["B owner", formatUserActor(attrs.participant_b_owner_id, publicUsers)],
|
|
464
|
+
["B channel", formatActor("channel", attrs.participant_b_channel_id)],
|
|
465
|
+
["B seen", formatTimestamp(attrs.participant_b_seen_at)],
|
|
466
|
+
[
|
|
467
|
+
"B archived",
|
|
468
|
+
attrs.participant_b_archived_at
|
|
469
|
+
? formatTimestamp(attrs.participant_b_archived_at)
|
|
470
|
+
: undefined,
|
|
471
|
+
],
|
|
472
|
+
[
|
|
473
|
+
"B blocked",
|
|
474
|
+
attrs.participant_b_blocked_at
|
|
475
|
+
? formatTimestamp(attrs.participant_b_blocked_at)
|
|
476
|
+
: undefined,
|
|
477
|
+
],
|
|
478
|
+
[
|
|
479
|
+
"B resolved",
|
|
480
|
+
attrs.participant_b_resolved_at
|
|
481
|
+
? formatTimestamp(attrs.participant_b_resolved_at)
|
|
482
|
+
: undefined,
|
|
483
|
+
],
|
|
484
|
+
]),
|
|
485
|
+
),
|
|
486
|
+
renderSection("Messages", messageBlocks),
|
|
487
|
+
renderPagination(messages.nextCursor),
|
|
488
|
+
]);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function renderMessage(
|
|
492
|
+
message: {
|
|
493
|
+
id: string;
|
|
494
|
+
attributes: MessageAttributes;
|
|
495
|
+
},
|
|
496
|
+
publicUsers: Map<string, string>,
|
|
497
|
+
): string {
|
|
498
|
+
const attrs = message.attributes;
|
|
499
|
+
const headingParts = [
|
|
500
|
+
formatTimestamp(attrs.inserted_at),
|
|
501
|
+
shortId(message.id),
|
|
502
|
+
formatUserActor(attrs.sender_owner_id, publicUsers),
|
|
503
|
+
formatActor("channel", attrs.sender_channel_id),
|
|
504
|
+
].filter((part) => part !== "-");
|
|
505
|
+
const contextPost = attrs.context_post_id
|
|
506
|
+
? `Context post: ${attrs.context_post_id}\n`
|
|
507
|
+
: "";
|
|
508
|
+
|
|
509
|
+
return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function renderThreadAction(
|
|
513
|
+
action: string,
|
|
514
|
+
thread: { id: string; attributes: ThreadAttributes },
|
|
515
|
+
): string {
|
|
516
|
+
const attrs = thread.attributes;
|
|
517
|
+
|
|
518
|
+
return joinBlocks([
|
|
519
|
+
`${action}: ${thread.id}`,
|
|
520
|
+
renderFields([
|
|
521
|
+
["Mailbox", attrs.mailbox_type],
|
|
522
|
+
["Status", attrs.status],
|
|
523
|
+
["Last message", formatTimestamp(attrs.last_message_at)],
|
|
524
|
+
["Opened", formatTimestamp(attrs.opened_at)],
|
|
525
|
+
[
|
|
526
|
+
"Expires",
|
|
527
|
+
attrs.expires_at ? formatTimestamp(attrs.expires_at) : undefined,
|
|
528
|
+
],
|
|
529
|
+
]),
|
|
530
|
+
]);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function formatActor(kind: "user" | "channel", id?: string | null): string {
|
|
534
|
+
if (!id) {
|
|
535
|
+
return "-";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return `${kind}:${shortId(id)}`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function formatUserActor(
|
|
542
|
+
id: string | null | undefined,
|
|
543
|
+
publicUsers: Map<string, string>,
|
|
544
|
+
): string {
|
|
545
|
+
if (!id) {
|
|
546
|
+
return "-";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return publicUsers.get(id) ?? formatActor("user", id);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function ownerIdsForThreadDisplay(
|
|
553
|
+
thread: { attributes: ThreadAttributes },
|
|
554
|
+
messages: Array<{ attributes: MessageAttributes }>,
|
|
555
|
+
): string[] {
|
|
556
|
+
const ids = [
|
|
557
|
+
thread.attributes.participant_a_owner_id,
|
|
558
|
+
thread.attributes.participant_b_owner_id,
|
|
559
|
+
...messages.map((message) => message.attributes.sender_owner_id),
|
|
560
|
+
];
|
|
561
|
+
|
|
562
|
+
return Array.from(new Set(ids.filter((id): id is string => Boolean(id))));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function ownerIdsForThreadList(
|
|
566
|
+
peers: Array<ThreadPeer>,
|
|
567
|
+
): string[] {
|
|
568
|
+
return Array.from(
|
|
569
|
+
new Set(
|
|
570
|
+
peers
|
|
571
|
+
.map((peer) => peer.ownerId)
|
|
572
|
+
.filter((id): id is string => Boolean(id)),
|
|
573
|
+
),
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
interface ThreadPeer {
|
|
578
|
+
ownerId?: string | null;
|
|
579
|
+
channelId?: string | null;
|
|
386
580
|
}
|
|
387
581
|
|
|
388
|
-
function
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
582
|
+
function threadPeer(attrs: ThreadAttributes, actor: WhoamiActor): ThreadPeer {
|
|
583
|
+
const side = threadActorSide(attrs, actor);
|
|
584
|
+
|
|
585
|
+
if (side === "a") {
|
|
586
|
+
return {
|
|
587
|
+
ownerId: attrs.participant_b_owner_id,
|
|
588
|
+
channelId: attrs.participant_b_channel_id,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (side === "b") {
|
|
593
|
+
return {
|
|
594
|
+
ownerId: attrs.participant_a_owner_id,
|
|
595
|
+
channelId: attrs.participant_a_channel_id,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
392
599
|
return {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
status: thread.attributes.status,
|
|
396
|
-
lastMessageAt: thread.attributes.last_message_at ?? "",
|
|
397
|
-
openedAt: thread.attributes.opened_at ?? "",
|
|
398
|
-
expiresAt: thread.attributes.expires_at ?? "",
|
|
399
|
-
participantASeenAt: thread.attributes.participant_a_seen_at ?? "",
|
|
400
|
-
participantAArchivedAt: thread.attributes.participant_a_archived_at ?? "",
|
|
401
|
-
participantABlockedAt: thread.attributes.participant_a_blocked_at ?? "",
|
|
402
|
-
participantAResolvedAt: thread.attributes.participant_a_resolved_at ?? "",
|
|
403
|
-
participantBSeenAt: thread.attributes.participant_b_seen_at ?? "",
|
|
404
|
-
participantBArchivedAt: thread.attributes.participant_b_archived_at ?? "",
|
|
405
|
-
participantBBlockedAt: thread.attributes.participant_b_blocked_at ?? "",
|
|
406
|
-
participantBResolvedAt: thread.attributes.participant_b_resolved_at ?? "",
|
|
600
|
+
ownerId: attrs.participant_a_owner_id,
|
|
601
|
+
channelId: attrs.participant_a_channel_id,
|
|
407
602
|
};
|
|
408
603
|
}
|
|
604
|
+
|
|
605
|
+
function threadActorSide(
|
|
606
|
+
attrs: ThreadAttributes,
|
|
607
|
+
actor: WhoamiActor,
|
|
608
|
+
): "a" | "b" | undefined {
|
|
609
|
+
if (actor.type === "channel") {
|
|
610
|
+
if (attrs.participant_a_channel_id === actor.id) {
|
|
611
|
+
return "a";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (attrs.participant_b_channel_id === actor.id) {
|
|
615
|
+
return "b";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const ownerId = actor.type === "user" ? actor.id : actor.owner_id;
|
|
620
|
+
|
|
621
|
+
if (attrs.participant_a_owner_id === ownerId) {
|
|
622
|
+
return "a";
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (attrs.participant_b_owner_id === ownerId) {
|
|
626
|
+
return "b";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function formatThreadPeer(
|
|
633
|
+
peer: ThreadPeer | undefined,
|
|
634
|
+
publicUsers: Map<string, string>,
|
|
635
|
+
): string {
|
|
636
|
+
if (!peer) {
|
|
637
|
+
return "-";
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (peer.channelId) {
|
|
641
|
+
return formatActor("channel", peer.channelId);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return formatUserActor(peer.ownerId, publicUsers);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function publicUserHandlesById(
|
|
648
|
+
users: Array<{ id: string; attributes: { public_handle?: string | null } }>,
|
|
649
|
+
): Map<string, string> {
|
|
650
|
+
const handles = new Map<string, string>();
|
|
651
|
+
|
|
652
|
+
for (const user of users) {
|
|
653
|
+
const handle = user.attributes.public_handle?.trim();
|
|
654
|
+
|
|
655
|
+
if (handle) {
|
|
656
|
+
handles.set(user.id, `@${handle}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return handles;
|
|
661
|
+
}
|
package/src/commands/post.ts
CHANGED
|
@@ -9,8 +9,14 @@ import {
|
|
|
9
9
|
import { resolveBodyInput } from "../lib/body-input";
|
|
10
10
|
import { createCommandContext } from "../lib/context";
|
|
11
11
|
import { CliError } from "../lib/errors";
|
|
12
|
+
import {
|
|
13
|
+
formatTimestamp,
|
|
14
|
+
joinBlocks,
|
|
15
|
+
renderBodyBlock,
|
|
16
|
+
renderFields,
|
|
17
|
+
} from "../lib/human";
|
|
12
18
|
import { printJson, printValue, type Io } from "../lib/output";
|
|
13
|
-
import type { PostAttributes } from "../types/api";
|
|
19
|
+
import type { PostAttributes, ShareTokenResponse } from "../types/api";
|
|
14
20
|
|
|
15
21
|
export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
16
22
|
const subcommand = args.positionals[0];
|
|
@@ -36,12 +42,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
36
42
|
context.outputMode,
|
|
37
43
|
context.outputMode === "json"
|
|
38
44
|
? post
|
|
39
|
-
: {
|
|
40
|
-
id: post.id,
|
|
41
|
-
channelId,
|
|
42
|
-
source: post.attributes.source,
|
|
43
|
-
body: post.attributes.body,
|
|
44
|
-
},
|
|
45
|
+
: renderPostDetail(post, { channelId, title: "Published post" }),
|
|
45
46
|
);
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
@@ -105,11 +106,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
105
106
|
context.outputMode,
|
|
106
107
|
context.outputMode === "json"
|
|
107
108
|
? post
|
|
108
|
-
: {
|
|
109
|
-
id: post.id,
|
|
110
|
-
source: post.attributes.source,
|
|
111
|
-
body: post.attributes.body,
|
|
112
|
-
},
|
|
109
|
+
: renderPostDetail(post, { title: "Updated post" }),
|
|
113
110
|
);
|
|
114
111
|
return;
|
|
115
112
|
}
|
|
@@ -141,11 +138,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
141
138
|
context.outputMode,
|
|
142
139
|
context.outputMode === "json"
|
|
143
140
|
? post
|
|
144
|
-
:
|
|
145
|
-
id: post.id,
|
|
146
|
-
source: post.attributes.source,
|
|
147
|
-
body: post.attributes.body,
|
|
148
|
-
},
|
|
141
|
+
: renderPostDetail(post),
|
|
149
142
|
);
|
|
150
143
|
return;
|
|
151
144
|
}
|
|
@@ -170,11 +163,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
170
163
|
context.outputMode,
|
|
171
164
|
context.outputMode === "json"
|
|
172
165
|
? post
|
|
173
|
-
:
|
|
174
|
-
id: post.id,
|
|
175
|
-
source: post.attributes.source,
|
|
176
|
-
body: post.attributes.body,
|
|
177
|
-
},
|
|
166
|
+
: renderPostDetail(post),
|
|
178
167
|
);
|
|
179
168
|
return;
|
|
180
169
|
}
|
|
@@ -189,11 +178,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
189
178
|
context.outputMode,
|
|
190
179
|
context.outputMode === "json"
|
|
191
180
|
? post
|
|
192
|
-
:
|
|
193
|
-
id: post.id,
|
|
194
|
-
source: post.attributes.source,
|
|
195
|
-
body: post.attributes.body,
|
|
196
|
-
},
|
|
181
|
+
: renderPostDetail(post),
|
|
197
182
|
);
|
|
198
183
|
return;
|
|
199
184
|
}
|
|
@@ -208,7 +193,13 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
208
193
|
return;
|
|
209
194
|
}
|
|
210
195
|
|
|
211
|
-
printValue(
|
|
196
|
+
printValue(
|
|
197
|
+
io,
|
|
198
|
+
context.outputMode,
|
|
199
|
+
context.outputMode === "json"
|
|
200
|
+
? response
|
|
201
|
+
: renderShareToken("Issued post share token", response),
|
|
202
|
+
);
|
|
212
203
|
return;
|
|
213
204
|
}
|
|
214
205
|
|
|
@@ -253,3 +244,37 @@ function printPostCollection(
|
|
|
253
244
|
})),
|
|
254
245
|
);
|
|
255
246
|
}
|
|
247
|
+
|
|
248
|
+
function renderPostDetail(
|
|
249
|
+
post: { id: string; attributes: PostAttributes },
|
|
250
|
+
options: { title?: string; channelId?: string } = {},
|
|
251
|
+
): string {
|
|
252
|
+
return joinBlocks([
|
|
253
|
+
options.title ?? `Post ${post.id}`,
|
|
254
|
+
renderFields([
|
|
255
|
+
["ID", post.id],
|
|
256
|
+
["Channel", options.channelId],
|
|
257
|
+
["Source", post.attributes.source],
|
|
258
|
+
[
|
|
259
|
+
"Inserted",
|
|
260
|
+
post.attributes.inserted_at
|
|
261
|
+
? formatTimestamp(post.attributes.inserted_at)
|
|
262
|
+
: undefined,
|
|
263
|
+
],
|
|
264
|
+
[
|
|
265
|
+
"Updated",
|
|
266
|
+
post.attributes.updated_at
|
|
267
|
+
? formatTimestamp(post.attributes.updated_at)
|
|
268
|
+
: undefined,
|
|
269
|
+
],
|
|
270
|
+
]),
|
|
271
|
+
"Body\n" + renderBodyBlock(post.attributes.body),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderShareToken(title: string, response: ShareTokenResponse): string {
|
|
276
|
+
return joinBlocks([
|
|
277
|
+
title,
|
|
278
|
+
renderFields([["Token", response.token]]),
|
|
279
|
+
]);
|
|
280
|
+
}
|
package/src/lib/client.ts
CHANGED
|
@@ -142,6 +142,12 @@ export class ClankmatesClient {
|
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
async listPublicUsersById(ids: string[]) {
|
|
146
|
+
const path = withRepeatedQuery(`${API_PREFIX}/public/users/by-id`, "ids[]", ids);
|
|
147
|
+
|
|
148
|
+
return this.requestCollection<UserAttributes>(path, {});
|
|
149
|
+
}
|
|
150
|
+
|
|
145
151
|
async listChannels(input: { limit?: number; cursor?: string } = {}) {
|
|
146
152
|
return this.requestCollection<ChannelAttributes>(
|
|
147
153
|
withQuery(`${API_PREFIX}/channels`, {
|
|
@@ -814,3 +820,14 @@ function withQuery(
|
|
|
814
820
|
const query = search.toString();
|
|
815
821
|
return query ? `${path}?${query}` : path;
|
|
816
822
|
}
|
|
823
|
+
|
|
824
|
+
function withRepeatedQuery(path: string, key: string, values: string[]): string {
|
|
825
|
+
const search = new URLSearchParams();
|
|
826
|
+
|
|
827
|
+
for (const value of values) {
|
|
828
|
+
search.append(key, value);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const query = search.toString();
|
|
832
|
+
return query ? `${path}?${query}` : path;
|
|
833
|
+
}
|
package/src/lib/human.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type FieldValue = string | number | boolean | null | undefined;
|
|
2
|
+
|
|
3
|
+
export function formatEmpty(value: FieldValue): string {
|
|
4
|
+
if (value === null || value === undefined || value === "") {
|
|
5
|
+
return "-";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatTimestamp(value?: string | null): string {
|
|
12
|
+
if (!value) {
|
|
13
|
+
return "-";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const timestamp = Date.parse(value);
|
|
17
|
+
|
|
18
|
+
if (Number.isNaN(timestamp)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return formatLocalTimestamp(new Date(timestamp));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function shortId(id?: string | null): string {
|
|
26
|
+
if (!id) {
|
|
27
|
+
return "-";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return id.length > 12 ? `${id.slice(0, 8)}...` : id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function renderFields(fields: Array<[label: string, value: FieldValue]>): string {
|
|
34
|
+
const visibleFields = fields.filter(([, value]) => value !== undefined);
|
|
35
|
+
|
|
36
|
+
if (visibleFields.length === 0) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const labelWidth = Math.max(...visibleFields.map(([label]) => label.length));
|
|
41
|
+
|
|
42
|
+
return visibleFields
|
|
43
|
+
.map(([label, value]) => `${label.padEnd(labelWidth)} ${formatEmpty(value)}`)
|
|
44
|
+
.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderSection(title: string, body: string): string {
|
|
48
|
+
const trimmed = body.trimEnd();
|
|
49
|
+
|
|
50
|
+
if (!trimmed) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${title}\n${trimmed}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderBullets(items: string[]): string {
|
|
58
|
+
if (items.length === 0) {
|
|
59
|
+
return "-";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function indent(text: string, spaces = 2): string {
|
|
66
|
+
const prefix = " ".repeat(spaces);
|
|
67
|
+
return text
|
|
68
|
+
.split("\n")
|
|
69
|
+
.map((line) => (line ? `${prefix}${line}` : ""))
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function renderBodyBlock(body: string): string {
|
|
74
|
+
const normalized = body.replace(/\r\n/g, "\n").trimEnd();
|
|
75
|
+
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
return indent("(empty)");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return indent(normalized);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderPagination(nextCursor?: string | null): string {
|
|
84
|
+
return nextCursor ? `More results: --cursor ${nextCursor}` : "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function joinBlocks(blocks: Array<string | undefined | null | false>): string {
|
|
88
|
+
return blocks.filter(Boolean).join("\n\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderTokenAction(input: {
|
|
92
|
+
title: string;
|
|
93
|
+
id?: string | null;
|
|
94
|
+
name?: string | null;
|
|
95
|
+
token?: string | null;
|
|
96
|
+
issuedAt?: string | null;
|
|
97
|
+
expiresAt?: string | null;
|
|
98
|
+
}): string {
|
|
99
|
+
return joinBlocks([
|
|
100
|
+
input.title,
|
|
101
|
+
renderFields([
|
|
102
|
+
["ID", input.id],
|
|
103
|
+
["Name", input.name],
|
|
104
|
+
["Token", input.token],
|
|
105
|
+
["Issued", input.issuedAt ? formatTimestamp(input.issuedAt) : undefined],
|
|
106
|
+
["Expires", input.expiresAt ? formatTimestamp(input.expiresAt) : undefined],
|
|
107
|
+
]),
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatLocalTimestamp(value: Date): string {
|
|
112
|
+
const year = value.getFullYear();
|
|
113
|
+
const month = padDatePart(value.getMonth() + 1);
|
|
114
|
+
const day = padDatePart(value.getDate());
|
|
115
|
+
const hours = padDatePart(value.getHours());
|
|
116
|
+
const minutes = padDatePart(value.getMinutes());
|
|
117
|
+
const seconds = padDatePart(value.getSeconds());
|
|
118
|
+
|
|
119
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${formatTimezoneOffset(value)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatTimezoneOffset(value: Date): string {
|
|
123
|
+
const offsetMinutes = -value.getTimezoneOffset();
|
|
124
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
125
|
+
const absoluteMinutes = Math.abs(offsetMinutes);
|
|
126
|
+
const hours = padDatePart(Math.floor(absoluteMinutes / 60));
|
|
127
|
+
const minutes = padDatePart(absoluteMinutes % 60);
|
|
128
|
+
|
|
129
|
+
return `${sign}${hours}:${minutes}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function padDatePart(value: number): string {
|
|
133
|
+
return String(value).padStart(2, "0");
|
|
134
|
+
}
|
package/src/types/api.ts
CHANGED
|
@@ -44,7 +44,8 @@ export interface JsonApiDocument<TAttributes extends object> {
|
|
|
44
44
|
export type AccessKeyScope = "master" | "read_only";
|
|
45
45
|
|
|
46
46
|
export interface UserAttributes {
|
|
47
|
-
email
|
|
47
|
+
email?: string;
|
|
48
|
+
public_profile_id?: string;
|
|
48
49
|
public_handle?: string | null;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -95,6 +96,10 @@ export interface InboxSender {
|
|
|
95
96
|
export interface ThreadAttributes {
|
|
96
97
|
mailbox_type: MailboxType;
|
|
97
98
|
status: ThreadStatus;
|
|
99
|
+
participant_a_owner_id?: string | null;
|
|
100
|
+
participant_a_channel_id?: string | null;
|
|
101
|
+
participant_b_owner_id?: string | null;
|
|
102
|
+
participant_b_channel_id?: string | null;
|
|
98
103
|
opened_at?: string | null;
|
|
99
104
|
expires_at?: string | null;
|
|
100
105
|
last_message_at?: string;
|
|
@@ -112,6 +117,9 @@ export interface ThreadAttributes {
|
|
|
112
117
|
|
|
113
118
|
export interface MessageAttributes {
|
|
114
119
|
body: string;
|
|
120
|
+
sender_owner_id?: string | null;
|
|
121
|
+
sender_channel_id?: string | null;
|
|
122
|
+
thread_id?: string | null;
|
|
115
123
|
context_post_id?: string | null;
|
|
116
124
|
inserted_at?: string;
|
|
117
125
|
}
|