@fentz26/envcp 1.0.7 → 1.0.9

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/dist/cli/index.js CHANGED
@@ -2,11 +2,13 @@ import { Command } from 'commander';
2
2
  import inquirer from 'inquirer';
3
3
  import chalk from 'chalk';
4
4
  import * as path from 'path';
5
+ import * as os from 'os';
5
6
  import fs from 'fs-extra';
6
- import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig } from '../config/manager.js';
7
+ import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig, isBlacklisted, canAccess } from '../config/manager.js';
7
8
  import { StorageManager } from '../storage/index.js';
8
9
  import { SessionManager } from '../utils/session.js';
9
10
  import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
11
+ import { KeychainManager } from '../utils/keychain.js';
10
12
  async function withSession(fn) {
11
13
  const projectPath = process.cwd();
12
14
  const config = await loadConfig(projectPath);
@@ -21,14 +23,28 @@ async function withSession(fn) {
21
23
  let session = await sessionManager.load();
22
24
  let password = '';
23
25
  if (!session) {
24
- const answer = await inquirer.prompt([
25
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
26
- ]);
27
- password = answer.password;
28
- const validation = validatePassword(password, config.password || {});
29
- if (!validation.valid) {
30
- console.log(chalk.red(validation.error));
31
- return;
26
+ // Try OS keychain first if enabled
27
+ if (config.keychain?.enabled) {
28
+ const keychain = new KeychainManager(config.keychain.service || 'envcp');
29
+ const stored = await keychain.retrievePassword(projectPath);
30
+ if (stored) {
31
+ password = stored;
32
+ console.log(chalk.gray('Password retrieved from OS keychain'));
33
+ }
34
+ }
35
+ if (!password) {
36
+ const answer = await inquirer.prompt([
37
+ { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
38
+ ]);
39
+ password = answer.password;
40
+ const validation = validatePassword(password, config.password || {});
41
+ if (!validation.valid) {
42
+ console.log(chalk.red(validation.error));
43
+ return;
44
+ }
45
+ if (validation.warning) {
46
+ console.log(chalk.yellow(`⚠ ${validation.warning}`));
47
+ }
32
48
  }
33
49
  session = await sessionManager.create(password);
34
50
  }
@@ -145,7 +161,7 @@ program
145
161
  // Generate recovery key for encrypted recoverable mode
146
162
  if (securityChoice === 'recoverable' && pwd) {
147
163
  const recoveryKey = generateRecoveryKey();
148
- const recoveryData = createRecoveryData(pwd, recoveryKey);
164
+ const recoveryData = await createRecoveryData(pwd, recoveryKey);
149
165
  const recoveryPath = path.join(projectPath, config.security.recovery_file);
150
166
  await fs.writeFile(recoveryPath, recoveryData, 'utf8');
151
167
  console.log('');
@@ -185,6 +201,7 @@ program
185
201
  .command('unlock')
186
202
  .description('Unlock EnvCP session with password')
187
203
  .option('-p, --password <password>', 'Password (will prompt if not provided)')
204
+ .option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
188
205
  .action(async (options) => {
189
206
  const projectPath = process.cwd();
190
207
  const config = await loadConfig(projectPath);
@@ -205,6 +222,9 @@ program
205
222
  console.log(chalk.red(validation.error));
206
223
  return;
207
224
  }
225
+ if (validation.warning) {
226
+ console.log(chalk.yellow(`⚠ ${validation.warning}`));
227
+ }
208
228
  const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
209
229
  await sessionManager.init();
210
230
  const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
@@ -223,7 +243,7 @@ program
223
243
  const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
224
244
  if (!await fs.pathExists(recoveryPath)) {
225
245
  const recoveryKey = generateRecoveryKey();
226
- const recoveryData = createRecoveryData(password, recoveryKey);
246
+ const recoveryData = await createRecoveryData(password, recoveryKey);
227
247
  await fs.ensureDir(path.dirname(recoveryPath));
228
248
  await fs.writeFile(recoveryPath, recoveryData, 'utf8');
229
249
  console.log('');
@@ -247,6 +267,26 @@ program
247
267
  console.log(chalk.gray(` Expires in: ${config.session?.timeout_minutes || 30} minutes`));
248
268
  const maxExt = config.session?.max_extensions || 5;
249
269
  console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
270
+ // Save to keychain if requested
271
+ if (options.saveToKeychain) {
272
+ const keychain = new KeychainManager(config.keychain?.service || 'envcp');
273
+ if (await keychain.isAvailable()) {
274
+ const result = await keychain.storePassword(password, projectPath);
275
+ if (result.success) {
276
+ // Enable keychain in config
277
+ config.keychain = { ...config.keychain, enabled: true };
278
+ await saveConfig(config, projectPath);
279
+ console.log(chalk.green(`Password saved to ${keychain.backendName}`));
280
+ console.log(chalk.gray(' Future sessions will auto-unlock from keychain'));
281
+ }
282
+ else {
283
+ console.log(chalk.red(`Failed to save to keychain: ${result.error}`));
284
+ }
285
+ }
286
+ else {
287
+ console.log(chalk.red(`OS keychain not available (${keychain.backendName})`));
288
+ }
289
+ }
250
290
  });
251
291
  program
252
292
  .command('lock')
@@ -331,7 +371,7 @@ program
331
371
  const recoveryData = await fs.readFile(recoveryPath, 'utf8');
332
372
  let oldPassword;
333
373
  try {
334
- oldPassword = recoverPassword(recoveryData, recoveryKey);
374
+ oldPassword = await recoverPassword(recoveryData, recoveryKey);
335
375
  }
336
376
  catch {
337
377
  console.log(chalk.red('Invalid recovery key.'));
@@ -366,7 +406,7 @@ program
366
406
  await storage.save(variables);
367
407
  // Update recovery file with new password
368
408
  const newRecoveryKey = generateRecoveryKey();
369
- const newRecoveryData = createRecoveryData(newPassword, newRecoveryKey);
409
+ const newRecoveryData = await createRecoveryData(newPassword, newRecoveryKey);
370
410
  await fs.writeFile(recoveryPath, newRecoveryData, 'utf8');
371
411
  console.log(chalk.green('Password reset successfully!'));
372
412
  console.log('');
@@ -452,7 +492,7 @@ program
452
492
  .description('List all variables (names only, values hidden)')
453
493
  .option('-v, --show-values', 'Show actual values')
454
494
  .action(async (options) => {
455
- await withSession(async (storage) => {
495
+ await withSession(async (storage, _password, config) => {
456
496
  const variables = await storage.load();
457
497
  const names = Object.keys(variables);
458
498
  if (names.length === 0) {
@@ -462,7 +502,9 @@ program
462
502
  console.log(chalk.blue(`\nVariables (${names.length}):\n`));
463
503
  for (const name of names) {
464
504
  const v = variables[name];
465
- const value = options.showValues ? v.value : maskValue(v.value);
505
+ const value = config.access?.mask_values && !options.showValues
506
+ ? maskValue(v.value)
507
+ : v.value;
466
508
  const tags = v.tags ? chalk.gray(` [${v.tags.join(', ')}]`) : '';
467
509
  console.log(` ${chalk.cyan(name)} = ${value}${tags}`);
468
510
  }
@@ -472,15 +514,27 @@ program
472
514
  program
473
515
  .command('get <name>')
474
516
  .description('Get a variable value')
475
- .action(async (name) => {
476
- await withSession(async (storage) => {
517
+ .option('--show-value', 'Reveal the unmasked value')
518
+ .action(async (name, options) => {
519
+ await withSession(async (storage, _password, config) => {
520
+ if (isBlacklisted(name, config)) {
521
+ console.log(chalk.red(`Variable '${name}' is blacklisted and cannot be accessed`));
522
+ return;
523
+ }
524
+ if (!canAccess(name, config)) {
525
+ console.log(chalk.red(`Access denied to variable '${name}'`));
526
+ return;
527
+ }
477
528
  const variable = await storage.get(name);
478
529
  if (!variable) {
479
530
  console.log(chalk.red(`Variable '${name}' not found`));
480
531
  return;
481
532
  }
533
+ const value = config.access?.mask_values && !options.showValue
534
+ ? maskValue(variable.value)
535
+ : variable.value;
482
536
  console.log(chalk.cyan(name));
483
- console.log(` Value: ${variable.value}`);
537
+ console.log(` Value: ${value}`);
484
538
  if (variable.tags)
485
539
  console.log(` Tags: ${variable.tags.join(', ')}`);
486
540
  if (variable.description)
@@ -517,6 +571,16 @@ program
517
571
  lines.push(config.sync.header);
518
572
  }
519
573
  for (const [name, variable] of Object.entries(variables)) {
574
+ if (isBlacklisted(name, config) || !canAccess(name, config))
575
+ continue;
576
+ if (!variable.sync_to_env)
577
+ continue;
578
+ const excluded = config.sync.exclude?.some((pattern) => {
579
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
580
+ return regex.test(name);
581
+ });
582
+ if (excluded)
583
+ continue;
520
584
  const needsQuoting = /[\s#"'\\]/.test(variable.value);
521
585
  const val = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
522
586
  lines.push(`${name}=${val}`);
@@ -532,6 +596,16 @@ program
532
596
  const updated = [];
533
597
  const removed = [];
534
598
  for (const [name, variable] of Object.entries(variables)) {
599
+ if (isBlacklisted(name, config) || !canAccess(name, config))
600
+ continue;
601
+ if (!variable.sync_to_env)
602
+ continue;
603
+ const excluded = config.sync.exclude?.some((pattern) => {
604
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
605
+ return regex.test(name);
606
+ });
607
+ if (excluded)
608
+ continue;
535
609
  if (name in existing) {
536
610
  if (existing[name] !== variable.value)
537
611
  updated.push(name);
@@ -613,6 +687,9 @@ program
613
687
  console.log(chalk.red(validation.error));
614
688
  return;
615
689
  }
690
+ if (validation.warning) {
691
+ console.log(chalk.yellow(`⚠ ${validation.warning}`));
692
+ }
616
693
  session = await sessionManager.create(password);
617
694
  }
618
695
  password = sessionManager.getPassword() || password;
@@ -640,7 +717,7 @@ program
640
717
  console.log(chalk.gray(` Host: ${host}`));
641
718
  console.log(chalk.gray(` Port: ${port}`));
642
719
  if (apiKey)
643
- console.log(chalk.gray(` API Key: ${apiKey.substring(0, 4)}...`));
720
+ console.log(chalk.gray(` API Key: ${'*'.repeat(apiKey.length)}`));
644
721
  console.log('');
645
722
  await server.start();
646
723
  console.log(chalk.green(`EnvCP server running at http://${host}:${port}`));
@@ -708,7 +785,7 @@ program
708
785
  meta: { project: config.project, timestamp: new Date().toISOString(), count: Object.keys(variables).length, version: '1.0' },
709
786
  variables,
710
787
  }, null, 2);
711
- const encrypted = encrypt(exportData, exportPassword);
788
+ const encrypted = await encrypt(exportData, exportPassword);
712
789
  await fs.writeFile(outputPath, encrypted, 'utf8');
713
790
  console.log(chalk.green(`Encrypted export saved to: ${outputPath}`));
714
791
  console.log(chalk.gray(` Variables: ${Object.keys(variables).length}`));
@@ -753,7 +830,7 @@ program
753
830
  const fileContent = await fs.readFile(file, 'utf8');
754
831
  let importData;
755
832
  try {
756
- const decrypted = decrypt(fileContent, importPassword);
833
+ const decrypted = await decrypt(fileContent, importPassword);
757
834
  importData = JSON.parse(decrypted);
758
835
  }
759
836
  catch {
@@ -852,7 +929,7 @@ program
852
929
  },
853
930
  variables,
854
931
  }, null, 2);
855
- const encrypted = encrypt(backupData, password);
932
+ const encrypted = await encrypt(backupData, password);
856
933
  await fs.ensureDir(path.dirname(outputPath));
857
934
  await fs.writeFile(outputPath, encrypted, 'utf8');
858
935
  console.log(chalk.green(`Backup created: ${outputPath}`));
@@ -873,7 +950,7 @@ program
873
950
  const encrypted = await fs.readFile(file, 'utf8');
874
951
  let backupData;
875
952
  try {
876
- const decrypted = decrypt(encrypted, password);
953
+ const decrypted = await decrypt(encrypted, password);
877
954
  backupData = JSON.parse(decrypted);
878
955
  }
879
956
  catch {
@@ -925,7 +1002,7 @@ program
925
1002
  checks.push({ name: 'Config', status: 'pass', detail: `Loaded (project: ${config.project || 'unnamed'})` });
926
1003
  // 2. Encryption mode
927
1004
  const encrypted = config.encryption?.enabled !== false;
928
- checks.push({ name: 'Encryption', status: 'pass', detail: encrypted ? `Enabled (${config.storage.algorithm})` : 'Disabled (passwordless)' });
1005
+ checks.push({ name: 'Encryption', status: 'pass', detail: encrypted ? 'Enabled (AES-256-GCM)' : 'Disabled (passwordless)' });
929
1006
  // 3. Security mode
930
1007
  checks.push({ name: 'Security mode', status: 'pass', detail: config.security?.mode || 'recoverable' });
931
1008
  // 4. Store file
@@ -1020,5 +1097,145 @@ program
1020
1097
  console.log(chalk.green('All checks passed.'));
1021
1098
  }
1022
1099
  });
1100
+ program
1101
+ .command('vault')
1102
+ .description('Manage vault settings')
1103
+ .addCommand(new Command('rename')
1104
+ .description('Rename the current vault (updates project name in config)')
1105
+ .argument('<name>', 'New vault name')
1106
+ .action(async (name) => {
1107
+ const projectPath = process.cwd();
1108
+ try {
1109
+ const config = await loadConfig(projectPath);
1110
+ const old = config.project || path.basename(projectPath);
1111
+ config.project = name;
1112
+ await saveConfig(config, projectPath);
1113
+ console.log(`Vault renamed: ${old} -> ${name}`);
1114
+ }
1115
+ catch (error) {
1116
+ console.error(`Failed to rename vault: ${error.message}`);
1117
+ process.exit(1);
1118
+ }
1119
+ }));
1120
+ program
1121
+ .command('keychain')
1122
+ .description('Manage OS keychain integration')
1123
+ .addCommand(new Command('status')
1124
+ .description('Check keychain availability and stored credentials')
1125
+ .action(async () => {
1126
+ const projectPath = process.cwd();
1127
+ const config = await loadConfig(projectPath);
1128
+ const keychain = new KeychainManager(config.keychain?.service || 'envcp');
1129
+ const status = await keychain.getStatus(projectPath);
1130
+ console.log(chalk.bold('Keychain Status'));
1131
+ console.log(chalk.gray(` Backend: ${status.backend}`));
1132
+ console.log(chalk.gray(` Available: ${status.available ? chalk.green('yes') : chalk.red('no')}`));
1133
+ console.log(chalk.gray(` Stored: ${status.hasPassword ? chalk.green('yes') : chalk.yellow('no')}`));
1134
+ console.log(chalk.gray(` Enabled: ${config.keychain?.enabled ? chalk.green('yes') : chalk.yellow('no')}`));
1135
+ if (!status.available) {
1136
+ console.log('');
1137
+ if (process.platform === 'linux') {
1138
+ console.log(chalk.yellow('Install libsecret: sudo apt install libsecret-tools'));
1139
+ }
1140
+ else if (process.platform === 'darwin') {
1141
+ console.log(chalk.yellow('macOS Keychain should be available by default'));
1142
+ }
1143
+ }
1144
+ else if (!status.hasPassword) {
1145
+ console.log('');
1146
+ console.log(chalk.gray('Run: envcp unlock --save-to-keychain'));
1147
+ }
1148
+ }))
1149
+ .addCommand(new Command('save')
1150
+ .description('Save current password to OS keychain')
1151
+ .action(async () => {
1152
+ await withSession(async (storage, password, config, projectPath) => {
1153
+ if (!password) {
1154
+ console.log(chalk.red('No password available (encryption disabled?)'));
1155
+ return;
1156
+ }
1157
+ const keychain = new KeychainManager(config.keychain?.service || 'envcp');
1158
+ if (!await keychain.isAvailable()) {
1159
+ console.log(chalk.red(`OS keychain not available (${keychain.backendName})`));
1160
+ return;
1161
+ }
1162
+ const result = await keychain.storePassword(password, projectPath);
1163
+ if (result.success) {
1164
+ config.keychain = { ...config.keychain, enabled: true };
1165
+ await saveConfig(config, projectPath);
1166
+ console.log(chalk.green(`Password saved to ${keychain.backendName}`));
1167
+ console.log(chalk.gray(' Future sessions will auto-unlock from keychain'));
1168
+ }
1169
+ else {
1170
+ console.log(chalk.red(`Failed: ${result.error}`));
1171
+ }
1172
+ });
1173
+ }))
1174
+ .addCommand(new Command('remove')
1175
+ .description('Remove stored password from OS keychain')
1176
+ .action(async () => {
1177
+ const projectPath = process.cwd();
1178
+ const config = await loadConfig(projectPath);
1179
+ const keychain = new KeychainManager(config.keychain?.service || 'envcp');
1180
+ const result = await keychain.removePassword(projectPath);
1181
+ if (result.success) {
1182
+ config.keychain = { ...config.keychain, enabled: false };
1183
+ await saveConfig(config, projectPath);
1184
+ console.log(chalk.green('Password removed from keychain'));
1185
+ }
1186
+ else {
1187
+ console.log(chalk.yellow(`Nothing to remove or error: ${result.error}`));
1188
+ }
1189
+ }))
1190
+ .addCommand(new Command('disable')
1191
+ .description('Disable keychain auto-unlock (keeps stored credential)')
1192
+ .action(async () => {
1193
+ const projectPath = process.cwd();
1194
+ const config = await loadConfig(projectPath);
1195
+ config.keychain = { ...config.keychain, enabled: false };
1196
+ await saveConfig(config, projectPath);
1197
+ console.log(chalk.green('Keychain auto-unlock disabled'));
1198
+ }));
1199
+ // Show welcome screen on first ever run
1200
+ const firstRunMarker = path.join(os.homedir(), '.envcp', '.welcomed');
1201
+ if (!await fs.pathExists(firstRunMarker)) {
1202
+ await fs.ensureDir(path.dirname(firstRunMarker));
1203
+ await fs.writeFile(firstRunMarker, new Date().toISOString());
1204
+ console.log(`
1205
+ ███████╗███╗ ██╗██╗ ██╗ ██████╗██████╗
1206
+ ██╔════╝████╗ ██║██║ ██║██╔════╝██╔══██╗
1207
+ █████╗ ██╔██╗ ██║██║ ██║██║ ██████╔╝
1208
+ ██╔══╝ ██║╚██╗██║╚██╗ ██╔╝██║ ██╔═══╝
1209
+ ███████╗██║ ╚████║ ╚████╔╝ ╚██████╗██║
1210
+ ╚══════╝╚═╝ ╚═══╝ ╚═══╝ ╚═════╝╚═╝
1211
+
1212
+ Thanks for installing EnvCP!
1213
+ Keep your secrets safe from AI agents.
1214
+
1215
+ ─────────────────────────────────────────────
1216
+
1217
+ Vault location:
1218
+
1219
+ ~/ or / -> Global vault (shared across all projects)
1220
+ any folder -> Project vault (named after the folder)
1221
+ Rename anytime: envcp vault rename <name>
1222
+
1223
+ ─────────────────────────────────────────────
1224
+
1225
+ Get started:
1226
+
1227
+ Simple (one-time setup):
1228
+ $ envcp init # Interactive guided setup
1229
+
1230
+ Advanced (manual config):
1231
+ $ envcp init --advanced # Full config options
1232
+ $ envcp add [NAME] [VALUE] # Add a secret manually
1233
+
1234
+ Explore:
1235
+ $ envcp --help # See all commands
1236
+
1237
+ Docs: https://github.com/fentz26/EnvCP
1238
+ `);
1239
+ }
1023
1240
  program.parse();
1024
1241
  //# sourceMappingURL=index.js.map