@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.
- package/README.md +140 -30
- package/dist/client.d.ts +4 -0
- package/dist/client.js +12 -11
- package/dist/commands/admin.js +5 -5
- package/dist/commands/ai.js +17 -4
- package/dist/commands/auth.js +10 -105
- package/dist/commands/booking.d.ts +2 -0
- package/dist/commands/booking.js +739 -0
- package/dist/commands/calendar.js +725 -6
- package/dist/commands/completion.d.ts +5 -0
- package/dist/commands/completion.js +60 -0
- package/dist/commands/config.js +17 -16
- package/dist/commands/connectors.js +12 -1
- package/dist/commands/custom-domains.js +6 -1
- package/dist/commands/docs.js +12 -5
- package/dist/commands/hooks.js +6 -1
- package/dist/commands/links.js +9 -2
- package/dist/commands/mfa.js +1 -70
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +172 -0
- package/dist/commands/publish.js +13 -3
- package/dist/commands/saml.d.ts +2 -0
- package/dist/commands/saml.js +220 -0
- package/dist/commands/scim.d.ts +2 -0
- package/dist/commands/scim.js +238 -0
- package/dist/commands/shares.js +25 -3
- package/dist/commands/subscription.js +9 -2
- package/dist/commands/sync.js +3 -0
- package/dist/commands/teams.js +141 -8
- package/dist/commands/user.js +122 -9
- package/dist/commands/vaults.js +17 -8
- package/dist/commands/webhooks.js +6 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -3
- package/dist/index.js +20 -1
- package/dist/lib/credential-manager.js +32 -7
- package/dist/lib/migration.js +2 -2
- package/dist/lib/profiles.js +4 -4
- package/dist/sync/config.js +2 -2
- package/dist/sync/daemon-worker.js +13 -6
- package/dist/sync/daemon.js +2 -1
- package/dist/sync/remote-poller.js +7 -3
- package/dist/sync/state.js +2 -2
- package/dist/utils/confirm.d.ts +11 -0
- package/dist/utils/confirm.js +23 -0
- package/dist/utils/format.js +1 -1
- package/dist/utils/output.js +4 -1
- package/dist/utils/prompt.d.ts +29 -0
- package/dist/utils/prompt.js +146 -0
- package/package.json +2 -2
package/dist/commands/teams.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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.
|
|
347
|
-
process.stdout.write(`${chalk.cyan(date)}: ${day.
|
|
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.
|
|
378
|
-
process.stdout.write(`${chalk.cyan(day.date)}: ${day.
|
|
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
|
}
|
package/dist/commands/user.js
CHANGED
|
@@ -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,
|
|
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
|
-
.
|
|
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
|
|
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
|
-
.
|
|
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
|
|
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({
|
|
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
|
-
.
|
|
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
|
|
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')
|
package/dist/commands/vaults.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
},
|
package/dist/lib/migration.js
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
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 };
|