@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.
- package/LICENSE +21 -0
- package/README.md +759 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.js +79 -0
- package/dist/commands/admin.d.ts +2 -0
- package/dist/commands/admin.js +263 -0
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.js +119 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +256 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +130 -0
- package/dist/commands/connectors.d.ts +2 -0
- package/dist/commands/connectors.js +224 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +194 -0
- package/dist/commands/hooks.d.ts +2 -0
- package/dist/commands/hooks.js +159 -0
- package/dist/commands/keys.d.ts +2 -0
- package/dist/commands/keys.js +165 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +138 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +61 -0
- package/dist/commands/shares.d.ts +2 -0
- package/dist/commands/shares.js +121 -0
- package/dist/commands/subscription.d.ts +2 -0
- package/dist/commands/subscription.js +166 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +565 -0
- package/dist/commands/teams.d.ts +2 -0
- package/dist/commands/teams.js +322 -0
- package/dist/commands/user.d.ts +2 -0
- package/dist/commands/user.js +48 -0
- package/dist/commands/vaults.d.ts +2 -0
- package/dist/commands/vaults.js +157 -0
- package/dist/commands/versions.d.ts +2 -0
- package/dist/commands/versions.js +219 -0
- package/dist/commands/webhooks.d.ts +2 -0
- package/dist/commands/webhooks.js +181 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -0
- package/dist/lib/credential-manager.d.ts +48 -0
- package/dist/lib/credential-manager.js +101 -0
- package/dist/lib/encrypted-config.d.ts +20 -0
- package/dist/lib/encrypted-config.js +102 -0
- package/dist/lib/keychain.d.ts +8 -0
- package/dist/lib/keychain.js +82 -0
- package/dist/lib/migration.d.ts +31 -0
- package/dist/lib/migration.js +92 -0
- package/dist/lib/profiles.d.ts +43 -0
- package/dist/lib/profiles.js +104 -0
- package/dist/sync/config.d.ts +32 -0
- package/dist/sync/config.js +100 -0
- package/dist/sync/conflict.d.ts +30 -0
- package/dist/sync/conflict.js +60 -0
- package/dist/sync/daemon-worker.d.ts +1 -0
- package/dist/sync/daemon-worker.js +128 -0
- package/dist/sync/daemon.d.ts +44 -0
- package/dist/sync/daemon.js +174 -0
- package/dist/sync/diff.d.ts +43 -0
- package/dist/sync/diff.js +166 -0
- package/dist/sync/engine.d.ts +41 -0
- package/dist/sync/engine.js +233 -0
- package/dist/sync/ignore.d.ts +16 -0
- package/dist/sync/ignore.js +72 -0
- package/dist/sync/remote-poller.d.ts +23 -0
- package/dist/sync/remote-poller.js +145 -0
- package/dist/sync/state.d.ts +32 -0
- package/dist/sync/state.js +98 -0
- package/dist/sync/types.d.ts +68 -0
- package/dist/sync/types.js +4 -0
- package/dist/sync/watcher.d.ts +23 -0
- package/dist/sync/watcher.js +207 -0
- package/dist/utils/flags.d.ts +18 -0
- package/dist/utils/flags.js +31 -0
- package/dist/utils/format.d.ts +2 -0
- package/dist/utils/format.js +22 -0
- package/dist/utils/output.d.ts +87 -0
- package/dist/utils/output.js +229 -0
- 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,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,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
|
+
}
|