@lifestreamdynamics/vault-cli 1.3.7 → 1.3.9

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.
@@ -1,158 +1,208 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
3
2
  import { getClientAsync } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
4
5
  import { promptPassword, promptMfaCode } from '../utils/prompt.js';
5
6
  export function registerMfaCommands(program) {
6
7
  const mfa = program.command('mfa')
7
8
  .description('Multi-factor authentication management (requires JWT auth — use "lsvault auth login" first)')
8
9
  .addHelpText('after', '\nNOTE: MFA management requires JWT authentication. API key auth is not sufficient.\nRun "lsvault auth login" to authenticate with email/password first.');
9
- mfa.command('status')
10
- .description('Show MFA status and configured methods')
11
- .action(async () => {
12
- const spinner = ora('Fetching MFA status...').start();
10
+ addGlobalFlags(mfa.command('status')
11
+ .description('Show MFA status and configured methods'))
12
+ .action(async (_opts) => {
13
+ const flags = resolveFlags(_opts);
14
+ const out = createOutput(flags);
15
+ out.startSpinner('Fetching MFA status...');
13
16
  try {
14
17
  const client = await getClientAsync();
15
18
  const status = await client.mfa.getStatus();
16
- spinner.stop();
17
- console.log(chalk.bold('MFA Status'));
18
- console.log(` Enabled: ${status.mfaEnabled ? chalk.green('Yes') : chalk.dim('No')}`);
19
- console.log(` TOTP Configured: ${status.totpConfigured ? chalk.green('Yes') : chalk.dim('No')}`);
20
- console.log(` Passkeys Registered: ${status.passkeyCount > 0 ? chalk.cyan(status.passkeyCount) : chalk.dim('0')}`);
21
- console.log(` Backup Codes Left: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}`);
22
- if (status.passkeys.length > 0) {
23
- console.log('');
24
- console.log(chalk.bold('Registered Passkeys:'));
25
- for (const passkey of status.passkeys) {
26
- const lastUsed = passkey.lastUsedAt
27
- ? new Date(passkey.lastUsedAt).toLocaleDateString()
28
- : chalk.dim('never');
29
- console.log(` - ${chalk.cyan(passkey.name)} (last used: ${lastUsed})`);
19
+ out.stopSpinner();
20
+ if (flags.output === 'json') {
21
+ out.record({
22
+ mfaEnabled: status.mfaEnabled,
23
+ totpConfigured: status.totpConfigured,
24
+ passkeyCount: status.passkeyCount,
25
+ backupCodesRemaining: status.backupCodesRemaining,
26
+ passkeys: status.passkeys,
27
+ });
28
+ }
29
+ else {
30
+ out.raw(chalk.bold('MFA Status') + '\n');
31
+ out.raw(` Enabled: ${status.mfaEnabled ? chalk.green('Yes') : chalk.dim('No')}\n`);
32
+ out.raw(` TOTP Configured: ${status.totpConfigured ? chalk.green('Yes') : chalk.dim('No')}\n`);
33
+ out.raw(` Passkeys Registered: ${status.passkeyCount > 0 ? chalk.cyan(status.passkeyCount) : chalk.dim('0')}\n`);
34
+ out.raw(` Backup Codes Left: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}\n`);
35
+ if (status.passkeys.length > 0) {
36
+ out.raw('\n');
37
+ out.raw(chalk.bold('Registered Passkeys:') + '\n');
38
+ for (const passkey of status.passkeys) {
39
+ const lastUsed = passkey.lastUsedAt
40
+ ? new Date(passkey.lastUsedAt).toLocaleDateString()
41
+ : chalk.dim('never');
42
+ out.raw(` - ${chalk.cyan(passkey.name)} (last used: ${lastUsed})\n`);
43
+ }
30
44
  }
31
45
  }
32
46
  }
33
47
  catch (err) {
34
- spinner.fail('Failed to fetch MFA status');
35
- console.error(err instanceof Error ? err.message : String(err));
48
+ out.failSpinner('Failed to fetch MFA status');
49
+ handleError(out, err, 'MFA management requires JWT authentication');
36
50
  }
37
51
  });
38
- mfa.command('setup-totp')
39
- .description('Set up TOTP authenticator app (Google Authenticator, Authy, etc.)')
40
- .action(async () => {
41
- const spinner = ora('Generating TOTP secret...').start();
52
+ addGlobalFlags(mfa.command('setup-totp')
53
+ .description('Set up TOTP authenticator app (Google Authenticator, Authy, etc.)'))
54
+ .action(async (_opts) => {
55
+ const flags = resolveFlags(_opts);
56
+ const out = createOutput(flags);
57
+ out.startSpinner('Generating TOTP secret...');
42
58
  try {
43
59
  const client = await getClientAsync();
44
60
  const setup = await client.mfa.setupTotp();
45
- spinner.stop();
46
- console.log(chalk.bold('TOTP Setup'));
47
- console.log('');
48
- console.log(`Secret: ${chalk.cyan(setup.secret)}`);
49
- console.log('');
50
- console.log('Add this URI to your authenticator app:');
51
- console.log(chalk.dim(setup.otpauthUri));
52
- console.log('');
53
- console.log(chalk.yellow('Note: QR codes cannot be displayed in the terminal.'));
54
- console.log(chalk.yellow(' Copy the URI above to any authenticator app that supports otpauth:// URIs.'));
55
- console.log('');
56
- // Prompt for verification code
61
+ out.stopSpinner();
62
+ if (flags.output === 'json') {
63
+ out.record({ secret: setup.secret, otpauthUri: setup.otpauthUri });
64
+ }
65
+ else {
66
+ out.raw(chalk.bold('TOTP Setup') + '\n');
67
+ out.raw('\n');
68
+ out.raw(`Secret: ${chalk.cyan(setup.secret)}\n`);
69
+ out.raw('\n');
70
+ out.raw('Add this URI to your authenticator app:\n');
71
+ out.raw(chalk.dim(setup.otpauthUri) + '\n');
72
+ out.raw('\n');
73
+ out.raw(chalk.yellow('Note: QR codes cannot be displayed in the terminal.') + '\n');
74
+ out.raw(chalk.yellow(' Copy the URI above to any authenticator app that supports otpauth:// URIs.') + '\n');
75
+ out.raw('\n');
76
+ }
77
+ // Prompt for verification code — skip in quiet mode
78
+ if (flags.quiet)
79
+ return;
57
80
  const code = await promptMfaCode();
58
81
  if (!code) {
59
- console.log(chalk.yellow('Setup cancelled.'));
82
+ out.status(chalk.yellow('Setup cancelled.'));
60
83
  return;
61
84
  }
62
- const verifySpinner = ora('Verifying code and enabling TOTP...').start();
85
+ out.startSpinner('Verifying code and enabling TOTP...');
63
86
  const result = await client.mfa.verifyTotp(code);
64
- verifySpinner.succeed('TOTP enabled successfully!');
65
- console.log('');
66
- console.log(chalk.bold.yellow('IMPORTANT: Save these backup codes securely!'));
67
- console.log(chalk.dim('You can use them to access your account if you lose your authenticator device.'));
68
- console.log('');
69
- // Display backup codes in a grid (2 columns)
70
- const codes = result.backupCodes;
71
- for (let i = 0; i < codes.length; i += 2) {
72
- const left = codes[i] || '';
73
- const right = codes[i + 1] || '';
74
- console.log(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}`);
87
+ out.stopSpinner();
88
+ out.success('TOTP enabled successfully!');
89
+ if (flags.output === 'json') {
90
+ out.list(result.backupCodes.map((code) => ({ code })), { emptyMessage: 'No backup codes returned.' });
91
+ }
92
+ else {
93
+ out.raw('\n');
94
+ out.raw(chalk.bold.yellow('IMPORTANT: Save these backup codes securely!') + '\n');
95
+ out.raw(chalk.dim('You can use them to access your account if you lose your authenticator device.') + '\n');
96
+ out.raw('\n');
97
+ // Display backup codes in a grid (2 columns)
98
+ const codes = result.backupCodes;
99
+ for (let i = 0; i < codes.length; i += 2) {
100
+ const left = codes[i] || '';
101
+ const right = codes[i + 1] || '';
102
+ out.raw(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}\n`);
103
+ }
104
+ out.raw('\n');
75
105
  }
76
- console.log('');
77
106
  }
78
107
  catch (err) {
79
- spinner.fail('TOTP setup failed');
80
- console.error(err instanceof Error ? err.message : String(err));
108
+ handleError(out, err, 'TOTP setup failed');
81
109
  }
82
110
  });
83
- mfa.command('disable-totp')
84
- .description('Disable TOTP authentication (requires password)')
85
- .action(async () => {
111
+ addGlobalFlags(mfa.command('disable-totp')
112
+ .description('Disable TOTP authentication (requires password)'))
113
+ .action(async (_opts) => {
114
+ const flags = resolveFlags(_opts);
115
+ const out = createOutput(flags);
116
+ // Skip interactive prompt in quiet mode
117
+ if (flags.quiet) {
118
+ out.error('Password prompt required — cannot run in quiet mode without --password-stdin.');
119
+ process.exitCode = 1;
120
+ return;
121
+ }
86
122
  const password = await promptPassword();
87
123
  if (!password) {
88
- console.log(chalk.yellow('Operation cancelled.'));
124
+ out.status(chalk.yellow('Operation cancelled.'));
89
125
  return;
90
126
  }
91
- const spinner = ora('Disabling TOTP...').start();
127
+ out.startSpinner('Disabling TOTP...');
92
128
  try {
93
129
  const client = await getClientAsync();
94
130
  const result = await client.mfa.disableTotp(password);
95
- spinner.succeed(result.message);
131
+ out.stopSpinner();
132
+ out.success(result.message, { message: result.message });
96
133
  }
97
134
  catch (err) {
98
- spinner.fail('Failed to disable TOTP');
99
- console.error(err instanceof Error ? err.message : String(err));
135
+ handleError(out, err, 'Failed to disable TOTP');
100
136
  }
101
137
  });
102
- mfa.command('backup-codes')
138
+ addGlobalFlags(mfa.command('backup-codes')
103
139
  .description('Show remaining backup code count or regenerate codes')
104
- .option('--regenerate', 'Generate new backup codes (requires password, invalidates old codes)')
105
- .action(async (opts) => {
106
- if (opts.regenerate) {
140
+ .option('--regenerate', 'Generate new backup codes (requires password, invalidates old codes)'))
141
+ .action(async (_opts) => {
142
+ const flags = resolveFlags(_opts);
143
+ const out = createOutput(flags);
144
+ if (_opts.regenerate) {
107
145
  // Regenerate backup codes
146
+ if (flags.quiet) {
147
+ out.error('Password prompt required — cannot run in quiet mode without --password-stdin.');
148
+ process.exitCode = 1;
149
+ return;
150
+ }
108
151
  const password = await promptPassword();
109
152
  if (!password) {
110
- console.log(chalk.yellow('Operation cancelled.'));
153
+ out.status(chalk.yellow('Operation cancelled.'));
111
154
  return;
112
155
  }
113
- const spinner = ora('Regenerating backup codes...').start();
156
+ out.startSpinner('Regenerating backup codes...');
114
157
  try {
115
158
  const client = await getClientAsync();
116
159
  const result = await client.mfa.regenerateBackupCodes(password);
117
- spinner.succeed('Backup codes regenerated!');
118
- console.log('');
119
- console.log(chalk.bold.yellow('IMPORTANT: Save these new backup codes securely!'));
120
- console.log(chalk.dim('All previous backup codes have been invalidated.'));
121
- console.log('');
122
- // Display backup codes in a grid (2 columns)
123
- const codes = result.backupCodes;
124
- for (let i = 0; i < codes.length; i += 2) {
125
- const left = codes[i] || '';
126
- const right = codes[i + 1] || '';
127
- console.log(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}`);
160
+ out.stopSpinner();
161
+ out.success('Backup codes regenerated!');
162
+ if (flags.output === 'json') {
163
+ out.list(result.backupCodes.map((code) => ({ code })), { emptyMessage: 'No backup codes returned.' });
164
+ }
165
+ else {
166
+ out.raw('\n');
167
+ out.raw(chalk.bold.yellow('IMPORTANT: Save these new backup codes securely!') + '\n');
168
+ out.raw(chalk.dim('All previous backup codes have been invalidated.') + '\n');
169
+ out.raw('\n');
170
+ // Display backup codes in a grid (2 columns)
171
+ const codes = result.backupCodes;
172
+ for (let i = 0; i < codes.length; i += 2) {
173
+ const left = codes[i] || '';
174
+ const right = codes[i + 1] || '';
175
+ out.raw(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}\n`);
176
+ }
177
+ out.raw('\n');
128
178
  }
129
- console.log('');
130
179
  }
131
180
  catch (err) {
132
- spinner.fail('Failed to regenerate backup codes');
133
- console.error(err instanceof Error ? err.message : String(err));
134
- process.exitCode = 1;
181
+ handleError(out, err, 'Failed to regenerate backup codes');
135
182
  }
136
183
  }
137
184
  else {
138
185
  // Show backup code count
139
- const spinner = ora('Fetching backup code count...').start();
186
+ out.startSpinner('Fetching backup code count...');
140
187
  try {
141
188
  const client = await getClientAsync();
142
189
  const status = await client.mfa.getStatus();
143
- spinner.stop();
144
- console.log(chalk.bold('Backup Codes'));
145
- console.log(` Remaining: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}`);
146
- if (status.backupCodesRemaining === 0) {
147
- console.log('');
148
- console.log(chalk.yellow('You have no backup codes remaining.'));
149
- console.log(chalk.yellow('Run `lsvault mfa backup-codes --regenerate` to generate new codes.'));
190
+ out.stopSpinner();
191
+ if (flags.output === 'json') {
192
+ out.record({ backupCodesRemaining: status.backupCodesRemaining });
193
+ }
194
+ else {
195
+ out.raw(chalk.bold('Backup Codes') + '\n');
196
+ out.raw(` Remaining: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}\n`);
197
+ if (status.backupCodesRemaining === 0) {
198
+ out.raw('\n');
199
+ out.raw(chalk.yellow('You have no backup codes remaining.') + '\n');
200
+ out.raw(chalk.yellow('Run `lsvault mfa backup-codes --regenerate` to generate new codes.') + '\n');
201
+ }
150
202
  }
151
203
  }
152
204
  catch (err) {
153
- spinner.fail('Failed to fetch backup code count');
154
- console.error(err instanceof Error ? err.message : String(err));
155
- process.exitCode = 1;
205
+ handleError(out, err, 'Failed to fetch backup code count');
156
206
  }
157
207
  }
158
208
  });
@@ -44,7 +44,7 @@ export function registerPluginCommands(program) {
44
44
  addGlobalFlags(plugins.command('install')
45
45
  .description('Install a plugin from the marketplace')
46
46
  .requiredOption('--plugin-id <pluginId>', 'Plugin marketplace identifier (e.g. org/plugin-name)')
47
- .requiredOption('--version <version>', 'Version to install'))
47
+ .requiredOption('--plugin-version <version>', 'Version to install'))
48
48
  .action(async (_opts) => {
49
49
  const flags = resolveFlags(_opts);
50
50
  const out = createOutput(flags);
@@ -53,7 +53,7 @@ export function registerPluginCommands(program) {
53
53
  const client = await getClientAsync();
54
54
  const installed = await client.plugins.install({
55
55
  pluginId: _opts.pluginId,
56
- version: _opts.version,
56
+ version: _opts.pluginVersion,
57
57
  });
58
58
  out.stopSpinner();
59
59
  if (flags.output === 'json') {
@@ -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 { confirmAction } from '../utils/confirm.js';
5
6
  export function registerPublishVaultCommands(program) {
6
7
  const pv = program.command('publish-vault').description('Manage whole-vault publishing (public sites)');
7
8
  addGlobalFlags(pv.command('list')
@@ -37,7 +38,7 @@ export function registerPublishVaultCommands(program) {
37
38
  .option('--description <desc>', 'Site description')
38
39
  .option('--show-sidebar', 'Show sidebar navigation')
39
40
  .option('--enable-search', 'Enable search on the site')
40
- .option('--theme <theme>', 'Site theme')
41
+ .option('--theme <theme>', 'Site theme (e.g. default, minimal, blog, docs)')
41
42
  .option('--domain <domainId>', 'Custom domain ID'))
42
43
  .action(async (vaultId, _opts) => {
43
44
  const flags = resolveFlags(_opts);
@@ -68,7 +69,7 @@ export function registerPublishVaultCommands(program) {
68
69
  .option('--description <desc>', 'Site description')
69
70
  .option('--show-sidebar', 'Show sidebar')
70
71
  .option('--enable-search', 'Enable search')
71
- .option('--theme <theme>', 'Site theme')
72
+ .option('--theme <theme>', 'Site theme (e.g. default, minimal, blog, docs)')
72
73
  .option('--domain <domainId>', 'Custom domain ID'))
73
74
  .action(async (vaultId, _opts) => {
74
75
  const flags = resolveFlags(_opts);
@@ -100,12 +101,18 @@ export function registerPublishVaultCommands(program) {
100
101
  });
101
102
  addGlobalFlags(pv.command('unpublish')
102
103
  .description('Unpublish a vault site')
103
- .argument('<vaultId>', 'Vault ID'))
104
+ .argument('<vaultId>', 'Vault ID')
105
+ .option('-y, --yes', 'Skip confirmation prompt'))
104
106
  .action(async (vaultId, _opts) => {
105
107
  const flags = resolveFlags(_opts);
106
108
  const out = createOutput(flags);
107
- out.startSpinner('Unpublishing vault...');
108
109
  try {
110
+ const confirmed = await confirmAction(`Unpublish vault site for vault ${vaultId}?`, { yes: _opts.yes });
111
+ if (!confirmed) {
112
+ out.status('Unpublish cancelled.');
113
+ return;
114
+ }
115
+ out.startSpinner('Unpublishing vault...');
109
116
  const client = await getClientAsync();
110
117
  await client.publishVault.unpublish(vaultId);
111
118
  out.success('Vault unpublished', { vaultId });
@@ -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 { resolveVaultId } from '../utils/resolve-vault.js';
6
+ import { loadConfigAsync } from '../config.js';
6
7
  export function registerPublishCommands(program) {
7
8
  const publish = program.command('publish').description('Publish documents to public profile pages');
8
9
  addGlobalFlags(publish.command('list')
@@ -63,6 +64,7 @@ export function registerPublishCommands(program) {
63
64
  out.startSpinner('Publishing document...');
64
65
  try {
65
66
  vaultId = await resolveVaultId(vaultId);
67
+ out.debug(`API: POST publish ${vaultId}/${docPath}`);
66
68
  const client = await getClientAsync();
67
69
  const params = {
68
70
  slug: String(_opts.slug),
@@ -74,9 +76,11 @@ export function registerPublishCommands(program) {
74
76
  if (_opts.ogImage)
75
77
  params.ogImage = String(_opts.ogImage);
76
78
  const pub = await client.publish.create(vaultId, docPath, params);
79
+ const config = await loadConfigAsync();
80
+ const baseUrl = config.apiUrl.replace(/\/api\/v\d+\/?$/, '');
77
81
  out.success('Document published successfully!', {
78
82
  slug: pub.slug,
79
- url: `/${pub.publishedBy}/${pub.slug}`,
83
+ url: `${baseUrl}/${pub.publishedBy}/${pub.slug}`,
80
84
  isPublished: pub.isPublished,
81
85
  seoTitle: pub.seoTitle || null,
82
86
  seoDescription: pub.seoDescription || null,
@@ -3,7 +3,9 @@ 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
  export function registerSamlCommands(program) {
6
- const saml = program.command('saml').description('SAML SSO configuration management (requires admin role)');
6
+ const saml = program.command('saml')
7
+ .description('SAML SSO configuration management (requires admin role)')
8
+ .addHelpText('after', '\nNOTE: SAML commands require JWT authentication with admin role.\nRun "lsvault auth login" to authenticate first.');
7
9
  // ── list-configs ─────────────────────────────────────────────────────────
8
10
  addGlobalFlags(saml.command('list-configs')
9
11
  .description('List all SSO configurations'))
@@ -154,12 +156,14 @@ export function registerSamlCommands(program) {
154
156
  addGlobalFlags(saml.command('delete-config')
155
157
  .description('Delete an SSO configuration')
156
158
  .argument('<id>', 'SSO config ID')
157
- .option('--force', 'Skip confirmation prompt'))
159
+ .option('--force', 'Skip confirmation prompt')
160
+ .option('-y, --yes', 'Alias for --force'))
158
161
  .action(async (id, _opts) => {
159
162
  const flags = resolveFlags(_opts);
160
163
  const out = createOutput(flags);
161
- if (!_opts.force) {
162
- out.raw(chalk.yellow(`Pass --force to delete SSO config ${id}.`) + '\n');
164
+ if (!_opts.force && !_opts.yes) {
165
+ out.warn(`Pass --force to delete SSO config ${id}.`);
166
+ process.exitCode = 1;
163
167
  return;
164
168
  }
165
169
  out.startSpinner('Deleting SSO config...');
@@ -203,6 +207,11 @@ export function registerSamlCommands(program) {
203
207
  .action(async (slug, _opts) => {
204
208
  const flags = resolveFlags(_opts);
205
209
  const out = createOutput(flags);
210
+ if (!slug || !slug.trim()) {
211
+ out.error('Slug cannot be empty.');
212
+ process.exitCode = 1;
213
+ return;
214
+ }
206
215
  try {
207
216
  const client = await getClientAsync();
208
217
  const url = client.saml.getLoginUrl(slug);
@@ -16,7 +16,11 @@ EXAMPLES
16
16
  lsvault search "meeting notes"
17
17
  lsvault search "project plan" --vault abc123
18
18
  lsvault search "typescript" --tags dev,code --limit 5
19
- lsvault search "machine learning" --mode semantic`))
19
+ lsvault search "machine learning" --mode semantic
20
+
21
+ NOTE
22
+ Semantic search (--mode semantic) requires the embedding worker to have
23
+ processed documents. If results are empty, ensure the worker is running.`))
20
24
  .action(async (query, _opts) => {
21
25
  const flags = resolveFlags(_opts);
22
26
  const out = createOutput(flags);
@@ -5,6 +5,7 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
5
5
  import { createOutput, handleError } from '../utils/output.js';
6
6
  import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
7
7
  import { resolveVaultId } from '../utils/resolve-vault.js';
8
+ import { confirmAction } from '../utils/confirm.js';
8
9
  export function registerShareCommands(program) {
9
10
  const shares = program.command('shares').description('Create, list, and revoke document share links');
10
11
  addGlobalFlags(shares.command('list')
@@ -134,12 +135,18 @@ export function registerShareCommands(program) {
134
135
  addGlobalFlags(shares.command('revoke')
135
136
  .description('Revoke a share link')
136
137
  .argument('<vaultId>', 'Vault ID or slug')
137
- .argument('<shareId>', 'Share link ID'))
138
+ .argument('<shareId>', 'Share link ID')
139
+ .option('-y, --yes', 'Skip confirmation prompt'))
138
140
  .action(async (vaultId, shareId, _opts) => {
139
141
  const flags = resolveFlags(_opts);
140
142
  const out = createOutput(flags);
141
- out.startSpinner('Revoking share link...');
142
143
  try {
144
+ const confirmed = await confirmAction(`Revoke share link ${shareId}?`, { yes: _opts.yes });
145
+ if (!confirmed) {
146
+ out.status('Revoke cancelled.');
147
+ return;
148
+ }
149
+ out.startSpinner('Revoking share link...');
143
150
  vaultId = await resolveVaultId(vaultId);
144
151
  const client = await getClientAsync();
145
152
  await client.shares.revoke(vaultId, shareId);
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { getClientAsync } from '../client.js';
5
5
  import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
6
6
  import { createOutput, handleError } from '../utils/output.js';
7
+ import { confirmAction } from '../utils/confirm.js';
7
8
  import { formatUptime } from '../utils/format.js';
8
9
  import { loadSyncConfigs, createSyncConfig, deleteSyncConfig, getSyncConfig, } from '../sync/config.js';
9
10
  import { deleteSyncState, loadSyncState, saveSyncState, hashFileContent, buildRemoteFileState } from '../sync/state.js';
@@ -124,12 +125,18 @@ Sync modes:
124
125
  // sync delete <syncId>
125
126
  addGlobalFlags(sync.command('delete')
126
127
  .description('Delete a sync configuration')
127
- .argument('<syncId>', 'Sync configuration ID'))
128
+ .argument('<syncId>', 'Sync configuration ID')
129
+ .option('-y, --yes', 'Skip confirmation prompt'))
128
130
  .action(async (syncId, _opts) => {
129
131
  const flags = resolveFlags(_opts);
130
132
  const out = createOutput(flags);
131
- out.startSpinner('Deleting sync configuration...');
132
133
  try {
134
+ const confirmed = await confirmAction(`Delete sync configuration ${syncId}?`, { yes: _opts.yes });
135
+ if (!confirmed) {
136
+ out.status('Delete cancelled.');
137
+ return;
138
+ }
139
+ out.startSpinner('Deleting sync configuration...');
133
140
  const deleted = deleteSyncConfig(syncId);
134
141
  if (!deleted) {
135
142
  out.failSpinner('Sync configuration not found');
@@ -274,8 +281,6 @@ Sync modes:
274
281
  }
275
282
  out.stopSpinner();
276
283
  if (flags.dryRun) {
277
- out.status(chalk.yellow('Dry run — no changes will be made:'));
278
- out.status(formatDiff(diff));
279
284
  if (flags.output === 'json') {
280
285
  out.record({
281
286
  dryRun: true,
@@ -285,6 +290,10 @@ Sync modes:
285
290
  totalBytes: diff.totalBytes,
286
291
  });
287
292
  }
293
+ else {
294
+ out.status(chalk.yellow('Dry run — no changes will be made:'));
295
+ out.status(formatDiff(diff));
296
+ }
288
297
  return;
289
298
  }
290
299
  if (flags.verbose) {
@@ -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 { confirmAction } from '../utils/confirm.js';
5
6
  export function registerTeamCommands(program) {
6
7
  const teams = program.command('teams').description('Manage teams, members, invitations, and shared vaults');
7
8
  // ── Team CRUD ──────────────────────────────────────────────────────
@@ -12,8 +13,10 @@ export function registerTeamCommands(program) {
12
13
  const out = createOutput(flags);
13
14
  out.startSpinner('Fetching teams...');
14
15
  try {
16
+ out.debug('API: GET teams');
15
17
  const client = await getClientAsync();
16
18
  const teamList = await client.teams.list();
19
+ out.debug(`Response: ${teamList.length} teams`);
17
20
  out.stopSpinner();
18
21
  out.list(teamList.map(t => ({ name: t.name, id: t.id, description: t.description || 'No description' })), {
19
22
  emptyMessage: 'No teams found.',
@@ -152,7 +155,7 @@ EXAMPLES
152
155
  .description('Update a member role')
153
156
  .argument('<teamId>', 'Team ID')
154
157
  .argument('<userId>', 'User ID')
155
- .requiredOption('-r, --role <role>', 'New role (admin or member)'))
158
+ .requiredOption('-r, --role <role>', 'New role: admin, editor, or viewer'))
156
159
  .action(async (teamId, userId, _opts) => {
157
160
  const flags = resolveFlags(_opts);
158
161
  const out = createOutput(flags);
@@ -195,10 +198,16 @@ EXAMPLES
195
198
  });
196
199
  addGlobalFlags(teams.command('leave')
197
200
  .description('Leave a team')
198
- .argument('<teamId>', 'Team ID'))
201
+ .argument('<teamId>', 'Team ID')
202
+ .option('-y, --yes', 'Skip confirmation prompt'))
199
203
  .action(async (teamId, _opts) => {
200
204
  const flags = resolveFlags(_opts);
201
205
  const out = createOutput(flags);
206
+ const confirmed = await confirmAction(`Are you sure you want to leave team ${teamId}?`, { yes: !!_opts.yes });
207
+ if (!confirmed) {
208
+ out.status('Cancelled.');
209
+ return;
210
+ }
202
211
  out.startSpinner('Leaving team...');
203
212
  try {
204
213
  const client = await getClientAsync();
@@ -245,11 +254,11 @@ EXAMPLES
245
254
  .description('Invite a user to the team')
246
255
  .argument('<teamId>', 'Team ID')
247
256
  .argument('<email>', 'Email address')
248
- .requiredOption('-r, --role <role>', 'Role (admin or member)'))
257
+ .requiredOption('-r, --role <role>', 'Role to assign: admin, editor, or viewer (default: editor)'))
249
258
  .action(async (teamId, email, _opts) => {
250
259
  const flags = resolveFlags(_opts);
251
260
  const out = createOutput(flags);
252
- const role = String(_opts.role);
261
+ const role = (String(_opts.role) || 'editor');
253
262
  out.startSpinner('Sending invitation...');
254
263
  try {
255
264
  const client = await getClientAsync();
@@ -267,10 +276,16 @@ EXAMPLES
267
276
  addGlobalFlags(invitations.command('revoke')
268
277
  .description('Revoke a pending invitation')
269
278
  .argument('<teamId>', 'Team ID')
270
- .argument('<invitationId>', 'Invitation ID'))
279
+ .argument('<invitationId>', 'Invitation ID')
280
+ .option('-y, --yes', 'Skip confirmation prompt'))
271
281
  .action(async (teamId, invitationId, _opts) => {
272
282
  const flags = resolveFlags(_opts);
273
283
  const out = createOutput(flags);
284
+ const confirmed = await confirmAction(`Revoke invitation ${invitationId}?`, { yes: !!_opts.yes });
285
+ if (!confirmed) {
286
+ out.status('Cancelled.');
287
+ return;
288
+ }
274
289
  out.startSpinner('Revoking invitation...');
275
290
  try {
276
291
  const client = await getClientAsync();
@@ -5,6 +5,7 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
5
5
  import { createOutput, handleError } from '../utils/output.js';
6
6
  import { formatBytes } from '../utils/format.js';
7
7
  import { promptPassword, readPasswordFromStdin } from '../utils/prompt.js';
8
+ import { confirmAction } from '../utils/confirm.js';
8
9
  export function registerUserCommands(program) {
9
10
  const user = program.command('user').description('View account details and storage usage');
10
11
  addGlobalFlags(user.command('storage')
@@ -176,10 +177,16 @@ export function registerUserCommands(program) {
176
177
  .description('Request account deletion')
177
178
  .option('--password-stdin', 'Read password from stdin for CI usage')
178
179
  .option('--reason <reason>', 'Reason for deletion')
179
- .option('--export-data', 'Request data export before deletion'))
180
+ .option('--export-data', 'Request data export before deletion')
181
+ .option('-y, --yes', 'Skip confirmation prompt'))
180
182
  .action(async (_opts) => {
181
183
  const flags = resolveFlags(_opts);
182
184
  const out = createOutput(flags);
185
+ const confirmed = await confirmAction('Are you sure you want to delete your account? This cannot be undone.', { yes: _opts.yes });
186
+ if (!confirmed) {
187
+ out.status('Account deletion cancelled.');
188
+ return;
189
+ }
183
190
  let password;
184
191
  if (_opts.passwordStdin) {
185
192
  password = await readPasswordFromStdin();