@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.
- 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.d.ts +2 -0
- package/dist/commands/ai.js +124 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +84 -0
- 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 +778 -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.d.ts +2 -0
- package/dist/commands/custom-domains.js +154 -0
- package/dist/commands/docs.js +152 -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-vault.d.ts +2 -0
- package/dist/commands/publish-vault.js +117 -0
- package/dist/commands/publish.js +63 -2
- 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 +233 -4
- package/dist/commands/user.js +444 -0
- package/dist/commands/vaults.js +240 -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 +28 -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
|
@@ -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
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
45
|
+
process.stdout.write(value + '\n');
|
|
45
46
|
}
|
|
46
47
|
else {
|
|
47
|
-
|
|
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
|
-
|
|
64
|
+
process.stdout.write(chalk.yellow(`Profile "${profile}" has no configuration values.`) + '\n');
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
|
|
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
|
|
70
|
-
const display =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
process.stdout.write(chalk.green(`Profile "${name}" deleted.`) + '\n');
|
|
118
119
|
}
|
|
119
120
|
else {
|
|
120
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|
package/dist/commands/docs.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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
|
}
|
package/dist/commands/hooks.js
CHANGED
|
@@ -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();
|
package/dist/commands/links.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
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');
|
package/dist/commands/mfa.js
CHANGED
|
@@ -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
|
-
}
|