@bananasplitapp/cli 0.1.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.
@@ -0,0 +1,627 @@
1
+ import {
2
+ DEFAULT_LIST_LIMIT,
3
+ appendQuery,
4
+ asArray,
5
+ asRecord,
6
+ cleanUserSummary,
7
+ currencyCode,
8
+ display,
9
+ encodedDetailId,
10
+ enumValue,
11
+ formatCard,
12
+ humanAmount,
13
+ namedEntity,
14
+ numeric,
15
+ parseJsonBody,
16
+ parseOptions,
17
+ positiveInteger,
18
+ repeatedStrings,
19
+ requiredString,
20
+ requirePositionals,
21
+ wantsHelp,
22
+ yesNo,
23
+ } from "../shared";
24
+ import { CliFailure, type ParsedCommand, type Presenter } from "../types";
25
+
26
+ const HELP = `Usage: banana groups <command>
27
+
28
+ Commands:
29
+ list [--limit N] [--cursor CURSOR] [--archived] [--sort balance|lastActivity]
30
+ create JSON
31
+ create --name TEXT --currency-id ID [--description TEXT]
32
+ [--type vacation|roommates|couple|travel|party|other]
33
+ [--member USER_ID]...
34
+ get <group-id>
35
+ members <group-id>
36
+ activities <group-id> [--search QUERY] [--limit N] [--page N]
37
+ [--type all|expenses|payments|recurring_expenses]
38
+ [--sort date|amount] [--direction asc|desc]`;
39
+ const LIST_HELP = `Usage: banana groups list [options]
40
+
41
+ Options:
42
+ --limit N (default: ${DEFAULT_LIST_LIMIT})
43
+ --cursor CURSOR
44
+ --archived
45
+ --sort balance|lastActivity`;
46
+ const GET_HELP = "Usage: banana groups get <group-id>";
47
+ const MEMBERS_HELP = "Usage: banana groups members <group-id>";
48
+ const CREATE_HELP = `Usage: banana groups create JSON
49
+ or: banana groups create --name TEXT --currency-id ID [options]
50
+
51
+ Options:
52
+ --description TEXT
53
+ --type vacation|roommates|couple|travel|party|other
54
+ --member USER_ID Repeat to add multiple members`;
55
+ const ACTIVITIES_HELP = `Usage: banana groups activities <group-id> [options]
56
+
57
+ Options:
58
+ --search QUERY
59
+ --limit N
60
+ --page N
61
+ --type all|expenses|payments|recurring_expenses
62
+ --sort date|amount
63
+ --direction asc|desc`;
64
+
65
+ function parseGroupsList(args: string[]): ParsedCommand {
66
+ if (wantsHelp(args)) return { kind: "help", text: LIST_HELP };
67
+ const { positionals, values } = parseOptions(args, {
68
+ archived: { type: "boolean" },
69
+ cursor: { type: "string" },
70
+ limit: { type: "string" },
71
+ sort: { type: "string" },
72
+ });
73
+ requirePositionals(positionals, 0, LIST_HELP);
74
+
75
+ const query = new URLSearchParams();
76
+ appendQuery(
77
+ query,
78
+ "l",
79
+ positiveInteger(values.limit, "--limit") ?? String(DEFAULT_LIST_LIMIT),
80
+ );
81
+ appendQuery(query, "cursor", values.cursor as string | undefined);
82
+ appendQuery(query, "archived", values.archived as boolean | undefined);
83
+ appendQuery(
84
+ query,
85
+ "sort",
86
+ enumValue(values.sort, "--sort", ["balance", "lastActivity"] as const),
87
+ );
88
+ return {
89
+ kind: "request",
90
+ path: "/groups",
91
+ presentation: "group-list",
92
+ query,
93
+ };
94
+ }
95
+
96
+ function parseGroupsCreate(args: string[]): ParsedCommand {
97
+ if (wantsHelp(args)) return { kind: "help", text: CREATE_HELP };
98
+ const { positionals, values } = parseOptions(args, {
99
+ "currency-id": { type: "string" },
100
+ description: { type: "string" },
101
+ member: { type: "string", multiple: true },
102
+ name: { type: "string" },
103
+ type: { type: "string" },
104
+ });
105
+ const jsonBody = parseJsonBody(positionals, values, CREATE_HELP);
106
+ if (jsonBody !== undefined) {
107
+ return {
108
+ kind: "request",
109
+ method: "POST",
110
+ path: "/groups",
111
+ presentation: "group-created",
112
+ body: jsonBody,
113
+ };
114
+ }
115
+ requirePositionals(positionals, 0, CREATE_HELP);
116
+
117
+ const description = values.description as string | undefined;
118
+ const groupMembers = repeatedStrings(values.member);
119
+ const type = enumValue(values.type, "--type", [
120
+ "vacation",
121
+ "roommates",
122
+ "couple",
123
+ "travel",
124
+ "party",
125
+ "other",
126
+ ] as const);
127
+ return {
128
+ kind: "request",
129
+ method: "POST",
130
+ path: "/groups",
131
+ presentation: "group-created",
132
+ body: {
133
+ name: requiredString(values.name, "--name", CREATE_HELP),
134
+ currencyId: requiredString(
135
+ values["currency-id"],
136
+ "--currency-id",
137
+ CREATE_HELP,
138
+ ),
139
+ ...(description === undefined ? {} : { description }),
140
+ ...(type === undefined ? {} : { type }),
141
+ ...(groupMembers.length === 0 ? {} : { groupMembers }),
142
+ },
143
+ };
144
+ }
145
+
146
+ function parseGroupsActivities(args: string[]): ParsedCommand {
147
+ if (wantsHelp(args)) return { kind: "help", text: ACTIVITIES_HELP };
148
+ const { positionals, values } = parseOptions(args, {
149
+ direction: { type: "string" },
150
+ limit: { type: "string" },
151
+ page: { type: "string" },
152
+ search: { type: "string" },
153
+ sort: { type: "string" },
154
+ type: { type: "string" },
155
+ });
156
+ requirePositionals(positionals, 1, ACTIVITIES_HELP);
157
+
158
+ const query = new URLSearchParams();
159
+ appendQuery(query, "l", positiveInteger(values.limit, "--limit"));
160
+ appendQuery(query, "p", positiveInteger(values.page, "--page"));
161
+ appendQuery(
162
+ query,
163
+ "type",
164
+ enumValue(values.type, "--type", [
165
+ "all",
166
+ "expenses",
167
+ "payments",
168
+ "recurring_expenses",
169
+ ] as const),
170
+ );
171
+ appendQuery(
172
+ query,
173
+ "sort",
174
+ enumValue(values.sort, "--sort", ["date", "amount"] as const),
175
+ );
176
+ appendQuery(
177
+ query,
178
+ "direction",
179
+ enumValue(values.direction, "--direction", ["asc", "desc"] as const),
180
+ );
181
+
182
+ const groupId = encodeURIComponent(positionals[0]);
183
+ const search = values.search as string | undefined;
184
+ if (search !== undefined) query.set("q", search);
185
+ return {
186
+ kind: "request",
187
+ path: `/groups/${groupId}/activities${search === undefined ? "" : "/search"}`,
188
+ presentation: "activities",
189
+ query,
190
+ };
191
+ }
192
+
193
+ export function parseGroups(args: string[]): ParsedCommand {
194
+ if (args.length === 0) return parseGroupsList(args);
195
+ if (args[0] === "--help" || args[0] === "-h") {
196
+ return { kind: "help", text: HELP };
197
+ }
198
+
199
+ const [command, ...rest] = args;
200
+ if (command === "list") return parseGroupsList(rest);
201
+ if (command === "create") return parseGroupsCreate(rest);
202
+ if (command === "activities") return parseGroupsActivities(rest);
203
+ if (command === "get" || command === "members") {
204
+ const help = command === "get" ? GET_HELP : MEMBERS_HELP;
205
+ if (wantsHelp(rest)) return { kind: "help", text: help };
206
+ const { positionals } = parseOptions(rest);
207
+ requirePositionals(positionals, 1, help);
208
+ return {
209
+ kind: "request",
210
+ path: `/groups/${encodeURIComponent(positionals[0])}${
211
+ command === "members" ? "/members" : ""
212
+ }`,
213
+ presentation: command === "members" ? "members" : "group",
214
+ };
215
+ }
216
+ throw new CliFailure("usage", HELP);
217
+ }
218
+
219
+ function cleanGroupList(body: unknown) {
220
+ const response = asRecord(body);
221
+ return {
222
+ items: asArray(response.items).map((value) => {
223
+ const group = asRecord(value);
224
+ const members = asArray(group.groupMembers)
225
+ .map((member) => asRecord(member).name)
226
+ .filter((name): name is string => typeof name === "string");
227
+ return {
228
+ id: group.id ?? null,
229
+ name: group.name ?? null,
230
+ description: group.description ?? null,
231
+ type: group.type ?? null,
232
+ currency: currencyCode(group.currency),
233
+ balance: numeric(group.balance),
234
+ memberCount: members.length,
235
+ members,
236
+ mostRecentActivity: group.mostRecentActivity ?? null,
237
+ };
238
+ }),
239
+ hasMore: response.hasMore === true,
240
+ nextCursor: response.nextCursor ?? null,
241
+ };
242
+ }
243
+
244
+ function cleanGroup(body: unknown) {
245
+ const group = asRecord(body);
246
+ return {
247
+ id: group.id ?? null,
248
+ name: group.name ?? null,
249
+ description: group.description ?? null,
250
+ type: group.type ?? null,
251
+ currency: currencyCode(group.currency),
252
+ balance: numeric(group.balance),
253
+ totalOwed: numeric(group.totalOwed),
254
+ totalOwing: numeric(group.totalOwing),
255
+ memberCount: numeric(group.memberCount),
256
+ defaultSplitType: group.defaultSplitType ?? null,
257
+ useOptimalSettlement: group.useOptimalSettlement === true,
258
+ memberBalanceVisibility: group.memberBalanceVisibility ?? null,
259
+ };
260
+ }
261
+
262
+ function cleanMembers(body: unknown) {
263
+ return asArray(body).map((value) => {
264
+ const member = asRecord(value);
265
+ return {
266
+ id: member.id ?? null,
267
+ userId: member.userId ?? null,
268
+ name: member.name ?? null,
269
+ role: member.role ?? null,
270
+ isGuest: member.isGuest === true,
271
+ isGold: member.isGold === true,
272
+ defaultSplitPercentage: numeric(member.defaultSplitPercentage),
273
+ joinedAt: member.joinedAt ?? null,
274
+ };
275
+ });
276
+ }
277
+
278
+ function cleanActivities(body: unknown) {
279
+ return asArray(body).map((value) => {
280
+ const activity = asRecord(value);
281
+ if (activity.entity === "payment") {
282
+ return {
283
+ entity: "payment",
284
+ id: activity.id ?? null,
285
+ description: activity.description ?? null,
286
+ amount: numeric(activity.amount),
287
+ currency: currencyCode(activity.currency),
288
+ date: activity.date ?? null,
289
+ from: cleanUserSummary(activity.fromUser),
290
+ to: cleanUserSummary(activity.toUser),
291
+ isSettlement: activity.isSettlement === true,
292
+ };
293
+ }
294
+ return {
295
+ entity: "expense",
296
+ id: activity.id ?? null,
297
+ title: activity.title ?? null,
298
+ amount: numeric(activity.amount),
299
+ currency: currencyCode(activity.currency),
300
+ date: activity.date ?? null,
301
+ paidBy: cleanUserSummary(activity.paidByUser),
302
+ category: asRecord(activity.category).name ?? null,
303
+ splitType: activity.splitType ?? null,
304
+ isRecurring: activity.recurringExpenseRuleId != null,
305
+ };
306
+ });
307
+ }
308
+
309
+ function cleanCreatedGroup(body: unknown) {
310
+ const group = asRecord(body);
311
+ return {
312
+ id: group.id ?? null,
313
+ name: group.name ?? null,
314
+ description: group.description ?? null,
315
+ type: group.type ?? null,
316
+ currencyId: group.currencyId ?? null,
317
+ creatorId: group.creatorId ?? null,
318
+ defaultSplitType: group.defaultSplitType ?? null,
319
+ memberBalanceVisibility: group.memberBalanceVisibility ?? null,
320
+ };
321
+ }
322
+
323
+ function cleanMemberDetail(body: unknown) {
324
+ const member = asRecord(body);
325
+ const user = asRecord(member.user);
326
+ return {
327
+ id: member.id ?? null,
328
+ userId: member.userId ?? user.id ?? null,
329
+ name: user.name ?? member.name ?? null,
330
+ username: user.displayUsername ?? user.username ?? null,
331
+ email: user.email ?? null,
332
+ bio: user.bio ?? null,
333
+ role: member.role ?? null,
334
+ isGuest: member.isGuest === true,
335
+ isGold: member.isGold === true,
336
+ defaultSplitPercentage: numeric(member.defaultSplitPercentage),
337
+ joinedAt: member.joinedAt ?? null,
338
+ };
339
+ }
340
+
341
+ function cleanExpenseDetail(body: unknown) {
342
+ const expense = asRecord(body);
343
+ const recurrence = asRecord(expense.recurrence);
344
+ return {
345
+ id: expense.id ?? null,
346
+ title: expense.title ?? null,
347
+ description: expense.description ?? null,
348
+ amount: numeric(expense.amount),
349
+ currency: currencyCode(expense.currency),
350
+ date: expense.date ?? null,
351
+ timezone: expense.timezone ?? null,
352
+ paidBy: cleanUserSummary(expense.paidByUser),
353
+ creator: cleanUserSummary(expense.creator),
354
+ group: cleanUserSummary(expense.group),
355
+ category: asRecord(expense.category).name ?? null,
356
+ splitType: expense.splitType ?? null,
357
+ isRecurring:
358
+ expense.recurringExpenseRuleId != null || expense.recurrence != null,
359
+ recurrence:
360
+ expense.recurrence == null
361
+ ? null
362
+ : {
363
+ frequency: recurrence.frequency ?? null,
364
+ interval: numeric(recurrence.interval),
365
+ },
366
+ splits: asArray(expense.shares).map((value) => {
367
+ const share = asRecord(value);
368
+ return {
369
+ user: cleanUserSummary(share.user),
370
+ userId: share.userId ?? null,
371
+ amount: numeric(share.amount),
372
+ };
373
+ }),
374
+ };
375
+ }
376
+
377
+ function cleanPaymentDetail(body: unknown) {
378
+ const payment = asRecord(body);
379
+ return {
380
+ id: payment.id ?? null,
381
+ description: payment.description ?? null,
382
+ amount: numeric(payment.amount),
383
+ currency: currencyCode(payment.currency),
384
+ date: payment.date ?? null,
385
+ timezone: payment.timezone ?? null,
386
+ from: cleanUserSummary(payment.fromUser),
387
+ to: cleanUserSummary(payment.toUser),
388
+ creator: cleanUserSummary(payment.creator),
389
+ group: cleanUserSummary(payment.group),
390
+ isSettlement: payment.isSettlement === true,
391
+ usedOptimalSettlement: payment.usedOptimalSettlement === true,
392
+ };
393
+ }
394
+
395
+ function formatGroup(body: unknown) {
396
+ const response = asRecord(body);
397
+ return [
398
+ `Name: ${display(response.name)}`,
399
+ `Description: ${display(response.description)}`,
400
+ `Type: ${display(response.type)}`,
401
+ `Currency: ${display(response.currency)}`,
402
+ `Balance: ${humanAmount(response.balance, response.currency)}`,
403
+ `Owed: ${humanAmount(response.totalOwed, response.currency)}`,
404
+ `Owing: ${humanAmount(response.totalOwing, response.currency)}`,
405
+ `Members: ${display(response.memberCount)}`,
406
+ `Default split: ${display(response.defaultSplitType)}`,
407
+ `Optimal settlement: ${yesNo(response.useOptimalSettlement)}`,
408
+ `Balance visibility: ${display(response.memberBalanceVisibility)}`,
409
+ `ID: ${display(response.id)}`,
410
+ ].join("\n");
411
+ }
412
+
413
+ function activityDetailPath(item: Record<string, unknown>) {
414
+ const collection = item.entity === "payment" ? "payments" : "expenses";
415
+ return `/${collection}/${encodedDetailId(item.id)}`;
416
+ }
417
+
418
+ function formatActivityDetail(item: Record<string, unknown>, body: unknown) {
419
+ if (item.entity === "payment") {
420
+ const payment = asRecord(cleanPaymentDetail(body));
421
+ return [
422
+ `Description: ${display(payment.description)}`,
423
+ `Amount: ${humanAmount(payment.amount, payment.currency)}`,
424
+ `From: ${namedEntity(payment.from)}`,
425
+ `To: ${namedEntity(payment.to)}`,
426
+ `Settlement: ${yesNo(payment.isSettlement)}`,
427
+ `Optimal settlement: ${yesNo(payment.usedOptimalSettlement)}`,
428
+ `Date: ${display(payment.date)}`,
429
+ `Timezone: ${display(payment.timezone)}`,
430
+ `Group: ${namedEntity(payment.group)}`,
431
+ `Created by: ${namedEntity(payment.creator)}`,
432
+ `ID: ${display(payment.id)}`,
433
+ ].join("\n");
434
+ }
435
+
436
+ const expense = asRecord(cleanExpenseDetail(body));
437
+ const recurrence = asRecord(expense.recurrence);
438
+ const splits = asArray(expense.splits);
439
+ return [
440
+ `Description: ${display(expense.description)}`,
441
+ `Amount: ${humanAmount(expense.amount, expense.currency)}`,
442
+ `Paid by: ${namedEntity(expense.paidBy)}`,
443
+ `Category: ${display(expense.category)}`,
444
+ `Split: ${display(expense.splitType)}`,
445
+ `Recurring: ${yesNo(expense.isRecurring)}`,
446
+ ...(expense.recurrence == null
447
+ ? []
448
+ : [
449
+ `Recurrence frequency: ${display(recurrence.frequency)}`,
450
+ `Recurrence interval: ${display(recurrence.interval)}`,
451
+ ]),
452
+ `Date: ${display(expense.date)}`,
453
+ `Timezone: ${display(expense.timezone)}`,
454
+ `Group: ${namedEntity(expense.group)}`,
455
+ `Created by: ${namedEntity(expense.creator)}`,
456
+ `ID: ${display(expense.id)}`,
457
+ "",
458
+ "Splits",
459
+ ...(splits.length
460
+ ? splits.map((value) => {
461
+ const split = asRecord(value);
462
+ const user = asRecord(split.user);
463
+ return `${display(user.name ?? split.userId)}: ${humanAmount(split.amount, expense.currency)}${
464
+ split.userId ? ` · ${String(split.userId)}` : ""
465
+ }`;
466
+ })
467
+ : ["—"]),
468
+ ].join("\n");
469
+ }
470
+
471
+ export const groupPresenters = {
472
+ "group-list": {
473
+ clean: cleanGroupList,
474
+ format(body) {
475
+ const response = asRecord(body);
476
+ const items = asArray(response.items);
477
+ const lines = items.length
478
+ ? [
479
+ "Groups",
480
+ ...items.map((value, index) => {
481
+ const group = asRecord(value);
482
+ return formatCard(index, group.name, [
483
+ `ID: ${display(group.id)}`,
484
+ `Type: ${display(group.type)}`,
485
+ `Description: ${display(group.description)}`,
486
+ `Balance: ${humanAmount(group.balance, group.currency)}`,
487
+ `Members: ${display(group.memberCount)}`,
488
+ `Last activity: ${display(group.mostRecentActivity)}`,
489
+ ]);
490
+ }),
491
+ ]
492
+ : ["No groups."];
493
+ lines.push(
494
+ response.hasMore && response.nextCursor
495
+ ? [
496
+ "More groups available.",
497
+ `Next page: banana groups list --cursor ${JSON.stringify(response.nextCursor)}`,
498
+ ].join("\n")
499
+ : "End of groups.",
500
+ );
501
+ return lines.join("\n\n");
502
+ },
503
+ browser: {
504
+ detailPath(_command, item) {
505
+ return `/groups/${encodedDetailId(item.id)}`;
506
+ },
507
+ formatDetail(item, body) {
508
+ return formatGroup(cleanGroup({
509
+ ...asRecord(body),
510
+ memberCount: item.memberCount,
511
+ }));
512
+ },
513
+ },
514
+ },
515
+ group: { clean: cleanGroup, format: formatGroup },
516
+ members: {
517
+ clean: cleanMembers,
518
+ format(body) {
519
+ const items = asArray(body);
520
+ if (!items.length) return "No group members.";
521
+ return [
522
+ "Group members",
523
+ ...items.map((value, index) => {
524
+ const member = asRecord(value);
525
+ return formatCard(index, member.name, [
526
+ `Role: ${display(member.role)}`,
527
+ `Guest: ${yesNo(member.isGuest)}`,
528
+ `Gold: ${yesNo(member.isGold)}`,
529
+ `Default split: ${display(member.defaultSplitPercentage)}`,
530
+ `Joined: ${display(member.joinedAt)}`,
531
+ `Member ID: ${display(member.id)}`,
532
+ `User ID: ${display(member.userId)}`,
533
+ ]);
534
+ }),
535
+ ].join("\n\n");
536
+ },
537
+ browser: {
538
+ detailPath(command, item) {
539
+ return `${command.path}/${encodedDetailId(item.id)}`;
540
+ },
541
+ formatDetail(_item, body) {
542
+ const member = asRecord(cleanMemberDetail(body));
543
+ return [
544
+ `Name: ${display(member.name)}`,
545
+ `Username: ${display(member.username)}`,
546
+ `Email: ${display(member.email)}`,
547
+ `Bio: ${display(member.bio)}`,
548
+ `Role: ${display(member.role)}`,
549
+ `Guest: ${yesNo(member.isGuest)}`,
550
+ `Gold: ${yesNo(member.isGold)}`,
551
+ `Default split: ${display(member.defaultSplitPercentage)}`,
552
+ `Joined: ${display(member.joinedAt)}`,
553
+ `Member ID: ${display(member.id)}`,
554
+ `User ID: ${display(member.userId)}`,
555
+ ].join("\n");
556
+ },
557
+ },
558
+ },
559
+ activities: {
560
+ clean: cleanActivities,
561
+ format(body) {
562
+ const items = asArray(body);
563
+ if (!items.length) return "No group activities.";
564
+ return [
565
+ "Group activities",
566
+ ...items.map((value, index) => {
567
+ const activity = asRecord(value);
568
+ if (activity.entity === "payment") {
569
+ const from = asRecord(activity.from);
570
+ const to = asRecord(activity.to);
571
+ return formatCard(
572
+ index,
573
+ `Payment: ${display(activity.description)}`,
574
+ [
575
+ `Amount: ${humanAmount(activity.amount, activity.currency)}`,
576
+ `From: ${display(from.name)}`,
577
+ `To: ${display(to.name)}`,
578
+ `Settlement: ${yesNo(activity.isSettlement)}`,
579
+ `Date: ${display(activity.date)}`,
580
+ `ID: ${display(activity.id)}`,
581
+ ],
582
+ );
583
+ }
584
+ const paidBy = asRecord(activity.paidBy);
585
+ return formatCard(
586
+ index,
587
+ `Expense: ${display(activity.title)}`,
588
+ [
589
+ `Amount: ${humanAmount(activity.amount, activity.currency)}`,
590
+ `Paid by: ${display(paidBy.name)}`,
591
+ `Category: ${display(activity.category)}`,
592
+ `Split: ${display(activity.splitType)}`,
593
+ `Recurring: ${yesNo(activity.isRecurring)}`,
594
+ `Date: ${display(activity.date)}`,
595
+ `ID: ${display(activity.id)}`,
596
+ ],
597
+ );
598
+ }),
599
+ ].join("\n\n");
600
+ },
601
+ browser: {
602
+ detailPath(_command, item) {
603
+ return activityDetailPath(item);
604
+ },
605
+ formatDetail: formatActivityDetail,
606
+ },
607
+ },
608
+ "group-created": {
609
+ clean: cleanCreatedGroup,
610
+ format(body) {
611
+ const response = asRecord(body);
612
+ return [
613
+ "Group created",
614
+ `Name: ${display(response.name)}`,
615
+ `Description: ${display(response.description)}`,
616
+ `Type: ${display(response.type)}`,
617
+ `Currency ID: ${display(response.currencyId)}`,
618
+ `Default split: ${display(response.defaultSplitType)}`,
619
+ `Balance visibility: ${display(response.memberBalanceVisibility)}`,
620
+ `ID: ${display(response.id)}`,
621
+ ].join("\n");
622
+ },
623
+ },
624
+ } satisfies Record<
625
+ "group-list" | "group" | "members" | "activities" | "group-created",
626
+ Presenter
627
+ >;
@@ -0,0 +1,45 @@
1
+ import {
2
+ asRecord,
3
+ display,
4
+ parseOptions,
5
+ requirePositionals,
6
+ wantsHelp,
7
+ yesNo,
8
+ } from "../shared";
9
+ import type { ParsedCommand, Presenter } from "../types";
10
+
11
+ const HELP = "Usage: banana me";
12
+
13
+ export function parseMe(args: string[]): ParsedCommand {
14
+ if (wantsHelp(args)) return { kind: "help", text: HELP };
15
+ const { positionals } = parseOptions(args);
16
+ requirePositionals(positionals, 0, HELP);
17
+ return { kind: "request", path: "/current-user", presentation: "user" };
18
+ }
19
+
20
+ export const mePresenters = {
21
+ user: {
22
+ clean(body) {
23
+ const user = asRecord(body);
24
+ return {
25
+ id: user.id ?? null,
26
+ name: user.name ?? null,
27
+ email: user.email ?? null,
28
+ username: user.username ?? null,
29
+ isGuest: user.isGuest === true,
30
+ currencyId: user.currencyId ?? null,
31
+ };
32
+ },
33
+ format(body) {
34
+ const user = asRecord(body);
35
+ return [
36
+ `Name: ${display(user.name)}`,
37
+ `Email: ${display(user.email)}`,
38
+ `Username: ${display(user.username)}`,
39
+ `Guest: ${yesNo(user.isGuest)}`,
40
+ `Currency ID: ${display(user.currencyId)}`,
41
+ `ID: ${display(user.id)}`,
42
+ ].join("\n");
43
+ },
44
+ },
45
+ } satisfies Record<"user", Presenter>;