@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.
Files changed (3) hide show
  1. package/README.md +58 -2
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-agent/envcheck",
3
- "version": "1.2.0",
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": {
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
- // Skip empty lines and comments
27
- if (!trimmed || trimmed.startsWith('#')) {
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
- // First validate the env file
311
- const validation = validate(envPath, { required, noEmpty });
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,