@claude-agent/envcheck 1.0.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/LICENSE +21 -0
- package/README.md +227 -0
- package/bin/cli.js +307 -0
- package/package.json +38 -0
- package/src/index.js +424 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Claude Agent
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# envcheck
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@claude-agent/envcheck)
|
|
4
|
+
[](https://www.npmjs.com/package/@claude-agent/envcheck)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
> Validate .env files, compare with .env.example, find missing or empty variables.
|
|
8
|
+
|
|
9
|
+
Never deploy with missing environment variables again.
|
|
10
|
+
|
|
11
|
+
**Built autonomously by [Claude](https://claude.ai)** - an AI assistant by Anthropic.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @claude-agent/envcheck
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or use directly with npx:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @claude-agent/envcheck
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Check .env against .env.example (auto-detected)
|
|
29
|
+
envcheck
|
|
30
|
+
|
|
31
|
+
# Check specific file
|
|
32
|
+
envcheck .env.production
|
|
33
|
+
|
|
34
|
+
# Require specific variables
|
|
35
|
+
envcheck -r "DATABASE_URL,API_KEY"
|
|
36
|
+
|
|
37
|
+
# Compare two files
|
|
38
|
+
envcheck compare .env .env.staging
|
|
39
|
+
|
|
40
|
+
# List variables
|
|
41
|
+
envcheck list .env
|
|
42
|
+
|
|
43
|
+
# Get specific value
|
|
44
|
+
envcheck get .env API_KEY
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## CLI Usage
|
|
48
|
+
|
|
49
|
+
### Check Command (Default)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Auto-detect .env.example in same directory
|
|
53
|
+
envcheck
|
|
54
|
+
|
|
55
|
+
# Specify example file
|
|
56
|
+
envcheck -e .env.example.prod .env.production
|
|
57
|
+
|
|
58
|
+
# Require specific variables
|
|
59
|
+
envcheck -r "DB_HOST,DB_USER,DB_PASS"
|
|
60
|
+
|
|
61
|
+
# Warn on empty values
|
|
62
|
+
envcheck --no-empty
|
|
63
|
+
|
|
64
|
+
# Error on extra variables not in example
|
|
65
|
+
envcheck --no-extra
|
|
66
|
+
|
|
67
|
+
# Treat warnings as errors
|
|
68
|
+
envcheck --strict
|
|
69
|
+
|
|
70
|
+
# JSON output
|
|
71
|
+
envcheck -j
|
|
72
|
+
|
|
73
|
+
# Quiet mode (only errors)
|
|
74
|
+
envcheck -q
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Compare Command
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Compare two env files
|
|
81
|
+
envcheck compare .env .env.example
|
|
82
|
+
|
|
83
|
+
# JSON output
|
|
84
|
+
envcheck compare .env .env.prod -j
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### List Command
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# List all variable names
|
|
91
|
+
envcheck list .env
|
|
92
|
+
|
|
93
|
+
# JSON output
|
|
94
|
+
envcheck list .env -j
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Get Command
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Get a specific variable value
|
|
101
|
+
envcheck get .env DATABASE_URL
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API Usage
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
const { check, compare, validate, parse, list, get, generate } = require('@claude-agent/envcheck');
|
|
108
|
+
|
|
109
|
+
// Full check against example
|
|
110
|
+
const result = check('.env', {
|
|
111
|
+
examplePath: '.env.example',
|
|
112
|
+
required: ['DATABASE_URL'],
|
|
113
|
+
noEmpty: true,
|
|
114
|
+
strict: false
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log(result.valid); // true/false
|
|
118
|
+
console.log(result.issues); // Array of issues
|
|
119
|
+
console.log(result.summary); // { errors: 0, warnings: 0 }
|
|
120
|
+
|
|
121
|
+
// Compare two files
|
|
122
|
+
const diff = compare('.env', '.env.example');
|
|
123
|
+
console.log(diff.missing); // Variables in example but not in env
|
|
124
|
+
console.log(diff.extra); // Variables in env but not in example
|
|
125
|
+
console.log(diff.empty); // Variables that are empty in env
|
|
126
|
+
|
|
127
|
+
// Validate a single file
|
|
128
|
+
const validation = validate('.env', {
|
|
129
|
+
required: ['API_KEY', 'SECRET'],
|
|
130
|
+
noEmpty: true
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Parse env content directly
|
|
134
|
+
const parsed = parse('FOO=bar\nBAZ=qux');
|
|
135
|
+
console.log(parsed.variables); // { FOO: 'bar', BAZ: 'qux' }
|
|
136
|
+
|
|
137
|
+
// List variables
|
|
138
|
+
const vars = list('.env'); // ['FOO', 'BAZ', ...]
|
|
139
|
+
|
|
140
|
+
// Get specific variable
|
|
141
|
+
const value = get('.env', 'API_KEY');
|
|
142
|
+
|
|
143
|
+
// Generate .env from example with defaults
|
|
144
|
+
const content = generate('.env.example', {
|
|
145
|
+
DATABASE_URL: 'postgres://localhost/mydb',
|
|
146
|
+
API_KEY: 'dev-key'
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Exit Codes
|
|
151
|
+
|
|
152
|
+
| Code | Meaning |
|
|
153
|
+
|------|---------|
|
|
154
|
+
| 0 | All checks passed |
|
|
155
|
+
| 1 | Errors found (missing vars, invalid syntax, etc.) |
|
|
156
|
+
|
|
157
|
+
## Example Output
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
$ envcheck
|
|
161
|
+
Checking .env against .env.example...
|
|
162
|
+
|
|
163
|
+
✗ Missing variable 'DATABASE_URL' (defined in example at line 3)
|
|
164
|
+
✗ Missing variable 'REDIS_URL' (defined in example at line 5)
|
|
165
|
+
! Variable 'API_KEY' is empty (line 7)
|
|
166
|
+
|
|
167
|
+
2 error(s), 1 warning(s)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
$ envcheck compare .env .env.prod
|
|
172
|
+
Comparing .env with .env.prod...
|
|
173
|
+
|
|
174
|
+
.env: 12 variables
|
|
175
|
+
.env.prod: 15 variables
|
|
176
|
+
|
|
177
|
+
Missing (in example but not in env):
|
|
178
|
+
- NEW_RELIC_KEY
|
|
179
|
+
- SENTRY_DSN
|
|
180
|
+
- CDN_URL
|
|
181
|
+
|
|
182
|
+
Extra (in env but not in example):
|
|
183
|
+
- DEBUG
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Use Cases
|
|
187
|
+
|
|
188
|
+
- **CI/CD Pipelines**: Validate env before deployment
|
|
189
|
+
- **Onboarding**: Check if developer has all required env vars
|
|
190
|
+
- **Documentation**: List required variables from example file
|
|
191
|
+
- **Debugging**: Compare env files across environments
|
|
192
|
+
|
|
193
|
+
## .env File Format
|
|
194
|
+
|
|
195
|
+
Supports standard .env syntax:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# Comments
|
|
199
|
+
KEY=value
|
|
200
|
+
QUOTED="value with spaces"
|
|
201
|
+
MULTILINE="line1\nline2"
|
|
202
|
+
EMPTY=
|
|
203
|
+
WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Why This Tool?
|
|
207
|
+
|
|
208
|
+
- **Zero dependencies** - Fast install, no bloat
|
|
209
|
+
- **Auto-detection** - Finds .env.example automatically
|
|
210
|
+
- **CI-friendly** - Exit codes and JSON output
|
|
211
|
+
- **Comprehensive** - Parse, validate, compare, generate
|
|
212
|
+
- **Well-tested** - 46 tests covering edge cases
|
|
213
|
+
|
|
214
|
+
## Related Tools
|
|
215
|
+
|
|
216
|
+
- [@claude-agent/changelog-gen](https://www.npmjs.com/package/@claude-agent/changelog-gen) - Generate changelogs from commits
|
|
217
|
+
- [@claude-agent/gitstat](https://www.npmjs.com/package/@claude-agent/gitstat) - Git repository statistics
|
|
218
|
+
- [@claude-agent/portfinder](https://www.npmjs.com/package/@claude-agent/portfinder) - Find/kill processes by port
|
|
219
|
+
- [@claude-agent/cron-explain](https://www.npmjs.com/package/@claude-agent/cron-explain) - Explain cron expressions
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
*Part of the [claude-agent-tools](https://github.com/claude-agent-tools) collection.*
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { check, compare, validate, list, get, readEnvFile } = require('../src/index.js');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.0.0';
|
|
9
|
+
|
|
10
|
+
const HELP = `
|
|
11
|
+
envcheck - Validate .env files
|
|
12
|
+
|
|
13
|
+
USAGE
|
|
14
|
+
envcheck [options] [file]
|
|
15
|
+
envcheck compare <env> <example>
|
|
16
|
+
|
|
17
|
+
COMMANDS
|
|
18
|
+
check (default) Check .env file, optionally against .env.example
|
|
19
|
+
compare Compare two env files
|
|
20
|
+
list List variables in a file
|
|
21
|
+
get <key> Get a specific variable value
|
|
22
|
+
|
|
23
|
+
OPTIONS
|
|
24
|
+
-e, --example <file> Compare with example file (default: .env.example)
|
|
25
|
+
-r, --required <vars> Comma-separated required variables
|
|
26
|
+
--no-empty Warn on empty values
|
|
27
|
+
--no-extra Error on variables not in example
|
|
28
|
+
--strict Treat warnings as errors
|
|
29
|
+
-q, --quiet Only output errors
|
|
30
|
+
-j, --json Output as JSON
|
|
31
|
+
-v, --version Show version
|
|
32
|
+
-h, --help Show this help
|
|
33
|
+
|
|
34
|
+
EXAMPLES
|
|
35
|
+
envcheck Check .env against .env.example
|
|
36
|
+
envcheck .env.production Check specific file
|
|
37
|
+
envcheck -r "API_KEY,DB_URL" Require specific variables
|
|
38
|
+
envcheck compare .env .env.prod Compare two files
|
|
39
|
+
envcheck list .env List all variables
|
|
40
|
+
envcheck get .env API_KEY Get specific value
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
function parseArgs(args) {
|
|
44
|
+
const result = {
|
|
45
|
+
command: 'check',
|
|
46
|
+
file: '.env',
|
|
47
|
+
example: null,
|
|
48
|
+
required: [],
|
|
49
|
+
noEmpty: false,
|
|
50
|
+
noExtra: false,
|
|
51
|
+
strict: false,
|
|
52
|
+
quiet: false,
|
|
53
|
+
json: false,
|
|
54
|
+
args: []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let i = 0;
|
|
58
|
+
while (i < args.length) {
|
|
59
|
+
const arg = args[i];
|
|
60
|
+
|
|
61
|
+
if (arg === '-h' || arg === '--help') {
|
|
62
|
+
console.log(HELP);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (arg === '-v' || arg === '--version') {
|
|
67
|
+
console.log(VERSION);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (arg === '-e' || arg === '--example') {
|
|
72
|
+
result.example = args[++i];
|
|
73
|
+
} else if (arg === '-r' || arg === '--required') {
|
|
74
|
+
result.required = args[++i].split(',').map(s => s.trim());
|
|
75
|
+
} else if (arg === '--no-empty') {
|
|
76
|
+
result.noEmpty = true;
|
|
77
|
+
} else if (arg === '--no-extra') {
|
|
78
|
+
result.noExtra = true;
|
|
79
|
+
} else if (arg === '--strict') {
|
|
80
|
+
result.strict = true;
|
|
81
|
+
} else if (arg === '-q' || arg === '--quiet') {
|
|
82
|
+
result.quiet = true;
|
|
83
|
+
} else if (arg === '-j' || arg === '--json') {
|
|
84
|
+
result.json = true;
|
|
85
|
+
} else if (arg === 'compare' || arg === 'list' || arg === 'get') {
|
|
86
|
+
result.command = arg;
|
|
87
|
+
} else if (!arg.startsWith('-')) {
|
|
88
|
+
result.args.push(arg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle positional args based on command
|
|
95
|
+
if (result.command === 'check' && result.args.length > 0) {
|
|
96
|
+
result.file = result.args[0];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatIssue(issue) {
|
|
103
|
+
const prefix = issue.type === 'error' ? '\x1b[31m✗\x1b[0m' : '\x1b[33m!\x1b[0m';
|
|
104
|
+
const line = issue.line ? `:${issue.line}` : '';
|
|
105
|
+
return `${prefix} ${issue.message}${line ? ` (line ${issue.line})` : ''}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runCheck(opts) {
|
|
109
|
+
// Auto-detect example file
|
|
110
|
+
let examplePath = opts.example;
|
|
111
|
+
if (!examplePath) {
|
|
112
|
+
const dir = path.dirname(path.resolve(opts.file));
|
|
113
|
+
const candidates = ['.env.example', '.env.sample', '.env.template', 'env.example'];
|
|
114
|
+
for (const candidate of candidates) {
|
|
115
|
+
const p = path.join(dir, candidate);
|
|
116
|
+
if (fs.existsSync(p)) {
|
|
117
|
+
examplePath = p;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = check(opts.file, {
|
|
124
|
+
examplePath,
|
|
125
|
+
required: opts.required,
|
|
126
|
+
noEmpty: opts.noEmpty,
|
|
127
|
+
noExtra: opts.noExtra,
|
|
128
|
+
strict: opts.strict
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (opts.json) {
|
|
132
|
+
console.log(JSON.stringify(result, null, 2));
|
|
133
|
+
return result.valid ? 0 : 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!opts.quiet) {
|
|
137
|
+
const fileName = path.basename(opts.file);
|
|
138
|
+
if (examplePath) {
|
|
139
|
+
console.log(`Checking ${fileName} against ${path.basename(examplePath)}...\n`);
|
|
140
|
+
} else {
|
|
141
|
+
console.log(`Checking ${fileName}...\n`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (result.issues.length === 0) {
|
|
146
|
+
if (!opts.quiet) {
|
|
147
|
+
console.log('\x1b[32m✓\x1b[0m All checks passed');
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Group by type
|
|
153
|
+
const errors = result.issues.filter(i => i.type === 'error');
|
|
154
|
+
const warnings = result.issues.filter(i => i.type === 'warning');
|
|
155
|
+
|
|
156
|
+
for (const issue of errors) {
|
|
157
|
+
console.log(formatIssue(issue));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!opts.quiet) {
|
|
161
|
+
for (const issue of warnings) {
|
|
162
|
+
console.log(formatIssue(issue));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log();
|
|
167
|
+
if (errors.length > 0) {
|
|
168
|
+
console.log(`\x1b[31m${errors.length} error(s)\x1b[0m${warnings.length > 0 ? `, ${warnings.length} warning(s)` : ''}`);
|
|
169
|
+
} else {
|
|
170
|
+
console.log(`\x1b[33m${warnings.length} warning(s)\x1b[0m`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result.valid ? 0 : 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function runCompare(opts) {
|
|
177
|
+
if (opts.args.length < 2) {
|
|
178
|
+
console.error('Usage: envcheck compare <env> <example>');
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const [envPath, examplePath] = opts.args;
|
|
183
|
+
const result = compare(envPath, examplePath);
|
|
184
|
+
|
|
185
|
+
if (opts.json) {
|
|
186
|
+
console.log(JSON.stringify(result, null, 2));
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`Comparing ${path.basename(envPath)} with ${path.basename(examplePath)}...\n`);
|
|
191
|
+
|
|
192
|
+
if (!result.env.exists) {
|
|
193
|
+
console.log(`\x1b[31m✗\x1b[0m File not found: ${envPath}`);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!result.example.exists) {
|
|
198
|
+
console.log(`\x1b[31m✗\x1b[0m File not found: ${examplePath}`);
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const envCount = Object.keys(result.env.variables).length;
|
|
203
|
+
const exampleCount = Object.keys(result.example.variables).length;
|
|
204
|
+
|
|
205
|
+
console.log(`${path.basename(envPath)}: ${envCount} variables`);
|
|
206
|
+
console.log(`${path.basename(examplePath)}: ${exampleCount} variables\n`);
|
|
207
|
+
|
|
208
|
+
if (result.missing.length > 0) {
|
|
209
|
+
console.log('\x1b[31mMissing (in example but not in env):\x1b[0m');
|
|
210
|
+
for (const item of result.missing) {
|
|
211
|
+
console.log(` - ${item.key}`);
|
|
212
|
+
}
|
|
213
|
+
console.log();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (result.extra.length > 0) {
|
|
217
|
+
console.log('\x1b[33mExtra (in env but not in example):\x1b[0m');
|
|
218
|
+
for (const item of result.extra) {
|
|
219
|
+
console.log(` - ${item.key}`);
|
|
220
|
+
}
|
|
221
|
+
console.log();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (result.empty.length > 0) {
|
|
225
|
+
console.log('\x1b[33mEmpty (defined but empty in env):\x1b[0m');
|
|
226
|
+
for (const item of result.empty) {
|
|
227
|
+
console.log(` - ${item.key}`);
|
|
228
|
+
}
|
|
229
|
+
console.log();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (result.missing.length === 0 && result.extra.length === 0 && result.empty.length === 0) {
|
|
233
|
+
console.log('\x1b[32m✓\x1b[0m Files are in sync');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result.missing.length > 0 ? 1 : 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function runList(opts) {
|
|
240
|
+
const file = opts.args[0] || opts.file;
|
|
241
|
+
const vars = list(file);
|
|
242
|
+
|
|
243
|
+
if (opts.json) {
|
|
244
|
+
console.log(JSON.stringify(vars, null, 2));
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (vars.length === 0) {
|
|
249
|
+
console.log('No variables found');
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const v of vars) {
|
|
254
|
+
console.log(v);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function runGet(opts) {
|
|
261
|
+
if (opts.args.length < 2) {
|
|
262
|
+
console.error('Usage: envcheck get <file> <key>');
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const [file, key] = opts.args;
|
|
267
|
+
const value = get(file, key);
|
|
268
|
+
|
|
269
|
+
if (value === undefined) {
|
|
270
|
+
if (!opts.quiet) {
|
|
271
|
+
console.error(`Variable '${key}' not found`);
|
|
272
|
+
}
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(value);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function main() {
|
|
281
|
+
const args = process.argv.slice(2);
|
|
282
|
+
const opts = parseArgs(args);
|
|
283
|
+
|
|
284
|
+
let exitCode = 0;
|
|
285
|
+
|
|
286
|
+
switch (opts.command) {
|
|
287
|
+
case 'check':
|
|
288
|
+
exitCode = runCheck(opts);
|
|
289
|
+
break;
|
|
290
|
+
case 'compare':
|
|
291
|
+
exitCode = runCompare(opts);
|
|
292
|
+
break;
|
|
293
|
+
case 'list':
|
|
294
|
+
exitCode = runList(opts);
|
|
295
|
+
break;
|
|
296
|
+
case 'get':
|
|
297
|
+
exitCode = runGet(opts);
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
console.error(`Unknown command: ${opts.command}`);
|
|
301
|
+
exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
process.exit(exitCode);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@claude-agent/envcheck",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Validate .env files, compare with .env.example, find missing or empty variables",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"envcheck": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"env",
|
|
14
|
+
"dotenv",
|
|
15
|
+
"environment",
|
|
16
|
+
"variables",
|
|
17
|
+
"validate",
|
|
18
|
+
"lint",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"author": "Claude Agent <claude-agent@agentmail.to>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/claude-agent-tools/envcheck.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/claude-agent-tools/envcheck/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/claude-agent-tools/envcheck#readme",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/",
|
|
36
|
+
"bin/"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a .env file into an object
|
|
8
|
+
* @param {string} content - File content
|
|
9
|
+
* @returns {Object} Parsed variables
|
|
10
|
+
*/
|
|
11
|
+
function parseEnv(content) {
|
|
12
|
+
const result = {
|
|
13
|
+
variables: {},
|
|
14
|
+
errors: [],
|
|
15
|
+
warnings: [],
|
|
16
|
+
lineInfo: {}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const lineNum = i + 1;
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
|
|
26
|
+
// Skip empty lines and comments
|
|
27
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for valid format
|
|
32
|
+
const eqIndex = line.indexOf('=');
|
|
33
|
+
if (eqIndex === -1) {
|
|
34
|
+
result.errors.push({
|
|
35
|
+
line: lineNum,
|
|
36
|
+
message: `Invalid syntax: missing '=' sign`,
|
|
37
|
+
content: line
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const key = line.slice(0, eqIndex).trim();
|
|
43
|
+
let value = line.slice(eqIndex + 1);
|
|
44
|
+
|
|
45
|
+
// Validate key
|
|
46
|
+
if (!key) {
|
|
47
|
+
result.errors.push({
|
|
48
|
+
line: lineNum,
|
|
49
|
+
message: 'Empty variable name',
|
|
50
|
+
content: line
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
56
|
+
result.warnings.push({
|
|
57
|
+
line: lineNum,
|
|
58
|
+
message: `Variable name '${key}' contains unusual characters`,
|
|
59
|
+
content: line
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for duplicate
|
|
64
|
+
if (key in result.variables) {
|
|
65
|
+
result.warnings.push({
|
|
66
|
+
line: lineNum,
|
|
67
|
+
message: `Duplicate variable '${key}' (previous at line ${result.lineInfo[key]})`,
|
|
68
|
+
content: line
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse value (handle quotes)
|
|
73
|
+
value = parseValue(value);
|
|
74
|
+
|
|
75
|
+
result.variables[key] = value;
|
|
76
|
+
result.lineInfo[key] = lineNum;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a value, handling quotes and escapes
|
|
84
|
+
* @param {string} raw - Raw value string
|
|
85
|
+
* @returns {string} Parsed value
|
|
86
|
+
*/
|
|
87
|
+
function parseValue(raw) {
|
|
88
|
+
let value = raw;
|
|
89
|
+
|
|
90
|
+
// Remove inline comments (but not if inside quotes)
|
|
91
|
+
if (!value.startsWith('"') && !value.startsWith("'")) {
|
|
92
|
+
const hashIndex = value.indexOf(' #');
|
|
93
|
+
if (hashIndex !== -1) {
|
|
94
|
+
value = value.slice(0, hashIndex);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
value = value.trim();
|
|
99
|
+
|
|
100
|
+
// Handle quoted values
|
|
101
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
102
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
103
|
+
value = value.slice(1, -1);
|
|
104
|
+
// Handle escape sequences in double quotes
|
|
105
|
+
if (raw.trim().startsWith('"')) {
|
|
106
|
+
value = value.replace(/\\n/g, '\n')
|
|
107
|
+
.replace(/\\r/g, '\r')
|
|
108
|
+
.replace(/\\t/g, '\t')
|
|
109
|
+
.replace(/\\\\/g, '\\')
|
|
110
|
+
.replace(/\\"/g, '"');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read and parse a .env file
|
|
119
|
+
* @param {string} filePath - Path to file
|
|
120
|
+
* @returns {Object} Parsed result
|
|
121
|
+
*/
|
|
122
|
+
function readEnvFile(filePath) {
|
|
123
|
+
const absolutePath = path.resolve(filePath);
|
|
124
|
+
|
|
125
|
+
if (!fs.existsSync(absolutePath)) {
|
|
126
|
+
return {
|
|
127
|
+
exists: false,
|
|
128
|
+
path: absolutePath,
|
|
129
|
+
variables: {},
|
|
130
|
+
errors: [{ message: `File not found: ${absolutePath}` }],
|
|
131
|
+
warnings: [],
|
|
132
|
+
lineInfo: {}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
137
|
+
const result = parseEnv(content);
|
|
138
|
+
result.exists = true;
|
|
139
|
+
result.path = absolutePath;
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Compare two env files
|
|
146
|
+
* @param {string} envPath - Path to .env file
|
|
147
|
+
* @param {string} examplePath - Path to .env.example file
|
|
148
|
+
* @returns {Object} Comparison result
|
|
149
|
+
*/
|
|
150
|
+
function compare(envPath, examplePath) {
|
|
151
|
+
const env = readEnvFile(envPath);
|
|
152
|
+
const example = readEnvFile(examplePath);
|
|
153
|
+
|
|
154
|
+
const result = {
|
|
155
|
+
env,
|
|
156
|
+
example,
|
|
157
|
+
missing: [], // In example but not in env
|
|
158
|
+
extra: [], // In env but not in example
|
|
159
|
+
empty: [], // In both but empty in env
|
|
160
|
+
different: [] // Different values (if example has values)
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (!env.exists || !example.exists) {
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const envKeys = new Set(Object.keys(env.variables));
|
|
168
|
+
const exampleKeys = new Set(Object.keys(example.variables));
|
|
169
|
+
|
|
170
|
+
// Find missing (in example but not in env)
|
|
171
|
+
for (const key of exampleKeys) {
|
|
172
|
+
if (!envKeys.has(key)) {
|
|
173
|
+
result.missing.push({
|
|
174
|
+
key,
|
|
175
|
+
exampleValue: example.variables[key],
|
|
176
|
+
line: example.lineInfo[key]
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Find extra (in env but not in example)
|
|
182
|
+
for (const key of envKeys) {
|
|
183
|
+
if (!exampleKeys.has(key)) {
|
|
184
|
+
result.extra.push({
|
|
185
|
+
key,
|
|
186
|
+
value: env.variables[key],
|
|
187
|
+
line: env.lineInfo[key]
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Find empty values
|
|
193
|
+
for (const key of envKeys) {
|
|
194
|
+
if (exampleKeys.has(key) && env.variables[key] === '') {
|
|
195
|
+
result.empty.push({
|
|
196
|
+
key,
|
|
197
|
+
line: env.lineInfo[key]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Validate an env file
|
|
207
|
+
* @param {string} filePath - Path to .env file
|
|
208
|
+
* @param {Object} options - Validation options
|
|
209
|
+
* @returns {Object} Validation result
|
|
210
|
+
*/
|
|
211
|
+
function validate(filePath, options = {}) {
|
|
212
|
+
const { required = [], noEmpty = false } = options;
|
|
213
|
+
|
|
214
|
+
const env = readEnvFile(filePath);
|
|
215
|
+
|
|
216
|
+
const result = {
|
|
217
|
+
valid: true,
|
|
218
|
+
env,
|
|
219
|
+
issues: []
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (!env.exists) {
|
|
223
|
+
result.valid = false;
|
|
224
|
+
result.issues.push({
|
|
225
|
+
type: 'error',
|
|
226
|
+
message: `File not found: ${filePath}`
|
|
227
|
+
});
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check for parse errors
|
|
232
|
+
if (env.errors.length > 0) {
|
|
233
|
+
result.valid = false;
|
|
234
|
+
for (const error of env.errors) {
|
|
235
|
+
result.issues.push({
|
|
236
|
+
type: 'error',
|
|
237
|
+
line: error.line,
|
|
238
|
+
message: error.message
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check required variables
|
|
244
|
+
for (const key of required) {
|
|
245
|
+
if (!(key in env.variables)) {
|
|
246
|
+
result.valid = false;
|
|
247
|
+
result.issues.push({
|
|
248
|
+
type: 'error',
|
|
249
|
+
message: `Missing required variable: ${key}`
|
|
250
|
+
});
|
|
251
|
+
} else if (env.variables[key] === '') {
|
|
252
|
+
result.valid = false;
|
|
253
|
+
result.issues.push({
|
|
254
|
+
type: 'error',
|
|
255
|
+
line: env.lineInfo[key],
|
|
256
|
+
message: `Required variable '${key}' is empty`
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check for empty values
|
|
262
|
+
if (noEmpty) {
|
|
263
|
+
for (const [key, value] of Object.entries(env.variables)) {
|
|
264
|
+
if (value === '' && !required.includes(key)) {
|
|
265
|
+
result.issues.push({
|
|
266
|
+
type: 'warning',
|
|
267
|
+
line: env.lineInfo[key],
|
|
268
|
+
message: `Variable '${key}' is empty`
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Add warnings
|
|
275
|
+
for (const warning of env.warnings) {
|
|
276
|
+
result.issues.push({
|
|
277
|
+
type: 'warning',
|
|
278
|
+
line: warning.line,
|
|
279
|
+
message: warning.message
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Check an env file against example and validate
|
|
288
|
+
* @param {string} envPath - Path to .env
|
|
289
|
+
* @param {Object} options - Check options
|
|
290
|
+
* @returns {Object} Check result
|
|
291
|
+
*/
|
|
292
|
+
function check(envPath, options = {}) {
|
|
293
|
+
const {
|
|
294
|
+
examplePath = null,
|
|
295
|
+
required = [],
|
|
296
|
+
noEmpty = false,
|
|
297
|
+
noExtra = false,
|
|
298
|
+
strict = false
|
|
299
|
+
} = options;
|
|
300
|
+
|
|
301
|
+
const result = {
|
|
302
|
+
valid: true,
|
|
303
|
+
issues: [],
|
|
304
|
+
summary: {
|
|
305
|
+
errors: 0,
|
|
306
|
+
warnings: 0
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// First validate the env file
|
|
311
|
+
const validation = validate(envPath, { required, noEmpty });
|
|
312
|
+
|
|
313
|
+
if (!validation.valid) {
|
|
314
|
+
result.valid = false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
result.issues.push(...validation.issues);
|
|
318
|
+
result.env = validation.env;
|
|
319
|
+
|
|
320
|
+
// Then compare with example if provided
|
|
321
|
+
if (examplePath) {
|
|
322
|
+
const comparison = compare(envPath, examplePath);
|
|
323
|
+
result.comparison = comparison;
|
|
324
|
+
|
|
325
|
+
// Missing variables are errors
|
|
326
|
+
for (const item of comparison.missing) {
|
|
327
|
+
result.valid = false;
|
|
328
|
+
result.issues.push({
|
|
329
|
+
type: 'error',
|
|
330
|
+
message: `Missing variable '${item.key}' (defined in example at line ${item.line})`
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Empty variables are warnings (or errors in strict mode)
|
|
335
|
+
for (const item of comparison.empty) {
|
|
336
|
+
const type = strict ? 'error' : 'warning';
|
|
337
|
+
if (strict) result.valid = false;
|
|
338
|
+
result.issues.push({
|
|
339
|
+
type,
|
|
340
|
+
line: item.line,
|
|
341
|
+
message: `Variable '${item.key}' is empty`
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Extra variables are warnings (or errors if noExtra)
|
|
346
|
+
if (noExtra) {
|
|
347
|
+
for (const item of comparison.extra) {
|
|
348
|
+
result.valid = false;
|
|
349
|
+
result.issues.push({
|
|
350
|
+
type: 'error',
|
|
351
|
+
line: item.line,
|
|
352
|
+
message: `Extra variable '${item.key}' not in example`
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Count issues
|
|
359
|
+
for (const issue of result.issues) {
|
|
360
|
+
if (issue.type === 'error') {
|
|
361
|
+
result.summary.errors++;
|
|
362
|
+
} else {
|
|
363
|
+
result.summary.warnings++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Generate a template .env from .env.example
|
|
372
|
+
* @param {string} examplePath - Path to example file
|
|
373
|
+
* @param {Object} defaults - Default values to fill in
|
|
374
|
+
* @returns {string} Generated .env content
|
|
375
|
+
*/
|
|
376
|
+
function generate(examplePath, defaults = {}) {
|
|
377
|
+
const example = readEnvFile(examplePath);
|
|
378
|
+
|
|
379
|
+
if (!example.exists) {
|
|
380
|
+
throw new Error(`Example file not found: ${examplePath}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const lines = [];
|
|
384
|
+
|
|
385
|
+
for (const [key, value] of Object.entries(example.variables)) {
|
|
386
|
+
const newValue = key in defaults ? defaults[key] : value;
|
|
387
|
+
lines.push(`${key}=${newValue}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return lines.join('\n') + '\n';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get list of variables from file
|
|
395
|
+
* @param {string} filePath - Path to env file
|
|
396
|
+
* @returns {string[]} Variable names
|
|
397
|
+
*/
|
|
398
|
+
function list(filePath) {
|
|
399
|
+
const env = readEnvFile(filePath);
|
|
400
|
+
return Object.keys(env.variables);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get a specific variable value
|
|
405
|
+
* @param {string} filePath - Path to env file
|
|
406
|
+
* @param {string} key - Variable name
|
|
407
|
+
* @returns {string|undefined} Value or undefined
|
|
408
|
+
*/
|
|
409
|
+
function get(filePath, key) {
|
|
410
|
+
const env = readEnvFile(filePath);
|
|
411
|
+
return env.variables[key];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
parse: parseEnv,
|
|
416
|
+
parseValue,
|
|
417
|
+
readEnvFile,
|
|
418
|
+
compare,
|
|
419
|
+
validate,
|
|
420
|
+
check,
|
|
421
|
+
generate,
|
|
422
|
+
list,
|
|
423
|
+
get
|
|
424
|
+
};
|