@fentz26/envcp 1.0.92 → 1.1.0

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 (64) hide show
  1. package/README.md +81 -20
  2. package/dist/adapters/base.d.ts +10 -2
  3. package/dist/adapters/base.d.ts.map +1 -1
  4. package/dist/adapters/base.js +160 -11
  5. package/dist/adapters/base.js.map +1 -1
  6. package/dist/adapters/gemini.d.ts +1 -1
  7. package/dist/adapters/gemini.d.ts.map +1 -1
  8. package/dist/adapters/gemini.js +3 -2
  9. package/dist/adapters/gemini.js.map +1 -1
  10. package/dist/adapters/openai.d.ts +1 -1
  11. package/dist/adapters/openai.d.ts.map +1 -1
  12. package/dist/adapters/openai.js +3 -2
  13. package/dist/adapters/openai.js.map +1 -1
  14. package/dist/adapters/rest.d.ts +1 -1
  15. package/dist/adapters/rest.d.ts.map +1 -1
  16. package/dist/adapters/rest.js +3 -2
  17. package/dist/adapters/rest.js.map +1 -1
  18. package/dist/cli/index.js +238 -53
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/config/config-guard.d.ts +37 -0
  21. package/dist/config/config-guard.d.ts.map +1 -0
  22. package/dist/config/config-guard.js +212 -0
  23. package/dist/config/config-guard.js.map +1 -0
  24. package/dist/config/manager.d.ts.map +1 -1
  25. package/dist/config/manager.js +17 -11
  26. package/dist/config/manager.js.map +1 -1
  27. package/dist/mcp/server.d.ts +1 -1
  28. package/dist/mcp/server.d.ts.map +1 -1
  29. package/dist/mcp/server.js +2 -2
  30. package/dist/mcp/server.js.map +1 -1
  31. package/dist/server/unified.d.ts.map +1 -1
  32. package/dist/server/unified.js +13 -9
  33. package/dist/server/unified.js.map +1 -1
  34. package/dist/storage/index.d.ts +1 -1
  35. package/dist/storage/index.d.ts.map +1 -1
  36. package/dist/storage/index.js +35 -31
  37. package/dist/storage/index.js.map +1 -1
  38. package/dist/types.d.ts +127 -22
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js +25 -7
  41. package/dist/types.js.map +1 -1
  42. package/dist/utils/crypto.d.ts +18 -0
  43. package/dist/utils/crypto.d.ts.map +1 -1
  44. package/dist/utils/crypto.js +31 -0
  45. package/dist/utils/crypto.js.map +1 -1
  46. package/dist/utils/fs.d.ts +4 -0
  47. package/dist/utils/fs.d.ts.map +1 -0
  48. package/dist/utils/fs.js +40 -0
  49. package/dist/utils/fs.js.map +1 -0
  50. package/dist/utils/http.d.ts.map +1 -1
  51. package/dist/utils/http.js +4 -1
  52. package/dist/utils/http.js.map +1 -1
  53. package/dist/utils/session.d.ts.map +1 -1
  54. package/dist/utils/session.js +24 -25
  55. package/dist/utils/session.js.map +1 -1
  56. package/dist/utils/update-checker.d.ts +45 -0
  57. package/dist/utils/update-checker.d.ts.map +1 -0
  58. package/dist/utils/update-checker.js +200 -0
  59. package/dist/utils/update-checker.js.map +1 -0
  60. package/dist/vault/index.d.ts +13 -0
  61. package/dist/vault/index.d.ts.map +1 -0
  62. package/dist/vault/index.js +90 -0
  63. package/dist/vault/index.js.map +1 -0
  64. package/package.json +1 -6
package/dist/cli/index.js CHANGED
@@ -3,18 +3,26 @@ import inquirer from 'inquirer';
3
3
  import chalk from 'chalk';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
- import fs from 'fs-extra';
6
+ import * as fs from 'fs/promises';
7
+ import { ensureDir, pathExists } from '../utils/fs.js';
7
8
  import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig, isBlacklisted, canAccess } from '../config/manager.js';
9
+ import { ConfigGuard } from '../config/config-guard.js';
8
10
  import { StorageManager } from '../storage/index.js';
9
11
  import { SessionManager } from '../utils/session.js';
10
12
  import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
11
13
  import { KeychainManager } from '../utils/keychain.js';
12
- async function withSession(fn) {
14
+ import { checkForUpdate, formatUpdateMessage, logUpdateCheck } from '../utils/update-checker.js';
15
+ import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
16
+ async function withSession(fn, vaultOverride) {
13
17
  const projectPath = process.cwd();
14
18
  const config = await loadConfig(projectPath);
15
- // Passwordless mode: no session, no password
19
+ const vaultPath = vaultOverride
20
+ ? vaultOverride === 'global'
21
+ ? getGlobalVaultPath(config)
22
+ : getProjectVaultPath(projectPath, config)
23
+ : await resolveVaultPath(projectPath, config);
16
24
  if (config.encryption?.enabled === false) {
17
- const storage = new StorageManager(path.join(projectPath, config.storage.path), false);
25
+ const storage = new StorageManager(vaultPath, false);
18
26
  await fn(storage, '', config, projectPath);
19
27
  return;
20
28
  }
@@ -23,7 +31,6 @@ async function withSession(fn) {
23
31
  let session = await sessionManager.load();
24
32
  let password = '';
25
33
  if (!session) {
26
- // Try OS keychain first if enabled
27
34
  if (config.keychain?.enabled) {
28
35
  const keychain = new KeychainManager(config.keychain.service || 'envcp');
29
36
  const stored = await keychain.retrievePassword(projectPath);
@@ -37,19 +44,19 @@ async function withSession(fn) {
37
44
  { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
38
45
  ]);
39
46
  password = answer.password;
40
- const validation = validatePassword(password, config.password || {});
41
- if (!validation.valid) {
42
- console.log(chalk.red(validation.error));
47
+ const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
48
+ if (!passwordValid) {
49
+ console.log(chalk.red("Invalid password"));
43
50
  return;
44
51
  }
45
- if (validation.warning) {
46
- console.log(chalk.yellow(`⚠ ${validation.warning}`));
52
+ if (passwordValid && passwordWarning) {
53
+ console.log(chalk.yellow('⚠ Weak password detected'));
47
54
  }
48
55
  }
49
56
  session = await sessionManager.create(password);
50
57
  }
51
58
  password = sessionManager.getPassword() || password;
52
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
59
+ const storage = new StorageManager(vaultPath, config.storage.encrypted);
53
60
  if (password)
54
61
  storage.setPassword(password);
55
62
  await fn(storage, password, config, projectPath);
@@ -130,7 +137,7 @@ program
130
137
  // Auto-import .env
131
138
  if (!options.skipEnv) {
132
139
  const envPath = path.join(projectPath, '.env');
133
- if (await fs.pathExists(envPath)) {
140
+ if (await pathExists(envPath)) {
134
141
  const envContent = await fs.readFile(envPath, 'utf8');
135
142
  const vars = parseEnvFile(envContent);
136
143
  const count = Object.keys(vars).length;
@@ -145,6 +152,7 @@ program
145
152
  encrypted: config.storage.encrypted,
146
153
  created: now, updated: now,
147
154
  sync_to_env: true,
155
+ protected: false,
148
156
  });
149
157
  }
150
158
  // Create session for encrypted mode
@@ -217,13 +225,14 @@ program
217
225
  ]);
218
226
  password = answer.password;
219
227
  }
220
- const validation = validatePassword(password, config.password || {});
221
- if (!validation.valid) {
222
- console.log(chalk.red(validation.error));
228
+ const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
229
+ if (!passwordValid) {
230
+ // nosem: no tainted data flows to log
231
+ console.log(chalk.red("Invalid password"));
223
232
  return;
224
233
  }
225
- if (validation.warning) {
226
- console.log(chalk.yellow(`⚠ ${validation.warning}`));
234
+ if (passwordValid && passwordWarning) {
235
+ console.log(chalk.yellow('⚠ Weak password detected'));
227
236
  }
228
237
  const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
229
238
  await sessionManager.init();
@@ -241,10 +250,10 @@ program
241
250
  // Generate recovery key for new stores in recoverable mode
242
251
  if (config.security?.mode === 'recoverable') {
243
252
  const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
244
- if (!await fs.pathExists(recoveryPath)) {
253
+ if (!await pathExists(recoveryPath)) {
245
254
  const recoveryKey = generateRecoveryKey();
246
255
  const recoveryData = await createRecoveryData(password, recoveryKey);
247
- await fs.ensureDir(path.dirname(recoveryPath));
256
+ await ensureDir(path.dirname(recoveryPath));
248
257
  await fs.writeFile(recoveryPath, recoveryData, 'utf8');
249
258
  console.log('');
250
259
  console.log(chalk.yellow.bold('RECOVERY KEY (save this somewhere safe!):'));
@@ -320,8 +329,28 @@ program
320
329
  const maxExt = config.session?.max_extensions || 5;
321
330
  console.log(chalk.green('Session active'));
322
331
  console.log(chalk.gray(` Session ID: ${session.id}`));
323
- console.log(chalk.gray(` Remaining: ${remaining} minutes`));
324
- console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
332
+ console.log(chalk.gray(` Remaining: ${remaining} minutes`));
333
+ console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
334
+ });
335
+ program
336
+ .command('config')
337
+ .description('Config management commands')
338
+ .command('reload')
339
+ .description('Reload config from envcp.yaml (requires password)')
340
+ .action(async () => {
341
+ const projectPath = process.cwd();
342
+ const configGuard = new ConfigGuard(projectPath);
343
+ const { password } = await inquirer.prompt([
344
+ { type: 'password', name: 'password', message: 'Enter password to reload config:', mask: '*' }
345
+ ]);
346
+ const result = await configGuard.reload(password);
347
+ if (result.success) {
348
+ console.log(chalk.green('Config reloaded successfully'));
349
+ console.log(chalk.gray(' New config hash: ' + configGuard.getHash()?.substring(0, 16) + '...'));
350
+ }
351
+ else {
352
+ console.log(chalk.red(result.error || 'Failed to reload config'));
353
+ }
325
354
  });
326
355
  program
327
356
  .command('extend')
@@ -361,7 +390,7 @@ program
361
390
  return;
362
391
  }
363
392
  const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
364
- if (!await fs.pathExists(recoveryPath)) {
393
+ if (!await pathExists(recoveryPath)) {
365
394
  console.log(chalk.red('No recovery file found. Recovery is not available.'));
366
395
  return;
367
396
  }
@@ -432,7 +461,7 @@ program
432
461
  // Check recovery file
433
462
  if (config.security?.mode === 'recoverable') {
434
463
  const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
435
- const hasRecovery = await fs.pathExists(recoveryPath);
464
+ const hasRecovery = await pathExists(recoveryPath);
436
465
  console.log(chalk.gray(` Recovery: ${hasRecovery ? 'available' : 'not found'}`));
437
466
  }
438
467
  else {
@@ -482,6 +511,7 @@ program
482
511
  created: now,
483
512
  updated: now,
484
513
  sync_to_env: true,
514
+ protected: false,
485
515
  };
486
516
  await storage.set(name, variable);
487
517
  console.log(chalk.green(`Variable '${name}' added successfully`));
@@ -588,7 +618,7 @@ program
588
618
  if (options.dryRun) {
589
619
  const envPath = path.join(projectPath, config.sync.target);
590
620
  const existing = {};
591
- if (await fs.pathExists(envPath)) {
621
+ if (await pathExists(envPath)) {
592
622
  const content = await fs.readFile(envPath, 'utf8');
593
623
  Object.assign(existing, parseEnvFile(content));
594
624
  }
@@ -652,7 +682,8 @@ program
652
682
  .option('-k, --api-key <key>', 'API key for HTTP authentication')
653
683
  .action(async (options) => {
654
684
  const projectPath = process.cwd();
655
- const config = await loadConfig(projectPath);
685
+ const configGuard = new ConfigGuard(projectPath);
686
+ const config = await configGuard.loadAndLock();
656
687
  const mode = options.mode;
657
688
  const port = parseInt(options.port, 10);
658
689
  const host = options.host;
@@ -682,13 +713,14 @@ program
682
713
  { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
683
714
  ]);
684
715
  password = answer.password;
685
- const validation = validatePassword(password, config.password || {});
686
- if (!validation.valid) {
687
- console.log(chalk.red(validation.error));
716
+ const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
717
+ if (!passwordValid) {
718
+ // nosem: no tainted data flows to log
719
+ console.log(chalk.red("Invalid password"));
688
720
  return;
689
721
  }
690
- if (validation.warning) {
691
- console.log(chalk.yellow(`⚠ ${validation.warning}`));
722
+ if (passwordValid && passwordWarning) {
723
+ console.log(chalk.yellow('⚠ Weak password detected'));
692
724
  }
693
725
  session = await sessionManager.create(password);
694
726
  }
@@ -710,6 +742,7 @@ program
710
742
  api_key: apiKey,
711
743
  cors: true,
712
744
  auto_detect: mode === 'auto',
745
+ rate_limit: config.server?.rate_limit,
713
746
  };
714
747
  const server = new UnifiedServer(config, serverConfig, projectPath, password);
715
748
  console.log(chalk.blue('Starting EnvCP server...'));
@@ -820,7 +853,7 @@ program
820
853
  .option('--dry-run', 'Preview what would be imported without writing')
821
854
  .action(async (file, options) => {
822
855
  await withSession(async (storage) => {
823
- if (!await fs.pathExists(file)) {
856
+ if (!await pathExists(file)) {
824
857
  console.log(chalk.red(`File not found: ${file}`));
825
858
  return;
826
859
  }
@@ -930,7 +963,7 @@ program
930
963
  variables,
931
964
  }, null, 2);
932
965
  const encrypted = await encrypt(backupData, password);
933
- await fs.ensureDir(path.dirname(outputPath));
966
+ await ensureDir(path.dirname(outputPath));
934
967
  await fs.writeFile(outputPath, encrypted, 'utf8');
935
968
  console.log(chalk.green(`Backup created: ${outputPath}`));
936
969
  console.log(chalk.gray(` Variables: ${count}`));
@@ -943,7 +976,7 @@ program
943
976
  .option('--merge', 'Merge with existing variables (default: replace)')
944
977
  .action(async (file, options) => {
945
978
  await withSession(async (storage, password) => {
946
- if (!await fs.pathExists(file)) {
979
+ if (!await pathExists(file)) {
947
980
  console.log(chalk.red(`Backup file not found: ${file}`));
948
981
  return;
949
982
  }
@@ -1007,7 +1040,7 @@ program
1007
1040
  checks.push({ name: 'Security mode', status: 'pass', detail: config.security?.mode || 'recoverable' });
1008
1041
  // 4. Store file
1009
1042
  const storePath = path.join(projectPath, config.storage.path);
1010
- if (await fs.pathExists(storePath)) {
1043
+ if (await pathExists(storePath)) {
1011
1044
  const stat = await fs.stat(storePath);
1012
1045
  checks.push({ name: 'Store file', status: 'pass', detail: `Exists (${stat.size} bytes)` });
1013
1046
  }
@@ -1033,7 +1066,7 @@ program
1033
1066
  // 6. Recovery file
1034
1067
  if (config.security?.mode === 'recoverable') {
1035
1068
  const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
1036
- if (await fs.pathExists(recoveryPath)) {
1069
+ if (await pathExists(recoveryPath)) {
1037
1070
  checks.push({ name: 'Recovery file', status: 'pass', detail: 'Present' });
1038
1071
  }
1039
1072
  else {
@@ -1045,7 +1078,7 @@ program
1045
1078
  }
1046
1079
  // 7. .envcp directory
1047
1080
  const envcpDir = path.join(projectPath, '.envcp');
1048
- if (await fs.pathExists(envcpDir)) {
1081
+ if (await pathExists(envcpDir)) {
1049
1082
  checks.push({ name: '.envcp directory', status: 'pass', detail: 'Exists' });
1050
1083
  }
1051
1084
  else {
@@ -1053,7 +1086,7 @@ program
1053
1086
  }
1054
1087
  // 8. .gitignore check
1055
1088
  const gitignorePath = path.join(projectPath, '.gitignore');
1056
- if (await fs.pathExists(gitignorePath)) {
1089
+ if (await pathExists(gitignorePath)) {
1057
1090
  const gitignore = await fs.readFile(gitignorePath, 'utf8');
1058
1091
  if (gitignore.includes('.envcp/')) {
1059
1092
  checks.push({ name: '.gitignore', status: 'pass', detail: '.envcp/ is ignored' });
@@ -1097,26 +1130,178 @@ program
1097
1130
  console.log(chalk.green('All checks passed.'));
1098
1131
  }
1099
1132
  });
1133
+ program
1134
+ .command('update')
1135
+ .description('Check for EnvCP updates')
1136
+ .option('--check', 'Check for available updates')
1137
+ .action(async (options) => {
1138
+ const projectPath = process.cwd();
1139
+ if (options.check || Object.keys(options).length === 0) {
1140
+ console.log(chalk.blue('Checking for updates...'));
1141
+ try {
1142
+ const info = await checkForUpdate(projectPath);
1143
+ const message = formatUpdateMessage(info);
1144
+ await logUpdateCheck(projectPath, info);
1145
+ if (info.updateAvailable) {
1146
+ if (info.critical) {
1147
+ console.log(chalk.red.bold(message));
1148
+ }
1149
+ else {
1150
+ console.log(chalk.yellow(message));
1151
+ }
1152
+ }
1153
+ else {
1154
+ console.log(chalk.green(message));
1155
+ }
1156
+ }
1157
+ catch (error) {
1158
+ console.log(chalk.yellow('Could not check for updates (offline or rate-limited)'));
1159
+ console.log(chalk.gray(` Current version: v${require('../../package.json').version}`));
1160
+ }
1161
+ }
1162
+ });
1100
1163
  program
1101
1164
  .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) => {
1165
+ .description('Manage vaults (global, project, or named)')
1166
+ .option('--global', 'Operate on the global vault')
1167
+ .option('--project', 'Operate on the project vault')
1168
+ .option('--name <name>', 'Operate on a named vault')
1169
+ .addCommand(new Command('init')
1170
+ .description('Initialize a vault')
1171
+ .action(async (options, cmd) => {
1172
+ const parentOpts = cmd.parent.opts();
1107
1173
  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}`);
1174
+ const config = await loadConfig(projectPath);
1175
+ let vaultPath;
1176
+ let vaultName;
1177
+ if (parentOpts.global) {
1178
+ vaultPath = getGlobalVaultPath(config);
1179
+ vaultName = 'global';
1114
1180
  }
1115
- catch (error) {
1116
- console.error(`Failed to rename vault: ${error.message}`);
1117
- process.exit(1);
1181
+ else if (parentOpts.name) {
1182
+ vaultPath = await initNamedVault(projectPath, parentOpts.name);
1183
+ vaultName = parentOpts.name;
1184
+ }
1185
+ else {
1186
+ vaultPath = getProjectVaultPath(projectPath, config);
1187
+ vaultName = 'project';
1118
1188
  }
1189
+ const vaultDir = path.dirname(vaultPath);
1190
+ await ensureDir(vaultDir);
1191
+ console.log(chalk.green(`Vault "${vaultName}" initialized at ${vaultPath}`));
1192
+ }))
1193
+ .addCommand(new Command('add')
1194
+ .description('Add a variable to vault')
1195
+ .argument('<name>', 'Variable name')
1196
+ .option('-v, --value <value>', 'Variable value')
1197
+ .option('-t, --tags <tags>', 'Tags (comma-separated)')
1198
+ .action(async (name, options, cmd) => {
1199
+ const parentOpts = cmd.parent.opts();
1200
+ const vaultOverride = parentOpts.global ? 'global' : parentOpts.project ? 'project' : undefined;
1201
+ await withSession(async (storage) => {
1202
+ const value = options.value || (await inquirer.prompt([{ type: 'input', name: 'value', message: 'Value:' }])).value;
1203
+ const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
1204
+ const now = new Date().toISOString();
1205
+ await storage.set(name, {
1206
+ name,
1207
+ value,
1208
+ encrypted: storage.encrypted,
1209
+ created: now,
1210
+ updated: now,
1211
+ sync_to_env: true,
1212
+ tags,
1213
+ protected: false,
1214
+ });
1215
+ console.log(chalk.green(`Variable '${name}' added to vault`));
1216
+ }, vaultOverride);
1217
+ }))
1218
+ .addCommand(new Command('list')
1219
+ .description('List vault contents')
1220
+ .option('-v, --show-values', 'Show actual values')
1221
+ .action(async (options, cmd) => {
1222
+ const parentOpts = cmd.parent.opts();
1223
+ const vaultOverride = parentOpts.global ? 'global' : parentOpts.project ? 'project' : undefined;
1224
+ await withSession(async (storage) => {
1225
+ const variables = await storage.load();
1226
+ const names = Object.keys(variables);
1227
+ if (names.length === 0) {
1228
+ console.log(chalk.gray('Vault is empty'));
1229
+ return;
1230
+ }
1231
+ for (const name of names) {
1232
+ const v = variables[name];
1233
+ const display = options.showValues ? v.value : maskValue(v.value);
1234
+ const tags = v.tags?.length ? ` [${v.tags.join(', ')}]` : '';
1235
+ console.log(` ${name} = ${display}${tags}`);
1236
+ }
1237
+ }, vaultOverride);
1238
+ }))
1239
+ .addCommand(new Command('get')
1240
+ .description('Get a variable from vault')
1241
+ .argument('<name>', 'Variable name')
1242
+ .option('-v, --show-value', 'Show actual value')
1243
+ .action(async (name, options, cmd) => {
1244
+ const parentOpts = cmd.parent.opts();
1245
+ const vaultOverride = parentOpts.global ? 'global' : parentOpts.project ? 'project' : undefined;
1246
+ await withSession(async (storage) => {
1247
+ const v = await storage.get(name);
1248
+ if (!v) {
1249
+ console.log(chalk.red(`Variable '${name}' not found`));
1250
+ return;
1251
+ }
1252
+ const display = options.showValue ? v.value : maskValue(v.value);
1253
+ console.log(`${name} = ${display}`);
1254
+ }, vaultOverride);
1255
+ }))
1256
+ .addCommand(new Command('delete')
1257
+ .description('Delete a variable from vault')
1258
+ .argument('<name>', 'Variable name')
1259
+ .action(async (name, cmd) => {
1260
+ const parentOpts = cmd.parent.parent.opts();
1261
+ const vaultOverride = parentOpts.global ? 'global' : parentOpts.project ? 'project' : undefined;
1262
+ await withSession(async (storage) => {
1263
+ const deleted = await storage.delete(name);
1264
+ if (deleted) {
1265
+ console.log(chalk.green(`Variable '${name}' deleted`));
1266
+ }
1267
+ else {
1268
+ console.log(chalk.red(`Variable '${name}' not found`));
1269
+ }
1270
+ }, vaultOverride);
1119
1271
  }));
1272
+ program
1273
+ .command('vault-switch')
1274
+ .description('Switch active vault context')
1275
+ .argument('<name>', 'Vault name (global, project, or named vault)')
1276
+ .action(async (name) => {
1277
+ const projectPath = process.cwd();
1278
+ const config = await loadConfig(projectPath);
1279
+ if (name !== 'global' && name !== 'project') {
1280
+ const vaultDir = path.join(projectPath, '.envcp/vaults', name);
1281
+ if (!await pathExists(vaultDir)) {
1282
+ console.log(chalk.red(`Named vault "${name}" does not exist. Create it with: envcp vault --name ${name} init`));
1283
+ return;
1284
+ }
1285
+ }
1286
+ await setActiveVault(projectPath, name);
1287
+ console.log(chalk.green(`Switched to vault: ${name}`));
1288
+ });
1289
+ program
1290
+ .command('vault-list')
1291
+ .description('List all available vaults')
1292
+ .action(async () => {
1293
+ const projectPath = process.cwd();
1294
+ const config = await loadConfig(projectPath);
1295
+ const vaults = await listVaults(projectPath, config);
1296
+ console.log('Available vaults:');
1297
+ for (const v of vaults) {
1298
+ const active = v.active ? chalk.green(' (active)') : '';
1299
+ const exists = await pathExists(v.path);
1300
+ const status = exists ? '' : chalk.gray(' [not initialized]');
1301
+ console.log(` ${v.name}${active}${status}`);
1302
+ console.log(chalk.gray(` ${v.path}`));
1303
+ }
1304
+ });
1120
1305
  program
1121
1306
  .command('keychain')
1122
1307
  .description('Manage OS keychain integration')
@@ -1198,8 +1383,8 @@ program
1198
1383
  }));
1199
1384
  // Show welcome screen on first ever run
1200
1385
  const firstRunMarker = path.join(os.homedir(), '.envcp', '.welcomed');
1201
- if (!await fs.pathExists(firstRunMarker)) {
1202
- await fs.ensureDir(path.dirname(firstRunMarker));
1386
+ if (!await pathExists(firstRunMarker)) {
1387
+ await ensureDir(path.dirname(firstRunMarker));
1203
1388
  await fs.writeFile(firstRunMarker, new Date().toISOString());
1204
1389
  console.log(`
1205
1390
  ███████╗███╗ ██╗██╗ ██╗ ██████╗██████╗