@claude-agent/envcheck 1.4.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 +121 -18
- package/bin/cli.js +45 -3
- package/package.json +5 -2
- package/src/index.js +277 -1
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@claude-agent/envcheck)
|
|
5
5
|
[](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
|
|
@@ -357,22 +458,24 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
|
357
458
|
- **Auto-detection** - Finds .env.example automatically
|
|
358
459
|
- **CI-friendly** - Exit codes and JSON output
|
|
359
460
|
- **Comprehensive** - Parse, validate, compare, generate
|
|
360
|
-
- **
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
|
368
|
-
|
|
|
369
|
-
| **
|
|
370
|
-
| **
|
|
371
|
-
|
|
|
372
|
-
|
|
|
373
|
-
|
|
|
374
|
-
|
|
375
|
-
|
|
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.
|
|
376
479
|
|
|
377
480
|
## License
|
|
378
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.
|
|
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
|
+
"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": {
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
"pre-commit",
|
|
23
23
|
"git-hooks",
|
|
24
24
|
"secrets",
|
|
25
|
-
"security"
|
|
25
|
+
"security",
|
|
26
|
+
"monorepo",
|
|
27
|
+
"turborepo",
|
|
28
|
+
"workspaces"
|
|
26
29
|
],
|
|
27
30
|
"author": "Claude Agent <claude-agent@agentmail.to>",
|
|
28
31
|
"license": "MIT",
|
package/src/index.js
CHANGED
|
@@ -683,6 +683,278 @@ function get(filePath, key) {
|
|
|
683
683
|
return env.variables[key];
|
|
684
684
|
}
|
|
685
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
|
+
|
|
686
958
|
module.exports = {
|
|
687
959
|
parse: parseEnv,
|
|
688
960
|
parseValue,
|
|
@@ -697,5 +969,9 @@ module.exports = {
|
|
|
697
969
|
check,
|
|
698
970
|
generate,
|
|
699
971
|
list,
|
|
700
|
-
get
|
|
972
|
+
get,
|
|
973
|
+
// Monorepo support
|
|
974
|
+
findMonorepoApps,
|
|
975
|
+
scanMonorepo,
|
|
976
|
+
formatMonorepoResult
|
|
701
977
|
};
|