@fentz26/envcp 1.2.0-exp.1 → 1.2.0-exp.92feaad

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 (96) hide show
  1. package/README.md +8 -1
  2. package/VERSION +1 -1
  3. package/dist/adapters/base.d.ts +42 -4
  4. package/dist/adapters/base.d.ts.map +1 -1
  5. package/dist/adapters/base.js +153 -26
  6. package/dist/adapters/base.js.map +1 -1
  7. package/dist/adapters/gemini.d.ts +1 -4
  8. package/dist/adapters/gemini.d.ts.map +1 -1
  9. package/dist/adapters/gemini.js +22 -111
  10. package/dist/adapters/gemini.js.map +1 -1
  11. package/dist/adapters/openai.d.ts +1 -4
  12. package/dist/adapters/openai.d.ts.map +1 -1
  13. package/dist/adapters/openai.js +26 -125
  14. package/dist/adapters/openai.js.map +1 -1
  15. package/dist/adapters/rest.d.ts +1 -1
  16. package/dist/adapters/rest.d.ts.map +1 -1
  17. package/dist/adapters/rest.js +36 -16
  18. package/dist/adapters/rest.js.map +1 -1
  19. package/dist/cli/index.js +278 -107
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/config/config-guard.d.ts.map +1 -1
  22. package/dist/config/config-guard.js +9 -4
  23. package/dist/config/config-guard.js.map +1 -1
  24. package/dist/config/config-hmac.js +2 -2
  25. package/dist/config/config-hmac.js.map +1 -1
  26. package/dist/config/manager.d.ts +6 -2
  27. package/dist/config/manager.d.ts.map +1 -1
  28. package/dist/config/manager.js +53 -33
  29. package/dist/config/manager.js.map +1 -1
  30. package/dist/mcp/server.d.ts +1 -1
  31. package/dist/mcp/server.d.ts.map +1 -1
  32. package/dist/mcp/server.js +7 -3
  33. package/dist/mcp/server.js.map +1 -1
  34. package/dist/server/unified.d.ts.map +1 -1
  35. package/dist/server/unified.js +10 -10
  36. package/dist/server/unified.js.map +1 -1
  37. package/dist/service/config.d.ts.map +1 -1
  38. package/dist/service/config.js +5 -4
  39. package/dist/service/config.js.map +1 -1
  40. package/dist/service/generators.d.ts.map +1 -1
  41. package/dist/service/generators.js +6 -8
  42. package/dist/service/generators.js.map +1 -1
  43. package/dist/service/index.js +4 -3
  44. package/dist/service/index.js.map +1 -1
  45. package/dist/service/platform.js +2 -2
  46. package/dist/service/platform.js.map +1 -1
  47. package/dist/storage/index.d.ts +14 -1
  48. package/dist/storage/index.d.ts.map +1 -1
  49. package/dist/storage/index.js +67 -27
  50. package/dist/storage/index.js.map +1 -1
  51. package/dist/types.d.ts +29 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/types.js +6 -1
  54. package/dist/types.js.map +1 -1
  55. package/dist/utils/crypto.js +2 -2
  56. package/dist/utils/crypto.js.map +1 -1
  57. package/dist/utils/fs.d.ts +1 -0
  58. package/dist/utils/fs.d.ts.map +1 -1
  59. package/dist/utils/fs.js +17 -5
  60. package/dist/utils/fs.js.map +1 -1
  61. package/dist/utils/hsm.d.ts +8 -8
  62. package/dist/utils/hsm.d.ts.map +1 -1
  63. package/dist/utils/hsm.js +7 -6
  64. package/dist/utils/hsm.js.map +1 -1
  65. package/dist/utils/http.d.ts +1 -1
  66. package/dist/utils/http.d.ts.map +1 -1
  67. package/dist/utils/http.js +1 -1
  68. package/dist/utils/http.js.map +1 -1
  69. package/dist/utils/keychain.js +4 -4
  70. package/dist/utils/keychain.js.map +1 -1
  71. package/dist/utils/lock.d.ts.map +1 -1
  72. package/dist/utils/lock.js +2 -1
  73. package/dist/utils/lock.js.map +1 -1
  74. package/dist/utils/lockout.d.ts +2 -2
  75. package/dist/utils/lockout.d.ts.map +1 -1
  76. package/dist/utils/lockout.js +3 -3
  77. package/dist/utils/lockout.js.map +1 -1
  78. package/dist/utils/prompt.js +3 -3
  79. package/dist/utils/prompt.js.map +1 -1
  80. package/dist/utils/secure-memory.d.ts.map +1 -1
  81. package/dist/utils/secure-memory.js +6 -3
  82. package/dist/utils/secure-memory.js.map +1 -1
  83. package/dist/utils/session.js +3 -3
  84. package/dist/utils/session.js.map +1 -1
  85. package/dist/utils/update-checker.d.ts +3 -0
  86. package/dist/utils/update-checker.d.ts.map +1 -1
  87. package/dist/utils/update-checker.js +51 -5
  88. package/dist/utils/update-checker.js.map +1 -1
  89. package/dist/vault/index.d.ts +2 -0
  90. package/dist/vault/index.d.ts.map +1 -1
  91. package/dist/vault/index.js +17 -5
  92. package/dist/vault/index.js.map +1 -1
  93. package/dist/version.js +3 -3
  94. package/dist/version.js.map +1 -1
  95. package/package.json +1 -1
  96. package/scripts/sync-version.js +65 -23
package/dist/cli/index.js CHANGED
@@ -7,20 +7,67 @@ import { promptPassword, promptInput, promptConfirm, promptList } from '../utils
7
7
  import { ensureDir, pathExists } from '../utils/fs.js';
8
8
  import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig, isBlacklisted, canAccess } from '../config/manager.js';
9
9
  import { ConfigGuard } from '../config/config-guard.js';
10
- import { StorageManager, LogManager } from '../storage/index.js';
10
+ import { StorageManager, LogManager, resolveLogPath } from '../storage/index.js';
11
11
  import { VERSION } from '../version.js';
12
12
  import { SessionManager } from '../utils/session.js';
13
13
  import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
14
14
  import { KeychainManager } from '../utils/keychain.js';
15
15
  import { HsmManager } from '../utils/hsm.js';
16
- import { checkForUpdate, formatUpdateMessage, logUpdateCheck } from '../utils/update-checker.js';
16
+ import { checkForUpdate, formatUpdateMessage, logUpdateCheck, fetchReleases, filterByChannel } from '../utils/update-checker.js';
17
+ import { spawnSync } from 'child_process';
17
18
  import { LockoutManager } from '../utils/lockout.js';
18
19
  import { initMemoryProtection } from '../utils/secure-memory.js';
19
- import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
20
+ import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, resolveSessionPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
21
+ import { findProjectRoot } from '../utils/fs.js';
20
22
  initMemoryProtection();
21
- async function withSession(fn, vaultOverride) {
23
+ async function resolveCliContext(vaultOverride) {
24
+ if (vaultOverride === 'global') {
25
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
26
+ const config = await loadConfig(home);
27
+ config.vault = { ...config.vault, mode: 'global' };
28
+ return { projectPath: home, config };
29
+ }
22
30
  const projectPath = process.cwd();
23
31
  const config = await loadConfig(projectPath);
32
+ if (vaultOverride === 'project') {
33
+ config.vault = { ...config.vault, mode: 'project' };
34
+ }
35
+ return { projectPath, config };
36
+ }
37
+ function buildSecuritySettings(securityChoice) {
38
+ const bruteForceProtection = {
39
+ enabled: true,
40
+ max_attempts: 5,
41
+ lockout_duration: 300,
42
+ progressive_delay: true,
43
+ max_delay: 60,
44
+ permanent_lockout_threshold: 50,
45
+ permanent_lockout_action: 'require_recovery_key',
46
+ notifications: {},
47
+ };
48
+ if (securityChoice === 'none') {
49
+ return {
50
+ encryption: { enabled: false },
51
+ storageEncrypted: false,
52
+ security: {
53
+ mode: 'recoverable',
54
+ recovery_file: '.envcp/.recovery',
55
+ brute_force_protection: bruteForceProtection,
56
+ },
57
+ };
58
+ }
59
+ return {
60
+ encryption: { enabled: true },
61
+ storageEncrypted: true,
62
+ security: {
63
+ mode: securityChoice,
64
+ recovery_file: '.envcp/.recovery',
65
+ brute_force_protection: bruteForceProtection,
66
+ },
67
+ };
68
+ }
69
+ async function withSession(fn, vaultOverride) {
70
+ const { projectPath, config } = await resolveCliContext(vaultOverride);
24
71
  const vaultPath = vaultOverride
25
72
  ? vaultOverride === 'global'
26
73
  ? getGlobalVaultPath(config)
@@ -28,15 +75,15 @@ async function withSession(fn, vaultOverride) {
28
75
  : await resolveVaultPath(projectPath, config);
29
76
  if (config.encryption?.enabled === false) {
30
77
  const storage = new StorageManager(vaultPath, false);
31
- const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
78
+ const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
32
79
  await logManager.init();
33
80
  await fn(storage, '', config, projectPath, logManager);
34
81
  return;
35
82
  }
36
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
83
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
37
84
  await sessionManager.init();
38
85
  // Initialize audit logging
39
- const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
86
+ const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
40
87
  await logManager.init();
41
88
  let session = await sessionManager.load();
42
89
  let password = '';
@@ -136,12 +183,32 @@ program
136
183
  .option('--hsm-type <type>', 'HSM type for --auth-method hsm|multi: yubikey | gpg | pkcs11')
137
184
  .option('--key-id <id>', 'GPG key ID or PKCS#11 key label for HSM auth')
138
185
  .option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library for --hsm-type pkcs11')
186
+ .option('--global', 'Initialize a global vault at ~/.envcp/ (config + store live in $HOME)')
187
+ .option('--force', 'Overwrite existing envcp.yaml (DESTRUCTIVE: existing vault may become inaccessible)')
139
188
  .action(async (options) => {
140
- const projectPath = process.cwd();
141
- const projectName = options.project || path.basename(projectPath);
142
- console.log(chalk.blue('Initializing EnvCP...'));
189
+ const useGlobal = !!options.global;
190
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
191
+ const projectPath = useGlobal ? home : process.cwd();
192
+ const projectName = options.project || (useGlobal ? 'global' : path.basename(projectPath));
193
+ const configPath = path.join(projectPath, 'envcp.yaml');
194
+ if (await pathExists(configPath) && !options.force) {
195
+ const vaultRelPath = path.join('.envcp', 'store.enc');
196
+ console.log(chalk.yellow('EnvCP is already initialized here.'));
197
+ console.log(chalk.gray(` Config: ${configPath}`));
198
+ if (await pathExists(path.join(projectPath, vaultRelPath))) {
199
+ console.log(chalk.gray(` Vault: ${path.join(projectPath, vaultRelPath)}`));
200
+ }
201
+ console.log('');
202
+ console.log(chalk.gray('Running init again would overwrite your config and may make'));
203
+ console.log(chalk.gray('existing encrypted variables permanently inaccessible.'));
204
+ console.log('');
205
+ console.log(chalk.gray('To reconfigure: edit envcp.yaml directly'));
206
+ console.log(chalk.gray('To force re-init (DESTRUCTIVE): envcp init --force'));
207
+ process.exit(1);
208
+ }
209
+ console.log(chalk.blue(useGlobal ? 'Initializing EnvCP (global vault)...' : 'Initializing EnvCP...'));
143
210
  console.log('');
144
- const config = await initConfig(projectPath, projectName);
211
+ const config = await initConfig(projectPath, projectName, { global: useGlobal });
145
212
  // Single security question (or skip if --no-encrypt)
146
213
  let securityChoice;
147
214
  if (options.encrypt === false) {
@@ -154,43 +221,10 @@ program
154
221
  { name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
155
222
  ], 'recoverable');
156
223
  }
157
- // Apply security choice to config
158
- if (securityChoice === 'none') {
159
- config.encryption = { enabled: false };
160
- config.storage.encrypted = false;
161
- config.security = {
162
- mode: 'recoverable',
163
- recovery_file: '.envcp/.recovery',
164
- brute_force_protection: {
165
- enabled: true,
166
- max_attempts: 5,
167
- lockout_duration: 300,
168
- progressive_delay: true,
169
- max_delay: 60,
170
- permanent_lockout_threshold: 50,
171
- permanent_lockout_action: 'require_recovery_key',
172
- notifications: {}
173
- }
174
- };
175
- }
176
- else {
177
- config.encryption = { enabled: true };
178
- config.storage.encrypted = true;
179
- config.security = {
180
- mode: securityChoice,
181
- recovery_file: '.envcp/.recovery',
182
- brute_force_protection: {
183
- enabled: true,
184
- max_attempts: 5,
185
- lockout_duration: 300,
186
- progressive_delay: true,
187
- max_delay: 60,
188
- permanent_lockout_threshold: 50,
189
- permanent_lockout_action: 'require_recovery_key',
190
- notifications: {},
191
- }
192
- };
193
- }
224
+ const securitySettings = buildSecuritySettings(securityChoice);
225
+ config.encryption = securitySettings.encryption;
226
+ config.storage.encrypted = securitySettings.storageEncrypted;
227
+ config.security = securitySettings.security;
194
228
  // For encrypted modes: get password now
195
229
  let pwd = '';
196
230
  if (securityChoice !== 'none') {
@@ -203,7 +237,7 @@ program
203
237
  }
204
238
  pwd = password;
205
239
  }
206
- await saveConfig(config, projectPath);
240
+ await saveConfig(config, projectPath, { global: useGlobal });
207
241
  const modeLabel = securityChoice === 'none' ? 'no encryption' : securityChoice;
208
242
  console.log(chalk.green('EnvCP initialized!'));
209
243
  console.log(chalk.gray(` Project: ${config.project}`));
@@ -236,7 +270,7 @@ program
236
270
  await storage.save(existing);
237
271
  // Create session for encrypted mode
238
272
  if (pwd) {
239
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
273
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
240
274
  await sessionManager.init();
241
275
  await sessionManager.create(pwd);
242
276
  }
@@ -303,7 +337,7 @@ program
303
337
  if (await hsm.isAvailable()) {
304
338
  try {
305
339
  await hsm.protectVaultPassword(pwd);
306
- await saveConfig(config, projectPath);
340
+ await saveConfig(config, projectPath, { global: useGlobal });
307
341
  console.log('');
308
342
  console.log(chalk.green(` Vault password protected by ${hsm.backendName}`));
309
343
  console.log(chalk.gray(` Future sessions will authenticate via hardware`));
@@ -317,7 +351,7 @@ program
317
351
  console.log(chalk.yellow(` HSM device (${hsm.backendName}) not available at init time.`));
318
352
  console.log(chalk.gray(' Run "envcp unlock --setup-hsm" later to enable hardware authentication.'));
319
353
  config.auth = { method: authMethod, multi_factors: ['password', 'hsm'], fallback: 'password' };
320
- await saveConfig(config, projectPath);
354
+ await saveConfig(config, projectPath, { global: useGlobal });
321
355
  }
322
356
  }
323
357
  console.log('');
@@ -326,20 +360,16 @@ program
326
360
  program
327
361
  .command('unlock')
328
362
  .description('Unlock EnvCP session with password')
329
- .option('-p, --password <password>', 'Password (will prompt if not provided)')
330
363
  .option('--recovery-key <key>', 'Recovery key to clear permanent lockout')
331
364
  .option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
332
365
  .option('--setup-hsm', 'Protect vault password with hardware security module')
333
366
  .option('--hsm-type <type>', 'HSM type: yubikey | gpg | pkcs11 (default: yubikey)')
334
367
  .option('--key-id <id>', 'GPG key ID or PKCS#11 key label')
335
368
  .option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library (.so / .dll)')
369
+ .option('--global', 'Unlock the global vault at ~/.envcp')
336
370
  .action(async (options) => {
337
- const projectPath = process.cwd();
338
- const config = await loadConfig(projectPath);
339
- let password = options.password;
340
- if (!password) {
341
- password = await promptPassword('Enter password:');
342
- }
371
+ const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
372
+ const password = await promptPassword('Enter password:');
343
373
  const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
344
374
  if (!passwordValid) {
345
375
  // nosem: no tainted data flows to log
@@ -349,7 +379,7 @@ program
349
379
  if (passwordValid && passwordWarning) {
350
380
  console.log(chalk.yellow('⚠ Weak password detected'));
351
381
  }
352
- const sessionDir = path.join(projectPath, path.dirname(config.session?.path || '.envcp/.session'));
382
+ const sessionDir = path.dirname(resolveSessionPath(projectPath, config));
353
383
  let lockoutManager = new LockoutManager(path.join(sessionDir, '.lockout'));
354
384
  // Handle recovery key if provided
355
385
  if (options.recoveryKey) {
@@ -365,7 +395,7 @@ program
365
395
  await lockoutManager.clearPermanentLockout();
366
396
  console.log(chalk.green('Permanent lockout cleared.'));
367
397
  // Log recovery event
368
- const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
398
+ const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
369
399
  await logManager.init();
370
400
  await logManager.log({
371
401
  timestamp: new Date().toISOString(),
@@ -394,12 +424,13 @@ program
394
424
  const progressiveDelay = bfpConfig?.progressive_delay ?? true;
395
425
  const maxDelay = bfpConfig?.max_delay ?? 60;
396
426
  const permanentThreshold = bfpConfig?.permanent_lockout_threshold ?? 0;
397
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
427
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
398
428
  await sessionManager.init();
399
429
  // Initialize audit logging
400
- const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
430
+ const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
401
431
  await logManager.init();
402
- const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
432
+ const vaultPathForUnlock = await resolveVaultPath(projectPath, config);
433
+ const storage = new StorageManager(vaultPathForUnlock, config.storage.encrypted);
403
434
  storage.setPassword(password);
404
435
  const storeExists = await storage.exists();
405
436
  // Check lockout before attempting password verification on existing stores
@@ -618,10 +649,10 @@ program
618
649
  program
619
650
  .command('lock')
620
651
  .description('Lock EnvCP session')
621
- .action(async () => {
622
- const projectPath = process.cwd();
623
- const config = await loadConfig(projectPath);
624
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
652
+ .option('--global', 'Lock the global vault session at ~/.envcp/.session')
653
+ .action(async (options) => {
654
+ const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
655
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
625
656
  await sessionManager.init();
626
657
  await sessionManager.destroy();
627
658
  console.log(chalk.green('Session locked'));
@@ -629,10 +660,10 @@ program
629
660
  program
630
661
  .command('status')
631
662
  .description('Check session status')
632
- .action(async () => {
633
- const projectPath = process.cwd();
634
- const config = await loadConfig(projectPath);
635
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
663
+ .option('--global', 'Check status of the global vault session at ~/.envcp/.session')
664
+ .action(async (options) => {
665
+ const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
666
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
636
667
  await sessionManager.init();
637
668
  const password = await promptPassword('Enter password:');
638
669
  const session = await sessionManager.load(password);
@@ -669,10 +700,10 @@ program
669
700
  program
670
701
  .command('extend')
671
702
  .description('Extend session timeout')
672
- .action(async () => {
673
- const projectPath = process.cwd();
674
- const config = await loadConfig(projectPath);
675
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
703
+ .option('--global', 'Extend the global vault session at ~/.envcp/.session')
704
+ .action(async (options) => {
705
+ const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
706
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
676
707
  await sessionManager.init();
677
708
  const password = await promptPassword('Enter password:');
678
709
  const loaded = await sessionManager.load(password);
@@ -749,7 +780,7 @@ program
749
780
  console.log(chalk.yellow.bold(` ${newRecoveryKey}`));
750
781
  console.log(chalk.gray('Your old recovery key no longer works.'));
751
782
  // Create a session with new password
752
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
783
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
753
784
  await sessionManager.init();
754
785
  await sessionManager.create(newPassword);
755
786
  console.log(chalk.green('Session unlocked with new password.'));
@@ -786,15 +817,70 @@ program
786
817
  program
787
818
  .command('add <name>')
788
819
  .description('Add a new environment variable')
789
- .option('-v, --value <value>', 'Variable value')
820
+ .option('-v, --value <value>', 'Variable value (WARNING: leaks in shell history; prefer --from-env/--from-file/--stdin)')
821
+ .option('--from-env <envVar>', 'Read value from the named environment variable')
822
+ .option('--from-file <path>', 'Read value from a file (trailing newline trimmed)')
823
+ .option('--stdin', 'Read value from piped stdin')
790
824
  .option('-t, --tags <tags>', 'Tags (comma-separated)')
791
825
  .option('-d, --description <desc>', 'Description')
792
826
  .action(async (name, options) => {
827
+ const sourceFlags = [
828
+ options.value !== undefined ? '--value' : null,
829
+ options.fromEnv !== undefined ? '--from-env' : null,
830
+ options.fromFile !== undefined ? '--from-file' : null,
831
+ options.stdin ? '--stdin' : null,
832
+ ].filter(Boolean);
833
+ if (sourceFlags.length > 1) {
834
+ console.error(chalk.red(`Error: ${sourceFlags.join(', ')} are mutually exclusive — pick one`));
835
+ process.exit(1);
836
+ }
837
+ let value;
838
+ let sourced = false;
839
+ if (options.value !== undefined) {
840
+ value = options.value;
841
+ sourced = true;
842
+ if (process.stdout.isTTY) {
843
+ console.warn(chalk.yellow('⚠ --value is visible in shell history and process list. Use --from-env, --from-file, or --stdin for secrets.'));
844
+ }
845
+ }
846
+ else if (options.fromEnv) {
847
+ const envValue = process.env[options.fromEnv];
848
+ if (envValue === undefined) {
849
+ console.error(chalk.red(`Error: environment variable '${options.fromEnv}' is not set`));
850
+ process.exit(1);
851
+ }
852
+ value = envValue;
853
+ sourced = true;
854
+ }
855
+ else if (options.fromFile) {
856
+ try {
857
+ const raw = await fs.readFile(options.fromFile, 'utf-8');
858
+ value = raw.replace(/\r?\n$/, '');
859
+ sourced = true;
860
+ }
861
+ catch (err) {
862
+ const msg = err instanceof Error ? err.message : String(err);
863
+ console.error(chalk.red(`Error: cannot read '${options.fromFile}': ${msg}`));
864
+ process.exit(1);
865
+ }
866
+ }
867
+ else if (options.stdin) {
868
+ if (process.stdin.isTTY) {
869
+ console.error(chalk.red('Error: --stdin requires piped input (e.g., `echo "$SECRET" | envcp add NAME --stdin`)'));
870
+ process.exit(1);
871
+ }
872
+ value = await new Promise((resolve, reject) => {
873
+ const chunks = [];
874
+ process.stdin.on('data', (c) => chunks.push(typeof c === 'string' ? Buffer.from(c) : c));
875
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').replace(/\r?\n$/, '')));
876
+ process.stdin.on('error', reject);
877
+ });
878
+ sourced = true;
879
+ }
793
880
  await withSession(async (storage, _password, config) => {
794
- let value = options.value;
795
881
  let tags = [];
796
882
  let description = options.description;
797
- if (!value) {
883
+ if (!sourced) {
798
884
  value = await promptPassword('Enter value:');
799
885
  const tagsInput = await promptInput('Tags (comma-separated):');
800
886
  tags = tagsInput.split(',').map((t) => t.trim()).filter(Boolean);
@@ -803,6 +889,10 @@ program
803
889
  else if (options.tags) {
804
890
  tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
805
891
  }
892
+ if (value === undefined) {
893
+ console.error(chalk.red('Error: no value provided'));
894
+ process.exit(1);
895
+ }
806
896
  const now = new Date().toISOString();
807
897
  const variable = {
808
898
  name,
@@ -909,13 +999,13 @@ program
909
999
  continue;
910
1000
  const excluded = config.sync.exclude?.some((pattern) => {
911
1001
  // eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
912
- const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
1002
+ const regex = new RegExp('^' + pattern.replaceAll(/[.+?^${}()|[\]\\]/g, '\\$&').replaceAll('*', '.*') + '$');
913
1003
  return regex.test(name);
914
1004
  });
915
1005
  if (excluded)
916
1006
  continue;
917
1007
  const needsQuoting = /[\s#"'\\]/.test(variable.value);
918
- const val = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
1008
+ const val = needsQuoting ? `"${variable.value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"` : variable.value;
919
1009
  lines.push(`${name}=${val}`);
920
1010
  }
921
1011
  if (options.dryRun) {
@@ -935,7 +1025,7 @@ program
935
1025
  continue;
936
1026
  const excluded = config.sync.exclude?.some((pattern) => {
937
1027
  // eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
938
- const regex = new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
1028
+ const regex = new RegExp('^' + pattern.replaceAll(/[.+?^${}()|[\]\\]/g, '\\$&').replaceAll('*', '.*') + '$');
939
1029
  return regex.test(name);
940
1030
  });
941
1031
  if (excluded)
@@ -979,38 +1069,67 @@ program
979
1069
  program
980
1070
  .command('serve')
981
1071
  .description('Start EnvCP server')
982
- .option('-p, --password <password>', 'Encryption password')
983
1072
  .option('-m, --mode <mode>', 'Server mode: mcp, rest, openai, gemini, all, auto', 'auto')
984
1073
  .option('--port <port>', 'HTTP port (for non-MCP modes)', '3456')
985
1074
  .option('--host <host>', 'HTTP host', '127.0.0.1')
986
1075
  .option('-k, --api-key <key>', 'API key for HTTP authentication')
1076
+ .option('--global', 'Force the global vault at ~/.envcp (skip project lookup)')
987
1077
  .action(async (options) => {
988
- const projectPath = process.cwd();
1078
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
1079
+ const globalConfigPath = path.join(home, '.envcp', 'config.yaml');
1080
+ // Resolve where the vault lives: --global forces home; otherwise walk up
1081
+ // from cwd looking for envcp.yaml; otherwise fall back to a global install.
1082
+ let projectPath;
1083
+ let forceGlobalMode = false;
1084
+ if (options.global) {
1085
+ projectPath = home;
1086
+ forceGlobalMode = true;
1087
+ }
1088
+ else {
1089
+ const found = await findProjectRoot(process.cwd());
1090
+ if (found) {
1091
+ projectPath = found;
1092
+ }
1093
+ else if (await pathExists(globalConfigPath)) {
1094
+ projectPath = home;
1095
+ forceGlobalMode = true;
1096
+ }
1097
+ else {
1098
+ process.stderr.write('Error: No envcp.yaml found in cwd or any ancestor, and no global config at ~/.envcp/config.yaml.\n' +
1099
+ 'Run `envcp init` (project) or `envcp init --global` first.\n');
1100
+ process.exit(1);
1101
+ }
1102
+ }
989
1103
  const configGuard = new ConfigGuard(projectPath);
990
1104
  const config = await configGuard.loadAndLock();
1105
+ if (forceGlobalMode) {
1106
+ config.vault = { ...config.vault, mode: 'global' };
1107
+ }
1108
+ const vaultPath = await resolveVaultPath(projectPath, config);
1109
+ const sessionPath = resolveSessionPath(projectPath, config);
991
1110
  const mode = options.mode;
992
1111
  const port = parseInt(options.port, 10);
993
1112
  const host = options.host;
994
1113
  const apiKey = options.apiKey;
995
- let password = options.password || '';
1114
+ let password = '';
996
1115
  // Passwordless mode: skip all session/password logic
997
1116
  if (config.encryption?.enabled === false) {
998
1117
  if (mode === 'mcp') {
999
1118
  const { EnvCPServer } = await import('../mcp/server.js');
1000
- const server = new EnvCPServer(config, projectPath);
1119
+ const server = new EnvCPServer(config, projectPath, undefined, vaultPath, sessionPath);
1001
1120
  await server.start();
1002
1121
  return;
1003
1122
  }
1004
1123
  }
1005
1124
  else {
1006
1125
  // Encrypted mode: need password
1007
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
1126
+ const sessionManager = new SessionManager(sessionPath, config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
1008
1127
  await sessionManager.init();
1009
1128
  let session = await sessionManager.load();
1010
1129
  if (!session && !password) {
1011
1130
  // MCP mode uses stdio — can't prompt interactively
1012
1131
  if (mode === 'mcp') {
1013
- process.stderr.write('Error: No active session. Run `envcp unlock` first, or use --password flag.\n');
1132
+ process.stderr.write(`Error: No active session at ${sessionPath}. Run \`envcp unlock${forceGlobalMode ? ' --global' : ''}\` first.\n`);
1014
1133
  process.exit(1);
1015
1134
  }
1016
1135
  password = await promptPassword('Enter password:');
@@ -1029,7 +1148,7 @@ program
1029
1148
  // MCP mode uses stdio
1030
1149
  if (mode === 'mcp') {
1031
1150
  const { EnvCPServer } = await import('../mcp/server.js');
1032
- const server = new EnvCPServer(config, projectPath, password);
1151
+ const server = new EnvCPServer(config, projectPath, password, vaultPath, sessionPath);
1033
1152
  await server.start();
1034
1153
  return;
1035
1154
  }
@@ -1134,7 +1253,7 @@ program
1134
1253
  default: {
1135
1254
  const lines = Object.entries(variables).map(([k, v]) => {
1136
1255
  const needsQuoting = /[\s#"'\\]/.test(v.value);
1137
- const val = needsQuoting ? `"${v.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : v.value;
1256
+ const val = needsQuoting ? `"${v.value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"` : v.value;
1138
1257
  return `${k}=${val}`;
1139
1258
  });
1140
1259
  output = lines.join('\n');
@@ -1243,7 +1362,7 @@ program
1243
1362
  console.log(chalk.yellow('No variables to backup'));
1244
1363
  return;
1245
1364
  }
1246
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1365
+ const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
1247
1366
  const defaultPath = path.join(projectPath, `.envcp/backup-${timestamp}.enc`);
1248
1367
  const outputPath = options.output || defaultPath;
1249
1368
  const backupData = JSON.stringify({
@@ -1340,7 +1459,7 @@ program
1340
1459
  }
1341
1460
  // 5. Session status
1342
1461
  if (encrypted) {
1343
- const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
1462
+ const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
1344
1463
  await sessionManager.init();
1345
1464
  const session = await sessionManager.load();
1346
1465
  if (session) {
@@ -1423,32 +1542,84 @@ program
1423
1542
  });
1424
1543
  program
1425
1544
  .command('update')
1426
- .description('Check for EnvCP updates')
1427
- .option('--check', 'Check for available updates')
1545
+ .description('Check for or install EnvCP updates')
1546
+ .option('-l, --latest', 'List and install latest stable versions')
1547
+ .option('-e, --experimental', 'List and install experimental versions')
1548
+ .option('-c, --canary', 'List and install canary versions')
1549
+ .option('--backup', 'Backup vault before installing')
1428
1550
  .action(async (options) => {
1429
1551
  const projectPath = process.cwd();
1430
- if (options.check || Object.keys(options).length === 0) {
1552
+ const home = os.homedir();
1553
+ const channel = options.latest ? 'latest' :
1554
+ options.experimental ? 'experimental' :
1555
+ options.canary ? 'canary' : null;
1556
+ if (!channel) {
1557
+ // Original behaviour: just check and notify
1431
1558
  console.log(chalk.blue('Checking for updates...'));
1432
1559
  try {
1433
1560
  const info = await checkForUpdate(projectPath);
1434
1561
  const message = formatUpdateMessage(info);
1435
1562
  await logUpdateCheck(projectPath, info);
1436
1563
  if (info.updateAvailable) {
1437
- if (info.critical) {
1438
- console.log(chalk.red.bold(message));
1439
- }
1440
- else {
1441
- console.log(chalk.yellow(message));
1442
- }
1564
+ console.log(info.critical ? chalk.red.bold(message) : chalk.yellow(message));
1443
1565
  }
1444
1566
  else {
1445
1567
  console.log(chalk.green(message));
1446
1568
  }
1447
1569
  }
1448
- catch (error) {
1570
+ catch {
1449
1571
  console.log(chalk.yellow('Could not check for updates (offline or rate-limited)'));
1450
- console.log(chalk.gray(` Current version: v${require('../../package.json').version}`));
1451
1572
  }
1573
+ return;
1574
+ }
1575
+ console.log(chalk.blue(`Fetching ${channel} releases...`));
1576
+ let releases;
1577
+ try {
1578
+ const all = await fetchReleases();
1579
+ releases = filterByChannel(all, channel).slice(0, 3);
1580
+ }
1581
+ catch {
1582
+ console.log(chalk.red('Could not fetch releases (offline or rate-limited)'));
1583
+ return;
1584
+ }
1585
+ if (releases.length === 0) {
1586
+ console.log(chalk.yellow(`No ${channel} releases found.`));
1587
+ return;
1588
+ }
1589
+ console.log(chalk.bold(`\n Available ${channel} versions:\n`));
1590
+ releases.forEach((r, i) => {
1591
+ console.log(chalk.cyan(` [${i + 1}] v${r.tag}`));
1592
+ });
1593
+ console.log(chalk.gray('\n [0] Cancel\n'));
1594
+ const answer = await promptInput('Pick a version (0 to cancel):');
1595
+ const picked = parseInt(answer.trim(), 10);
1596
+ if (!picked || picked < 1 || picked > releases.length) {
1597
+ console.log(chalk.gray('Cancelled.'));
1598
+ return;
1599
+ }
1600
+ const chosen = releases[picked - 1];
1601
+ if (options.backup) {
1602
+ const backupPath = path.join(home, `.envcp.bak.${Date.now()}`);
1603
+ const vaultDir = path.join(home, '.envcp');
1604
+ const confirmed = await promptConfirm(`Backup ~/.envcp → ${backupPath}?`);
1605
+ if (confirmed) {
1606
+ try {
1607
+ await fs.cp(vaultDir, backupPath, { recursive: true });
1608
+ console.log(chalk.green(` ✓ Backed up to ${backupPath}`));
1609
+ }
1610
+ catch (e) {
1611
+ console.log(chalk.red(` Backup failed: ${e.message}`));
1612
+ return;
1613
+ }
1614
+ }
1615
+ }
1616
+ console.log(chalk.blue(`\nInstalling @fentz26/envcp@${chosen.tag}...`));
1617
+ const result = spawnSync('npm', ['install', '-g', `@fentz26/envcp@${chosen.tag}`], { stdio: 'inherit' });
1618
+ if (result.status === 0) {
1619
+ console.log(chalk.green(`\n ✓ Installed v${chosen.tag}`));
1620
+ }
1621
+ else {
1622
+ console.log(chalk.red('\n Installation failed.'));
1452
1623
  }
1453
1624
  });
1454
1625
  program
@@ -1727,7 +1898,7 @@ program
1727
1898
  .action(async (options) => {
1728
1899
  const projectPath = process.cwd();
1729
1900
  const config = await loadConfig(projectPath);
1730
- const logDir = path.join(projectPath, '.envcp', 'logs');
1901
+ const logDir = resolveLogPath(config.audit, projectPath);
1731
1902
  const logs = new LogManager(logDir, config.audit);
1732
1903
  await logs.init();
1733
1904
  if (options.dates) {
@@ -1794,7 +1965,7 @@ program
1794
1965
  .action(async (options) => {
1795
1966
  const projectPath = process.cwd();
1796
1967
  const config = await loadConfig(projectPath);
1797
- const logDir = path.join(projectPath, '.envcp', 'logs');
1968
+ const logDir = resolveLogPath(config.audit, projectPath);
1798
1969
  const logs = new LogManager(logDir, config.audit);
1799
1970
  await logs.init();
1800
1971
  if (!config.audit.hmac_chain) {
@@ -1817,7 +1988,7 @@ program
1817
1988
  .action(async () => {
1818
1989
  const projectPath = process.cwd();
1819
1990
  const config = await loadConfig(projectPath);
1820
- const logDir = path.join(projectPath, '.envcp', 'logs');
1991
+ const logDir = resolveLogPath(config.audit, projectPath);
1821
1992
  const logs = new LogManager(logDir, config.audit);
1822
1993
  await logs.init();
1823
1994
  if (config.audit.protection === 'none') {