@fentz26/envcp 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -133
- package/dist/adapters/base.d.ts +1 -2
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +139 -14
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +13 -99
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +13 -99
- package/dist/adapters/openai.js.map +1 -1
- package/dist/adapters/rest.d.ts +1 -0
- package/dist/adapters/rest.d.ts.map +1 -1
- package/dist/adapters/rest.js +16 -13
- package/dist/adapters/rest.js.map +1 -1
- package/dist/cli/index.js +510 -197
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts +6 -0
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +81 -1
- package/dist/config/manager.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -16
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +23 -511
- package/dist/mcp/server.js.map +1 -1
- package/dist/server/unified.d.ts +1 -0
- package/dist/server/unified.d.ts.map +1 -1
- package/dist/server/unified.js +31 -19
- package/dist/server/unified.js.map +1 -1
- package/dist/storage/index.d.ts +12 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +107 -10
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +12 -0
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/http.d.ts +13 -1
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +65 -2
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +8 -3
- package/dist/utils/session.js.map +1 -1
- package/package.json +9 -3
- package/.github/workflows/publish.yml +0 -48
- package/src/adapters/base.ts +0 -411
- package/src/adapters/gemini.ts +0 -314
- package/src/adapters/index.ts +0 -4
- package/src/adapters/openai.ts +0 -324
- package/src/adapters/rest.ts +0 -294
- package/src/cli/index.ts +0 -640
- package/src/cli.ts +0 -2
- package/src/config/manager.ts +0 -134
- package/src/index.ts +0 -4
- package/src/mcp/index.ts +0 -1
- package/src/mcp/server.ts +0 -623
- package/src/server/index.ts +0 -1
- package/src/server/unified.ts +0 -460
- package/src/storage/index.ts +0 -112
- package/src/types.ts +0 -181
- package/src/utils/crypto.ts +0 -100
- package/src/utils/http.ts +0 -45
- package/src/utils/session.ts +0 -141
- package/tsconfig.json +0 -20
package/dist/cli/index.js
CHANGED
|
@@ -3,10 +3,35 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as fs from 'fs-extra';
|
|
6
|
-
import { loadConfig, initConfig } from '../config/manager.js';
|
|
6
|
+
import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig } from '../config/manager.js';
|
|
7
7
|
import { StorageManager } from '../storage/index.js';
|
|
8
8
|
import { SessionManager } from '../utils/session.js';
|
|
9
|
-
import { maskValue, validatePassword } from '../utils/crypto.js';
|
|
9
|
+
import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
|
|
10
|
+
async function withSession(fn) {
|
|
11
|
+
const projectPath = process.cwd();
|
|
12
|
+
const config = await loadConfig(projectPath);
|
|
13
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
14
|
+
await sessionManager.init();
|
|
15
|
+
let session = await sessionManager.load();
|
|
16
|
+
let password = '';
|
|
17
|
+
if (!session) {
|
|
18
|
+
const answer = await inquirer.prompt([
|
|
19
|
+
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
20
|
+
]);
|
|
21
|
+
password = answer.password;
|
|
22
|
+
const validation = validatePassword(password, config.password || {});
|
|
23
|
+
if (!validation.valid) {
|
|
24
|
+
console.log(chalk.red(validation.error));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
session = await sessionManager.create(password);
|
|
28
|
+
}
|
|
29
|
+
password = sessionManager.getPassword() || password;
|
|
30
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
31
|
+
if (password)
|
|
32
|
+
storage.setPassword(password);
|
|
33
|
+
await fn(storage, password, config, projectPath);
|
|
34
|
+
}
|
|
10
35
|
const program = new Command();
|
|
11
36
|
program
|
|
12
37
|
.name('envcp')
|
|
@@ -17,17 +42,110 @@ program
|
|
|
17
42
|
.description('Initialize EnvCP in the current project')
|
|
18
43
|
.option('-p, --project <name>', 'Project name')
|
|
19
44
|
.option('-e, --encrypted', 'Enable encryption', true)
|
|
45
|
+
.option('--skip-env', 'Skip .env auto-import')
|
|
46
|
+
.option('--skip-mcp', 'Skip MCP auto-registration')
|
|
20
47
|
.action(async (options) => {
|
|
21
48
|
const projectPath = process.cwd();
|
|
22
49
|
const projectName = options.project || path.basename(projectPath);
|
|
23
50
|
console.log(chalk.blue('Initializing EnvCP...'));
|
|
24
51
|
const config = await initConfig(projectPath, projectName);
|
|
52
|
+
// Security mode selection
|
|
53
|
+
const { securityMode } = await inquirer.prompt([
|
|
54
|
+
{
|
|
55
|
+
type: 'list',
|
|
56
|
+
name: 'securityMode',
|
|
57
|
+
message: 'Security mode:',
|
|
58
|
+
choices: [
|
|
59
|
+
{ name: 'Recoverable - Generate a recovery key (recommended)', value: 'recoverable' },
|
|
60
|
+
{ name: 'Hard-lock - Lose password = lose everything (maximum security)', value: 'hard-lock' },
|
|
61
|
+
],
|
|
62
|
+
default: 'recoverable',
|
|
63
|
+
}
|
|
64
|
+
]);
|
|
65
|
+
config.security = { mode: securityMode, recovery_file: '.envcp/.recovery' };
|
|
66
|
+
await saveConfig(config, projectPath);
|
|
25
67
|
console.log(chalk.green('EnvCP initialized successfully!'));
|
|
26
68
|
console.log(chalk.gray(` Project: ${config.project}`));
|
|
27
69
|
console.log(chalk.gray(` Storage: ${config.storage.path}`));
|
|
28
70
|
console.log(chalk.gray(` Encrypted: ${config.storage.encrypted}`));
|
|
71
|
+
console.log(chalk.gray(` Security: ${securityMode}`));
|
|
29
72
|
console.log(chalk.gray(` Session timeout: ${config.session?.timeout_minutes || 30} minutes`));
|
|
30
73
|
console.log(chalk.gray(` AI active check: ${config.access?.allow_ai_active_check ? 'enabled' : 'disabled'}`));
|
|
74
|
+
// Auto-import .env file
|
|
75
|
+
if (!options.skipEnv) {
|
|
76
|
+
const envPath = path.join(projectPath, '.env');
|
|
77
|
+
if (await fs.pathExists(envPath)) {
|
|
78
|
+
const { importEnv } = await inquirer.prompt([
|
|
79
|
+
{ type: 'confirm', name: 'importEnv', message: 'Found .env file. Import variables into EnvCP?', default: true }
|
|
80
|
+
]);
|
|
81
|
+
if (importEnv) {
|
|
82
|
+
const { password: pwd } = await inquirer.prompt([
|
|
83
|
+
{ type: 'password', name: 'password', message: 'Set encryption password:', mask: '*' }
|
|
84
|
+
]);
|
|
85
|
+
const { confirm } = await inquirer.prompt([
|
|
86
|
+
{ type: 'password', name: 'confirm', message: 'Confirm password:', mask: '*' }
|
|
87
|
+
]);
|
|
88
|
+
if (pwd !== confirm) {
|
|
89
|
+
console.log(chalk.red('Passwords do not match. Skipping .env import.'));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const envContent = await fs.readFile(envPath, 'utf8');
|
|
93
|
+
const vars = parseEnvFile(envContent);
|
|
94
|
+
const count = Object.keys(vars).length;
|
|
95
|
+
if (count > 0) {
|
|
96
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
97
|
+
storage.setPassword(pwd);
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
100
|
+
await storage.set(name, {
|
|
101
|
+
name,
|
|
102
|
+
value,
|
|
103
|
+
encrypted: config.storage.encrypted,
|
|
104
|
+
created: now,
|
|
105
|
+
updated: now,
|
|
106
|
+
sync_to_env: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Create session so user doesn't have to unlock immediately
|
|
110
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
111
|
+
await sessionManager.init();
|
|
112
|
+
await sessionManager.create(pwd);
|
|
113
|
+
// Generate recovery key if recoverable mode
|
|
114
|
+
if (securityMode === 'recoverable') {
|
|
115
|
+
const recoveryKey = generateRecoveryKey();
|
|
116
|
+
const recoveryData = createRecoveryData(pwd, recoveryKey);
|
|
117
|
+
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
118
|
+
await fs.writeFile(recoveryPath, recoveryData, 'utf8');
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(chalk.yellow.bold(' RECOVERY KEY (save this somewhere safe!):'));
|
|
121
|
+
console.log(chalk.yellow.bold(` ${recoveryKey}`));
|
|
122
|
+
console.log(chalk.gray(' This key is shown ONCE. If you lose it, you cannot recover your password.'));
|
|
123
|
+
console.log('');
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.green(` Imported ${count} variables from .env`));
|
|
126
|
+
console.log(chalk.gray(` Variables: ${Object.keys(vars).join(', ')}`));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(chalk.yellow(' .env file is empty, nothing to import'));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Auto-register MCP config
|
|
136
|
+
if (!options.skipMcp) {
|
|
137
|
+
const registered = await registerMcpConfig(projectPath);
|
|
138
|
+
if (registered.length > 0) {
|
|
139
|
+
console.log(chalk.green(' MCP auto-registered:'));
|
|
140
|
+
for (const name of registered) {
|
|
141
|
+
console.log(chalk.gray(` - ${name}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
console.log(chalk.gray(' No AI tool configs detected for MCP auto-registration'));
|
|
146
|
+
console.log(chalk.gray(' You can manually add EnvCP to your AI tool config later'));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
31
149
|
});
|
|
32
150
|
program
|
|
33
151
|
.command('unlock')
|
|
@@ -66,6 +184,21 @@ program
|
|
|
66
184
|
console.log(chalk.red('Passwords do not match'));
|
|
67
185
|
return;
|
|
68
186
|
}
|
|
187
|
+
// Generate recovery key for new stores in recoverable mode
|
|
188
|
+
if (config.security?.mode === 'recoverable') {
|
|
189
|
+
const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
|
|
190
|
+
if (!await fs.pathExists(recoveryPath)) {
|
|
191
|
+
const recoveryKey = generateRecoveryKey();
|
|
192
|
+
const recoveryData = createRecoveryData(password, recoveryKey);
|
|
193
|
+
await fs.ensureDir(path.dirname(recoveryPath));
|
|
194
|
+
await fs.writeFile(recoveryPath, recoveryData, 'utf8');
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(chalk.yellow.bold('RECOVERY KEY (save this somewhere safe!):'));
|
|
197
|
+
console.log(chalk.yellow.bold(` ${recoveryKey}`));
|
|
198
|
+
console.log(chalk.gray('This key is shown ONCE. If you lose it, you cannot recover your password.'));
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
69
202
|
}
|
|
70
203
|
try {
|
|
71
204
|
await storage.load();
|
|
@@ -142,6 +275,105 @@ program
|
|
|
142
275
|
console.log(chalk.gray(` Remaining: ${sessionManager.getRemainingTime()} minutes`));
|
|
143
276
|
console.log(chalk.gray(` Extensions remaining: ${maxExt - session.extensions}/${maxExt}`));
|
|
144
277
|
});
|
|
278
|
+
program
|
|
279
|
+
.command('recover')
|
|
280
|
+
.description('Recover access using recovery key (reset password)')
|
|
281
|
+
.action(async () => {
|
|
282
|
+
const projectPath = process.cwd();
|
|
283
|
+
const config = await loadConfig(projectPath);
|
|
284
|
+
if (config.security?.mode === 'hard-lock') {
|
|
285
|
+
console.log(chalk.red('Recovery is not available in hard-lock mode.'));
|
|
286
|
+
console.log(chalk.gray('Hard-lock mode means lost password = lost data.'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const recoveryPath = path.join(projectPath, config.security?.recovery_file || '.envcp/.recovery');
|
|
290
|
+
if (!await fs.pathExists(recoveryPath)) {
|
|
291
|
+
console.log(chalk.red('No recovery file found. Recovery is not available.'));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const { recoveryKey } = await inquirer.prompt([
|
|
295
|
+
{ type: 'password', name: 'recoveryKey', message: 'Enter your recovery key:', mask: '*' }
|
|
296
|
+
]);
|
|
297
|
+
const recoveryData = await fs.readFile(recoveryPath, 'utf8');
|
|
298
|
+
let oldPassword;
|
|
299
|
+
try {
|
|
300
|
+
oldPassword = recoverPassword(recoveryData, recoveryKey);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
console.log(chalk.red('Invalid recovery key.'));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Verify old password actually works by loading the store
|
|
307
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
308
|
+
storage.setPassword(oldPassword);
|
|
309
|
+
let variables;
|
|
310
|
+
try {
|
|
311
|
+
variables = await storage.load();
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
console.log(chalk.red('Recovery key decrypted but store could not be loaded. Data may be corrupted.'));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log(chalk.green('Recovery key verified. Store contains ' + Object.keys(variables).length + ' variables.'));
|
|
318
|
+
// Set new password
|
|
319
|
+
const { newPassword } = await inquirer.prompt([
|
|
320
|
+
{ type: 'password', name: 'newPassword', message: 'Set new password:', mask: '*' }
|
|
321
|
+
]);
|
|
322
|
+
const { confirmPassword } = await inquirer.prompt([
|
|
323
|
+
{ type: 'password', name: 'confirmPassword', message: 'Confirm new password:', mask: '*' }
|
|
324
|
+
]);
|
|
325
|
+
if (newPassword !== confirmPassword) {
|
|
326
|
+
console.log(chalk.red('Passwords do not match'));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Re-encrypt store with new password
|
|
330
|
+
storage.invalidateCache();
|
|
331
|
+
storage.setPassword(newPassword);
|
|
332
|
+
await storage.save(variables);
|
|
333
|
+
// Update recovery file with new password
|
|
334
|
+
const newRecoveryKey = generateRecoveryKey();
|
|
335
|
+
const newRecoveryData = createRecoveryData(newPassword, newRecoveryKey);
|
|
336
|
+
await fs.writeFile(recoveryPath, newRecoveryData, 'utf8');
|
|
337
|
+
console.log(chalk.green('Password reset successfully!'));
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(chalk.yellow.bold('NEW RECOVERY KEY (save this somewhere safe!):'));
|
|
340
|
+
console.log(chalk.yellow.bold(` ${newRecoveryKey}`));
|
|
341
|
+
console.log(chalk.gray('Your old recovery key no longer works.'));
|
|
342
|
+
// Create a session with new password
|
|
343
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
344
|
+
await sessionManager.init();
|
|
345
|
+
await sessionManager.create(newPassword);
|
|
346
|
+
console.log(chalk.green('Session unlocked with new password.'));
|
|
347
|
+
});
|
|
348
|
+
program
|
|
349
|
+
.command('verify')
|
|
350
|
+
.description('Verify store integrity and check backups')
|
|
351
|
+
.action(async () => {
|
|
352
|
+
await withSession(async (storage, _password, config, projectPath) => {
|
|
353
|
+
const result = await storage.verify();
|
|
354
|
+
if (result.valid) {
|
|
355
|
+
console.log(chalk.green('Store integrity: OK'));
|
|
356
|
+
console.log(chalk.gray(` Variables: ${result.count}`));
|
|
357
|
+
console.log(chalk.gray(` Backups: ${result.backups}`));
|
|
358
|
+
// Check recovery file
|
|
359
|
+
if (config.security?.mode === 'recoverable') {
|
|
360
|
+
const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
|
|
361
|
+
const hasRecovery = await fs.pathExists(recoveryPath);
|
|
362
|
+
console.log(chalk.gray(` Recovery: ${hasRecovery ? 'available' : 'not found'}`));
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.log(chalk.gray(` Recovery: hard-lock mode (disabled)`));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.log(chalk.red(`Store integrity: FAILED`));
|
|
370
|
+
console.log(chalk.red(` Error: ${result.error}`));
|
|
371
|
+
if (result.backups && result.backups > 0) {
|
|
372
|
+
console.log(chalk.yellow(` ${result.backups} backup(s) available — store may auto-restore on next load`));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
145
377
|
program
|
|
146
378
|
.command('add <name>')
|
|
147
379
|
.description('Add a new environment variable')
|
|
@@ -149,192 +381,114 @@ program
|
|
|
149
381
|
.option('-t, --tags <tags>', 'Tags (comma-separated)')
|
|
150
382
|
.option('-d, --description <desc>', 'Description')
|
|
151
383
|
.action(async (name, options) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
console.log(chalk.red(validation.error));
|
|
166
|
-
return;
|
|
384
|
+
await withSession(async (storage, _password, config) => {
|
|
385
|
+
let value = options.value;
|
|
386
|
+
let tags = [];
|
|
387
|
+
let description = options.description;
|
|
388
|
+
if (!value) {
|
|
389
|
+
const answers = await inquirer.prompt([
|
|
390
|
+
{ type: 'password', name: 'value', message: 'Enter value:', mask: '*' },
|
|
391
|
+
{ type: 'input', name: 'tags', message: 'Tags (comma-separated):' },
|
|
392
|
+
{ type: 'input', name: 'description', message: 'Description:' },
|
|
393
|
+
]);
|
|
394
|
+
value = answers.value;
|
|
395
|
+
tags = answers.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
396
|
+
description = answers.description;
|
|
167
397
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
186
|
-
}
|
|
187
|
-
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
188
|
-
if (password)
|
|
189
|
-
storage.setPassword(password);
|
|
190
|
-
const now = new Date().toISOString();
|
|
191
|
-
const variable = {
|
|
192
|
-
name,
|
|
193
|
-
value,
|
|
194
|
-
encrypted: config.storage.encrypted,
|
|
195
|
-
tags: tags.length > 0 ? tags : undefined,
|
|
196
|
-
description,
|
|
197
|
-
created: now,
|
|
198
|
-
updated: now,
|
|
199
|
-
sync_to_env: true,
|
|
200
|
-
};
|
|
201
|
-
await storage.set(name, variable);
|
|
202
|
-
console.log(chalk.green(`Variable '${name}' added successfully`));
|
|
398
|
+
else if (options.tags) {
|
|
399
|
+
tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
400
|
+
}
|
|
401
|
+
const now = new Date().toISOString();
|
|
402
|
+
const variable = {
|
|
403
|
+
name,
|
|
404
|
+
value,
|
|
405
|
+
encrypted: config.storage.encrypted,
|
|
406
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
407
|
+
description,
|
|
408
|
+
created: now,
|
|
409
|
+
updated: now,
|
|
410
|
+
sync_to_env: true,
|
|
411
|
+
};
|
|
412
|
+
await storage.set(name, variable);
|
|
413
|
+
console.log(chalk.green(`Variable '${name}' added successfully`));
|
|
414
|
+
});
|
|
203
415
|
});
|
|
204
416
|
program
|
|
205
417
|
.command('list')
|
|
206
418
|
.description('List all variables (names only, values hidden)')
|
|
207
419
|
.option('-v, --show-values', 'Show actual values')
|
|
208
420
|
.action(async (options) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
storage.setPassword(password);
|
|
226
|
-
const variables = await storage.load();
|
|
227
|
-
const names = Object.keys(variables);
|
|
228
|
-
if (names.length === 0) {
|
|
229
|
-
console.log(chalk.yellow('No variables found'));
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
console.log(chalk.blue(`\nVariables (${names.length}):\n`));
|
|
233
|
-
for (const name of names) {
|
|
234
|
-
const v = variables[name];
|
|
235
|
-
const value = options.showValues ? v.value : maskValue(v.value);
|
|
236
|
-
const tags = v.tags ? chalk.gray(` [${v.tags.join(', ')}]`) : '';
|
|
237
|
-
console.log(` ${chalk.cyan(name)} = ${value}${tags}`);
|
|
238
|
-
}
|
|
239
|
-
console.log('');
|
|
421
|
+
await withSession(async (storage) => {
|
|
422
|
+
const variables = await storage.load();
|
|
423
|
+
const names = Object.keys(variables);
|
|
424
|
+
if (names.length === 0) {
|
|
425
|
+
console.log(chalk.yellow('No variables found'));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.log(chalk.blue(`\nVariables (${names.length}):\n`));
|
|
429
|
+
for (const name of names) {
|
|
430
|
+
const v = variables[name];
|
|
431
|
+
const value = options.showValues ? v.value : maskValue(v.value);
|
|
432
|
+
const tags = v.tags ? chalk.gray(` [${v.tags.join(', ')}]`) : '';
|
|
433
|
+
console.log(` ${chalk.cyan(name)} = ${value}${tags}`);
|
|
434
|
+
}
|
|
435
|
+
console.log('');
|
|
436
|
+
});
|
|
240
437
|
});
|
|
241
438
|
program
|
|
242
439
|
.command('get <name>')
|
|
243
440
|
.description('Get a variable value')
|
|
244
441
|
.action(async (name) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
password = sessionManager.getPassword() || password;
|
|
259
|
-
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
260
|
-
if (password)
|
|
261
|
-
storage.setPassword(password);
|
|
262
|
-
const variable = await storage.get(name);
|
|
263
|
-
if (!variable) {
|
|
264
|
-
console.log(chalk.red(`Variable '${name}' not found`));
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
console.log(chalk.cyan(name));
|
|
268
|
-
console.log(` Value: ${variable.value}`);
|
|
269
|
-
if (variable.tags)
|
|
270
|
-
console.log(` Tags: ${variable.tags.join(', ')}`);
|
|
271
|
-
if (variable.description)
|
|
272
|
-
console.log(` Description: ${variable.description}`);
|
|
442
|
+
await withSession(async (storage) => {
|
|
443
|
+
const variable = await storage.get(name);
|
|
444
|
+
if (!variable) {
|
|
445
|
+
console.log(chalk.red(`Variable '${name}' not found`));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
console.log(chalk.cyan(name));
|
|
449
|
+
console.log(` Value: ${variable.value}`);
|
|
450
|
+
if (variable.tags)
|
|
451
|
+
console.log(` Tags: ${variable.tags.join(', ')}`);
|
|
452
|
+
if (variable.description)
|
|
453
|
+
console.log(` Description: ${variable.description}`);
|
|
454
|
+
});
|
|
273
455
|
});
|
|
274
456
|
program
|
|
275
457
|
.command('remove <name>')
|
|
276
458
|
.description('Remove a variable')
|
|
277
459
|
.action(async (name) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
]);
|
|
288
|
-
password = answer.password;
|
|
289
|
-
session = await sessionManager.create(password);
|
|
290
|
-
}
|
|
291
|
-
password = sessionManager.getPassword() || password;
|
|
292
|
-
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
293
|
-
if (password)
|
|
294
|
-
storage.setPassword(password);
|
|
295
|
-
const deleted = await storage.delete(name);
|
|
296
|
-
if (deleted) {
|
|
297
|
-
console.log(chalk.green(`Variable '${name}' removed`));
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
console.log(chalk.red(`Variable '${name}' not found`));
|
|
301
|
-
}
|
|
460
|
+
await withSession(async (storage) => {
|
|
461
|
+
const deleted = await storage.delete(name);
|
|
462
|
+
if (deleted) {
|
|
463
|
+
console.log(chalk.green(`Variable '${name}' removed`));
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
console.log(chalk.red(`Variable '${name}' not found`));
|
|
467
|
+
}
|
|
468
|
+
});
|
|
302
469
|
});
|
|
303
470
|
program
|
|
304
471
|
.command('sync')
|
|
305
472
|
.description('Sync variables to .env file')
|
|
306
473
|
.action(async () => {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
326
|
-
if (password)
|
|
327
|
-
storage.setPassword(password);
|
|
328
|
-
const variables = await storage.load();
|
|
329
|
-
const lines = [];
|
|
330
|
-
if (config.sync.header) {
|
|
331
|
-
lines.push(config.sync.header);
|
|
332
|
-
}
|
|
333
|
-
for (const [name, variable] of Object.entries(variables)) {
|
|
334
|
-
lines.push(`${name}=${variable.value}`);
|
|
335
|
-
}
|
|
336
|
-
await fs.writeFile(path.join(projectPath, config.sync.target), lines.join('\n'), 'utf8');
|
|
337
|
-
console.log(chalk.green(`Synced ${lines.length} variables to ${config.sync.target}`));
|
|
474
|
+
await withSession(async (storage, _password, config, projectPath) => {
|
|
475
|
+
if (!config.sync.enabled) {
|
|
476
|
+
console.log(chalk.yellow('Sync is disabled in configuration'));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const variables = await storage.load();
|
|
480
|
+
const lines = [];
|
|
481
|
+
if (config.sync.header) {
|
|
482
|
+
lines.push(config.sync.header);
|
|
483
|
+
}
|
|
484
|
+
for (const [name, variable] of Object.entries(variables)) {
|
|
485
|
+
const needsQuoting = /[\s#"'\\]/.test(variable.value);
|
|
486
|
+
const val = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
|
|
487
|
+
lines.push(`${name}=${val}`);
|
|
488
|
+
}
|
|
489
|
+
await fs.writeFile(path.join(projectPath, config.sync.target), lines.join('\n'), 'utf8');
|
|
490
|
+
console.log(chalk.green(`Synced ${lines.length} variables to ${config.sync.target}`));
|
|
491
|
+
});
|
|
338
492
|
});
|
|
339
493
|
program
|
|
340
494
|
.command('serve')
|
|
@@ -434,39 +588,198 @@ program
|
|
|
434
588
|
.command('export')
|
|
435
589
|
.description('Export variables')
|
|
436
590
|
.option('-f, --format <format>', 'Output format: env, json, yaml', 'env')
|
|
591
|
+
.option('--encrypted', 'Create an encrypted portable export file')
|
|
592
|
+
.option('-o, --output <path>', 'Output file (required for --encrypted)')
|
|
437
593
|
.action(async (options) => {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
{
|
|
594
|
+
await withSession(async (storage, _password, config) => {
|
|
595
|
+
const variables = await storage.load();
|
|
596
|
+
if (options.encrypted) {
|
|
597
|
+
const outputPath = options.output;
|
|
598
|
+
if (!outputPath) {
|
|
599
|
+
console.log(chalk.red('--output <path> is required with --encrypted'));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const { exportPassword } = await inquirer.prompt([
|
|
603
|
+
{ type: 'password', name: 'exportPassword', message: 'Set export password:', mask: '*' }
|
|
604
|
+
]);
|
|
605
|
+
const { confirmExport } = await inquirer.prompt([
|
|
606
|
+
{ type: 'password', name: 'confirmExport', message: 'Confirm export password:', mask: '*' }
|
|
607
|
+
]);
|
|
608
|
+
if (exportPassword !== confirmExport) {
|
|
609
|
+
console.log(chalk.red('Passwords do not match'));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const exportData = JSON.stringify({
|
|
613
|
+
meta: { project: config.project, timestamp: new Date().toISOString(), count: Object.keys(variables).length, version: '1.0' },
|
|
614
|
+
variables,
|
|
615
|
+
}, null, 2);
|
|
616
|
+
const encrypted = encrypt(exportData, exportPassword);
|
|
617
|
+
await fs.writeFile(outputPath, encrypted, 'utf8');
|
|
618
|
+
console.log(chalk.green(`Encrypted export saved to: ${outputPath}`));
|
|
619
|
+
console.log(chalk.gray(` Variables: ${Object.keys(variables).length}`));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
let output;
|
|
623
|
+
switch (options.format) {
|
|
624
|
+
case 'json':
|
|
625
|
+
output = JSON.stringify(variables, null, 2);
|
|
626
|
+
break;
|
|
627
|
+
case 'yaml': {
|
|
628
|
+
const yaml = await import('js-yaml');
|
|
629
|
+
output = yaml.dump(variables);
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
default: {
|
|
633
|
+
const lines = Object.entries(variables).map(([k, v]) => {
|
|
634
|
+
const needsQuoting = /[\s#"'\\]/.test(v.value);
|
|
635
|
+
const val = needsQuoting ? `"${v.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : v.value;
|
|
636
|
+
return `${k}=${val}`;
|
|
637
|
+
});
|
|
638
|
+
output = lines.join('\n');
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
console.log(output);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
program
|
|
645
|
+
.command('import <file>')
|
|
646
|
+
.description('Import variables from an encrypted export file')
|
|
647
|
+
.option('--merge', 'Merge with existing variables (default: replace)')
|
|
648
|
+
.action(async (file, options) => {
|
|
649
|
+
await withSession(async (storage) => {
|
|
650
|
+
if (!await fs.pathExists(file)) {
|
|
651
|
+
console.log(chalk.red(`File not found: ${file}`));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const { importPassword } = await inquirer.prompt([
|
|
655
|
+
{ type: 'password', name: 'importPassword', message: 'Enter export file password:', mask: '*' }
|
|
447
656
|
]);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
657
|
+
const fileContent = await fs.readFile(file, 'utf8');
|
|
658
|
+
let importData;
|
|
659
|
+
try {
|
|
660
|
+
const decrypted = decrypt(fileContent, importPassword);
|
|
661
|
+
importData = JSON.parse(decrypted);
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
console.log(chalk.red('Failed to decrypt. Wrong password or invalid file.'));
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const meta = importData.meta;
|
|
668
|
+
const variables = importData.variables;
|
|
669
|
+
if (!variables || typeof variables !== 'object') {
|
|
670
|
+
console.log(chalk.red('Invalid export format'));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (meta) {
|
|
674
|
+
console.log(chalk.blue('Import info:'));
|
|
675
|
+
if (meta.project)
|
|
676
|
+
console.log(chalk.gray(` From project: ${meta.project}`));
|
|
677
|
+
if (meta.timestamp)
|
|
678
|
+
console.log(chalk.gray(` Exported: ${meta.timestamp}`));
|
|
679
|
+
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
680
|
+
}
|
|
681
|
+
const { confirm } = await inquirer.prompt([
|
|
682
|
+
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge into current store?' : 'Replace current store?', default: false }
|
|
683
|
+
]);
|
|
684
|
+
if (!confirm) {
|
|
685
|
+
console.log(chalk.yellow('Import cancelled'));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (options.merge) {
|
|
689
|
+
const current = await storage.load();
|
|
690
|
+
await storage.save({ ...current, ...variables });
|
|
691
|
+
console.log(chalk.green(`Merged ${Object.keys(variables).length} variables`));
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
await storage.save(variables);
|
|
695
|
+
console.log(chalk.green(`Imported ${Object.keys(variables).length} variables`));
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
program
|
|
700
|
+
.command('backup')
|
|
701
|
+
.description('Create an encrypted backup of all variables')
|
|
702
|
+
.option('-o, --output <path>', 'Output file path')
|
|
703
|
+
.action(async (options) => {
|
|
704
|
+
await withSession(async (storage, password, config, projectPath) => {
|
|
705
|
+
const variables = await storage.load();
|
|
706
|
+
const count = Object.keys(variables).length;
|
|
707
|
+
if (count === 0) {
|
|
708
|
+
console.log(chalk.yellow('No variables to backup'));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
712
|
+
const defaultPath = path.join(projectPath, `.envcp/backup-${timestamp}.enc`);
|
|
713
|
+
const outputPath = options.output || defaultPath;
|
|
714
|
+
const backupData = JSON.stringify({
|
|
715
|
+
meta: {
|
|
716
|
+
project: config.project,
|
|
717
|
+
timestamp: new Date().toISOString(),
|
|
718
|
+
count,
|
|
719
|
+
version: '1.0',
|
|
720
|
+
},
|
|
721
|
+
variables,
|
|
722
|
+
}, null, 2);
|
|
723
|
+
const encrypted = encrypt(backupData, password);
|
|
724
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
725
|
+
await fs.writeFile(outputPath, encrypted, 'utf8');
|
|
726
|
+
console.log(chalk.green(`Backup created: ${outputPath}`));
|
|
727
|
+
console.log(chalk.gray(` Variables: ${count}`));
|
|
728
|
+
console.log(chalk.gray(` Encrypted: yes`));
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
program
|
|
732
|
+
.command('restore <file>')
|
|
733
|
+
.description('Restore variables from an encrypted backup')
|
|
734
|
+
.option('--merge', 'Merge with existing variables (default: replace)')
|
|
735
|
+
.action(async (file, options) => {
|
|
736
|
+
await withSession(async (storage, password) => {
|
|
737
|
+
if (!await fs.pathExists(file)) {
|
|
738
|
+
console.log(chalk.red(`Backup file not found: ${file}`));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const encrypted = await fs.readFile(file, 'utf8');
|
|
742
|
+
let backupData;
|
|
743
|
+
try {
|
|
744
|
+
const decrypted = decrypt(encrypted, password);
|
|
745
|
+
backupData = JSON.parse(decrypted);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
console.log(chalk.red('Failed to decrypt backup. Wrong password or corrupted file.'));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const meta = backupData.meta;
|
|
752
|
+
const variables = backupData.variables;
|
|
753
|
+
if (!variables || typeof variables !== 'object') {
|
|
754
|
+
console.log(chalk.red('Invalid backup format'));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (meta) {
|
|
758
|
+
console.log(chalk.blue('Backup info:'));
|
|
759
|
+
if (meta.project)
|
|
760
|
+
console.log(chalk.gray(` Project: ${meta.project}`));
|
|
761
|
+
if (meta.timestamp)
|
|
762
|
+
console.log(chalk.gray(` Created: ${meta.timestamp}`));
|
|
763
|
+
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
764
|
+
}
|
|
765
|
+
const { confirm } = await inquirer.prompt([
|
|
766
|
+
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge backup into current store?' : 'Replace current store with backup?', default: false }
|
|
767
|
+
]);
|
|
768
|
+
if (!confirm) {
|
|
769
|
+
console.log(chalk.yellow('Restore cancelled'));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (options.merge) {
|
|
773
|
+
const current = await storage.load();
|
|
774
|
+
const merged = { ...current, ...variables };
|
|
775
|
+
await storage.save(merged);
|
|
776
|
+
console.log(chalk.green(`Merged ${Object.keys(variables).length} variables from backup`));
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
await storage.save(variables);
|
|
780
|
+
console.log(chalk.green(`Restored ${Object.keys(variables).length} variables from backup`));
|
|
781
|
+
}
|
|
782
|
+
});
|
|
470
783
|
});
|
|
471
784
|
program.parse();
|
|
472
785
|
//# sourceMappingURL=index.js.map
|