@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,256 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { LifestreamVaultClient } from '@lifestreamdynamics/vault-sdk';
4
+ import { loadConfigAsync, getCredentialManager } from '../config.js';
5
+ import { getClient } from '../client.js';
6
+ import { migrateCredentials, hasPlaintextCredentials, checkAndPromptMigration } from '../lib/migration.js';
7
+ export function registerAuthCommands(program) {
8
+ const auth = program.command('auth').description('Authentication and credential management');
9
+ auth.command('login')
10
+ .description('Authenticate with an API key or email/password credentials')
11
+ .option('--api-key <key>', 'API key (lsv_k_... prefix)')
12
+ .option('--email <email>', 'Email address for password login')
13
+ .option('--password <password>', 'Password (prompts interactively if omitted)')
14
+ .option('--api-url <url>', 'API server URL (default: http://localhost:4660)')
15
+ .addHelpText('after', `
16
+ EXAMPLES
17
+ lsvault auth login --api-key lsv_k_abc123
18
+ lsvault auth login --email user@example.com
19
+ lsvault auth login --email user@example.com --api-url https://api.example.com`)
20
+ .action(async (opts) => {
21
+ const cm = getCredentialManager();
22
+ // Set API URL first if provided
23
+ if (opts.apiUrl) {
24
+ try {
25
+ await cm.saveCredentials({ apiUrl: opts.apiUrl });
26
+ console.log(chalk.green(`API URL set to ${opts.apiUrl}`));
27
+ }
28
+ catch {
29
+ const { saveConfig } = await import('../config.js');
30
+ saveConfig({ apiUrl: opts.apiUrl });
31
+ console.log(chalk.green(`API URL set to ${opts.apiUrl}`));
32
+ }
33
+ }
34
+ // Password-based login
35
+ if (opts.email) {
36
+ const password = opts.password ?? await promptPassword();
37
+ if (!password) {
38
+ console.error(chalk.red('Password is required for email login.'));
39
+ return;
40
+ }
41
+ const config = await loadConfigAsync();
42
+ const apiUrl = opts.apiUrl ?? config.apiUrl;
43
+ const spinner = ora('Authenticating...').start();
44
+ try {
45
+ const { tokens, refreshToken } = await LifestreamVaultClient.login(apiUrl, opts.email, password);
46
+ // Save tokens to secure storage
47
+ await cm.saveCredentials({
48
+ accessToken: tokens.accessToken,
49
+ refreshToken: refreshToken ?? undefined,
50
+ });
51
+ spinner.succeed(`Logged in as ${chalk.cyan(tokens.user.email)}`);
52
+ console.log(` Name: ${tokens.user.name || chalk.dim('not set')}`);
53
+ console.log(` Role: ${tokens.user.role}`);
54
+ if (!refreshToken) {
55
+ console.log(chalk.yellow(' Note: No refresh token received. Session will expire.'));
56
+ }
57
+ }
58
+ catch (err) {
59
+ spinner.fail('Login failed');
60
+ console.error(err instanceof Error ? err.message : String(err));
61
+ }
62
+ return;
63
+ }
64
+ // API key login
65
+ if (opts.apiKey) {
66
+ const spinner = ora('Saving API key to secure storage...').start();
67
+ try {
68
+ await cm.saveCredentials({ apiKey: opts.apiKey });
69
+ const method = await cm.getStorageMethod();
70
+ spinner.succeed(`API key saved to ${formatMethod(method)}.`);
71
+ }
72
+ catch (err) {
73
+ spinner.fail('Failed to save API key to secure storage');
74
+ console.error(err instanceof Error ? err.message : String(err));
75
+ }
76
+ return;
77
+ }
78
+ if (!opts.apiUrl) {
79
+ console.log('Usage: lsvault auth login --api-key <key> [--api-url <url>]');
80
+ console.log(' or: lsvault auth login --email <email> [--password <pass>] [--api-url <url>]');
81
+ }
82
+ });
83
+ auth.command('refresh')
84
+ .description('Refresh the JWT access token using the stored refresh token')
85
+ .action(async () => {
86
+ const cm = getCredentialManager();
87
+ const config = await loadConfigAsync();
88
+ if (!config.refreshToken) {
89
+ console.error(chalk.red('No refresh token stored. Login first with --email.'));
90
+ return;
91
+ }
92
+ const spinner = ora('Refreshing access token...').start();
93
+ try {
94
+ // Create a client with the current tokens to trigger refresh
95
+ const client = new LifestreamVaultClient({
96
+ baseUrl: config.apiUrl,
97
+ accessToken: config.accessToken || 'expired',
98
+ refreshToken: config.refreshToken,
99
+ refreshBufferMs: Number.MAX_SAFE_INTEGER, // Force immediate refresh
100
+ onTokenRefresh: async (tokens) => {
101
+ await cm.saveCredentials({
102
+ accessToken: tokens.accessToken,
103
+ });
104
+ },
105
+ });
106
+ // Trigger the refresh by making a request
107
+ const user = await client.user.me();
108
+ spinner.succeed(`Token refreshed. Logged in as ${chalk.cyan(user.email)}`);
109
+ }
110
+ catch (err) {
111
+ spinner.fail('Token refresh failed');
112
+ console.error(err instanceof Error ? err.message : String(err));
113
+ console.log(chalk.dim('You may need to log in again: lsvault auth login --email <email>'));
114
+ }
115
+ });
116
+ auth.command('logout')
117
+ .description('Clear all stored credentials from keychain and config')
118
+ .action(async () => {
119
+ const cm = getCredentialManager();
120
+ const spinner = ora('Clearing credentials...').start();
121
+ try {
122
+ await cm.clearCredentials();
123
+ spinner.succeed('All credentials cleared.');
124
+ }
125
+ catch (err) {
126
+ spinner.fail('Failed to clear credentials');
127
+ console.error(err instanceof Error ? err.message : String(err));
128
+ }
129
+ });
130
+ auth.command('status')
131
+ .description('Show credential storage method, auth type, and connection info')
132
+ .action(async () => {
133
+ const cm = getCredentialManager();
134
+ const method = await cm.getStorageMethod();
135
+ const config = await loadConfigAsync();
136
+ console.log(chalk.bold('Credential Storage Status'));
137
+ console.log(` Storage method: ${formatMethod(method)}`);
138
+ console.log(` API URL: ${config.apiUrl}`);
139
+ console.log(` API Key: ${config.apiKey ? config.apiKey.slice(0, 12) + '...' : chalk.yellow('not set')}`);
140
+ console.log(` JWT Auth: ${config.accessToken ? chalk.green('active') : chalk.dim('not set')}`);
141
+ console.log(` Refresh Token: ${config.refreshToken ? chalk.green('stored') : chalk.dim('not set')}`);
142
+ if (hasPlaintextCredentials()) {
143
+ console.log('');
144
+ console.log(chalk.yellow(' Warning: Plaintext credentials found in ~/.lsvault/config.json'));
145
+ console.log(chalk.yellow(' Run `lsvault auth migrate` to migrate to secure storage.'));
146
+ }
147
+ });
148
+ auth.command('migrate')
149
+ .description('Migrate plaintext credentials from config.json to secure storage')
150
+ .action(async () => {
151
+ if (!hasPlaintextCredentials()) {
152
+ console.log('No plaintext credentials found. Nothing to migrate.');
153
+ return;
154
+ }
155
+ const cm = getCredentialManager();
156
+ const spinner = ora('Migrating credentials to secure storage...').start();
157
+ const result = await migrateCredentials(cm);
158
+ if (result.migrated) {
159
+ spinner.succeed(`API key migrated to ${formatMethod(result.method)}.`);
160
+ }
161
+ else if (result.error) {
162
+ spinner.fail(`Migration failed: ${result.error}`);
163
+ }
164
+ else {
165
+ spinner.info('Migration skipped.');
166
+ }
167
+ });
168
+ auth.command('whoami')
169
+ .description('Show the currently authenticated user, plan, and API URL')
170
+ .action(async () => {
171
+ const config = await loadConfigAsync();
172
+ console.log(`API URL: ${config.apiUrl}`);
173
+ console.log(`API Key: ${config.apiKey ? config.apiKey.slice(0, 12) + '...' : chalk.yellow('not set')}`);
174
+ if (config.accessToken) {
175
+ console.log(`Auth: ${chalk.green('JWT (email/password)')}`);
176
+ }
177
+ // Warn about plaintext credentials
178
+ await checkAndPromptMigration(getCredentialManager());
179
+ if (config.apiKey || config.accessToken) {
180
+ const spinner = ora('Fetching user info...').start();
181
+ try {
182
+ const client = getClient();
183
+ const user = await client.user.me();
184
+ spinner.stop();
185
+ console.log(`User: ${chalk.cyan(user.email)}`);
186
+ console.log(`Name: ${user.name || chalk.dim('not set')}`);
187
+ console.log(`Role: ${user.role}`);
188
+ console.log(`Plan: ${chalk.green(user.subscriptionTier)}`);
189
+ }
190
+ catch (err) {
191
+ spinner.fail('Could not fetch user info');
192
+ console.error(err instanceof Error ? err.message : err);
193
+ }
194
+ }
195
+ });
196
+ }
197
+ /**
198
+ * Prompt for a password from stdin (non-echoing).
199
+ * Returns the password or null if stdin is not a TTY.
200
+ */
201
+ async function promptPassword() {
202
+ // In non-interactive mode, cannot prompt
203
+ if (!process.stdin.isTTY) {
204
+ return null;
205
+ }
206
+ const readline = await import('node:readline');
207
+ return new Promise((resolve) => {
208
+ const rl = readline.createInterface({
209
+ input: process.stdin,
210
+ output: process.stderr,
211
+ terminal: true,
212
+ });
213
+ // Disable echoing
214
+ process.stderr.write('Password: ');
215
+ process.stdin.setRawMode?.(true);
216
+ let password = '';
217
+ const onData = (chunk) => {
218
+ const char = chunk.toString('utf-8');
219
+ if (char === '\n' || char === '\r' || char === '\u0004') {
220
+ process.stderr.write('\n');
221
+ process.stdin.setRawMode?.(false);
222
+ process.stdin.removeListener('data', onData);
223
+ rl.close();
224
+ resolve(password);
225
+ }
226
+ else if (char === '\u0003') {
227
+ // Ctrl+C
228
+ process.stderr.write('\n');
229
+ process.stdin.setRawMode?.(false);
230
+ process.stdin.removeListener('data', onData);
231
+ rl.close();
232
+ resolve(null);
233
+ }
234
+ else if (char === '\u007F' || char === '\b') {
235
+ // Backspace
236
+ if (password.length > 0) {
237
+ password = password.slice(0, -1);
238
+ }
239
+ }
240
+ else {
241
+ password += char;
242
+ }
243
+ };
244
+ process.stdin.on('data', onData);
245
+ process.stdin.resume();
246
+ });
247
+ }
248
+ function formatMethod(method) {
249
+ switch (method) {
250
+ case 'keychain': return chalk.green('OS Keychain');
251
+ case 'encrypted-config': return chalk.cyan('Encrypted Config (~/.lsvault/credentials.enc)');
252
+ case 'env': return chalk.blue('Environment Variable');
253
+ case 'plaintext-config': return chalk.yellow('Plaintext Config (deprecated)');
254
+ default: return chalk.dim(method);
255
+ }
256
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerConfigCommands(program: Command): void;
@@ -0,0 +1,130 @@
1
+ import chalk from 'chalk';
2
+ import { resolveProfileName, getActiveProfile, setActiveProfile, loadProfile, setProfileValue, getProfileValue, listProfiles, deleteProfile, } from '../lib/profiles.js';
3
+ export function registerConfigCommands(program) {
4
+ const config = program
5
+ .command('config')
6
+ .description('Manage CLI configuration profiles')
7
+ .addHelpText('after', `
8
+ EXAMPLES
9
+ lsvault config set apiUrl https://api.example.com --profile prod
10
+ lsvault config set apiKey lsv_k_abc123 --profile prod
11
+ lsvault config get apiUrl --profile prod
12
+ lsvault config list --profile prod
13
+ lsvault config use prod
14
+ lsvault config profiles`);
15
+ config
16
+ .command('set')
17
+ .description('Set a configuration value in a profile')
18
+ .argument('<key>', 'Configuration key (e.g., apiUrl, apiKey)')
19
+ .argument('<value>', 'Configuration value')
20
+ .option('-p, --profile <name>', 'Profile name (default: active profile)')
21
+ .addHelpText('after', `
22
+ EXAMPLES
23
+ lsvault config set apiUrl https://api.lifestream.com --profile prod
24
+ lsvault config set apiKey lsv_k_prod... --profile prod
25
+ lsvault config set apiUrl http://localhost:4660 --profile dev`)
26
+ .action((key, value, opts) => {
27
+ const profile = resolveProfileName(opts.profile);
28
+ setProfileValue(profile, key, value);
29
+ console.log(chalk.green(`Set ${chalk.bold(key)} in profile ${chalk.bold(profile)}`));
30
+ });
31
+ config
32
+ .command('get')
33
+ .description('Get a configuration value from a profile')
34
+ .argument('<key>', 'Configuration key to read')
35
+ .option('-p, --profile <name>', 'Profile name (default: active profile)')
36
+ .addHelpText('after', `
37
+ EXAMPLES
38
+ lsvault config get apiUrl
39
+ lsvault config get apiKey --profile prod`)
40
+ .action((key, opts) => {
41
+ const profile = resolveProfileName(opts.profile);
42
+ const value = getProfileValue(profile, key);
43
+ if (value !== undefined) {
44
+ console.log(value);
45
+ }
46
+ else {
47
+ console.log(chalk.yellow(`Key "${key}" not set in profile "${profile}"`));
48
+ }
49
+ });
50
+ config
51
+ .command('list')
52
+ .description('List all configuration values in a profile')
53
+ .option('-p, --profile <name>', 'Profile name (default: active profile)')
54
+ .addHelpText('after', `
55
+ EXAMPLES
56
+ lsvault config list
57
+ lsvault config list --profile prod`)
58
+ .action((opts) => {
59
+ const profile = resolveProfileName(opts.profile);
60
+ const profileConfig = loadProfile(profile);
61
+ const keys = Object.keys(profileConfig);
62
+ if (keys.length === 0) {
63
+ console.log(chalk.yellow(`Profile "${profile}" has no configuration values.`));
64
+ return;
65
+ }
66
+ console.log(chalk.bold(`Profile: ${profile}\n`));
67
+ for (const key of keys) {
68
+ const value = profileConfig[key];
69
+ // Mask API keys for display
70
+ const display = key.toLowerCase().includes('key') && value
71
+ ? value.slice(0, 12) + '...'
72
+ : value;
73
+ console.log(` ${chalk.cyan(key)}: ${display}`);
74
+ }
75
+ });
76
+ config
77
+ .command('use')
78
+ .description('Set the default active profile')
79
+ .argument('<name>', 'Profile name to activate')
80
+ .addHelpText('after', `
81
+ EXAMPLES
82
+ lsvault config use prod
83
+ lsvault config use dev`)
84
+ .action((name) => {
85
+ setActiveProfile(name);
86
+ console.log(chalk.green(`Active profile set to ${chalk.bold(name)}`));
87
+ });
88
+ config
89
+ .command('profiles')
90
+ .description('List all available profiles')
91
+ .addHelpText('after', `
92
+ EXAMPLES
93
+ lsvault config profiles`)
94
+ .action(() => {
95
+ const profiles = listProfiles();
96
+ const active = getActiveProfile();
97
+ if (profiles.length === 0) {
98
+ console.log(chalk.yellow('No profiles configured.'));
99
+ console.log(chalk.dim('Create one with: lsvault config set <key> <value> --profile <name>'));
100
+ return;
101
+ }
102
+ console.log(chalk.bold('Available profiles:\n'));
103
+ for (const name of profiles) {
104
+ const marker = name === active ? chalk.green(' (active)') : '';
105
+ console.log(` ${chalk.cyan(name)}${marker}`);
106
+ }
107
+ });
108
+ config
109
+ .command('delete')
110
+ .description('Delete a configuration profile')
111
+ .argument('<name>', 'Profile name to delete')
112
+ .addHelpText('after', `
113
+ EXAMPLES
114
+ lsvault config delete staging`)
115
+ .action((name) => {
116
+ if (deleteProfile(name)) {
117
+ console.log(chalk.green(`Profile "${name}" deleted.`));
118
+ }
119
+ else {
120
+ console.log(chalk.yellow(`Profile "${name}" not found.`));
121
+ }
122
+ });
123
+ config
124
+ .command('current')
125
+ .description('Show the active profile name')
126
+ .action(() => {
127
+ const active = getActiveProfile();
128
+ console.log(active);
129
+ });
130
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerConnectorCommands(program: Command): void;
@@ -0,0 +1,224 @@
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 registerConnectorCommands(program) {
6
+ const connectors = program.command('connectors').description('Manage external service connectors (e.g., Google Drive)');
7
+ addGlobalFlags(connectors.command('list')
8
+ .description('List connectors')
9
+ .option('--vault <vaultId>', 'Filter by vault ID'))
10
+ .action(async (_opts) => {
11
+ const flags = resolveFlags(_opts);
12
+ const out = createOutput(flags);
13
+ out.startSpinner('Fetching connectors...');
14
+ try {
15
+ const client = getClient();
16
+ const connectorList = await client.connectors.list(_opts.vault);
17
+ out.stopSpinner();
18
+ out.list(connectorList.map(c => ({
19
+ name: c.name,
20
+ id: c.id,
21
+ provider: c.provider,
22
+ status: c.status,
23
+ syncDirection: c.syncDirection,
24
+ })), {
25
+ emptyMessage: 'No connectors found.',
26
+ columns: [
27
+ { key: 'name', header: 'Name' },
28
+ { key: 'provider', header: 'Provider' },
29
+ { key: 'status', header: 'Status' },
30
+ { key: 'syncDirection', header: 'Direction' },
31
+ ],
32
+ textFn: (c) => {
33
+ const status = String(c.status) === 'active' ? chalk.green(String(c.status)) :
34
+ String(c.status) === 'error' ? chalk.red(String(c.status)) : chalk.dim(String(c.status));
35
+ return `${chalk.cyan(String(c.name))} ${chalk.dim(`(${String(c.id)})`)} -- ${String(c.provider)} ${status} [${String(c.syncDirection)}]`;
36
+ },
37
+ });
38
+ }
39
+ catch (err) {
40
+ handleError(out, err, 'Failed to fetch connectors');
41
+ }
42
+ });
43
+ addGlobalFlags(connectors.command('get')
44
+ .description('Get connector details')
45
+ .argument('<connectorId>', 'Connector ID'))
46
+ .action(async (connectorId, _opts) => {
47
+ const flags = resolveFlags(_opts);
48
+ const out = createOutput(flags);
49
+ out.startSpinner('Fetching connector...');
50
+ try {
51
+ const client = getClient();
52
+ const c = await client.connectors.get(connectorId);
53
+ out.stopSpinner();
54
+ out.record({
55
+ name: c.name,
56
+ id: c.id,
57
+ provider: c.provider,
58
+ vaultId: c.vaultId,
59
+ syncDirection: c.syncDirection,
60
+ syncPath: c.syncPath,
61
+ status: c.status,
62
+ isActive: c.isActive,
63
+ lastSyncAt: c.lastSyncAt,
64
+ createdAt: c.createdAt,
65
+ updatedAt: c.updatedAt,
66
+ });
67
+ }
68
+ catch (err) {
69
+ handleError(out, err, 'Failed to fetch connector');
70
+ }
71
+ });
72
+ addGlobalFlags(connectors.command('create')
73
+ .description('Create a connector')
74
+ .argument('<provider>', 'Provider (e.g., google_drive)')
75
+ .argument('<name>', 'Connector name')
76
+ .requiredOption('--vault <vaultId>', 'Vault ID')
77
+ .requiredOption('-d, --direction <direction>', 'Sync direction (pull, push, bidirectional)')
78
+ .option('-p, --sync-path <path>', 'Sync path prefix'))
79
+ .action(async (provider, name, _opts) => {
80
+ const flags = resolveFlags(_opts);
81
+ const out = createOutput(flags);
82
+ out.startSpinner('Creating connector...');
83
+ try {
84
+ const client = getClient();
85
+ const connector = await client.connectors.create({
86
+ provider: provider,
87
+ name,
88
+ vaultId: String(_opts.vault),
89
+ syncDirection: String(_opts.direction),
90
+ syncPath: _opts.syncPath,
91
+ });
92
+ out.success(`Connector created: ${chalk.cyan(connector.name)} (${connector.id})`, {
93
+ id: connector.id,
94
+ name: connector.name,
95
+ provider: connector.provider,
96
+ });
97
+ }
98
+ catch (err) {
99
+ handleError(out, err, 'Failed to create connector');
100
+ }
101
+ });
102
+ addGlobalFlags(connectors.command('update')
103
+ .description('Update a connector')
104
+ .argument('<connectorId>', 'Connector ID')
105
+ .option('-n, --name <name>', 'New name')
106
+ .option('-d, --direction <direction>', 'New sync direction'))
107
+ .action(async (connectorId, _opts) => {
108
+ const flags = resolveFlags(_opts);
109
+ const out = createOutput(flags);
110
+ out.startSpinner('Updating connector...');
111
+ try {
112
+ const client = getClient();
113
+ const params = {};
114
+ if (_opts.name)
115
+ params.name = _opts.name;
116
+ if (_opts.direction)
117
+ params.syncDirection = _opts.direction;
118
+ const connector = await client.connectors.update(connectorId, params);
119
+ out.success(`Connector updated: ${chalk.cyan(connector.name)}`, {
120
+ id: connector.id,
121
+ name: connector.name,
122
+ });
123
+ }
124
+ catch (err) {
125
+ handleError(out, err, 'Failed to update connector');
126
+ }
127
+ });
128
+ addGlobalFlags(connectors.command('delete')
129
+ .description('Delete a connector')
130
+ .argument('<connectorId>', 'Connector ID'))
131
+ .action(async (connectorId, _opts) => {
132
+ const flags = resolveFlags(_opts);
133
+ const out = createOutput(flags);
134
+ out.startSpinner('Deleting connector...');
135
+ try {
136
+ const client = getClient();
137
+ await client.connectors.delete(connectorId);
138
+ out.success('Connector deleted.', { id: connectorId, deleted: true });
139
+ }
140
+ catch (err) {
141
+ handleError(out, err, 'Failed to delete connector');
142
+ }
143
+ });
144
+ addGlobalFlags(connectors.command('test')
145
+ .description('Test a connector connection')
146
+ .argument('<connectorId>', 'Connector ID'))
147
+ .action(async (connectorId, _opts) => {
148
+ const flags = resolveFlags(_opts);
149
+ const out = createOutput(flags);
150
+ out.startSpinner('Testing connection...');
151
+ try {
152
+ const client = getClient();
153
+ const result = await client.connectors.test(connectorId);
154
+ if (result.success) {
155
+ out.success('Connection test passed.', { success: true });
156
+ }
157
+ else {
158
+ out.failSpinner(`Connection test failed: ${result.error || 'Unknown error'}`);
159
+ if (flags.output === 'json') {
160
+ out.record({ success: false, error: result.error });
161
+ }
162
+ process.exitCode = 1;
163
+ }
164
+ }
165
+ catch (err) {
166
+ handleError(out, err, 'Failed to test connection');
167
+ }
168
+ });
169
+ addGlobalFlags(connectors.command('sync')
170
+ .description('Trigger a sync for a connector')
171
+ .argument('<connectorId>', 'Connector ID'))
172
+ .action(async (connectorId, _opts) => {
173
+ const flags = resolveFlags(_opts);
174
+ const out = createOutput(flags);
175
+ out.startSpinner('Triggering sync...');
176
+ try {
177
+ const client = getClient();
178
+ const result = await client.connectors.sync(connectorId);
179
+ out.success(result.message, { message: result.message });
180
+ }
181
+ catch (err) {
182
+ handleError(out, err, 'Failed to trigger sync');
183
+ }
184
+ });
185
+ addGlobalFlags(connectors.command('logs')
186
+ .description('View sync logs for a connector')
187
+ .argument('<connectorId>', 'Connector ID'))
188
+ .action(async (connectorId, _opts) => {
189
+ const flags = resolveFlags(_opts);
190
+ const out = createOutput(flags);
191
+ out.startSpinner('Fetching sync logs...');
192
+ try {
193
+ const client = getClient();
194
+ const logs = await client.connectors.logs(connectorId);
195
+ out.stopSpinner();
196
+ out.list(logs.map(log => ({
197
+ status: log.status,
198
+ createdAt: log.createdAt,
199
+ filesAdded: log.filesAdded,
200
+ filesUpdated: log.filesUpdated,
201
+ filesDeleted: log.filesDeleted,
202
+ durationMs: log.durationMs,
203
+ })), {
204
+ emptyMessage: 'No sync logs found.',
205
+ columns: [
206
+ { key: 'status', header: 'Status' },
207
+ { key: 'createdAt', header: 'Time' },
208
+ { key: 'filesAdded', header: 'Added' },
209
+ { key: 'filesUpdated', header: 'Updated' },
210
+ { key: 'filesDeleted', header: 'Deleted' },
211
+ { key: 'durationMs', header: 'Duration (ms)' },
212
+ ],
213
+ textFn: (log) => {
214
+ const status = String(log.status) === 'success' ? chalk.green(String(log.status)) : chalk.red(String(log.status));
215
+ const duration = log.durationMs ? `${String(log.durationMs)}ms` : 'n/a';
216
+ return `${status} ${chalk.dim(String(log.createdAt))} -- +${String(log.filesAdded)} ~${String(log.filesUpdated)} -${String(log.filesDeleted)} (${duration})`;
217
+ },
218
+ });
219
+ }
220
+ catch (err) {
221
+ handleError(out, err, 'Failed to fetch sync logs');
222
+ }
223
+ });
224
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerDocCommands(program: Command): void;