@benzid.wael/secure-vault 0.0.1
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 +268 -0
- package/bin/cli.js +83 -0
- package/bin/commands/env.js +807 -0
- package/package.json +167 -0
- package/src/electron/models/EnvironmentVault.js +251 -0
- package/src/electron/models/Vault.js +87 -0
- package/src/electron/services/CryptographyService.js +54 -0
- package/src/electron/services/EnvironmentVaultService.js +564 -0
- package/src/electron/services/ImportExportService.js +126 -0
- package/src/electron/services/MenuService.js +110 -0
- package/src/electron/services/SecurityManager.js +109 -0
- package/src/electron/services/VaultFileService.js +137 -0
- package/src/electron/services/VaultRecoveryService.js +134 -0
- package/src/electron/services/VaultService.js +578 -0
- package/src/electron/services/VaultSettingsService.js +78 -0
- package/src/electron/services/WindowManager.js +266 -0
- package/src/electron/services/recovery/IRecoveryMethod.js +88 -0
- package/src/electron/services/recovery/KeyRecoveryService.js +245 -0
- package/src/electron/services/recovery/PasswordRecoveryService.js +128 -0
- package/src/electron/services/recovery/SecretQuestionRecoveryService.js +267 -0
- package/src/electron/services/recovery/UsbRecoveryService.js +244 -0
- package/src/electron/utils/appPaths.js +50 -0
- package/src/electron/utils/passwordValidation.js +29 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import password from '@inquirer/password';
|
|
8
|
+
|
|
9
|
+
import { EnvironmentVaultService } from '../../src/electron/services/EnvironmentVaultService.js';
|
|
10
|
+
import { EnvironmentVault } from '../../src/electron/models/EnvironmentVault.js';
|
|
11
|
+
|
|
12
|
+
function clipboardWrite(text) {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
const cmd =
|
|
15
|
+
platform === 'darwin' ? 'pbcopy' : platform === 'win32' ? 'clip' : 'xclip';
|
|
16
|
+
const args = platform === 'linux' ? ['-selection', 'clipboard'] : [];
|
|
17
|
+
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const proc = spawn(cmd, args, { stdio: 'pipe' });
|
|
20
|
+
proc.on('error', () => reject(new Error('Clipboard not available')));
|
|
21
|
+
proc.on('close', resolve);
|
|
22
|
+
proc.stdin.end(text);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function copyToClipboard(text, label) {
|
|
27
|
+
try {
|
|
28
|
+
await clipboardWrite(text);
|
|
29
|
+
console.log(chalk.gray(` Copied ${label} to clipboard`));
|
|
30
|
+
} catch {
|
|
31
|
+
console.log(chalk.yellow(' Clipboard not available on this system'));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseEnvOption(val) {
|
|
36
|
+
const entries = {};
|
|
37
|
+
for (const pair of val.split(',')) {
|
|
38
|
+
const sep = pair.includes('=') ? '=' : ':';
|
|
39
|
+
const idx = pair.indexOf(sep);
|
|
40
|
+
if (idx === -1) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid --env format: "${pair}". Expected envName=filePath`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const envName = pair.slice(0, idx);
|
|
46
|
+
let filePath = pair.slice(idx + 1).trim();
|
|
47
|
+
if (filePath.startsWith('~')) {
|
|
48
|
+
filePath = os.homedir() + filePath.slice(1);
|
|
49
|
+
}
|
|
50
|
+
if (!envName || !filePath) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Invalid --env format: "${pair}". Expected envName=filePath`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
entries[envName.trim()] = filePath;
|
|
56
|
+
}
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getPassword(provided, promptMessage) {
|
|
61
|
+
if (provided) return provided;
|
|
62
|
+
if (process.env.VAULT_ENV_PASSWORD) return process.env.VAULT_ENV_PASSWORD;
|
|
63
|
+
return password({ message: promptMessage, mask: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolveVaultPath(options) {
|
|
67
|
+
const vaultPath = EnvironmentVaultService.resolveVaultPath({
|
|
68
|
+
vault: options.vault,
|
|
69
|
+
name: options.name,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!vaultPath) {
|
|
73
|
+
const cwdName = process
|
|
74
|
+
.cwd()
|
|
75
|
+
.split(/[/\\]/)
|
|
76
|
+
.pop()
|
|
77
|
+
.replace(/[^a-z0-9_-]/gi, '_')
|
|
78
|
+
.toLowerCase();
|
|
79
|
+
console.log(chalk.red('No vault found.'));
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.yellow(` Create one: vault env init --name ${cwdName}`)
|
|
82
|
+
);
|
|
83
|
+
console.log(chalk.yellow(` Specify: --vault <path> or --name <name>`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return vaultPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadVault(options) {
|
|
91
|
+
const vaultPassword = await getPassword(
|
|
92
|
+
options.password,
|
|
93
|
+
'Enter vault password:'
|
|
94
|
+
);
|
|
95
|
+
const vaultPath = await resolveVaultPath(options);
|
|
96
|
+
const result = await EnvironmentVaultService.loadVault(
|
|
97
|
+
vaultPath,
|
|
98
|
+
vaultPassword
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!result.success) {
|
|
102
|
+
console.log(chalk.red(result.error));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { vaultPath, vaultPassword, vault: result.data };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function registerEnvCommand(program) {
|
|
110
|
+
const env = program.command('env').description('Manage environment vaults');
|
|
111
|
+
|
|
112
|
+
env
|
|
113
|
+
.command('init')
|
|
114
|
+
.description('Initialize a new environment vault')
|
|
115
|
+
.option('-n, --name <name>', 'Vault name (defaults to CWD directory name)')
|
|
116
|
+
.option('-v, --vault <path>', 'Exact path for the vault file')
|
|
117
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
118
|
+
.option(
|
|
119
|
+
'-e, --env <pairs>',
|
|
120
|
+
'Import .env files: envName=path (or envName:path)',
|
|
121
|
+
(val, prev = {}) => ({ ...prev, ...parseEnvOption(val) }),
|
|
122
|
+
{}
|
|
123
|
+
)
|
|
124
|
+
.action(async (options) => {
|
|
125
|
+
try {
|
|
126
|
+
const vaultPassword = await getPassword(
|
|
127
|
+
options.password,
|
|
128
|
+
'Enter vault password:'
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!options.password && !process.env.VAULT_ENV_PASSWORD) {
|
|
132
|
+
const confirm = await password({
|
|
133
|
+
message: 'Confirm vault password:',
|
|
134
|
+
mask: true,
|
|
135
|
+
});
|
|
136
|
+
if (vaultPassword !== confirm) {
|
|
137
|
+
console.log(chalk.red('Passwords do not match.'));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const vaultPath = options.vault
|
|
143
|
+
? path.resolve(options.vault)
|
|
144
|
+
: options.name
|
|
145
|
+
? EnvironmentVaultService.getEnvVaultPath(options.name)
|
|
146
|
+
: EnvironmentVaultService.defaultVaultPath();
|
|
147
|
+
|
|
148
|
+
const vaultModel = new EnvironmentVault();
|
|
149
|
+
const envs = options.env || {};
|
|
150
|
+
|
|
151
|
+
for (const [envName, envFile] of Object.entries(envs)) {
|
|
152
|
+
const step = ora(
|
|
153
|
+
`Reading ${chalk.cyan(envName)} from ${chalk.gray(envFile)}`
|
|
154
|
+
).start();
|
|
155
|
+
let content;
|
|
156
|
+
try {
|
|
157
|
+
content = await fs.readFile(envFile, 'utf-8');
|
|
158
|
+
} catch (err) {
|
|
159
|
+
step.fail(chalk.red(`Cannot read ${envFile}: ${err.message}`));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
const parsed = EnvironmentVault.parseEnvFile(content);
|
|
163
|
+
const count = Object.keys(parsed).length;
|
|
164
|
+
vaultModel.importFromEnvFile(envName, content, {
|
|
165
|
+
message: 'Initial import',
|
|
166
|
+
});
|
|
167
|
+
step.succeed(`Imported ${chalk.bold(envName)} (${count} variables)`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const step = ora('Encrypting vault…').start();
|
|
171
|
+
const result = await EnvironmentVaultService.createVault(
|
|
172
|
+
vaultPath,
|
|
173
|
+
vaultPassword,
|
|
174
|
+
vaultModel.toJSON()
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!result.success) {
|
|
178
|
+
step.fail(chalk.red(result.error));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
step.succeed(
|
|
183
|
+
chalk.green(`Environment vault created at ${result.path}`)
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(chalk.red(error.message));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
env
|
|
192
|
+
.command('import')
|
|
193
|
+
.description('Import .env files into an existing vault')
|
|
194
|
+
.argument('<envName>', 'Environment name (e.g. staging, production)')
|
|
195
|
+
.argument('[files...]', 'One or more .env files to import')
|
|
196
|
+
.option('-n, --name <name>', 'Vault name')
|
|
197
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
198
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
199
|
+
.action(async (envName, files, options) => {
|
|
200
|
+
const spinner = ora('Importing .env files...').start();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const vaultPassword = await getPassword(
|
|
204
|
+
options.password,
|
|
205
|
+
'Enter vault password:'
|
|
206
|
+
);
|
|
207
|
+
const vaultPath = await resolveVaultPath(options);
|
|
208
|
+
|
|
209
|
+
if (!files || files.length === 0) {
|
|
210
|
+
spinner.fail(chalk.red('No .env files specified.'));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
spinner.text = `Importing ${file} as "${envName}"...`;
|
|
216
|
+
|
|
217
|
+
const result = await EnvironmentVaultService.importEnvFile(
|
|
218
|
+
vaultPath,
|
|
219
|
+
vaultPassword,
|
|
220
|
+
envName,
|
|
221
|
+
file
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (!result.success) {
|
|
225
|
+
spinner.fail(chalk.red(`${file}: ${result.error}`));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
spinner.succeed(
|
|
231
|
+
chalk.green(`Imported ${files.length} file(s) into "${envName}".`)
|
|
232
|
+
);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
spinner.fail(chalk.red(error.message));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
env
|
|
240
|
+
.command('set')
|
|
241
|
+
.description('Set an environment variable')
|
|
242
|
+
.argument('<key>', 'Variable name')
|
|
243
|
+
.argument('<value>', 'Variable value')
|
|
244
|
+
.option('-n, --name <name>', 'Vault name')
|
|
245
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
246
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
247
|
+
.option('-e, --env <name>', 'Environment name (defaults to "default")')
|
|
248
|
+
.option('--public', 'Mark variable as non-sensitive')
|
|
249
|
+
.option('-m, --message <text>', 'Version message')
|
|
250
|
+
.action(async (key, value, options) => {
|
|
251
|
+
try {
|
|
252
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
253
|
+
const envName = options.env || 'default';
|
|
254
|
+
|
|
255
|
+
const spinner = ora(`Setting ${key}...`).start();
|
|
256
|
+
|
|
257
|
+
const result = await EnvironmentVaultService.setEnv(
|
|
258
|
+
vaultPath,
|
|
259
|
+
vaultPassword,
|
|
260
|
+
envName,
|
|
261
|
+
key,
|
|
262
|
+
value,
|
|
263
|
+
{ isPublic: !!options.public, message: options.message }
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (!result.success) {
|
|
267
|
+
spinner.fail(chalk.red(result.error));
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
spinner.succeed(chalk.green(`Set ${key} in "${envName}".`));
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.log(chalk.red(error.message));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
env
|
|
279
|
+
.command('get')
|
|
280
|
+
.description('Get an environment variable')
|
|
281
|
+
.argument('<key>', 'Variable name')
|
|
282
|
+
.option('-n, --name <name>', 'Vault name')
|
|
283
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
284
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
285
|
+
.option('-e, --env <name>', 'Environment name (defaults to "default")')
|
|
286
|
+
.option('--pair', 'Output as KEY=VALUE')
|
|
287
|
+
.option('--clip', 'Copy value to clipboard')
|
|
288
|
+
.action(async (key, options) => {
|
|
289
|
+
try {
|
|
290
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
291
|
+
const envName = options.env || 'default';
|
|
292
|
+
|
|
293
|
+
const result = await EnvironmentVaultService.getEnv(
|
|
294
|
+
vaultPath,
|
|
295
|
+
vaultPassword,
|
|
296
|
+
envName,
|
|
297
|
+
key
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (!result.success) {
|
|
301
|
+
console.log(chalk.red(result.error));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (options.pair) {
|
|
306
|
+
const pair = `${key}=${result.data.value}`;
|
|
307
|
+
console.log(pair);
|
|
308
|
+
if (options.clip) await copyToClipboard(pair, 'pair');
|
|
309
|
+
} else if (options.clip) {
|
|
310
|
+
await copyToClipboard(result.data.value, 'value');
|
|
311
|
+
} else {
|
|
312
|
+
console.log(result.data.value);
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.log(chalk.red(error.message));
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
env
|
|
321
|
+
.command('show')
|
|
322
|
+
.description('Show environment details and all variables')
|
|
323
|
+
.argument('[envName]', 'Environment name (defaults to "default")')
|
|
324
|
+
.option('-n, --name <name>', 'Vault name')
|
|
325
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
326
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
327
|
+
.action(async (envName, options) => {
|
|
328
|
+
try {
|
|
329
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
330
|
+
const name = envName || 'default';
|
|
331
|
+
|
|
332
|
+
const result = await EnvironmentVaultService.showEnv(
|
|
333
|
+
vaultPath,
|
|
334
|
+
vaultPassword,
|
|
335
|
+
name
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (!result.success) {
|
|
339
|
+
console.log(chalk.red(result.error));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const { data } = result;
|
|
344
|
+
console.log(chalk.bold(`\nEnvironment: ${data.name}`));
|
|
345
|
+
console.log(
|
|
346
|
+
chalk.gray(
|
|
347
|
+
` Active version: v${data.activeVersion} of ${data.totalVersions}`
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
console.log(chalk.gray(` Variables: ${data.keyCount}\n`));
|
|
351
|
+
|
|
352
|
+
for (const k of data.keys) {
|
|
353
|
+
const icon = k.sensitive ? chalk.red('🔒') : chalk.green('🔓');
|
|
354
|
+
const display = k.sensitive ? chalk.gray('***') : k.value;
|
|
355
|
+
console.log(` ${icon} ${chalk.cyan(k.key)} = ${display}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log();
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.log(chalk.red(error.message));
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
env
|
|
366
|
+
.command('list')
|
|
367
|
+
.description('List all environments in the vault')
|
|
368
|
+
.option('-n, --name <name>', 'Vault name')
|
|
369
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
370
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
371
|
+
.action(async (options) => {
|
|
372
|
+
try {
|
|
373
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
374
|
+
|
|
375
|
+
const result = await EnvironmentVaultService.listEnvs(
|
|
376
|
+
vaultPath,
|
|
377
|
+
vaultPassword
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (!result.success) {
|
|
381
|
+
console.log(chalk.red(result.error));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (result.data.length === 0) {
|
|
386
|
+
console.log(chalk.yellow('\n No environments in this vault.\n'));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
console.log(chalk.bold(`\nEnvironments (${result.data.length}):\n`));
|
|
391
|
+
for (const env of result.data) {
|
|
392
|
+
console.log(
|
|
393
|
+
` ${chalk.cyan(env.name)}` +
|
|
394
|
+
` (v${env.activeVersion}, ${env.keyCount} keys, ${env.versionCount} versions)`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
console.log();
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.log(chalk.red(error.message));
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
env
|
|
405
|
+
.command('rm')
|
|
406
|
+
.description('Remove a variable from an environment')
|
|
407
|
+
.argument('<key>', 'Variable name to remove')
|
|
408
|
+
.option('-n, --name <name>', 'Vault name')
|
|
409
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
410
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
411
|
+
.option('-e, --env <name>', 'Environment name (defaults to "default")')
|
|
412
|
+
.action(async (key, options) => {
|
|
413
|
+
const spinner = ora(`Removing ${key}...`).start();
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
417
|
+
const envName = options.env || 'default';
|
|
418
|
+
|
|
419
|
+
const result = await EnvironmentVaultService.removeKey(
|
|
420
|
+
vaultPath,
|
|
421
|
+
vaultPassword,
|
|
422
|
+
envName,
|
|
423
|
+
key
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
if (!result.success) {
|
|
427
|
+
spinner.fail(chalk.red(result.error));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
spinner.succeed(chalk.green(`Removed ${key} from "${envName}".`));
|
|
432
|
+
} catch (error) {
|
|
433
|
+
spinner.fail(chalk.red(error.message));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
env
|
|
439
|
+
.command('export')
|
|
440
|
+
.description('Export an environment as dotenv or JSON')
|
|
441
|
+
.argument('[envName]', 'Environment name (defaults to "default")')
|
|
442
|
+
.option('-n, --name <name>', 'Vault name')
|
|
443
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
444
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
445
|
+
.option(
|
|
446
|
+
'-f, --format <format>',
|
|
447
|
+
'Output format: dotenv (default) or json',
|
|
448
|
+
'dotenv'
|
|
449
|
+
)
|
|
450
|
+
.option('--clip', 'Copy output to clipboard')
|
|
451
|
+
.action(async (envName, options) => {
|
|
452
|
+
try {
|
|
453
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
454
|
+
const name = envName || 'default';
|
|
455
|
+
|
|
456
|
+
const result = await EnvironmentVaultService.exportEnv(
|
|
457
|
+
vaultPath,
|
|
458
|
+
vaultPassword,
|
|
459
|
+
name,
|
|
460
|
+
options.format
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
if (!result.success) {
|
|
464
|
+
console.log(chalk.red(result.error));
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (options.clip) {
|
|
469
|
+
const text =
|
|
470
|
+
typeof result.data === 'string'
|
|
471
|
+
? result.data
|
|
472
|
+
: JSON.stringify(result.data, null, 2);
|
|
473
|
+
await copyToClipboard(text, 'export');
|
|
474
|
+
console.log(text);
|
|
475
|
+
} else {
|
|
476
|
+
console.log(result.data);
|
|
477
|
+
}
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.log(chalk.red(error.message));
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
env
|
|
485
|
+
.command('delete')
|
|
486
|
+
.description('Delete an entire environment')
|
|
487
|
+
.argument('<envName>', 'Environment name to delete')
|
|
488
|
+
.option('-n, --name <name>', 'Vault name')
|
|
489
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
490
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
491
|
+
.action(async (envName, options) => {
|
|
492
|
+
const spinner = ora(`Deleting environment "${envName}"...`).start();
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
496
|
+
|
|
497
|
+
const result = await EnvironmentVaultService.deleteEnv(
|
|
498
|
+
vaultPath,
|
|
499
|
+
vaultPassword,
|
|
500
|
+
envName
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (!result.success) {
|
|
504
|
+
spinner.fail(chalk.red(result.error));
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
spinner.succeed(chalk.green(`Deleted environment "${envName}".`));
|
|
509
|
+
} catch (error) {
|
|
510
|
+
spinner.fail(chalk.red(error.message));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
env
|
|
516
|
+
.command('rename')
|
|
517
|
+
.description('Rename an environment')
|
|
518
|
+
.argument('<oldName>', 'Current environment name')
|
|
519
|
+
.argument('<newName>', 'New environment name')
|
|
520
|
+
.option('-n, --name <name>', 'Vault name')
|
|
521
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
522
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
523
|
+
.action(async (oldName, newName, options) => {
|
|
524
|
+
const spinner = ora(`Renaming "${oldName}" to "${newName}"...`).start();
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
528
|
+
|
|
529
|
+
const result = await EnvironmentVaultService.renameEnv(
|
|
530
|
+
vaultPath,
|
|
531
|
+
vaultPassword,
|
|
532
|
+
oldName,
|
|
533
|
+
newName
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
if (!result.success) {
|
|
537
|
+
spinner.fail(chalk.red(result.error));
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
spinner.succeed(chalk.green(`Renamed "${oldName}" to "${newName}".`));
|
|
542
|
+
} catch (error) {
|
|
543
|
+
spinner.fail(chalk.red(error.message));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
env
|
|
549
|
+
.command('template')
|
|
550
|
+
.description('Generate a .env.template from an environment')
|
|
551
|
+
.argument('[envName]', 'Environment name (defaults to "default")')
|
|
552
|
+
.option('-n, --name <name>', 'Vault name')
|
|
553
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
554
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
555
|
+
.option('--clip', 'Copy template to clipboard')
|
|
556
|
+
.action(async (envName, options) => {
|
|
557
|
+
try {
|
|
558
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
559
|
+
const name = envName || 'default';
|
|
560
|
+
|
|
561
|
+
const result = await EnvironmentVaultService.templateEnv(
|
|
562
|
+
vaultPath,
|
|
563
|
+
vaultPassword,
|
|
564
|
+
name
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (!result.success) {
|
|
568
|
+
console.log(chalk.red(result.error));
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
console.log(result.data);
|
|
573
|
+
if (options.clip) await copyToClipboard(result.data, 'template');
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.log(chalk.red(error.message));
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
env
|
|
581
|
+
.command('history')
|
|
582
|
+
.description('Show version history for an environment')
|
|
583
|
+
.argument('[envName]', 'Environment name (defaults to "default")')
|
|
584
|
+
.option('-n, --name <name>', 'Vault name')
|
|
585
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
586
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
587
|
+
.action(async (envName, options) => {
|
|
588
|
+
try {
|
|
589
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
590
|
+
const name = envName || 'default';
|
|
591
|
+
|
|
592
|
+
const result = await EnvironmentVaultService.getHistory(
|
|
593
|
+
vaultPath,
|
|
594
|
+
vaultPassword,
|
|
595
|
+
name
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
if (!result.success) {
|
|
599
|
+
console.log(chalk.red(result.error));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (result.data.length === 0) {
|
|
604
|
+
console.log(chalk.yellow('\n No history for this environment.\n'));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.log(
|
|
609
|
+
chalk.bold(
|
|
610
|
+
`\nHistory for "${name}" (${result.data.length} versions):\n`
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
for (const v of result.data) {
|
|
615
|
+
const active = v.active ? chalk.green(' (active)') : '';
|
|
616
|
+
const msg = v.message ? ` - ${v.message}` : '';
|
|
617
|
+
console.log(` v${v.n}${active}${chalk.gray(msg)}`);
|
|
618
|
+
for (const [key, val] of Object.entries(v.vars)) {
|
|
619
|
+
const icon = v.nonSensitive?.includes(key) ? '🔓' : '🔒';
|
|
620
|
+
console.log(` ${icon} ${chalk.cyan(key)}=${val}`);
|
|
621
|
+
}
|
|
622
|
+
console.log();
|
|
623
|
+
}
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.log(chalk.red(error.message));
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
env
|
|
631
|
+
.command('rollback')
|
|
632
|
+
.description('Rollback to a previous version')
|
|
633
|
+
.argument('<versionN>', 'Version number to rollback to', Number)
|
|
634
|
+
.option('-n, --name <name>', 'Vault name')
|
|
635
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
636
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
637
|
+
.option('-e, --env <name>', 'Environment name (defaults to "default")')
|
|
638
|
+
.action(async (versionN, options) => {
|
|
639
|
+
const spinner = ora(`Rolling back to v${versionN}...`).start();
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
643
|
+
const envName = options.env || 'default';
|
|
644
|
+
|
|
645
|
+
const result = await EnvironmentVaultService.rollbackEnv(
|
|
646
|
+
vaultPath,
|
|
647
|
+
vaultPassword,
|
|
648
|
+
envName,
|
|
649
|
+
versionN
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
if (!result.success) {
|
|
653
|
+
spinner.fail(chalk.red(result.error));
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
spinner.succeed(
|
|
658
|
+
chalk.green(`Rolled back "${envName}" to v${versionN}.`)
|
|
659
|
+
);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
spinner.fail(chalk.red(error.message));
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
env
|
|
667
|
+
.command('squash')
|
|
668
|
+
.description('Squash version history')
|
|
669
|
+
.option('-n, --name <name>', 'Vault name')
|
|
670
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
671
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
672
|
+
.option('-e, --env <name>', 'Environment name (defaults to "default")')
|
|
673
|
+
.option('-k, --keep <count>', 'Number of versions to keep', Number, 1)
|
|
674
|
+
.action(async (options) => {
|
|
675
|
+
const spinner = ora(
|
|
676
|
+
`Squashing history (keeping ${options.keep})...`
|
|
677
|
+
).start();
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
681
|
+
const envName = options.env || 'default';
|
|
682
|
+
|
|
683
|
+
const result = await EnvironmentVaultService.squashEnv(
|
|
684
|
+
vaultPath,
|
|
685
|
+
vaultPassword,
|
|
686
|
+
envName,
|
|
687
|
+
options.keep
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if (!result.success) {
|
|
691
|
+
spinner.fail(chalk.red(result.error));
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
spinner.succeed(
|
|
696
|
+
chalk.green(
|
|
697
|
+
`Squashed "${envName}" history to ${options.keep} version(s).`
|
|
698
|
+
)
|
|
699
|
+
);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
spinner.fail(chalk.red(error.message));
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
env
|
|
707
|
+
.command('diff')
|
|
708
|
+
.description('Diff two environments')
|
|
709
|
+
.argument('<envA>', 'First environment')
|
|
710
|
+
.argument('<envB>', 'Second environment')
|
|
711
|
+
.option('-n, --name <name>', 'Vault name')
|
|
712
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
713
|
+
.option('--password <password>', 'Vault password (non-interactive)')
|
|
714
|
+
.action(async (envA, envB, options) => {
|
|
715
|
+
try {
|
|
716
|
+
const { vaultPath, vaultPassword } = await loadVault(options);
|
|
717
|
+
|
|
718
|
+
const result = await EnvironmentVaultService.diffEnvs(
|
|
719
|
+
vaultPath,
|
|
720
|
+
vaultPassword,
|
|
721
|
+
envA,
|
|
722
|
+
envB
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
if (!result.success) {
|
|
726
|
+
console.log(chalk.red(result.error));
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const { data } = result;
|
|
731
|
+
|
|
732
|
+
console.log(chalk.bold(`\nDiff: ${envA} → ${envB}\n`));
|
|
733
|
+
|
|
734
|
+
if (data.added.length > 0) {
|
|
735
|
+
console.log(chalk.green(' Added:'));
|
|
736
|
+
for (const k of data.added) console.log(` + ${chalk.cyan(k)}`);
|
|
737
|
+
}
|
|
738
|
+
if (data.removed.length > 0) {
|
|
739
|
+
console.log(chalk.red(' Removed:'));
|
|
740
|
+
for (const k of data.removed) console.log(` - ${chalk.cyan(k)}`);
|
|
741
|
+
}
|
|
742
|
+
if (data.changed.length > 0) {
|
|
743
|
+
console.log(chalk.yellow(' Changed:'));
|
|
744
|
+
for (const k of data.changed) console.log(` ~ ${chalk.cyan(k)}`);
|
|
745
|
+
}
|
|
746
|
+
if (data.unchanged.length > 0) {
|
|
747
|
+
console.log(
|
|
748
|
+
chalk.gray(` Unchanged: ${data.unchanged.length} key(s)`)
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
console.log();
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.log(chalk.red(error.message));
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
env
|
|
760
|
+
.command('change-password')
|
|
761
|
+
.description('Change the vault password')
|
|
762
|
+
.option('-n, --name <name>', 'Vault name')
|
|
763
|
+
.option('-v, --vault <path>', 'Exact vault file path')
|
|
764
|
+
.option('--password <password>', 'Current vault password (non-interactive)')
|
|
765
|
+
.action(async (options) => {
|
|
766
|
+
const spinner = ora('Changing vault password...').start();
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const vaultPath = await resolveVaultPath(options);
|
|
770
|
+
const currentPassword = await getPassword(
|
|
771
|
+
options.password,
|
|
772
|
+
'Enter current password:'
|
|
773
|
+
);
|
|
774
|
+
const newPassword = await password({
|
|
775
|
+
message: 'Enter new password:',
|
|
776
|
+
mask: true,
|
|
777
|
+
});
|
|
778
|
+
const confirmPassword = await password({
|
|
779
|
+
message: 'Confirm new password:',
|
|
780
|
+
mask: true,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
if (newPassword !== confirmPassword) {
|
|
784
|
+
spinner.fail(chalk.red('Passwords do not match.'));
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const result = await EnvironmentVaultService.changePassword(
|
|
789
|
+
vaultPath,
|
|
790
|
+
currentPassword,
|
|
791
|
+
newPassword
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
if (!result.success) {
|
|
795
|
+
spinner.fail(chalk.red(result.error));
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
spinner.succeed(chalk.green('Vault password changed.'));
|
|
800
|
+
} catch (error) {
|
|
801
|
+
spinner.fail(chalk.red(error.message));
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
return env;
|
|
807
|
+
}
|