@fentz26/envcp 1.1.0 → 1.2.0-exp.1

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 (105) hide show
  1. package/README.md +59 -422
  2. package/VERSION +1 -0
  3. package/dist/adapters/base.d.ts +13 -0
  4. package/dist/adapters/base.d.ts.map +1 -1
  5. package/dist/adapters/base.js +153 -9
  6. package/dist/adapters/base.js.map +1 -1
  7. package/dist/adapters/gemini.d.ts.map +1 -1
  8. package/dist/adapters/gemini.js +7 -4
  9. package/dist/adapters/gemini.js.map +1 -1
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +9 -6
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/adapters/rest.d.ts +2 -0
  14. package/dist/adapters/rest.d.ts.map +1 -1
  15. package/dist/adapters/rest.js +72 -4
  16. package/dist/adapters/rest.js.map +1 -1
  17. package/dist/cli/index.js +519 -103
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/config/config-guard.d.ts.map +1 -1
  20. package/dist/config/config-guard.js +3 -2
  21. package/dist/config/config-guard.js.map +1 -1
  22. package/dist/config/config-hmac.d.ts +5 -0
  23. package/dist/config/config-hmac.d.ts.map +1 -0
  24. package/dist/config/config-hmac.js +29 -0
  25. package/dist/config/config-hmac.js.map +1 -0
  26. package/dist/config/manager.d.ts +14 -0
  27. package/dist/config/manager.d.ts.map +1 -1
  28. package/dist/config/manager.js +72 -13
  29. package/dist/config/manager.js.map +1 -1
  30. package/dist/mcp/server.d.ts.map +1 -1
  31. package/dist/mcp/server.js +2 -1
  32. package/dist/mcp/server.js.map +1 -1
  33. package/dist/server/unified.d.ts +4 -1
  34. package/dist/server/unified.d.ts.map +1 -1
  35. package/dist/server/unified.js +102 -33
  36. package/dist/server/unified.js.map +1 -1
  37. package/dist/service/config.d.ts +21 -0
  38. package/dist/service/config.d.ts.map +1 -0
  39. package/dist/service/config.js +94 -0
  40. package/dist/service/config.js.map +1 -0
  41. package/dist/service/generators.d.ts +14 -0
  42. package/dist/service/generators.d.ts.map +1 -0
  43. package/dist/service/generators.js +92 -0
  44. package/dist/service/generators.js.map +1 -0
  45. package/dist/service/index.d.ts +23 -0
  46. package/dist/service/index.d.ts.map +1 -0
  47. package/dist/service/index.js +215 -0
  48. package/dist/service/index.js.map +1 -0
  49. package/dist/service/platform.d.ts +5 -0
  50. package/dist/service/platform.d.ts.map +1 -0
  51. package/dist/service/platform.js +25 -0
  52. package/dist/service/platform.js.map +1 -0
  53. package/dist/storage/index.d.ts +39 -10
  54. package/dist/storage/index.d.ts.map +1 -1
  55. package/dist/storage/index.js +318 -31
  56. package/dist/storage/index.js.map +1 -1
  57. package/dist/types.d.ts +545 -14
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js +74 -2
  60. package/dist/types.js.map +1 -1
  61. package/dist/utils/crypto.d.ts +9 -0
  62. package/dist/utils/crypto.d.ts.map +1 -1
  63. package/dist/utils/crypto.js +88 -19
  64. package/dist/utils/crypto.js.map +1 -1
  65. package/dist/utils/hsm.d.ts +82 -0
  66. package/dist/utils/hsm.d.ts.map +1 -0
  67. package/dist/utils/hsm.js +344 -0
  68. package/dist/utils/hsm.js.map +1 -0
  69. package/dist/utils/http.d.ts.map +1 -1
  70. package/dist/utils/http.js +14 -4
  71. package/dist/utils/http.js.map +1 -1
  72. package/dist/utils/keychain.d.ts.map +1 -1
  73. package/dist/utils/keychain.js +0 -8
  74. package/dist/utils/keychain.js.map +1 -1
  75. package/dist/utils/lock.d.ts +1 -1
  76. package/dist/utils/lock.d.ts.map +1 -1
  77. package/dist/utils/lock.js +4 -2
  78. package/dist/utils/lock.js.map +1 -1
  79. package/dist/utils/lockout.d.ts +56 -0
  80. package/dist/utils/lockout.d.ts.map +1 -0
  81. package/dist/utils/lockout.js +209 -0
  82. package/dist/utils/lockout.js.map +1 -0
  83. package/dist/utils/prompt.d.ts +13 -0
  84. package/dist/utils/prompt.d.ts.map +1 -0
  85. package/dist/utils/prompt.js +124 -0
  86. package/dist/utils/prompt.js.map +1 -0
  87. package/dist/utils/secure-memory.d.ts +20 -0
  88. package/dist/utils/secure-memory.d.ts.map +1 -0
  89. package/dist/utils/secure-memory.js +122 -0
  90. package/dist/utils/secure-memory.js.map +1 -0
  91. package/dist/utils/session.d.ts.map +1 -1
  92. package/dist/utils/session.js +14 -4
  93. package/dist/utils/session.js.map +1 -1
  94. package/dist/utils/update-checker.d.ts +1 -1
  95. package/dist/utils/update-checker.d.ts.map +1 -1
  96. package/dist/utils/update-checker.js +7 -5
  97. package/dist/utils/update-checker.js.map +1 -1
  98. package/dist/vault/index.d.ts.map +1 -1
  99. package/dist/version.d.ts +2 -0
  100. package/dist/version.d.ts.map +1 -0
  101. package/dist/version.js +8 -0
  102. package/dist/version.js.map +1 -0
  103. package/package.json +18 -9
  104. package/scripts/sync-version.js +88 -0
  105. package/scripts/install.sh +0 -54
package/dist/cli/index.js CHANGED
@@ -1,18 +1,23 @@
1
1
  import { Command } from 'commander';
2
- import inquirer from 'inquirer';
3
2
  import chalk from 'chalk';
4
3
  import * as path from 'path';
5
4
  import * as os from 'os';
6
5
  import * as fs from 'fs/promises';
6
+ import { promptPassword, promptInput, promptConfirm, promptList } from '../utils/prompt.js';
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 } from '../storage/index.js';
10
+ import { StorageManager, LogManager } from '../storage/index.js';
11
+ import { VERSION } from '../version.js';
11
12
  import { SessionManager } from '../utils/session.js';
12
13
  import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
13
14
  import { KeychainManager } from '../utils/keychain.js';
15
+ import { HsmManager } from '../utils/hsm.js';
14
16
  import { checkForUpdate, formatUpdateMessage, logUpdateCheck } from '../utils/update-checker.js';
17
+ import { LockoutManager } from '../utils/lockout.js';
18
+ import { initMemoryProtection } from '../utils/secure-memory.js';
15
19
  import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
20
+ initMemoryProtection();
16
21
  async function withSession(fn, vaultOverride) {
17
22
  const projectPath = process.cwd();
18
23
  const config = await loadConfig(projectPath);
@@ -23,27 +28,81 @@ async function withSession(fn, vaultOverride) {
23
28
  : await resolveVaultPath(projectPath, config);
24
29
  if (config.encryption?.enabled === false) {
25
30
  const storage = new StorageManager(vaultPath, false);
26
- await fn(storage, '', config, projectPath);
31
+ const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
32
+ await logManager.init();
33
+ await fn(storage, '', config, projectPath, logManager);
27
34
  return;
28
35
  }
29
36
  const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
30
37
  await sessionManager.init();
38
+ // Initialize audit logging
39
+ const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
40
+ await logManager.init();
31
41
  let session = await sessionManager.load();
32
42
  let password = '';
33
43
  if (!session) {
34
- if (config.keychain?.enabled) {
35
- const keychain = new KeychainManager(config.keychain.service || 'envcp');
44
+ const authMethod = config.auth?.method ?? 'password';
45
+ // --- HSM-only or multi-factor auth ---
46
+ if (authMethod === 'hsm' || authMethod === 'multi') {
47
+ const hsm = HsmManager.fromConfig(config, projectPath);
48
+ const hsmAvailable = await hsm.isAvailable();
49
+ if (hsmAvailable) {
50
+ try {
51
+ const hsmSecret = await hsm.retrieveVaultPassword();
52
+ if (authMethod === 'multi') {
53
+ const factors = config.auth?.multi_factors ?? ['password', 'hsm'];
54
+ let userPassword = '';
55
+ if (factors.includes('password')) {
56
+ userPassword = await promptPassword('Enter password (multi-factor):');
57
+ }
58
+ else if (factors.includes('keychain')) {
59
+ const keychain = new KeychainManager(config.keychain?.service || 'envcp');
60
+ const stored = await keychain.retrievePassword(projectPath);
61
+ if (stored) {
62
+ userPassword = stored;
63
+ console.log(chalk.gray('Password retrieved from OS keychain (multi-factor)'));
64
+ }
65
+ }
66
+ password = HsmManager.combineSecrets(hsmSecret, userPassword);
67
+ console.log(chalk.gray(`Authenticated via ${hsm.backendName} + password`));
68
+ }
69
+ else {
70
+ password = hsmSecret;
71
+ console.log(chalk.gray(`Authenticated via ${hsm.backendName}`));
72
+ }
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ const fallback = config.auth?.fallback ?? 'password';
77
+ if (fallback === 'none') {
78
+ console.log(chalk.red(`HSM authentication failed: ${msg}`));
79
+ return;
80
+ }
81
+ console.log(chalk.yellow(`HSM unavailable: ${msg}`));
82
+ console.log(chalk.gray('Falling back to password...'));
83
+ }
84
+ }
85
+ else {
86
+ const fallback = config.auth?.fallback ?? 'password';
87
+ if (fallback === 'none') {
88
+ console.log(chalk.red(`HSM device (${hsm.backendName}) not found. No fallback configured.`));
89
+ return;
90
+ }
91
+ console.log(chalk.yellow(`HSM device (${hsm.backendName}) not available. Falling back to password...`));
92
+ }
93
+ }
94
+ // --- OS keychain ---
95
+ if (!password && (authMethod === 'keychain' || config.keychain?.enabled)) {
96
+ const keychain = new KeychainManager(config.keychain?.service || 'envcp');
36
97
  const stored = await keychain.retrievePassword(projectPath);
37
98
  if (stored) {
38
99
  password = stored;
39
100
  console.log(chalk.gray('Password retrieved from OS keychain'));
40
101
  }
41
102
  }
103
+ // --- Password prompt fallback ---
42
104
  if (!password) {
43
- const answer = await inquirer.prompt([
44
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
45
- ]);
46
- password = answer.password;
105
+ password = await promptPassword('Enter password:');
47
106
  const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
48
107
  if (!passwordValid) {
49
108
  console.log(chalk.red("Invalid password"));
@@ -59,13 +118,13 @@ async function withSession(fn, vaultOverride) {
59
118
  const storage = new StorageManager(vaultPath, config.storage.encrypted);
60
119
  if (password)
61
120
  storage.setPassword(password);
62
- await fn(storage, password, config, projectPath);
121
+ await fn(storage, password, config, projectPath, logManager);
63
122
  }
64
123
  const program = new Command();
65
124
  program
66
125
  .name('envcp')
67
126
  .description('Secure environment variable management for AI-assisted coding')
68
- .version('1.0.0');
127
+ .version(VERSION);
69
128
  program
70
129
  .command('init')
71
130
  .description('Initialize EnvCP in the current project')
@@ -73,6 +132,10 @@ program
73
132
  .option('--no-encrypt', 'Skip encryption (passwordless mode)')
74
133
  .option('--skip-env', 'Skip .env auto-import')
75
134
  .option('--skip-mcp', 'Skip MCP auto-registration')
135
+ .option('--auth-method <method>', 'Authentication method: password | keychain | hsm | multi (default: password)')
136
+ .option('--hsm-type <type>', 'HSM type for --auth-method hsm|multi: yubikey | gpg | pkcs11')
137
+ .option('--key-id <id>', 'GPG key ID or PKCS#11 key label for HSM auth')
138
+ .option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library for --hsm-type pkcs11')
76
139
  .action(async (options) => {
77
140
  const projectPath = process.cwd();
78
141
  const projectName = options.project || path.basename(projectPath);
@@ -85,42 +148,56 @@ program
85
148
  securityChoice = 'none';
86
149
  }
87
150
  else {
88
- const { mode } = await inquirer.prompt([
89
- {
90
- type: 'list',
91
- name: 'mode',
92
- message: 'How would you like to secure your variables?',
93
- choices: [
94
- { name: 'No encryption (fastest setup, for local dev)', value: 'none' },
95
- { name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
96
- { name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
97
- ],
98
- default: 'recoverable',
99
- }
100
- ]);
101
- securityChoice = mode;
151
+ securityChoice = await promptList('How would you like to secure your variables?', [
152
+ { name: 'No encryption (fastest setup, for local dev)', value: 'none' },
153
+ { name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
154
+ { name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
155
+ ], 'recoverable');
102
156
  }
103
157
  // Apply security choice to config
104
158
  if (securityChoice === 'none') {
105
159
  config.encryption = { enabled: false };
106
160
  config.storage.encrypted = false;
107
- config.security = { mode: 'recoverable', recovery_file: '.envcp/.recovery' };
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
+ };
108
175
  }
109
176
  else {
110
177
  config.encryption = { enabled: true };
111
178
  config.storage.encrypted = true;
112
- config.security = { mode: securityChoice, recovery_file: '.envcp/.recovery' };
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
+ };
113
193
  }
114
194
  // For encrypted modes: get password now
115
195
  let pwd = '';
116
196
  if (securityChoice !== 'none') {
117
- const { password } = await inquirer.prompt([
118
- { type: 'password', name: 'password', message: 'Set encryption password:', mask: '*' }
119
- ]);
120
- const { confirm } = await inquirer.prompt([
121
- { type: 'password', name: 'confirm', message: 'Confirm password:', mask: '*' }
122
- ]);
123
- if (password !== confirm) {
197
+ const password = await promptPassword('Set encryption password:');
198
+ const confirmPwd = await promptPassword('Confirm password:');
199
+ // eslint-disable-next-line security/detect-possible-timing-attacks -- comparing two user-typed confirm fields, not a secret-vs-known value
200
+ if (password !== confirmPwd) {
124
201
  console.log(chalk.red('Passwords do not match. Aborting.'));
125
202
  return;
126
203
  }
@@ -146,15 +223,17 @@ program
146
223
  if (pwd)
147
224
  storage.setPassword(pwd);
148
225
  const now = new Date().toISOString();
226
+ const existing = await storage.load();
149
227
  for (const [name, value] of Object.entries(vars)) {
150
- await storage.set(name, {
228
+ existing[name] = {
151
229
  name, value,
152
230
  encrypted: config.storage.encrypted,
153
231
  created: now, updated: now,
154
232
  sync_to_env: true,
155
233
  protected: false,
156
- });
234
+ };
157
235
  }
236
+ await storage.save(existing);
158
237
  // Create session for encrypted mode
159
238
  if (pwd) {
160
239
  const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
@@ -202,6 +281,45 @@ program
202
281
  console.log(chalk.gray(' No AI tools detected for auto-registration'));
203
282
  }
204
283
  }
284
+ // Setup HSM authentication if --auth-method hsm|multi provided
285
+ const authMethod = options.authMethod;
286
+ if (pwd && (authMethod === 'hsm' || authMethod === 'multi')) {
287
+ const hsmType = options.hsmType || 'yubikey';
288
+ config.hsm = {
289
+ ...config.hsm,
290
+ enabled: true,
291
+ type: hsmType,
292
+ key_id: options.keyId ?? config.hsm?.key_id,
293
+ pkcs11_lib: options.pkcs11Lib ?? config.hsm?.pkcs11_lib,
294
+ require_touch: config.hsm?.require_touch ?? true,
295
+ protected_key_path: config.hsm?.protected_key_path ?? '.envcp/.hsm-key',
296
+ };
297
+ config.auth = {
298
+ method: authMethod,
299
+ multi_factors: authMethod === 'multi' ? ['password', 'hsm'] : ['hsm'],
300
+ fallback: 'password',
301
+ };
302
+ const hsm = HsmManager.fromConfig(config, projectPath);
303
+ if (await hsm.isAvailable()) {
304
+ try {
305
+ await hsm.protectVaultPassword(pwd);
306
+ await saveConfig(config, projectPath);
307
+ console.log('');
308
+ console.log(chalk.green(` Vault password protected by ${hsm.backendName}`));
309
+ console.log(chalk.gray(` Future sessions will authenticate via hardware`));
310
+ }
311
+ catch (err) {
312
+ console.log(chalk.yellow(` HSM setup warning: ${err instanceof Error ? err.message : String(err)}`));
313
+ console.log(chalk.gray(' Falling back to password authentication'));
314
+ }
315
+ }
316
+ else {
317
+ console.log(chalk.yellow(` HSM device (${hsm.backendName}) not available at init time.`));
318
+ console.log(chalk.gray(' Run "envcp unlock --setup-hsm" later to enable hardware authentication.'));
319
+ config.auth = { method: authMethod, multi_factors: ['password', 'hsm'], fallback: 'password' };
320
+ await saveConfig(config, projectPath);
321
+ }
322
+ }
205
323
  console.log('');
206
324
  console.log(chalk.green('Done! Your AI tools can now use EnvCP.'));
207
325
  });
@@ -209,21 +327,18 @@ program
209
327
  .command('unlock')
210
328
  .description('Unlock EnvCP session with password')
211
329
  .option('-p, --password <password>', 'Password (will prompt if not provided)')
330
+ .option('--recovery-key <key>', 'Recovery key to clear permanent lockout')
212
331
  .option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
332
+ .option('--setup-hsm', 'Protect vault password with hardware security module')
333
+ .option('--hsm-type <type>', 'HSM type: yubikey | gpg | pkcs11 (default: yubikey)')
334
+ .option('--key-id <id>', 'GPG key ID or PKCS#11 key label')
335
+ .option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library (.so / .dll)')
213
336
  .action(async (options) => {
214
337
  const projectPath = process.cwd();
215
338
  const config = await loadConfig(projectPath);
216
339
  let password = options.password;
217
340
  if (!password) {
218
- const answer = await inquirer.prompt([
219
- {
220
- type: 'password',
221
- name: 'password',
222
- message: 'Enter password:',
223
- mask: '*'
224
- }
225
- ]);
226
- password = answer.password;
341
+ password = await promptPassword('Enter password:');
227
342
  }
228
343
  const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
229
344
  if (!passwordValid) {
@@ -234,16 +349,115 @@ program
234
349
  if (passwordValid && passwordWarning) {
235
350
  console.log(chalk.yellow('⚠ Weak password detected'));
236
351
  }
352
+ const sessionDir = path.join(projectPath, path.dirname(config.session?.path || '.envcp/.session'));
353
+ let lockoutManager = new LockoutManager(path.join(sessionDir, '.lockout'));
354
+ // Handle recovery key if provided
355
+ if (options.recoveryKey) {
356
+ const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
357
+ if (!await pathExists(recoveryPath)) {
358
+ console.log(chalk.red('No recovery file found.'));
359
+ return;
360
+ }
361
+ const recoveryData = await fs.readFile(recoveryPath, 'utf8');
362
+ try {
363
+ await recoverPassword(recoveryData, options.recoveryKey);
364
+ // Recovery key is valid - clear permanent lockout
365
+ await lockoutManager.clearPermanentLockout();
366
+ console.log(chalk.green('Permanent lockout cleared.'));
367
+ // Log recovery event
368
+ const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
369
+ await logManager.init();
370
+ await logManager.log({
371
+ timestamp: new Date().toISOString(),
372
+ operation: 'unlock',
373
+ variable: '',
374
+ source: 'cli',
375
+ success: true,
376
+ message: 'Permanent lockout cleared with recovery key (via --recovery-key flag)',
377
+ session_id: '',
378
+ client_id: 'cli',
379
+ client_type: 'terminal',
380
+ ip: '127.0.0.1'
381
+ });
382
+ console.log(chalk.yellow('Note: You still need to enter the correct password.'));
383
+ // Continue with password prompt
384
+ }
385
+ catch {
386
+ console.log(chalk.red('Invalid recovery key.'));
387
+ return;
388
+ }
389
+ }
390
+ // Use new brute_force_protection config if available, fall back to session config
391
+ const bfpConfig = config.security?.brute_force_protection;
392
+ const lockoutThreshold = bfpConfig?.max_attempts ?? config.session?.lockout_threshold ?? 5;
393
+ const lockoutBaseSeconds = bfpConfig?.lockout_duration ?? config.session?.lockout_base_seconds ?? 60;
394
+ const progressiveDelay = bfpConfig?.progressive_delay ?? true;
395
+ const maxDelay = bfpConfig?.max_delay ?? 60;
396
+ const permanentThreshold = bfpConfig?.permanent_lockout_threshold ?? 0;
237
397
  const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
238
398
  await sessionManager.init();
399
+ // Initialize audit logging
400
+ const logManager = new LogManager(path.join(projectPath, '.envcp', 'logs'), config.audit);
401
+ await logManager.init();
239
402
  const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
240
403
  storage.setPassword(password);
241
404
  const storeExists = await storage.exists();
405
+ // Check lockout before attempting password verification on existing stores
406
+ if (storeExists) {
407
+ const lockoutStatus = await lockoutManager.check();
408
+ if (lockoutStatus.locked) {
409
+ if (lockoutStatus.permanent_locked) {
410
+ console.log(chalk.red.bold('PERMANENT LOCKOUT: Too many failed attempts.'));
411
+ console.log(chalk.red('Recovery key or administrator intervention required.'));
412
+ const useRecovery = await promptConfirm('Use recovery key to clear lockout?', true);
413
+ if (useRecovery) {
414
+ const recoveryKey = await promptPassword('Enter recovery key:');
415
+ const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
416
+ if (!await pathExists(recoveryPath)) {
417
+ console.log(chalk.red('No recovery file found.'));
418
+ return;
419
+ }
420
+ const recoveryData = await fs.readFile(recoveryPath, 'utf8');
421
+ try {
422
+ await recoverPassword(recoveryData, recoveryKey);
423
+ // Recovery key is valid - clear permanent lockout
424
+ await lockoutManager.clearPermanentLockout();
425
+ console.log(chalk.green('Permanent lockout cleared. You can now attempt to unlock.'));
426
+ console.log(chalk.yellow('Note: You still need to enter the correct password.'));
427
+ // Log recovery event
428
+ await logManager.log({
429
+ timestamp: new Date().toISOString(),
430
+ operation: 'unlock',
431
+ variable: '',
432
+ source: 'cli',
433
+ success: true,
434
+ message: 'Permanent lockout cleared with recovery key',
435
+ session_id: '',
436
+ client_id: 'cli',
437
+ client_type: 'terminal',
438
+ ip: '127.0.0.1'
439
+ });
440
+ // Continue with password prompt
441
+ }
442
+ catch {
443
+ console.log(chalk.red('Invalid recovery key.'));
444
+ return;
445
+ }
446
+ }
447
+ else {
448
+ return;
449
+ }
450
+ }
451
+ else {
452
+ console.log(chalk.red(`Too many failed attempts. Try again in ${lockoutStatus.remaining_seconds} second(s).`));
453
+ return;
454
+ }
455
+ }
456
+ }
242
457
  if (!storeExists) {
243
- const confirm = await inquirer.prompt([
244
- { type: 'password', name: 'password', message: 'Confirm password:', mask: '*' }
245
- ]);
246
- if (confirm.password !== password) {
458
+ const confirmPasswordValue = await promptPassword('Confirm password:');
459
+ // eslint-disable-next-line security/detect-possible-timing-attacks -- comparing two user-typed confirm fields, not a secret-vs-known value
460
+ if (confirmPasswordValue !== password) {
247
461
  console.log(chalk.red('Passwords do not match'));
248
462
  return;
249
463
  }
@@ -267,10 +481,85 @@ program
267
481
  await storage.load();
268
482
  }
269
483
  catch (error) {
270
- console.log(chalk.red('Invalid password'));
484
+ if (storeExists) {
485
+ const status = await lockoutManager.recordFailure(lockoutThreshold, lockoutBaseSeconds, progressiveDelay, maxDelay, permanentThreshold);
486
+ // Log the failed attempt
487
+ await logManager.log({
488
+ timestamp: new Date().toISOString(),
489
+ operation: 'auth_failure',
490
+ variable: '',
491
+ source: 'cli',
492
+ success: false,
493
+ message: `Failed unlock attempt (attempt ${status.attempts})`,
494
+ session_id: '',
495
+ client_id: 'cli',
496
+ client_type: 'terminal',
497
+ ip: '127.0.0.1'
498
+ });
499
+ if (status.permanent_locked) {
500
+ console.log(chalk.red.bold('PERMANENT LOCKOUT TRIGGERED: Too many failed attempts.'));
501
+ console.log(chalk.red('Recovery key or administrator intervention required.'));
502
+ // Log permanent lockout event
503
+ await logManager.log({
504
+ timestamp: new Date().toISOString(),
505
+ operation: 'permanent_lockout',
506
+ variable: '',
507
+ source: 'cli',
508
+ success: false,
509
+ message: `Permanent lockout triggered after ${status.permanent_lockout_count} lockouts`,
510
+ session_id: '',
511
+ client_id: 'cli',
512
+ client_type: 'terminal',
513
+ ip: '127.0.0.1'
514
+ });
515
+ }
516
+ else if (status.locked) {
517
+ console.log(chalk.red(`Invalid password. Too many failed attempts — locked out for ${status.remaining_seconds} second(s).`));
518
+ // Log lockout event
519
+ await logManager.log({
520
+ timestamp: new Date().toISOString(),
521
+ operation: 'lockout_triggered',
522
+ variable: '',
523
+ source: 'cli',
524
+ success: false,
525
+ message: `Lockout triggered for ${status.remaining_seconds}s (lockout #${status.lockout_count})`,
526
+ session_id: '',
527
+ client_id: 'cli',
528
+ client_type: 'terminal',
529
+ ip: '127.0.0.1'
530
+ });
531
+ }
532
+ else {
533
+ const remaining = lockoutThreshold - status.attempts;
534
+ let message = `Invalid password. ${remaining} attempt(s) remaining before lockout.`;
535
+ // Show progressive delay if applied
536
+ if (status.delay_seconds && status.delay_seconds > 0) {
537
+ message += ` (Delayed ${status.delay_seconds}s)`;
538
+ }
539
+ console.log(chalk.red(message));
540
+ }
541
+ }
542
+ else {
543
+ console.log(chalk.red('Invalid password'));
544
+ }
271
545
  return;
272
546
  }
547
+ // Successful unlock — clear any lockout state
548
+ await lockoutManager.reset();
273
549
  const session = await sessionManager.create(password);
550
+ // Log successful unlock
551
+ await logManager.log({
552
+ timestamp: new Date().toISOString(),
553
+ operation: 'unlock',
554
+ variable: '',
555
+ source: 'cli',
556
+ success: true,
557
+ message: 'Session unlocked successfully',
558
+ session_id: session.id,
559
+ client_id: 'cli',
560
+ client_type: 'terminal',
561
+ ip: '127.0.0.1'
562
+ });
274
563
  console.log(chalk.green('Session unlocked!'));
275
564
  console.log(chalk.gray(` Session ID: ${session.id}`));
276
565
  console.log(chalk.gray(` Expires in: ${config.session?.timeout_minutes || 30} minutes`));
@@ -296,6 +585,35 @@ program
296
585
  console.log(chalk.red(`OS keychain not available (${keychain.backendName})`));
297
586
  }
298
587
  }
588
+ // Setup HSM if requested
589
+ if (options.setupHsm) {
590
+ const hsmType = options.hsmType || 'yubikey';
591
+ config.hsm = {
592
+ ...config.hsm,
593
+ enabled: true,
594
+ type: hsmType,
595
+ key_id: options.keyId ?? config.hsm?.key_id,
596
+ pkcs11_lib: options.pkcs11Lib ?? config.hsm?.pkcs11_lib,
597
+ require_touch: config.hsm?.require_touch ?? true,
598
+ protected_key_path: config.hsm?.protected_key_path ?? '.envcp/.hsm-key',
599
+ };
600
+ config.auth = { ...config.auth, method: 'hsm', fallback: config.auth?.fallback ?? 'password' };
601
+ const hsm = HsmManager.fromConfig(config, projectPath);
602
+ if (!await hsm.isAvailable()) {
603
+ console.log(chalk.red(`HSM device (${hsm.backendName}) not available. Aborting HSM setup.`));
604
+ return;
605
+ }
606
+ try {
607
+ await hsm.protectVaultPassword(password);
608
+ await saveConfig(config, projectPath);
609
+ console.log(chalk.green(`Vault password protected by ${hsm.backendName}`));
610
+ console.log(chalk.gray(` Key file: ${config.hsm.protected_key_path}`));
611
+ console.log(chalk.gray(' Future sessions will authenticate via hardware'));
612
+ }
613
+ catch (err) {
614
+ console.log(chalk.red(`HSM setup failed: ${err instanceof Error ? err.message : String(err)}`));
615
+ }
616
+ }
299
617
  });
300
618
  program
301
619
  .command('lock')
@@ -316,10 +634,8 @@ program
316
634
  const config = await loadConfig(projectPath);
317
635
  const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
318
636
  await sessionManager.init();
319
- const answer = await inquirer.prompt([
320
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
321
- ]);
322
- const session = await sessionManager.load(answer.password);
637
+ const password = await promptPassword('Enter password:');
638
+ const session = await sessionManager.load(password);
323
639
  if (!session) {
324
640
  console.log(chalk.yellow('No active session (expired, invalid password, or not unlocked)'));
325
641
  console.log(chalk.gray('Run: envcp unlock'));
@@ -340,9 +656,7 @@ program
340
656
  .action(async () => {
341
657
  const projectPath = process.cwd();
342
658
  const configGuard = new ConfigGuard(projectPath);
343
- const { password } = await inquirer.prompt([
344
- { type: 'password', name: 'password', message: 'Enter password to reload config:', mask: '*' }
345
- ]);
659
+ const password = await promptPassword('Enter password to reload config:');
346
660
  const result = await configGuard.reload(password);
347
661
  if (result.success) {
348
662
  console.log(chalk.green('Config reloaded successfully'));
@@ -360,10 +674,8 @@ program
360
674
  const config = await loadConfig(projectPath);
361
675
  const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
362
676
  await sessionManager.init();
363
- const answer = await inquirer.prompt([
364
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
365
- ]);
366
- const loaded = await sessionManager.load(answer.password);
677
+ const password = await promptPassword('Enter password:');
678
+ const loaded = await sessionManager.load(password);
367
679
  if (!loaded) {
368
680
  console.log(chalk.red('Cannot extend session. No active session or invalid password.'));
369
681
  return;
@@ -394,9 +706,7 @@ program
394
706
  console.log(chalk.red('No recovery file found. Recovery is not available.'));
395
707
  return;
396
708
  }
397
- const { recoveryKey } = await inquirer.prompt([
398
- { type: 'password', name: 'recoveryKey', message: 'Enter your recovery key:', mask: '*' }
399
- ]);
709
+ const recoveryKey = await promptPassword('Enter your recovery key:');
400
710
  const recoveryData = await fs.readFile(recoveryPath, 'utf8');
401
711
  let oldPassword;
402
712
  try {
@@ -419,12 +729,8 @@ program
419
729
  }
420
730
  console.log(chalk.green('Recovery key verified. Store contains ' + Object.keys(variables).length + ' variables.'));
421
731
  // Set new password
422
- const { newPassword } = await inquirer.prompt([
423
- { type: 'password', name: 'newPassword', message: 'Set new password:', mask: '*' }
424
- ]);
425
- const { confirmPassword } = await inquirer.prompt([
426
- { type: 'password', name: 'confirmPassword', message: 'Confirm new password:', mask: '*' }
427
- ]);
732
+ const newPassword = await promptPassword('Set new password:');
733
+ const confirmPassword = await promptPassword('Confirm new password:');
428
734
  if (newPassword !== confirmPassword) {
429
735
  console.log(chalk.red('Passwords do not match'));
430
736
  return;
@@ -489,14 +795,10 @@ program
489
795
  let tags = [];
490
796
  let description = options.description;
491
797
  if (!value) {
492
- const answers = await inquirer.prompt([
493
- { type: 'password', name: 'value', message: 'Enter value:', mask: '*' },
494
- { type: 'input', name: 'tags', message: 'Tags (comma-separated):' },
495
- { type: 'input', name: 'description', message: 'Description:' },
496
- ]);
497
- value = answers.value;
498
- tags = answers.tags.split(',').map((t) => t.trim()).filter(Boolean);
499
- description = answers.description;
798
+ value = await promptPassword('Enter value:');
799
+ const tagsInput = await promptInput('Tags (comma-separated):');
800
+ tags = tagsInput.split(',').map((t) => t.trim()).filter(Boolean);
801
+ description = await promptInput('Description:');
500
802
  }
501
803
  else if (options.tags) {
502
804
  tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
@@ -606,7 +908,8 @@ program
606
908
  if (!variable.sync_to_env)
607
909
  continue;
608
910
  const excluded = config.sync.exclude?.some((pattern) => {
609
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
911
+ // 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, '.*') + '$');
610
913
  return regex.test(name);
611
914
  });
612
915
  if (excluded)
@@ -631,7 +934,8 @@ program
631
934
  if (!variable.sync_to_env)
632
935
  continue;
633
936
  const excluded = config.sync.exclude?.some((pattern) => {
634
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
937
+ // 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, '.*') + '$');
635
939
  return regex.test(name);
636
940
  });
637
941
  if (excluded)
@@ -709,10 +1013,7 @@ program
709
1013
  process.stderr.write('Error: No active session. Run `envcp unlock` first, or use --password flag.\n');
710
1014
  process.exit(1);
711
1015
  }
712
- const answer = await inquirer.prompt([
713
- { type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
714
- ]);
715
- password = answer.password;
1016
+ password = await promptPassword('Enter password:');
716
1017
  const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
717
1018
  if (!passwordValid) {
718
1019
  // nosem: no tainted data flows to log
@@ -804,12 +1105,8 @@ program
804
1105
  console.log(chalk.red('--output <path> is required with --encrypted'));
805
1106
  return;
806
1107
  }
807
- const { exportPassword } = await inquirer.prompt([
808
- { type: 'password', name: 'exportPassword', message: 'Set export password:', mask: '*' }
809
- ]);
810
- const { confirmExport } = await inquirer.prompt([
811
- { type: 'password', name: 'confirmExport', message: 'Confirm export password:', mask: '*' }
812
- ]);
1108
+ const exportPassword = await promptPassword('Set export password:');
1109
+ const confirmExport = await promptPassword('Confirm export password:');
813
1110
  if (exportPassword !== confirmExport) {
814
1111
  console.log(chalk.red('Passwords do not match'));
815
1112
  return;
@@ -857,9 +1154,7 @@ program
857
1154
  console.log(chalk.red(`File not found: ${file}`));
858
1155
  return;
859
1156
  }
860
- const { importPassword } = await inquirer.prompt([
861
- { type: 'password', name: 'importPassword', message: 'Enter export file password:', mask: '*' }
862
- ]);
1157
+ const importPassword = await promptPassword('Enter export file password:');
863
1158
  const fileContent = await fs.readFile(file, 'utf8');
864
1159
  let importData;
865
1160
  try {
@@ -920,9 +1215,7 @@ program
920
1215
  console.log(chalk.gray('\nNo files were modified.'));
921
1216
  return;
922
1217
  }
923
- const { confirm } = await inquirer.prompt([
924
- { type: 'confirm', name: 'confirm', message: options.merge ? 'Merge into current store?' : 'Replace current store?', default: false }
925
- ]);
1218
+ const confirm = await promptConfirm(options.merge ? 'Merge into current store?' : 'Replace current store?', false);
926
1219
  if (!confirm) {
927
1220
  console.log(chalk.yellow('Import cancelled'));
928
1221
  return;
@@ -1004,9 +1297,7 @@ program
1004
1297
  console.log(chalk.gray(` Created: ${meta.timestamp}`));
1005
1298
  console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
1006
1299
  }
1007
- const { confirm } = await inquirer.prompt([
1008
- { type: 'confirm', name: 'confirm', message: options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', default: false }
1009
- ]);
1300
+ const confirm = await promptConfirm(options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', false);
1010
1301
  if (!confirm) {
1011
1302
  console.log(chalk.yellow('Restore cancelled'));
1012
1303
  return;
@@ -1168,7 +1459,7 @@ program
1168
1459
  .option('--name <name>', 'Operate on a named vault')
1169
1460
  .addCommand(new Command('init')
1170
1461
  .description('Initialize a vault')
1171
- .action(async (options, cmd) => {
1462
+ .action(async (_options, cmd) => {
1172
1463
  const parentOpts = cmd.parent.opts();
1173
1464
  const projectPath = process.cwd();
1174
1465
  const config = await loadConfig(projectPath);
@@ -1199,7 +1490,7 @@ program
1199
1490
  const parentOpts = cmd.parent.opts();
1200
1491
  const vaultOverride = parentOpts.global ? 'global' : parentOpts.project ? 'project' : undefined;
1201
1492
  await withSession(async (storage) => {
1202
- const value = options.value || (await inquirer.prompt([{ type: 'input', name: 'value', message: 'Value:' }])).value;
1493
+ const value = options.value || await promptInput('Value:');
1203
1494
  const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
1204
1495
  const now = new Date().toISOString();
1205
1496
  await storage.set(name, {
@@ -1275,7 +1566,6 @@ program
1275
1566
  .argument('<name>', 'Vault name (global, project, or named vault)')
1276
1567
  .action(async (name) => {
1277
1568
  const projectPath = process.cwd();
1278
- const config = await loadConfig(projectPath);
1279
1569
  if (name !== 'global' && name !== 'project') {
1280
1570
  const vaultDir = path.join(projectPath, '.envcp/vaults', name);
1281
1571
  if (!await pathExists(vaultDir)) {
@@ -1334,7 +1624,7 @@ program
1334
1624
  .addCommand(new Command('save')
1335
1625
  .description('Save current password to OS keychain')
1336
1626
  .action(async () => {
1337
- await withSession(async (storage, password, config, projectPath) => {
1627
+ await withSession(async (_storage, password, config, projectPath) => {
1338
1628
  if (!password) {
1339
1629
  console.log(chalk.red('No password available (encryption disabled?)'));
1340
1630
  return;
@@ -1422,5 +1712,131 @@ if (!await pathExists(firstRunMarker)) {
1422
1712
  Docs: https://github.com/fentz26/EnvCP
1423
1713
  `);
1424
1714
  }
1715
+ program
1716
+ .command('logs')
1717
+ .description('View audit logs')
1718
+ .option('--date <date>', 'Log date (YYYY-MM-DD, default: today)')
1719
+ .option('--operation <op>', 'Filter by operation (add, get, update, delete, list, sync, export, unlock, lock, check_access, run, auth_failure)')
1720
+ .option('--variable <name>', 'Filter by variable name')
1721
+ .option('--source <source>', 'Filter by source (cli, mcp, api)')
1722
+ .option('--success', 'Show only successful operations')
1723
+ .option('--failure', 'Show only failed operations')
1724
+ .option('--tail <n>', 'Show last N entries', parseInt)
1725
+ .option('--dates', 'List all available log dates')
1726
+ .option('--verify', 'Verify HMAC integrity of log entries')
1727
+ .action(async (options) => {
1728
+ const projectPath = process.cwd();
1729
+ const config = await loadConfig(projectPath);
1730
+ const logDir = path.join(projectPath, '.envcp', 'logs');
1731
+ const logs = new LogManager(logDir, config.audit);
1732
+ await logs.init();
1733
+ if (options.dates) {
1734
+ const dates = await logs.getLogDates();
1735
+ if (dates.length === 0) {
1736
+ console.log(chalk.gray('No log files found.'));
1737
+ }
1738
+ else {
1739
+ console.log(chalk.bold('Available log dates:'));
1740
+ dates.forEach(d => console.log(chalk.gray(` ${d}`)));
1741
+ }
1742
+ return;
1743
+ }
1744
+ const filter = {};
1745
+ if (options.date)
1746
+ filter.date = options.date;
1747
+ if (options.operation)
1748
+ filter.operation = options.operation;
1749
+ if (options.variable)
1750
+ filter.variable = options.variable;
1751
+ if (options.source)
1752
+ filter.source = options.source;
1753
+ if (options.success)
1754
+ filter.success = true;
1755
+ if (options.failure)
1756
+ filter.success = false;
1757
+ if (options.tail)
1758
+ filter.tail = options.tail;
1759
+ const entries = await logs.getLogs(filter);
1760
+ if (entries.length === 0) {
1761
+ console.log(chalk.gray('No log entries found.'));
1762
+ return;
1763
+ }
1764
+ let failed = 0;
1765
+ entries.forEach(entry => {
1766
+ const ts = chalk.gray(entry.timestamp);
1767
+ const op = entry.success ? chalk.green(entry.operation) : chalk.red(entry.operation);
1768
+ const src = chalk.blue(entry.source);
1769
+ const varName = entry.variable ? chalk.yellow(` [${entry.variable}]`) : '';
1770
+ const msg = entry.message ? chalk.gray(` — ${entry.message}`) : '';
1771
+ if (options.verify && entry.hmac !== undefined) {
1772
+ const valid = logs.verifyEntry(entry);
1773
+ if (!valid) {
1774
+ failed++;
1775
+ console.log(`${ts} ${chalk.red('[TAMPERED]')} ${op} ${src}${varName}${msg}`);
1776
+ return;
1777
+ }
1778
+ }
1779
+ console.log(`${ts} ${op} ${src}${varName}${msg}`);
1780
+ });
1781
+ if (options.verify) {
1782
+ if (failed === 0) {
1783
+ console.log(chalk.green(`\nAll ${entries.length} entries verified OK.`));
1784
+ }
1785
+ else {
1786
+ console.log(chalk.red(`\n${failed}/${entries.length} entries failed HMAC verification.`));
1787
+ }
1788
+ }
1789
+ });
1790
+ program
1791
+ .command('verify-logs')
1792
+ .description('Verify HMAC chain integrity of audit logs')
1793
+ .option('--date <date>', 'Verify specific date (YYYY-MM-DD, default: all dates)')
1794
+ .action(async (options) => {
1795
+ const projectPath = process.cwd();
1796
+ const config = await loadConfig(projectPath);
1797
+ const logDir = path.join(projectPath, '.envcp', 'logs');
1798
+ const logs = new LogManager(logDir, config.audit);
1799
+ await logs.init();
1800
+ if (!config.audit.hmac_chain) {
1801
+ console.log(chalk.yellow('HMAC chain is not enabled in audit config.'));
1802
+ return;
1803
+ }
1804
+ console.log(chalk.bold('Verifying log chain integrity...'));
1805
+ const result = await logs.verifyLogChain(options.date);
1806
+ if (result.valid) {
1807
+ console.log(chalk.green(`✓ Chain integrity verified: ${result.entries} entries OK`));
1808
+ }
1809
+ else {
1810
+ console.log(chalk.red(`✗ Chain integrity FAILED: ${result.tampered.length}/${result.entries} entries tampered`));
1811
+ console.log(chalk.red(` Tampered indices: ${result.tampered.join(', ')}`));
1812
+ }
1813
+ });
1814
+ program
1815
+ .command('protect-logs')
1816
+ .description('Apply OS-level protection to audit log files (Linux only)')
1817
+ .action(async () => {
1818
+ const projectPath = process.cwd();
1819
+ const config = await loadConfig(projectPath);
1820
+ const logDir = path.join(projectPath, '.envcp', 'logs');
1821
+ const logs = new LogManager(logDir, config.audit);
1822
+ await logs.init();
1823
+ if (config.audit.protection === 'none') {
1824
+ console.log(chalk.yellow('Log protection is disabled in config (protection: none).'));
1825
+ return;
1826
+ }
1827
+ console.log(chalk.bold(`Applying ${config.audit.protection} protection to log files...`));
1828
+ const result = await logs.protectLogFiles();
1829
+ if (result.protected.length > 0) {
1830
+ console.log(chalk.green(`✓ Protected ${result.protected.length} files:`));
1831
+ result.protected.forEach((f) => console.log(chalk.gray(` ${f}`)));
1832
+ }
1833
+ if (result.failed.length > 0) {
1834
+ console.log(chalk.red(`✗ Failed to protect ${result.failed.length} files:`));
1835
+ result.failed.forEach((f) => console.log(chalk.gray(` ${f}`)));
1836
+ }
1837
+ if (result.protected.length === 0 && result.failed.length === 0) {
1838
+ console.log(chalk.gray('No log files to protect.'));
1839
+ }
1840
+ });
1425
1841
  program.parse();
1426
1842
  //# sourceMappingURL=index.js.map