@fentz26/envcp 1.0.3 → 1.0.5
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 +12 -5
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +12 -2
- package/dist/adapters/base.js.map +1 -1
- package/dist/cli/index.js +345 -106
- package/dist/cli/index.js.map +1 -1
- package/dist/config/manager.d.ts +5 -5
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +204 -44
- package/dist/config/manager.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -10,6 +10,12 @@ import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, cre
|
|
|
10
10
|
async function withSession(fn) {
|
|
11
11
|
const projectPath = process.cwd();
|
|
12
12
|
const config = await loadConfig(projectPath);
|
|
13
|
+
// Passwordless mode: no session, no password
|
|
14
|
+
if (config.encryption?.enabled === false) {
|
|
15
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), false);
|
|
16
|
+
await fn(storage, '', config, projectPath);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
13
19
|
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
14
20
|
await sessionManager.init();
|
|
15
21
|
let session = await sessionManager.load();
|
|
@@ -41,111 +47,139 @@ program
|
|
|
41
47
|
.command('init')
|
|
42
48
|
.description('Initialize EnvCP in the current project')
|
|
43
49
|
.option('-p, --project <name>', 'Project name')
|
|
44
|
-
.option('-
|
|
50
|
+
.option('--no-encrypt', 'Skip encryption (passwordless mode)')
|
|
45
51
|
.option('--skip-env', 'Skip .env auto-import')
|
|
46
52
|
.option('--skip-mcp', 'Skip MCP auto-registration')
|
|
47
53
|
.action(async (options) => {
|
|
48
54
|
const projectPath = process.cwd();
|
|
49
55
|
const projectName = options.project || path.basename(projectPath);
|
|
50
56
|
console.log(chalk.blue('Initializing EnvCP...'));
|
|
57
|
+
console.log('');
|
|
51
58
|
const config = await initConfig(projectPath, projectName);
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
// Single security question (or skip if --no-encrypt)
|
|
60
|
+
let securityChoice;
|
|
61
|
+
if (options.encrypt === false) {
|
|
62
|
+
securityChoice = 'none';
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const { mode } = await inquirer.prompt([
|
|
66
|
+
{
|
|
67
|
+
type: 'list',
|
|
68
|
+
name: 'mode',
|
|
69
|
+
message: 'How would you like to secure your variables?',
|
|
70
|
+
choices: [
|
|
71
|
+
{ name: 'No encryption (fastest setup, for local dev)', value: 'none' },
|
|
72
|
+
{ name: 'Encrypted with recovery key (recommended)', value: 'recoverable' },
|
|
73
|
+
{ name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
|
|
74
|
+
],
|
|
75
|
+
default: 'recoverable',
|
|
76
|
+
}
|
|
77
|
+
]);
|
|
78
|
+
securityChoice = mode;
|
|
79
|
+
}
|
|
80
|
+
// Apply security choice to config
|
|
81
|
+
if (securityChoice === 'none') {
|
|
82
|
+
config.encryption = { enabled: false };
|
|
83
|
+
config.storage.encrypted = false;
|
|
84
|
+
config.security = { mode: 'recoverable', recovery_file: '.envcp/.recovery' };
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
config.encryption = { enabled: true };
|
|
88
|
+
config.storage.encrypted = true;
|
|
89
|
+
config.security = { mode: securityChoice, recovery_file: '.envcp/.recovery' };
|
|
90
|
+
}
|
|
91
|
+
// For encrypted modes: get password now
|
|
92
|
+
let pwd = '';
|
|
93
|
+
if (securityChoice !== 'none') {
|
|
94
|
+
const { password } = await inquirer.prompt([
|
|
95
|
+
{ type: 'password', name: 'password', message: 'Set encryption password:', mask: '*' }
|
|
96
|
+
]);
|
|
97
|
+
const { confirm } = await inquirer.prompt([
|
|
98
|
+
{ type: 'password', name: 'confirm', message: 'Confirm password:', mask: '*' }
|
|
99
|
+
]);
|
|
100
|
+
if (password !== confirm) {
|
|
101
|
+
console.log(chalk.red('Passwords do not match. Aborting.'));
|
|
102
|
+
return;
|
|
63
103
|
}
|
|
64
|
-
|
|
65
|
-
|
|
104
|
+
pwd = password;
|
|
105
|
+
}
|
|
66
106
|
await saveConfig(config, projectPath);
|
|
67
|
-
|
|
107
|
+
const modeLabel = securityChoice === 'none' ? 'no encryption' : securityChoice;
|
|
108
|
+
console.log(chalk.green('EnvCP initialized!'));
|
|
68
109
|
console.log(chalk.gray(` Project: ${config.project}`));
|
|
69
|
-
console.log(chalk.gray(`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Auto-import .env file
|
|
110
|
+
console.log(chalk.gray(` Security: ${modeLabel}`));
|
|
111
|
+
if (securityChoice !== 'none') {
|
|
112
|
+
console.log(chalk.gray(` Session timeout: ${config.session?.timeout_minutes || 30} minutes`));
|
|
113
|
+
}
|
|
114
|
+
// Auto-import .env
|
|
75
115
|
if (!options.skipEnv) {
|
|
76
116
|
const envPath = path.join(projectPath, '.env');
|
|
77
117
|
if (await fs.pathExists(envPath)) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
const envContent = await fs.readFile(envPath, 'utf8');
|
|
119
|
+
const vars = parseEnvFile(envContent);
|
|
120
|
+
const count = Object.keys(vars).length;
|
|
121
|
+
if (count > 0) {
|
|
122
|
+
const storage = new StorageManager(path.join(projectPath, config.storage.path), config.storage.encrypted);
|
|
123
|
+
if (pwd)
|
|
124
|
+
storage.setPassword(pwd);
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
127
|
+
await storage.set(name, {
|
|
128
|
+
name, value,
|
|
129
|
+
encrypted: config.storage.encrypted,
|
|
130
|
+
created: now, updated: now,
|
|
131
|
+
sync_to_env: true,
|
|
132
|
+
});
|
|
90
133
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
}
|
|
134
|
+
// Create session for encrypted mode
|
|
135
|
+
if (pwd) {
|
|
136
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
137
|
+
await sessionManager.init();
|
|
138
|
+
await sessionManager.create(pwd);
|
|
131
139
|
}
|
|
140
|
+
console.log(chalk.green(` Imported ${count} variables from .env`));
|
|
141
|
+
console.log(chalk.gray(` Variables: ${Object.keys(vars).join(', ')}`));
|
|
132
142
|
}
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
|
-
//
|
|
145
|
+
// Generate recovery key for encrypted recoverable mode
|
|
146
|
+
if (securityChoice === 'recoverable' && pwd) {
|
|
147
|
+
const recoveryKey = generateRecoveryKey();
|
|
148
|
+
const recoveryData = createRecoveryData(pwd, recoveryKey);
|
|
149
|
+
const recoveryPath = path.join(projectPath, config.security.recovery_file);
|
|
150
|
+
await fs.writeFile(recoveryPath, recoveryData, 'utf8');
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(chalk.yellow.bold(' RECOVERY KEY (save this somewhere safe!):'));
|
|
153
|
+
console.log(chalk.yellow.bold(` ${recoveryKey}`));
|
|
154
|
+
console.log(chalk.gray(' This key is shown ONCE. If you lose it, you cannot recover your password.'));
|
|
155
|
+
}
|
|
156
|
+
// Auto-register MCP in all detected tools
|
|
136
157
|
if (!options.skipMcp) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
158
|
+
const result = await registerMcpConfig(projectPath);
|
|
159
|
+
console.log('');
|
|
160
|
+
if (result.registered.length > 0) {
|
|
161
|
+
console.log(chalk.green(' MCP registered:'));
|
|
162
|
+
for (const name of result.registered) {
|
|
163
|
+
console.log(chalk.gray(` + ${name}`));
|
|
142
164
|
}
|
|
143
165
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
if (result.alreadyConfigured.length > 0) {
|
|
167
|
+
for (const name of result.alreadyConfigured) {
|
|
168
|
+
console.log(chalk.gray(` = ${name} (already configured)`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (result.manual.length > 0) {
|
|
172
|
+
console.log(chalk.gray(' Manual setup needed:'));
|
|
173
|
+
for (const name of result.manual) {
|
|
174
|
+
console.log(chalk.gray(` ? ${name}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (result.registered.length === 0 && result.alreadyConfigured.length === 0) {
|
|
178
|
+
console.log(chalk.gray(' No AI tools detected for auto-registration'));
|
|
147
179
|
}
|
|
148
180
|
}
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.green('Done! Your AI tools can now use EnvCP.'));
|
|
149
183
|
});
|
|
150
184
|
program
|
|
151
185
|
.command('unlock')
|
|
@@ -470,7 +504,8 @@ program
|
|
|
470
504
|
program
|
|
471
505
|
.command('sync')
|
|
472
506
|
.description('Sync variables to .env file')
|
|
473
|
-
.
|
|
507
|
+
.option('--dry-run', 'Preview changes without writing')
|
|
508
|
+
.action(async (options) => {
|
|
474
509
|
await withSession(async (storage, _password, config, projectPath) => {
|
|
475
510
|
if (!config.sync.enabled) {
|
|
476
511
|
console.log(chalk.yellow('Sync is disabled in configuration'));
|
|
@@ -486,6 +521,49 @@ program
|
|
|
486
521
|
const val = needsQuoting ? `"${variable.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : variable.value;
|
|
487
522
|
lines.push(`${name}=${val}`);
|
|
488
523
|
}
|
|
524
|
+
if (options.dryRun) {
|
|
525
|
+
const envPath = path.join(projectPath, config.sync.target);
|
|
526
|
+
const existing = {};
|
|
527
|
+
if (await fs.pathExists(envPath)) {
|
|
528
|
+
const content = await fs.readFile(envPath, 'utf8');
|
|
529
|
+
Object.assign(existing, parseEnvFile(content));
|
|
530
|
+
}
|
|
531
|
+
const newVars = [];
|
|
532
|
+
const updated = [];
|
|
533
|
+
const removed = [];
|
|
534
|
+
for (const [name, variable] of Object.entries(variables)) {
|
|
535
|
+
if (name in existing) {
|
|
536
|
+
if (existing[name] !== variable.value)
|
|
537
|
+
updated.push(name);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
newVars.push(name);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const storeNames = new Set(Object.keys(variables));
|
|
544
|
+
for (const name of Object.keys(existing)) {
|
|
545
|
+
if (!storeNames.has(name))
|
|
546
|
+
removed.push(name);
|
|
547
|
+
}
|
|
548
|
+
console.log(chalk.blue(`Dry run: sync to ${config.sync.target}\n`));
|
|
549
|
+
if (newVars.length > 0) {
|
|
550
|
+
for (const n of newVars)
|
|
551
|
+
console.log(chalk.green(` + ${n} = ${maskValue(variables[n].value)}`));
|
|
552
|
+
}
|
|
553
|
+
if (updated.length > 0) {
|
|
554
|
+
for (const n of updated)
|
|
555
|
+
console.log(chalk.yellow(` ~ ${n} = ${maskValue(variables[n].value)}`));
|
|
556
|
+
}
|
|
557
|
+
if (removed.length > 0) {
|
|
558
|
+
for (const n of removed)
|
|
559
|
+
console.log(chalk.red(` - ${n}`));
|
|
560
|
+
}
|
|
561
|
+
if (newVars.length === 0 && updated.length === 0 && removed.length === 0) {
|
|
562
|
+
console.log(chalk.gray(' No changes'));
|
|
563
|
+
}
|
|
564
|
+
console.log(chalk.gray('\nNo files were modified.'));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
489
567
|
await fs.writeFile(path.join(projectPath, config.sync.target), lines.join('\n'), 'utf8');
|
|
490
568
|
console.log(chalk.green(`Synced ${lines.length} variables to ${config.sync.target}`));
|
|
491
569
|
});
|
|
@@ -501,33 +579,50 @@ program
|
|
|
501
579
|
.action(async (options) => {
|
|
502
580
|
const projectPath = process.cwd();
|
|
503
581
|
const config = await loadConfig(projectPath);
|
|
504
|
-
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
505
|
-
await sessionManager.init();
|
|
506
|
-
let session = await sessionManager.load();
|
|
507
|
-
let password = options.password;
|
|
508
|
-
if (!session && !password) {
|
|
509
|
-
const answer = await inquirer.prompt([
|
|
510
|
-
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
511
|
-
]);
|
|
512
|
-
password = answer.password;
|
|
513
|
-
const validation = validatePassword(password, config.password || {});
|
|
514
|
-
if (!validation.valid) {
|
|
515
|
-
console.log(chalk.red(validation.error));
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
session = await sessionManager.create(password);
|
|
519
|
-
}
|
|
520
|
-
password = sessionManager.getPassword() || password;
|
|
521
582
|
const mode = options.mode;
|
|
522
583
|
const port = parseInt(options.port, 10);
|
|
523
584
|
const host = options.host;
|
|
524
585
|
const apiKey = options.apiKey;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
586
|
+
let password = options.password || '';
|
|
587
|
+
// Passwordless mode: skip all session/password logic
|
|
588
|
+
if (config.encryption?.enabled === false) {
|
|
589
|
+
if (mode === 'mcp') {
|
|
590
|
+
const { EnvCPServer } = await import('../mcp/server.js');
|
|
591
|
+
const server = new EnvCPServer(config, projectPath);
|
|
592
|
+
await server.start();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
// Encrypted mode: need password
|
|
598
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
599
|
+
await sessionManager.init();
|
|
600
|
+
let session = await sessionManager.load();
|
|
601
|
+
if (!session && !password) {
|
|
602
|
+
// MCP mode uses stdio — can't prompt interactively
|
|
603
|
+
if (mode === 'mcp') {
|
|
604
|
+
process.stderr.write('Error: No active session. Run `envcp unlock` first, or use --password flag.\n');
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
const answer = await inquirer.prompt([
|
|
608
|
+
{ type: 'password', name: 'password', message: 'Enter password:', mask: '*' }
|
|
609
|
+
]);
|
|
610
|
+
password = answer.password;
|
|
611
|
+
const validation = validatePassword(password, config.password || {});
|
|
612
|
+
if (!validation.valid) {
|
|
613
|
+
console.log(chalk.red(validation.error));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
session = await sessionManager.create(password);
|
|
617
|
+
}
|
|
618
|
+
password = sessionManager.getPassword() || password;
|
|
619
|
+
// MCP mode uses stdio
|
|
620
|
+
if (mode === 'mcp') {
|
|
621
|
+
const { EnvCPServer } = await import('../mcp/server.js');
|
|
622
|
+
const server = new EnvCPServer(config, projectPath, password);
|
|
623
|
+
await server.start();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
531
626
|
}
|
|
532
627
|
// HTTP-based modes
|
|
533
628
|
const { UnifiedServer } = await import('../server/unified.js');
|
|
@@ -645,6 +740,7 @@ program
|
|
|
645
740
|
.command('import <file>')
|
|
646
741
|
.description('Import variables from an encrypted export file')
|
|
647
742
|
.option('--merge', 'Merge with existing variables (default: replace)')
|
|
743
|
+
.option('--dry-run', 'Preview what would be imported without writing')
|
|
648
744
|
.action(async (file, options) => {
|
|
649
745
|
await withSession(async (storage) => {
|
|
650
746
|
if (!await fs.pathExists(file)) {
|
|
@@ -678,6 +774,42 @@ program
|
|
|
678
774
|
console.log(chalk.gray(` Exported: ${meta.timestamp}`));
|
|
679
775
|
console.log(chalk.gray(` Variables: ${meta.count || Object.keys(variables).length}`));
|
|
680
776
|
}
|
|
777
|
+
if (options.dryRun) {
|
|
778
|
+
const current = await storage.load();
|
|
779
|
+
const importNames = Object.keys(variables);
|
|
780
|
+
console.log(chalk.blue(`\nDry run: import ${options.merge ? '(merge)' : '(replace)'}\n`));
|
|
781
|
+
const newVars = [];
|
|
782
|
+
const updated = [];
|
|
783
|
+
for (const name of importNames) {
|
|
784
|
+
if (name in current) {
|
|
785
|
+
if (current[name].value !== variables[name].value)
|
|
786
|
+
updated.push(name);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
newVars.push(name);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (!options.merge) {
|
|
793
|
+
const removed = Object.keys(current).filter(n => !importNames.includes(n));
|
|
794
|
+
if (removed.length > 0) {
|
|
795
|
+
for (const n of removed)
|
|
796
|
+
console.log(chalk.red(` - ${n} (will be removed)`));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (newVars.length > 0) {
|
|
800
|
+
for (const n of newVars)
|
|
801
|
+
console.log(chalk.green(` + ${n} = ${maskValue(variables[n].value)}`));
|
|
802
|
+
}
|
|
803
|
+
if (updated.length > 0) {
|
|
804
|
+
for (const n of updated)
|
|
805
|
+
console.log(chalk.yellow(` ~ ${n} = ${maskValue(variables[n].value)}`));
|
|
806
|
+
}
|
|
807
|
+
if (newVars.length === 0 && updated.length === 0) {
|
|
808
|
+
console.log(chalk.gray(' No changes'));
|
|
809
|
+
}
|
|
810
|
+
console.log(chalk.gray('\nNo files were modified.'));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
681
813
|
const { confirm } = await inquirer.prompt([
|
|
682
814
|
{ type: 'confirm', name: 'confirm', message: options.merge ? 'Merge into current store?' : 'Replace current store?', default: false }
|
|
683
815
|
]);
|
|
@@ -781,5 +913,112 @@ program
|
|
|
781
913
|
}
|
|
782
914
|
});
|
|
783
915
|
});
|
|
916
|
+
program
|
|
917
|
+
.command('doctor')
|
|
918
|
+
.description('Diagnose common issues and check system health')
|
|
919
|
+
.action(async () => {
|
|
920
|
+
const projectPath = process.cwd();
|
|
921
|
+
const checks = [];
|
|
922
|
+
// 1. Config check
|
|
923
|
+
try {
|
|
924
|
+
const config = await loadConfig(projectPath);
|
|
925
|
+
checks.push({ name: 'Config', status: 'pass', detail: `Loaded (project: ${config.project || 'unnamed'})` });
|
|
926
|
+
// 2. Encryption mode
|
|
927
|
+
const encrypted = config.encryption?.enabled !== false;
|
|
928
|
+
checks.push({ name: 'Encryption', status: 'pass', detail: encrypted ? `Enabled (${config.storage.algorithm})` : 'Disabled (passwordless)' });
|
|
929
|
+
// 3. Security mode
|
|
930
|
+
checks.push({ name: 'Security mode', status: 'pass', detail: config.security?.mode || 'recoverable' });
|
|
931
|
+
// 4. Store file
|
|
932
|
+
const storePath = path.join(projectPath, config.storage.path);
|
|
933
|
+
if (await fs.pathExists(storePath)) {
|
|
934
|
+
const stat = await fs.stat(storePath);
|
|
935
|
+
checks.push({ name: 'Store file', status: 'pass', detail: `Exists (${stat.size} bytes)` });
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
checks.push({ name: 'Store file', status: 'warn', detail: 'Not found (no variables stored yet)' });
|
|
939
|
+
}
|
|
940
|
+
// 5. Session status
|
|
941
|
+
if (encrypted) {
|
|
942
|
+
const sessionManager = new SessionManager(path.join(projectPath, config.session?.path || '.envcp/.session'), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
943
|
+
await sessionManager.init();
|
|
944
|
+
const session = await sessionManager.load();
|
|
945
|
+
if (session) {
|
|
946
|
+
const remaining = sessionManager.getRemainingTime();
|
|
947
|
+
checks.push({ name: 'Session', status: 'pass', detail: `Active (${remaining}min remaining)` });
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
checks.push({ name: 'Session', status: 'warn', detail: 'No active session — run `envcp unlock`' });
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
checks.push({ name: 'Session', status: 'pass', detail: 'Not needed (passwordless mode)' });
|
|
955
|
+
}
|
|
956
|
+
// 6. Recovery file
|
|
957
|
+
if (config.security?.mode === 'recoverable') {
|
|
958
|
+
const recoveryPath = path.join(projectPath, config.security.recovery_file || '.envcp/.recovery');
|
|
959
|
+
if (await fs.pathExists(recoveryPath)) {
|
|
960
|
+
checks.push({ name: 'Recovery file', status: 'pass', detail: 'Present' });
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
checks.push({ name: 'Recovery file', status: 'warn', detail: 'Missing — password recovery will not work' });
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
else if (config.security?.mode === 'hard-lock') {
|
|
967
|
+
checks.push({ name: 'Recovery file', status: 'pass', detail: 'N/A (hard-lock mode)' });
|
|
968
|
+
}
|
|
969
|
+
// 7. .envcp directory
|
|
970
|
+
const envcpDir = path.join(projectPath, '.envcp');
|
|
971
|
+
if (await fs.pathExists(envcpDir)) {
|
|
972
|
+
checks.push({ name: '.envcp directory', status: 'pass', detail: 'Exists' });
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
checks.push({ name: '.envcp directory', status: 'fail', detail: 'Missing — run `envcp init`' });
|
|
976
|
+
}
|
|
977
|
+
// 8. .gitignore check
|
|
978
|
+
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
979
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
980
|
+
const gitignore = await fs.readFile(gitignorePath, 'utf8');
|
|
981
|
+
if (gitignore.includes('.envcp/')) {
|
|
982
|
+
checks.push({ name: '.gitignore', status: 'pass', detail: '.envcp/ is ignored' });
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
checks.push({ name: '.gitignore', status: 'warn', detail: '.envcp/ not in .gitignore — secrets may be committed' });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
checks.push({ name: '.gitignore', status: 'warn', detail: 'No .gitignore found' });
|
|
990
|
+
}
|
|
991
|
+
// 9. MCP registration
|
|
992
|
+
const mcpResult = await registerMcpConfig(projectPath);
|
|
993
|
+
const totalMcp = mcpResult.registered.length + mcpResult.alreadyConfigured.length;
|
|
994
|
+
if (totalMcp > 0) {
|
|
995
|
+
checks.push({ name: 'MCP registration', status: 'pass', detail: `${mcpResult.alreadyConfigured.length} tool(s) configured` });
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
checks.push({ name: 'MCP registration', status: 'warn', detail: 'No AI tools detected' });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
catch (error) {
|
|
1002
|
+
checks.push({ name: 'Config', status: 'fail', detail: `Failed to load: ${error.message}` });
|
|
1003
|
+
}
|
|
1004
|
+
// Print results
|
|
1005
|
+
console.log(chalk.blue('\nEnvCP Doctor\n'));
|
|
1006
|
+
for (const check of checks) {
|
|
1007
|
+
const icon = check.status === 'pass' ? chalk.green('PASS') : check.status === 'warn' ? chalk.yellow('WARN') : chalk.red('FAIL');
|
|
1008
|
+
console.log(` [${icon}] ${check.name}: ${chalk.gray(check.detail)}`);
|
|
1009
|
+
}
|
|
1010
|
+
const fails = checks.filter(c => c.status === 'fail').length;
|
|
1011
|
+
const warns = checks.filter(c => c.status === 'warn').length;
|
|
1012
|
+
console.log('');
|
|
1013
|
+
if (fails > 0) {
|
|
1014
|
+
console.log(chalk.red(`${fails} issue(s) need attention.`));
|
|
1015
|
+
}
|
|
1016
|
+
else if (warns > 0) {
|
|
1017
|
+
console.log(chalk.yellow(`All checks passed with ${warns} warning(s).`));
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
console.log(chalk.green('All checks passed.'));
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
784
1023
|
program.parse();
|
|
785
1024
|
//# sourceMappingURL=index.js.map
|