envmatic 1.0.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 (102) hide show
  1. package/README.md +567 -0
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +203 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +77 -0
  9. package/dist/commands/add.js.map +1 -0
  10. package/dist/commands/delete.d.ts +6 -0
  11. package/dist/commands/delete.d.ts.map +1 -0
  12. package/dist/commands/delete.js +78 -0
  13. package/dist/commands/delete.js.map +1 -0
  14. package/dist/commands/edit.d.ts +13 -0
  15. package/dist/commands/edit.d.ts.map +1 -0
  16. package/dist/commands/edit.js +364 -0
  17. package/dist/commands/edit.js.map +1 -0
  18. package/dist/commands/import.d.ts +11 -0
  19. package/dist/commands/import.d.ts.map +1 -0
  20. package/dist/commands/import.js +103 -0
  21. package/dist/commands/import.js.map +1 -0
  22. package/dist/commands/init.d.ts +8 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +237 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/link.d.ts +16 -0
  27. package/dist/commands/link.d.ts.map +1 -0
  28. package/dist/commands/link.js +157 -0
  29. package/dist/commands/link.js.map +1 -0
  30. package/dist/commands/list.d.ts +9 -0
  31. package/dist/commands/list.d.ts.map +1 -0
  32. package/dist/commands/list.js +73 -0
  33. package/dist/commands/list.js.map +1 -0
  34. package/dist/commands/lock.d.ts +16 -0
  35. package/dist/commands/lock.d.ts.map +1 -0
  36. package/dist/commands/lock.js +245 -0
  37. package/dist/commands/lock.js.map +1 -0
  38. package/dist/commands/rotate.d.ts +15 -0
  39. package/dist/commands/rotate.d.ts.map +1 -0
  40. package/dist/commands/rotate.js +406 -0
  41. package/dist/commands/rotate.js.map +1 -0
  42. package/dist/commands/show.d.ts +9 -0
  43. package/dist/commands/show.d.ts.map +1 -0
  44. package/dist/commands/show.js +72 -0
  45. package/dist/commands/show.js.map +1 -0
  46. package/dist/commands/sync.d.ts +13 -0
  47. package/dist/commands/sync.d.ts.map +1 -0
  48. package/dist/commands/sync.js +174 -0
  49. package/dist/commands/sync.js.map +1 -0
  50. package/dist/commands/use.d.ts +19 -0
  51. package/dist/commands/use.d.ts.map +1 -0
  52. package/dist/commands/use.js +238 -0
  53. package/dist/commands/use.js.map +1 -0
  54. package/dist/constants.d.ts +20 -0
  55. package/dist/constants.d.ts.map +1 -0
  56. package/dist/constants.js +47 -0
  57. package/dist/constants.js.map +1 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +21 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/services/config.d.ts +64 -0
  63. package/dist/services/config.d.ts.map +1 -0
  64. package/dist/services/config.js +133 -0
  65. package/dist/services/config.js.map +1 -0
  66. package/dist/services/encryption.d.ts +30 -0
  67. package/dist/services/encryption.d.ts.map +1 -0
  68. package/dist/services/encryption.js +146 -0
  69. package/dist/services/encryption.js.map +1 -0
  70. package/dist/services/envfile.d.ts +76 -0
  71. package/dist/services/envfile.d.ts.map +1 -0
  72. package/dist/services/envfile.js +247 -0
  73. package/dist/services/envfile.js.map +1 -0
  74. package/dist/services/git.d.ts +60 -0
  75. package/dist/services/git.d.ts.map +1 -0
  76. package/dist/services/git.js +239 -0
  77. package/dist/services/git.js.map +1 -0
  78. package/dist/services/linker.d.ts +46 -0
  79. package/dist/services/linker.d.ts.map +1 -0
  80. package/dist/services/linker.js +222 -0
  81. package/dist/services/linker.js.map +1 -0
  82. package/dist/services/protection.d.ts +32 -0
  83. package/dist/services/protection.d.ts.map +1 -0
  84. package/dist/services/protection.js +190 -0
  85. package/dist/services/protection.js.map +1 -0
  86. package/dist/types/index.d.ts +73 -0
  87. package/dist/types/index.d.ts.map +1 -0
  88. package/dist/types/index.js +5 -0
  89. package/dist/types/index.js.map +1 -0
  90. package/dist/utils/display.d.ts +74 -0
  91. package/dist/utils/display.d.ts.map +1 -0
  92. package/dist/utils/display.js +138 -0
  93. package/dist/utils/display.js.map +1 -0
  94. package/dist/utils/editor.d.ts +22 -0
  95. package/dist/utils/editor.d.ts.map +1 -0
  96. package/dist/utils/editor.js +159 -0
  97. package/dist/utils/editor.js.map +1 -0
  98. package/dist/utils/prompts.d.ts +41 -0
  99. package/dist/utils/prompts.d.ts.map +1 -0
  100. package/dist/utils/prompts.js +222 -0
  101. package/dist/utils/prompts.js.map +1 -0
  102. package/package.json +69 -0
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Lock Command
3
+ * List and lock mutable (unlocked) env files
4
+ */
5
+ import ora from 'ora';
6
+ import inquirer from 'inquirer';
7
+ import { listEnvFiles, getEnvFilePath, readEnvFile, updateEnvFile } from '../services/envfile.js';
8
+ import { sync, commitChanges } from '../services/git.js';
9
+ import { getConfig } from '../services/config.js';
10
+ import { makeImmutable, isImmutable } from '../services/protection.js';
11
+ import { syncCopies } from '../services/linker.js';
12
+ import { printBanner, success, error, info, warning, colors, formatFileId } from '../utils/display.js';
13
+ import { getEncryptionOptions, confirm } from '../utils/prompts.js';
14
+ /**
15
+ * Find all unlocked (mutable) env files
16
+ */
17
+ async function findUnlockedFiles() {
18
+ const files = await listEnvFiles();
19
+ const unlocked = [];
20
+ for (const file of files) {
21
+ // Only check files that should be immutable
22
+ if (file.immutable) {
23
+ const filePath = getEnvFilePath(file.id, file.encrypted);
24
+ const currentlyImmutable = await isImmutable(filePath);
25
+ if (!currentlyImmutable) {
26
+ unlocked.push({ file, filePath });
27
+ }
28
+ }
29
+ }
30
+ return unlocked;
31
+ }
32
+ /**
33
+ * Lock a single file
34
+ */
35
+ async function lockFile(file, filePath) {
36
+ try {
37
+ await makeImmutable(filePath);
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * Lock command - list and lock mutable files
46
+ */
47
+ export async function lockCommand(fileId, options = {}) {
48
+ printBanner();
49
+ const config = await getConfig();
50
+ if (!config) {
51
+ error('Envmatic is not initialized. Run `envmatic init` first.');
52
+ return;
53
+ }
54
+ // If specific file ID provided, lock just that file
55
+ if (fileId) {
56
+ const files = await listEnvFiles();
57
+ const file = files.find(f => f.id === fileId);
58
+ if (!file) {
59
+ error(`Env file not found: ${fileId}`);
60
+ return;
61
+ }
62
+ const filePath = getEnvFilePath(file.id, file.encrypted);
63
+ const currentlyImmutable = await isImmutable(filePath);
64
+ if (currentlyImmutable) {
65
+ info(`${formatFileId(fileId)} is already locked.`);
66
+ return;
67
+ }
68
+ const spinner = ora('Locking file...').start();
69
+ const locked = await lockFile(file, filePath);
70
+ if (locked) {
71
+ spinner.succeed('File locked');
72
+ // If file was edited, we should sync
73
+ const shouldSync = await confirm('Sync changes to remote?', true);
74
+ if (shouldSync) {
75
+ const encryptionOptions = await getEncryptionOptions();
76
+ // Re-encrypt if needed (in case content was modified)
77
+ try {
78
+ const { variables } = await readEnvFile(file.id, encryptionOptions);
79
+ await updateEnvFile(file.id, variables, encryptionOptions);
80
+ await syncCopies(file.id, encryptionOptions);
81
+ }
82
+ catch (err) {
83
+ // File might not have been modified, that's ok
84
+ }
85
+ const syncSpinner = ora('Syncing to remote...').start();
86
+ try {
87
+ await commitChanges(`Update ${file.id}`);
88
+ await sync();
89
+ syncSpinner.succeed('Synced');
90
+ }
91
+ catch {
92
+ syncSpinner.warn('Could not sync (will sync later)');
93
+ }
94
+ }
95
+ console.log();
96
+ success(`${formatFileId(fileId)} is now locked.`);
97
+ }
98
+ else {
99
+ spinner.fail('Failed to lock file');
100
+ }
101
+ return;
102
+ }
103
+ // Find all unlocked files
104
+ const unlockedFiles = await findUnlockedFiles();
105
+ if (unlockedFiles.length === 0) {
106
+ success('All files are locked.');
107
+ console.log();
108
+ console.log(colors.muted('No unlocked files found.'));
109
+ return;
110
+ }
111
+ console.log(colors.warning(`Found ${unlockedFiles.length} unlocked file(s):\n`));
112
+ for (const { file } of unlockedFiles) {
113
+ const flags = [];
114
+ if (file.encrypted)
115
+ flags.push('🔒');
116
+ const flagStr = flags.length > 0 ? ' ' + flags.join(' ') : '';
117
+ console.log(` ${colors.accent('🔓')} ${formatFileId(file.id)}${flagStr}`);
118
+ }
119
+ console.log();
120
+ // Lock all option
121
+ if (options.all) {
122
+ const spinner = ora('Locking all files...').start();
123
+ let locked = 0;
124
+ let failed = 0;
125
+ for (const { file, filePath } of unlockedFiles) {
126
+ if (await lockFile(file, filePath)) {
127
+ locked++;
128
+ }
129
+ else {
130
+ failed++;
131
+ }
132
+ }
133
+ if (failed > 0) {
134
+ spinner.warn(`Locked ${locked} file(s), ${failed} failed`);
135
+ }
136
+ else {
137
+ spinner.succeed(`Locked ${locked} file(s)`);
138
+ }
139
+ // Sync changes
140
+ const shouldSync = await confirm('Sync changes to remote?', true);
141
+ if (shouldSync) {
142
+ const syncSpinner = ora('Syncing to remote...').start();
143
+ try {
144
+ await commitChanges('Lock files after editing');
145
+ await sync();
146
+ syncSpinner.succeed('Synced');
147
+ }
148
+ catch {
149
+ syncSpinner.warn('Could not sync (will sync later)');
150
+ }
151
+ }
152
+ console.log();
153
+ success('All files locked.');
154
+ return;
155
+ }
156
+ // Interactive selection
157
+ const { action } = await inquirer.prompt([
158
+ {
159
+ type: 'list',
160
+ name: 'action',
161
+ message: 'What would you like to do?',
162
+ choices: [
163
+ { name: 'Lock all unlocked files', value: 'all' },
164
+ { name: 'Select files to lock', value: 'select' },
165
+ { name: 'Cancel', value: 'cancel' },
166
+ ],
167
+ },
168
+ ]);
169
+ if (action === 'cancel') {
170
+ info('No files were locked.');
171
+ console.log();
172
+ warning('Remember: Unlocked files are not protected from accidental edits.');
173
+ return;
174
+ }
175
+ if (action === 'all') {
176
+ return lockCommand(undefined, { all: true });
177
+ }
178
+ // Select specific files
179
+ const { selectedFiles } = await inquirer.prompt([
180
+ {
181
+ type: 'checkbox',
182
+ name: 'selectedFiles',
183
+ message: 'Select files to lock:',
184
+ choices: unlockedFiles.map(({ file }) => ({
185
+ name: file.id,
186
+ value: file.id,
187
+ checked: true,
188
+ })),
189
+ },
190
+ ]);
191
+ if (selectedFiles.length === 0) {
192
+ info('No files selected.');
193
+ return;
194
+ }
195
+ const spinner = ora('Locking selected files...').start();
196
+ let locked = 0;
197
+ let failed = 0;
198
+ for (const id of selectedFiles) {
199
+ const item = unlockedFiles.find(u => u.file.id === id);
200
+ if (item && await lockFile(item.file, item.filePath)) {
201
+ locked++;
202
+ }
203
+ else {
204
+ failed++;
205
+ }
206
+ }
207
+ if (failed > 0) {
208
+ spinner.warn(`Locked ${locked} file(s), ${failed} failed`);
209
+ }
210
+ else {
211
+ spinner.succeed(`Locked ${locked} file(s)`);
212
+ }
213
+ // Sync changes
214
+ const shouldSync = await confirm('Sync changes to remote?', true);
215
+ if (shouldSync) {
216
+ const syncSpinner = ora('Syncing to remote...').start();
217
+ try {
218
+ await commitChanges('Lock files after editing');
219
+ await sync();
220
+ syncSpinner.succeed('Synced');
221
+ }
222
+ catch {
223
+ syncSpinner.warn('Could not sync (will sync later)');
224
+ }
225
+ }
226
+ console.log();
227
+ success('Selected files locked.');
228
+ }
229
+ /**
230
+ * Unlock command - unlock a file for editing
231
+ * (This is primarily used internally by the edit --editor command)
232
+ */
233
+ export async function unlockFileForEditing(fileId) {
234
+ const files = await listEnvFiles();
235
+ const file = files.find(f => f.id === fileId);
236
+ if (!file) {
237
+ return null;
238
+ }
239
+ const filePath = getEnvFilePath(file.id, file.encrypted);
240
+ // Make it mutable
241
+ const { makeMutable } = await import('../services/protection.js');
242
+ await makeMutable(filePath);
243
+ return filePath;
244
+ }
245
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/commands/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Rotate Command
3
+ * Change encryption password or rotate encryption key/method
4
+ */
5
+ /**
6
+ * Change password command
7
+ * Requires old password to decrypt, then re-encrypts with new password
8
+ */
9
+ export declare function changePasswordCommand(): Promise<void>;
10
+ /**
11
+ * Rotate encryption key/method
12
+ * Can switch between password and SSH key encryption
13
+ */
14
+ export declare function rotateKeyCommand(): Promise<void>;
15
+ //# sourceMappingURL=rotate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotate.d.ts","sourceRoot":"","sources":["../../src/commands/rotate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAyBH;;;GAGG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAsK3D;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqRtD"}
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Rotate Command
3
+ * Change encryption password or rotate encryption key/method
4
+ */
5
+ import ora from 'ora';
6
+ import inquirer from 'inquirer';
7
+ import { readEnvFile, updateEnvFile, listEnvFiles, getEnvFilePath } from '../services/envfile.js';
8
+ import { sync } from '../services/git.js';
9
+ import { getConfig, updateConfig } from '../services/config.js';
10
+ import { verifyEncryption, validateSSHKey } from '../services/encryption.js';
11
+ import { makeMutable, makeImmutable } from '../services/protection.js';
12
+ import { printBanner, success, error, info, warning, colors } from '../utils/display.js';
13
+ /**
14
+ * Change password command
15
+ * Requires old password to decrypt, then re-encrypts with new password
16
+ */
17
+ export async function changePasswordCommand() {
18
+ printBanner();
19
+ const config = await getConfig();
20
+ if (!config) {
21
+ error('Envmatic is not initialized. Run `envmatic init` first.');
22
+ return;
23
+ }
24
+ if (!config.encryptionEnabled) {
25
+ error('Encryption is not enabled. Nothing to change.');
26
+ info('Run `envmatic rotate-key` to enable encryption.');
27
+ return;
28
+ }
29
+ if (config.encryptionMethod !== 'password') {
30
+ error('Current encryption method is SSH key, not password.');
31
+ info('Use `envmatic rotate-key` to switch encryption methods.');
32
+ return;
33
+ }
34
+ console.log(colors.muted('Change your encryption password.\n'));
35
+ warning('You will need to enter your current password to proceed.');
36
+ console.log();
37
+ // Get old password
38
+ const { oldPassword } = await inquirer.prompt([
39
+ {
40
+ type: 'password',
41
+ name: 'oldPassword',
42
+ message: 'Enter your CURRENT password:',
43
+ mask: '*',
44
+ validate: (input) => {
45
+ if (!input || input.length < 8) {
46
+ return 'Password must be at least 8 characters';
47
+ }
48
+ return true;
49
+ },
50
+ },
51
+ ]);
52
+ // Verify old password works
53
+ const oldOptions = {
54
+ method: 'password',
55
+ password: oldPassword,
56
+ };
57
+ const verifySpinner = ora('Verifying current password...').start();
58
+ const isValid = await verifyEncryption(oldOptions);
59
+ if (!isValid) {
60
+ verifySpinner.fail('Current password is incorrect');
61
+ error('Cannot proceed without the correct current password.');
62
+ return;
63
+ }
64
+ verifySpinner.succeed('Current password verified');
65
+ // Get new password
66
+ console.log();
67
+ const { newPassword } = await inquirer.prompt([
68
+ {
69
+ type: 'password',
70
+ name: 'newPassword',
71
+ message: 'Enter your NEW password:',
72
+ mask: '*',
73
+ validate: (input) => {
74
+ if (!input || input.length < 8) {
75
+ return 'Password must be at least 8 characters';
76
+ }
77
+ if (input === oldPassword) {
78
+ return 'New password must be different from current password';
79
+ }
80
+ return true;
81
+ },
82
+ },
83
+ ]);
84
+ // Confirm new password
85
+ const { confirmPassword } = await inquirer.prompt([
86
+ {
87
+ type: 'password',
88
+ name: 'confirmPassword',
89
+ message: 'Confirm your NEW password:',
90
+ mask: '*',
91
+ validate: (input) => {
92
+ if (input !== newPassword) {
93
+ return 'Passwords do not match';
94
+ }
95
+ return true;
96
+ },
97
+ },
98
+ ]);
99
+ const newOptions = {
100
+ method: 'password',
101
+ password: newPassword,
102
+ };
103
+ // Get all encrypted files
104
+ const files = await listEnvFiles();
105
+ const encryptedFiles = files.filter(f => f.encrypted);
106
+ if (encryptedFiles.length === 0) {
107
+ info('No encrypted files found. Password updated for future files.');
108
+ success('Password changed successfully!');
109
+ return;
110
+ }
111
+ console.log();
112
+ console.log(colors.muted(`Found ${encryptedFiles.length} encrypted file(s) to re-encrypt.\n`));
113
+ // Re-encrypt all files
114
+ const spinner = ora('Re-encrypting files...').start();
115
+ let processed = 0;
116
+ let errors = 0;
117
+ for (const file of encryptedFiles) {
118
+ try {
119
+ // Make file mutable if needed
120
+ const filePath = getEnvFilePath(file.id, file.encrypted);
121
+ if (file.immutable) {
122
+ await makeMutable(filePath);
123
+ }
124
+ // Read with old password
125
+ const { variables } = await readEnvFile(file.id, oldOptions);
126
+ // Write with new password
127
+ await updateEnvFile(file.id, variables, newOptions);
128
+ // Restore protection if needed
129
+ if (file.immutable) {
130
+ await makeImmutable(filePath);
131
+ }
132
+ processed++;
133
+ spinner.text = `Re-encrypting files... (${processed}/${encryptedFiles.length})`;
134
+ }
135
+ catch (err) {
136
+ errors++;
137
+ const errorMessage = err instanceof Error ? err.message : String(err);
138
+ console.error(`\nFailed to re-encrypt ${file.id}: ${errorMessage}`);
139
+ }
140
+ }
141
+ if (errors > 0) {
142
+ spinner.warn(`Re-encrypted ${processed} files with ${errors} error(s)`);
143
+ }
144
+ else {
145
+ spinner.succeed(`Re-encrypted ${processed} file(s)`);
146
+ }
147
+ // Sync to remote
148
+ const syncSpinner = ora('Syncing to remote...').start();
149
+ try {
150
+ await sync();
151
+ syncSpinner.succeed('Synced to remote');
152
+ }
153
+ catch {
154
+ syncSpinner.warn('Could not sync to remote (will sync later)');
155
+ }
156
+ console.log();
157
+ success('Password changed successfully!');
158
+ console.log();
159
+ warning('Remember your new password! It cannot be recovered.');
160
+ warning('If you forget it, all encrypted data will be permanently lost.');
161
+ }
162
+ /**
163
+ * Rotate encryption key/method
164
+ * Can switch between password and SSH key encryption
165
+ */
166
+ export async function rotateKeyCommand() {
167
+ printBanner();
168
+ const config = await getConfig();
169
+ if (!config) {
170
+ error('Envmatic is not initialized. Run `envmatic init` first.');
171
+ return;
172
+ }
173
+ console.log(colors.muted('Rotate your encryption key or change encryption method.\n'));
174
+ // Determine current state
175
+ const currentMethod = config.encryptionEnabled
176
+ ? config.encryptionMethod || 'none'
177
+ : 'none';
178
+ console.log(colors.muted('Current encryption:') + ' ' +
179
+ (currentMethod === 'none'
180
+ ? colors.warning('disabled')
181
+ : colors.secondary(currentMethod)));
182
+ console.log();
183
+ // Get current encryption options if encryption is enabled
184
+ let oldOptions;
185
+ if (config.encryptionEnabled && config.encryptionMethod) {
186
+ warning('You will need to provide your current credentials to proceed.');
187
+ console.log();
188
+ if (config.encryptionMethod === 'password') {
189
+ const { password } = await inquirer.prompt([
190
+ {
191
+ type: 'password',
192
+ name: 'password',
193
+ message: 'Enter your CURRENT password:',
194
+ mask: '*',
195
+ },
196
+ ]);
197
+ oldOptions = { method: 'password', password };
198
+ // Verify
199
+ const isValid = await verifyEncryption(oldOptions);
200
+ if (!isValid) {
201
+ error('Current password is incorrect. Cannot proceed.');
202
+ return;
203
+ }
204
+ success('Current password verified');
205
+ }
206
+ else if (config.encryptionMethod === 'ssh') {
207
+ oldOptions = {
208
+ method: 'ssh',
209
+ sshKeyPath: config.sshKeyPath
210
+ };
211
+ // Verify SSH key exists
212
+ const isValid = await validateSSHKey(config.sshKeyPath);
213
+ if (!isValid) {
214
+ error('Current SSH key is not accessible. Cannot proceed.');
215
+ return;
216
+ }
217
+ success('Current SSH key verified');
218
+ }
219
+ }
220
+ console.log();
221
+ // Ask for new method
222
+ const { newMethod } = await inquirer.prompt([
223
+ {
224
+ type: 'list',
225
+ name: 'newMethod',
226
+ message: 'Select new encryption method:',
227
+ choices: [
228
+ { name: 'Password encryption', value: 'password' },
229
+ { name: 'SSH key encryption', value: 'ssh' },
230
+ { name: 'Disable encryption (not recommended)', value: 'none' },
231
+ ],
232
+ },
233
+ ]);
234
+ let newOptions;
235
+ let newSshKeyPath;
236
+ if (newMethod === 'password') {
237
+ // Show password warning
238
+ console.log();
239
+ console.log(colors.error('╔══════════════════════════════════════════════════════════════╗'));
240
+ console.log(colors.error('║') + colors.warning(' ⚠️ PASSWORD SECURITY WARNING ') + colors.error('║'));
241
+ console.log(colors.error('╠══════════════════════════════════════════════════════════════╣'));
242
+ console.log(colors.error('║') + ' Your password is the ONLY way to decrypt your secrets. ' + colors.error('║'));
243
+ console.log(colors.error('║') + ' There is NO password recovery mechanism. ' + colors.error('║'));
244
+ console.log(colors.error('║') + ' ' + colors.error('║'));
245
+ console.log(colors.error('║') + colors.warning(' If you forget your password: ') + colors.error('║'));
246
+ console.log(colors.error('║') + colors.error(' → All encrypted data will be PERMANENTLY LOST ') + colors.error('║'));
247
+ console.log(colors.error('║') + colors.error(' → There is NO way to recover your secrets ') + colors.error('║'));
248
+ console.log(colors.error('║') + ' ' + colors.error('║'));
249
+ console.log(colors.error('║') + ' We strongly recommend: ' + colors.error('║'));
250
+ console.log(colors.error('║') + ' • Using a password manager to store your password ' + colors.error('║'));
251
+ console.log(colors.error('║') + ' • Writing it down and storing it securely offline ' + colors.error('║'));
252
+ console.log(colors.error('╚══════════════════════════════════════════════════════════════╝'));
253
+ console.log();
254
+ const { understood } = await inquirer.prompt([
255
+ {
256
+ type: 'confirm',
257
+ name: 'understood',
258
+ message: 'I understand that forgetting my password means losing all encrypted data',
259
+ default: false,
260
+ },
261
+ ]);
262
+ if (!understood) {
263
+ info('Operation cancelled.');
264
+ return;
265
+ }
266
+ const { password } = await inquirer.prompt([
267
+ {
268
+ type: 'password',
269
+ name: 'password',
270
+ message: 'Enter your NEW password:',
271
+ mask: '*',
272
+ validate: (input) => {
273
+ if (!input || input.length < 8) {
274
+ return 'Password must be at least 8 characters';
275
+ }
276
+ return true;
277
+ },
278
+ },
279
+ ]);
280
+ const { confirmPwd } = await inquirer.prompt([
281
+ {
282
+ type: 'password',
283
+ name: 'confirmPwd',
284
+ message: 'Confirm your NEW password:',
285
+ mask: '*',
286
+ validate: (input) => {
287
+ if (input !== password) {
288
+ return 'Passwords do not match';
289
+ }
290
+ return true;
291
+ },
292
+ },
293
+ ]);
294
+ newOptions = { method: 'password', password };
295
+ }
296
+ else if (newMethod === 'ssh') {
297
+ const { keyPath } = await inquirer.prompt([
298
+ {
299
+ type: 'input',
300
+ name: 'keyPath',
301
+ message: 'Path to SSH private key:',
302
+ default: '~/.ssh/id_rsa',
303
+ },
304
+ ]);
305
+ newSshKeyPath = keyPath.replace(/^~/, process.env.HOME || '');
306
+ const validKey = await validateSSHKey(newSshKeyPath);
307
+ if (!validKey) {
308
+ error('Invalid SSH key file. Please check the path and try again.');
309
+ return;
310
+ }
311
+ success('SSH key validated');
312
+ newOptions = { method: 'ssh', sshKeyPath: newSshKeyPath };
313
+ }
314
+ else {
315
+ // Disabling encryption
316
+ warning('Disabling encryption will store all files in PLAIN TEXT.');
317
+ warning('Anyone with access to the repository will be able to read your secrets.');
318
+ const { confirm } = await inquirer.prompt([
319
+ {
320
+ type: 'confirm',
321
+ name: 'confirm',
322
+ message: 'Are you sure you want to disable encryption?',
323
+ default: false,
324
+ },
325
+ ]);
326
+ if (!confirm) {
327
+ info('Operation cancelled.');
328
+ return;
329
+ }
330
+ }
331
+ // Get all files
332
+ const files = await listEnvFiles();
333
+ const encryptedFiles = files.filter(f => f.encrypted);
334
+ console.log();
335
+ if (encryptedFiles.length === 0) {
336
+ // No files to re-encrypt, just update config
337
+ await updateConfig({
338
+ encryptionEnabled: newMethod !== 'none',
339
+ encryptionMethod: newMethod === 'none' ? undefined : newMethod,
340
+ sshKeyPath: newSshKeyPath,
341
+ });
342
+ success('Encryption settings updated!');
343
+ info('New settings will apply to future files.');
344
+ return;
345
+ }
346
+ console.log(colors.muted(`Found ${encryptedFiles.length} encrypted file(s) to process.\n`));
347
+ // Re-encrypt all files
348
+ const spinner = ora('Processing files...').start();
349
+ let processed = 0;
350
+ let errors = 0;
351
+ for (const file of encryptedFiles) {
352
+ try {
353
+ const filePath = getEnvFilePath(file.id, file.encrypted);
354
+ // Make file mutable if needed
355
+ if (file.immutable) {
356
+ await makeMutable(filePath);
357
+ }
358
+ // Read with old options
359
+ const { variables } = await readEnvFile(file.id, oldOptions);
360
+ // Write with new options (or no encryption if disabling)
361
+ await updateEnvFile(file.id, variables, newOptions);
362
+ // Restore protection if needed
363
+ if (file.immutable) {
364
+ // File path might have changed (added/removed .enc extension)
365
+ const newFilePath = getEnvFilePath(file.id, newMethod !== 'none');
366
+ await makeImmutable(newFilePath);
367
+ }
368
+ processed++;
369
+ spinner.text = `Processing files... (${processed}/${encryptedFiles.length})`;
370
+ }
371
+ catch (err) {
372
+ errors++;
373
+ const errorMessage = err instanceof Error ? err.message : String(err);
374
+ console.error(`\nFailed to process ${file.id}: ${errorMessage}`);
375
+ }
376
+ }
377
+ if (errors > 0) {
378
+ spinner.warn(`Processed ${processed} files with ${errors} error(s)`);
379
+ }
380
+ else {
381
+ spinner.succeed(`Processed ${processed} file(s)`);
382
+ }
383
+ // Update config
384
+ await updateConfig({
385
+ encryptionEnabled: newMethod !== 'none',
386
+ encryptionMethod: newMethod === 'none' ? undefined : newMethod,
387
+ sshKeyPath: newSshKeyPath,
388
+ });
389
+ // Sync to remote
390
+ const syncSpinner = ora('Syncing to remote...').start();
391
+ try {
392
+ await sync();
393
+ syncSpinner.succeed('Synced to remote');
394
+ }
395
+ catch {
396
+ syncSpinner.warn('Could not sync to remote (will sync later)');
397
+ }
398
+ console.log();
399
+ success('Encryption key rotated successfully!');
400
+ if (newMethod === 'password') {
401
+ console.log();
402
+ warning('Remember your password! It cannot be recovered.');
403
+ warning('If you forget it, all encrypted data will be permanently lost.');
404
+ }
405
+ }
406
+ //# sourceMappingURL=rotate.js.map