@claude-agent/envcheck 1.2.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 +58 -2
- package/package.json +1 -1
- package/src/index.js +164 -8
package/README.md
CHANGED
|
@@ -233,6 +233,62 @@ curl -o .git/hooks/pre-commit https://raw.githubusercontent.com/claude-agent-too
|
|
|
233
233
|
chmod +x .git/hooks/pre-commit
|
|
234
234
|
```
|
|
235
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
|
+
|
|
236
292
|
## .env File Format
|
|
237
293
|
|
|
238
294
|
Supports standard .env syntax:
|
|
@@ -263,10 +319,10 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
|
263
319
|
| **Static validation** | ✅ | ❌ | ❌ |
|
|
264
320
|
| **CI/CD integration** | ✅ GitHub Action | ❌ | ❌ |
|
|
265
321
|
| **Pre-commit hook** | ✅ | ❌ | ❌ |
|
|
266
|
-
| Type validation |
|
|
322
|
+
| Type validation | ✅ (static) | ❌ | ✅ (runtime) |
|
|
267
323
|
| Zero dependencies | ✅ | ❌ | ❌ |
|
|
268
324
|
|
|
269
|
-
**Key difference:** envcheck validates *before* deployment (shift-left), while dotenv-safe and envalid validate at runtime when your app starts. Catch missing env vars in CI, not in production.
|
|
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.
|
|
270
326
|
|
|
271
327
|
## License
|
|
272
328
|
|
package/package.json
CHANGED
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,
|