@lifestreamdynamics/vault-cli 1.1.0 → 1.3.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.
Files changed (56) 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.d.ts +2 -0
  6. package/dist/commands/ai.js +124 -0
  7. package/dist/commands/analytics.d.ts +2 -0
  8. package/dist/commands/analytics.js +84 -0
  9. package/dist/commands/auth.js +10 -105
  10. package/dist/commands/booking.d.ts +2 -0
  11. package/dist/commands/booking.js +739 -0
  12. package/dist/commands/calendar.js +778 -6
  13. package/dist/commands/completion.d.ts +5 -0
  14. package/dist/commands/completion.js +60 -0
  15. package/dist/commands/config.js +17 -16
  16. package/dist/commands/connectors.js +12 -1
  17. package/dist/commands/custom-domains.d.ts +2 -0
  18. package/dist/commands/custom-domains.js +154 -0
  19. package/dist/commands/docs.js +152 -5
  20. package/dist/commands/hooks.js +6 -1
  21. package/dist/commands/links.js +9 -2
  22. package/dist/commands/mfa.js +1 -70
  23. package/dist/commands/plugins.d.ts +2 -0
  24. package/dist/commands/plugins.js +172 -0
  25. package/dist/commands/publish-vault.d.ts +2 -0
  26. package/dist/commands/publish-vault.js +117 -0
  27. package/dist/commands/publish.js +63 -2
  28. package/dist/commands/saml.d.ts +2 -0
  29. package/dist/commands/saml.js +220 -0
  30. package/dist/commands/scim.d.ts +2 -0
  31. package/dist/commands/scim.js +238 -0
  32. package/dist/commands/shares.js +25 -3
  33. package/dist/commands/subscription.js +9 -2
  34. package/dist/commands/sync.js +3 -0
  35. package/dist/commands/teams.js +233 -4
  36. package/dist/commands/user.js +444 -0
  37. package/dist/commands/vaults.js +240 -8
  38. package/dist/commands/webhooks.js +6 -1
  39. package/dist/config.d.ts +2 -0
  40. package/dist/config.js +7 -3
  41. package/dist/index.js +28 -1
  42. package/dist/lib/credential-manager.js +32 -7
  43. package/dist/lib/migration.js +2 -2
  44. package/dist/lib/profiles.js +4 -4
  45. package/dist/sync/config.js +2 -2
  46. package/dist/sync/daemon-worker.js +13 -6
  47. package/dist/sync/daemon.js +2 -1
  48. package/dist/sync/remote-poller.js +7 -3
  49. package/dist/sync/state.js +2 -2
  50. package/dist/utils/confirm.d.ts +11 -0
  51. package/dist/utils/confirm.js +23 -0
  52. package/dist/utils/format.js +1 -1
  53. package/dist/utils/output.js +4 -1
  54. package/dist/utils/prompt.d.ts +29 -0
  55. package/dist/utils/prompt.js +146 -0
  56. package/package.json +2 -2
@@ -0,0 +1,238 @@
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 registerScimCommands(program) {
6
+ const scim = program.command('scim').description('SCIM 2.0 user provisioning management (requires scimToken)');
7
+ // ── list-users ────────────────────────────────────────────────────────────
8
+ addGlobalFlags(scim.command('list-users')
9
+ .description('List SCIM-provisioned users')
10
+ .option('--filter <expr>', 'SCIM filter expression (e.g. \'userName eq "user@example.com"\')')
11
+ .option('--start-index <n>', 'Pagination start index (1-based)', parseInt)
12
+ .option('--count <n>', 'Number of results per page (max 100)', parseInt))
13
+ .action(async (_opts) => {
14
+ const flags = resolveFlags(_opts);
15
+ const out = createOutput(flags);
16
+ out.startSpinner('Fetching SCIM users...');
17
+ try {
18
+ const client = await getClientAsync();
19
+ if (!client.scim) {
20
+ out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ const result = await client.scim.listUsers({
25
+ filter: _opts.filter,
26
+ startIndex: _opts.startIndex,
27
+ count: _opts.count,
28
+ });
29
+ out.stopSpinner();
30
+ if (flags.output !== 'json') {
31
+ out.status(chalk.dim(`Total: ${result.totalResults} Start: ${result.startIndex} Page: ${result.itemsPerPage}`));
32
+ }
33
+ out.list(result.Resources.map((u) => ({
34
+ id: u.id,
35
+ userName: u.userName,
36
+ name: u.name.formatted,
37
+ active: u.active ? 'yes' : 'no',
38
+ externalId: u.externalId ?? '',
39
+ })), {
40
+ emptyMessage: 'No SCIM users found.',
41
+ columns: [
42
+ { key: 'id', header: 'ID' },
43
+ { key: 'userName', header: 'Username' },
44
+ { key: 'name', header: 'Name' },
45
+ { key: 'active', header: 'Active' },
46
+ { key: 'externalId', header: 'External ID' },
47
+ ],
48
+ textFn: (u) => `${chalk.cyan(String(u.userName))} ${chalk.dim(`(${String(u.id)})`)} — ${u.name} — ${u.active === 'yes' ? chalk.green('active') : chalk.red('inactive')}`,
49
+ });
50
+ }
51
+ catch (err) {
52
+ handleError(out, err, 'Failed to list SCIM users');
53
+ }
54
+ });
55
+ // ── get-user ──────────────────────────────────────────────────────────────
56
+ addGlobalFlags(scim.command('get-user')
57
+ .description('Get a SCIM user by internal ID')
58
+ .argument('<id>', 'Internal user ID'))
59
+ .action(async (id, _opts) => {
60
+ const flags = resolveFlags(_opts);
61
+ const out = createOutput(flags);
62
+ out.startSpinner('Fetching SCIM user...');
63
+ try {
64
+ const client = await getClientAsync();
65
+ if (!client.scim) {
66
+ out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ const user = await client.scim.getUser(id);
71
+ out.stopSpinner();
72
+ out.record({
73
+ id: user.id,
74
+ userName: user.userName,
75
+ givenName: user.name.givenName,
76
+ familyName: user.name.familyName,
77
+ email: user.emails[0]?.value ?? '',
78
+ active: user.active,
79
+ externalId: user.externalId,
80
+ created: user.meta.created,
81
+ lastModified: user.meta.lastModified,
82
+ });
83
+ }
84
+ catch (err) {
85
+ handleError(out, err, 'Failed to fetch SCIM user');
86
+ }
87
+ });
88
+ // ── create-user ───────────────────────────────────────────────────────────
89
+ addGlobalFlags(scim.command('create-user')
90
+ .description('Provision a new user via SCIM')
91
+ .requiredOption('--user-name <userName>', 'Login name (email address)')
92
+ .requiredOption('--email <email>', 'Primary email address')
93
+ .option('--given-name <name>', 'First/given name')
94
+ .option('--family-name <name>', 'Last/family name')
95
+ .option('--external-id <id>', 'External IdP subject ID'))
96
+ .action(async (_opts) => {
97
+ const flags = resolveFlags(_opts);
98
+ const out = createOutput(flags);
99
+ out.startSpinner('Creating SCIM user...');
100
+ try {
101
+ const client = await getClientAsync();
102
+ if (!client.scim) {
103
+ out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
104
+ process.exitCode = 1;
105
+ return;
106
+ }
107
+ const created = await client.scim.createUser({
108
+ userName: _opts.userName,
109
+ emails: [{ value: _opts.email, primary: true }],
110
+ name: {
111
+ givenName: _opts.givenName,
112
+ familyName: _opts.familyName,
113
+ },
114
+ externalId: _opts.externalId,
115
+ });
116
+ out.stopSpinner();
117
+ if (flags.output === 'json') {
118
+ out.raw(JSON.stringify(created, null, 2) + '\n');
119
+ }
120
+ else {
121
+ out.raw(chalk.green(`SCIM user created: ${created.userName} (${created.id})`) + '\n');
122
+ }
123
+ }
124
+ catch (err) {
125
+ handleError(out, err, 'Failed to create SCIM user');
126
+ }
127
+ });
128
+ // ── update-user ───────────────────────────────────────────────────────────
129
+ addGlobalFlags(scim.command('update-user')
130
+ .description('Update a SCIM user (full replace)')
131
+ .argument('<id>', 'Internal user ID')
132
+ .option('--user-name <userName>', 'Updated login name')
133
+ .option('--email <email>', 'Updated primary email')
134
+ .option('--given-name <name>', 'Updated first/given name')
135
+ .option('--family-name <name>', 'Updated last/family name'))
136
+ .action(async (id, _opts) => {
137
+ const flags = resolveFlags(_opts);
138
+ const out = createOutput(flags);
139
+ const data = {};
140
+ if (_opts.userName)
141
+ data.userName = _opts.userName;
142
+ if (_opts.email)
143
+ data.emails = [{ value: _opts.email, primary: true }];
144
+ if (_opts.givenName || _opts.familyName) {
145
+ data.name = {
146
+ givenName: _opts.givenName,
147
+ familyName: _opts.familyName,
148
+ };
149
+ }
150
+ if (Object.keys(data).length === 0) {
151
+ out.error('No updates specified. Use --user-name, --email, --given-name, or --family-name.');
152
+ process.exitCode = 2;
153
+ return;
154
+ }
155
+ out.startSpinner('Updating SCIM user...');
156
+ try {
157
+ const client = await getClientAsync();
158
+ if (!client.scim) {
159
+ out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
160
+ process.exitCode = 1;
161
+ return;
162
+ }
163
+ const updated = await client.scim.updateUser(id, data);
164
+ out.stopSpinner();
165
+ if (flags.output === 'json') {
166
+ out.raw(JSON.stringify(updated, null, 2) + '\n');
167
+ }
168
+ else {
169
+ out.raw(chalk.green(`SCIM user updated: ${updated.userName} (${updated.id})`) + '\n');
170
+ }
171
+ }
172
+ catch (err) {
173
+ handleError(out, err, 'Failed to update SCIM user');
174
+ }
175
+ });
176
+ // ── delete-user ───────────────────────────────────────────────────────────
177
+ addGlobalFlags(scim.command('delete-user')
178
+ .description('Deprovision a SCIM user (removes SSO bindings)')
179
+ .argument('<id>', 'Internal user ID')
180
+ .option('--force', 'Skip confirmation prompt'))
181
+ .action(async (id, _opts) => {
182
+ const flags = resolveFlags(_opts);
183
+ const out = createOutput(flags);
184
+ if (!_opts.force) {
185
+ out.raw(chalk.yellow(`Pass --force to deprovision SCIM user ${id}.`) + '\n');
186
+ return;
187
+ }
188
+ out.startSpinner('Deprovisioning SCIM user...');
189
+ try {
190
+ const client = await getClientAsync();
191
+ if (!client.scim) {
192
+ out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
193
+ process.exitCode = 1;
194
+ return;
195
+ }
196
+ await client.scim.deleteUser(id);
197
+ out.stopSpinner();
198
+ out.raw(chalk.green(`SCIM user ${id} deprovisioned.`) + '\n');
199
+ }
200
+ catch (err) {
201
+ handleError(out, err, 'Failed to deprovision SCIM user');
202
+ }
203
+ });
204
+ // ── service-config ────────────────────────────────────────────────────────
205
+ addGlobalFlags(scim.command('service-config')
206
+ .description('Show SCIM service provider capabilities'))
207
+ .action(async (_opts) => {
208
+ const flags = resolveFlags(_opts);
209
+ const out = createOutput(flags);
210
+ out.startSpinner('Fetching SCIM service provider config...');
211
+ try {
212
+ const client = await getClientAsync();
213
+ if (!client.scim) {
214
+ out.error('SCIM resource is not configured. Provide a scimToken when creating the client.');
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+ const config = await client.scim.getServiceProviderConfig();
219
+ out.stopSpinner();
220
+ if (flags.output === 'json') {
221
+ out.raw(JSON.stringify(config, null, 2) + '\n');
222
+ }
223
+ else {
224
+ out.record({
225
+ patch: String(config.patch.supported),
226
+ bulk: String(config.bulk.supported),
227
+ filter: String(config.filter.supported),
228
+ filterMaxResults: String(config.filter.maxResults),
229
+ changePassword: String(config.changePassword.supported),
230
+ sort: String(config.sort.supported),
231
+ });
232
+ }
233
+ }
234
+ catch (err) {
235
+ handleError(out, err, 'Failed to fetch SCIM service provider config');
236
+ }
237
+ });
238
+ }
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { getClientAsync } from '../client.js';
3
3
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
+ import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
5
6
  export function registerShareCommands(program) {
6
7
  const shares = program.command('shares').description('Create, list, and revoke document share links');
7
8
  addGlobalFlags(shares.command('list')
@@ -55,20 +56,41 @@ export function registerShareCommands(program) {
55
56
  .argument('<vaultId>', 'Vault ID')
56
57
  .argument('<docPath>', 'Document path (e.g., notes/meeting.md)')
57
58
  .option('--permission <perm>', 'Permission level: view or edit', 'view')
58
- .option('--password <password>', 'Password to protect the link')
59
+ .option('--protect-with-password', 'Prompt for a password to protect the link (interactive TTY only)')
60
+ .option('--password-stdin', 'Read link password from stdin for CI usage')
59
61
  .option('--expires <date>', 'Expiration date (ISO 8601)')
60
62
  .option('--max-views <count>', 'Maximum number of views'))
61
63
  .action(async (vaultId, docPath, _opts) => {
62
64
  const flags = resolveFlags(_opts);
63
65
  const out = createOutput(flags);
66
+ // Resolve optional link password without exposing it in the process list.
67
+ let linkPassword;
68
+ if (_opts.passwordStdin) {
69
+ const pw = await readPasswordFromStdin();
70
+ if (!pw) {
71
+ out.error('--password-stdin was set but no password was provided on stdin.');
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ linkPassword = pw;
76
+ }
77
+ else if (_opts.protectWithPassword) {
78
+ const pw = await promptPassword('Share link password: ');
79
+ if (!pw) {
80
+ out.error('A password is required when --protect-with-password is set.');
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ linkPassword = pw;
85
+ }
64
86
  out.startSpinner('Creating share link...');
65
87
  try {
66
88
  const client = await getClientAsync();
67
89
  const params = {};
68
90
  if (_opts.permission)
69
91
  params.permission = String(_opts.permission);
70
- if (_opts.password)
71
- params.password = String(_opts.password);
92
+ if (linkPassword)
93
+ params.password = linkPassword;
72
94
  if (_opts.expires)
73
95
  params.expiresAt = String(_opts.expires);
74
96
  if (_opts.maxViews)
@@ -3,6 +3,7 @@ import { getClientAsync } from '../client.js';
3
3
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
4
  import { createOutput, handleError } from '../utils/output.js';
5
5
  import { formatBytes } from '../utils/format.js';
6
+ import { confirmAction } from '../utils/confirm.js';
6
7
  export function registerSubscriptionCommands(program) {
7
8
  const sub = program.command('subscription').description('View plans, manage subscription, and access billing');
8
9
  addGlobalFlags(sub.command('status')
@@ -98,12 +99,18 @@ EXAMPLES
98
99
  });
99
100
  addGlobalFlags(sub.command('cancel')
100
101
  .description('Cancel your current subscription')
101
- .option('--reason <text>', 'Reason for cancellation'))
102
+ .option('--reason <text>', 'Reason for cancellation')
103
+ .option('-y, --yes', 'Skip confirmation prompt'))
102
104
  .action(async (_opts) => {
103
105
  const flags = resolveFlags(_opts);
104
106
  const out = createOutput(flags);
105
- out.startSpinner('Cancelling subscription...');
106
107
  try {
108
+ const confirmed = await confirmAction('Are you sure you want to cancel your subscription?', { yes: _opts.yes });
109
+ if (!confirmed) {
110
+ out.status('Cancellation aborted.');
111
+ return;
112
+ }
113
+ out.startSpinner('Cancelling subscription...');
107
114
  const client = await getClientAsync();
108
115
  await client.subscription.cancel(_opts.reason);
109
116
  out.success('Subscription cancelled', { cancelled: true });
@@ -35,6 +35,9 @@ export function registerSyncCommands(program) {
35
35
  const absPath = path.resolve(localPath);
36
36
  const mode = _opts.mode ?? 'sync';
37
37
  const onConflict = _opts.onConflict ?? 'newer';
38
+ if (!_opts.onConflict) {
39
+ out.warn('No --on-conflict strategy specified; defaulting to "newer" (keeps the file with the more recent modification time). Use --on-conflict local|remote|ask to override.');
40
+ }
38
41
  const ignore = _opts.ignore;
39
42
  const syncInterval = _opts.interval;
40
43
  const autoSync = _opts.autoSync === true;
@@ -97,10 +97,15 @@ EXAMPLES
97
97
  });
98
98
  addGlobalFlags(teams.command('delete')
99
99
  .description('Permanently delete a team and all its data')
100
- .argument('<teamId>', 'Team ID'))
100
+ .argument('<teamId>', 'Team ID')
101
+ .option('-y, --yes', 'Skip confirmation prompt'))
101
102
  .action(async (teamId, _opts) => {
102
103
  const flags = resolveFlags(_opts);
103
104
  const out = createOutput(flags);
105
+ if (!_opts.yes) {
106
+ out.status(chalk.yellow(`Pass --yes to permanently delete team ${teamId} and all its data.`));
107
+ return;
108
+ }
104
109
  out.startSpinner('Deleting team...');
105
110
  try {
106
111
  const client = await getClientAsync();
@@ -125,7 +130,7 @@ EXAMPLES
125
130
  const memberList = await client.teams.listMembers(teamId);
126
131
  out.stopSpinner();
127
132
  out.list(memberList.map(m => ({
128
- name: m.user.name || m.user.email,
133
+ name: m.user.displayName || m.user.email,
129
134
  userId: m.userId,
130
135
  role: m.role,
131
136
  email: m.user.email,
@@ -169,10 +174,15 @@ EXAMPLES
169
174
  addGlobalFlags(members.command('remove')
170
175
  .description('Remove a member from the team')
171
176
  .argument('<teamId>', 'Team ID')
172
- .argument('<userId>', 'User ID'))
177
+ .argument('<userId>', 'User ID')
178
+ .option('-y, --yes', 'Skip confirmation prompt'))
173
179
  .action(async (teamId, userId, _opts) => {
174
180
  const flags = resolveFlags(_opts);
175
181
  const out = createOutput(flags);
182
+ if (!_opts.yes) {
183
+ out.status(chalk.yellow(`Pass --yes to remove user ${userId} from team ${teamId}.`));
184
+ return;
185
+ }
176
186
  out.startSpinner('Removing member...');
177
187
  try {
178
188
  const client = await getClientAsync();
@@ -284,7 +294,7 @@ EXAMPLES
284
294
  const client = await getClientAsync();
285
295
  const vaultList = await client.teams.listVaults(teamId);
286
296
  out.stopSpinner();
287
- out.list(vaultList.map(v => ({ name: String(v.name), slug: String(v.slug), description: String(v.description) || 'No description' })), {
297
+ out.list(vaultList.map(v => ({ name: String(v.name), slug: String(v.slug), description: v.description ?? 'No description' })), {
288
298
  emptyMessage: 'No team vaults found.',
289
299
  columns: [
290
300
  { key: 'name', header: 'Name' },
@@ -319,4 +329,223 @@ EXAMPLES
319
329
  handleError(out, err, 'Failed to create team vault');
320
330
  }
321
331
  });
332
+ const teamCalendar = teams.command('calendar').description('Team calendar operations');
333
+ addGlobalFlags(teamCalendar.command('view')
334
+ .description('View team calendar')
335
+ .argument('<teamId>', 'Team ID')
336
+ .requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
337
+ .requiredOption('--end <date>', 'End date (YYYY-MM-DD)')
338
+ .option('--types <types>', 'Event types to include'))
339
+ .action(async (teamId, _opts) => {
340
+ const flags = resolveFlags(_opts);
341
+ const out = createOutput(flags);
342
+ out.startSpinner('Fetching team calendar...');
343
+ try {
344
+ const client = await getClientAsync();
345
+ const cal = await client.teams.getCalendar(teamId, {
346
+ start: _opts.start,
347
+ end: _opts.end,
348
+ types: _opts.types,
349
+ });
350
+ out.stopSpinner();
351
+ if (flags.output === 'json') {
352
+ out.raw(JSON.stringify(cal, null, 2) + '\n');
353
+ }
354
+ else {
355
+ for (const [date, day] of Object.entries(cal.days ?? {})) {
356
+ if (day && day.activityCount) {
357
+ process.stdout.write(`${chalk.cyan(date)}: ${day.activityCount} events\n`);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ catch (err) {
363
+ handleError(out, err, 'Failed to fetch team calendar');
364
+ }
365
+ });
366
+ addGlobalFlags(teamCalendar.command('activity')
367
+ .description('View team calendar activity')
368
+ .argument('<teamId>', 'Team ID')
369
+ .requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
370
+ .requiredOption('--end <date>', 'End date (YYYY-MM-DD)'))
371
+ .action(async (teamId, _opts) => {
372
+ const flags = resolveFlags(_opts);
373
+ const out = createOutput(flags);
374
+ out.startSpinner('Fetching team calendar activity...');
375
+ try {
376
+ const client = await getClientAsync();
377
+ const activity = await client.teams.getCalendarActivity(teamId, {
378
+ start: _opts.start,
379
+ end: _opts.end,
380
+ });
381
+ out.stopSpinner();
382
+ if (flags.output === 'json') {
383
+ out.raw(JSON.stringify(activity, null, 2) + '\n');
384
+ }
385
+ else {
386
+ for (const day of activity.days ?? []) {
387
+ if (day.total) {
388
+ process.stdout.write(`${chalk.cyan(day.date)}: ${day.total} activities\n`);
389
+ }
390
+ }
391
+ }
392
+ }
393
+ catch (err) {
394
+ handleError(out, err, 'Failed to fetch team calendar activity');
395
+ }
396
+ });
397
+ addGlobalFlags(teamCalendar.command('events')
398
+ .description('List team calendar events')
399
+ .argument('<teamId>', 'Team ID')
400
+ .option('--start <date>', 'Start date (YYYY-MM-DD)')
401
+ .option('--end <date>', 'End date (YYYY-MM-DD)'))
402
+ .action(async (teamId, _opts) => {
403
+ const flags = resolveFlags(_opts);
404
+ const out = createOutput(flags);
405
+ out.startSpinner('Fetching team calendar events...');
406
+ try {
407
+ const client = await getClientAsync();
408
+ const events = await client.teams.getCalendarEvents(teamId, {
409
+ start: _opts.start,
410
+ end: _opts.end,
411
+ });
412
+ out.stopSpinner();
413
+ out.list(events.map(e => ({ id: e.id, title: e.title, startDate: e.startDate, endDate: e.endDate ?? '' })), {
414
+ emptyMessage: 'No events found.',
415
+ columns: [
416
+ { key: 'id', header: 'ID' },
417
+ { key: 'title', header: 'Title' },
418
+ { key: 'startDate', header: 'Start' },
419
+ { key: 'endDate', header: 'End' },
420
+ ],
421
+ textFn: (e) => `${chalk.cyan(String(e.title))} — ${String(e.startDate)}`,
422
+ });
423
+ }
424
+ catch (err) {
425
+ handleError(out, err, 'Failed to fetch team calendar events');
426
+ }
427
+ });
428
+ addGlobalFlags(teamCalendar.command('upcoming')
429
+ .description('View upcoming events and due items for a team')
430
+ .argument('<teamId>', 'Team ID'))
431
+ .action(async (teamId, _opts) => {
432
+ const flags = resolveFlags(_opts);
433
+ const out = createOutput(flags);
434
+ out.startSpinner('Fetching upcoming items...');
435
+ try {
436
+ const client = await getClientAsync();
437
+ const upcoming = await client.teams.getUpcoming(teamId);
438
+ out.stopSpinner();
439
+ if (flags.output === 'json') {
440
+ out.raw(JSON.stringify(upcoming, null, 2) + '\n');
441
+ }
442
+ else {
443
+ if (upcoming.events && upcoming.events.length > 0) {
444
+ process.stdout.write(chalk.bold('Events:\n'));
445
+ for (const e of upcoming.events) {
446
+ process.stdout.write(` ${chalk.cyan(String(e.title))} — ${String(e.startDate)}\n`);
447
+ }
448
+ }
449
+ else {
450
+ process.stdout.write(chalk.dim('No upcoming events.\n'));
451
+ }
452
+ if (upcoming.dueDocs && upcoming.dueDocs.length > 0) {
453
+ process.stdout.write(chalk.bold('Due Documents:\n'));
454
+ for (const d of upcoming.dueDocs) {
455
+ process.stdout.write(` ${chalk.cyan(String(d.path))} — due ${String(d.dueAt)}\n`);
456
+ }
457
+ }
458
+ else {
459
+ process.stdout.write(chalk.dim('No upcoming due documents.\n'));
460
+ }
461
+ }
462
+ }
463
+ catch (err) {
464
+ handleError(out, err, 'Failed to fetch upcoming items');
465
+ }
466
+ });
467
+ addGlobalFlags(teamCalendar.command('due')
468
+ .description('List due documents for a team')
469
+ .argument('<teamId>', 'Team ID')
470
+ .option('--status <status>', 'Filter by status (overdue|upcoming|all)'))
471
+ .action(async (teamId, _opts) => {
472
+ const flags = resolveFlags(_opts);
473
+ const out = createOutput(flags);
474
+ out.startSpinner('Fetching due documents...');
475
+ try {
476
+ const client = await getClientAsync();
477
+ const dueDocs = await client.teams.getDue(teamId, {
478
+ status: _opts.status,
479
+ });
480
+ out.stopSpinner();
481
+ out.list(dueDocs.map(d => ({
482
+ path: d.path,
483
+ title: d.title ?? '',
484
+ dueAt: d.dueAt,
485
+ overdue: d.overdue ? 'yes' : 'no',
486
+ })), {
487
+ emptyMessage: 'No due documents found.',
488
+ columns: [
489
+ { key: 'path', header: 'Path' },
490
+ { key: 'title', header: 'Title' },
491
+ { key: 'dueAt', header: 'Due At' },
492
+ { key: 'overdue', header: 'Overdue' },
493
+ ],
494
+ textFn: (d) => `${chalk.cyan(String(d.path))} — due ${String(d.dueAt)} ${d.overdue === 'yes' ? chalk.red('[overdue]') : ''}`,
495
+ });
496
+ }
497
+ catch (err) {
498
+ handleError(out, err, 'Failed to fetch due documents');
499
+ }
500
+ });
501
+ addGlobalFlags(teamCalendar.command('agenda')
502
+ .description('View the team calendar agenda')
503
+ .argument('<teamId>', 'Team ID')
504
+ .option('--range <days>', 'Number of days to include')
505
+ .option('--group-by <period>', 'Group by period (day|week|month)'))
506
+ .action(async (teamId, _opts) => {
507
+ const flags = resolveFlags(_opts);
508
+ const out = createOutput(flags);
509
+ out.startSpinner('Fetching team agenda...');
510
+ try {
511
+ const client = await getClientAsync();
512
+ const agenda = await client.teams.getAgenda(teamId, {
513
+ range: _opts.range,
514
+ groupBy: _opts.groupBy,
515
+ });
516
+ out.stopSpinner();
517
+ if (flags.output === 'json') {
518
+ out.raw(JSON.stringify(agenda, null, 2) + '\n');
519
+ }
520
+ else {
521
+ process.stdout.write(`Total items: ${chalk.cyan(String(agenda.total))}\n`);
522
+ for (const group of agenda.groups ?? []) {
523
+ process.stdout.write(`\n${chalk.bold(String(group.label))}:\n`);
524
+ for (const item of group.items ?? []) {
525
+ process.stdout.write(` ${chalk.cyan(String(item.title ?? item.path))} — ${String(item.dueAt ?? '')}\n`);
526
+ }
527
+ }
528
+ }
529
+ }
530
+ catch (err) {
531
+ handleError(out, err, 'Failed to fetch team agenda');
532
+ }
533
+ });
534
+ addGlobalFlags(teamCalendar.command('ical')
535
+ .description('Get the iCal feed for a team calendar')
536
+ .argument('<teamId>', 'Team ID'))
537
+ .action(async (teamId, _opts) => {
538
+ const flags = resolveFlags(_opts);
539
+ const out = createOutput(flags);
540
+ out.startSpinner('Fetching iCal feed...');
541
+ try {
542
+ const client = await getClientAsync();
543
+ const ical = await client.teams.getICalFeed(teamId);
544
+ out.stopSpinner();
545
+ out.raw(ical);
546
+ }
547
+ catch (err) {
548
+ handleError(out, err, 'Failed to fetch iCal feed');
549
+ }
550
+ });
322
551
  }