@fentz26/envcp 1.0.1 → 1.0.3

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 (71) hide show
  1. package/README.md +82 -133
  2. package/dist/adapters/base.d.ts +1 -2
  3. package/dist/adapters/base.d.ts.map +1 -1
  4. package/dist/adapters/base.js +139 -14
  5. package/dist/adapters/base.js.map +1 -1
  6. package/dist/adapters/gemini.d.ts +1 -0
  7. package/dist/adapters/gemini.d.ts.map +1 -1
  8. package/dist/adapters/gemini.js +13 -99
  9. package/dist/adapters/gemini.js.map +1 -1
  10. package/dist/adapters/openai.d.ts +1 -0
  11. package/dist/adapters/openai.d.ts.map +1 -1
  12. package/dist/adapters/openai.js +13 -99
  13. package/dist/adapters/openai.js.map +1 -1
  14. package/dist/adapters/rest.d.ts +1 -0
  15. package/dist/adapters/rest.d.ts.map +1 -1
  16. package/dist/adapters/rest.js +16 -13
  17. package/dist/adapters/rest.js.map +1 -1
  18. package/dist/cli/index.js +510 -197
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/config/manager.d.ts +6 -0
  21. package/dist/config/manager.d.ts.map +1 -1
  22. package/dist/config/manager.js +81 -1
  23. package/dist/config/manager.js.map +1 -1
  24. package/dist/mcp/server.d.ts +1 -16
  25. package/dist/mcp/server.d.ts.map +1 -1
  26. package/dist/mcp/server.js +23 -511
  27. package/dist/mcp/server.js.map +1 -1
  28. package/dist/server/unified.d.ts +1 -0
  29. package/dist/server/unified.d.ts.map +1 -1
  30. package/dist/server/unified.js +31 -19
  31. package/dist/server/unified.js.map +1 -1
  32. package/dist/storage/index.d.ts +12 -1
  33. package/dist/storage/index.d.ts.map +1 -1
  34. package/dist/storage/index.js +107 -10
  35. package/dist/storage/index.js.map +1 -1
  36. package/dist/types.d.ts +28 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.js +6 -0
  39. package/dist/types.js.map +1 -1
  40. package/dist/utils/crypto.d.ts +3 -0
  41. package/dist/utils/crypto.d.ts.map +1 -1
  42. package/dist/utils/crypto.js +12 -0
  43. package/dist/utils/crypto.js.map +1 -1
  44. package/dist/utils/http.d.ts +13 -1
  45. package/dist/utils/http.d.ts.map +1 -1
  46. package/dist/utils/http.js +65 -2
  47. package/dist/utils/http.js.map +1 -1
  48. package/dist/utils/session.d.ts.map +1 -1
  49. package/dist/utils/session.js +8 -3
  50. package/dist/utils/session.js.map +1 -1
  51. package/package.json +9 -3
  52. package/.github/workflows/publish.yml +0 -48
  53. package/src/adapters/base.ts +0 -411
  54. package/src/adapters/gemini.ts +0 -314
  55. package/src/adapters/index.ts +0 -4
  56. package/src/adapters/openai.ts +0 -324
  57. package/src/adapters/rest.ts +0 -294
  58. package/src/cli/index.ts +0 -640
  59. package/src/cli.ts +0 -2
  60. package/src/config/manager.ts +0 -134
  61. package/src/index.ts +0 -4
  62. package/src/mcp/index.ts +0 -1
  63. package/src/mcp/server.ts +0 -623
  64. package/src/server/index.ts +0 -1
  65. package/src/server/unified.ts +0 -460
  66. package/src/storage/index.ts +0 -112
  67. package/src/types.ts +0 -181
  68. package/src/utils/crypto.ts +0 -100
  69. package/src/utils/http.ts +0 -45
  70. package/src/utils/session.ts +0 -141
  71. package/tsconfig.json +0 -20
package/dist/cli/index.js CHANGED
@@ -3,10 +3,35 @@ 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
+ async function withSession(fn) {
11
+ const projectPath = process.cwd();
12
+ const config = await loadConfig(projectPath);
13
+ const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
14
+ await sessionManager.init();
15
+ let session = await sessionManager.load();
16
+ let password = '';
17
+ if (!session) {
18
+ const answer = await inquirer.prompt([
19
+ { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
20
+ ]);
21
+ password = answer.password;
22
+ const validation = validatePassword(password, config.password || {});
23
+ if (!validation.valid) {
24
+ console.log(chalk.red(validation.error));
25
+ return;
26
+ }
27
+ session = await sessionManager.create(password);
28
+ }
29
+ password = sessionManager.getPassword() || password;
30
+ const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
31
+ if (password)
32
+ storage.setPassword(password);
33
+ await fn(storage, password, config, projectPath);
34
+ }
10
35
  const program = new Command();
11
36
  program
12
37
  .name('envcp')
@@ -17,17 +42,110 @@ program
17
42
  .description('Initialize EnvCP in the current project')
18
43
  .option('-p, --project <name>', 'Project name')
19
44
  .option('-e, --encrypted', 'Enable encryption', true)
45
+ .option('--skip-env', 'Skip .env auto-import')
46
+ .option('--skip-mcp', 'Skip MCP auto-registration')
20
47
  .action(async (options) => {
21
48
  const projectPath = process.cwd();
22
49
  const projectName = options.project || path.basename(projectPath);
23
50
  console.log(chalk.blue('Initializing EnvCP...'));
24
51
  const config = await initConfig(projectPath, projectName);
52
+ // Security mode selection
53
+ const { securityMode } = await inquirer.prompt([
54
+ {
55
+ type: 'list',
56
+ name: 'securityMode',
57
+ message: 'Security mode:',
58
+ choices: [
59
+ { name: 'Recoverable - Generate a recovery key (recommended)', value: 'recoverable' },
60
+ { name: 'Hard-lock - Lose password = lose everything (maximum security)', value: 'hard-lock' },
61
+ ],
62
+ default: 'recoverable',
63
+ }
64
+ ]);
65
+ config.security = { mode: securityMode, recovery_file: '.envcp/.recovery' };
66
+ await saveConfig(config, projectPath);
25
67
  console.log(chalk.green('EnvCP initialized successfully!'));
26
68
  console.log(chalk.gray(` Project: ${config.project}`));
27
69
  console.log(chalk.gray(` Storage: ${config.storage.path}`));
28
70
  console.log(chalk.gray(` Encrypted: ${config.storage.encrypted}`));
71
+ console.log(chalk.gray(` Security: ${securityMode}`));
29
72
  console.log(chalk.gray(` Session timeout: ${config.session?.timeout_minutes || 30} minutes`));
30
73
  console.log(chalk.gray(` AI active check: ${config.access?.allow_ai_active_check ? 'enabled' : 'disabled'}`));
74
+ // Auto-import .env file
75
+ if (!options.skipEnv) {
76
+ const envPath = path.join(projectPath, '.env');
77
+ if (await fs.pathExists(envPath)) {
78
+ const { importEnv } = await inquirer.prompt([
79
+ { type: 'confirm', name: 'importEnv', message: 'Found .env file. Import variables into EnvCP?', default: true }
80
+ ]);
81
+ if (importEnv) {
82
+ const { password: pwd } = await inquirer.prompt([
83
+ { type: 'password', name: 'password', message: 'Set encryption password:', mask: '*' }
84
+ ]);
85
+ const { confirm } = await inquirer.prompt([
86
+ { type: 'password', name: 'confirm', message: 'Confirm password:', mask: '*' }
87
+ ]);
88
+ if (pwd !== confirm) {
89
+ console.log(chalk.red('Passwords do not match. Skipping .env import.'));
90
+ }
91
+ else {
92
+ const envContent = await fs.readFile(envPath, 'utf8');
93
+ const vars = parseEnvFile(envContent);
94
+ const count = Object.keys(vars).length;
95
+ if (count > 0) {
96
+ const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
97
+ storage.setPassword(pwd);
98
+ const now = new Date().toISOString();
99
+ for (const [name, value] of Object.entries(vars)) {
100
+ await storage.set(name, {
101
+ name,
102
+ value,
103
+ encrypted: config.storage.encrypted,
104
+ created: now,
105
+ updated: now,
106
+ sync_to_env: true,
107
+ });
108
+ }
109
+ // Create session so user doesn't have to unlock immediately
110
+ const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
111
+ await sessionManager.init();
112
+ await sessionManager.create(pwd);
113
+ // Generate recovery key if recoverable mode
114
+ if (securityMode === 'recoverable') {
115
+ const recoveryKey = generateRecoveryKey();
116
+ const recoveryData = createRecoveryData(pwd, recoveryKey);
117
+ const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
118
+ await fs.writeFile(recoveryPath, recoveryData, 'utf8');
119
+ console.log('');
120
+ console.log(chalk.yellow.bold(' RECOVERY KEY (save this somewhere safe!):'));
121
+ console.log(chalk.yellow.bold(` ${recoveryKey}`));
122
+ console.log(chalk.gray(' This key is shown ONCE. If you lose it, you cannot recover your password.'));
123
+ console.log('');
124
+ }
125
+ console.log(chalk.green(` Imported ${count} variables from .env`));
126
+ console.log(chalk.gray(` Variables: ${Object.keys(vars).join(', ')}`));
127
+ }
128
+ else {
129
+ console.log(chalk.yellow(' .env file is empty, nothing to import'));
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ // Auto-register MCP config
136
+ if (!options.skipMcp) {
137
+ const registered = await registerMcpConfig(projectPath);
138
+ if (registered.length > 0) {
139
+ console.log(chalk.green(' MCP auto-registered:'));
140
+ for (const name of registered) {
141
+ console.log(chalk.gray(` - ${name}`));
142
+ }
143
+ }
144
+ else {
145
+ console.log(chalk.gray(' No AI tool configs detected for MCP auto-registration'));
146
+ console.log(chalk.gray(' You can manually add EnvCP to your AI tool config later'));
147
+ }
148
+ }
31
149
  });
32
150
  program
33
151
  .command('unlock')
@@ -66,6 +184,21 @@ program
66
184
  console.log(chalk.red('Passwords do not match'));
67
185
  return;
68
186
  }
187
+ // Generate recovery key for new stores in recoverable mode
188
+ if (config.security?.mode === 'recoverable') {
189
+ const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
190
+ if (!await fs.pathExists(recoveryPath)) {
191
+ const recoveryKey = generateRecoveryKey();
192
+ const recoveryData = createRecoveryData(password, recoveryKey);
193
+ await fs.ensureDir(path.dirname(recoveryPath));
194
+ await fs.writeFile(recoveryPath, recoveryData, 'utf8');
195
+ console.log('');
196
+ console.log(chalk.yellow.bold('RECOVERY KEY (save this somewhere safe!):'));
197
+ console.log(chalk.yellow.bold(` ${recoveryKey}`));
198
+ console.log(chalk.gray('This key is shown ONCE. If you lose it, you cannot recover your password.'));
199
+ console.log('');
200
+ }
201
+ }
69
202
  }
70
203
  try {
71
204
  await storage.load();
@@ -142,6 +275,105 @@ program
142
275
  console.log(chalk.gray(` Remaining: ${sessionManager.getRemainingTime()} minutes`));
143
276
  console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
144
277
  });
278
+ program
279
+ .command('recover')
280
+ .description('Recover access using recovery key (reset password)')
281
+ .action(async () => {
282
+ const projectPath = process.cwd();
283
+ const config = await loadConfig(projectPath);
284
+ if (config.security?.mode === 'hard-lock') {
285
+ console.log(chalk.red('Recovery is not available in hard-lock mode.'));
286
+ console.log(chalk.gray('Hard-lock mode means lost password = lost data.'));
287
+ return;
288
+ }
289
+ const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
290
+ if (!await fs.pathExists(recoveryPath)) {
291
+ console.log(chalk.red('No recovery file found. Recovery is not available.'));
292
+ return;
293
+ }
294
+ const { recoveryKey } = await inquirer.prompt([
295
+ { type: 'password', name: 'recoveryKey', message: 'Enter your recovery key:', mask: '*' }
296
+ ]);
297
+ const recoveryData = await fs.readFile(recoveryPath, 'utf8');
298
+ let oldPassword;
299
+ try {
300
+ oldPassword = recoverPassword(recoveryData, recoveryKey);
301
+ }
302
+ catch {
303
+ console.log(chalk.red('Invalid recovery key.'));
304
+ return;
305
+ }
306
+ // Verify old password actually works by loading the store
307
+ const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
308
+ storage.setPassword(oldPassword);
309
+ let variables;
310
+ try {
311
+ variables = await storage.load();
312
+ }
313
+ catch {
314
+ console.log(chalk.red('Recovery key decrypted but store could not be loaded. Data may be corrupted.'));
315
+ return;
316
+ }
317
+ console.log(chalk.green('Recovery key verified. Store contains ' + Object.keys(variables).length + ' variables.'));
318
+ // Set new password
319
+ const { newPassword } = await inquirer.prompt([
320
+ { type: 'password', name: 'newPassword', message: 'Set new password:', mask: '*' }
321
+ ]);
322
+ const { confirmPassword } = await inquirer.prompt([
323
+ { type: 'password', name: 'confirmPassword', message: 'Confirm new password:', mask: '*' }
324
+ ]);
325
+ if (newPassword !== confirmPassword) {
326
+ console.log(chalk.red('Passwords do not match'));
327
+ return;
328
+ }
329
+ // Re-encrypt store with new password
330
+ storage.invalidateCache();
331
+ storage.setPassword(newPassword);
332
+ await storage.save(variables);
333
+ // Update recovery file with new password
334
+ const newRecoveryKey = generateRecoveryKey();
335
+ const newRecoveryData = createRecoveryData(newPassword, newRecoveryKey);
336
+ await fs.writeFile(recoveryPath, newRecoveryData, 'utf8');
337
+ console.log(chalk.green('Password reset successfully!'));
338
+ console.log('');
339
+ console.log(chalk.yellow.bold('NEW RECOVERY KEY (save this somewhere safe!):'));
340
+ console.log(chalk.yellow.bold(` ${newRecoveryKey}`));
341
+ console.log(chalk.gray('Your old recovery key no longer works.'));
342
+ // Create a session with new password
343
+ const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
344
+ await sessionManager.init();
345
+ await sessionManager.create(newPassword);
346
+ console.log(chalk.green('Session unlocked with new password.'));
347
+ });
348
+ program
349
+ .command('verify')
350
+ .description('Verify store integrity and check backups')
351
+ .action(async () => {
352
+ await withSession(async (storage, _password, config, projectPath) => {
353
+ const result = await storage.verify();
354
+ if (result.valid) {
355
+ console.log(chalk.green('Store integrity: OK'));
356
+ console.log(chalk.gray(` Variables: ${result.count}`));
357
+ console.log(chalk.gray(` Backups: ${result.backups}`));
358
+ // Check recovery file
359
+ if (config.security?.mode === 'recoverable') {
360
+ const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
361
+ const hasRecovery = await fs.pathExists(recoveryPath);
362
+ console.log(chalk.gray(` Recovery: ${hasRecovery ? 'available' : 'not found'}`));
363
+ }
364
+ else {
365
+ console.log(chalk.gray(` Recovery: hard-lock mode (disabled)`));
366
+ }
367
+ }
368
+ else {
369
+ console.log(chalk.red(`Store integrity: FAILED`));
370
+ console.log(chalk.red(` Error: ${result.error}`));
371
+ if (result.backups && result.backups > 0) {
372
+ console.log(chalk.yellow(` ${result.backups} backup(s) available — store may auto-restore on next load`));
373
+ }
374
+ }
375
+ });
376
+ });
145
377
  program
146
378
  .command('add <name>')
147
379
  .description('Add a new environment variable')
@@ -149,192 +381,114 @@ program
149
381
  .option('-t, --tags <tags>', 'Tags (comma-separated)')
150
382
  .option('-d, --description <desc>', 'Description')
151
383
  .action(async (name, options) => {
152
- const projectPath = process.cwd();
153
- const config = await loadConfig(projectPath);
154
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
155
- await sessionManager.init();
156
- let session = await sessionManager.load();
157
- let password = '';
158
- if (!session) {
159
- const answer = await inquirer.prompt([
160
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
161
- ]);
162
- password = answer.password;
163
- const validation = validatePassword(password, config.password || {});
164
- if (!validation.valid) {
165
- console.log(chalk.red(validation.error));
166
- return;
384
+ await withSession(async (storage, _password, config) => {
385
+ let value = options.value;
386
+ let tags = [];
387
+ let description = options.description;
388
+ if (!value) {
389
+ const answers = await inquirer.prompt([
390
+ { type: 'password', name: 'value', message: 'Enter value:', mask: '*' },
391
+ { type: 'input', name: 'tags', message: 'Tags (comma-separated):' },
392
+ { type: 'input', name: 'description', message: 'Description:' },
393
+ ]);
394
+ value = answers.value;
395
+ tags = answers.tags.split(',').map((t) => t.trim()).filter(Boolean);
396
+ description = answers.description;
167
397
  }
168
- session = await sessionManager.create(password);
169
- }
170
- password = sessionManager.getPassword() || password;
171
- let value = options.value;
172
- let tags = [];
173
- let description = options.description;
174
- if (!value) {
175
- const answers = await inquirer.prompt([
176
- { type: 'password', name: 'value', message: 'Enter value:', mask: '*' },
177
- { type: 'input', name: 'tags', message: 'Tags (comma-separated):' },
178
- { type: 'input', name: 'description', message: 'Description:' },
179
- ]);
180
- value = answers.value;
181
- tags = answers.tags.split(',').map((t) => t.trim()).filter(Boolean);
182
- description = answers.description;
183
- }
184
- else if (options.tags) {
185
- tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
186
- }
187
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
188
- if (password)
189
- storage.setPassword(password);
190
- const now = new Date().toISOString();
191
- const variable = {
192
- name,
193
- value,
194
- encrypted: config.storage.encrypted,
195
- tags: tags.length > 0 ? tags : undefined,
196
- description,
197
- created: now,
198
- updated: now,
199
- sync_to_env: true,
200
- };
201
- await storage.set(name, variable);
202
- console.log(chalk.green(`Variable '${name}' added successfully`));
398
+ else if (options.tags) {
399
+ tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
400
+ }
401
+ const now = new Date().toISOString();
402
+ const variable = {
403
+ name,
404
+ value,
405
+ encrypted: config.storage.encrypted,
406
+ tags: tags.length > 0 ? tags : undefined,
407
+ description,
408
+ created: now,
409
+ updated: now,
410
+ sync_to_env: true,
411
+ };
412
+ await storage.set(name, variable);
413
+ console.log(chalk.green(`Variable '${name}' added successfully`));
414
+ });
203
415
  });
204
416
  program
205
417
  .command('list')
206
418
  .description('List all variables (names only, values hidden)')
207
419
  .option('-v, --show-values', 'Show actual values')
208
420
  .action(async (options) => {
209
- const projectPath = process.cwd();
210
- const config = await loadConfig(projectPath);
211
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
212
- await sessionManager.init();
213
- let session = await sessionManager.load();
214
- let password = '';
215
- if (!session) {
216
- const answer = await inquirer.prompt([
217
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
218
- ]);
219
- password = answer.password;
220
- session = await sessionManager.create(password);
221
- }
222
- password = sessionManager.getPassword() || password;
223
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
224
- if (password)
225
- storage.setPassword(password);
226
- const variables = await storage.load();
227
- const names = Object.keys(variables);
228
- if (names.length === 0) {
229
- console.log(chalk.yellow('No variables found'));
230
- return;
231
- }
232
- console.log(chalk.blue(`\nVariables (${names.length}):\n`));
233
- for (const name of names) {
234
- const v = variables[name];
235
- const value = options.showValues ? v.value : maskValue(v.value);
236
- const tags = v.tags ? chalk.gray(` [${v.tags.join(', ')}]`) : '';
237
- console.log(` ${chalk.cyan(name)} = ${value}${tags}`);
238
- }
239
- console.log('');
421
+ await withSession(async (storage) => {
422
+ const variables = await storage.load();
423
+ const names = Object.keys(variables);
424
+ if (names.length === 0) {
425
+ console.log(chalk.yellow('No variables found'));
426
+ return;
427
+ }
428
+ console.log(chalk.blue(`\nVariables (${names.length}):\n`));
429
+ for (const name of names) {
430
+ const v = variables[name];
431
+ const value = options.showValues ? v.value : maskValue(v.value);
432
+ const tags = v.tags ? chalk.gray(` [${v.tags.join(', ')}]`) : '';
433
+ console.log(` ${chalk.cyan(name)} = ${value}${tags}`);
434
+ }
435
+ console.log('');
436
+ });
240
437
  });
241
438
  program
242
439
  .command('get <name>')
243
440
  .description('Get a variable value')
244
441
  .action(async (name) => {
245
- const projectPath = process.cwd();
246
- const config = await loadConfig(projectPath);
247
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
248
- await sessionManager.init();
249
- let session = await sessionManager.load();
250
- let password = '';
251
- if (!session) {
252
- const answer = await inquirer.prompt([
253
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
254
- ]);
255
- password = answer.password;
256
- session = await sessionManager.create(password);
257
- }
258
- password = sessionManager.getPassword() || password;
259
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
260
- if (password)
261
- storage.setPassword(password);
262
- const variable = await storage.get(name);
263
- if (!variable) {
264
- console.log(chalk.red(`Variable '${name}' not found`));
265
- return;
266
- }
267
- console.log(chalk.cyan(name));
268
- console.log(` Value: ${variable.value}`);
269
- if (variable.tags)
270
- console.log(` Tags: ${variable.tags.join(', ')}`);
271
- if (variable.description)
272
- console.log(` Description: ${variable.description}`);
442
+ await withSession(async (storage) => {
443
+ const variable = await storage.get(name);
444
+ if (!variable) {
445
+ console.log(chalk.red(`Variable '${name}' not found`));
446
+ return;
447
+ }
448
+ console.log(chalk.cyan(name));
449
+ console.log(` Value: ${variable.value}`);
450
+ if (variable.tags)
451
+ console.log(` Tags: ${variable.tags.join(', ')}`);
452
+ if (variable.description)
453
+ console.log(` Description: ${variable.description}`);
454
+ });
273
455
  });
274
456
  program
275
457
  .command('remove <name>')
276
458
  .description('Remove a variable')
277
459
  .action(async (name) => {
278
- const projectPath = process.cwd();
279
- const config = await loadConfig(projectPath);
280
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
281
- await sessionManager.init();
282
- let session = await sessionManager.load();
283
- let password = '';
284
- if (!session) {
285
- const answer = await inquirer.prompt([
286
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
287
- ]);
288
- password = answer.password;
289
- session = await sessionManager.create(password);
290
- }
291
- password = sessionManager.getPassword() || password;
292
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
293
- if (password)
294
- storage.setPassword(password);
295
- const deleted = await storage.delete(name);
296
- if (deleted) {
297
- console.log(chalk.green(`Variable '${name}' removed`));
298
- }
299
- else {
300
- console.log(chalk.red(`Variable '${name}' not found`));
301
- }
460
+ await withSession(async (storage) => {
461
+ const deleted = await storage.delete(name);
462
+ if (deleted) {
463
+ console.log(chalk.green(`Variable '${name}' removed`));
464
+ }
465
+ else {
466
+ console.log(chalk.red(`Variable '${name}' not found`));
467
+ }
468
+ });
302
469
  });
303
470
  program
304
471
  .command('sync')
305
472
  .description('Sync variables to .env file')
306
473
  .action(async () => {
307
- const projectPath = process.cwd();
308
- const config = await loadConfig(projectPath);
309
- if (!config.sync.enabled) {
310
- console.log(chalk.yellow('Sync is disabled in configuration'));
311
- return;
312
- }
313
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
314
- await sessionManager.init();
315
- let session = await sessionManager.load();
316
- let password = '';
317
- if (!session) {
318
- const answer = await inquirer.prompt([
319
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
320
- ]);
321
- password = answer.password;
322
- session = await sessionManager.create(password);
323
- }
324
- password = sessionManager.getPassword() || password;
325
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
326
- if (password)
327
- storage.setPassword(password);
328
- const variables = await storage.load();
329
- const lines = [];
330
- if (config.sync.header) {
331
- lines.push(config.sync.header);
332
- }
333
- for (const [name, variable] of Object.entries(variables)) {
334
- lines.push(`${name}=${variable.value}`);
335
- }
336
- await fs.writeFile(path.join(projectPath, config.sync.target), lines.join('\n'), 'utf8');
337
- console.log(chalk.green(`Synced ${lines.length} variables to ${config.sync.target}`));
474
+ await withSession(async (storage, _password, config, projectPath) => {
475
+ if (!config.sync.enabled) {
476
+ console.log(chalk.yellow('Sync is disabled in configuration'));
477
+ return;
478
+ }
479
+ const variables = await storage.load();
480
+ const lines = [];
481
+ if (config.sync.header) {
482
+ lines.push(config.sync.header);
483
+ }
484
+ for (const [name, variable] of Object.entries(variables)) {
485
+ const needsQuoting = /[\s#"'\\]/.test(variable.value);
486
+ const val = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
487
+ lines.push(`${name}=${val}`);
488
+ }
489
+ await fs.writeFile(path.join(projectPath, config.sync.target), lines.join('\n'), 'utf8');
490
+ console.log(chalk.green(`Synced ${lines.length} variables to ${config.sync.target}`));
491
+ });
338
492
  });
339
493
  program
340
494
  .command('serve')
@@ -434,39 +588,198 @@ program
434
588
  .command('export')
435
589
  .description('Export variables')
436
590
  .option('-f, --format <format>', 'Output format: env, json, yaml', 'env')
591
+ .option('--encrypted', 'Create an encrypted portable export file')
592
+ .option('-o, --output <path>', 'Output file (required for --encrypted)')
437
593
  .action(async (options) => {
438
- const projectPath = process.cwd();
439
- const config = await loadConfig(projectPath);
440
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
441
- await sessionManager.init();
442
- let session = await sessionManager.load();
443
- let password = '';
444
- if (!session) {
445
- const answer = await inquirer.prompt([
446
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
594
+ await withSession(async (storage, _password, config) => {
595
+ const variables = await storage.load();
596
+ if (options.encrypted) {
597
+ const outputPath = options.output;
598
+ if (!outputPath) {
599
+ console.log(chalk.red('--output <path> is required with --encrypted'));
600
+ return;
601
+ }
602
+ const { exportPassword } = await inquirer.prompt([
603
+ { type: 'password', name: 'exportPassword', message: 'Set export password:', mask: '*' }
604
+ ]);
605
+ const { confirmExport } = await inquirer.prompt([
606
+ { type: 'password', name: 'confirmExport', message: 'Confirm export password:', mask: '*' }
607
+ ]);
608
+ if (exportPassword !== confirmExport) {
609
+ console.log(chalk.red('Passwords do not match'));
610
+ return;
611
+ }
612
+ const exportData = JSON.stringify({
613
+ meta: { project: config.project, timestamp: new Date().toISOString(), count: Object.keys(variables).length, version: '1.0' },
614
+ variables,
615
+ }, null, 2);
616
+ const encrypted = encrypt(exportData, exportPassword);
617
+ await fs.writeFile(outputPath, encrypted, 'utf8');
618
+ console.log(chalk.green(`Encrypted export saved to: ${outputPath}`));
619
+ console.log(chalk.gray(` Variables: ${Object.keys(variables).length}`));
620
+ return;
621
+ }
622
+ let output;
623
+ switch (options.format) {
624
+ case 'json':
625
+ output = JSON.stringify(variables, null, 2);
626
+ break;
627
+ case 'yaml': {
628
+ const yaml = await import('js-yaml');
629
+ output = yaml.dump(variables);
630
+ break;
631
+ }
632
+ default: {
633
+ const lines = Object.entries(variables).map(([k, v]) => {
634
+ const needsQuoting = /[\s#"'\\]/.test(v.value);
635
+ const val = needsQuoting ? `"${v.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : v.value;
636
+ return `${k}=${val}`;
637
+ });
638
+ output = lines.join('\n');
639
+ }
640
+ }
641
+ console.log(output);
642
+ });
643
+ });
644
+ program
645
+ .command('import <file>')
646
+ .description('Import variables from an encrypted export file')
647
+ .option('--merge', 'Merge with existing variables (default: replace)')
648
+ .action(async (file, options) => {
649
+ await withSession(async (storage) => {
650
+ if (!await fs.pathExists(file)) {
651
+ console.log(chalk.red(`File not found: ${file}`));
652
+ return;
653
+ }
654
+ const { importPassword } = await inquirer.prompt([
655
+ { type: 'password', name: 'importPassword', message: 'Enter export file password:', mask: '*' }
447
656
  ]);
448
- password = answer.password;
449
- session = await sessionManager.create(password);
450
- }
451
- password = sessionManager.getPassword() || password;
452
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
453
- if (password)
454
- storage.setPassword(password);
455
- const variables = await storage.load();
456
- let output;
457
- switch (options.format) {
458
- case 'json':
459
- output = JSON.stringify(variables, null, 2);
460
- break;
461
- case 'yaml':
462
- const yaml = await import('js-yaml');
463
- output = yaml.dump(variables);
464
- break;
465
- default:
466
- const lines = Object.entries(variables).map(([k, v]) => `${k}=${v.value}`);
467
- output = lines.join('\n');
468
- }
469
- console.log(output);
657
+ const fileContent = await fs.readFile(file, 'utf8');
658
+ let importData;
659
+ try {
660
+ const decrypted = decrypt(fileContent, importPassword);
661
+ importData = JSON.parse(decrypted);
662
+ }
663
+ catch {
664
+ console.log(chalk.red('Failed to decrypt. Wrong password or invalid file.'));
665
+ return;
666
+ }
667
+ const meta = importData.meta;
668
+ const variables = importData.variables;
669
+ if (!variables || typeof variables !== 'object') {
670
+ console.log(chalk.red('Invalid export format'));
671
+ return;
672
+ }
673
+ if (meta) {
674
+ console.log(chalk.blue('Import info:'));
675
+ if (meta.project)
676
+ console.log(chalk.gray(` From project: ${meta.project}`));
677
+ if (meta.timestamp)
678
+ console.log(chalk.gray(` Exported: ${meta.timestamp}`));
679
+ console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
680
+ }
681
+ const { confirm } = await inquirer.prompt([
682
+ { type: 'confirm', name: 'confirm', message: options.merge ? 'Merge into current store?' : 'Replace current store?', default: false }
683
+ ]);
684
+ if (!confirm) {
685
+ console.log(chalk.yellow('Import cancelled'));
686
+ return;
687
+ }
688
+ if (options.merge) {
689
+ const current = await storage.load();
690
+ await storage.save({ ...current, ...variables });
691
+ console.log(chalk.green(`Merged ${Object.keys(variables).length} variables`));
692
+ }
693
+ else {
694
+ await storage.save(variables);
695
+ console.log(chalk.green(`Imported ${Object.keys(variables).length} variables`));
696
+ }
697
+ });
698
+ });
699
+ program
700
+ .command('backup')
701
+ .description('Create an encrypted backup of all variables')
702
+ .option('-o, --output <path>', 'Output file path')
703
+ .action(async (options) => {
704
+ await withSession(async (storage, password, config, projectPath) => {
705
+ const variables = await storage.load();
706
+ const count = Object.keys(variables).length;
707
+ if (count === 0) {
708
+ console.log(chalk.yellow('No variables to backup'));
709
+ return;
710
+ }
711
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
712
+ const defaultPath = path.join(projectPath, `.envcp/backup-${timestamp}.enc`);
713
+ const outputPath = options.output || defaultPath;
714
+ const backupData = JSON.stringify({
715
+ meta: {
716
+ project: config.project,
717
+ timestamp: new Date().toISOString(),
718
+ count,
719
+ version: '1.0',
720
+ },
721
+ variables,
722
+ }, null, 2);
723
+ const encrypted = encrypt(backupData, password);
724
+ await fs.ensureDir(path.dirname(outputPath));
725
+ await fs.writeFile(outputPath, encrypted, 'utf8');
726
+ console.log(chalk.green(`Backup created: ${outputPath}`));
727
+ console.log(chalk.gray(` Variables: ${count}`));
728
+ console.log(chalk.gray(` Encrypted: yes`));
729
+ });
730
+ });
731
+ program
732
+ .command('restore <file>')
733
+ .description('Restore variables from an encrypted backup')
734
+ .option('--merge', 'Merge with existing variables (default: replace)')
735
+ .action(async (file, options) => {
736
+ await withSession(async (storage, password) => {
737
+ if (!await fs.pathExists(file)) {
738
+ console.log(chalk.red(`Backup file not found: ${file}`));
739
+ return;
740
+ }
741
+ const encrypted = await fs.readFile(file, 'utf8');
742
+ let backupData;
743
+ try {
744
+ const decrypted = decrypt(encrypted, password);
745
+ backupData = JSON.parse(decrypted);
746
+ }
747
+ catch {
748
+ console.log(chalk.red('Failed to decrypt backup. Wrong password or corrupted file.'));
749
+ return;
750
+ }
751
+ const meta = backupData.meta;
752
+ const variables = backupData.variables;
753
+ if (!variables || typeof variables !== 'object') {
754
+ console.log(chalk.red('Invalid backup format'));
755
+ return;
756
+ }
757
+ if (meta) {
758
+ console.log(chalk.blue('Backup info:'));
759
+ if (meta.project)
760
+ console.log(chalk.gray(` Project: ${meta.project}`));
761
+ if (meta.timestamp)
762
+ console.log(chalk.gray(` Created: ${meta.timestamp}`));
763
+ console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
764
+ }
765
+ const { confirm } = await inquirer.prompt([
766
+ { type: 'confirm', name: 'confirm', message: options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', default: false }
767
+ ]);
768
+ if (!confirm) {
769
+ console.log(chalk.yellow('Restore cancelled'));
770
+ return;
771
+ }
772
+ if (options.merge) {
773
+ const current = await storage.load();
774
+ const merged = { ...current, ...variables };
775
+ await storage.save(merged);
776
+ console.log(chalk.green(`Merged ${Object.keys(variables).length} variables from backup`));
777
+ }
778
+ else {
779
+ await storage.save(variables);
780
+ console.log(chalk.green(`Restored ${Object.keys(variables).length} variables from backup`));
781
+ }
782
+ });
470
783
  });
471
784
  program.parse();
472
785
  //# sourceMappingURL=index.js.map