@lifestreamdynamics/vault-cli 1.3.7 → 1.3.8
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/dist/commands/admin.js +3 -1
- package/dist/commands/analytics.js +1 -1
- package/dist/commands/booking.js +2 -1
- package/dist/commands/completion.js +188 -16
- package/dist/commands/config.js +1 -0
- package/dist/commands/custom-domains.js +6 -4
- package/dist/commands/docs.js +5 -3
- package/dist/commands/links.js +11 -6
- package/dist/commands/mfa.js +143 -93
- package/dist/commands/plugins.js +2 -2
- package/dist/commands/publish-vault.js +11 -4
- package/dist/commands/saml.js +3 -1
- package/dist/commands/search.js +5 -1
- package/dist/commands/shares.js +9 -2
- package/dist/commands/sync.js +13 -4
- package/dist/commands/user.js +8 -1
- package/dist/commands/vaults.js +15 -4
- package/dist/utils/output.js +7 -5
- package/dist/utils/resolve-vault.js +1 -1
- package/package.json +1 -1
package/dist/commands/admin.js
CHANGED
|
@@ -4,7 +4,9 @@ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
|
|
|
4
4
|
import { createOutput, handleError } from '../utils/output.js';
|
|
5
5
|
import { formatBytes, formatUptime } from '../utils/format.js';
|
|
6
6
|
export function registerAdminCommands(program) {
|
|
7
|
-
const admin = program.command('admin')
|
|
7
|
+
const admin = program.command('admin')
|
|
8
|
+
.description('System administration (requires admin role)')
|
|
9
|
+
.addHelpText('after', '\nNOTE: Admin commands require JWT authentication with admin role.\nRun "lsvault auth login" to authenticate first.');
|
|
8
10
|
// ── System Stats ────────────────────────────────────────────────────
|
|
9
11
|
const stats = admin.command('stats').description('View system-wide statistics and metrics');
|
|
10
12
|
const statsAction = async (_opts) => {
|
|
@@ -67,7 +67,7 @@ export function registerAnalyticsCommands(program) {
|
|
|
67
67
|
.addHelpText('after', `
|
|
68
68
|
NOTE
|
|
69
69
|
The publishedDocId is a UUID, NOT a document path. Get it from:
|
|
70
|
-
lsvault
|
|
70
|
+
lsvault analytics published`))
|
|
71
71
|
.action(async (vaultId, publishedDocId, _opts) => {
|
|
72
72
|
const flags = resolveFlags(_opts);
|
|
73
73
|
const out = createOutput(flags);
|
package/dist/commands/booking.js
CHANGED
|
@@ -405,7 +405,8 @@ export function registerBookingCommands(program) {
|
|
|
405
405
|
addGlobalFlags(booking.command('reschedule')
|
|
406
406
|
.description('Reschedule a booking by guest reschedule token')
|
|
407
407
|
.argument('<token>', 'Reschedule token (from guest email link)')
|
|
408
|
-
.argument('<newStartAt>', 'New start time in ISO 8601 format (e.g. 2026-03-15T10:00:00Z)')
|
|
408
|
+
.argument('<newStartAt>', 'New start time in ISO 8601 format (e.g. 2026-03-15T10:00:00Z)')
|
|
409
|
+
.addHelpText('after', '\n The <token> is the guest reschedule token from the booking confirmation email,\n not the booking ID.'))
|
|
409
410
|
.action(async (token, newStartAt, _opts) => {
|
|
410
411
|
const flags = resolveFlags(_opts);
|
|
411
412
|
const out = createOutput(flags);
|
|
@@ -1,45 +1,217 @@
|
|
|
1
1
|
const SAFE_COMMAND_NAME = /^[a-z][a-z0-9-]*$/;
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Recursively walk the Commander tree and produce a structured CommandNode tree.
|
|
4
|
+
* Only names matching SAFE_COMMAND_NAME are included (shell injection guard).
|
|
5
|
+
*/
|
|
6
|
+
function collectCommandTree(cmd) {
|
|
7
|
+
const nodes = [];
|
|
4
8
|
for (const sub of cmd.commands) {
|
|
5
9
|
const name = sub.name();
|
|
6
|
-
// Skip any name that doesn't match the safe pattern to prevent shell injection
|
|
7
10
|
if (!SAFE_COMMAND_NAME.test(name))
|
|
8
11
|
continue;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
// Collect long-form option flags from this command's declared options.
|
|
13
|
+
const options = sub.options
|
|
14
|
+
.map((o) => o.long)
|
|
15
|
+
.filter((flag) => typeof flag === 'string');
|
|
16
|
+
nodes.push({
|
|
17
|
+
name,
|
|
18
|
+
description: sub.description() ?? '',
|
|
19
|
+
subcommands: collectCommandTree(sub),
|
|
20
|
+
options,
|
|
21
|
+
});
|
|
12
22
|
}
|
|
13
|
-
return
|
|
23
|
+
return nodes;
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Bash
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/**
|
|
29
|
+
* Emit the bash case arm for a top-level command.
|
|
30
|
+
* Handles COMP_CWORD == 2 (show subcommand names) and COMP_CWORD >= 3 (show
|
|
31
|
+
* a flat list of options for the matched sub/sub-subcommand).
|
|
32
|
+
*/
|
|
33
|
+
function bashCaseArm(node, indent) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
lines.push(`${indent}${node.name})`);
|
|
36
|
+
if (node.subcommands.length > 0) {
|
|
37
|
+
const subNames = node.subcommands.map(s => s.name).join(' ');
|
|
38
|
+
lines.push(`${indent} if [[ $COMP_CWORD -eq 2 ]]; then`);
|
|
39
|
+
lines.push(`${indent} COMPREPLY=($(compgen -W "${subNames}" -- "$cur"))`);
|
|
40
|
+
lines.push(`${indent} else`);
|
|
41
|
+
// Third-level: match sub-subcommand name in COMP_WORDS[2] and show its options
|
|
42
|
+
lines.push(`${indent} case "$cmd2" in`);
|
|
43
|
+
for (const sub of node.subcommands) {
|
|
44
|
+
const allOpts = sub.options.join(' ');
|
|
45
|
+
// Also include sub-subcommand names if this node has nested commands
|
|
46
|
+
const deepNames = sub.subcommands.map(s => s.name).join(' ');
|
|
47
|
+
const completions = [deepNames, allOpts].filter(Boolean).join(' ');
|
|
48
|
+
lines.push(`${indent} ${sub.name})`);
|
|
49
|
+
if (completions) {
|
|
50
|
+
lines.push(`${indent} COMPREPLY=($(compgen -W "${completions}" -- "$cur"))`);
|
|
51
|
+
}
|
|
52
|
+
lines.push(`${indent} ;;`);
|
|
53
|
+
}
|
|
54
|
+
lines.push(`${indent} esac`);
|
|
55
|
+
lines.push(`${indent} fi`);
|
|
56
|
+
}
|
|
57
|
+
else if (node.options.length > 0) {
|
|
58
|
+
// Leaf command with flags only
|
|
59
|
+
const opts = node.options.join(' ');
|
|
60
|
+
lines.push(`${indent} COMPREPLY=($(compgen -W "${opts}" -- "$cur"))`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(`${indent} ;;`);
|
|
63
|
+
return lines.join('\n');
|
|
14
64
|
}
|
|
15
65
|
export function generateBashCompletion(program) {
|
|
16
|
-
const
|
|
66
|
+
const tree = collectCommandTree(program);
|
|
67
|
+
const topLevelNames = tree.map(n => n.name).join(' ');
|
|
68
|
+
const caseArms = tree.map(node => bashCaseArm(node, ' ')).join('\n');
|
|
17
69
|
return `# Bash completion for lsvault
|
|
18
70
|
# Add to ~/.bashrc: eval "$(lsvault completion bash)"
|
|
19
71
|
_lsvault_completions() {
|
|
20
72
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
21
|
-
local
|
|
22
|
-
|
|
73
|
+
local cmd1="\${COMP_WORDS[1]:-}"
|
|
74
|
+
local cmd2="\${COMP_WORDS[2]:-}"
|
|
75
|
+
|
|
76
|
+
# Top-level completions
|
|
77
|
+
if [[ $COMP_CWORD -eq 1 ]]; then
|
|
78
|
+
COMPREPLY=($(compgen -W "${topLevelNames}" -- "$cur"))
|
|
79
|
+
return
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Second-level and deeper
|
|
83
|
+
case "$cmd1" in
|
|
84
|
+
${caseArms}
|
|
85
|
+
esac
|
|
23
86
|
}
|
|
24
87
|
complete -F _lsvault_completions lsvault
|
|
25
88
|
`;
|
|
26
89
|
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Zsh
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
function zshDescribeArray(nodes, varName, indent) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
lines.push(`${indent}local -a ${varName}`);
|
|
96
|
+
lines.push(`${indent}${varName}=(`);
|
|
97
|
+
for (const n of nodes) {
|
|
98
|
+
// Escape single quotes in description
|
|
99
|
+
const desc = n.description.replace(/'/g, "'\\''");
|
|
100
|
+
lines.push(`${indent} '${n.name}:${desc}'`);
|
|
101
|
+
}
|
|
102
|
+
lines.push(`${indent})`);
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
function zshCaseArm(node, indent) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
lines.push(`${indent}${node.name})`);
|
|
108
|
+
if (node.subcommands.length > 0) {
|
|
109
|
+
lines.push(zshDescribeArray(node.subcommands, 'subcmds', indent + ' '));
|
|
110
|
+
lines.push(`${indent} if (( CURRENT == 3 )); then`);
|
|
111
|
+
lines.push(`${indent} _describe 'subcommand' subcmds`);
|
|
112
|
+
lines.push(`${indent} else`);
|
|
113
|
+
// Third-level: show options for matched subcommand
|
|
114
|
+
lines.push(`${indent} case "$words[3]" in`);
|
|
115
|
+
for (const sub of node.subcommands) {
|
|
116
|
+
if (sub.options.length > 0 || sub.subcommands.length > 0) {
|
|
117
|
+
lines.push(`${indent} ${sub.name})`);
|
|
118
|
+
if (sub.subcommands.length > 0) {
|
|
119
|
+
lines.push(zshDescribeArray(sub.subcommands, 'deepcmds', indent + ' '));
|
|
120
|
+
lines.push(`${indent} if (( CURRENT == 4 )); then`);
|
|
121
|
+
lines.push(`${indent} _describe 'subcommand' deepcmds`);
|
|
122
|
+
if (sub.options.length > 0) {
|
|
123
|
+
const optSpecs = sub.options.map(o => `'${o}'`).join(' ');
|
|
124
|
+
lines.push(`${indent} else`);
|
|
125
|
+
lines.push(`${indent} _arguments ${optSpecs}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push(`${indent} fi`);
|
|
128
|
+
}
|
|
129
|
+
else if (sub.options.length > 0) {
|
|
130
|
+
const optSpecs = sub.options.map(o => `'${o}'`).join(' ');
|
|
131
|
+
lines.push(`${indent} _arguments ${optSpecs}`);
|
|
132
|
+
}
|
|
133
|
+
lines.push(`${indent} ;;`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
lines.push(`${indent} esac`);
|
|
137
|
+
lines.push(`${indent} fi`);
|
|
138
|
+
}
|
|
139
|
+
else if (node.options.length > 0) {
|
|
140
|
+
const optSpecs = node.options.map(o => `'${o}'`).join(' ');
|
|
141
|
+
lines.push(`${indent} _arguments ${optSpecs}`);
|
|
142
|
+
}
|
|
143
|
+
lines.push(`${indent} ;;`);
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
27
146
|
export function generateZshCompletion(program) {
|
|
28
|
-
const
|
|
147
|
+
const tree = collectCommandTree(program);
|
|
148
|
+
const topLevelArray = zshDescribeArray(tree, 'toplevel', ' ');
|
|
149
|
+
const caseArms = tree.map(node => zshCaseArm(node, ' ')).join('\n');
|
|
29
150
|
return `# Zsh completion for lsvault
|
|
30
151
|
# Add to ~/.zshrc: eval "$(lsvault completion zsh)"
|
|
31
152
|
_lsvault() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
153
|
+
${topLevelArray}
|
|
154
|
+
|
|
155
|
+
if (( CURRENT == 2 )); then
|
|
156
|
+
_describe 'command' toplevel
|
|
157
|
+
return
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
case "$words[2]" in
|
|
161
|
+
${caseArms}
|
|
162
|
+
esac
|
|
35
163
|
}
|
|
36
164
|
compdef _lsvault lsvault
|
|
37
165
|
`;
|
|
38
166
|
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Fish
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Escape a string for use inside a fish completion -d '...' description.
|
|
172
|
+
* Fish uses single quotes and doesn't support backslash escapes within them,
|
|
173
|
+
* so we just strip single quotes entirely.
|
|
174
|
+
*/
|
|
175
|
+
function fishDesc(s) {
|
|
176
|
+
return s.replace(/'/g, '');
|
|
177
|
+
}
|
|
39
178
|
export function generateFishCompletion(program) {
|
|
40
|
-
const
|
|
41
|
-
|
|
179
|
+
const tree = collectCommandTree(program);
|
|
180
|
+
const lines = [];
|
|
181
|
+
// Helper: the names of all top-level commands (used in __fish_seen_subcommand_from guards)
|
|
182
|
+
const topLevelNames = tree.map(n => n.name).join(' ');
|
|
183
|
+
for (const node of tree) {
|
|
184
|
+
// Top-level command — only show when no subcommand has been entered yet
|
|
185
|
+
lines.push(`complete -c lsvault -n '__fish_use_subcommand' -a '${node.name}' -d '${fishDesc(node.description)}'`);
|
|
186
|
+
if (node.subcommands.length > 0) {
|
|
187
|
+
// Subcommand names — show when this top-level command is active and no sub-subcommand yet
|
|
188
|
+
for (const sub of node.subcommands) {
|
|
189
|
+
lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and not __fish_seen_subcommand_from ${sub.subcommands.map(s => s.name).concat(node.subcommands.map(s => s.name)).join(' ')}' -a '${sub.name}' -d '${fishDesc(sub.description)}'`);
|
|
190
|
+
// Options for this subcommand — show when top-level AND subcommand are both seen
|
|
191
|
+
for (const opt of sub.options) {
|
|
192
|
+
lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and __fish_seen_subcommand_from ${sub.name}' -a '${opt}'`);
|
|
193
|
+
}
|
|
194
|
+
// Third-level sub-subcommands
|
|
195
|
+
for (const deep of sub.subcommands) {
|
|
196
|
+
lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and __fish_seen_subcommand_from ${sub.name}' -a '${deep.name}' -d '${fishDesc(deep.description)}'`);
|
|
197
|
+
for (const opt of deep.options) {
|
|
198
|
+
lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${node.name}; and __fish_seen_subcommand_from ${deep.name}' -a '${opt}'`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Leaf top-level command — offer its own options when active
|
|
205
|
+
for (const opt of node.options) {
|
|
206
|
+
lines.push(`complete -c lsvault -n '__fish_seen_subcommand_from ${topLevelNames}' -f -a '${opt}'`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return lines.join('\n') + '\n';
|
|
42
211
|
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Registration
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
43
215
|
export function registerCompletionCommands(program) {
|
|
44
216
|
const completion = program.command('completion').description('Generate shell completion scripts');
|
|
45
217
|
completion.command('bash')
|
package/dist/commands/config.js
CHANGED
|
@@ -106,8 +106,9 @@ export function registerCustomDomainCommands(program) {
|
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
108
|
addGlobalFlags(domains.command('verify')
|
|
109
|
-
.description('
|
|
110
|
-
.argument('<domainId>', 'Domain ID')
|
|
109
|
+
.description('Trigger DNS verification check for a custom domain')
|
|
110
|
+
.argument('<domainId>', 'Domain ID')
|
|
111
|
+
.addHelpText('after', '\n Submits a verification request to the server, which checks your DNS TXT record.\n Use "check" to read the current verification status without triggering a new check.'))
|
|
111
112
|
.action(async (domainId, _opts) => {
|
|
112
113
|
const flags = resolveFlags(_opts);
|
|
113
114
|
const out = createOutput(flags);
|
|
@@ -122,8 +123,9 @@ export function registerCustomDomainCommands(program) {
|
|
|
122
123
|
}
|
|
123
124
|
});
|
|
124
125
|
addGlobalFlags(domains.command('check')
|
|
125
|
-
.description('
|
|
126
|
-
.argument('<domainId>', 'Domain ID')
|
|
126
|
+
.description('Show current DNS verification status for a custom domain')
|
|
127
|
+
.argument('<domainId>', 'Domain ID')
|
|
128
|
+
.addHelpText('after', '\n Returns the current verification status without triggering a new check.\n Use "verify" to submit a fresh DNS verification request to the server.'))
|
|
127
129
|
.action(async (domainId, _opts) => {
|
|
128
130
|
const flags = resolveFlags(_opts);
|
|
129
131
|
const out = createOutput(flags);
|
package/dist/commands/docs.js
CHANGED
|
@@ -165,9 +165,11 @@ EXAMPLES
|
|
|
165
165
|
else {
|
|
166
166
|
doc = await client.documents.put(vaultId, docPath, content);
|
|
167
167
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
const docPath2 = doc.path ?? docPath;
|
|
169
|
+
const size = doc.sizeBytes ?? Buffer.byteLength(content, 'utf8');
|
|
170
|
+
out.success(`Document saved: ${chalk.cyan(docPath2)} (${size} bytes)`, {
|
|
171
|
+
path: docPath2,
|
|
172
|
+
sizeBytes: size,
|
|
171
173
|
encrypted: doc.encrypted ?? false,
|
|
172
174
|
});
|
|
173
175
|
}
|
package/dist/commands/links.js
CHANGED
|
@@ -22,16 +22,16 @@ export function registerLinkCommands(program) {
|
|
|
22
22
|
out.list(linkList.map(link => ({
|
|
23
23
|
targetPath: link.targetPath,
|
|
24
24
|
linkText: link.linkText,
|
|
25
|
-
|
|
25
|
+
isResolved: link.isResolved,
|
|
26
26
|
})), {
|
|
27
27
|
emptyMessage: 'No forward links found.',
|
|
28
28
|
columns: [
|
|
29
29
|
{ key: 'targetPath', header: 'Target' },
|
|
30
30
|
{ key: 'linkText', header: 'Link Text' },
|
|
31
|
-
{ key: '
|
|
31
|
+
{ key: 'isResolved', header: 'Resolved' },
|
|
32
32
|
],
|
|
33
33
|
textFn: (link) => {
|
|
34
|
-
const resolved = link.
|
|
34
|
+
const resolved = link.isResolved ? chalk.green('✓') : chalk.red('✗');
|
|
35
35
|
return ` ${resolved} [[${String(link.linkText)}]] → ${String(link.targetPath)}`;
|
|
36
36
|
},
|
|
37
37
|
});
|
|
@@ -55,18 +55,23 @@ export function registerLinkCommands(program) {
|
|
|
55
55
|
const backlinks = await client.documents.getBacklinks(vaultId, docPath);
|
|
56
56
|
out.stopSpinner();
|
|
57
57
|
out.list(backlinks.map(bl => ({
|
|
58
|
-
|
|
58
|
+
sourcePath: bl.sourceDocument.path,
|
|
59
|
+
sourceTitle: bl.sourceDocument.title || null,
|
|
59
60
|
linkText: bl.linkText,
|
|
60
61
|
context: bl.contextSnippet || '',
|
|
61
62
|
})), {
|
|
62
63
|
emptyMessage: 'No backlinks found.',
|
|
63
64
|
columns: [
|
|
64
|
-
{ key: '
|
|
65
|
+
{ key: 'sourcePath', header: 'Source Path' },
|
|
66
|
+
{ key: 'sourceTitle', header: 'Source Title' },
|
|
65
67
|
{ key: 'linkText', header: 'Link Text' },
|
|
66
68
|
{ key: 'context', header: 'Context' },
|
|
67
69
|
],
|
|
68
70
|
textFn: (bl) => {
|
|
69
|
-
const
|
|
71
|
+
const displayName = bl.sourceTitle ? String(bl.sourceTitle) : String(bl.sourcePath);
|
|
72
|
+
const lines = [chalk.cyan(` ${displayName}`)];
|
|
73
|
+
if (bl.sourceTitle)
|
|
74
|
+
lines.push(` Path: ${String(bl.sourcePath)}`);
|
|
70
75
|
lines.push(` Link: [[${String(bl.linkText)}]]`);
|
|
71
76
|
if (bl.context)
|
|
72
77
|
lines.push(` Context: ...${String(bl.context)}...`);
|
package/dist/commands/mfa.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
82
|
+
out.status(chalk.yellow('Setup cancelled.'));
|
|
60
83
|
return;
|
|
61
84
|
}
|
|
62
|
-
|
|
85
|
+
out.startSpinner('Verifying code and enabling TOTP...');
|
|
63
86
|
const result = await client.mfa.verifyTotp(code);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
out.status(chalk.yellow('Operation cancelled.'));
|
|
89
125
|
return;
|
|
90
126
|
}
|
|
91
|
-
|
|
127
|
+
out.startSpinner('Disabling TOTP...');
|
|
92
128
|
try {
|
|
93
129
|
const client = await getClientAsync();
|
|
94
130
|
const result = await client.mfa.disableTotp(password);
|
|
95
|
-
|
|
131
|
+
out.stopSpinner();
|
|
132
|
+
out.success(result.message, { message: result.message });
|
|
96
133
|
}
|
|
97
134
|
catch (err) {
|
|
98
|
-
|
|
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 (
|
|
106
|
-
|
|
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
|
-
|
|
153
|
+
out.status(chalk.yellow('Operation cancelled.'));
|
|
111
154
|
return;
|
|
112
155
|
}
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/commands/plugins.js
CHANGED
|
@@ -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.
|
|
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 });
|
package/dist/commands/saml.js
CHANGED
|
@@ -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')
|
|
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'))
|
package/dist/commands/search.js
CHANGED
|
@@ -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);
|
package/dist/commands/shares.js
CHANGED
|
@@ -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);
|
package/dist/commands/sync.js
CHANGED
|
@@ -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) {
|
package/dist/commands/user.js
CHANGED
|
@@ -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();
|
package/dist/commands/vaults.js
CHANGED
|
@@ -5,17 +5,21 @@ import { createOutput, handleError } from '../utils/output.js';
|
|
|
5
5
|
import { generateVaultKey } from '@lifestreamdynamics/vault-sdk';
|
|
6
6
|
import { getCredentialManager } from '../config.js';
|
|
7
7
|
import { resolveVaultId } from '../utils/resolve-vault.js';
|
|
8
|
+
import { confirmAction } from '../utils/confirm.js';
|
|
8
9
|
export function registerVaultCommands(program) {
|
|
9
10
|
const vaults = program.command('vaults').description('Create, list, and inspect document vaults');
|
|
10
11
|
addGlobalFlags(vaults.command('list')
|
|
11
|
-
.description('List all vaults accessible to the current user')
|
|
12
|
+
.description('List all vaults accessible to the current user')
|
|
13
|
+
.option('--include-archived', 'Include archived vaults in the list'))
|
|
12
14
|
.action(async (_opts) => {
|
|
13
15
|
const flags = resolveFlags(_opts);
|
|
14
16
|
const out = createOutput(flags);
|
|
15
17
|
out.startSpinner('Fetching vaults...');
|
|
16
18
|
try {
|
|
17
19
|
const client = await getClientAsync();
|
|
18
|
-
const vaultList = await client.vaults.list(
|
|
20
|
+
const vaultList = await client.vaults.list({
|
|
21
|
+
includeArchived: _opts.includeArchived === true,
|
|
22
|
+
});
|
|
19
23
|
out.stopSpinner();
|
|
20
24
|
out.list(vaultList.map(v => ({ name: v.name, slug: v.slug, encrypted: v.encryptionEnabled ? 'yes' : 'no', description: v.description ?? null, id: v.id })), {
|
|
21
25
|
emptyMessage: 'No vaults found.',
|
|
@@ -45,6 +49,7 @@ export function registerVaultCommands(program) {
|
|
|
45
49
|
const out = createOutput(flags);
|
|
46
50
|
out.startSpinner('Fetching vault...');
|
|
47
51
|
try {
|
|
52
|
+
vaultId = await resolveVaultId(vaultId);
|
|
48
53
|
const client = await getClientAsync();
|
|
49
54
|
const vault = await client.vaults.get(vaultId);
|
|
50
55
|
out.stopSpinner();
|
|
@@ -196,12 +201,18 @@ EXAMPLES
|
|
|
196
201
|
// vault archive
|
|
197
202
|
addGlobalFlags(vaults.command('archive')
|
|
198
203
|
.description('Archive a vault')
|
|
199
|
-
.argument('<vaultId>', 'Vault ID')
|
|
204
|
+
.argument('<vaultId>', 'Vault ID')
|
|
205
|
+
.option('-y, --yes', 'Skip confirmation prompt'))
|
|
200
206
|
.action(async (vaultId, _opts) => {
|
|
201
207
|
const flags = resolveFlags(_opts);
|
|
202
208
|
const out = createOutput(flags);
|
|
203
|
-
out.startSpinner('Archiving vault...');
|
|
204
209
|
try {
|
|
210
|
+
const confirmed = await confirmAction(`Archive vault ${vaultId}?`, { yes: _opts.yes });
|
|
211
|
+
if (!confirmed) {
|
|
212
|
+
out.status('Archive cancelled.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
out.startSpinner('Archiving vault...');
|
|
205
216
|
const client = await getClientAsync();
|
|
206
217
|
const vault = await client.vaults.archive(vaultId);
|
|
207
218
|
out.success(`Vault archived: ${vault.name}`, { id: vault.id, name: vault.name, isArchived: vault.isArchived });
|
package/dist/utils/output.js
CHANGED
|
@@ -96,6 +96,8 @@ export class Output {
|
|
|
96
96
|
* - table: prints a single-row table
|
|
97
97
|
*/
|
|
98
98
|
record(data, columns) {
|
|
99
|
+
if (this.flags.quiet)
|
|
100
|
+
return;
|
|
99
101
|
switch (this.flags.output) {
|
|
100
102
|
case 'json':
|
|
101
103
|
process.stdout.write(JSON.stringify(data) + '\n');
|
|
@@ -131,11 +133,11 @@ export class Output {
|
|
|
131
133
|
}
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
136
|
+
if (this.flags.quiet)
|
|
137
|
+
return;
|
|
134
138
|
switch (this.flags.output) {
|
|
135
139
|
case 'json':
|
|
136
|
-
|
|
137
|
-
process.stdout.write(JSON.stringify(item) + '\n');
|
|
138
|
-
}
|
|
140
|
+
process.stdout.write(JSON.stringify(data) + '\n');
|
|
139
141
|
break;
|
|
140
142
|
case 'table':
|
|
141
143
|
this.table(data, options?.columns);
|
|
@@ -166,8 +168,8 @@ export class Output {
|
|
|
166
168
|
* Print a success result (used for create/update/delete confirmations).
|
|
167
169
|
*/
|
|
168
170
|
success(message, data) {
|
|
169
|
-
if (this.flags.output === 'json'
|
|
170
|
-
process.stdout.write(JSON.stringify(data) + '\n');
|
|
171
|
+
if (this.flags.output === 'json') {
|
|
172
|
+
process.stdout.write(JSON.stringify(data ?? { success: true, message }) + '\n');
|
|
171
173
|
}
|
|
172
174
|
else if (!this.flags.quiet) {
|
|
173
175
|
this.succeedSpinner(message);
|
|
@@ -11,7 +11,7 @@ export async function resolveVaultId(idOrSlug) {
|
|
|
11
11
|
if (UUID_RE.test(idOrSlug))
|
|
12
12
|
return idOrSlug;
|
|
13
13
|
const client = await getClientAsync();
|
|
14
|
-
const vaults = await client.vaults.list();
|
|
14
|
+
const vaults = await client.vaults.list({ includeArchived: true });
|
|
15
15
|
const match = vaults.find(v => v.slug === idOrSlug);
|
|
16
16
|
if (!match)
|
|
17
17
|
throw new Error(`Vault not found: "${idOrSlug}"`);
|