@lifestreamdynamics/vault-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +759 -0
  3. package/dist/client.d.ts +12 -0
  4. package/dist/client.js +79 -0
  5. package/dist/commands/admin.d.ts +2 -0
  6. package/dist/commands/admin.js +263 -0
  7. package/dist/commands/audit.d.ts +2 -0
  8. package/dist/commands/audit.js +119 -0
  9. package/dist/commands/auth.d.ts +2 -0
  10. package/dist/commands/auth.js +256 -0
  11. package/dist/commands/config.d.ts +2 -0
  12. package/dist/commands/config.js +130 -0
  13. package/dist/commands/connectors.d.ts +2 -0
  14. package/dist/commands/connectors.js +224 -0
  15. package/dist/commands/docs.d.ts +2 -0
  16. package/dist/commands/docs.js +194 -0
  17. package/dist/commands/hooks.d.ts +2 -0
  18. package/dist/commands/hooks.js +159 -0
  19. package/dist/commands/keys.d.ts +2 -0
  20. package/dist/commands/keys.js +165 -0
  21. package/dist/commands/publish.d.ts +2 -0
  22. package/dist/commands/publish.js +138 -0
  23. package/dist/commands/search.d.ts +2 -0
  24. package/dist/commands/search.js +61 -0
  25. package/dist/commands/shares.d.ts +2 -0
  26. package/dist/commands/shares.js +121 -0
  27. package/dist/commands/subscription.d.ts +2 -0
  28. package/dist/commands/subscription.js +166 -0
  29. package/dist/commands/sync.d.ts +2 -0
  30. package/dist/commands/sync.js +565 -0
  31. package/dist/commands/teams.d.ts +2 -0
  32. package/dist/commands/teams.js +322 -0
  33. package/dist/commands/user.d.ts +2 -0
  34. package/dist/commands/user.js +48 -0
  35. package/dist/commands/vaults.d.ts +2 -0
  36. package/dist/commands/vaults.js +157 -0
  37. package/dist/commands/versions.d.ts +2 -0
  38. package/dist/commands/versions.js +219 -0
  39. package/dist/commands/webhooks.d.ts +2 -0
  40. package/dist/commands/webhooks.js +181 -0
  41. package/dist/config.d.ts +24 -0
  42. package/dist/config.js +88 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +63 -0
  45. package/dist/lib/credential-manager.d.ts +48 -0
  46. package/dist/lib/credential-manager.js +101 -0
  47. package/dist/lib/encrypted-config.d.ts +20 -0
  48. package/dist/lib/encrypted-config.js +102 -0
  49. package/dist/lib/keychain.d.ts +8 -0
  50. package/dist/lib/keychain.js +82 -0
  51. package/dist/lib/migration.d.ts +31 -0
  52. package/dist/lib/migration.js +92 -0
  53. package/dist/lib/profiles.d.ts +43 -0
  54. package/dist/lib/profiles.js +104 -0
  55. package/dist/sync/config.d.ts +32 -0
  56. package/dist/sync/config.js +100 -0
  57. package/dist/sync/conflict.d.ts +30 -0
  58. package/dist/sync/conflict.js +60 -0
  59. package/dist/sync/daemon-worker.d.ts +1 -0
  60. package/dist/sync/daemon-worker.js +128 -0
  61. package/dist/sync/daemon.d.ts +44 -0
  62. package/dist/sync/daemon.js +174 -0
  63. package/dist/sync/diff.d.ts +43 -0
  64. package/dist/sync/diff.js +166 -0
  65. package/dist/sync/engine.d.ts +41 -0
  66. package/dist/sync/engine.js +233 -0
  67. package/dist/sync/ignore.d.ts +16 -0
  68. package/dist/sync/ignore.js +72 -0
  69. package/dist/sync/remote-poller.d.ts +23 -0
  70. package/dist/sync/remote-poller.js +145 -0
  71. package/dist/sync/state.d.ts +32 -0
  72. package/dist/sync/state.js +98 -0
  73. package/dist/sync/types.d.ts +68 -0
  74. package/dist/sync/types.js +4 -0
  75. package/dist/sync/watcher.d.ts +23 -0
  76. package/dist/sync/watcher.js +207 -0
  77. package/dist/utils/flags.d.ts +18 -0
  78. package/dist/utils/flags.js +31 -0
  79. package/dist/utils/format.d.ts +2 -0
  80. package/dist/utils/format.js +22 -0
  81. package/dist/utils/output.d.ts +87 -0
  82. package/dist/utils/output.js +229 -0
  83. package/package.json +62 -0
@@ -0,0 +1,219 @@
1
+ import chalk from 'chalk';
2
+ import { getClient } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerVersionCommands(program) {
6
+ const versions = program.command('versions').description('View and manage document version history');
7
+ addGlobalFlags(versions.command('list')
8
+ .description('List version history for a document')
9
+ .argument('<vaultId>', 'Vault ID')
10
+ .argument('<path>', 'Document path (e.g., notes/todo.md)')
11
+ .addHelpText('after', `
12
+ EXAMPLES
13
+ lsvault versions list abc123 notes/todo.md`))
14
+ .action(async (vaultId, docPath, _opts) => {
15
+ const flags = resolveFlags(_opts);
16
+ const out = createOutput(flags);
17
+ out.startSpinner('Fetching versions...');
18
+ try {
19
+ const client = getClient();
20
+ const versionList = await client.documents.listVersions(vaultId, docPath);
21
+ out.stopSpinner();
22
+ out.list(versionList.map(v => ({
23
+ version: v.versionNum,
24
+ source: v.changeSource,
25
+ sizeBytes: v.sizeBytes,
26
+ pinned: v.isPinned ? 'yes' : 'no',
27
+ createdAt: v.createdAt,
28
+ })), {
29
+ emptyMessage: 'No versions found.',
30
+ columns: [
31
+ { key: 'version', header: 'Version' },
32
+ { key: 'source', header: 'Source' },
33
+ { key: 'sizeBytes', header: 'Size' },
34
+ { key: 'pinned', header: 'Pinned' },
35
+ { key: 'createdAt', header: 'Created' },
36
+ ],
37
+ textFn: (v) => {
38
+ const pin = v.pinned === 'yes' ? chalk.yellow(' [pinned]') : '';
39
+ return `v${String(v.version)} ${chalk.dim(String(v.source))} ${chalk.dim(String(v.sizeBytes) + 'B')} ${chalk.dim(String(v.createdAt))}${pin}`;
40
+ },
41
+ });
42
+ if (flags.output === 'text' && versionList.length > 0) {
43
+ out.status(chalk.dim(`\n${versionList.length} version(s)`));
44
+ }
45
+ }
46
+ catch (err) {
47
+ handleError(out, err, 'Failed to fetch versions');
48
+ }
49
+ });
50
+ addGlobalFlags(versions.command('view')
51
+ .description('View content of a specific version')
52
+ .argument('<vaultId>', 'Vault ID')
53
+ .argument('<path>', 'Document path')
54
+ .argument('<version>', 'Version number')
55
+ .addHelpText('after', `
56
+ EXAMPLES
57
+ lsvault versions view abc123 notes/todo.md 3`))
58
+ .action(async (vaultId, docPath, versionStr, _opts) => {
59
+ const flags = resolveFlags(_opts);
60
+ const out = createOutput(flags);
61
+ const versionNum = parseInt(versionStr, 10);
62
+ if (isNaN(versionNum)) {
63
+ out.error('Version must be a number');
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ try {
68
+ const client = getClient();
69
+ const version = await client.documents.getVersion(vaultId, docPath, versionNum);
70
+ if (version.content === null) {
71
+ out.error('Version content is no longer available (expired or pruned)');
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ out.raw(version.content);
76
+ }
77
+ catch (err) {
78
+ handleError(out, err, 'Failed to get version');
79
+ }
80
+ });
81
+ addGlobalFlags(versions.command('diff')
82
+ .description('Show diff between two versions')
83
+ .argument('<vaultId>', 'Vault ID')
84
+ .argument('<path>', 'Document path')
85
+ .argument('<from>', 'Source version number')
86
+ .argument('<to>', 'Target version number')
87
+ .addHelpText('after', `
88
+ EXAMPLES
89
+ lsvault versions diff abc123 notes/todo.md 1 3`))
90
+ .action(async (vaultId, docPath, fromStr, toStr, _opts) => {
91
+ const flags = resolveFlags(_opts);
92
+ const out = createOutput(flags);
93
+ const from = parseInt(fromStr, 10);
94
+ const to = parseInt(toStr, 10);
95
+ if (isNaN(from) || isNaN(to)) {
96
+ out.error('Version numbers must be integers');
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ out.startSpinner('Computing diff...');
101
+ try {
102
+ const client = getClient();
103
+ const diff = await client.documents.diffVersions(vaultId, docPath, from, to);
104
+ out.stopSpinner();
105
+ if (flags.output === 'json') {
106
+ out.raw(JSON.stringify(diff, null, 2));
107
+ }
108
+ else {
109
+ out.status(`Diff: v${diff.fromVersion} -> v${diff.toVersion}\n`);
110
+ for (const change of diff.changes) {
111
+ if (change.added) {
112
+ out.raw(chalk.green(`+ ${change.value.replace(/\n$/, '')}`));
113
+ }
114
+ else if (change.removed) {
115
+ out.raw(chalk.red(`- ${change.value.replace(/\n$/, '')}`));
116
+ }
117
+ else {
118
+ out.raw(chalk.dim(` ${change.value.replace(/\n$/, '')}`));
119
+ }
120
+ }
121
+ }
122
+ }
123
+ catch (err) {
124
+ handleError(out, err, 'Failed to compute diff');
125
+ }
126
+ });
127
+ addGlobalFlags(versions.command('restore')
128
+ .description('Restore a document to a previous version')
129
+ .argument('<vaultId>', 'Vault ID')
130
+ .argument('<path>', 'Document path')
131
+ .argument('<version>', 'Version number to restore')
132
+ .addHelpText('after', `
133
+ EXAMPLES
134
+ lsvault versions restore abc123 notes/todo.md 2`))
135
+ .action(async (vaultId, docPath, versionStr, _opts) => {
136
+ const flags = resolveFlags(_opts);
137
+ const out = createOutput(flags);
138
+ const versionNum = parseInt(versionStr, 10);
139
+ if (isNaN(versionNum)) {
140
+ out.error('Version must be a number');
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+ out.startSpinner(`Restoring to version ${versionNum}...`);
145
+ try {
146
+ const client = getClient();
147
+ const doc = await client.documents.restoreVersion(vaultId, docPath, versionNum);
148
+ out.success(`Restored ${chalk.cyan(docPath)} to version ${versionNum}`, {
149
+ path: doc.path,
150
+ version: versionNum,
151
+ });
152
+ }
153
+ catch (err) {
154
+ handleError(out, err, 'Failed to restore version');
155
+ }
156
+ });
157
+ addGlobalFlags(versions.command('pin')
158
+ .description('Pin a version to prevent pruning')
159
+ .argument('<vaultId>', 'Vault ID')
160
+ .argument('<path>', 'Document path')
161
+ .argument('<version>', 'Version number to pin')
162
+ .addHelpText('after', `
163
+ EXAMPLES
164
+ lsvault versions pin abc123 notes/todo.md 5`))
165
+ .action(async (vaultId, docPath, versionStr, _opts) => {
166
+ const flags = resolveFlags(_opts);
167
+ const out = createOutput(flags);
168
+ const versionNum = parseInt(versionStr, 10);
169
+ if (isNaN(versionNum)) {
170
+ out.error('Version must be a number');
171
+ process.exitCode = 1;
172
+ return;
173
+ }
174
+ out.startSpinner(`Pinning version ${versionNum}...`);
175
+ try {
176
+ const client = getClient();
177
+ await client.documents.pinVersion(vaultId, docPath, versionNum);
178
+ out.success(`Pinned version ${versionNum} of ${chalk.cyan(docPath)}`, {
179
+ path: docPath,
180
+ version: versionNum,
181
+ pinned: true,
182
+ });
183
+ }
184
+ catch (err) {
185
+ handleError(out, err, 'Failed to pin version');
186
+ }
187
+ });
188
+ addGlobalFlags(versions.command('unpin')
189
+ .description('Unpin a version, allowing it to be pruned')
190
+ .argument('<vaultId>', 'Vault ID')
191
+ .argument('<path>', 'Document path')
192
+ .argument('<version>', 'Version number to unpin')
193
+ .addHelpText('after', `
194
+ EXAMPLES
195
+ lsvault versions unpin abc123 notes/todo.md 5`))
196
+ .action(async (vaultId, docPath, versionStr, _opts) => {
197
+ const flags = resolveFlags(_opts);
198
+ const out = createOutput(flags);
199
+ const versionNum = parseInt(versionStr, 10);
200
+ if (isNaN(versionNum)) {
201
+ out.error('Version must be a number');
202
+ process.exitCode = 1;
203
+ return;
204
+ }
205
+ out.startSpinner(`Unpinning version ${versionNum}...`);
206
+ try {
207
+ const client = getClient();
208
+ await client.documents.unpinVersion(vaultId, docPath, versionNum);
209
+ out.success(`Unpinned version ${versionNum} of ${chalk.cyan(docPath)}`, {
210
+ path: docPath,
211
+ version: versionNum,
212
+ pinned: false,
213
+ });
214
+ }
215
+ catch (err) {
216
+ handleError(out, err, 'Failed to unpin version');
217
+ }
218
+ });
219
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerWebhookCommands(program: Command): void;
@@ -0,0 +1,181 @@
1
+ import chalk from 'chalk';
2
+ import { getClient } from '../client.js';
3
+ import { addGlobalFlags, resolveFlags } from '../utils/flags.js';
4
+ import { createOutput, handleError } from '../utils/output.js';
5
+ export function registerWebhookCommands(program) {
6
+ const webhooks = program.command('webhooks').description('Manage vault webhooks');
7
+ addGlobalFlags(webhooks.command('list')
8
+ .description('List all webhooks for a vault')
9
+ .argument('<vaultId>', 'Vault ID'))
10
+ .action(async (vaultId, _opts) => {
11
+ const flags = resolveFlags(_opts);
12
+ const out = createOutput(flags);
13
+ out.startSpinner('Fetching webhooks...');
14
+ try {
15
+ const client = getClient();
16
+ const webhookList = await client.webhooks.list(vaultId);
17
+ out.stopSpinner();
18
+ out.list(webhookList.map(wh => ({
19
+ url: wh.url,
20
+ id: wh.id,
21
+ events: wh.events.join(', '),
22
+ isActive: wh.isActive,
23
+ })), {
24
+ emptyMessage: 'No webhooks found.',
25
+ columns: [
26
+ { key: 'url', header: 'URL' },
27
+ { key: 'events', header: 'Events' },
28
+ { key: 'isActive', header: 'Active' },
29
+ ],
30
+ textFn: (wh) => {
31
+ const lines = [chalk.cyan(` ${String(wh.url)}`)];
32
+ lines.push(` ID: ${String(wh.id)}`);
33
+ lines.push(` Events: ${String(wh.events)}`);
34
+ lines.push(` Active: ${wh.isActive ? chalk.green('Yes') : chalk.red('No')}`);
35
+ return lines.join('\n');
36
+ },
37
+ });
38
+ }
39
+ catch (err) {
40
+ handleError(out, err, 'Failed to fetch webhooks');
41
+ }
42
+ });
43
+ addGlobalFlags(webhooks.command('create')
44
+ .description('Create a new webhook')
45
+ .argument('<vaultId>', 'Vault ID')
46
+ .argument('<url>', 'Webhook endpoint URL')
47
+ .option('--events <events>', 'Comma-separated events (e.g., create,update,delete)', 'create,update,delete'))
48
+ .action(async (vaultId, url, _opts) => {
49
+ const flags = resolveFlags(_opts);
50
+ const out = createOutput(flags);
51
+ out.startSpinner('Creating webhook...');
52
+ try {
53
+ const client = getClient();
54
+ const params = {
55
+ url,
56
+ events: String(_opts.events || 'create,update,delete').split(',').map((e) => e.trim()),
57
+ };
58
+ const webhook = await client.webhooks.create(vaultId, params);
59
+ out.stopSpinner();
60
+ if (flags.output === 'json') {
61
+ out.record({
62
+ id: webhook.id,
63
+ url: webhook.url,
64
+ events: webhook.events.join(', '),
65
+ secret: webhook.secret,
66
+ });
67
+ }
68
+ else {
69
+ out.warn('\nIMPORTANT: Save this secret securely. It cannot be retrieved later.\n');
70
+ process.stdout.write(chalk.green.bold(`Secret: ${webhook.secret}\n`));
71
+ process.stdout.write(`\nID: ${webhook.id}\n`);
72
+ process.stdout.write(`URL: ${webhook.url}\n`);
73
+ process.stdout.write(`Events: ${webhook.events.join(', ')}\n`);
74
+ }
75
+ }
76
+ catch (err) {
77
+ handleError(out, err, 'Failed to create webhook');
78
+ }
79
+ });
80
+ addGlobalFlags(webhooks.command('update')
81
+ .description('Update a webhook')
82
+ .argument('<vaultId>', 'Vault ID')
83
+ .argument('<webhookId>', 'Webhook ID')
84
+ .option('--url <url>', 'New webhook URL')
85
+ .option('--events <events>', 'Comma-separated events')
86
+ .option('--active', 'Mark webhook as active')
87
+ .option('--inactive', 'Mark webhook as inactive'))
88
+ .action(async (vaultId, webhookId, _opts) => {
89
+ const flags = resolveFlags(_opts);
90
+ const out = createOutput(flags);
91
+ if (!_opts.url && !_opts.events && !_opts.active && !_opts.inactive) {
92
+ out.error('Must specify at least one update option (--url, --events, --active, or --inactive)');
93
+ process.exitCode = 2;
94
+ return;
95
+ }
96
+ out.startSpinner('Updating webhook...');
97
+ try {
98
+ const client = getClient();
99
+ const params = {};
100
+ if (_opts.url)
101
+ params.url = String(_opts.url);
102
+ if (_opts.events)
103
+ params.events = String(_opts.events).split(',').map((e) => e.trim());
104
+ if (_opts.active)
105
+ params.isActive = true;
106
+ if (_opts.inactive)
107
+ params.isActive = false;
108
+ const updated = await client.webhooks.update(vaultId, webhookId, params);
109
+ out.success('Webhook updated successfully', {
110
+ url: updated.url,
111
+ events: updated.events.join(', '),
112
+ isActive: updated.isActive,
113
+ });
114
+ }
115
+ catch (err) {
116
+ handleError(out, err, 'Failed to update webhook');
117
+ }
118
+ });
119
+ addGlobalFlags(webhooks.command('delete')
120
+ .description('Delete a webhook')
121
+ .argument('<vaultId>', 'Vault ID')
122
+ .argument('<webhookId>', 'Webhook ID'))
123
+ .action(async (vaultId, webhookId, _opts) => {
124
+ const flags = resolveFlags(_opts);
125
+ const out = createOutput(flags);
126
+ out.startSpinner('Deleting webhook...');
127
+ try {
128
+ const client = getClient();
129
+ await client.webhooks.delete(vaultId, webhookId);
130
+ out.success('Webhook deleted successfully', { id: webhookId, deleted: true });
131
+ }
132
+ catch (err) {
133
+ handleError(out, err, 'Failed to delete webhook');
134
+ }
135
+ });
136
+ addGlobalFlags(webhooks.command('deliveries')
137
+ .description('List recent deliveries for a webhook')
138
+ .argument('<vaultId>', 'Vault ID')
139
+ .argument('<webhookId>', 'Webhook ID'))
140
+ .action(async (vaultId, webhookId, _opts) => {
141
+ const flags = resolveFlags(_opts);
142
+ const out = createOutput(flags);
143
+ out.startSpinner('Fetching deliveries...');
144
+ try {
145
+ const client = getClient();
146
+ const deliveries = await client.webhooks.listDeliveries(vaultId, webhookId);
147
+ out.stopSpinner();
148
+ out.list(deliveries.map(d => ({
149
+ id: d.id,
150
+ statusCode: d.statusCode,
151
+ attempt: d.attempt,
152
+ error: d.error || null,
153
+ deliveredAt: d.deliveredAt || null,
154
+ createdAt: d.createdAt,
155
+ })), {
156
+ emptyMessage: 'No deliveries found.',
157
+ columns: [
158
+ { key: 'statusCode', header: 'Status' },
159
+ { key: 'id', header: 'ID' },
160
+ { key: 'attempt', header: 'Attempt' },
161
+ { key: 'createdAt', header: 'Time' },
162
+ ],
163
+ textFn: (d) => {
164
+ const statusStr = d.statusCode !== null
165
+ ? (Number(d.statusCode) < 300 ? chalk.green(String(d.statusCode)) : chalk.red(String(d.statusCode)))
166
+ : chalk.red('FAILED');
167
+ const lines = [` ${statusStr} ${String(d.id)} (attempt ${String(d.attempt)})`];
168
+ if (d.error)
169
+ lines.push(` Error: ${chalk.red(String(d.error))}`);
170
+ if (d.deliveredAt)
171
+ lines.push(` Delivered: ${new Date(String(d.deliveredAt)).toLocaleString()}`);
172
+ lines.push(` Created: ${new Date(String(d.createdAt)).toLocaleString()}`);
173
+ return lines.join('\n');
174
+ },
175
+ });
176
+ }
177
+ catch (err) {
178
+ handleError(out, err, 'Failed to fetch deliveries');
179
+ }
180
+ });
181
+ }
@@ -0,0 +1,24 @@
1
+ import { type CredentialManager } from './lib/credential-manager.js';
2
+ export interface CliConfig {
3
+ apiUrl: string;
4
+ apiKey?: string;
5
+ accessToken?: string;
6
+ refreshToken?: string;
7
+ }
8
+ export declare function getCredentialManager(): CredentialManager;
9
+ /**
10
+ * Sets the credential manager instance (for testing).
11
+ */
12
+ export declare function setCredentialManager(cm: CredentialManager): void;
13
+ /**
14
+ * Loads config synchronously from env vars and plaintext config file.
15
+ * This is the legacy loader — still used by commands that need sync access.
16
+ * For secure credential loading, use loadConfigAsync().
17
+ */
18
+ export declare function loadConfig(): CliConfig;
19
+ /**
20
+ * Loads config with secure credential resolution.
21
+ * Priority: env vars > keychain > encrypted config > plaintext config (deprecated).
22
+ */
23
+ export declare function loadConfigAsync(): Promise<CliConfig>;
24
+ export declare function saveConfig(config: Partial<CliConfig>): void;
package/dist/config.js ADDED
@@ -0,0 +1,88 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { createCredentialManager } from './lib/credential-manager.js';
5
+ const CONFIG_DIR = path.join(os.homedir(), '.lsvault');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+ // Singleton credential manager
8
+ let _credentialManager;
9
+ export function getCredentialManager() {
10
+ if (!_credentialManager) {
11
+ _credentialManager = createCredentialManager();
12
+ }
13
+ return _credentialManager;
14
+ }
15
+ /**
16
+ * Sets the credential manager instance (for testing).
17
+ */
18
+ export function setCredentialManager(cm) {
19
+ _credentialManager = cm;
20
+ }
21
+ /**
22
+ * Loads config synchronously from env vars and plaintext config file.
23
+ * This is the legacy loader — still used by commands that need sync access.
24
+ * For secure credential loading, use loadConfigAsync().
25
+ */
26
+ export function loadConfig() {
27
+ const config = {
28
+ apiUrl: process.env.LSVAULT_API_URL || 'http://localhost:4660',
29
+ };
30
+ if (process.env.LSVAULT_API_KEY) {
31
+ config.apiKey = process.env.LSVAULT_API_KEY;
32
+ }
33
+ // Read config file if it exists
34
+ if (fs.existsSync(CONFIG_FILE)) {
35
+ const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
36
+ if (fileConfig.apiUrl)
37
+ config.apiUrl = fileConfig.apiUrl;
38
+ if (fileConfig.apiKey && !config.apiKey)
39
+ config.apiKey = fileConfig.apiKey;
40
+ }
41
+ return config;
42
+ }
43
+ /**
44
+ * Loads config with secure credential resolution.
45
+ * Priority: env vars > keychain > encrypted config > plaintext config (deprecated).
46
+ */
47
+ export async function loadConfigAsync() {
48
+ const cm = getCredentialManager();
49
+ const secureCreds = await cm.getCredentials();
50
+ const config = {
51
+ apiUrl: secureCreds.apiUrl || process.env.LSVAULT_API_URL || 'http://localhost:4660',
52
+ };
53
+ // Load JWT tokens if available
54
+ if (secureCreds.accessToken) {
55
+ config.accessToken = secureCreds.accessToken;
56
+ }
57
+ if (secureCreds.refreshToken) {
58
+ config.refreshToken = secureCreds.refreshToken;
59
+ }
60
+ if (secureCreds.apiKey) {
61
+ config.apiKey = secureCreds.apiKey;
62
+ return config;
63
+ }
64
+ // JWT tokens are sufficient auth (no API key needed)
65
+ if (config.accessToken) {
66
+ return config;
67
+ }
68
+ // Fall back to plaintext config (deprecated)
69
+ if (fs.existsSync(CONFIG_FILE)) {
70
+ const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
71
+ if (fileConfig.apiUrl && !config.apiUrl)
72
+ config.apiUrl = fileConfig.apiUrl;
73
+ if (fileConfig.apiKey && !config.apiKey)
74
+ config.apiKey = fileConfig.apiKey;
75
+ }
76
+ return config;
77
+ }
78
+ export function saveConfig(config) {
79
+ if (!fs.existsSync(CONFIG_DIR)) {
80
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
81
+ }
82
+ let existing = {};
83
+ if (fs.existsSync(CONFIG_FILE)) {
84
+ existing = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
85
+ }
86
+ const merged = { ...existing, ...config };
87
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n');
88
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { registerAuthCommands } from './commands/auth.js';
4
+ import { registerVaultCommands } from './commands/vaults.js';
5
+ import { registerDocCommands } from './commands/docs.js';
6
+ import { registerSearchCommands } from './commands/search.js';
7
+ import { registerKeyCommands } from './commands/keys.js';
8
+ import { registerUserCommands } from './commands/user.js';
9
+ import { registerSubscriptionCommands } from './commands/subscription.js';
10
+ import { registerTeamCommands } from './commands/teams.js';
11
+ import { registerAuditCommands } from './commands/audit.js';
12
+ import { registerAdminCommands } from './commands/admin.js';
13
+ import { registerConnectorCommands } from './commands/connectors.js';
14
+ import { registerShareCommands } from './commands/shares.js';
15
+ import { registerPublishCommands } from './commands/publish.js';
16
+ import { registerHookCommands } from './commands/hooks.js';
17
+ import { registerWebhookCommands } from './commands/webhooks.js';
18
+ import { registerConfigCommands } from './commands/config.js';
19
+ import { registerSyncCommands } from './commands/sync.js';
20
+ import { registerVersionCommands } from './commands/versions.js';
21
+ const program = new Command();
22
+ program
23
+ .name('lsvault')
24
+ .description('Lifestream Vault CLI - manage vaults, documents, and settings')
25
+ .version('0.1.0')
26
+ .addHelpText('after', `
27
+ GETTING STARTED
28
+ lsvault auth login --email <email> Log in with email/password
29
+ lsvault auth login --api-key <key> Log in with an API key
30
+ lsvault config use <profile> Switch configuration profile
31
+
32
+ COMMON WORKFLOWS
33
+ lsvault vaults list List your vaults
34
+ lsvault docs list <vaultId> List documents in a vault
35
+ lsvault search <query> Search across documents
36
+ lsvault docs get <vaultId> <path> Print document content to stdout
37
+
38
+ CONFIGURATION
39
+ lsvault config set <key> <value> Set a config value
40
+ lsvault config profiles List available profiles
41
+
42
+ LEARN MORE
43
+ lsvault <command> --help Show help for a command
44
+ lsvault <command> <subcommand> --help Show help for a subcommand`);
45
+ registerAuthCommands(program);
46
+ registerVaultCommands(program);
47
+ registerDocCommands(program);
48
+ registerSearchCommands(program);
49
+ registerKeyCommands(program);
50
+ registerUserCommands(program);
51
+ registerSubscriptionCommands(program);
52
+ registerTeamCommands(program);
53
+ registerAuditCommands(program);
54
+ registerAdminCommands(program);
55
+ registerConnectorCommands(program);
56
+ registerShareCommands(program);
57
+ registerPublishCommands(program);
58
+ registerHookCommands(program);
59
+ registerWebhookCommands(program);
60
+ registerConfigCommands(program);
61
+ registerSyncCommands(program);
62
+ registerVersionCommands(program);
63
+ program.parse();
@@ -0,0 +1,48 @@
1
+ import type { CliConfig } from '../config.js';
2
+ import { type KeychainBackend } from './keychain.js';
3
+ import { type EncryptedConfigBackend } from './encrypted-config.js';
4
+ export type StorageMethod = 'env' | 'keychain' | 'encrypted-config' | 'plaintext-config' | 'none';
5
+ export interface CredentialManager {
6
+ /**
7
+ * Resolves credentials using the priority chain:
8
+ * 1. Environment variables
9
+ * 2. OS Keychain (keytar)
10
+ * 3. Encrypted config (~/.lsvault/credentials.enc)
11
+ *
12
+ * Does NOT read plaintext config — that's handled by loadConfig() for
13
+ * backwards compat with a migration warning.
14
+ */
15
+ getCredentials(): Promise<Partial<CliConfig>>;
16
+ /**
17
+ * Saves credentials to the best available backend.
18
+ * Prefers keychain, falls back to encrypted config.
19
+ */
20
+ saveCredentials(config: Partial<CliConfig>): Promise<void>;
21
+ /**
22
+ * Clears credentials from all secure backends.
23
+ */
24
+ clearCredentials(): Promise<void>;
25
+ /**
26
+ * Returns the storage method that currently holds credentials.
27
+ */
28
+ getStorageMethod(): Promise<StorageMethod>;
29
+ /**
30
+ * Retrieve the encryption key for a vault.
31
+ * Looks up from environment, then keychain, then encrypted config.
32
+ */
33
+ getVaultKey(vaultId: string): Promise<string | null>;
34
+ /**
35
+ * Save a vault encryption key to the best available backend.
36
+ */
37
+ saveVaultKey(vaultId: string, keyHex: string): Promise<void>;
38
+ /**
39
+ * Delete a vault encryption key from all backends.
40
+ */
41
+ deleteVaultKey(vaultId: string): Promise<void>;
42
+ }
43
+ export interface CredentialManagerOptions {
44
+ keychain?: KeychainBackend;
45
+ encryptedConfig?: EncryptedConfigBackend;
46
+ passphrase?: string;
47
+ }
48
+ export declare function createCredentialManager(options?: CredentialManagerOptions): CredentialManager;