@lifestreamdynamics/vault-cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +140 -30
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.js +12 -11
  4. package/dist/commands/admin.js +5 -5
  5. package/dist/commands/ai.d.ts +2 -0
  6. package/dist/commands/ai.js +124 -0
  7. package/dist/commands/analytics.d.ts +2 -0
  8. package/dist/commands/analytics.js +84 -0
  9. package/dist/commands/auth.js +10 -105
  10. package/dist/commands/booking.d.ts +2 -0
  11. package/dist/commands/booking.js +739 -0
  12. package/dist/commands/calendar.js +778 -6
  13. package/dist/commands/completion.d.ts +5 -0
  14. package/dist/commands/completion.js +60 -0
  15. package/dist/commands/config.js +17 -16
  16. package/dist/commands/connectors.js +12 -1
  17. package/dist/commands/custom-domains.d.ts +2 -0
  18. package/dist/commands/custom-domains.js +154 -0
  19. package/dist/commands/docs.js +152 -5
  20. package/dist/commands/hooks.js +6 -1
  21. package/dist/commands/links.js +9 -2
  22. package/dist/commands/mfa.js +1 -70
  23. package/dist/commands/plugins.d.ts +2 -0
  24. package/dist/commands/plugins.js +172 -0
  25. package/dist/commands/publish-vault.d.ts +2 -0
  26. package/dist/commands/publish-vault.js +117 -0
  27. package/dist/commands/publish.js +63 -2
  28. package/dist/commands/saml.d.ts +2 -0
  29. package/dist/commands/saml.js +220 -0
  30. package/dist/commands/scim.d.ts +2 -0
  31. package/dist/commands/scim.js +238 -0
  32. package/dist/commands/shares.js +25 -3
  33. package/dist/commands/subscription.js +9 -2
  34. package/dist/commands/sync.js +3 -0
  35. package/dist/commands/teams.js +233 -4
  36. package/dist/commands/user.js +444 -0
  37. package/dist/commands/vaults.js +240 -8
  38. package/dist/commands/webhooks.js +6 -1
  39. package/dist/config.d.ts +2 -0
  40. package/dist/config.js +7 -3
  41. package/dist/index.js +28 -1
  42. package/dist/lib/credential-manager.js +32 -7
  43. package/dist/lib/migration.js +2 -2
  44. package/dist/lib/profiles.js +4 -4
  45. package/dist/sync/config.js +2 -2
  46. package/dist/sync/daemon-worker.js +13 -6
  47. package/dist/sync/daemon.js +2 -1
  48. package/dist/sync/remote-poller.js +7 -3
  49. package/dist/sync/state.js +2 -2
  50. package/dist/utils/confirm.d.ts +11 -0
  51. package/dist/utils/confirm.js +23 -0
  52. package/dist/utils/format.js +1 -1
  53. package/dist/utils/output.js +4 -1
  54. package/dist/utils/prompt.d.ts +29 -0
  55. package/dist/utils/prompt.js +146 -0
  56. package/package.json +2 -2
@@ -0,0 +1,5 @@
1
+ import type { Command } from 'commander';
2
+ export declare function generateBashCompletion(program: Command): string;
3
+ export declare function generateZshCompletion(program: Command): string;
4
+ export declare function generateFishCompletion(program: Command): string;
5
+ export declare function registerCompletionCommands(program: Command): void;
@@ -0,0 +1,60 @@
1
+ const SAFE_COMMAND_NAME = /^[a-z][a-z0-9-]*$/;
2
+ function collectCommandNames(cmd, prefix = '') {
3
+ const names = [];
4
+ for (const sub of cmd.commands) {
5
+ const name = sub.name();
6
+ // Skip any name that doesn't match the safe pattern to prevent shell injection
7
+ if (!SAFE_COMMAND_NAME.test(name))
8
+ continue;
9
+ const full = prefix ? `${prefix} ${name}` : name;
10
+ names.push(name);
11
+ names.push(...collectCommandNames(sub, full));
12
+ }
13
+ return names;
14
+ }
15
+ export function generateBashCompletion(program) {
16
+ const commands = collectCommandNames(program);
17
+ return `# Bash completion for lsvault
18
+ # Add to ~/.bashrc: eval "$(lsvault completion bash)"
19
+ _lsvault_completions() {
20
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
21
+ local commands="${commands.join(' ')}"
22
+ COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}"))
23
+ }
24
+ complete -F _lsvault_completions lsvault
25
+ `;
26
+ }
27
+ export function generateZshCompletion(program) {
28
+ const commands = collectCommandNames(program);
29
+ return `# Zsh completion for lsvault
30
+ # Add to ~/.zshrc: eval "$(lsvault completion zsh)"
31
+ _lsvault() {
32
+ local -a commands
33
+ commands=(${commands.map(c => `'${c}'`).join(' ')})
34
+ _describe 'lsvault commands' commands
35
+ }
36
+ compdef _lsvault lsvault
37
+ `;
38
+ }
39
+ export function generateFishCompletion(program) {
40
+ const commands = collectCommandNames(program);
41
+ return commands.map(c => `complete -c lsvault -n '__fish_use_subcommand' -a '${c}' -d '${c} command'`).join('\n') + '\n';
42
+ }
43
+ export function registerCompletionCommands(program) {
44
+ const completion = program.command('completion').description('Generate shell completion scripts');
45
+ completion.command('bash')
46
+ .description('Generate bash completion script')
47
+ .action(() => {
48
+ process.stdout.write(generateBashCompletion(program));
49
+ });
50
+ completion.command('zsh')
51
+ .description('Generate zsh completion script')
52
+ .action(() => {
53
+ process.stdout.write(generateZshCompletion(program));
54
+ });
55
+ completion.command('fish')
56
+ .description('Generate fish completion script')
57
+ .action(() => {
58
+ process.stdout.write(generateFishCompletion(program));
59
+ });
60
+ }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { resolveProfileName, getActiveProfile, setActiveProfile, loadProfile, setProfileValue, getProfileValue, listProfiles, deleteProfile, } from '../lib/profiles.js';
3
+ const SENSITIVE_KEY_PATTERN = /key|token|secret|password|credential/i;
3
4
  export function registerConfigCommands(program) {
4
5
  const config = program
5
6
  .command('config')
@@ -26,7 +27,7 @@ EXAMPLES
26
27
  .action((key, value, opts) => {
27
28
  const profile = resolveProfileName(opts.profile);
28
29
  setProfileValue(profile, key, value);
29
- console.log(chalk.green(`Set ${chalk.bold(key)} in profile ${chalk.bold(profile)}`));
30
+ process.stdout.write(chalk.green(`Set ${chalk.bold(key)} in profile ${chalk.bold(profile)}`) + '\n');
30
31
  });
31
32
  config
32
33
  .command('get')
@@ -41,10 +42,10 @@ EXAMPLES
41
42
  const profile = resolveProfileName(opts.profile);
42
43
  const value = getProfileValue(profile, key);
43
44
  if (value !== undefined) {
44
- console.log(value);
45
+ process.stdout.write(value + '\n');
45
46
  }
46
47
  else {
47
- console.log(chalk.yellow(`Key "${key}" not set in profile "${profile}"`));
48
+ process.stdout.write(chalk.yellow(`Key "${key}" not set in profile "${profile}"`) + '\n');
48
49
  }
49
50
  });
50
51
  config
@@ -60,17 +61,17 @@ EXAMPLES
60
61
  const profileConfig = loadProfile(profile);
61
62
  const keys = Object.keys(profileConfig);
62
63
  if (keys.length === 0) {
63
- console.log(chalk.yellow(`Profile "${profile}" has no configuration values.`));
64
+ process.stdout.write(chalk.yellow(`Profile "${profile}" has no configuration values.`) + '\n');
64
65
  return;
65
66
  }
66
- console.log(chalk.bold(`Profile: ${profile}\n`));
67
+ process.stdout.write(chalk.bold(`Profile: ${profile}\n`) + '\n');
67
68
  for (const key of keys) {
68
69
  const value = profileConfig[key];
69
- // Mask API keys for display
70
- const display = key.toLowerCase().includes('key') && value
70
+ // Mask sensitive values for display
71
+ const display = SENSITIVE_KEY_PATTERN.test(key) && value
71
72
  ? value.slice(0, 12) + '...'
72
73
  : value;
73
- console.log(` ${chalk.cyan(key)}: ${display}`);
74
+ process.stdout.write(` ${chalk.cyan(key)}: ${display}\n`);
74
75
  }
75
76
  });
76
77
  config
@@ -83,7 +84,7 @@ EXAMPLES
83
84
  lsvault config use dev`)
84
85
  .action((name) => {
85
86
  setActiveProfile(name);
86
- console.log(chalk.green(`Active profile set to ${chalk.bold(name)}`));
87
+ process.stdout.write(chalk.green(`Active profile set to ${chalk.bold(name)}`) + '\n');
87
88
  });
88
89
  config
89
90
  .command('profiles')
@@ -95,14 +96,14 @@ EXAMPLES
95
96
  const profiles = listProfiles();
96
97
  const active = getActiveProfile();
97
98
  if (profiles.length === 0) {
98
- console.log(chalk.yellow('No profiles configured.'));
99
- console.log(chalk.dim('Create one with: lsvault config set <key> <value> --profile <name>'));
99
+ process.stdout.write(chalk.yellow('No profiles configured.') + '\n');
100
+ process.stdout.write(chalk.dim('Create one with: lsvault config set <key> <value> --profile <name>') + '\n');
100
101
  return;
101
102
  }
102
- console.log(chalk.bold('Available profiles:\n'));
103
+ process.stdout.write(chalk.bold('Available profiles:\n') + '\n');
103
104
  for (const name of profiles) {
104
105
  const marker = name === active ? chalk.green(' (active)') : '';
105
- console.log(` ${chalk.cyan(name)}${marker}`);
106
+ process.stdout.write(` ${chalk.cyan(name)}${marker}\n`);
106
107
  }
107
108
  });
108
109
  config
@@ -114,10 +115,10 @@ EXAMPLES
114
115
  lsvault config delete staging`)
115
116
  .action((name) => {
116
117
  if (deleteProfile(name)) {
117
- console.log(chalk.green(`Profile "${name}" deleted.`));
118
+ process.stdout.write(chalk.green(`Profile "${name}" deleted.`) + '\n');
118
119
  }
119
120
  else {
120
- console.log(chalk.yellow(`Profile "${name}" not found.`));
121
+ process.stdout.write(chalk.yellow(`Profile "${name}" not found.`) + '\n');
121
122
  }
122
123
  });
123
124
  config
@@ -125,6 +126,6 @@ EXAMPLES
125
126
  .description('Show the active profile name')
126
127
  .action(() => {
127
128
  const active = getActiveProfile();
128
- console.log(active);
129
+ process.stdout.write(active + '\n');
129
130
  });
130
131
  }
@@ -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
+ const VALID_PROVIDERS = ['google_drive', 'dropbox', 'onedrive'];
5
6
  export function registerConnectorCommands(program) {
6
7
  const connectors = program.command('connectors').description('Manage external service connectors (e.g., Google Drive)');
7
8
  addGlobalFlags(connectors.command('list')
@@ -79,6 +80,11 @@ export function registerConnectorCommands(program) {
79
80
  .action(async (provider, name, _opts) => {
80
81
  const flags = resolveFlags(_opts);
81
82
  const out = createOutput(flags);
83
+ if (!VALID_PROVIDERS.includes(provider)) {
84
+ out.error(`Invalid provider "${provider}". Must be one of: ${VALID_PROVIDERS.join(', ')}`);
85
+ process.exitCode = 1;
86
+ return;
87
+ }
82
88
  out.startSpinner('Creating connector...');
83
89
  try {
84
90
  const client = await getClientAsync();
@@ -127,10 +133,15 @@ export function registerConnectorCommands(program) {
127
133
  });
128
134
  addGlobalFlags(connectors.command('delete')
129
135
  .description('Delete a connector')
130
- .argument('<connectorId>', 'Connector ID'))
136
+ .argument('<connectorId>', 'Connector ID')
137
+ .option('-y, --yes', 'Skip confirmation prompt'))
131
138
  .action(async (connectorId, _opts) => {
132
139
  const flags = resolveFlags(_opts);
133
140
  const out = createOutput(flags);
141
+ if (!_opts.yes) {
142
+ out.status(chalk.yellow(`Pass --yes to delete connector ${connectorId}.`));
143
+ return;
144
+ }
134
145
  out.startSpinner('Deleting connector...');
135
146
  try {
136
147
  const client = await getClientAsync();
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCustomDomainCommands(program: Command): void;
@@ -0,0 +1,154 @@
1
+ import chalk from 'chalk';
2
+ import { getClientAsync } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerCustomDomainCommands(program) {
6
+ const domains = program.command('custom-domains').description('Manage custom domains for published vaults');
7
+ addGlobalFlags(domains.command('list')
8
+ .description('List custom domains'))
9
+ .action(async (_opts) => {
10
+ const flags = resolveFlags(_opts);
11
+ const out = createOutput(flags);
12
+ out.startSpinner('Fetching custom domains...');
13
+ try {
14
+ const client = await getClientAsync();
15
+ const list = await client.customDomains.list();
16
+ out.stopSpinner();
17
+ out.list(list.map(d => ({ id: d.id, domain: d.domain, verified: d.verified ? 'yes' : 'no', createdAt: d.createdAt })), {
18
+ emptyMessage: 'No custom domains found.',
19
+ columns: [
20
+ { key: 'id', header: 'ID' },
21
+ { key: 'domain', header: 'Domain' },
22
+ { key: 'verified', header: 'Verified' },
23
+ { key: 'createdAt', header: 'Created' },
24
+ ],
25
+ textFn: (d) => `${chalk.cyan(String(d.domain))} — ${d.verified === 'yes' ? chalk.green('verified') : chalk.yellow('unverified')}`,
26
+ });
27
+ }
28
+ catch (err) {
29
+ handleError(out, err, 'Failed to fetch custom domains');
30
+ }
31
+ });
32
+ addGlobalFlags(domains.command('get')
33
+ .description('Get a custom domain')
34
+ .argument('<domainId>', 'Domain ID'))
35
+ .action(async (domainId, _opts) => {
36
+ const flags = resolveFlags(_opts);
37
+ const out = createOutput(flags);
38
+ out.startSpinner('Fetching custom domain...');
39
+ try {
40
+ const client = await getClientAsync();
41
+ const d = await client.customDomains.get(domainId);
42
+ out.stopSpinner();
43
+ out.record({ id: d.id, domain: d.domain, verified: d.verified, verificationToken: d.verificationToken, createdAt: d.createdAt });
44
+ }
45
+ catch (err) {
46
+ handleError(out, err, 'Failed to fetch custom domain');
47
+ }
48
+ });
49
+ addGlobalFlags(domains.command('add')
50
+ .description('Add a custom domain')
51
+ .argument('<domain>', 'Domain name (e.g., docs.example.com)'))
52
+ .action(async (domain, _opts) => {
53
+ const flags = resolveFlags(_opts);
54
+ const out = createOutput(flags);
55
+ out.startSpinner('Adding custom domain...');
56
+ try {
57
+ const client = await getClientAsync();
58
+ const d = await client.customDomains.create({ domain });
59
+ out.success(`Domain added: ${d.domain}`, { id: d.id, domain: d.domain, verificationToken: d.verificationToken });
60
+ if (flags.output !== 'json') {
61
+ process.stdout.write(`\nTo verify, add this DNS TXT record:\n`);
62
+ process.stdout.write(` ${chalk.cyan('_lsvault-verification.' + d.domain)} TXT ${chalk.green(d.verificationToken)}\n`);
63
+ process.stdout.write(`\nThen run: lsvault custom-domains verify ${d.id}\n`);
64
+ }
65
+ }
66
+ catch (err) {
67
+ handleError(out, err, 'Failed to add custom domain');
68
+ }
69
+ });
70
+ addGlobalFlags(domains.command('update')
71
+ .description('Update a custom domain')
72
+ .argument('<domainId>', 'Domain ID')
73
+ .requiredOption('--domain <domain>', 'New domain name'))
74
+ .action(async (domainId, _opts) => {
75
+ const flags = resolveFlags(_opts);
76
+ const out = createOutput(flags);
77
+ out.startSpinner('Updating custom domain...');
78
+ try {
79
+ const client = await getClientAsync();
80
+ const d = await client.customDomains.update(domainId, { domain: _opts.domain });
81
+ out.success(`Domain updated: ${d.domain}`, { id: d.id, domain: d.domain });
82
+ }
83
+ catch (err) {
84
+ handleError(out, err, 'Failed to update custom domain');
85
+ }
86
+ });
87
+ addGlobalFlags(domains.command('remove')
88
+ .description('Remove a custom domain')
89
+ .argument('<domainId>', 'Domain ID')
90
+ .option('-y, --yes', 'Skip confirmation prompt'))
91
+ .action(async (domainId, _opts) => {
92
+ const flags = resolveFlags(_opts);
93
+ const out = createOutput(flags);
94
+ if (!_opts.yes) {
95
+ out.status(chalk.yellow(`Pass --yes to remove custom domain ${domainId}.`));
96
+ return;
97
+ }
98
+ out.startSpinner('Removing custom domain...');
99
+ try {
100
+ const client = await getClientAsync();
101
+ await client.customDomains.delete(domainId);
102
+ out.success('Custom domain removed', { id: domainId });
103
+ }
104
+ catch (err) {
105
+ handleError(out, err, 'Failed to remove custom domain');
106
+ }
107
+ });
108
+ addGlobalFlags(domains.command('verify')
109
+ .description('Verify a custom domain via DNS')
110
+ .argument('<domainId>', 'Domain ID'))
111
+ .action(async (domainId, _opts) => {
112
+ const flags = resolveFlags(_opts);
113
+ const out = createOutput(flags);
114
+ out.startSpinner('Verifying custom domain...');
115
+ try {
116
+ const client = await getClientAsync();
117
+ const d = await client.customDomains.verify(domainId);
118
+ out.success(`Domain ${d.verified ? 'verified' : 'not yet verified'}: ${d.domain}`, { id: d.id, domain: d.domain, verified: d.verified });
119
+ }
120
+ catch (err) {
121
+ handleError(out, err, 'Failed to verify custom domain');
122
+ }
123
+ });
124
+ addGlobalFlags(domains.command('check')
125
+ .description('Check DNS configuration for a custom domain')
126
+ .argument('<domainId>', 'Domain ID'))
127
+ .action(async (domainId, _opts) => {
128
+ const flags = resolveFlags(_opts);
129
+ const out = createOutput(flags);
130
+ out.startSpinner('Checking DNS...');
131
+ try {
132
+ const client = await getClientAsync();
133
+ const result = await client.customDomains.checkDns(domainId);
134
+ out.stopSpinner();
135
+ out.record({
136
+ domain: result.domain,
137
+ resolved: result.resolved,
138
+ expectedValue: result.expectedValue,
139
+ actualValue: result.actualValue ?? 'N/A',
140
+ });
141
+ if (flags.output !== 'json') {
142
+ if (result.resolved) {
143
+ process.stdout.write(chalk.green('\n✓ DNS configured correctly\n'));
144
+ }
145
+ else {
146
+ process.stdout.write(chalk.yellow(`\n⚠ DNS not yet propagated. Expected: ${result.expectedValue}\n`));
147
+ }
148
+ }
149
+ }
150
+ catch (err) {
151
+ handleError(out, err, 'Failed to check DNS');
152
+ }
153
+ });
154
+ }
@@ -2,7 +2,8 @@ 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 { createCredentialManager } from '../lib/credential-manager.js';
5
+ import { getCredentialManager } from '../config.js';
6
+ import { confirmAction } from '../utils/confirm.js';
6
7
  export function registerDocCommands(program) {
7
8
  const docs = program.command('docs').description('Read, write, move, and delete documents in a vault');
8
9
  addGlobalFlags(docs.command('list')
@@ -66,7 +67,7 @@ EXAMPLES
66
67
  const result = await client.documents.get(vaultId, docPath);
67
68
  // Auto-decrypt if the document is encrypted
68
69
  if (result.document.encrypted && !_opts.meta) {
69
- const credManager = createCredentialManager();
70
+ const credManager = getCredentialManager();
70
71
  const vaultKey = await credManager.getVaultKey(vaultId);
71
72
  if (!vaultKey) {
72
73
  out.error('Document is encrypted but no vault key found.');
@@ -124,7 +125,7 @@ EXAMPLES
124
125
  const vault = await client.vaults.get(vaultId);
125
126
  let doc;
126
127
  if (vault.encryptionEnabled) {
127
- const credManager = createCredentialManager();
128
+ const credManager = getCredentialManager();
128
129
  const vaultKey = await credManager.getVaultKey(vaultId);
129
130
  if (!vaultKey) {
130
131
  out.failSpinner('Failed to save document');
@@ -151,12 +152,18 @@ EXAMPLES
151
152
  addGlobalFlags(docs.command('delete')
152
153
  .description('Permanently delete a document from a vault')
153
154
  .argument('<vaultId>', 'Vault ID')
154
- .argument('<path>', 'Document path to delete'))
155
+ .argument('<path>', 'Document path to delete')
156
+ .option('-y, --yes', 'Skip confirmation prompt'))
155
157
  .action(async (vaultId, docPath, _opts) => {
156
158
  const flags = resolveFlags(_opts);
157
159
  const out = createOutput(flags);
158
- out.startSpinner('Deleting document...');
159
160
  try {
161
+ const confirmed = await confirmAction(`Permanently delete "${docPath}" from vault ${vaultId}?`, { yes: _opts.yes });
162
+ if (!confirmed) {
163
+ out.status('Deletion cancelled.');
164
+ return;
165
+ }
166
+ out.startSpinner('Deleting document...');
160
167
  const client = await getClientAsync();
161
168
  await client.documents.delete(vaultId, docPath);
162
169
  out.success(`Deleted: ${chalk.cyan(docPath)}`, { path: docPath, deleted: true });
@@ -191,4 +198,144 @@ EXAMPLES
191
198
  handleError(out, err, 'Failed to move document');
192
199
  }
193
200
  });
201
+ addGlobalFlags(docs.command('bulk-move')
202
+ .description('Move multiple documents to a target directory')
203
+ .argument('<vaultId>', 'Vault ID')
204
+ .requiredOption('--paths <csv>', 'Comma-separated list of document paths')
205
+ .requiredOption('--target <dir>', 'Target directory'))
206
+ .action(async (vaultId, _opts) => {
207
+ const flags = resolveFlags(_opts);
208
+ const out = createOutput(flags);
209
+ out.startSpinner('Moving documents...');
210
+ try {
211
+ const client = await getClientAsync();
212
+ const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
213
+ const result = await client.documents.bulkMove(vaultId, { paths, targetDirectory: _opts.target });
214
+ out.stopSpinner();
215
+ if (flags.output === 'json') {
216
+ out.raw(JSON.stringify(result, null, 2) + '\n');
217
+ }
218
+ else {
219
+ process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
220
+ if (result.failed.length > 0) {
221
+ for (const f of result.failed)
222
+ process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
223
+ }
224
+ }
225
+ }
226
+ catch (err) {
227
+ handleError(out, err, 'Failed to bulk move documents');
228
+ }
229
+ });
230
+ addGlobalFlags(docs.command('bulk-copy')
231
+ .description('Copy multiple documents to a target directory')
232
+ .argument('<vaultId>', 'Vault ID')
233
+ .requiredOption('--paths <csv>', 'Comma-separated list of document paths')
234
+ .requiredOption('--target <dir>', 'Target directory'))
235
+ .action(async (vaultId, _opts) => {
236
+ const flags = resolveFlags(_opts);
237
+ const out = createOutput(flags);
238
+ out.startSpinner('Copying documents...');
239
+ try {
240
+ const client = await getClientAsync();
241
+ const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
242
+ const result = await client.documents.bulkCopy(vaultId, { paths, targetDirectory: _opts.target });
243
+ out.stopSpinner();
244
+ if (flags.output === 'json') {
245
+ out.raw(JSON.stringify(result, null, 2) + '\n');
246
+ }
247
+ else {
248
+ process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
249
+ if (result.failed.length > 0) {
250
+ for (const f of result.failed)
251
+ process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
252
+ }
253
+ }
254
+ }
255
+ catch (err) {
256
+ handleError(out, err, 'Failed to bulk copy documents');
257
+ }
258
+ });
259
+ addGlobalFlags(docs.command('bulk-delete')
260
+ .description('Delete multiple documents')
261
+ .argument('<vaultId>', 'Vault ID')
262
+ .requiredOption('--paths <csv>', 'Comma-separated list of document paths'))
263
+ .action(async (vaultId, _opts) => {
264
+ const flags = resolveFlags(_opts);
265
+ const out = createOutput(flags);
266
+ out.startSpinner('Deleting documents...');
267
+ try {
268
+ const client = await getClientAsync();
269
+ const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
270
+ const result = await client.documents.bulkDelete(vaultId, { paths });
271
+ out.stopSpinner();
272
+ if (flags.output === 'json') {
273
+ out.raw(JSON.stringify(result, null, 2) + '\n');
274
+ }
275
+ else {
276
+ process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
277
+ if (result.failed.length > 0) {
278
+ for (const f of result.failed)
279
+ process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
280
+ }
281
+ }
282
+ }
283
+ catch (err) {
284
+ handleError(out, err, 'Failed to bulk delete documents');
285
+ }
286
+ });
287
+ addGlobalFlags(docs.command('bulk-tag')
288
+ .description('Add or remove tags from multiple documents')
289
+ .argument('<vaultId>', 'Vault ID')
290
+ .requiredOption('--paths <csv>', 'Comma-separated list of document paths')
291
+ .option('--add <csv>', 'Tags to add (comma-separated)')
292
+ .option('--remove <csv>', 'Tags to remove (comma-separated)'))
293
+ .action(async (vaultId, _opts) => {
294
+ const flags = resolveFlags(_opts);
295
+ const out = createOutput(flags);
296
+ if (!_opts.add && !_opts.remove) {
297
+ out.error('At least one of --add or --remove must be specified');
298
+ process.exitCode = 1;
299
+ return;
300
+ }
301
+ out.startSpinner('Tagging documents...');
302
+ try {
303
+ const client = await getClientAsync();
304
+ const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
305
+ const addTags = _opts.add ? (String(_opts.add)).split(',').map(t => t.trim()).filter(Boolean) : undefined;
306
+ const removeTags = _opts.remove ? (String(_opts.remove)).split(',').map(t => t.trim()).filter(Boolean) : undefined;
307
+ const result = await client.documents.bulkTag(vaultId, { paths, addTags, removeTags });
308
+ out.stopSpinner();
309
+ if (flags.output === 'json') {
310
+ out.raw(JSON.stringify(result, null, 2) + '\n');
311
+ }
312
+ else {
313
+ process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
314
+ if (result.failed.length > 0) {
315
+ for (const f of result.failed)
316
+ process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
317
+ }
318
+ }
319
+ }
320
+ catch (err) {
321
+ handleError(out, err, 'Failed to bulk tag documents');
322
+ }
323
+ });
324
+ addGlobalFlags(docs.command('mkdir')
325
+ .description('Create a directory in a vault')
326
+ .argument('<vaultId>', 'Vault ID')
327
+ .argument('<path>', 'Directory path to create'))
328
+ .action(async (vaultId, path, _opts) => {
329
+ const flags = resolveFlags(_opts);
330
+ const out = createOutput(flags);
331
+ out.startSpinner('Creating directory...');
332
+ try {
333
+ const client = await getClientAsync();
334
+ const result = await client.documents.createDirectory(vaultId, path);
335
+ out.success(`Directory ${result.created ? 'created' : 'already exists'}: ${result.path}`, { path: result.path, created: result.created });
336
+ }
337
+ catch (err) {
338
+ handleError(out, err, 'Failed to create directory');
339
+ }
340
+ });
194
341
  }
@@ -100,10 +100,15 @@ export function registerHookCommands(program) {
100
100
  addGlobalFlags(hooks.command('delete')
101
101
  .description('Delete a hook')
102
102
  .argument('<vaultId>', 'Vault ID')
103
- .argument('<hookId>', 'Hook ID'))
103
+ .argument('<hookId>', 'Hook ID')
104
+ .option('-y, --yes', 'Skip confirmation prompt'))
104
105
  .action(async (vaultId, hookId, _opts) => {
105
106
  const flags = resolveFlags(_opts);
106
107
  const out = createOutput(flags);
108
+ if (!_opts.yes) {
109
+ out.status(chalk.yellow(`Pass --yes to delete hook ${hookId}.`));
110
+ return;
111
+ }
107
112
  out.startSpinner('Deleting hook...');
108
113
  try {
109
114
  const client = await getClientAsync();
@@ -87,8 +87,15 @@ export function registerLinkCommands(program) {
87
87
  const client = await getClientAsync();
88
88
  const graph = await client.vaults.getGraph(vaultId);
89
89
  out.stopSpinner();
90
- // For graph, output as JSON structure
91
- process.stdout.write(JSON.stringify({ nodes: graph.nodes, edges: graph.edges }) + '\n');
90
+ if (flags.output === 'json') {
91
+ process.stdout.write(JSON.stringify({ nodes: graph.nodes, edges: graph.edges }) + '\n');
92
+ }
93
+ else {
94
+ process.stdout.write(chalk.bold(`Nodes: ${graph.nodes.length} Edges: ${graph.edges.length}\n`));
95
+ for (const node of graph.nodes) {
96
+ process.stdout.write(` ${chalk.cyan(String(node.path ?? node.id))}\n`);
97
+ }
98
+ }
92
99
  }
93
100
  catch (err) {
94
101
  handleError(out, err, 'Failed to fetch link graph');
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { getClientAsync } from '../client.js';
4
+ import { promptPassword, promptMfaCode } from '../utils/prompt.js';
4
5
  export function registerMfaCommands(program) {
5
6
  const mfa = program.command('mfa').description('Multi-factor authentication management');
6
7
  mfa.command('status')
@@ -152,73 +153,3 @@ export function registerMfaCommands(program) {
152
153
  }
153
154
  });
154
155
  }
155
- /**
156
- * Prompt for a password from stdin (non-echoing).
157
- * Returns the password or null if stdin is not a TTY.
158
- */
159
- async function promptPassword() {
160
- if (!process.stdin.isTTY) {
161
- return null;
162
- }
163
- const readline = await import('node:readline');
164
- return new Promise((resolve) => {
165
- const rl = readline.createInterface({
166
- input: process.stdin,
167
- output: process.stderr,
168
- terminal: true,
169
- });
170
- process.stderr.write('Password: ');
171
- process.stdin.setRawMode?.(true);
172
- let password = '';
173
- const onData = (chunk) => {
174
- const char = chunk.toString('utf-8');
175
- if (char === '\n' || char === '\r' || char === '\u0004') {
176
- process.stderr.write('\n');
177
- process.stdin.setRawMode?.(false);
178
- process.stdin.removeListener('data', onData);
179
- rl.close();
180
- resolve(password);
181
- }
182
- else if (char === '\u0003') {
183
- // Ctrl+C
184
- process.stderr.write('\n');
185
- process.stdin.setRawMode?.(false);
186
- process.stdin.removeListener('data', onData);
187
- rl.close();
188
- resolve(null);
189
- }
190
- else if (char === '\u007F' || char === '\b') {
191
- // Backspace
192
- if (password.length > 0) {
193
- password = password.slice(0, -1);
194
- }
195
- }
196
- else {
197
- password += char;
198
- }
199
- };
200
- process.stdin.on('data', onData);
201
- process.stdin.resume();
202
- });
203
- }
204
- /**
205
- * Prompt for an MFA code from stdin (6 digits, echoed for visibility).
206
- * Returns the code or null if stdin is not a TTY.
207
- */
208
- async function promptMfaCode() {
209
- if (!process.stdin.isTTY) {
210
- return null;
211
- }
212
- const readline = await import('node:readline');
213
- return new Promise((resolve) => {
214
- const rl = readline.createInterface({
215
- input: process.stdin,
216
- output: process.stderr,
217
- terminal: true,
218
- });
219
- rl.question('Enter 6-digit code from authenticator app: ', (answer) => {
220
- rl.close();
221
- resolve(answer.trim() || null);
222
- });
223
- });
224
- }