@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/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('-e, --encrypted', 'Enable encryption', true)
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
- // 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',
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
- config.security = { mode: securityMode, recovery_file: '.envcp/.recovery' };
104
+ pwd = password;
105
+ }
66
106
  await saveConfig(config, projectPath);
67
- console.log(chalk.green('EnvCP initialized successfully!'));
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(` Storage: ${config.storage.path}`));
70
- console.log(chalk.gray(` Encrypted: ${config.storage.encrypted}`));
71
- console.log(chalk.gray(` Security: ${securityMode}`));
72
- console.log(chalk.gray(` Session timeout: ${config.session?.timeout_minutes || 30} minutes`));
73
- console.log(chalk.gray(` AI active check: ${config.access?.allow_ai_active_check ? 'enabled' : 'disabled'}`));
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 { 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.'));
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
- 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
- }
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
- // Auto-register MCP config
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 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}`));
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
- 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'));
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
- .action(async () => {
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
- // MCP mode uses stdio
526
- if (mode === 'mcp') {
527
- const { EnvCPServer } = await import('../mcp/server.js');
528
- const server = new EnvCPServer(config, projectPath, password);
529
- await server.start();
530
- return;
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