@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
@@ -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' },
@@ -343,8 +353,8 @@ EXAMPLES
343
353
  }
344
354
  else {
345
355
  for (const [date, day] of Object.entries(cal.days ?? {})) {
346
- if (day && day.count) {
347
- process.stdout.write(`${chalk.cyan(date)}: ${day.count} events\n`);
356
+ if (day && day.activityCount) {
357
+ process.stdout.write(`${chalk.cyan(date)}: ${day.activityCount} events\n`);
348
358
  }
349
359
  }
350
360
  }
@@ -374,8 +384,8 @@ EXAMPLES
374
384
  }
375
385
  else {
376
386
  for (const day of activity.days ?? []) {
377
- if (day.count) {
378
- process.stdout.write(`${chalk.cyan(day.date)}: ${day.count} activities\n`);
387
+ if (day.total) {
388
+ process.stdout.write(`${chalk.cyan(day.date)}: ${day.total} activities\n`);
379
389
  }
380
390
  }
381
391
  }
@@ -415,4 +425,127 @@ EXAMPLES
415
425
  handleError(out, err, 'Failed to fetch team calendar events');
416
426
  }
417
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
+ });
418
551
  }
@@ -1,8 +1,10 @@
1
1
  import chalk from 'chalk';
2
+ import { writeFile } from 'node:fs/promises';
2
3
  import { getClientAsync } from '../client.js';
3
4
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
5
  import { createOutput, handleError } from '../utils/output.js';
5
6
  import { formatBytes } from '../utils/format.js';
7
+ import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
6
8
  export function registerUserCommands(program) {
7
9
  const user = program.command('user').description('View account details and storage usage');
8
10
  addGlobalFlags(user.command('storage')
@@ -56,7 +58,7 @@ export function registerUserCommands(program) {
56
58
  const client = await getClientAsync();
57
59
  const me = await client.user.me();
58
60
  out.stopSpinner();
59
- out.record({ id: me.id, email: me.email, name: me.name, role: me.role, createdAt: me.createdAt });
61
+ out.record({ id: me.id, email: me.email, displayName: me.displayName, role: me.role, createdAt: me.createdAt });
60
62
  }
61
63
  catch (err) {
62
64
  handleError(out, err, 'Failed to fetch profile');
@@ -65,15 +67,39 @@ export function registerUserCommands(program) {
65
67
  // user password
66
68
  addGlobalFlags(user.command('password')
67
69
  .description('Change your password')
68
- .requiredOption('--current <pwd>', 'Current password')
69
- .requiredOption('--new <pwd>', 'New password'))
70
+ .option('--password-stdin', 'Read current and new passwords from stdin (one per line) for CI usage'))
70
71
  .action(async (_opts) => {
71
72
  const flags = resolveFlags(_opts);
72
73
  const out = createOutput(flags);
74
+ let currentPassword;
75
+ let newPassword;
76
+ if (_opts.passwordStdin) {
77
+ currentPassword = await readPasswordFromStdin();
78
+ newPassword = await readPasswordFromStdin();
79
+ }
80
+ else {
81
+ currentPassword = await promptPassword('Current password: ');
82
+ if (!currentPassword) {
83
+ out.error('Current password is required.');
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ newPassword = await promptPassword('New password: ');
88
+ }
89
+ if (!currentPassword) {
90
+ out.error('Current password is required.');
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+ if (!newPassword) {
95
+ out.error('New password is required.');
96
+ process.exitCode = 1;
97
+ return;
98
+ }
73
99
  out.startSpinner('Changing password...');
74
100
  try {
75
101
  const client = await getClientAsync();
76
- await client.user.changePassword({ currentPassword: _opts.current, newPassword: _opts.new });
102
+ await client.user.changePassword({ currentPassword, newPassword });
77
103
  out.success('Password changed successfully', { changed: true });
78
104
  }
79
105
  catch (err) {
@@ -84,14 +110,26 @@ export function registerUserCommands(program) {
84
110
  addGlobalFlags(user.command('email')
85
111
  .description('Request email address change')
86
112
  .requiredOption('--new <email>', 'New email address')
87
- .requiredOption('--password <pwd>', 'Current password'))
113
+ .option('--password-stdin', 'Read password from stdin for CI usage'))
88
114
  .action(async (_opts) => {
89
115
  const flags = resolveFlags(_opts);
90
116
  const out = createOutput(flags);
117
+ let password;
118
+ if (_opts.passwordStdin) {
119
+ password = await readPasswordFromStdin();
120
+ }
121
+ else {
122
+ password = await promptPassword('Password: ');
123
+ }
124
+ if (!password) {
125
+ out.error('Password is required to change your email address.');
126
+ process.exitCode = 1;
127
+ return;
128
+ }
91
129
  out.startSpinner('Requesting email change...');
92
130
  try {
93
131
  const client = await getClientAsync();
94
- const result = await client.user.requestEmailChange({ newEmail: _opts.new, password: _opts.password });
132
+ const result = await client.user.requestEmailChange({ newEmail: _opts.new, password });
95
133
  out.success(result.message, { message: result.message });
96
134
  }
97
135
  catch (err) {
@@ -126,7 +164,7 @@ export function registerUserCommands(program) {
126
164
  out.startSpinner('Updating profile...');
127
165
  try {
128
166
  const client = await getClientAsync();
129
- const result = await client.user.updateProfile({ name: _opts.name, slug: _opts.slug });
167
+ const result = await client.user.updateProfile({ displayName: _opts.name, profileSlug: _opts.slug });
130
168
  out.success(result.message, { message: result.message });
131
169
  }
132
170
  catch (err) {
@@ -136,17 +174,29 @@ export function registerUserCommands(program) {
136
174
  // user delete
137
175
  addGlobalFlags(user.command('delete')
138
176
  .description('Request account deletion')
139
- .requiredOption('--password <pwd>', 'Current password')
177
+ .option('--password-stdin', 'Read password from stdin for CI usage')
140
178
  .option('--reason <reason>', 'Reason for deletion')
141
179
  .option('--export-data', 'Request data export before deletion'))
142
180
  .action(async (_opts) => {
143
181
  const flags = resolveFlags(_opts);
144
182
  const out = createOutput(flags);
183
+ let password;
184
+ if (_opts.passwordStdin) {
185
+ password = await readPasswordFromStdin();
186
+ }
187
+ else {
188
+ password = await promptPassword('Password: ');
189
+ }
190
+ if (!password) {
191
+ out.error('Password is required to delete your account.');
192
+ process.exitCode = 1;
193
+ return;
194
+ }
145
195
  out.startSpinner('Requesting account deletion...');
146
196
  try {
147
197
  const client = await getClientAsync();
148
198
  const result = await client.user.requestAccountDeletion({
149
- password: _opts.password,
199
+ password,
150
200
  reason: _opts.reason,
151
201
  exportData: _opts.exportData === true,
152
202
  });
@@ -266,6 +316,69 @@ export function registerUserCommands(program) {
266
316
  handleError(out, err, 'Failed to fetch export status');
267
317
  }
268
318
  });
319
+ addGlobalFlags(userExport.command('list')
320
+ .description('List all data exports'))
321
+ .action(async (_opts) => {
322
+ const flags = resolveFlags(_opts);
323
+ const out = createOutput(flags);
324
+ out.startSpinner('Fetching data exports...');
325
+ try {
326
+ const client = await getClientAsync();
327
+ const exports = await client.user.listDataExports();
328
+ out.stopSpinner();
329
+ out.list(exports.map(e => ({
330
+ id: e.id,
331
+ status: e.status,
332
+ format: e.format,
333
+ createdAt: e.createdAt,
334
+ completedAt: e.completedAt ?? '',
335
+ })), {
336
+ emptyMessage: 'No data exports found.',
337
+ columns: [
338
+ { key: 'id', header: 'ID' },
339
+ { key: 'status', header: 'Status' },
340
+ { key: 'format', header: 'Format' },
341
+ { key: 'createdAt', header: 'Created' },
342
+ { key: 'completedAt', header: 'Completed' },
343
+ ],
344
+ textFn: (e) => `${chalk.cyan(String(e.id))} [${String(e.status)}] ${String(e.format)} — created ${String(e.createdAt)}`,
345
+ });
346
+ }
347
+ catch (err) {
348
+ handleError(out, err, 'Failed to fetch data exports');
349
+ }
350
+ });
351
+ addGlobalFlags(userExport.command('download')
352
+ .description('Download a completed data export')
353
+ .argument('<exportId>', 'Export ID')
354
+ .option('-f, --file <filepath>', 'File path to write the export to'))
355
+ .action(async (exportId, _opts) => {
356
+ const flags = resolveFlags(_opts);
357
+ const out = createOutput(flags);
358
+ const outputPath = _opts.file;
359
+ if (!outputPath && process.stdout.isTTY) {
360
+ process.stderr.write(chalk.yellow('Warning: binary export data would corrupt your terminal. Use -f <filepath> to save to a file.\n'));
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+ out.startSpinner('Downloading data export...');
365
+ try {
366
+ const client = await getClientAsync();
367
+ const blob = await client.user.downloadDataExport(exportId);
368
+ out.stopSpinner();
369
+ const buffer = Buffer.from(await blob.arrayBuffer());
370
+ if (outputPath) {
371
+ await writeFile(outputPath, buffer);
372
+ out.success(`Export saved to ${chalk.cyan(outputPath)}`, { exportId, path: outputPath });
373
+ }
374
+ else {
375
+ process.stdout.write(buffer);
376
+ }
377
+ }
378
+ catch (err) {
379
+ handleError(out, err, 'Failed to download data export');
380
+ }
381
+ });
269
382
  // user consents subgroup
270
383
  const consents = user.command('consents').description('Consent management');
271
384
  addGlobalFlags(consents.command('list')
@@ -3,7 +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 { generateVaultKey } from '@lifestreamdynamics/vault-sdk';
6
- import { createCredentialManager } from '../lib/credential-manager.js';
6
+ import { getCredentialManager } from '../config.js';
7
7
  export function registerVaultCommands(program) {
8
8
  const vaults = program.command('vaults').description('Create, list, and inspect document vaults');
9
9
  addGlobalFlags(vaults.command('list')
@@ -84,7 +84,7 @@ EXAMPLES
84
84
  });
85
85
  if (isEncrypted) {
86
86
  const key = generateVaultKey();
87
- const credManager = createCredentialManager();
87
+ const credManager = getCredentialManager();
88
88
  await credManager.saveVaultKey(vault.id, key);
89
89
  out.success(`Encrypted vault created: ${chalk.cyan(vault.name)} (${vault.slug})`, {
90
90
  id: vault.id,
@@ -93,10 +93,13 @@ EXAMPLES
93
93
  encrypted: true,
94
94
  vaultKey: key,
95
95
  });
96
- out.warn('IMPORTANT: Save this encryption key securely. If lost, your data cannot be recovered.');
97
- out.status(`Vault Key: ${chalk.green(key)}`);
98
- out.status(chalk.dim('The key has been saved to your credential store.'));
99
- out.status(chalk.dim('You can export it later with: lsvault vaults export-key ' + vault.id));
96
+ // Always write key warning to stderr even in JSON mode the caller must not miss this.
97
+ process.stderr.write(chalk.yellow('WARNING: Save this encryption key securely. If lost, your data cannot be recovered.\n'));
98
+ if (flags.output !== 'json') {
99
+ out.status(`Vault Key: ${chalk.green(key)}`);
100
+ out.status(chalk.dim('The key has been saved to your credential store.'));
101
+ out.status(chalk.dim('You can export it later with: lsvault vaults export-key ' + vault.id));
102
+ }
100
103
  out.warn('Encrypted vaults disable: full-text search, AI features, hooks, and webhooks.');
101
104
  }
102
105
  else {
@@ -118,7 +121,7 @@ EXAMPLES
118
121
  const flags = resolveFlags(_opts);
119
122
  const out = createOutput(flags);
120
123
  try {
121
- const credManager = createCredentialManager();
124
+ const credManager = getCredentialManager();
122
125
  const key = await credManager.getVaultKey(vaultId);
123
126
  if (!key) {
124
127
  out.error('No encryption key found for vault ' + vaultId);
@@ -146,7 +149,7 @@ EXAMPLES
146
149
  process.exitCode = 1;
147
150
  return;
148
151
  }
149
- const credManager = createCredentialManager();
152
+ const credManager = getCredentialManager();
150
153
  await credManager.saveVaultKey(vaultId, keyValue);
151
154
  out.success('Vault encryption key saved successfully.', { vaultId });
152
155
  }
@@ -343,6 +346,12 @@ EXAMPLES
343
346
  .action(async (vaultId, _opts) => {
344
347
  const flags = resolveFlags(_opts);
345
348
  const out = createOutput(flags);
349
+ // Require an explicit --require or --no-require flag to avoid silent side-effects.
350
+ if (_opts.require === undefined) {
351
+ out.error('You must pass --require or --no-require to set the MFA policy.');
352
+ process.exitCode = 1;
353
+ return;
354
+ }
346
355
  out.startSpinner('Updating MFA config...');
347
356
  try {
348
357
  const client = await getClientAsync();
@@ -119,10 +119,15 @@ export function registerWebhookCommands(program) {
119
119
  addGlobalFlags(webhooks.command('delete')
120
120
  .description('Delete a webhook')
121
121
  .argument('<vaultId>', 'Vault ID')
122
- .argument('<webhookId>', 'Webhook ID'))
122
+ .argument('<webhookId>', 'Webhook ID')
123
+ .option('-y, --yes', 'Skip confirmation prompt'))
123
124
  .action(async (vaultId, webhookId, _opts) => {
124
125
  const flags = resolveFlags(_opts);
125
126
  const out = createOutput(flags);
127
+ if (!_opts.yes) {
128
+ out.status(chalk.yellow(`Pass --yes to delete webhook ${webhookId}.`));
129
+ return;
130
+ }
126
131
  out.startSpinner('Deleting webhook...');
127
132
  try {
128
133
  const client = await getClientAsync();
package/dist/config.d.ts CHANGED
@@ -5,6 +5,8 @@ export interface CliConfig {
5
5
  apiKey?: string;
6
6
  accessToken?: string;
7
7
  refreshToken?: string;
8
+ /** Per-vault client-side encryption keys, keyed by vaultId */
9
+ vaultKeys?: Record<string, string>;
8
10
  }
9
11
  export declare function getCredentialManager(): CredentialManager;
10
12
  /**
package/dist/config.js CHANGED
@@ -71,19 +71,23 @@ export async function loadConfigAsync() {
71
71
  const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
72
72
  if (fileConfig.apiUrl && !config.apiUrl)
73
73
  config.apiUrl = fileConfig.apiUrl;
74
- if (fileConfig.apiKey && !config.apiKey)
74
+ if (fileConfig.apiKey && !config.apiKey) {
75
75
  config.apiKey = fileConfig.apiKey;
76
+ process.stderr.write('\x1b[33mWarning: API key loaded from plaintext config (~/.lsvault/config.json).\n' +
77
+ 'Run `lsvault auth migrate` to migrate to secure storage.\n' +
78
+ 'See: https://docs.lifestreamdynamics.com/migration-to-secure-storage\x1b[0m\n');
79
+ }
76
80
  }
77
81
  return config;
78
82
  }
79
83
  export function saveConfig(config) {
80
84
  if (!fs.existsSync(CONFIG_DIR)) {
81
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
85
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
82
86
  }
83
87
  let existing = {};
84
88
  if (fs.existsSync(CONFIG_FILE)) {
85
89
  existing = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
86
90
  }
87
91
  const merged = { ...existing, ...config };
88
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n');
92
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
89
93
  }
package/dist/index.js CHANGED
@@ -1,5 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
2
3
  import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ const require = createRequire(import.meta.url);
6
+ const pkg = require('../package.json');
7
+ // Apply --no-color / NO_COLOR at startup so it takes effect for all output,
8
+ // including messages printed before any action handler runs.
9
+ if (process.argv.includes('--no-color') || process.env.NO_COLOR !== undefined) {
10
+ chalk.level = 0;
11
+ }
3
12
  import { registerAuthCommands } from './commands/auth.js';
4
13
  import { registerMfaCommands } from './commands/mfa.js';
5
14
  import { registerVaultCommands } from './commands/vaults.js';
@@ -21,15 +30,20 @@ import { registerSyncCommands } from './commands/sync.js';
21
30
  import { registerVersionCommands } from './commands/versions.js';
22
31
  import { registerLinkCommands } from './commands/links.js';
23
32
  import { registerCalendarCommands } from './commands/calendar.js';
33
+ import { registerBookingCommands } from './commands/booking.js';
24
34
  import { registerAiCommands } from './commands/ai.js';
25
35
  import { registerAnalyticsCommands } from './commands/analytics.js';
26
36
  import { registerCustomDomainCommands } from './commands/custom-domains.js';
27
37
  import { registerPublishVaultCommands } from './commands/publish-vault.js';
38
+ import { registerSamlCommands } from './commands/saml.js';
39
+ import { registerScimCommands } from './commands/scim.js';
40
+ import { registerPluginCommands } from './commands/plugins.js';
41
+ import { registerCompletionCommands } from './commands/completion.js';
28
42
  const program = new Command();
29
43
  program
30
44
  .name('lsvault')
31
45
  .description('Lifestream Vault CLI - manage vaults, documents, and settings')
32
- .version('0.1.0')
46
+ .version(pkg.version)
33
47
  .addHelpText('after', `
34
48
  GETTING STARTED
35
49
  lsvault auth login --email <email> Log in with email/password
@@ -70,8 +84,13 @@ registerSyncCommands(program);
70
84
  registerVersionCommands(program);
71
85
  registerLinkCommands(program);
72
86
  registerCalendarCommands(program);
87
+ registerBookingCommands(program);
73
88
  registerAiCommands(program);
74
89
  registerAnalyticsCommands(program);
75
90
  registerCustomDomainCommands(program);
76
91
  registerPublishVaultCommands(program);
92
+ registerSamlCommands(program);
93
+ registerScimCommands(program);
94
+ registerPluginCommands(program);
95
+ registerCompletionCommands(program);
77
96
  program.parse();
@@ -1,13 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import os from 'node:os';
1
5
  import { createKeychainBackend } from './keychain.js';
2
6
  import { createEncryptedConfigBackend } from './encrypted-config.js';
3
- // Default passphrase for encrypted config when no interactive prompt is available.
4
- // This provides basic obfuscation the real security benefit is file permissions (0600)
5
- // and the fact that credentials aren't in a JSON file that's easy to grep for API keys.
6
- const DEFAULT_PASSPHRASE = 'lsvault-cli-local-encryption-key';
7
+ /**
8
+ * Returns the machine-specific passphrase used to encrypt the local credential
9
+ * store. On first call it generates a cryptographically random 32-byte hex
10
+ * string, writes it to `~/.lsvault/.passphrase` (mode 0o600, directory 0o700),
11
+ * and returns it. Subsequent calls read the stored value.
12
+ *
13
+ * This replaces the old hardcoded `DEFAULT_PASSPHRASE` constant so that the
14
+ * passphrase is never present in source code or version control.
15
+ */
16
+ function getOrCreatePassphrase() {
17
+ const configDir = path.join(os.homedir(), '.lsvault');
18
+ const passphrasePath = path.join(configDir, '.passphrase');
19
+ try {
20
+ return fs.readFileSync(passphrasePath, 'utf-8').trim();
21
+ }
22
+ catch {
23
+ // File does not exist yet — generate a new one.
24
+ const passphrase = crypto.randomBytes(32).toString('hex');
25
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
26
+ fs.writeFileSync(passphrasePath, passphrase, { mode: 0o600 });
27
+ return passphrase;
28
+ }
29
+ }
7
30
  export function createCredentialManager(options = {}) {
8
31
  const keychain = options.keychain ?? createKeychainBackend();
9
32
  const encryptedConfig = options.encryptedConfig ?? createEncryptedConfigBackend();
10
- const passphrase = options.passphrase ?? DEFAULT_PASSPHRASE;
33
+ // Use the caller-supplied passphrase (tests pass one explicitly) or lazily
34
+ // generate / load the per-machine passphrase from ~/.lsvault/.passphrase.
35
+ const passphrase = options.passphrase ?? getOrCreatePassphrase();
11
36
  return {
12
37
  async getCredentials() {
13
38
  const result = {};
@@ -87,13 +112,13 @@ export function createCredentialManager(options = {}) {
87
112
  async saveVaultKey(vaultId, keyHex) {
88
113
  // Read existing config, merge vault key, save
89
114
  const existing = encryptedConfig.getCredentials(passphrase) ?? {};
90
- const vaultKeys = existing.vaultKeys ?? {};
115
+ const vaultKeys = { ...(existing.vaultKeys ?? {}) };
91
116
  vaultKeys[vaultId] = keyHex;
92
117
  encryptedConfig.saveCredentials({ ...existing, vaultKeys }, passphrase);
93
118
  },
94
119
  async deleteVaultKey(vaultId) {
95
120
  const existing = encryptedConfig.getCredentials(passphrase) ?? {};
96
- const vaultKeys = existing.vaultKeys ?? {};
121
+ const vaultKeys = { ...(existing.vaultKeys ?? {}) };
97
122
  delete vaultKeys[vaultId];
98
123
  encryptedConfig.saveCredentials({ ...existing, vaultKeys }, passphrase);
99
124
  },
@@ -85,8 +85,8 @@ export async function migrateCredentials(credentialManager, promptFn) {
85
85
  export async function checkAndPromptMigration(credentialManager) {
86
86
  if (!hasPlaintextCredentials())
87
87
  return false;
88
- console.log(chalk.yellow('\nWarning: API key found in plaintext config (~/.lsvault/config.json)'));
89
- console.log(chalk.yellow('Run `lsvault auth migrate` to migrate to secure storage.\n'));
88
+ process.stderr.write(chalk.yellow('\nWarning: API key found in plaintext config (~/.lsvault/config.json)') + '\n');
89
+ process.stderr.write(chalk.yellow('Run `lsvault auth migrate` to migrate to secure storage.\n') + '\n');
90
90
  return false;
91
91
  }
92
92
  export { CONFIG_FILE, CONFIG_DIR };