@fentz26/envcp 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +5 -1
- package/dist/adapters/base.js.map +1 -1
- package/dist/cli/index.js +462 -34
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts +6 -0
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +243 -6
- package/dist/config/manager.js.map +1 -1
- package/dist/storage/index.d.ts +10 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +89 -6
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +12 -0
- package/dist/utils/crypto.js.map +1 -1
- package/package.json +6 -1
- package/.github/workflows/publish.yml +0 -48
- package/__tests__/config.test.ts +0 -65
- package/__tests__/crypto.test.ts +0 -76
- package/__tests__/http.test.ts +0 -49
- package/__tests__/storage.test.ts +0 -94
- package/jest.config.js +0 -11
- package/src/adapters/base.ts +0 -542
- package/src/adapters/gemini.ts +0 -228
- package/src/adapters/index.ts +0 -4
- package/src/adapters/openai.ts +0 -238
- package/src/adapters/rest.ts +0 -298
- package/src/cli/index.ts +0 -516
- package/src/cli.ts +0 -2
- package/src/config/manager.ts +0 -137
- package/src/index.ts +0 -4
- package/src/mcp/index.ts +0 -1
- package/src/mcp/server.ts +0 -67
- package/src/server/index.ts +0 -1
- package/src/server/unified.ts +0 -474
- package/src/storage/index.ts +0 -128
- package/src/types.ts +0 -183
- package/src/utils/crypto.ts +0 -100
- package/src/utils/http.ts +0 -119
- package/src/utils/session.ts +0 -146
- package/tsconfig.json +0 -20
package/dist/cli/index.js
CHANGED
|
@@ -3,13 +3,19 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as fs from 'fs-extra';
|
|
6
|
-
import { loadConfig, initConfig } from '../config/manager.js';
|
|
6
|
+
import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig } from '../config/manager.js';
|
|
7
7
|
import { StorageManager } from '../storage/index.js';
|
|
8
8
|
import { SessionManager } from '../utils/session.js';
|
|
9
|
-
import { maskValue, validatePassword } from '../utils/crypto.js';
|
|
9
|
+
import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
|
|
10
10
|
async function withSession(fn) {
|
|
11
11
|
const projectPath = process.cwd();
|
|
12
12
|
const config = await loadConfig(projectPath);
|
|
13
|
+
// Passwordless mode: no session, no password
|
|
14
|
+
if (config.encryption?.enabled === false) {
|
|
15
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), false);
|
|
16
|
+
await fn(storage, '', config, projectPath);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
13
19
|
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
14
20
|
await sessionManager.init();
|
|
15
21
|
let session = await sessionManager.load();
|
|
@@ -41,18 +47,139 @@ program
|
|
|
41
47
|
.command('init')
|
|
42
48
|
.description('Initialize EnvCP in the current project')
|
|
43
49
|
.option('-p, --project <name>', 'Project name')
|
|
44
|
-
.option('-
|
|
50
|
+
.option('--no-encrypt', 'Skip encryption (passwordless mode)')
|
|
51
|
+
.option('--skip-env', 'Skip .env auto-import')
|
|
52
|
+
.option('--skip-mcp', 'Skip MCP auto-registration')
|
|
45
53
|
.action(async (options) => {
|
|
46
54
|
const projectPath = process.cwd();
|
|
47
55
|
const projectName = options.project || path.basename(projectPath);
|
|
48
56
|
console.log(chalk.blue('Initializing EnvCP...'));
|
|
57
|
+
console.log('');
|
|
49
58
|
const config = await initConfig(projectPath, projectName);
|
|
50
|
-
|
|
59
|
+
// Single security question (or skip if --no-encrypt)
|
|
60
|
+
let securityChoice;
|
|
61
|
+
if (options.encrypt === false) {
|
|
62
|
+
securityChoice = 'none';
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const { mode } = await inquirer.prompt([
|
|
66
|
+
{
|
|
67
|
+
type: 'list',
|
|
68
|
+
name: 'mode',
|
|
69
|
+
message: 'How would you like to secure your variables?',
|
|
70
|
+
choices: [
|
|
71
|
+
{ name: 'No encryption (fastest setup, for local dev)', value: 'none' },
|
|
72
|
+
{ name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
|
|
73
|
+
{ name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
|
|
74
|
+
],
|
|
75
|
+
default: 'recoverable',
|
|
76
|
+
}
|
|
77
|
+
]);
|
|
78
|
+
securityChoice = mode;
|
|
79
|
+
}
|
|
80
|
+
// Apply security choice to config
|
|
81
|
+
if (securityChoice === 'none') {
|
|
82
|
+
config.encryption = { enabled: false };
|
|
83
|
+
config.storage.encrypted = false;
|
|
84
|
+
config.security = { mode: 'recoverable', recovery_file: '.envcp/.recovery' };
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
config.encryption = { enabled: true };
|
|
88
|
+
config.storage.encrypted = true;
|
|
89
|
+
config.security = { mode: securityChoice, recovery_file: '.envcp/.recovery' };
|
|
90
|
+
}
|
|
91
|
+
// For encrypted modes: get password now
|
|
92
|
+
let pwd = '';
|
|
93
|
+
if (securityChoice !== 'none') {
|
|
94
|
+
const { password } = await inquirer.prompt([
|
|
95
|
+
{ type: 'password', name: 'password', message: 'Set encryption password:', mask: '*' }
|
|
96
|
+
]);
|
|
97
|
+
const { confirm } = await inquirer.prompt([
|
|
98
|
+
{ type: 'password', name: 'confirm', message: 'Confirm password:', mask: '*' }
|
|
99
|
+
]);
|
|
100
|
+
if (password !== confirm) {
|
|
101
|
+
console.log(chalk.red('Passwords do not match. Aborting.'));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
pwd = password;
|
|
105
|
+
}
|
|
106
|
+
await saveConfig(config, projectPath);
|
|
107
|
+
const modeLabel = securityChoice === 'none' ? 'no encryption' : securityChoice;
|
|
108
|
+
console.log(chalk.green('EnvCP initialized!'));
|
|
51
109
|
console.log(chalk.gray(` Project: ${config.project}`));
|
|
52
|
-
console.log(chalk.gray(`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
110
|
+
console.log(chalk.gray(` Security: ${modeLabel}`));
|
|
111
|
+
if (securityChoice !== 'none') {
|
|
112
|
+
console.log(chalk.gray(` Session timeout: ${config.session?.timeout_minutes || 30} minutes`));
|
|
113
|
+
}
|
|
114
|
+
// Auto-import .env
|
|
115
|
+
if (!options.skipEnv) {
|
|
116
|
+
const envPath = path.join(projectPath, '.env');
|
|
117
|
+
if (await fs.pathExists(envPath)) {
|
|
118
|
+
const envContent = await fs.readFile(envPath, 'utf8');
|
|
119
|
+
const vars = parseEnvFile(envContent);
|
|
120
|
+
const count = Object.keys(vars).length;
|
|
121
|
+
if (count > 0) {
|
|
122
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
123
|
+
if (pwd)
|
|
124
|
+
storage.setPassword(pwd);
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
127
|
+
await storage.set(name, {
|
|
128
|
+
name, value,
|
|
129
|
+
encrypted: config.storage.encrypted,
|
|
130
|
+
created: now, updated: now,
|
|
131
|
+
sync_to_env: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Create session for encrypted mode
|
|
135
|
+
if (pwd) {
|
|
136
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
137
|
+
await sessionManager.init();
|
|
138
|
+
await sessionManager.create(pwd);
|
|
139
|
+
}
|
|
140
|
+
console.log(chalk.green(` Imported ${count} variables from .env`));
|
|
141
|
+
console.log(chalk.gray(` Variables: ${Object.keys(vars).join(', ')}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Generate recovery key for encrypted recoverable mode
|
|
146
|
+
if (securityChoice === 'recoverable' && pwd) {
|
|
147
|
+
const recoveryKey = generateRecoveryKey();
|
|
148
|
+
const recoveryData = createRecoveryData(pwd, recoveryKey);
|
|
149
|
+
const recoveryPath = path.join(projectPath, config.security.recovery_file);
|
|
150
|
+
await fs.writeFile(recoveryPath, recoveryData, 'utf8');
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(chalk.yellow.bold(' RECOVERY KEY (save this somewhere safe!):'));
|
|
153
|
+
console.log(chalk.yellow.bold(` ${recoveryKey}`));
|
|
154
|
+
console.log(chalk.gray(' This key is shown ONCE. If you lose it, you cannot recover your password.'));
|
|
155
|
+
}
|
|
156
|
+
// Auto-register MCP in all detected tools
|
|
157
|
+
if (!options.skipMcp) {
|
|
158
|
+
const result = await registerMcpConfig(projectPath);
|
|
159
|
+
console.log('');
|
|
160
|
+
if (result.registered.length > 0) {
|
|
161
|
+
console.log(chalk.green(' MCP registered:'));
|
|
162
|
+
for (const name of result.registered) {
|
|
163
|
+
console.log(chalk.gray(` + ${name}`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (result.alreadyConfigured.length > 0) {
|
|
167
|
+
for (const name of result.alreadyConfigured) {
|
|
168
|
+
console.log(chalk.gray(` = ${name} (already configured)`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (result.manual.length > 0) {
|
|
172
|
+
console.log(chalk.gray(' Manual setup needed:'));
|
|
173
|
+
for (const name of result.manual) {
|
|
174
|
+
console.log(chalk.gray(` ? ${name}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (result.registered.length === 0 && result.alreadyConfigured.length === 0) {
|
|
178
|
+
console.log(chalk.gray(' No AI tools detected for auto-registration'));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.green('Done! Your AI tools can now use EnvCP.'));
|
|
56
183
|
});
|
|
57
184
|
program
|
|
58
185
|
.command('unlock')
|
|
@@ -91,6 +218,21 @@ program
|
|
|
91
218
|
console.log(chalk.red('Passwords do not match'));
|
|
92
219
|
return;
|
|
93
220
|
}
|
|
221
|
+
// Generate recovery key for new stores in recoverable mode
|
|
222
|
+
if (config.security?.mode === 'recoverable') {
|
|
223
|
+
const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
|
|
224
|
+
if (!await fs.pathExists(recoveryPath)) {
|
|
225
|
+
const recoveryKey = generateRecoveryKey();
|
|
226
|
+
const recoveryData = createRecoveryData(password, recoveryKey);
|
|
227
|
+
await fs.ensureDir(path.dirname(recoveryPath));
|
|
228
|
+
await fs.writeFile(recoveryPath, recoveryData, 'utf8');
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(chalk.yellow.bold('RECOVERY KEY (save this somewhere safe!):'));
|
|
231
|
+
console.log(chalk.yellow.bold(` ${recoveryKey}`));
|
|
232
|
+
console.log(chalk.gray('This key is shown ONCE. If you lose it, you cannot recover your password.'));
|
|
233
|
+
console.log('');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
94
236
|
}
|
|
95
237
|
try {
|
|
96
238
|
await storage.load();
|
|
@@ -167,6 +309,105 @@ program
|
|
|
167
309
|
console.log(chalk.gray(` Remaining: ${sessionManager.getRemainingTime()} minutes`));
|
|
168
310
|
console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
|
|
169
311
|
});
|
|
312
|
+
program
|
|
313
|
+
.command('recover')
|
|
314
|
+
.description('Recover access using recovery key (reset password)')
|
|
315
|
+
.action(async () => {
|
|
316
|
+
const projectPath = process.cwd();
|
|
317
|
+
const config = await loadConfig(projectPath);
|
|
318
|
+
if (config.security?.mode === 'hard-lock') {
|
|
319
|
+
console.log(chalk.red('Recovery is not available in hard-lock mode.'));
|
|
320
|
+
console.log(chalk.gray('Hard-lock mode means lost password = lost data.'));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
324
|
+
if (!await fs.pathExists(recoveryPath)) {
|
|
325
|
+
console.log(chalk.red('No recovery file found. Recovery is not available.'));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const { recoveryKey } = await inquirer.prompt([
|
|
329
|
+
{ type: 'password', name: 'recoveryKey', message: 'Enter your recovery key:', mask: '*' }
|
|
330
|
+
]);
|
|
331
|
+
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
332
|
+
let oldPassword;
|
|
333
|
+
try {
|
|
334
|
+
oldPassword = recoverPassword(recoveryData, recoveryKey);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
console.log(chalk.red('Invalid recovery key.'));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Verify old password actually works by loading the store
|
|
341
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
342
|
+
storage.setPassword(oldPassword);
|
|
343
|
+
let variables;
|
|
344
|
+
try {
|
|
345
|
+
variables = await storage.load();
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
console.log(chalk.red('Recovery key decrypted but store could not be loaded. Data may be corrupted.'));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
console.log(chalk.green('Recovery key verified. Store contains ' + Object.keys(variables).length + ' variables.'));
|
|
352
|
+
// Set new password
|
|
353
|
+
const { newPassword } = await inquirer.prompt([
|
|
354
|
+
{ type: 'password', name: 'newPassword', message: 'Set new password:', mask: '*' }
|
|
355
|
+
]);
|
|
356
|
+
const { confirmPassword } = await inquirer.prompt([
|
|
357
|
+
{ type: 'password', name: 'confirmPassword', message: 'Confirm new password:', mask: '*' }
|
|
358
|
+
]);
|
|
359
|
+
if (newPassword !== confirmPassword) {
|
|
360
|
+
console.log(chalk.red('Passwords do not match'));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Re-encrypt store with new password
|
|
364
|
+
storage.invalidateCache();
|
|
365
|
+
storage.setPassword(newPassword);
|
|
366
|
+
await storage.save(variables);
|
|
367
|
+
// Update recovery file with new password
|
|
368
|
+
const newRecoveryKey = generateRecoveryKey();
|
|
369
|
+
const newRecoveryData = createRecoveryData(newPassword, newRecoveryKey);
|
|
370
|
+
await fs.writeFile(recoveryPath, newRecoveryData, 'utf8');
|
|
371
|
+
console.log(chalk.green('Password reset successfully!'));
|
|
372
|
+
console.log('');
|
|
373
|
+
console.log(chalk.yellow.bold('NEW RECOVERY KEY (save this somewhere safe!):'));
|
|
374
|
+
console.log(chalk.yellow.bold(` ${newRecoveryKey}`));
|
|
375
|
+
console.log(chalk.gray('Your old recovery key no longer works.'));
|
|
376
|
+
// Create a session with new password
|
|
377
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
378
|
+
await sessionManager.init();
|
|
379
|
+
await sessionManager.create(newPassword);
|
|
380
|
+
console.log(chalk.green('Session unlocked with new password.'));
|
|
381
|
+
});
|
|
382
|
+
program
|
|
383
|
+
.command('verify')
|
|
384
|
+
.description('Verify store integrity and check backups')
|
|
385
|
+
.action(async () => {
|
|
386
|
+
await withSession(async (storage, _password, config, projectPath) => {
|
|
387
|
+
const result = await storage.verify();
|
|
388
|
+
if (result.valid) {
|
|
389
|
+
console.log(chalk.green('Store integrity: OK'));
|
|
390
|
+
console.log(chalk.gray(` Variables: ${result.count}`));
|
|
391
|
+
console.log(chalk.gray(` Backups: ${result.backups}`));
|
|
392
|
+
// Check recovery file
|
|
393
|
+
if (config.security?.mode === 'recoverable') {
|
|
394
|
+
const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
|
|
395
|
+
const hasRecovery = await fs.pathExists(recoveryPath);
|
|
396
|
+
console.log(chalk.gray(` Recovery: ${hasRecovery ? 'available' : 'not found'}`));
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
console.log(chalk.gray(` Recovery: hard-lock mode (disabled)`));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
console.log(chalk.red(`Store integrity: FAILED`));
|
|
404
|
+
console.log(chalk.red(` Error: ${result.error}`));
|
|
405
|
+
if (result.backups && result.backups > 0) {
|
|
406
|
+
console.log(chalk.yellow(` ${result.backups} backup(s) available — store may auto-restore on next load`));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
170
411
|
program
|
|
171
412
|
.command('add <name>')
|
|
172
413
|
.description('Add a new environment variable')
|
|
@@ -294,33 +535,50 @@ program
|
|
|
294
535
|
.action(async (options) => {
|
|
295
536
|
const projectPath = process.cwd();
|
|
296
537
|
const config = await loadConfig(projectPath);
|
|
297
|
-
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
298
|
-
await sessionManager.init();
|
|
299
|
-
let session = await sessionManager.load();
|
|
300
|
-
let password = options.password;
|
|
301
|
-
if (!session && !password) {
|
|
302
|
-
const answer = await inquirer.prompt([
|
|
303
|
-
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
304
|
-
]);
|
|
305
|
-
password = answer.password;
|
|
306
|
-
const validation = validatePassword(password, config.password || {});
|
|
307
|
-
if (!validation.valid) {
|
|
308
|
-
console.log(chalk.red(validation.error));
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
session = await sessionManager.create(password);
|
|
312
|
-
}
|
|
313
|
-
password = sessionManager.getPassword() || password;
|
|
314
538
|
const mode = options.mode;
|
|
315
539
|
const port = parseInt(options.port, 10);
|
|
316
540
|
const host = options.host;
|
|
317
541
|
const apiKey = options.apiKey;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
542
|
+
let password = options.password || '';
|
|
543
|
+
// Passwordless mode: skip all session/password logic
|
|
544
|
+
if (config.encryption?.enabled === false) {
|
|
545
|
+
if (mode === 'mcp') {
|
|
546
|
+
const { EnvCPServer } = await import('../mcp/server.js');
|
|
547
|
+
const server = new EnvCPServer(config, projectPath);
|
|
548
|
+
await server.start();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
// Encrypted mode: need password
|
|
554
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
555
|
+
await sessionManager.init();
|
|
556
|
+
let session = await sessionManager.load();
|
|
557
|
+
if (!session && !password) {
|
|
558
|
+
// MCP mode uses stdio — can't prompt interactively
|
|
559
|
+
if (mode === 'mcp') {
|
|
560
|
+
process.stderr.write('Error: No active session. Run `envcp unlock` first, or use --password flag.\n');
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
const answer = await inquirer.prompt([
|
|
564
|
+
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
565
|
+
]);
|
|
566
|
+
password = answer.password;
|
|
567
|
+
const validation = validatePassword(password, config.password || {});
|
|
568
|
+
if (!validation.valid) {
|
|
569
|
+
console.log(chalk.red(validation.error));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
session = await sessionManager.create(password);
|
|
573
|
+
}
|
|
574
|
+
password = sessionManager.getPassword() || password;
|
|
575
|
+
// MCP mode uses stdio
|
|
576
|
+
if (mode === 'mcp') {
|
|
577
|
+
const { EnvCPServer } = await import('../mcp/server.js');
|
|
578
|
+
const server = new EnvCPServer(config, projectPath, password);
|
|
579
|
+
await server.start();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
324
582
|
}
|
|
325
583
|
// HTTP-based modes
|
|
326
584
|
const { UnifiedServer } = await import('../server/unified.js');
|
|
@@ -381,28 +639,198 @@ program
|
|
|
381
639
|
.command('export')
|
|
382
640
|
.description('Export variables')
|
|
383
641
|
.option('-f, --format <format>', 'Output format: env, json, yaml', 'env')
|
|
642
|
+
.option('--encrypted', 'Create an encrypted portable export file')
|
|
643
|
+
.option('-o, --output <path>', 'Output file (required for --encrypted)')
|
|
384
644
|
.action(async (options) => {
|
|
385
|
-
await withSession(async (storage) => {
|
|
645
|
+
await withSession(async (storage, _password, config) => {
|
|
386
646
|
const variables = await storage.load();
|
|
647
|
+
if (options.encrypted) {
|
|
648
|
+
const outputPath = options.output;
|
|
649
|
+
if (!outputPath) {
|
|
650
|
+
console.log(chalk.red('--output <path> is required with --encrypted'));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const { exportPassword } = await inquirer.prompt([
|
|
654
|
+
{ type: 'password', name: 'exportPassword', message: 'Set export password:', mask: '*' }
|
|
655
|
+
]);
|
|
656
|
+
const { confirmExport } = await inquirer.prompt([
|
|
657
|
+
{ type: 'password', name: 'confirmExport', message: 'Confirm export password:', mask: '*' }
|
|
658
|
+
]);
|
|
659
|
+
if (exportPassword !== confirmExport) {
|
|
660
|
+
console.log(chalk.red('Passwords do not match'));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const exportData = JSON.stringify({
|
|
664
|
+
meta: { project: config.project, timestamp: new Date().toISOString(), count: Object.keys(variables).length, version: '1.0' },
|
|
665
|
+
variables,
|
|
666
|
+
}, null, 2);
|
|
667
|
+
const encrypted = encrypt(exportData, exportPassword);
|
|
668
|
+
await fs.writeFile(outputPath, encrypted, 'utf8');
|
|
669
|
+
console.log(chalk.green(`Encrypted export saved to: ${outputPath}`));
|
|
670
|
+
console.log(chalk.gray(` Variables: ${Object.keys(variables).length}`));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
387
673
|
let output;
|
|
388
674
|
switch (options.format) {
|
|
389
675
|
case 'json':
|
|
390
676
|
output = JSON.stringify(variables, null, 2);
|
|
391
677
|
break;
|
|
392
|
-
case 'yaml':
|
|
678
|
+
case 'yaml': {
|
|
393
679
|
const yaml = await import('js-yaml');
|
|
394
680
|
output = yaml.dump(variables);
|
|
395
681
|
break;
|
|
396
|
-
|
|
682
|
+
}
|
|
683
|
+
default: {
|
|
397
684
|
const lines = Object.entries(variables).map(([k, v]) => {
|
|
398
685
|
const needsQuoting = /[\s#"'\\]/.test(v.value);
|
|
399
686
|
const val = needsQuoting ? `"${v.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : v.value;
|
|
400
687
|
return `${k}=${val}`;
|
|
401
688
|
});
|
|
402
689
|
output = lines.join('\n');
|
|
690
|
+
}
|
|
403
691
|
}
|
|
404
692
|
console.log(output);
|
|
405
693
|
});
|
|
406
694
|
});
|
|
695
|
+
program
|
|
696
|
+
.command('import <file>')
|
|
697
|
+
.description('Import variables from an encrypted export file')
|
|
698
|
+
.option('--merge', 'Merge with existing variables (default: replace)')
|
|
699
|
+
.action(async (file, options) => {
|
|
700
|
+
await withSession(async (storage) => {
|
|
701
|
+
if (!await fs.pathExists(file)) {
|
|
702
|
+
console.log(chalk.red(`File not found: ${file}`));
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const { importPassword } = await inquirer.prompt([
|
|
706
|
+
{ type: 'password', name: 'importPassword', message: 'Enter export file password:', mask: '*' }
|
|
707
|
+
]);
|
|
708
|
+
const fileContent = await fs.readFile(file, 'utf8');
|
|
709
|
+
let importData;
|
|
710
|
+
try {
|
|
711
|
+
const decrypted = decrypt(fileContent, importPassword);
|
|
712
|
+
importData = JSON.parse(decrypted);
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
console.log(chalk.red('Failed to decrypt. Wrong password or invalid file.'));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const meta = importData.meta;
|
|
719
|
+
const variables = importData.variables;
|
|
720
|
+
if (!variables || typeof variables !== 'object') {
|
|
721
|
+
console.log(chalk.red('Invalid export format'));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (meta) {
|
|
725
|
+
console.log(chalk.blue('Import info:'));
|
|
726
|
+
if (meta.project)
|
|
727
|
+
console.log(chalk.gray(` From project: ${meta.project}`));
|
|
728
|
+
if (meta.timestamp)
|
|
729
|
+
console.log(chalk.gray(` Exported: ${meta.timestamp}`));
|
|
730
|
+
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
731
|
+
}
|
|
732
|
+
const { confirm } = await inquirer.prompt([
|
|
733
|
+
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge into current store?' : 'Replace current store?', default: false }
|
|
734
|
+
]);
|
|
735
|
+
if (!confirm) {
|
|
736
|
+
console.log(chalk.yellow('Import cancelled'));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (options.merge) {
|
|
740
|
+
const current = await storage.load();
|
|
741
|
+
await storage.save({ ...current, ...variables });
|
|
742
|
+
console.log(chalk.green(`Merged ${Object.keys(variables).length} variables`));
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
await storage.save(variables);
|
|
746
|
+
console.log(chalk.green(`Imported ${Object.keys(variables).length} variables`));
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
program
|
|
751
|
+
.command('backup')
|
|
752
|
+
.description('Create an encrypted backup of all variables')
|
|
753
|
+
.option('-o, --output <path>', 'Output file path')
|
|
754
|
+
.action(async (options) => {
|
|
755
|
+
await withSession(async (storage, password, config, projectPath) => {
|
|
756
|
+
const variables = await storage.load();
|
|
757
|
+
const count = Object.keys(variables).length;
|
|
758
|
+
if (count === 0) {
|
|
759
|
+
console.log(chalk.yellow('No variables to backup'));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
763
|
+
const defaultPath = path.join(projectPath, `.envcp/backup-${timestamp}.enc`);
|
|
764
|
+
const outputPath = options.output || defaultPath;
|
|
765
|
+
const backupData = JSON.stringify({
|
|
766
|
+
meta: {
|
|
767
|
+
project: config.project,
|
|
768
|
+
timestamp: new Date().toISOString(),
|
|
769
|
+
count,
|
|
770
|
+
version: '1.0',
|
|
771
|
+
},
|
|
772
|
+
variables,
|
|
773
|
+
}, null, 2);
|
|
774
|
+
const encrypted = encrypt(backupData, password);
|
|
775
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
776
|
+
await fs.writeFile(outputPath, encrypted, 'utf8');
|
|
777
|
+
console.log(chalk.green(`Backup created: ${outputPath}`));
|
|
778
|
+
console.log(chalk.gray(` Variables: ${count}`));
|
|
779
|
+
console.log(chalk.gray(` Encrypted: yes`));
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
program
|
|
783
|
+
.command('restore <file>')
|
|
784
|
+
.description('Restore variables from an encrypted backup')
|
|
785
|
+
.option('--merge', 'Merge with existing variables (default: replace)')
|
|
786
|
+
.action(async (file, options) => {
|
|
787
|
+
await withSession(async (storage, password) => {
|
|
788
|
+
if (!await fs.pathExists(file)) {
|
|
789
|
+
console.log(chalk.red(`Backup file not found: ${file}`));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const encrypted = await fs.readFile(file, 'utf8');
|
|
793
|
+
let backupData;
|
|
794
|
+
try {
|
|
795
|
+
const decrypted = decrypt(encrypted, password);
|
|
796
|
+
backupData = JSON.parse(decrypted);
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
console.log(chalk.red('Failed to decrypt backup. Wrong password or corrupted file.'));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const meta = backupData.meta;
|
|
803
|
+
const variables = backupData.variables;
|
|
804
|
+
if (!variables || typeof variables !== 'object') {
|
|
805
|
+
console.log(chalk.red('Invalid backup format'));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (meta) {
|
|
809
|
+
console.log(chalk.blue('Backup info:'));
|
|
810
|
+
if (meta.project)
|
|
811
|
+
console.log(chalk.gray(` Project: ${meta.project}`));
|
|
812
|
+
if (meta.timestamp)
|
|
813
|
+
console.log(chalk.gray(` Created: ${meta.timestamp}`));
|
|
814
|
+
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
815
|
+
}
|
|
816
|
+
const { confirm } = await inquirer.prompt([
|
|
817
|
+
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', default: false }
|
|
818
|
+
]);
|
|
819
|
+
if (!confirm) {
|
|
820
|
+
console.log(chalk.yellow('Restore cancelled'));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (options.merge) {
|
|
824
|
+
const current = await storage.load();
|
|
825
|
+
const merged = { ...current, ...variables };
|
|
826
|
+
await storage.save(merged);
|
|
827
|
+
console.log(chalk.green(`Merged ${Object.keys(variables).length} variables from backup`));
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
await storage.save(variables);
|
|
831
|
+
console.log(chalk.green(`Restored ${Object.keys(variables).length} variables from backup`));
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
});
|
|
407
835
|
program.parse();
|
|
408
836
|
//# sourceMappingURL=index.js.map
|