@claude-agent/envcheck 1.3.0 → 1.5.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.
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@claude-agent/envcheck.svg)](https://www.npmjs.com/package/@claude-agent/envcheck)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- > Validate .env files, compare with .env.example, find missing or empty variables.
7
+ > Validate .env files, compare with .env.example, find missing or empty variables. **Now with monorepo support!**
8
8
 
9
- Never deploy with missing environment variables again.
9
+ Never deploy with missing environment variables again. Works across entire monorepos with a single command.
10
10
 
11
11
  **Built autonomously by [Claude](https://claude.ai)** - an AI assistant by Anthropic.
12
12
 
@@ -34,6 +34,9 @@ envcheck .env.production
34
34
  # Require specific variables
35
35
  envcheck -r "DATABASE_URL,API_KEY"
36
36
 
37
+ # Scan entire monorepo
38
+ envcheck monorepo
39
+
37
40
  # Compare two files
38
41
  envcheck compare .env .env.staging
39
42
 
@@ -101,6 +104,104 @@ envcheck list .env -j
101
104
  envcheck get .env DATABASE_URL
102
105
  ```
103
106
 
107
+ ### Monorepo Command
108
+
109
+ Scan all apps and packages in a monorepo with a single command:
110
+
111
+ ```bash
112
+ # Scan from current directory
113
+ envcheck monorepo
114
+
115
+ # Scan specific directory
116
+ envcheck monorepo ./my-monorepo
117
+
118
+ # With verbose output (shows all issues per app)
119
+ envcheck monorepo --verbose
120
+
121
+ # JSON output for CI/CD
122
+ envcheck monorepo --json
123
+
124
+ # Enable secret detection
125
+ envcheck monorepo --secrets
126
+ ```
127
+
128
+ ## Monorepo Support
129
+
130
+ envcheck can scan entire monorepos, validating environment variables across all apps and packages.
131
+
132
+ ### Supported Structures
133
+
134
+ envcheck automatically detects these common monorepo patterns:
135
+
136
+ ```
137
+ my-monorepo/
138
+ ├── apps/
139
+ │ ├── web/ # ✓ Scanned
140
+ │ │ ├── .env
141
+ │ │ └── .env.example
142
+ │ └── api/ # ✓ Scanned
143
+ │ ├── .env
144
+ │ └── .env.example
145
+ ├── packages/
146
+ │ ├── shared/ # ○ Skipped (no .env.example)
147
+ │ └── utils/ # ✓ Scanned
148
+ │ ├── .env
149
+ │ └── .env.example
150
+ └── .env.example # ✓ Root included if exists
151
+ ```
152
+
153
+ Supported directories: `apps/`, `packages/`, `workspaces/`, `services/`, `libs/`
154
+
155
+ ### Example Output
156
+
157
+ ```
158
+ $ envcheck monorepo
159
+
160
+ Monorepo Environment Check
161
+ Root: /path/to/monorepo
162
+
163
+ ✓ apps/web: passed
164
+ ✗ apps/api: 1 error(s)
165
+ ○ packages/shared: skipped (No .env.example found)
166
+ ✓ packages/utils: passed
167
+
168
+ Summary: 4 apps scanned
169
+ ✓ 2 passed
170
+ ✗ 1 failed
171
+ ○ 1 skipped
172
+
173
+ ✗ 1 error(s), 0 warning(s)
174
+ ```
175
+
176
+ ### Consistency Checks
177
+
178
+ envcheck can detect inconsistencies across apps:
179
+
180
+ - **Shared variables** - Track which variables appear in multiple apps
181
+ - **Type mismatches** - Detect when the same variable has different type hints in different apps
182
+
183
+ ```javascript
184
+ const { scanMonorepo } = require('@claude-agent/envcheck');
185
+
186
+ const result = scanMonorepo('.', { checkConsistency: true });
187
+
188
+ console.log(result.consistency.sharedVars);
189
+ // { API_URL: ['apps/web', 'apps/api'] }
190
+
191
+ console.log(result.consistency.mismatches);
192
+ // [{ variable: 'API_URL', issue: 'type_mismatch', details: [...] }]
193
+ ```
194
+
195
+ ### GitHub Action for Monorepos
196
+
197
+ ```yaml
198
+ - name: Validate all environment files
199
+ uses: claude-agent-tools/envcheck@v1
200
+ with:
201
+ monorepo: 'true'
202
+ strict: 'true'
203
+ ```
204
+
104
205
  ## API Usage
105
206
 
106
207
  ```javascript
@@ -269,6 +370,55 @@ ADMIN_EMAIL=admin@example.com
269
370
  | `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` |
270
371
  | `string`/`str` | Any string (no validation) | anything |
271
372
 
373
+ ## Secret Detection
374
+
375
+ envcheck can warn you if your `.env` file contains values that look like real secrets:
376
+
377
+ ```javascript
378
+ const result = check('.env', {
379
+ examplePath: '.env.example',
380
+ detectSecrets: true // Warns about potential secrets
381
+ });
382
+ ```
383
+
384
+ ### Detected Secret Patterns
385
+
386
+ - AWS Access Keys (`AKIA...`)
387
+ - GitHub Tokens (`ghp_...`, `gho_...`, etc.)
388
+ - Stripe Keys (`sk_live_...`, `rk_live_...`)
389
+ - Private Keys (`-----BEGIN PRIVATE KEY-----`)
390
+ - Slack Tokens (`xox...`)
391
+ - Twilio Credentials
392
+ - SendGrid API Keys
393
+ - Google API Keys
394
+ - High-entropy hex strings (with sensitive key names)
395
+
396
+ ### Placeholder Detection
397
+
398
+ envcheck won't warn about obvious placeholders like:
399
+ - `your-api-key`, `my-secret`
400
+ - `changeme`, `placeholder`
401
+ - `xxx`, `...`
402
+ - `example`, `test`, `dummy`
403
+
404
+ ### API Usage with Secrets
405
+
406
+ ```javascript
407
+ const { check, validate, detectSecret } = require('@claude-agent/envcheck');
408
+
409
+ // Enable secret detection
410
+ const result = check('.env', {
411
+ examplePath: '.env.example',
412
+ detectSecrets: true // Warns about potential secrets
413
+ });
414
+
415
+ // Check a single value
416
+ const secret = detectSecret('API_KEY', 'sk_live_abc123...');
417
+ if (secret) {
418
+ console.log(`Warning: ${secret.description} detected`);
419
+ }
420
+ ```
421
+
272
422
  ### API Usage with Types
273
423
 
274
424
  ```javascript
@@ -308,21 +458,24 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
308
458
  - **Auto-detection** - Finds .env.example automatically
309
459
  - **CI-friendly** - Exit codes and JSON output
310
460
  - **Comprehensive** - Parse, validate, compare, generate
311
- - **Well-tested** - 46 tests covering edge cases
312
-
313
- ## vs. dotenv-safe / envalid
314
-
315
- | Feature | envcheck | dotenv-safe | envalid |
316
- |---------|----------|-------------|---------|
317
- | Validates presence | ✅ | ✅ | ✅ |
318
- | Based on .env.example | ✅ | ✅ | ❌ (schema) |
319
- | **Static validation** | ✅ | ❌ | ❌ |
320
- | **CI/CD integration** | ✅ GitHub Action | ❌ | ❌ |
321
- | **Pre-commit hook** | ✅ | ❌ | ❌ |
322
- | Type validation | ✅ (static) | ❌ | (runtime) |
323
- | Zero dependencies | ✅ | ❌ | ❌ |
324
-
325
- **Key difference:** envcheck validates *before* deployment (shift-left), while dotenv-safe and envalid validate at runtime when your app starts. Catch missing env vars and type errors in CI, not in production.
461
+ - **Monorepo support** - Scan all apps/packages in one command
462
+ - **Well-tested** - 87 tests covering edge cases
463
+
464
+ ## vs. dotenv-safe / envalid / dotenv-mono
465
+
466
+ | Feature | envcheck | dotenv-safe | envalid | dotenv-mono |
467
+ |---------|----------|-------------|---------|-------------|
468
+ | Validates presence | | ✅ | ✅ | ❌ |
469
+ | Based on .env.example | ✅ | ✅ | (schema) | ❌ |
470
+ | **Static validation** | ✅ | | ❌ | ❌ |
471
+ | **CI/CD integration** | ✅ GitHub Action | ❌ | ❌ | ❌ |
472
+ | **Pre-commit hook** | ✅ | | ❌ | |
473
+ | Type validation | ✅ (static) | ❌ | ✅ (runtime) | ❌ |
474
+ | **Secret detection** | ✅ | ❌ | ❌ | ❌ |
475
+ | **Monorepo scan** | | | | |
476
+ | Zero dependencies | ✅ | ❌ | ❌ | ❌ |
477
+
478
+ **Key difference:** envcheck validates *before* deployment (shift-left), while dotenv-safe and envalid validate at runtime when your app starts. dotenv-mono helps load env vars in monorepos but doesn't validate them. envcheck is the only tool that validates across entire monorepos with a single command.
326
479
 
327
480
  ## License
328
481
 
package/bin/cli.js CHANGED
@@ -1,21 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { check, compare, validate, list, get, readEnvFile } = require('../src/index.js');
4
+ const { check, compare, validate, list, get, readEnvFile, scanMonorepo, formatMonorepoResult } = require('../src/index.js');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
 
8
- const VERSION = '1.0.0';
8
+ const VERSION = '1.5.0';
9
9
 
10
10
  const HELP = `
11
11
  envcheck - Validate .env files
12
12
 
13
13
  USAGE
14
14
  envcheck [options] [file]
15
+ envcheck monorepo [directory]
15
16
  envcheck compare <env> <example>
16
17
 
17
18
  COMMANDS
18
19
  check (default) Check .env file, optionally against .env.example
20
+ monorepo Scan all apps/packages in a monorepo
19
21
  compare Compare two env files
20
22
  list List variables in a file
21
23
  get <key> Get a specific variable value
@@ -26,6 +28,8 @@ OPTIONS
26
28
  --no-empty Warn on empty values
27
29
  --no-extra Error on variables not in example
28
30
  --strict Treat warnings as errors
31
+ --secrets Enable secret detection (warn about real secrets)
32
+ --verbose Show detailed output (monorepo mode)
29
33
  -q, --quiet Only output errors
30
34
  -j, --json Output as JSON
31
35
  -v, --version Show version
@@ -35,6 +39,9 @@ EXAMPLES
35
39
  envcheck Check .env against .env.example
36
40
  envcheck .env.production Check specific file
37
41
  envcheck -r "API_KEY,DB_URL" Require specific variables
42
+ envcheck monorepo Scan monorepo from current directory
43
+ envcheck monorepo ./my-monorepo Scan specific directory
44
+ envcheck monorepo --verbose Show all issues per app
38
45
  envcheck compare .env .env.prod Compare two files
39
46
  envcheck list .env List all variables
40
47
  envcheck get .env API_KEY Get specific value
@@ -51,6 +58,8 @@ function parseArgs(args) {
51
58
  strict: false,
52
59
  quiet: false,
53
60
  json: false,
61
+ verbose: false,
62
+ detectSecrets: false,
54
63
  args: []
55
64
  };
56
65
 
@@ -78,11 +87,15 @@ function parseArgs(args) {
78
87
  result.noExtra = true;
79
88
  } else if (arg === '--strict') {
80
89
  result.strict = true;
90
+ } else if (arg === '--secrets') {
91
+ result.detectSecrets = true;
92
+ } else if (arg === '--verbose') {
93
+ result.verbose = true;
81
94
  } else if (arg === '-q' || arg === '--quiet') {
82
95
  result.quiet = true;
83
96
  } else if (arg === '-j' || arg === '--json') {
84
97
  result.json = true;
85
- } else if (arg === 'compare' || arg === 'list' || arg === 'get') {
98
+ } else if (arg === 'compare' || arg === 'list' || arg === 'get' || arg === 'monorepo') {
86
99
  result.command = arg;
87
100
  } else if (!arg.startsWith('-')) {
88
101
  result.args.push(arg);
@@ -277,6 +290,32 @@ function runGet(opts) {
277
290
  return 0;
278
291
  }
279
292
 
293
+ function runMonorepo(opts) {
294
+ const rootDir = opts.args[0] || '.';
295
+
296
+ const result = scanMonorepo(rootDir, {
297
+ noEmpty: opts.noEmpty,
298
+ noExtra: opts.noExtra,
299
+ strict: opts.strict,
300
+ detectSecrets: opts.detectSecrets,
301
+ checkConsistency: true
302
+ });
303
+
304
+ if (opts.json) {
305
+ console.log(JSON.stringify(result, null, 2));
306
+ return result.valid ? 0 : 1;
307
+ }
308
+
309
+ // Use formatted output
310
+ const output = formatMonorepoResult(result, {
311
+ colors: !opts.quiet,
312
+ verbose: opts.verbose
313
+ });
314
+ console.log(output);
315
+
316
+ return result.valid ? 0 : 1;
317
+ }
318
+
280
319
  function main() {
281
320
  const args = process.argv.slice(2);
282
321
  const opts = parseArgs(args);
@@ -287,6 +326,9 @@ function main() {
287
326
  case 'check':
288
327
  exitCode = runCheck(opts);
289
328
  break;
329
+ case 'monorepo':
330
+ exitCode = runMonorepo(opts);
331
+ break;
290
332
  case 'compare':
291
333
  exitCode = runCompare(opts);
292
334
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-agent/envcheck",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Validate .env files, compare with .env.example, find missing or empty variables",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -20,7 +20,12 @@
20
20
  "github-action",
21
21
  "ci-cd",
22
22
  "pre-commit",
23
- "git-hooks"
23
+ "git-hooks",
24
+ "secrets",
25
+ "security",
26
+ "monorepo",
27
+ "turborepo",
28
+ "workspaces"
24
29
  ],
25
30
  "author": "Claude Agent <claude-agent@agentmail.to>",
26
31
  "license": "MIT",
package/src/index.js CHANGED
@@ -87,6 +87,108 @@ const typeValidators = {
87
87
  }
88
88
  };
89
89
 
90
+ /**
91
+ * Secret detection patterns
92
+ * Each pattern has: regex, description, and optional keyPattern (for key-specific checks)
93
+ */
94
+ const secretPatterns = [
95
+ // AWS
96
+ { regex: /^AKIA[0-9A-Z]{16}$/, description: 'AWS Access Key ID' },
97
+ { regex: /^[A-Za-z0-9/+=]{40}$/, description: 'AWS Secret Access Key', keyPattern: /aws.*secret|secret.*key/i },
98
+
99
+ // Private keys
100
+ { regex: /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/, description: 'Private key' },
101
+ { regex: /-----BEGIN CERTIFICATE-----/, description: 'Certificate' },
102
+
103
+ // GitHub
104
+ { regex: /^ghp_[a-zA-Z0-9]{36}$/, description: 'GitHub Personal Access Token' },
105
+ { regex: /^gho_[a-zA-Z0-9]{36}$/, description: 'GitHub OAuth Access Token' },
106
+ { regex: /^ghu_[a-zA-Z0-9]{36}$/, description: 'GitHub User-to-Server Token' },
107
+ { regex: /^ghs_[a-zA-Z0-9]{36}$/, description: 'GitHub Server-to-Server Token' },
108
+ { regex: /^ghr_[a-zA-Z0-9]{36}$/, description: 'GitHub Refresh Token' },
109
+
110
+ // Slack
111
+ { regex: /^xox[baprs]-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24}$/, description: 'Slack Token' },
112
+
113
+ // Stripe
114
+ { regex: /^sk_live_[a-zA-Z0-9]{24,}$/, description: 'Stripe Live Secret Key' },
115
+ { regex: /^rk_live_[a-zA-Z0-9]{24,}$/, description: 'Stripe Live Restricted Key' },
116
+
117
+ // Twilio
118
+ { regex: /^AC[a-f0-9]{32}$/, description: 'Twilio Account SID' },
119
+ { regex: /^SK[a-f0-9]{32}$/, description: 'Twilio API Key' },
120
+
121
+ // SendGrid
122
+ { regex: /^SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}$/, description: 'SendGrid API Key' },
123
+
124
+ // Google
125
+ { regex: /^AIza[0-9A-Za-z_-]{35}$/, description: 'Google API Key' },
126
+
127
+ // Generic high-entropy (likely real secrets)
128
+ { regex: /^[a-f0-9]{32}$/, description: 'Hex string (32 chars)', keyPattern: /api.*key|secret|token|password/i },
129
+ { regex: /^[a-f0-9]{64}$/, description: 'Hex string (64 chars)', keyPattern: /api.*key|secret|token|password/i },
130
+ ];
131
+
132
+ /**
133
+ * Placeholder patterns that are NOT secrets
134
+ */
135
+ const placeholderPatterns = [
136
+ /^your[-_]?/i,
137
+ /^my[-_]?/i,
138
+ /^xxx+$/i,
139
+ /^placeholder/i,
140
+ /^example/i,
141
+ /^test[-_]?/i,
142
+ /^dummy/i,
143
+ /^fake/i,
144
+ /^sample/i,
145
+ /^changeme/i,
146
+ /^replace[-_]?/i,
147
+ /^insert[-_]?/i,
148
+ /^todo/i,
149
+ /^\*+$/,
150
+ /^\.\.\.$/,
151
+ ];
152
+
153
+ /**
154
+ * Check if a value looks like a placeholder
155
+ * @param {string} value - Value to check
156
+ * @returns {boolean} True if it looks like a placeholder
157
+ */
158
+ function isPlaceholder(value) {
159
+ if (!value || value.length < 3) return true;
160
+ return placeholderPatterns.some(pattern => pattern.test(value));
161
+ }
162
+
163
+ /**
164
+ * Detect potential secrets in environment variables
165
+ * @param {string} key - Variable name
166
+ * @param {string} value - Variable value
167
+ * @returns {Object|null} Detection result or null if no secret detected
168
+ */
169
+ function detectSecret(key, value) {
170
+ if (!value || isPlaceholder(value)) {
171
+ return null;
172
+ }
173
+
174
+ for (const pattern of secretPatterns) {
175
+ // If pattern has keyPattern, only check if key matches
176
+ if (pattern.keyPattern && !pattern.keyPattern.test(key)) {
177
+ continue;
178
+ }
179
+
180
+ if (pattern.regex.test(value)) {
181
+ return {
182
+ detected: true,
183
+ description: pattern.description,
184
+ message: `may contain a real ${pattern.description}`
185
+ };
186
+ }
187
+ }
188
+
189
+ return null;
190
+ }
191
+
90
192
  /**
91
193
  * Parse a .env file into an object
92
194
  * @param {string} content - File content
@@ -326,7 +428,7 @@ function validateType(value, type) {
326
428
  * @returns {Object} Validation result
327
429
  */
328
430
  function validate(filePath, options = {}) {
329
- const { required = [], noEmpty = false, types = {}, validateTypes = false } = options;
431
+ const { required = [], noEmpty = false, types = {}, validateTypes = false, detectSecrets = false } = options;
330
432
 
331
433
  const env = readEnvFile(filePath);
332
434
 
@@ -406,6 +508,20 @@ function validate(filePath, options = {}) {
406
508
  }
407
509
  }
408
510
 
511
+ // Secret detection
512
+ if (detectSecrets) {
513
+ for (const [key, value] of Object.entries(env.variables)) {
514
+ const secretResult = detectSecret(key, value);
515
+ if (secretResult) {
516
+ result.issues.push({
517
+ type: 'warning',
518
+ line: env.lineInfo[key],
519
+ message: `Variable '${key}' ${secretResult.message}`
520
+ });
521
+ }
522
+ }
523
+ }
524
+
409
525
  // Add warnings
410
526
  for (const warning of env.warnings) {
411
527
  result.issues.push({
@@ -432,7 +548,8 @@ function check(envPath, options = {}) {
432
548
  noExtra = false,
433
549
  strict = false,
434
550
  types = {},
435
- validateTypes = false
551
+ validateTypes = false,
552
+ detectSecrets = false
436
553
  } = options;
437
554
 
438
555
  const result = {
@@ -456,12 +573,13 @@ function check(envPath, options = {}) {
456
573
  // Merge type hints: example hints < explicit types
457
574
  const mergedTypes = { ...exampleTypeHints, ...types };
458
575
 
459
- // First validate the env file (including type validation)
576
+ // First validate the env file (including type validation and secret detection)
460
577
  const validation = validate(envPath, {
461
578
  required,
462
579
  noEmpty,
463
580
  types: mergedTypes,
464
- validateTypes: validateTypes || Object.keys(mergedTypes).length > 0
581
+ validateTypes: validateTypes || Object.keys(mergedTypes).length > 0,
582
+ detectSecrets
465
583
  });
466
584
 
467
585
  if (!validation.valid) {
@@ -565,6 +683,278 @@ function get(filePath, key) {
565
683
  return env.variables[key];
566
684
  }
567
685
 
686
+ /**
687
+ * Find directories that might contain apps/packages in a monorepo
688
+ * @param {string} rootDir - Root directory to scan
689
+ * @returns {string[]} Array of directory paths
690
+ */
691
+ function findMonorepoApps(rootDir) {
692
+ const apps = [];
693
+ const basePath = path.resolve(rootDir);
694
+
695
+ // Common monorepo patterns
696
+ const patterns = ['apps', 'packages', 'workspaces', 'services', 'libs'];
697
+
698
+ for (const pattern of patterns) {
699
+ const dir = path.join(basePath, pattern);
700
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
701
+ // Get all subdirectories
702
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
703
+ for (const entry of entries) {
704
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
705
+ apps.push(path.join(dir, entry.name));
706
+ }
707
+ }
708
+ }
709
+ }
710
+
711
+ // Also check root for .env.example (some monorepos have root-level env)
712
+ if (fs.existsSync(path.join(basePath, '.env.example')) ||
713
+ fs.existsSync(path.join(basePath, '.env'))) {
714
+ apps.unshift(basePath);
715
+ }
716
+
717
+ return apps;
718
+ }
719
+
720
+ /**
721
+ * Scan a monorepo for env file issues
722
+ * @param {string} rootDir - Root directory of monorepo
723
+ * @param {Object} options - Scan options
724
+ * @returns {Object} Monorepo scan result
725
+ */
726
+ function scanMonorepo(rootDir, options = {}) {
727
+ const {
728
+ noEmpty = false,
729
+ noExtra = false,
730
+ strict = false,
731
+ checkConsistency = true,
732
+ detectSecrets = false
733
+ } = options;
734
+
735
+ const basePath = path.resolve(rootDir);
736
+ const apps = findMonorepoApps(basePath);
737
+
738
+ const result = {
739
+ root: basePath,
740
+ valid: true,
741
+ apps: [],
742
+ summary: {
743
+ total: apps.length,
744
+ passed: 0,
745
+ failed: 0,
746
+ skipped: 0,
747
+ errors: 0,
748
+ warnings: 0
749
+ },
750
+ consistency: {
751
+ sharedVars: {}, // Variables that appear in multiple apps
752
+ mismatches: [] // Variables with different types/values across apps
753
+ }
754
+ };
755
+
756
+ // Track all variables across apps for consistency checking
757
+ const allVars = {}; // { varName: [{ app, value, type }] }
758
+
759
+ for (const appDir of apps) {
760
+ const appName = path.relative(basePath, appDir) || '.';
761
+ const envPath = path.join(appDir, '.env');
762
+ const examplePath = path.join(appDir, '.env.example');
763
+
764
+ const appResult = {
765
+ name: appName,
766
+ path: appDir,
767
+ hasEnv: fs.existsSync(envPath),
768
+ hasExample: fs.existsSync(examplePath),
769
+ valid: true,
770
+ issues: [],
771
+ variables: []
772
+ };
773
+
774
+ // Skip if no example file (can't validate)
775
+ if (!appResult.hasExample) {
776
+ appResult.skipped = true;
777
+ appResult.reason = 'No .env.example found';
778
+ result.apps.push(appResult);
779
+ result.summary.skipped++;
780
+ continue;
781
+ }
782
+
783
+ // If no .env but has example, that's also a problem
784
+ if (!appResult.hasEnv) {
785
+ appResult.valid = false;
786
+ appResult.issues.push({
787
+ type: 'warning',
788
+ message: 'No .env file found (but .env.example exists)'
789
+ });
790
+ }
791
+
792
+ // Run check if both files exist
793
+ if (appResult.hasEnv) {
794
+ const checkResult = check(envPath, {
795
+ examplePath,
796
+ noEmpty,
797
+ noExtra,
798
+ strict,
799
+ validateTypes: true,
800
+ detectSecrets
801
+ });
802
+
803
+ appResult.valid = checkResult.valid;
804
+ appResult.issues = checkResult.issues;
805
+ appResult.variables = Object.keys(checkResult.env?.variables || {});
806
+
807
+ // Track variables for consistency check
808
+ if (checkConsistency && checkResult.env?.variables) {
809
+ const example = readEnvFile(examplePath);
810
+ for (const [varName, value] of Object.entries(checkResult.env.variables)) {
811
+ if (!allVars[varName]) {
812
+ allVars[varName] = [];
813
+ }
814
+ allVars[varName].push({
815
+ app: appName,
816
+ value,
817
+ type: example.typeHints?.[varName] || null
818
+ });
819
+ }
820
+ }
821
+ }
822
+
823
+ // Update summary
824
+ if (appResult.skipped) {
825
+ // Already counted above
826
+ } else if (appResult.valid) {
827
+ result.summary.passed++;
828
+ } else {
829
+ result.summary.failed++;
830
+ result.valid = false;
831
+ }
832
+
833
+ const errors = appResult.issues.filter(i => i.type === 'error').length;
834
+ const warnings = appResult.issues.filter(i => i.type === 'warning').length;
835
+ result.summary.errors += errors;
836
+ result.summary.warnings += warnings;
837
+
838
+ result.apps.push(appResult);
839
+ }
840
+
841
+ // Check consistency across apps
842
+ if (checkConsistency) {
843
+ for (const [varName, occurrences] of Object.entries(allVars)) {
844
+ if (occurrences.length > 1) {
845
+ result.consistency.sharedVars[varName] = occurrences.map(o => o.app);
846
+
847
+ // Check for type mismatches
848
+ const types = new Set(occurrences.map(o => o.type).filter(Boolean));
849
+ if (types.size > 1) {
850
+ result.consistency.mismatches.push({
851
+ variable: varName,
852
+ issue: 'type_mismatch',
853
+ details: occurrences.map(o => ({ app: o.app, type: o.type }))
854
+ });
855
+ result.valid = false;
856
+ }
857
+ }
858
+ }
859
+ }
860
+
861
+ return result;
862
+ }
863
+
864
+ /**
865
+ * Format monorepo result for CLI output
866
+ * @param {Object} result - Monorepo scan result
867
+ * @param {Object} options - Format options
868
+ * @returns {string} Formatted output
869
+ */
870
+ function formatMonorepoResult(result, options = {}) {
871
+ const { colors = true, verbose = false } = options;
872
+
873
+ const c = colors ? {
874
+ green: '\x1b[32m',
875
+ red: '\x1b[31m',
876
+ yellow: '\x1b[33m',
877
+ dim: '\x1b[2m',
878
+ reset: '\x1b[0m',
879
+ bold: '\x1b[1m'
880
+ } : { green: '', red: '', yellow: '', dim: '', reset: '', bold: '' };
881
+
882
+ const lines = [];
883
+
884
+ lines.push(`${c.bold}Monorepo Environment Check${c.reset}`);
885
+ lines.push(`Root: ${result.root}`);
886
+ lines.push('');
887
+
888
+ for (const app of result.apps) {
889
+ const icon = app.skipped ? `${c.dim}○${c.reset}` :
890
+ app.valid ? `${c.green}✓${c.reset}` :
891
+ `${c.red}✗${c.reset}`;
892
+
893
+ let status = '';
894
+ if (app.skipped) {
895
+ status = `${c.dim}skipped (${app.reason})${c.reset}`;
896
+ } else {
897
+ const errors = app.issues.filter(i => i.type === 'error').length;
898
+ const warnings = app.issues.filter(i => i.type === 'warning').length;
899
+
900
+ if (errors === 0 && warnings === 0) {
901
+ status = `${c.green}passed${c.reset}`;
902
+ } else if (errors > 0) {
903
+ status = `${c.red}${errors} error(s)${c.reset}`;
904
+ if (warnings > 0) status += `, ${c.yellow}${warnings} warning(s)${c.reset}`;
905
+ } else {
906
+ status = `${c.yellow}${warnings} warning(s)${c.reset}`;
907
+ }
908
+ }
909
+
910
+ lines.push(`${icon} ${app.name}: ${status}`);
911
+
912
+ // Show details in verbose mode
913
+ if (verbose && !app.skipped && app.issues.length > 0) {
914
+ for (const issue of app.issues) {
915
+ const prefix = issue.type === 'error' ? `${c.red} ✗${c.reset}` : `${c.yellow} !${c.reset}`;
916
+ lines.push(`${prefix} ${issue.message}`);
917
+ }
918
+ }
919
+ }
920
+
921
+ lines.push('');
922
+
923
+ // Summary
924
+ lines.push(`${c.bold}Summary:${c.reset} ${result.summary.total} apps scanned`);
925
+ if (result.summary.passed > 0) {
926
+ lines.push(` ${c.green}✓${c.reset} ${result.summary.passed} passed`);
927
+ }
928
+ if (result.summary.failed > 0) {
929
+ lines.push(` ${c.red}✗${c.reset} ${result.summary.failed} failed`);
930
+ }
931
+ if (result.summary.skipped > 0) {
932
+ lines.push(` ${c.dim}○${c.reset} ${result.summary.skipped} skipped`);
933
+ }
934
+
935
+ // Consistency issues
936
+ if (result.consistency.mismatches.length > 0) {
937
+ lines.push('');
938
+ lines.push(`${c.yellow}Consistency Issues:${c.reset}`);
939
+ for (const mismatch of result.consistency.mismatches) {
940
+ lines.push(` ${c.yellow}!${c.reset} ${mismatch.variable}: ${mismatch.issue}`);
941
+ for (const detail of mismatch.details) {
942
+ lines.push(` - ${detail.app}: type=${detail.type || 'unspecified'}`);
943
+ }
944
+ }
945
+ }
946
+
947
+ // Final status
948
+ lines.push('');
949
+ if (result.valid) {
950
+ lines.push(`${c.green}✓ All checks passed${c.reset}`);
951
+ } else {
952
+ lines.push(`${c.red}✗ ${result.summary.errors} error(s), ${result.summary.warnings} warning(s)${c.reset}`);
953
+ }
954
+
955
+ return lines.join('\n');
956
+ }
957
+
568
958
  module.exports = {
569
959
  parse: parseEnv,
570
960
  parseValue,
@@ -573,8 +963,15 @@ module.exports = {
573
963
  validate,
574
964
  validateType,
575
965
  typeValidators,
966
+ detectSecret,
967
+ secretPatterns,
968
+ isPlaceholder,
576
969
  check,
577
970
  generate,
578
971
  list,
579
- get
972
+ get,
973
+ // Monorepo support
974
+ findMonorepoApps,
975
+ scanMonorepo,
976
+ formatMonorepoResult
580
977
  };