@lifestreamdynamics/vault-cli 1.0.1 → 1.2.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 +36 -0
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.js +111 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +84 -0
- package/dist/commands/auth.js +67 -1
- package/dist/commands/calendar.d.ts +2 -0
- package/dist/commands/calendar.js +220 -0
- package/dist/commands/custom-domains.d.ts +2 -0
- package/dist/commands/custom-domains.js +149 -0
- package/dist/commands/docs.js +140 -0
- package/dist/commands/links.d.ts +2 -0
- package/dist/commands/links.js +126 -0
- package/dist/commands/mfa.d.ts +2 -0
- package/dist/commands/mfa.js +224 -0
- package/dist/commands/publish-vault.d.ts +2 -0
- package/dist/commands/publish-vault.js +117 -0
- package/dist/commands/publish.js +51 -0
- package/dist/commands/search.js +7 -2
- package/dist/commands/subscription.js +1 -1
- package/dist/commands/sync.js +4 -1
- package/dist/commands/teams.js +96 -0
- package/dist/commands/user.js +331 -0
- package/dist/commands/vaults.js +223 -0
- package/dist/index.js +14 -0
- package/dist/sync/daemon-worker.js +62 -0
- package/dist/sync/daemon.d.ts +9 -1
- package/dist/sync/daemon.js +22 -1
- package/package.json +2 -2
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
.action(async (domainId, _opts) => {
|
|
91
|
+
const flags = resolveFlags(_opts);
|
|
92
|
+
const out = createOutput(flags);
|
|
93
|
+
out.startSpinner('Removing custom domain...');
|
|
94
|
+
try {
|
|
95
|
+
const client = await getClientAsync();
|
|
96
|
+
await client.customDomains.delete(domainId);
|
|
97
|
+
out.success('Custom domain removed', { id: domainId });
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
handleError(out, err, 'Failed to remove custom domain');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
addGlobalFlags(domains.command('verify')
|
|
104
|
+
.description('Verify a custom domain via DNS')
|
|
105
|
+
.argument('<domainId>', 'Domain ID'))
|
|
106
|
+
.action(async (domainId, _opts) => {
|
|
107
|
+
const flags = resolveFlags(_opts);
|
|
108
|
+
const out = createOutput(flags);
|
|
109
|
+
out.startSpinner('Verifying custom domain...');
|
|
110
|
+
try {
|
|
111
|
+
const client = await getClientAsync();
|
|
112
|
+
const d = await client.customDomains.verify(domainId);
|
|
113
|
+
out.success(`Domain ${d.verified ? 'verified' : 'not yet verified'}: ${d.domain}`, { id: d.id, domain: d.domain, verified: d.verified });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
handleError(out, err, 'Failed to verify custom domain');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
addGlobalFlags(domains.command('check')
|
|
120
|
+
.description('Check DNS configuration for a custom domain')
|
|
121
|
+
.argument('<domainId>', 'Domain ID'))
|
|
122
|
+
.action(async (domainId, _opts) => {
|
|
123
|
+
const flags = resolveFlags(_opts);
|
|
124
|
+
const out = createOutput(flags);
|
|
125
|
+
out.startSpinner('Checking DNS...');
|
|
126
|
+
try {
|
|
127
|
+
const client = await getClientAsync();
|
|
128
|
+
const result = await client.customDomains.checkDns(domainId);
|
|
129
|
+
out.stopSpinner();
|
|
130
|
+
out.record({
|
|
131
|
+
domain: result.domain,
|
|
132
|
+
resolved: result.resolved,
|
|
133
|
+
expectedValue: result.expectedValue,
|
|
134
|
+
actualValue: result.actualValue ?? 'N/A',
|
|
135
|
+
});
|
|
136
|
+
if (flags.output !== 'json') {
|
|
137
|
+
if (result.resolved) {
|
|
138
|
+
process.stdout.write(chalk.green('\n✓ DNS configured correctly\n'));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
process.stdout.write(chalk.yellow(`\n⚠ DNS not yet propagated. Expected: ${result.expectedValue}\n`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
handleError(out, err, 'Failed to check DNS');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
package/dist/commands/docs.js
CHANGED
|
@@ -191,4 +191,144 @@ EXAMPLES
|
|
|
191
191
|
handleError(out, err, 'Failed to move document');
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
|
+
addGlobalFlags(docs.command('bulk-move')
|
|
195
|
+
.description('Move multiple documents to a target directory')
|
|
196
|
+
.argument('<vaultId>', 'Vault ID')
|
|
197
|
+
.requiredOption('--paths <csv>', 'Comma-separated list of document paths')
|
|
198
|
+
.requiredOption('--target <dir>', 'Target directory'))
|
|
199
|
+
.action(async (vaultId, _opts) => {
|
|
200
|
+
const flags = resolveFlags(_opts);
|
|
201
|
+
const out = createOutput(flags);
|
|
202
|
+
out.startSpinner('Moving documents...');
|
|
203
|
+
try {
|
|
204
|
+
const client = await getClientAsync();
|
|
205
|
+
const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
|
|
206
|
+
const result = await client.documents.bulkMove(vaultId, { paths, targetDirectory: _opts.target });
|
|
207
|
+
out.stopSpinner();
|
|
208
|
+
if (flags.output === 'json') {
|
|
209
|
+
out.raw(JSON.stringify(result, null, 2) + '\n');
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
|
|
213
|
+
if (result.failed.length > 0) {
|
|
214
|
+
for (const f of result.failed)
|
|
215
|
+
process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
handleError(out, err, 'Failed to bulk move documents');
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
addGlobalFlags(docs.command('bulk-copy')
|
|
224
|
+
.description('Copy multiple documents to a target directory')
|
|
225
|
+
.argument('<vaultId>', 'Vault ID')
|
|
226
|
+
.requiredOption('--paths <csv>', 'Comma-separated list of document paths')
|
|
227
|
+
.requiredOption('--target <dir>', 'Target directory'))
|
|
228
|
+
.action(async (vaultId, _opts) => {
|
|
229
|
+
const flags = resolveFlags(_opts);
|
|
230
|
+
const out = createOutput(flags);
|
|
231
|
+
out.startSpinner('Copying documents...');
|
|
232
|
+
try {
|
|
233
|
+
const client = await getClientAsync();
|
|
234
|
+
const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
|
|
235
|
+
const result = await client.documents.bulkCopy(vaultId, { paths, targetDirectory: _opts.target });
|
|
236
|
+
out.stopSpinner();
|
|
237
|
+
if (flags.output === 'json') {
|
|
238
|
+
out.raw(JSON.stringify(result, null, 2) + '\n');
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
|
|
242
|
+
if (result.failed.length > 0) {
|
|
243
|
+
for (const f of result.failed)
|
|
244
|
+
process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
handleError(out, err, 'Failed to bulk copy documents');
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
addGlobalFlags(docs.command('bulk-delete')
|
|
253
|
+
.description('Delete multiple documents')
|
|
254
|
+
.argument('<vaultId>', 'Vault ID')
|
|
255
|
+
.requiredOption('--paths <csv>', 'Comma-separated list of document paths'))
|
|
256
|
+
.action(async (vaultId, _opts) => {
|
|
257
|
+
const flags = resolveFlags(_opts);
|
|
258
|
+
const out = createOutput(flags);
|
|
259
|
+
out.startSpinner('Deleting documents...');
|
|
260
|
+
try {
|
|
261
|
+
const client = await getClientAsync();
|
|
262
|
+
const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
|
|
263
|
+
const result = await client.documents.bulkDelete(vaultId, { paths });
|
|
264
|
+
out.stopSpinner();
|
|
265
|
+
if (flags.output === 'json') {
|
|
266
|
+
out.raw(JSON.stringify(result, null, 2) + '\n');
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
|
|
270
|
+
if (result.failed.length > 0) {
|
|
271
|
+
for (const f of result.failed)
|
|
272
|
+
process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
handleError(out, err, 'Failed to bulk delete documents');
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
addGlobalFlags(docs.command('bulk-tag')
|
|
281
|
+
.description('Add or remove tags from multiple documents')
|
|
282
|
+
.argument('<vaultId>', 'Vault ID')
|
|
283
|
+
.requiredOption('--paths <csv>', 'Comma-separated list of document paths')
|
|
284
|
+
.option('--add <csv>', 'Tags to add (comma-separated)')
|
|
285
|
+
.option('--remove <csv>', 'Tags to remove (comma-separated)'))
|
|
286
|
+
.action(async (vaultId, _opts) => {
|
|
287
|
+
const flags = resolveFlags(_opts);
|
|
288
|
+
const out = createOutput(flags);
|
|
289
|
+
if (!_opts.add && !_opts.remove) {
|
|
290
|
+
out.error('At least one of --add or --remove must be specified');
|
|
291
|
+
process.exitCode = 1;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
out.startSpinner('Tagging documents...');
|
|
295
|
+
try {
|
|
296
|
+
const client = await getClientAsync();
|
|
297
|
+
const paths = (String(_opts.paths)).split(',').map(p => p.trim()).filter(Boolean);
|
|
298
|
+
const addTags = _opts.add ? (String(_opts.add)).split(',').map(t => t.trim()).filter(Boolean) : undefined;
|
|
299
|
+
const removeTags = _opts.remove ? (String(_opts.remove)).split(',').map(t => t.trim()).filter(Boolean) : undefined;
|
|
300
|
+
const result = await client.documents.bulkTag(vaultId, { paths, addTags, removeTags });
|
|
301
|
+
out.stopSpinner();
|
|
302
|
+
if (flags.output === 'json') {
|
|
303
|
+
out.raw(JSON.stringify(result, null, 2) + '\n');
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
process.stdout.write(`Succeeded: ${result.succeeded.length}, Failed: ${result.failed.length}\n`);
|
|
307
|
+
if (result.failed.length > 0) {
|
|
308
|
+
for (const f of result.failed)
|
|
309
|
+
process.stdout.write(` ${chalk.red('✗')} ${f.path}: ${f.error}\n`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
handleError(out, err, 'Failed to bulk tag documents');
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
addGlobalFlags(docs.command('mkdir')
|
|
318
|
+
.description('Create a directory in a vault')
|
|
319
|
+
.argument('<vaultId>', 'Vault ID')
|
|
320
|
+
.argument('<path>', 'Directory path to create'))
|
|
321
|
+
.action(async (vaultId, path, _opts) => {
|
|
322
|
+
const flags = resolveFlags(_opts);
|
|
323
|
+
const out = createOutput(flags);
|
|
324
|
+
out.startSpinner('Creating directory...');
|
|
325
|
+
try {
|
|
326
|
+
const client = await getClientAsync();
|
|
327
|
+
const result = await client.documents.createDirectory(vaultId, path);
|
|
328
|
+
out.success(`Directory ${result.created ? 'created' : 'already exists'}: ${result.path}`, { path: result.path, created: result.created });
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
handleError(out, err, 'Failed to create directory');
|
|
332
|
+
}
|
|
333
|
+
});
|
|
194
334
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 registerLinkCommands(program) {
|
|
6
|
+
const links = program.command('links').description('Manage document links and backlinks');
|
|
7
|
+
// lsvault links list <vaultId> <path> — forward links
|
|
8
|
+
addGlobalFlags(links.command('list')
|
|
9
|
+
.description('List forward links from a document')
|
|
10
|
+
.argument('<vaultId>', 'Vault ID')
|
|
11
|
+
.argument('<path>', 'Document path'))
|
|
12
|
+
.action(async (vaultId, docPath, _opts) => {
|
|
13
|
+
const flags = resolveFlags(_opts);
|
|
14
|
+
const out = createOutput(flags);
|
|
15
|
+
out.startSpinner('Fetching links...');
|
|
16
|
+
try {
|
|
17
|
+
const client = await getClientAsync();
|
|
18
|
+
const linkList = await client.documents.getLinks(vaultId, docPath);
|
|
19
|
+
out.stopSpinner();
|
|
20
|
+
out.list(linkList.map(link => ({
|
|
21
|
+
targetPath: link.targetPath,
|
|
22
|
+
linkText: link.linkText,
|
|
23
|
+
resolved: link.isResolved ? 'Yes' : 'No',
|
|
24
|
+
})), {
|
|
25
|
+
emptyMessage: 'No forward links found.',
|
|
26
|
+
columns: [
|
|
27
|
+
{ key: 'targetPath', header: 'Target' },
|
|
28
|
+
{ key: 'linkText', header: 'Link Text' },
|
|
29
|
+
{ key: 'resolved', header: 'Resolved' },
|
|
30
|
+
],
|
|
31
|
+
textFn: (link) => {
|
|
32
|
+
const resolved = link.resolved === 'Yes' ? chalk.green('✓') : chalk.red('✗');
|
|
33
|
+
return ` ${resolved} [[${String(link.linkText)}]] → ${String(link.targetPath)}`;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
handleError(out, err, 'Failed to fetch links');
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// lsvault links backlinks <vaultId> <path>
|
|
42
|
+
addGlobalFlags(links.command('backlinks')
|
|
43
|
+
.description('List backlinks pointing to a document')
|
|
44
|
+
.argument('<vaultId>', 'Vault ID')
|
|
45
|
+
.argument('<path>', 'Document path'))
|
|
46
|
+
.action(async (vaultId, docPath, _opts) => {
|
|
47
|
+
const flags = resolveFlags(_opts);
|
|
48
|
+
const out = createOutput(flags);
|
|
49
|
+
out.startSpinner('Fetching backlinks...');
|
|
50
|
+
try {
|
|
51
|
+
const client = await getClientAsync();
|
|
52
|
+
const backlinks = await client.documents.getBacklinks(vaultId, docPath);
|
|
53
|
+
out.stopSpinner();
|
|
54
|
+
out.list(backlinks.map(bl => ({
|
|
55
|
+
source: bl.sourceDocument.title || bl.sourceDocument.path,
|
|
56
|
+
linkText: bl.linkText,
|
|
57
|
+
context: bl.contextSnippet || '',
|
|
58
|
+
})), {
|
|
59
|
+
emptyMessage: 'No backlinks found.',
|
|
60
|
+
columns: [
|
|
61
|
+
{ key: 'source', header: 'Source' },
|
|
62
|
+
{ key: 'linkText', header: 'Link Text' },
|
|
63
|
+
{ key: 'context', header: 'Context' },
|
|
64
|
+
],
|
|
65
|
+
textFn: (bl) => {
|
|
66
|
+
const lines = [chalk.cyan(` ${String(bl.source)}`)];
|
|
67
|
+
lines.push(` Link: [[${String(bl.linkText)}]]`);
|
|
68
|
+
if (bl.context)
|
|
69
|
+
lines.push(` Context: ...${String(bl.context)}...`);
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
handleError(out, err, 'Failed to fetch backlinks');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// lsvault links graph <vaultId>
|
|
79
|
+
addGlobalFlags(links.command('graph')
|
|
80
|
+
.description('Get the link graph for a vault')
|
|
81
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
82
|
+
.action(async (vaultId, _opts) => {
|
|
83
|
+
const flags = resolveFlags(_opts);
|
|
84
|
+
const out = createOutput(flags);
|
|
85
|
+
out.startSpinner('Fetching link graph...');
|
|
86
|
+
try {
|
|
87
|
+
const client = await getClientAsync();
|
|
88
|
+
const graph = await client.vaults.getGraph(vaultId);
|
|
89
|
+
out.stopSpinner();
|
|
90
|
+
// For graph, output as JSON structure
|
|
91
|
+
process.stdout.write(JSON.stringify({ nodes: graph.nodes, edges: graph.edges }) + '\n');
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
handleError(out, err, 'Failed to fetch link graph');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// lsvault links broken <vaultId>
|
|
98
|
+
addGlobalFlags(links.command('broken')
|
|
99
|
+
.description('List unresolved (broken) links in a vault')
|
|
100
|
+
.argument('<vaultId>', 'Vault ID'))
|
|
101
|
+
.action(async (vaultId, _opts) => {
|
|
102
|
+
const flags = resolveFlags(_opts);
|
|
103
|
+
const out = createOutput(flags);
|
|
104
|
+
out.startSpinner('Fetching unresolved links...');
|
|
105
|
+
try {
|
|
106
|
+
const client = await getClientAsync();
|
|
107
|
+
const unresolved = await client.vaults.getUnresolvedLinks(vaultId);
|
|
108
|
+
out.stopSpinner();
|
|
109
|
+
if (unresolved.length === 0) {
|
|
110
|
+
out.success('No broken links found!');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Format as grouped output
|
|
114
|
+
for (const group of unresolved) {
|
|
115
|
+
process.stdout.write(chalk.red(` ✗ ${group.targetPath}`) + '\n');
|
|
116
|
+
for (const ref of group.references) {
|
|
117
|
+
process.stdout.write(` ← ${ref.sourcePath} (${chalk.dim(ref.linkText)})` + '\n');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(`\n ${chalk.yellow(`${unresolved.length} broken link target(s) found`)}` + '\n');
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
handleError(out, err, 'Failed to fetch unresolved links');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getClientAsync } from '../client.js';
|
|
4
|
+
export function registerMfaCommands(program) {
|
|
5
|
+
const mfa = program.command('mfa').description('Multi-factor authentication management');
|
|
6
|
+
mfa.command('status')
|
|
7
|
+
.description('Show MFA status and configured methods')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const spinner = ora('Fetching MFA status...').start();
|
|
10
|
+
try {
|
|
11
|
+
const client = await getClientAsync();
|
|
12
|
+
const status = await client.mfa.getStatus();
|
|
13
|
+
spinner.stop();
|
|
14
|
+
console.log(chalk.bold('MFA Status'));
|
|
15
|
+
console.log(` Enabled: ${status.mfaEnabled ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
16
|
+
console.log(` TOTP Configured: ${status.totpConfigured ? chalk.green('Yes') : chalk.dim('No')}`);
|
|
17
|
+
console.log(` Passkeys Registered: ${status.passkeyCount > 0 ? chalk.cyan(status.passkeyCount) : chalk.dim('0')}`);
|
|
18
|
+
console.log(` Backup Codes Left: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}`);
|
|
19
|
+
if (status.passkeys.length > 0) {
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(chalk.bold('Registered Passkeys:'));
|
|
22
|
+
for (const passkey of status.passkeys) {
|
|
23
|
+
const lastUsed = passkey.lastUsedAt
|
|
24
|
+
? new Date(passkey.lastUsedAt).toLocaleDateString()
|
|
25
|
+
: chalk.dim('never');
|
|
26
|
+
console.log(` - ${chalk.cyan(passkey.name)} (last used: ${lastUsed})`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
spinner.fail('Failed to fetch MFA status');
|
|
32
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
mfa.command('setup-totp')
|
|
36
|
+
.description('Set up TOTP authenticator app (Google Authenticator, Authy, etc.)')
|
|
37
|
+
.action(async () => {
|
|
38
|
+
const spinner = ora('Generating TOTP secret...').start();
|
|
39
|
+
try {
|
|
40
|
+
const client = await getClientAsync();
|
|
41
|
+
const setup = await client.mfa.setupTotp();
|
|
42
|
+
spinner.stop();
|
|
43
|
+
console.log(chalk.bold('TOTP Setup'));
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(`Secret: ${chalk.cyan(setup.secret)}`);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('Add this URI to your authenticator app:');
|
|
48
|
+
console.log(chalk.dim(setup.otpauthUri));
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(chalk.yellow('Note: QR codes cannot be displayed in the terminal.'));
|
|
51
|
+
console.log(chalk.yellow(' Copy the URI above to any authenticator app that supports otpauth:// URIs.'));
|
|
52
|
+
console.log('');
|
|
53
|
+
// Prompt for verification code
|
|
54
|
+
const code = await promptMfaCode();
|
|
55
|
+
if (!code) {
|
|
56
|
+
console.log(chalk.yellow('Setup cancelled.'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const verifySpinner = ora('Verifying code and enabling TOTP...').start();
|
|
60
|
+
const result = await client.mfa.verifyTotp(code);
|
|
61
|
+
verifySpinner.succeed('TOTP enabled successfully!');
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(chalk.bold.yellow('IMPORTANT: Save these backup codes securely!'));
|
|
64
|
+
console.log(chalk.dim('You can use them to access your account if you lose your authenticator device.'));
|
|
65
|
+
console.log('');
|
|
66
|
+
// Display backup codes in a grid (2 columns)
|
|
67
|
+
const codes = result.backupCodes;
|
|
68
|
+
for (let i = 0; i < codes.length; i += 2) {
|
|
69
|
+
const left = codes[i] || '';
|
|
70
|
+
const right = codes[i + 1] || '';
|
|
71
|
+
console.log(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}`);
|
|
72
|
+
}
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
spinner.fail('TOTP setup failed');
|
|
77
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
mfa.command('disable-totp')
|
|
81
|
+
.description('Disable TOTP authentication (requires password)')
|
|
82
|
+
.action(async () => {
|
|
83
|
+
const password = await promptPassword();
|
|
84
|
+
if (!password) {
|
|
85
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const spinner = ora('Disabling TOTP...').start();
|
|
89
|
+
try {
|
|
90
|
+
const client = await getClientAsync();
|
|
91
|
+
const result = await client.mfa.disableTotp(password);
|
|
92
|
+
spinner.succeed(result.message);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
spinner.fail('Failed to disable TOTP');
|
|
96
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
mfa.command('backup-codes')
|
|
100
|
+
.description('Show remaining backup code count or regenerate codes')
|
|
101
|
+
.option('--regenerate', 'Generate new backup codes (requires password, invalidates old codes)')
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
if (opts.regenerate) {
|
|
104
|
+
// Regenerate backup codes
|
|
105
|
+
const password = await promptPassword();
|
|
106
|
+
if (!password) {
|
|
107
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const spinner = ora('Regenerating backup codes...').start();
|
|
111
|
+
try {
|
|
112
|
+
const client = await getClientAsync();
|
|
113
|
+
const result = await client.mfa.regenerateBackupCodes(password);
|
|
114
|
+
spinner.succeed('Backup codes regenerated!');
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(chalk.bold.yellow('IMPORTANT: Save these new backup codes securely!'));
|
|
117
|
+
console.log(chalk.dim('All previous backup codes have been invalidated.'));
|
|
118
|
+
console.log('');
|
|
119
|
+
// Display backup codes in a grid (2 columns)
|
|
120
|
+
const codes = result.backupCodes;
|
|
121
|
+
for (let i = 0; i < codes.length; i += 2) {
|
|
122
|
+
const left = codes[i] || '';
|
|
123
|
+
const right = codes[i + 1] || '';
|
|
124
|
+
console.log(` ${chalk.cyan(left.padEnd(20))} ${chalk.cyan(right)}`);
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
spinner.fail('Failed to regenerate backup codes');
|
|
130
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Show backup code count
|
|
135
|
+
const spinner = ora('Fetching backup code count...').start();
|
|
136
|
+
try {
|
|
137
|
+
const client = await getClientAsync();
|
|
138
|
+
const status = await client.mfa.getStatus();
|
|
139
|
+
spinner.stop();
|
|
140
|
+
console.log(chalk.bold('Backup Codes'));
|
|
141
|
+
console.log(` Remaining: ${status.backupCodesRemaining > 0 ? chalk.cyan(status.backupCodesRemaining) : chalk.yellow('0')}`);
|
|
142
|
+
if (status.backupCodesRemaining === 0) {
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(chalk.yellow('You have no backup codes remaining.'));
|
|
145
|
+
console.log(chalk.yellow('Run `lsvault mfa backup-codes --regenerate` to generate new codes.'));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
spinner.fail('Failed to fetch backup code count');
|
|
150
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
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
|
+
}
|