@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.
Files changed (48) hide show
  1. package/README.md +13 -6
  2. package/dist/adapters/base.d.ts.map +1 -1
  3. package/dist/adapters/base.js +5 -1
  4. package/dist/adapters/base.js.map +1 -1
  5. package/dist/cli/index.js +462 -34
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/config/manager.d.ts +6 -0
  8. package/dist/config/manager.d.ts.map +1 -1
  9. package/dist/config/manager.js +243 -6
  10. package/dist/config/manager.js.map +1 -1
  11. package/dist/storage/index.d.ts +10 -1
  12. package/dist/storage/index.d.ts.map +1 -1
  13. package/dist/storage/index.js +89 -6
  14. package/dist/storage/index.js.map +1 -1
  15. package/dist/types.d.ts +31 -0
  16. package/dist/types.d.ts.map +1 -1
  17. package/dist/types.js +7 -0
  18. package/dist/types.js.map +1 -1
  19. package/dist/utils/crypto.d.ts +3 -0
  20. package/dist/utils/crypto.d.ts.map +1 -1
  21. package/dist/utils/crypto.js +12 -0
  22. package/dist/utils/crypto.js.map +1 -1
  23. package/package.json +6 -1
  24. package/.github/workflows/publish.yml +0 -48
  25. package/__tests__/config.test.ts +0 -65
  26. package/__tests__/crypto.test.ts +0 -76
  27. package/__tests__/http.test.ts +0 -49
  28. package/__tests__/storage.test.ts +0 -94
  29. package/jest.config.js +0 -11
  30. package/src/adapters/base.ts +0 -542
  31. package/src/adapters/gemini.ts +0 -228
  32. package/src/adapters/index.ts +0 -4
  33. package/src/adapters/openai.ts +0 -238
  34. package/src/adapters/rest.ts +0 -298
  35. package/src/cli/index.ts +0 -516
  36. package/src/cli.ts +0 -2
  37. package/src/config/manager.ts +0 -137
  38. package/src/index.ts +0 -4
  39. package/src/mcp/index.ts +0 -1
  40. package/src/mcp/server.ts +0 -67
  41. package/src/server/index.ts +0 -1
  42. package/src/server/unified.ts +0 -474
  43. package/src/storage/index.ts +0 -128
  44. package/src/types.ts +0 -183
  45. package/src/utils/crypto.ts +0 -100
  46. package/src/utils/http.ts +0 -119
  47. package/src/utils/session.ts +0 -146
  48. 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('-e, --encrypted', 'Enable encryption', true)
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
- console.log(chalk.green('EnvCP initialized successfully!'));
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(` Storage: ${config.storage.path}`));
53
- console.log(chalk.gray(` Encrypted: ${config.storage.encrypted}`));
54
- console.log(chalk.gray(` Session timeout: ${config.session?.timeout_minutes || 30} minutes`));
55
- console.log(chalk.gray(` AI active check: ${config.access?.allow_ai_active_check ? 'enabled' : 'disabled'}`));
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
- // MCP mode uses stdio
319
- if (mode === 'mcp') {
320
- const { EnvCPServer } = await import('../mcp/server.js');
321
- const server = new EnvCPServer(config, projectPath, password);
322
- await server.start();
323
- return;
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
- default:
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