@claude-agent/envcheck 1.1.0 → 1.3.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 +98 -1
- package/package.json +6 -3
- package/pre-commit-hook.sh +53 -0
- package/src/index.js +164 -8
package/README.md
CHANGED
|
@@ -190,7 +190,9 @@ Extra (in env but not in example):
|
|
|
190
190
|
- **Documentation**: List required variables from example file
|
|
191
191
|
- **Debugging**: Compare env files across environments
|
|
192
192
|
|
|
193
|
-
##
|
|
193
|
+
## CI/CD Integration
|
|
194
|
+
|
|
195
|
+
### GitHub Action
|
|
194
196
|
|
|
195
197
|
Use envcheck in your GitHub Actions workflow:
|
|
196
198
|
|
|
@@ -206,6 +208,87 @@ Use envcheck in your GitHub Actions workflow:
|
|
|
206
208
|
|
|
207
209
|
See [action/README.md](./action/README.md) for full documentation.
|
|
208
210
|
|
|
211
|
+
### Pre-commit Hook
|
|
212
|
+
|
|
213
|
+
Use with the [pre-commit](https://pre-commit.com/) framework:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
# .pre-commit-config.yaml
|
|
217
|
+
repos:
|
|
218
|
+
- repo: local
|
|
219
|
+
hooks:
|
|
220
|
+
- id: envcheck
|
|
221
|
+
name: Validate environment variables
|
|
222
|
+
entry: npx @claude-agent/envcheck
|
|
223
|
+
language: system
|
|
224
|
+
files: '\.env.*'
|
|
225
|
+
pass_filenames: false
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Or install the hook directly:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Copy hook to git hooks directory
|
|
232
|
+
curl -o .git/hooks/pre-commit https://raw.githubusercontent.com/claude-agent-tools/envcheck/master/pre-commit-hook.sh
|
|
233
|
+
chmod +x .git/hooks/pre-commit
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Type Validation
|
|
237
|
+
|
|
238
|
+
envcheck supports **static type validation** - validate variable formats without running your app.
|
|
239
|
+
|
|
240
|
+
### Using Type Hints in .env.example
|
|
241
|
+
|
|
242
|
+
Add type hints as comments above variables:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# type: url
|
|
246
|
+
DATABASE_URL=postgres://localhost/mydb
|
|
247
|
+
|
|
248
|
+
# @type port
|
|
249
|
+
PORT=3000
|
|
250
|
+
|
|
251
|
+
# type: boolean
|
|
252
|
+
DEBUG=false
|
|
253
|
+
|
|
254
|
+
# type: email
|
|
255
|
+
ADMIN_EMAIL=admin@example.com
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Supported Types
|
|
259
|
+
|
|
260
|
+
| Type | Description | Examples |
|
|
261
|
+
|------|-------------|----------|
|
|
262
|
+
| `url` | Valid URL | `https://example.com`, `postgres://host/db` |
|
|
263
|
+
| `port` | Port number (1-65535) | `3000`, `8080` |
|
|
264
|
+
| `boolean`/`bool` | Boolean values | `true`, `false`, `1`, `0`, `yes`, `no` |
|
|
265
|
+
| `email` | Email address | `user@example.com` |
|
|
266
|
+
| `number` | Any number | `42`, `3.14`, `-10` |
|
|
267
|
+
| `integer`/`int` | Whole numbers | `42`, `-10` |
|
|
268
|
+
| `json` | Valid JSON | `{"key":"value"}`, `[1,2,3]` |
|
|
269
|
+
| `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` |
|
|
270
|
+
| `string`/`str` | Any string (no validation) | anything |
|
|
271
|
+
|
|
272
|
+
### API Usage with Types
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
const { check, validate } = require('@claude-agent/envcheck');
|
|
276
|
+
|
|
277
|
+
// Explicit type validation
|
|
278
|
+
const result = validate('.env', {
|
|
279
|
+
types: {
|
|
280
|
+
DATABASE_URL: 'url',
|
|
281
|
+
PORT: 'port',
|
|
282
|
+
DEBUG: 'boolean'
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Types from example file are used automatically
|
|
287
|
+
const result2 = check('.env', {
|
|
288
|
+
examplePath: '.env.example' // Type hints in example are used
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
209
292
|
## .env File Format
|
|
210
293
|
|
|
211
294
|
Supports standard .env syntax:
|
|
@@ -227,6 +310,20 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
|
227
310
|
- **Comprehensive** - Parse, validate, compare, generate
|
|
228
311
|
- **Well-tested** - 46 tests covering edge cases
|
|
229
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.
|
|
326
|
+
|
|
230
327
|
## License
|
|
231
328
|
|
|
232
329
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-agent/envcheck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"lint",
|
|
19
19
|
"cli",
|
|
20
20
|
"github-action",
|
|
21
|
-
"ci-cd"
|
|
21
|
+
"ci-cd",
|
|
22
|
+
"pre-commit",
|
|
23
|
+
"git-hooks"
|
|
22
24
|
],
|
|
23
25
|
"author": "Claude Agent <claude-agent@agentmail.to>",
|
|
24
26
|
"license": "MIT",
|
|
@@ -35,6 +37,7 @@
|
|
|
35
37
|
},
|
|
36
38
|
"files": [
|
|
37
39
|
"src/",
|
|
38
|
-
"bin/"
|
|
40
|
+
"bin/",
|
|
41
|
+
"pre-commit-hook.sh"
|
|
39
42
|
]
|
|
40
43
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-commit hook for envcheck
|
|
3
|
+
# Add to .git/hooks/pre-commit or use with pre-commit framework
|
|
4
|
+
#
|
|
5
|
+
# Usage with pre-commit framework (.pre-commit-config.yaml):
|
|
6
|
+
# - repo: local
|
|
7
|
+
# hooks:
|
|
8
|
+
# - id: envcheck
|
|
9
|
+
# name: Validate environment variables
|
|
10
|
+
# entry: npx @claude-agent/envcheck
|
|
11
|
+
# language: system
|
|
12
|
+
# files: '\.env.*'
|
|
13
|
+
# pass_filenames: false
|
|
14
|
+
#
|
|
15
|
+
# Or install directly:
|
|
16
|
+
# cp pre-commit-hook.sh .git/hooks/pre-commit
|
|
17
|
+
# chmod +x .git/hooks/pre-commit
|
|
18
|
+
|
|
19
|
+
set -e
|
|
20
|
+
|
|
21
|
+
# Check if envcheck is available
|
|
22
|
+
if command -v envcheck &> /dev/null; then
|
|
23
|
+
ENVCHECK="envcheck"
|
|
24
|
+
elif command -v npx &> /dev/null; then
|
|
25
|
+
ENVCHECK="npx @claude-agent/envcheck"
|
|
26
|
+
else
|
|
27
|
+
echo "Warning: envcheck not found, skipping env validation"
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Find .env files being committed
|
|
32
|
+
ENV_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.env(\..+)?$' || true)
|
|
33
|
+
|
|
34
|
+
if [ -z "$ENV_FILES" ]; then
|
|
35
|
+
# No .env files being committed
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo "Validating environment files..."
|
|
40
|
+
|
|
41
|
+
# Run envcheck on each modified env file
|
|
42
|
+
for file in $ENV_FILES; do
|
|
43
|
+
if [ -f "$file" ]; then
|
|
44
|
+
echo "Checking $file..."
|
|
45
|
+
$ENVCHECK "$file" --quiet || {
|
|
46
|
+
echo "Error: Environment validation failed for $file"
|
|
47
|
+
exit 1
|
|
48
|
+
}
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
echo "Environment validation passed"
|
|
53
|
+
exit 0
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,90 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Type validators for environment variables
|
|
8
|
+
*/
|
|
9
|
+
const typeValidators = {
|
|
10
|
+
url: (value) => {
|
|
11
|
+
if (!value) return { valid: true };
|
|
12
|
+
try {
|
|
13
|
+
new URL(value);
|
|
14
|
+
return { valid: true };
|
|
15
|
+
} catch {
|
|
16
|
+
return { valid: false, message: 'must be a valid URL' };
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
port: (value) => {
|
|
21
|
+
if (!value) return { valid: true };
|
|
22
|
+
const num = parseInt(value, 10);
|
|
23
|
+
if (isNaN(num) || num < 1 || num > 65535 || String(num) !== value) {
|
|
24
|
+
return { valid: false, message: 'must be a valid port number (1-65535)' };
|
|
25
|
+
}
|
|
26
|
+
return { valid: true };
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
boolean: (value) => {
|
|
30
|
+
if (!value) return { valid: true };
|
|
31
|
+
const lower = value.toLowerCase();
|
|
32
|
+
const valid = ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off'].includes(lower);
|
|
33
|
+
if (!valid) {
|
|
34
|
+
return { valid: false, message: 'must be a boolean (true/false/1/0/yes/no)' };
|
|
35
|
+
}
|
|
36
|
+
return { valid: true };
|
|
37
|
+
},
|
|
38
|
+
bool: (value) => typeValidators.boolean(value),
|
|
39
|
+
|
|
40
|
+
email: (value) => {
|
|
41
|
+
if (!value) return { valid: true };
|
|
42
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
43
|
+
if (!emailRegex.test(value)) {
|
|
44
|
+
return { valid: false, message: 'must be a valid email address' };
|
|
45
|
+
}
|
|
46
|
+
return { valid: true };
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
number: (value) => {
|
|
50
|
+
if (!value) return { valid: true };
|
|
51
|
+
if (isNaN(parseFloat(value))) {
|
|
52
|
+
return { valid: false, message: 'must be a number' };
|
|
53
|
+
}
|
|
54
|
+
return { valid: true };
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
integer: (value) => {
|
|
58
|
+
if (!value) return { valid: true };
|
|
59
|
+
const num = parseInt(value, 10);
|
|
60
|
+
if (isNaN(num) || String(num) !== value) {
|
|
61
|
+
return { valid: false, message: 'must be an integer' };
|
|
62
|
+
}
|
|
63
|
+
return { valid: true };
|
|
64
|
+
},
|
|
65
|
+
int: (value) => typeValidators.integer(value),
|
|
66
|
+
|
|
67
|
+
string: () => ({ valid: true }),
|
|
68
|
+
str: () => ({ valid: true }),
|
|
69
|
+
|
|
70
|
+
json: (value) => {
|
|
71
|
+
if (!value) return { valid: true };
|
|
72
|
+
try {
|
|
73
|
+
JSON.parse(value);
|
|
74
|
+
return { valid: true };
|
|
75
|
+
} catch {
|
|
76
|
+
return { valid: false, message: 'must be valid JSON' };
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
uuid: (value) => {
|
|
81
|
+
if (!value) return { valid: true };
|
|
82
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
83
|
+
if (!uuidRegex.test(value)) {
|
|
84
|
+
return { valid: false, message: 'must be a valid UUID' };
|
|
85
|
+
}
|
|
86
|
+
return { valid: true };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
6
90
|
/**
|
|
7
91
|
* Parse a .env file into an object
|
|
8
92
|
* @param {string} content - File content
|
|
@@ -13,18 +97,30 @@ function parseEnv(content) {
|
|
|
13
97
|
variables: {},
|
|
14
98
|
errors: [],
|
|
15
99
|
warnings: [],
|
|
16
|
-
lineInfo: {}
|
|
100
|
+
lineInfo: {},
|
|
101
|
+
typeHints: {} // NEW: Store type hints from comments
|
|
17
102
|
};
|
|
18
103
|
|
|
19
104
|
const lines = content.split('\n');
|
|
105
|
+
let pendingTypeHint = null;
|
|
20
106
|
|
|
21
107
|
for (let i = 0; i < lines.length; i++) {
|
|
22
108
|
const lineNum = i + 1;
|
|
23
109
|
const line = lines[i];
|
|
24
110
|
const trimmed = line.trim();
|
|
25
111
|
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
112
|
+
// Check for type hint in comments: # type: url OR # @type url
|
|
113
|
+
if (trimmed.startsWith('#')) {
|
|
114
|
+
const typeMatch = trimmed.match(/^#\s*(?:type:|@type)\s*(\w+)/i);
|
|
115
|
+
if (typeMatch) {
|
|
116
|
+
pendingTypeHint = typeMatch[1].toLowerCase();
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Skip empty lines
|
|
122
|
+
if (!trimmed) {
|
|
123
|
+
pendingTypeHint = null;
|
|
28
124
|
continue;
|
|
29
125
|
}
|
|
30
126
|
|
|
@@ -74,6 +170,12 @@ function parseEnv(content) {
|
|
|
74
170
|
|
|
75
171
|
result.variables[key] = value;
|
|
76
172
|
result.lineInfo[key] = lineNum;
|
|
173
|
+
|
|
174
|
+
// Store type hint if present
|
|
175
|
+
if (pendingTypeHint) {
|
|
176
|
+
result.typeHints[key] = pendingTypeHint;
|
|
177
|
+
pendingTypeHint = null;
|
|
178
|
+
}
|
|
77
179
|
}
|
|
78
180
|
|
|
79
181
|
return result;
|
|
@@ -129,7 +231,8 @@ function readEnvFile(filePath) {
|
|
|
129
231
|
variables: {},
|
|
130
232
|
errors: [{ message: `File not found: ${absolutePath}` }],
|
|
131
233
|
warnings: [],
|
|
132
|
-
lineInfo: {}
|
|
234
|
+
lineInfo: {},
|
|
235
|
+
typeHints: {}
|
|
133
236
|
};
|
|
134
237
|
}
|
|
135
238
|
|
|
@@ -202,6 +305,20 @@ function compare(envPath, examplePath) {
|
|
|
202
305
|
return result;
|
|
203
306
|
}
|
|
204
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Validate a value against a type
|
|
310
|
+
* @param {string} value - Value to validate
|
|
311
|
+
* @param {string} type - Type name
|
|
312
|
+
* @returns {Object} Validation result {valid, message}
|
|
313
|
+
*/
|
|
314
|
+
function validateType(value, type) {
|
|
315
|
+
const validator = typeValidators[type.toLowerCase()];
|
|
316
|
+
if (!validator) {
|
|
317
|
+
return { valid: true, message: `Unknown type '${type}'` };
|
|
318
|
+
}
|
|
319
|
+
return validator(value);
|
|
320
|
+
}
|
|
321
|
+
|
|
205
322
|
/**
|
|
206
323
|
* Validate an env file
|
|
207
324
|
* @param {string} filePath - Path to .env file
|
|
@@ -209,7 +326,7 @@ function compare(envPath, examplePath) {
|
|
|
209
326
|
* @returns {Object} Validation result
|
|
210
327
|
*/
|
|
211
328
|
function validate(filePath, options = {}) {
|
|
212
|
-
const { required = [], noEmpty = false } = options;
|
|
329
|
+
const { required = [], noEmpty = false, types = {}, validateTypes = false } = options;
|
|
213
330
|
|
|
214
331
|
const env = readEnvFile(filePath);
|
|
215
332
|
|
|
@@ -271,6 +388,24 @@ function validate(filePath, options = {}) {
|
|
|
271
388
|
}
|
|
272
389
|
}
|
|
273
390
|
|
|
391
|
+
// Type validation (from options.types or env.typeHints)
|
|
392
|
+
if (validateTypes || Object.keys(types).length > 0) {
|
|
393
|
+
const allTypes = { ...env.typeHints, ...types }; // options.types override hints
|
|
394
|
+
for (const [key, typeName] of Object.entries(allTypes)) {
|
|
395
|
+
if (key in env.variables && env.variables[key] !== '') {
|
|
396
|
+
const typeResult = validateType(env.variables[key], typeName);
|
|
397
|
+
if (!typeResult.valid) {
|
|
398
|
+
result.valid = false;
|
|
399
|
+
result.issues.push({
|
|
400
|
+
type: 'error',
|
|
401
|
+
line: env.lineInfo[key],
|
|
402
|
+
message: `Variable '${key}' ${typeResult.message} (got: ${env.variables[key]})`
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
274
409
|
// Add warnings
|
|
275
410
|
for (const warning of env.warnings) {
|
|
276
411
|
result.issues.push({
|
|
@@ -295,7 +430,9 @@ function check(envPath, options = {}) {
|
|
|
295
430
|
required = [],
|
|
296
431
|
noEmpty = false,
|
|
297
432
|
noExtra = false,
|
|
298
|
-
strict = false
|
|
433
|
+
strict = false,
|
|
434
|
+
types = {},
|
|
435
|
+
validateTypes = false
|
|
299
436
|
} = options;
|
|
300
437
|
|
|
301
438
|
const result = {
|
|
@@ -307,8 +444,25 @@ function check(envPath, options = {}) {
|
|
|
307
444
|
}
|
|
308
445
|
};
|
|
309
446
|
|
|
310
|
-
//
|
|
311
|
-
|
|
447
|
+
// Get type hints from example file if provided
|
|
448
|
+
let exampleTypeHints = {};
|
|
449
|
+
if (examplePath) {
|
|
450
|
+
const example = readEnvFile(examplePath);
|
|
451
|
+
if (example.exists) {
|
|
452
|
+
exampleTypeHints = example.typeHints || {};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Merge type hints: example hints < explicit types
|
|
457
|
+
const mergedTypes = { ...exampleTypeHints, ...types };
|
|
458
|
+
|
|
459
|
+
// First validate the env file (including type validation)
|
|
460
|
+
const validation = validate(envPath, {
|
|
461
|
+
required,
|
|
462
|
+
noEmpty,
|
|
463
|
+
types: mergedTypes,
|
|
464
|
+
validateTypes: validateTypes || Object.keys(mergedTypes).length > 0
|
|
465
|
+
});
|
|
312
466
|
|
|
313
467
|
if (!validation.valid) {
|
|
314
468
|
result.valid = false;
|
|
@@ -417,6 +571,8 @@ module.exports = {
|
|
|
417
571
|
readEnvFile,
|
|
418
572
|
compare,
|
|
419
573
|
validate,
|
|
574
|
+
validateType,
|
|
575
|
+
typeValidators,
|
|
420
576
|
check,
|
|
421
577
|
generate,
|
|
422
578
|
list,
|