@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 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
- ## GitHub Action
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.1.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": {
@@ -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
- // 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,