@lifestreamdynamics/vault-cli 1.2.0 → 1.3.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.
Files changed (50) hide show
  1. package/README.md +140 -30
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.js +12 -11
  4. package/dist/commands/admin.js +5 -5
  5. package/dist/commands/ai.js +17 -4
  6. package/dist/commands/auth.js +10 -105
  7. package/dist/commands/booking.d.ts +2 -0
  8. package/dist/commands/booking.js +739 -0
  9. package/dist/commands/calendar.js +725 -6
  10. package/dist/commands/completion.d.ts +5 -0
  11. package/dist/commands/completion.js +60 -0
  12. package/dist/commands/config.js +17 -16
  13. package/dist/commands/connectors.js +12 -1
  14. package/dist/commands/custom-domains.js +6 -1
  15. package/dist/commands/docs.js +12 -5
  16. package/dist/commands/hooks.js +6 -1
  17. package/dist/commands/links.js +9 -2
  18. package/dist/commands/mfa.js +1 -70
  19. package/dist/commands/plugins.d.ts +2 -0
  20. package/dist/commands/plugins.js +172 -0
  21. package/dist/commands/publish.js +13 -3
  22. package/dist/commands/saml.d.ts +2 -0
  23. package/dist/commands/saml.js +220 -0
  24. package/dist/commands/scim.d.ts +2 -0
  25. package/dist/commands/scim.js +238 -0
  26. package/dist/commands/shares.js +25 -3
  27. package/dist/commands/subscription.js +9 -2
  28. package/dist/commands/sync.js +3 -0
  29. package/dist/commands/teams.js +141 -8
  30. package/dist/commands/user.js +122 -9
  31. package/dist/commands/vaults.js +17 -8
  32. package/dist/commands/webhooks.js +6 -1
  33. package/dist/config.d.ts +2 -0
  34. package/dist/config.js +7 -3
  35. package/dist/index.js +20 -1
  36. package/dist/lib/credential-manager.js +32 -7
  37. package/dist/lib/migration.js +2 -2
  38. package/dist/lib/profiles.js +4 -4
  39. package/dist/sync/config.js +2 -2
  40. package/dist/sync/daemon-worker.js +13 -6
  41. package/dist/sync/daemon.js +2 -1
  42. package/dist/sync/remote-poller.js +7 -3
  43. package/dist/sync/state.js +2 -2
  44. package/dist/utils/confirm.d.ts +11 -0
  45. package/dist/utils/confirm.js +23 -0
  46. package/dist/utils/format.js +1 -1
  47. package/dist/utils/output.js +4 -1
  48. package/dist/utils/prompt.d.ts +29 -0
  49. package/dist/utils/prompt.js +146 -0
  50. package/package.json +2 -2
@@ -0,0 +1,739 @@
1
+ import chalk from 'chalk';
2
+ import { getClientAsync } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerBookingCommands(program) {
6
+ const booking = program.command('booking').description('Booking slot and guest booking management');
7
+ // ---------------------------------------------------------------------------
8
+ // booking slots subgroup
9
+ // ---------------------------------------------------------------------------
10
+ const slots = booking.command('slots').description('Manage bookable event slots');
11
+ // booking slots list
12
+ addGlobalFlags(slots.command('list')
13
+ .description('List all event slots for a vault')
14
+ .argument('<vaultId>', 'Vault ID'))
15
+ .action(async (vaultId, _opts) => {
16
+ const flags = resolveFlags(_opts);
17
+ const out = createOutput(flags);
18
+ out.startSpinner('Loading slots...');
19
+ try {
20
+ const client = await getClientAsync();
21
+ const slotList = await client.booking.listSlots(vaultId);
22
+ out.stopSpinner();
23
+ out.list(slotList.map(s => ({
24
+ id: s.id,
25
+ title: s.title,
26
+ duration: `${s.durationMin}min`,
27
+ hours: `${s.startTime}–${s.endTime}`,
28
+ days: s.daysOfWeek.join(','),
29
+ timezone: s.timezone,
30
+ active: s.isActive ? 'yes' : 'no',
31
+ mode: s.confirmationMode,
32
+ price: s.priceCents != null ? `${(s.priceCents / 100).toFixed(2)} ${s.currency}` : 'free',
33
+ payment: s.requirePayment ? 'required' : 'no',
34
+ })), {
35
+ emptyMessage: 'No booking slots configured.',
36
+ columns: [
37
+ { key: 'id', header: 'ID' },
38
+ { key: 'title', header: 'Title' },
39
+ { key: 'duration', header: 'Duration' },
40
+ { key: 'hours', header: 'Hours' },
41
+ { key: 'days', header: 'Days' },
42
+ { key: 'timezone', header: 'Timezone' },
43
+ { key: 'active', header: 'Active' },
44
+ { key: 'mode', header: 'Confirmation' },
45
+ { key: 'price', header: 'Price' },
46
+ { key: 'payment', header: 'Payment' },
47
+ ],
48
+ textFn: (s) => `${chalk.cyan(String(s.title))} — ${s.duration}, ${s.hours}, ${s.days} [${s.active === 'yes' ? chalk.green('active') : chalk.dim('inactive')}] ${s.price !== 'free' ? chalk.yellow(String(s.price)) : chalk.dim('free')}`,
49
+ });
50
+ }
51
+ catch (err) {
52
+ handleError(out, err, 'List slots failed');
53
+ }
54
+ });
55
+ // booking slots create
56
+ addGlobalFlags(slots.command('create')
57
+ .description('Create a new bookable event slot')
58
+ .argument('<vaultId>', 'Vault ID')
59
+ .requiredOption('--title <title>', 'Slot title')
60
+ .requiredOption('--duration <minutes>', 'Slot duration in minutes')
61
+ .requiredOption('--start-time <HH:mm>', 'Availability window start time (HH:mm)')
62
+ .requiredOption('--end-time <HH:mm>', 'Availability window end time (HH:mm)')
63
+ .requiredOption('--days <days>', 'Comma-separated days of week (e.g. Mon,Tue,Wed)')
64
+ .requiredOption('--timezone <tz>', 'Timezone (e.g. America/New_York)')
65
+ .option('--buffer <minutes>', 'Buffer time between bookings in minutes', '0')
66
+ .option('--max-concurrent <n>', 'Maximum concurrent bookings', '1')
67
+ .option('--confirmation-mode <mode>', 'Confirmation mode: auto, email, manual', 'auto'))
68
+ .action(async (vaultId, _opts) => {
69
+ const flags = resolveFlags(_opts);
70
+ const out = createOutput(flags);
71
+ out.startSpinner('Creating slot...');
72
+ try {
73
+ const client = await getClientAsync();
74
+ const created = await client.booking.createSlot(vaultId, {
75
+ title: _opts.title,
76
+ durationMin: Number(_opts.duration),
77
+ startTime: _opts.startTime,
78
+ endTime: _opts.endTime,
79
+ daysOfWeek: _opts.days.split(',').map((d) => d.trim()),
80
+ timezone: _opts.timezone,
81
+ bufferMin: Number(_opts.buffer),
82
+ maxConcurrent: Number(_opts.maxConcurrent),
83
+ confirmationMode: _opts.confirmationMode,
84
+ });
85
+ out.stopSpinner();
86
+ if (flags.output === 'json') {
87
+ out.raw(JSON.stringify(created, null, 2) + '\n');
88
+ }
89
+ else {
90
+ out.status(chalk.green(`Slot created: ${created.title} (${created.id})`));
91
+ }
92
+ }
93
+ catch (err) {
94
+ handleError(out, err, 'Create slot failed');
95
+ }
96
+ });
97
+ // booking slots update
98
+ addGlobalFlags(slots.command('update')
99
+ .description('Update an existing event slot')
100
+ .argument('<vaultId>', 'Vault ID')
101
+ .argument('<slotId>', 'Slot ID')
102
+ .option('--title <title>', 'Slot title')
103
+ .option('--duration <minutes>', 'Slot duration in minutes')
104
+ .option('--start-time <HH:mm>', 'Availability window start time (HH:mm)')
105
+ .option('--end-time <HH:mm>', 'Availability window end time (HH:mm)')
106
+ .option('--days <days>', 'Comma-separated days of week')
107
+ .option('--timezone <tz>', 'Timezone')
108
+ .option('--buffer <minutes>', 'Buffer time between bookings in minutes')
109
+ .option('--max-concurrent <n>', 'Maximum concurrent bookings')
110
+ .option('--confirmation-mode <mode>', 'Confirmation mode: auto, email, manual'))
111
+ .action(async (vaultId, slotId, _opts) => {
112
+ const flags = resolveFlags(_opts);
113
+ const out = createOutput(flags);
114
+ out.startSpinner('Updating slot...');
115
+ try {
116
+ const client = await getClientAsync();
117
+ const data = {};
118
+ if (_opts.title)
119
+ data.title = _opts.title;
120
+ if (_opts.duration)
121
+ data.durationMin = Number(_opts.duration);
122
+ if (_opts.startTime)
123
+ data.startTime = _opts.startTime;
124
+ if (_opts.endTime)
125
+ data.endTime = _opts.endTime;
126
+ if (_opts.days)
127
+ data.daysOfWeek = _opts.days.split(',').map((d) => d.trim());
128
+ if (_opts.timezone)
129
+ data.timezone = _opts.timezone;
130
+ if (_opts.buffer)
131
+ data.bufferMin = Number(_opts.buffer);
132
+ if (_opts.maxConcurrent)
133
+ data.maxConcurrent = Number(_opts.maxConcurrent);
134
+ if (_opts.confirmationMode)
135
+ data.confirmationMode = _opts.confirmationMode;
136
+ const updated = await client.booking.updateSlot(vaultId, slotId, data);
137
+ out.stopSpinner();
138
+ if (flags.output === 'json') {
139
+ out.raw(JSON.stringify(updated, null, 2) + '\n');
140
+ }
141
+ else {
142
+ out.status(chalk.green(`Slot updated: ${updated.title}`));
143
+ }
144
+ }
145
+ catch (err) {
146
+ handleError(out, err, 'Update slot failed');
147
+ }
148
+ });
149
+ // booking slots delete
150
+ addGlobalFlags(slots.command('delete')
151
+ .description('Delete an event slot')
152
+ .argument('<vaultId>', 'Vault ID')
153
+ .argument('<slotId>', 'Slot ID')
154
+ .option('--confirm', 'Skip confirmation prompt'))
155
+ .action(async (vaultId, slotId, _opts) => {
156
+ const flags = resolveFlags(_opts);
157
+ const out = createOutput(flags);
158
+ if (!_opts.confirm) {
159
+ out.status(chalk.yellow(`Pass --confirm to delete slot ${slotId}.`));
160
+ return;
161
+ }
162
+ out.startSpinner('Deleting slot...');
163
+ try {
164
+ const client = await getClientAsync();
165
+ await client.booking.deleteSlot(vaultId, slotId);
166
+ out.stopSpinner();
167
+ out.status(chalk.green(`Slot ${slotId} deleted.`));
168
+ }
169
+ catch (err) {
170
+ handleError(out, err, 'Delete slot failed');
171
+ }
172
+ });
173
+ // ---------------------------------------------------------------------------
174
+ // booking list
175
+ // ---------------------------------------------------------------------------
176
+ const VALID_BOOKING_STATUSES = ['pending', 'confirmed', 'cancelled', 'no_show', 'completed'];
177
+ addGlobalFlags(booking.command('list')
178
+ .description('List bookings for a vault')
179
+ .argument('<vaultId>', 'Vault ID')
180
+ .option('--status <status>', 'Filter by status (pending/confirmed/cancelled/no_show/completed)')
181
+ .option('--slot-id <slotId>', 'Filter by slot ID')
182
+ .option('--from <date>', 'Filter bookings from this date (YYYY-MM-DD)')
183
+ .option('--to <date>', 'Filter bookings to this date (YYYY-MM-DD)'))
184
+ .action(async (vaultId, _opts) => {
185
+ const flags = resolveFlags(_opts);
186
+ const out = createOutput(flags);
187
+ const bookingStatus = _opts.status;
188
+ if (bookingStatus && !VALID_BOOKING_STATUSES.includes(bookingStatus)) {
189
+ out.error(`Invalid --status "${bookingStatus}". Must be one of: ${VALID_BOOKING_STATUSES.join(', ')}`);
190
+ process.exitCode = 1;
191
+ return;
192
+ }
193
+ out.startSpinner('Loading bookings...');
194
+ try {
195
+ const client = await getClientAsync();
196
+ const result = await client.booking.listBookings(vaultId, {
197
+ status: bookingStatus,
198
+ slotId: _opts.slotId,
199
+ startAfter: _opts.from,
200
+ startBefore: _opts.to,
201
+ });
202
+ out.stopSpinner();
203
+ out.list(result.bookings.map(b => ({
204
+ id: b.id,
205
+ guest: `${b.guestName} <${b.guestEmail}>`,
206
+ start: b.startAt.replace('T', ' ').slice(0, 16),
207
+ end: b.endAt.replace('T', ' ').slice(0, 16),
208
+ status: b.status,
209
+ payment: b.paymentStatus ?? 'unpaid',
210
+ })), {
211
+ emptyMessage: 'No bookings found.',
212
+ columns: [
213
+ { key: 'id', header: 'ID' },
214
+ { key: 'guest', header: 'Guest' },
215
+ { key: 'start', header: 'Start' },
216
+ { key: 'end', header: 'End' },
217
+ { key: 'status', header: 'Status' },
218
+ { key: 'payment', header: 'Payment' },
219
+ ],
220
+ textFn: (b) => {
221
+ const statusColor = b.status === 'confirmed' ? chalk.green :
222
+ b.status === 'cancelled' ? chalk.red :
223
+ b.status === 'pending' ? chalk.yellow : chalk.dim;
224
+ const paymentColor = b.payment === 'paid' ? chalk.green :
225
+ b.payment === 'invoiced' ? chalk.yellow :
226
+ b.payment === 'unpaid' ? chalk.dim : chalk.blue;
227
+ return `${chalk.cyan(String(b.guest))} ${chalk.dim(String(b.start))} ${statusColor(String(b.status))} [${paymentColor(String(b.payment))}]`;
228
+ },
229
+ });
230
+ if (flags.output !== 'json') {
231
+ out.status(chalk.dim(`${result.total} total`));
232
+ }
233
+ }
234
+ catch (err) {
235
+ handleError(out, err, 'List bookings failed');
236
+ }
237
+ });
238
+ // ---------------------------------------------------------------------------
239
+ // booking confirm
240
+ // ---------------------------------------------------------------------------
241
+ addGlobalFlags(booking.command('confirm')
242
+ .description('Confirm a pending booking')
243
+ .argument('<vaultId>', 'Vault ID')
244
+ .argument('<bookingId>', 'Booking ID'))
245
+ .action(async (vaultId, bookingId, _opts) => {
246
+ const flags = resolveFlags(_opts);
247
+ const out = createOutput(flags);
248
+ out.startSpinner('Confirming booking...');
249
+ try {
250
+ const client = await getClientAsync();
251
+ const updated = await client.booking.updateBookingStatus(vaultId, bookingId, 'confirmed');
252
+ out.stopSpinner();
253
+ if (flags.output === 'json') {
254
+ out.raw(JSON.stringify(updated, null, 2) + '\n');
255
+ }
256
+ else {
257
+ out.status(chalk.green(`Booking ${bookingId} confirmed.`));
258
+ }
259
+ }
260
+ catch (err) {
261
+ handleError(out, err, 'Confirm booking failed');
262
+ }
263
+ });
264
+ // ---------------------------------------------------------------------------
265
+ // booking cancel
266
+ // ---------------------------------------------------------------------------
267
+ addGlobalFlags(booking.command('cancel')
268
+ .description('Cancel a booking')
269
+ .argument('<vaultId>', 'Vault ID')
270
+ .argument('<bookingId>', 'Booking ID'))
271
+ .action(async (vaultId, bookingId, _opts) => {
272
+ const flags = resolveFlags(_opts);
273
+ const out = createOutput(flags);
274
+ out.startSpinner('Cancelling booking...');
275
+ try {
276
+ const client = await getClientAsync();
277
+ const updated = await client.booking.updateBookingStatus(vaultId, bookingId, 'cancelled');
278
+ out.stopSpinner();
279
+ if (flags.output === 'json') {
280
+ out.raw(JSON.stringify(updated, null, 2) + '\n');
281
+ }
282
+ else {
283
+ out.status(chalk.green(`Booking ${bookingId} cancelled.`));
284
+ }
285
+ }
286
+ catch (err) {
287
+ handleError(out, err, 'Cancel booking failed');
288
+ }
289
+ });
290
+ // ---------------------------------------------------------------------------
291
+ // booking templates subgroup
292
+ // ---------------------------------------------------------------------------
293
+ const templates = booking.command('templates').description('Manage event templates');
294
+ // booking templates list
295
+ addGlobalFlags(templates.command('list')
296
+ .description('List event templates for a vault')
297
+ .argument('<vaultId>', 'Vault ID'))
298
+ .action(async (vaultId, _opts) => {
299
+ const flags = resolveFlags(_opts);
300
+ const out = createOutput(flags);
301
+ out.startSpinner('Loading templates...');
302
+ try {
303
+ const client = await getClientAsync();
304
+ const list = await client.booking.listTemplates(vaultId);
305
+ out.stopSpinner();
306
+ out.list(list.map(t => ({
307
+ id: t.id,
308
+ name: t.name,
309
+ description: t.description ?? '',
310
+ created: t.createdAt.slice(0, 10),
311
+ })), {
312
+ emptyMessage: 'No templates found.',
313
+ columns: [
314
+ { key: 'id', header: 'ID' },
315
+ { key: 'name', header: 'Name' },
316
+ { key: 'description', header: 'Description' },
317
+ { key: 'created', header: 'Created' },
318
+ ],
319
+ textFn: (t) => `${chalk.cyan(String(t.name))} ${chalk.dim(String(t.description))}`,
320
+ });
321
+ }
322
+ catch (err) {
323
+ handleError(out, err, 'List templates failed');
324
+ }
325
+ });
326
+ // booking templates create
327
+ addGlobalFlags(templates.command('create')
328
+ .description('Create an event template')
329
+ .argument('<vaultId>', 'Vault ID')
330
+ .requiredOption('--name <name>', 'Template name')
331
+ .option('--description <desc>', 'Template description')
332
+ .option('--defaults <json>', 'Default values (JSON string)', '{}'))
333
+ .action(async (vaultId, _opts) => {
334
+ const flags = resolveFlags(_opts);
335
+ const out = createOutput(flags);
336
+ let defaults;
337
+ try {
338
+ defaults = JSON.parse(_opts.defaults);
339
+ }
340
+ catch {
341
+ out.error('--defaults must be valid JSON (e.g. \'{"key":"value"}\')');
342
+ process.exitCode = 2;
343
+ return;
344
+ }
345
+ out.startSpinner('Creating template...');
346
+ try {
347
+ const client = await getClientAsync();
348
+ const created = await client.booking.createTemplate(vaultId, {
349
+ name: _opts.name,
350
+ description: _opts.description,
351
+ defaults,
352
+ });
353
+ out.stopSpinner();
354
+ if (flags.output === 'json') {
355
+ out.raw(JSON.stringify(created, null, 2) + '\n');
356
+ }
357
+ else {
358
+ out.status(chalk.green(`Template created: ${created.name} (${created.id})`));
359
+ }
360
+ }
361
+ catch (err) {
362
+ handleError(out, err, 'Create template failed');
363
+ }
364
+ });
365
+ // booking templates delete
366
+ addGlobalFlags(templates.command('delete')
367
+ .description('Delete an event template')
368
+ .argument('<vaultId>', 'Vault ID')
369
+ .argument('<templateId>', 'Template ID')
370
+ .option('--confirm', 'Skip confirmation prompt'))
371
+ .action(async (vaultId, templateId, _opts) => {
372
+ const flags = resolveFlags(_opts);
373
+ const out = createOutput(flags);
374
+ if (!_opts.confirm) {
375
+ out.status(chalk.yellow(`Pass --confirm to delete template ${templateId}.`));
376
+ return;
377
+ }
378
+ out.startSpinner('Deleting template...');
379
+ try {
380
+ const client = await getClientAsync();
381
+ await client.booking.deleteTemplate(vaultId, templateId);
382
+ out.stopSpinner();
383
+ out.status(chalk.green(`Template ${templateId} deleted.`));
384
+ }
385
+ catch (err) {
386
+ handleError(out, err, 'Delete template failed');
387
+ }
388
+ });
389
+ // ---------------------------------------------------------------------------
390
+ // booking reschedule
391
+ // ---------------------------------------------------------------------------
392
+ addGlobalFlags(booking.command('reschedule')
393
+ .description('Reschedule a booking by guest reschedule token')
394
+ .argument('<token>', 'Reschedule token (from guest email link)')
395
+ .argument('<newStartAt>', 'New start time in ISO 8601 format (e.g. 2026-03-15T10:00:00Z)'))
396
+ .action(async (token, newStartAt, _opts) => {
397
+ const flags = resolveFlags(_opts);
398
+ const out = createOutput(flags);
399
+ out.startSpinner('Rescheduling booking...');
400
+ try {
401
+ const client = await getClientAsync();
402
+ const result = await client.booking.rescheduleBooking(token, newStartAt);
403
+ out.stopSpinner();
404
+ if (flags.output === 'json') {
405
+ out.raw(JSON.stringify(result, null, 2) + '\n');
406
+ }
407
+ else {
408
+ out.status(chalk.green(`Booking rescheduled for ${result.guestName} at ${new Date(result.startAt).toLocaleString()}`));
409
+ }
410
+ }
411
+ catch (err) {
412
+ handleError(out, err, 'Reschedule booking failed');
413
+ }
414
+ });
415
+ // ---------------------------------------------------------------------------
416
+ // booking analytics (Business tier)
417
+ // ---------------------------------------------------------------------------
418
+ const VALID_ANALYTICS_VIEWS = ['volume', 'funnel', 'peak-times'];
419
+ addGlobalFlags(booking.command('analytics')
420
+ .description('View booking analytics (Business tier)')
421
+ .argument('<vaultId>', 'Vault ID')
422
+ .option('--view <view>', 'Analytics view: volume, funnel, peak-times', 'volume')
423
+ .option('--from <date>', 'Start date (YYYY-MM-DD)')
424
+ .option('--to <date>', 'End date (YYYY-MM-DD)')
425
+ .option('--slot-id <slotId>', 'Filter by slot ID'))
426
+ .action(async (vaultId, _opts) => {
427
+ const flags = resolveFlags(_opts);
428
+ const out = createOutput(flags);
429
+ const analyticsView = _opts.view;
430
+ if (analyticsView && !VALID_ANALYTICS_VIEWS.includes(analyticsView)) {
431
+ out.error(`Invalid --view "${analyticsView}". Must be one of: ${VALID_ANALYTICS_VIEWS.join(', ')}`);
432
+ process.exitCode = 1;
433
+ return;
434
+ }
435
+ out.startSpinner('Loading analytics...');
436
+ try {
437
+ const client = await getClientAsync();
438
+ const result = await client.booking.getBookingAnalytics(vaultId, {
439
+ view: analyticsView,
440
+ from: _opts.from,
441
+ to: _opts.to,
442
+ slotId: _opts.slotId,
443
+ });
444
+ out.stopSpinner();
445
+ if (flags.output === 'json') {
446
+ out.raw(JSON.stringify(result, null, 2) + '\n');
447
+ }
448
+ else {
449
+ out.status(chalk.cyan(`Booking Analytics (${result.view})`));
450
+ if (result.data.length === 0) {
451
+ out.status(chalk.dim('No data available.'));
452
+ }
453
+ else {
454
+ const columns = Object.keys(result.data?.[0] ?? {}).map((k) => ({ key: k, header: k }));
455
+ out.list(result.data, {
456
+ emptyMessage: 'No data.',
457
+ columns,
458
+ textFn: (row) => Object.values(row)
459
+ .map(String)
460
+ .join(' | '),
461
+ });
462
+ }
463
+ }
464
+ }
465
+ catch (err) {
466
+ handleError(out, err, 'Load analytics failed');
467
+ }
468
+ });
469
+ // ---------------------------------------------------------------------------
470
+ // booking groups subgroup (Business tier — round-robin team scheduling)
471
+ // ---------------------------------------------------------------------------
472
+ const groups = booking.command('groups').description('Manage team booking groups (Business tier)');
473
+ // booking groups list <teamId>
474
+ addGlobalFlags(groups.command('list')
475
+ .description('List booking groups for a team')
476
+ .argument('<teamId>', 'Team ID'))
477
+ .action(async (teamId, _opts) => {
478
+ const flags = resolveFlags(_opts);
479
+ const out = createOutput(flags);
480
+ out.startSpinner('Loading booking groups...');
481
+ try {
482
+ const client = await getClientAsync();
483
+ const list = await client.teamBookingGroups.listGroups(teamId);
484
+ out.stopSpinner();
485
+ out.list(list.map((g) => ({
486
+ id: g.id,
487
+ name: g.name,
488
+ mode: g.assignmentMode,
489
+ active: g.isActive ? 'yes' : 'no',
490
+ })), {
491
+ emptyMessage: 'No booking groups found.',
492
+ columns: [
493
+ { key: 'id', header: 'ID' },
494
+ { key: 'name', header: 'Name' },
495
+ { key: 'mode', header: 'Mode' },
496
+ { key: 'active', header: 'Active' },
497
+ ],
498
+ textFn: (g) => `${chalk.cyan(String(g.name))} [${g.mode}] ${g.active === 'yes' ? chalk.green('active') : chalk.dim('inactive')}`,
499
+ });
500
+ }
501
+ catch (err) {
502
+ handleError(out, err, 'List booking groups failed');
503
+ }
504
+ });
505
+ // booking groups create <teamId>
506
+ addGlobalFlags(groups.command('create')
507
+ .description('Create a new booking group for a team')
508
+ .argument('<teamId>', 'Team ID')
509
+ .requiredOption('--name <name>', 'Group name')
510
+ .option('--mode <mode>', 'Assignment mode: round_robin, least_busy, attendee_choice', 'round_robin'))
511
+ .action(async (teamId, _opts) => {
512
+ const flags = resolveFlags(_opts);
513
+ const out = createOutput(flags);
514
+ out.startSpinner('Creating booking group...');
515
+ try {
516
+ const client = await getClientAsync();
517
+ const created = await client.teamBookingGroups.createGroup(teamId, {
518
+ name: _opts.name,
519
+ assignmentMode: _opts.mode,
520
+ });
521
+ out.stopSpinner();
522
+ if (flags.output === 'json') {
523
+ out.raw(JSON.stringify(created, null, 2) + '\n');
524
+ }
525
+ else {
526
+ out.status(chalk.green(`Booking group created: ${created.name} (${created.id})`));
527
+ }
528
+ }
529
+ catch (err) {
530
+ handleError(out, err, 'Create booking group failed');
531
+ }
532
+ });
533
+ // booking groups update <teamId> <groupId>
534
+ addGlobalFlags(groups.command('update')
535
+ .description('Update a booking group')
536
+ .argument('<teamId>', 'Team ID')
537
+ .argument('<groupId>', 'Group ID')
538
+ .option('--name <name>', 'Group name')
539
+ .option('--mode <mode>', 'Assignment mode: round_robin, least_busy, attendee_choice')
540
+ .option('--active', 'Set group as active')
541
+ .option('--inactive', 'Set group as inactive'))
542
+ .action(async (teamId, groupId, _opts) => {
543
+ const flags = resolveFlags(_opts);
544
+ const out = createOutput(flags);
545
+ out.startSpinner('Updating booking group...');
546
+ try {
547
+ const client = await getClientAsync();
548
+ const data = {};
549
+ if (_opts.name)
550
+ data.name = _opts.name;
551
+ if (_opts.mode)
552
+ data.assignmentMode = _opts.mode;
553
+ if (_opts.active)
554
+ data.isActive = true;
555
+ if (_opts.inactive)
556
+ data.isActive = false;
557
+ const updated = await client.teamBookingGroups.updateGroup(teamId, groupId, data);
558
+ out.stopSpinner();
559
+ if (flags.output === 'json') {
560
+ out.raw(JSON.stringify(updated, null, 2) + '\n');
561
+ }
562
+ else {
563
+ out.status(chalk.green(`Booking group updated: ${updated.name}`));
564
+ }
565
+ }
566
+ catch (err) {
567
+ handleError(out, err, 'Update booking group failed');
568
+ }
569
+ });
570
+ // booking groups delete <teamId> <groupId>
571
+ addGlobalFlags(groups.command('delete')
572
+ .description('Delete a booking group')
573
+ .argument('<teamId>', 'Team ID')
574
+ .argument('<groupId>', 'Group ID')
575
+ .option('--confirm', 'Skip confirmation prompt'))
576
+ .action(async (teamId, groupId, _opts) => {
577
+ const flags = resolveFlags(_opts);
578
+ const out = createOutput(flags);
579
+ if (!_opts.confirm) {
580
+ out.status(chalk.yellow(`Pass --confirm to delete booking group ${groupId}.`));
581
+ return;
582
+ }
583
+ out.startSpinner('Deleting booking group...');
584
+ try {
585
+ const client = await getClientAsync();
586
+ await client.teamBookingGroups.deleteGroup(teamId, groupId);
587
+ out.stopSpinner();
588
+ out.status(chalk.green(`Booking group ${groupId} deleted.`));
589
+ }
590
+ catch (err) {
591
+ handleError(out, err, 'Delete booking group failed');
592
+ }
593
+ });
594
+ // ---------------------------------------------------------------------------
595
+ // booking group-members subgroup (Business tier)
596
+ // ---------------------------------------------------------------------------
597
+ const groupMembers = booking
598
+ .command('group-members')
599
+ .description('Manage members of team booking groups (Business tier)');
600
+ // booking group-members list <teamId> <groupId>
601
+ addGlobalFlags(groupMembers.command('list')
602
+ .description('List members of a booking group')
603
+ .argument('<teamId>', 'Team ID')
604
+ .argument('<groupId>', 'Group ID'))
605
+ .action(async (teamId, groupId, _opts) => {
606
+ const flags = resolveFlags(_opts);
607
+ const out = createOutput(flags);
608
+ out.startSpinner('Loading group members...');
609
+ try {
610
+ const client = await getClientAsync();
611
+ const members = await client.teamBookingGroups.listMembers(teamId, groupId);
612
+ out.stopSpinner();
613
+ out.list(members.map((m) => ({
614
+ userId: m.userId,
615
+ name: m.user.displayName,
616
+ email: m.user.email,
617
+ weight: String(m.weight),
618
+ })), {
619
+ emptyMessage: 'No members in this booking group.',
620
+ columns: [
621
+ { key: 'userId', header: 'User ID' },
622
+ { key: 'name', header: 'Name' },
623
+ { key: 'email', header: 'Email' },
624
+ { key: 'weight', header: 'Weight' },
625
+ ],
626
+ textFn: (m) => `${chalk.cyan(String(m.name))} <${m.email}> weight=${m.weight}`,
627
+ });
628
+ }
629
+ catch (err) {
630
+ handleError(out, err, 'List group members failed');
631
+ }
632
+ });
633
+ // booking group-members add <teamId> <groupId>
634
+ addGlobalFlags(groupMembers.command('add')
635
+ .description('Add a member to a booking group')
636
+ .argument('<teamId>', 'Team ID')
637
+ .argument('<groupId>', 'Group ID')
638
+ .requiredOption('--user-id <userId>', 'User ID to add')
639
+ .option('--weight <n>', 'Scheduling weight (default 1)', '1'))
640
+ .action(async (teamId, groupId, _opts) => {
641
+ const flags = resolveFlags(_opts);
642
+ const out = createOutput(flags);
643
+ out.startSpinner('Adding group member...');
644
+ try {
645
+ const client = await getClientAsync();
646
+ const member = await client.teamBookingGroups.addMember(teamId, groupId, {
647
+ userId: _opts.userId,
648
+ weight: Number(_opts.weight ?? 1),
649
+ });
650
+ out.stopSpinner();
651
+ if (flags.output === 'json') {
652
+ out.raw(JSON.stringify(member, null, 2) + '\n');
653
+ }
654
+ else {
655
+ out.status(chalk.green(`Added ${member.user.displayName} (${member.userId}) to booking group.`));
656
+ }
657
+ }
658
+ catch (err) {
659
+ handleError(out, err, 'Add group member failed');
660
+ }
661
+ });
662
+ // booking group-members remove <teamId> <groupId> <userId>
663
+ addGlobalFlags(groupMembers.command('remove')
664
+ .description('Remove a member from a booking group')
665
+ .argument('<teamId>', 'Team ID')
666
+ .argument('<groupId>', 'Group ID')
667
+ .argument('<userId>', 'User ID to remove'))
668
+ .action(async (teamId, groupId, userId, _opts) => {
669
+ const flags = resolveFlags(_opts);
670
+ const out = createOutput(flags);
671
+ out.startSpinner('Removing group member...');
672
+ try {
673
+ const client = await getClientAsync();
674
+ await client.teamBookingGroups.removeMember(teamId, groupId, userId);
675
+ out.stopSpinner();
676
+ out.status(chalk.green(`User ${userId} removed from booking group.`));
677
+ }
678
+ catch (err) {
679
+ handleError(out, err, 'Remove group member failed');
680
+ }
681
+ });
682
+ // ---------------------------------------------------------------------------
683
+ // booking waitlist subgroup (Business tier)
684
+ // ---------------------------------------------------------------------------
685
+ const waitlist = booking.command('waitlist').description('Manage booking waitlists (Business tier)');
686
+ // booking waitlist list <vaultId> <slotId>
687
+ addGlobalFlags(waitlist.command('list')
688
+ .description('List waitlist entries for a booking slot')
689
+ .argument('<vaultId>', 'Vault ID')
690
+ .argument('<slotId>', 'Slot ID')
691
+ .option('--status <status>', 'Filter by status (waiting/notified/expired/left)')
692
+ .option('--start-at <iso>', 'Filter by specific start time (ISO 8601)'))
693
+ .action(async (vaultId, slotId, _opts) => {
694
+ const flags = resolveFlags(_opts);
695
+ const out = createOutput(flags);
696
+ out.startSpinner('Loading waitlist...');
697
+ try {
698
+ const client = await getClientAsync();
699
+ const result = await client.booking.getWaitlist(vaultId, slotId, {
700
+ status: _opts.status,
701
+ startAt: _opts.startAt,
702
+ });
703
+ out.stopSpinner();
704
+ out.list(result.entries.map((e) => ({
705
+ position: String(e.position),
706
+ guest: e.guestName,
707
+ email: e.guestEmail,
708
+ status: e.status,
709
+ notified: e.notifiedAt ? e.notifiedAt.replace('T', ' ').slice(0, 16) : '',
710
+ expires: e.expiresAt ? e.expiresAt.replace('T', ' ').slice(0, 16) : '',
711
+ created: e.createdAt.replace('T', ' ').slice(0, 16),
712
+ })), {
713
+ emptyMessage: 'No waitlist entries found.',
714
+ columns: [
715
+ { key: 'position', header: '#' },
716
+ { key: 'guest', header: 'Guest' },
717
+ { key: 'email', header: 'Email' },
718
+ { key: 'status', header: 'Status' },
719
+ { key: 'notified', header: 'Notified' },
720
+ { key: 'expires', header: 'Expires' },
721
+ { key: 'created', header: 'Joined' },
722
+ ],
723
+ textFn: (e) => {
724
+ const statusColor = e.status === 'waiting' ? chalk.yellow :
725
+ e.status === 'notified' ? chalk.cyan :
726
+ e.status === 'expired' ? chalk.dim :
727
+ chalk.red;
728
+ return `${chalk.bold(String(e.position))}. ${chalk.cyan(String(e.guest))} <${e.email}> ${statusColor(String(e.status))} ${chalk.dim(String(e.created))}`;
729
+ },
730
+ });
731
+ if (flags.output !== 'json') {
732
+ out.status(chalk.dim(`${result.total} total`));
733
+ }
734
+ }
735
+ catch (err) {
736
+ handleError(out, err, 'List waitlist failed');
737
+ }
738
+ });
739
+ }