@gokul-kannur/env-guard 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.
@@ -0,0 +1,11 @@
1
+ # Pre-commit hooks for env-guard
2
+ # See https://pre-commit.com for more information
3
+
4
+ - id: env-guard
5
+ name: env-guard
6
+ description: Validate .env files for common mistakes
7
+ entry: env-guard
8
+ language: node
9
+ files: '\.env$'
10
+ types: [text]
11
+ pass_filenames: true
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GokulKannur
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,135 @@
1
+ # env-guard
2
+
3
+ Catch stupid .env mistakes before deploy. No sync. No presets. Just validation.
4
+
5
+ ## Why?
6
+
7
+ Because you've broken production due to:
8
+
9
+ - Missing env variable
10
+ - Typo in variable name
11
+ - Empty value you forgot to fill
12
+ - `DEBUG=true` in production
13
+
14
+ This tool catches those. Nothing more.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install -g env-guard
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ # Check current directory
26
+ env-guard
27
+
28
+ # Check specific file
29
+ env-guard .env.production
30
+ ```
31
+
32
+ ### Common Options
33
+
34
+ ```bash
35
+ # Compare against a specific example file
36
+ env-guard --example .env.template
37
+
38
+ # Strict mode (treat all empty values as errors)
39
+ env-guard --strict
40
+
41
+ # CI mode (GitHub Actions annotations)
42
+ env-guard --ci
43
+ ```
44
+
45
+ > **Note**: If `.env.example` (or the specified example file) is missing, comparison checks are skipped.
46
+
47
+ That's it. It will:
48
+
49
+ 1. Read `.env`
50
+ 2. Compare with `.env.example` (if exists)
51
+ 3. Print errors/warnings
52
+ 4. Exit with code 1 if errors found
53
+
54
+ ## What it checks
55
+
56
+ | Check | Type | Description |
57
+ |-------|------|-------------|
58
+ | Missing vars | Error | Key in example but not in env |
59
+ | Duplicates | Error | Same key defined twice |
60
+ | Empty values | Warning* | Key has no value |
61
+ | Unused vars | Warning | Key in env but not in example |
62
+ | Unsafe defaults | Warning | `DEBUG=true`, `NODE_ENV=development`, etc. |
63
+
64
+ *Empty values become errors in `--strict` mode or for sensitive keys (DATABASE_*, PASSWORD, etc.)
65
+
66
+ ## Example output
67
+
68
+ ```
69
+ 🔍 env-guard validation report
70
+
71
+ Errors (2)
72
+ ❌ Missing variable: DATABASE_URL (defined in example)
73
+ ❌ Duplicate key: API_KEY (lines 5 and 12)
74
+
75
+ Warnings (3)
76
+ ⚠️ Empty value: OPTIONAL_VAR
77
+ ⚠️ Unused variable: OLD_CONFIG (not in example)
78
+ ⚠️ Unsafe default: DEBUG=true (may not be suitable for production)
79
+
80
+ ─────────────────────────────
81
+ Total: 2 error(s), 3 warning(s)
82
+
83
+ env-guard: 2 errors, 3 warnings
84
+ ```
85
+
86
+ ## CI/CD Usage
87
+
88
+ ### GitHub Actions
89
+
90
+ ```yaml
91
+ - name: Validate env
92
+ run: npx env-guard --ci --strict
93
+ ```
94
+
95
+ The `--ci` flag outputs GitHub Actions annotations:
96
+ ```
97
+ ::error file=.env,line=5::Missing variable: DATABASE_URL
98
+ ::warning file=.env,line=12::Unsafe default: DEBUG=true
99
+ ```
100
+
101
+ ### Pre-commit Hook
102
+
103
+ This uses the CLI via a local hook, not a custom pre-commit plugin.
104
+
105
+ Add to your `.pre-commit-config.yaml`:
106
+
107
+ ```yaml
108
+ repos:
109
+ - repo: https://github.com/YOUR_USERNAME/env-guard
110
+ rev: v1.0.0
111
+ hooks:
112
+ - id: env-guard
113
+ ```
114
+
115
+ ## What this tool does NOT do
116
+
117
+ - ❌ Sync files
118
+ - ❌ Generate example files
119
+ - ❌ Manage secrets
120
+ - ❌ Framework integrations
121
+ - ❌ Custom rules
122
+ - ❌ Type validation
123
+
124
+ If you need those, use something else.
125
+
126
+
127
+
128
+ ## Security & Privacy
129
+
130
+ - **Runs locally**: Your env vars never leave your machine.
131
+ - **No telemetry**: We don't track you.
132
+ - **No logging**: Values are never printed to stdout (except masked) or logged to files.
133
+ - **No external calls**: No analytics, no updates checks, nothing.
134
+
135
+ MIT
package/bin/env-guard ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/index.js');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const parse_1 = require("./parse");
41
+ const rules_1 = require("./rules");
42
+ const report_1 = require("./report");
43
+ const program = new commander_1.Command();
44
+ program
45
+ .name('env-guard')
46
+ .description('Catch stupid .env mistakes before deploy.')
47
+ .version('1.0.0')
48
+ .argument('[envFile]', 'Path to .env file', '.env')
49
+ .option('--example <file>', 'Path to .env.example file', '.env.example')
50
+ .option('--strict', 'Treat all empty values as errors')
51
+ .option('--json', 'Output as JSON')
52
+ .option('--ci', 'Output GitHub Actions annotations format')
53
+ .option('--no-example', 'Skip example file comparison')
54
+ .action((envFile, options) => {
55
+ try {
56
+ const envPath = path.resolve(process.cwd(), envFile);
57
+ if (!fs.existsSync(envPath)) {
58
+ console.error(`❌ Error: ${envFile} not found`);
59
+ process.exit(1);
60
+ }
61
+ const envResult = (0, parse_1.parseEnvFile)(envFile);
62
+ if (envResult.errors.length > 0) {
63
+ for (const error of envResult.errors) {
64
+ console.error(`❌ ${error}`);
65
+ }
66
+ process.exit(1);
67
+ }
68
+ let exampleVars = null;
69
+ if (options.example !== false) {
70
+ const examplePath = path.resolve(process.cwd(), options.example);
71
+ if (fs.existsSync(examplePath)) {
72
+ const exampleResult = (0, parse_1.parseEnvFile)(options.example);
73
+ if (exampleResult.errors.length === 0) {
74
+ exampleVars = exampleResult.vars;
75
+ }
76
+ }
77
+ }
78
+ const result = (0, rules_1.validateEnv)(envResult.vars, exampleVars, options.strict);
79
+ if (options.json) {
80
+ (0, report_1.printJson)(result);
81
+ }
82
+ else if (options.ci) {
83
+ (0, report_1.printCi)(result, envFile);
84
+ }
85
+ else {
86
+ (0, report_1.printReport)(result);
87
+ }
88
+ if (result.errors.length > 0) {
89
+ process.exit(1);
90
+ }
91
+ else {
92
+ process.exit(0);
93
+ }
94
+ }
95
+ catch (error) {
96
+ if (error instanceof Error) {
97
+ console.error(`❌ Error: ${error.message}`);
98
+ }
99
+ else {
100
+ console.error('❌ An unexpected error occurred');
101
+ }
102
+ process.exit(1);
103
+ }
104
+ });
105
+ program.parse();
@@ -0,0 +1,11 @@
1
+ export interface EnvVar {
2
+ key: string;
3
+ value: string;
4
+ line: number;
5
+ }
6
+ export interface ParseResult {
7
+ vars: EnvVar[];
8
+ errors: string[];
9
+ }
10
+ export declare function parseEnvFile(filePath: string): ParseResult;
11
+ export declare function getKeys(vars: EnvVar[]): Set<string>;
package/dist/parse.js ADDED
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseEnvFile = parseEnvFile;
37
+ exports.getKeys = getKeys;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ function parseEnvFile(filePath) {
41
+ const result = {
42
+ vars: [],
43
+ errors: [],
44
+ };
45
+ const absolutePath = path.resolve(process.cwd(), filePath);
46
+ if (!fs.existsSync(absolutePath)) {
47
+ result.errors.push(`File not found: ${filePath}`);
48
+ return result;
49
+ }
50
+ const content = fs.readFileSync(absolutePath, 'utf-8');
51
+ const lines = content.split('\n');
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i].trim();
54
+ const lineNumber = i + 1;
55
+ if (!line || line.startsWith('#')) {
56
+ continue;
57
+ }
58
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
59
+ if (!match) {
60
+ continue;
61
+ }
62
+ const [, key, value] = match;
63
+ let cleanValue = value;
64
+ if ((value.startsWith('"') && value.endsWith('"')) ||
65
+ (value.startsWith("'") && value.endsWith("'"))) {
66
+ cleanValue = value.slice(1, -1);
67
+ }
68
+ result.vars.push({
69
+ key,
70
+ value: cleanValue,
71
+ line: lineNumber,
72
+ });
73
+ }
74
+ return result;
75
+ }
76
+ function getKeys(vars) {
77
+ return new Set(vars.map(v => v.key));
78
+ }
@@ -0,0 +1,8 @@
1
+ import { ValidationResult } from './rules';
2
+ export declare function printReport(result: ValidationResult): void;
3
+ /**
4
+ * GitHub Actions annotation format
5
+ * https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
6
+ */
7
+ export declare function printCi(result: ValidationResult, envFile: string): void;
8
+ export declare function printJson(result: ValidationResult): void;
package/dist/report.js ADDED
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.printReport = printReport;
4
+ exports.printCi = printCi;
5
+ exports.printJson = printJson;
6
+ const colors = {
7
+ red: '\x1b[31m',
8
+ yellow: '\x1b[33m',
9
+ green: '\x1b[32m',
10
+ cyan: '\x1b[36m',
11
+ dim: '\x1b[2m',
12
+ reset: '\x1b[0m',
13
+ bold: '\x1b[1m',
14
+ };
15
+ function formatIssue(issue, isError) {
16
+ const icon = isError ? '❌' : '⚠️';
17
+ const color = isError ? colors.red : colors.yellow;
18
+ const lineInfo = (issue.line && !issue.hasInlineLineInfo) ? ` ${colors.dim}(line ${issue.line})${colors.reset}` : '';
19
+ return `${icon} ${color}${issue.message}${colors.reset}${lineInfo}`;
20
+ }
21
+ function printReport(result) {
22
+ const { errors, warnings } = result;
23
+ const total = errors.length + warnings.length;
24
+ console.log('');
25
+ console.log(`${colors.bold}🔍 env-guard validation report${colors.reset}`);
26
+ console.log('');
27
+ if (errors.length > 0) {
28
+ console.log(`${colors.red}${colors.bold}Errors (${errors.length})${colors.reset}`);
29
+ for (const error of errors) {
30
+ console.log(` ${formatIssue(error, true)}`);
31
+ }
32
+ console.log('');
33
+ }
34
+ if (warnings.length > 0) {
35
+ console.log(`${colors.yellow}${colors.bold}Warnings (${warnings.length})${colors.reset}`);
36
+ for (const warning of warnings) {
37
+ console.log(` ${formatIssue(warning, false)}`);
38
+ }
39
+ console.log('');
40
+ }
41
+ // Summary
42
+ if (total === 0) {
43
+ console.log(`${colors.green}✅ No issues found${colors.reset}`);
44
+ }
45
+ else {
46
+ console.log(`${colors.dim}─────────────────────────────${colors.reset}`);
47
+ console.log(`Total: ${errors.length} error(s), ${warnings.length} warning(s)`);
48
+ }
49
+ // One-line summary for logs
50
+ console.log('');
51
+ console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
52
+ }
53
+ /**
54
+ * GitHub Actions annotation format
55
+ * https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
56
+ */
57
+ function printCi(result, envFile) {
58
+ const { errors, warnings } = result;
59
+ // Output GitHub Actions annotations
60
+ for (const error of errors) {
61
+ const line = error.line ? `,line=${error.line}` : '';
62
+ console.log(`::error file=${envFile}${line}::${error.message}`);
63
+ }
64
+ for (const warning of warnings) {
65
+ const line = warning.line ? `,line=${warning.line}` : '';
66
+ console.log(`::warning file=${envFile}${line}::${warning.message}`);
67
+ }
68
+ // Summary line
69
+ if (errors.length === 0 && warnings.length === 0) {
70
+ console.log('env-guard: ✅ No issues found');
71
+ }
72
+ else {
73
+ console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
74
+ }
75
+ }
76
+ function printJson(result) {
77
+ console.log(JSON.stringify(result, null, 2));
78
+ }
@@ -0,0 +1,15 @@
1
+ import { EnvVar } from './parse';
2
+ export interface ValidationResult {
3
+ errors: ValidationIssue[];
4
+ warnings: ValidationIssue[];
5
+ }
6
+ export interface ValidationIssue {
7
+ type: string;
8
+ key: string;
9
+ message: string;
10
+ line?: number;
11
+ hasInlineLineInfo?: boolean;
12
+ }
13
+ export declare function isSensitiveKey(key: string): boolean;
14
+ export declare function maskValue(key: string, value: string): string;
15
+ export declare function validateEnv(envVars: EnvVar[], exampleVars: EnvVar[] | null, strict?: boolean): ValidationResult;
package/dist/rules.js ADDED
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSensitiveKey = isSensitiveKey;
4
+ exports.maskValue = maskValue;
5
+ exports.validateEnv = validateEnv;
6
+ // Keys that should NEVER be empty in production
7
+ const REQUIRED_PATTERNS = [
8
+ /^DATABASE/i,
9
+ /^DB_/i,
10
+ /^API_KEY/i,
11
+ /^SECRET/i,
12
+ /^PASSWORD/i,
13
+ ];
14
+ // Keys that indicate unsafe defaults
15
+ const UNSAFE_DEFAULTS = {
16
+ 'DEBUG': ['true', '1', 'yes'],
17
+ 'APP_DEBUG': ['true', '1', 'yes'],
18
+ 'NODE_ENV': ['development', 'dev'],
19
+ 'LOG_LEVEL': ['debug', 'trace'],
20
+ };
21
+ // Sensitive keys that should be masked in output
22
+ const SENSITIVE_PATTERNS = [
23
+ /SECRET/i,
24
+ /PASSWORD/i,
25
+ /TOKEN/i,
26
+ /KEY/i,
27
+ /CREDENTIAL/i,
28
+ /AUTH/i,
29
+ /PRIVATE/i,
30
+ ];
31
+ function isSensitiveKey(key) {
32
+ return SENSITIVE_PATTERNS.some(pattern => pattern.test(key));
33
+ }
34
+ function maskValue(key, value) {
35
+ if (isSensitiveKey(key)) {
36
+ if (value.length <= 4) {
37
+ return '****';
38
+ }
39
+ return value.substring(0, 2) + '****' + value.substring(value.length - 2);
40
+ }
41
+ return value;
42
+ }
43
+ function validateEnv(envVars, exampleVars, strict = false) {
44
+ const result = {
45
+ errors: [],
46
+ warnings: [],
47
+ };
48
+ const envKeys = new Set(envVars.map(v => v.key));
49
+ const exampleKeys = exampleVars ? new Set(exampleVars.map(v => v.key)) : null;
50
+ // Check for missing keys (in example but not in env)
51
+ if (exampleKeys) {
52
+ for (const exampleVar of exampleVars) {
53
+ if (!envKeys.has(exampleVar.key)) {
54
+ result.errors.push({
55
+ type: 'missing',
56
+ key: exampleVar.key,
57
+ message: `Missing variable: ${exampleVar.key} (defined in example)`,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ // Check for duplicates
63
+ const seen = new Map();
64
+ for (const envVar of envVars) {
65
+ if (seen.has(envVar.key)) {
66
+ result.errors.push({
67
+ type: 'duplicate',
68
+ key: envVar.key,
69
+ message: `Duplicate key: ${envVar.key} (lines ${seen.get(envVar.key)} and ${envVar.line})`,
70
+ line: envVar.line,
71
+ hasInlineLineInfo: true,
72
+ });
73
+ }
74
+ seen.set(envVar.key, envVar.line);
75
+ }
76
+ // Check for empty values
77
+ for (const envVar of envVars) {
78
+ if (envVar.value === '' || envVar.value.trim() === '') {
79
+ const isRequired = REQUIRED_PATTERNS.some(p => p.test(envVar.key));
80
+ if (isRequired || strict) {
81
+ result.errors.push({
82
+ type: 'empty',
83
+ key: envVar.key,
84
+ message: `Empty value: ${envVar.key}`,
85
+ line: envVar.line,
86
+ });
87
+ }
88
+ else {
89
+ result.warnings.push({
90
+ type: 'empty',
91
+ key: envVar.key,
92
+ message: `Empty value: ${envVar.key}`,
93
+ line: envVar.line,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ // Check for unused keys (in env but not in example)
99
+ if (exampleKeys) {
100
+ for (const envVar of envVars) {
101
+ if (!exampleKeys.has(envVar.key)) {
102
+ result.warnings.push({
103
+ type: 'unused',
104
+ key: envVar.key,
105
+ message: `Unused variable: ${envVar.key} (not in example)`,
106
+ line: envVar.line,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ // Check for unsafe defaults
112
+ for (const envVar of envVars) {
113
+ const unsafeValues = UNSAFE_DEFAULTS[envVar.key];
114
+ if (unsafeValues && unsafeValues.includes(envVar.value.toLowerCase())) {
115
+ result.warnings.push({
116
+ type: 'unsafe',
117
+ key: envVar.key,
118
+ message: `Unsafe default: ${envVar.key}=${envVar.value} (may not be suitable for production)`,
119
+ line: envVar.line,
120
+ });
121
+ }
122
+ }
123
+ return result;
124
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@gokul-kannur/env-guard",
3
+ "version": "1.0.0",
4
+ "description": "Catch stupid .env mistakes before deploy. No sync. No presets. Just validation.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "env-guard": "./bin/env-guard"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/index.ts",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "env",
16
+ "dotenv",
17
+ "validator",
18
+ "cli",
19
+ "developer-tools",
20
+ "ci"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/GokulKannur/env-guard.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/GokulKannur/env-guard/issues"
30
+ },
31
+ "homepage": "https://github.com/GokulKannur/env-guard#readme",
32
+ "devDependencies": {
33
+ "@types/node": "^20.10.0",
34
+ "typescript": "^5.3.0",
35
+ "ts-node": "^10.9.0"
36
+ },
37
+ "dependencies": {
38
+ "commander": "^11.1.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ }
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { parseEnvFile } from './parse';
7
+ import { validateEnv } from './rules';
8
+ import { printReport, printJson, printCi } from './report';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('env-guard')
14
+ .description('Catch stupid .env mistakes before deploy.')
15
+ .version('1.0.0')
16
+ .argument('[envFile]', 'Path to .env file', '.env')
17
+ .option('--example <file>', 'Path to .env.example file', '.env.example')
18
+ .option('--strict', 'Treat all empty values as errors')
19
+ .option('--json', 'Output as JSON')
20
+ .option('--ci', 'Output GitHub Actions annotations format')
21
+ .option('--no-example', 'Skip example file comparison')
22
+ .action((envFile, options) => {
23
+ try {
24
+ const envPath = path.resolve(process.cwd(), envFile);
25
+
26
+ if (!fs.existsSync(envPath)) {
27
+ console.error(`❌ Error: ${envFile} not found`);
28
+ process.exit(1);
29
+ }
30
+
31
+ const envResult = parseEnvFile(envFile);
32
+ if (envResult.errors.length > 0) {
33
+ for (const error of envResult.errors) {
34
+ console.error(`❌ ${error}`);
35
+ }
36
+ process.exit(1);
37
+ }
38
+
39
+ let exampleVars = null;
40
+ if (options.example !== false) {
41
+ const examplePath = path.resolve(process.cwd(), options.example);
42
+ if (fs.existsSync(examplePath)) {
43
+ const exampleResult = parseEnvFile(options.example);
44
+ if (exampleResult.errors.length === 0) {
45
+ exampleVars = exampleResult.vars;
46
+ }
47
+ }
48
+ }
49
+
50
+ const result = validateEnv(envResult.vars, exampleVars, options.strict);
51
+
52
+ if (options.json) {
53
+ printJson(result);
54
+ } else if (options.ci) {
55
+ printCi(result, envFile);
56
+ } else {
57
+ printReport(result);
58
+ }
59
+
60
+ if (result.errors.length > 0) {
61
+ process.exit(1);
62
+ } else {
63
+ process.exit(0);
64
+ }
65
+
66
+ } catch (error) {
67
+ if (error instanceof Error) {
68
+ console.error(`❌ Error: ${error.message}`);
69
+ } else {
70
+ console.error('❌ An unexpected error occurred');
71
+ }
72
+ process.exit(1);
73
+ }
74
+ });
75
+
76
+ program.parse();
package/src/parse.ts ADDED
@@ -0,0 +1,65 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export interface EnvVar {
5
+ key: string;
6
+ value: string;
7
+ line: number;
8
+ }
9
+
10
+ export interface ParseResult {
11
+ vars: EnvVar[];
12
+ errors: string[];
13
+ }
14
+
15
+ export function parseEnvFile(filePath: string): ParseResult {
16
+ const result: ParseResult = {
17
+ vars: [],
18
+ errors: [],
19
+ };
20
+
21
+ const absolutePath = path.resolve(process.cwd(), filePath);
22
+
23
+ if (!fs.existsSync(absolutePath)) {
24
+ result.errors.push(`File not found: ${filePath}`);
25
+ return result;
26
+ }
27
+
28
+ const content = fs.readFileSync(absolutePath, 'utf-8');
29
+ const lines = content.split('\n');
30
+
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const line = lines[i].trim();
33
+ const lineNumber = i + 1;
34
+
35
+ if (!line || line.startsWith('#')) {
36
+ continue;
37
+ }
38
+
39
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
40
+
41
+ if (!match) {
42
+ continue;
43
+ }
44
+
45
+ const [, key, value] = match;
46
+
47
+ let cleanValue = value;
48
+ if ((value.startsWith('"') && value.endsWith('"')) ||
49
+ (value.startsWith("'") && value.endsWith("'"))) {
50
+ cleanValue = value.slice(1, -1);
51
+ }
52
+
53
+ result.vars.push({
54
+ key,
55
+ value: cleanValue,
56
+ line: lineNumber,
57
+ });
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ export function getKeys(vars: EnvVar[]): Set<string> {
64
+ return new Set(vars.map(v => v.key));
65
+ }
package/src/report.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { ValidationResult, ValidationIssue } from './rules';
2
+
3
+ const colors = {
4
+ red: '\x1b[31m',
5
+ yellow: '\x1b[33m',
6
+ green: '\x1b[32m',
7
+ cyan: '\x1b[36m',
8
+ dim: '\x1b[2m',
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ };
12
+
13
+ function formatIssue(issue: ValidationIssue, isError: boolean): string {
14
+ const icon = isError ? '❌' : '⚠️';
15
+ const color = isError ? colors.red : colors.yellow;
16
+ const lineInfo = (issue.line && !issue.hasInlineLineInfo) ? ` ${colors.dim}(line ${issue.line})${colors.reset}` : '';
17
+
18
+ return `${icon} ${color}${issue.message}${colors.reset}${lineInfo}`;
19
+ }
20
+
21
+ export function printReport(result: ValidationResult): void {
22
+ const { errors, warnings } = result;
23
+ const total = errors.length + warnings.length;
24
+
25
+ console.log('');
26
+ console.log(`${colors.bold}🔍 env-guard validation report${colors.reset}`);
27
+ console.log('');
28
+
29
+ if (errors.length > 0) {
30
+ console.log(`${colors.red}${colors.bold}Errors (${errors.length})${colors.reset}`);
31
+ for (const error of errors) {
32
+ console.log(` ${formatIssue(error, true)}`);
33
+ }
34
+ console.log('');
35
+ }
36
+
37
+ if (warnings.length > 0) {
38
+ console.log(`${colors.yellow}${colors.bold}Warnings (${warnings.length})${colors.reset}`);
39
+ for (const warning of warnings) {
40
+ console.log(` ${formatIssue(warning, false)}`);
41
+ }
42
+ console.log('');
43
+ }
44
+
45
+ // Summary
46
+ if (total === 0) {
47
+ console.log(`${colors.green}✅ No issues found${colors.reset}`);
48
+ } else {
49
+ console.log(`${colors.dim}─────────────────────────────${colors.reset}`);
50
+ console.log(`Total: ${errors.length} error(s), ${warnings.length} warning(s)`);
51
+ }
52
+
53
+ // One-line summary for logs
54
+ console.log('');
55
+ console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
56
+ }
57
+
58
+ /**
59
+ * GitHub Actions annotation format
60
+ * https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
61
+ */
62
+ export function printCi(result: ValidationResult, envFile: string): void {
63
+ const { errors, warnings } = result;
64
+
65
+ // Output GitHub Actions annotations
66
+ for (const error of errors) {
67
+ const line = error.line ? `,line=${error.line}` : '';
68
+ console.log(`::error file=${envFile}${line}::${error.message}`);
69
+ }
70
+
71
+ for (const warning of warnings) {
72
+ const line = warning.line ? `,line=${warning.line}` : '';
73
+ console.log(`::warning file=${envFile}${line}::${warning.message}`);
74
+ }
75
+
76
+ // Summary line
77
+ if (errors.length === 0 && warnings.length === 0) {
78
+ console.log('env-guard: ✅ No issues found');
79
+ } else {
80
+ console.log(`env-guard: ${errors.length} errors, ${warnings.length} warnings`);
81
+ }
82
+ }
83
+
84
+ export function printJson(result: ValidationResult): void {
85
+ console.log(JSON.stringify(result, null, 2));
86
+ }
package/src/rules.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { EnvVar } from './parse';
2
+
3
+ export interface ValidationResult {
4
+ errors: ValidationIssue[];
5
+ warnings: ValidationIssue[];
6
+ }
7
+
8
+ export interface ValidationIssue {
9
+ type: string;
10
+ key: string;
11
+ message: string;
12
+ line?: number;
13
+ hasInlineLineInfo?: boolean;
14
+ }
15
+
16
+ // Keys that should NEVER be empty in production
17
+ const REQUIRED_PATTERNS = [
18
+ /^DATABASE/i,
19
+ /^DB_/i,
20
+ /^API_KEY/i,
21
+ /^SECRET/i,
22
+ /^PASSWORD/i,
23
+ ];
24
+
25
+ // Keys that indicate unsafe defaults
26
+ const UNSAFE_DEFAULTS: Record<string, string[]> = {
27
+ 'DEBUG': ['true', '1', 'yes'],
28
+ 'APP_DEBUG': ['true', '1', 'yes'],
29
+ 'NODE_ENV': ['development', 'dev'],
30
+ 'LOG_LEVEL': ['debug', 'trace'],
31
+ };
32
+
33
+ // Sensitive keys that should be masked in output
34
+ const SENSITIVE_PATTERNS = [
35
+ /SECRET/i,
36
+ /PASSWORD/i,
37
+ /TOKEN/i,
38
+ /KEY/i,
39
+ /CREDENTIAL/i,
40
+ /AUTH/i,
41
+ /PRIVATE/i,
42
+ ];
43
+
44
+ export function isSensitiveKey(key: string): boolean {
45
+ return SENSITIVE_PATTERNS.some(pattern => pattern.test(key));
46
+ }
47
+
48
+ export function maskValue(key: string, value: string): string {
49
+ if (isSensitiveKey(key)) {
50
+ if (value.length <= 4) {
51
+ return '****';
52
+ }
53
+ return value.substring(0, 2) + '****' + value.substring(value.length - 2);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ export function validateEnv(
59
+ envVars: EnvVar[],
60
+ exampleVars: EnvVar[] | null,
61
+ strict: boolean = false
62
+ ): ValidationResult {
63
+ const result: ValidationResult = {
64
+ errors: [],
65
+ warnings: [],
66
+ };
67
+
68
+ const envKeys = new Set(envVars.map(v => v.key));
69
+ const exampleKeys = exampleVars ? new Set(exampleVars.map(v => v.key)) : null;
70
+
71
+ // Check for missing keys (in example but not in env)
72
+ if (exampleKeys) {
73
+ for (const exampleVar of exampleVars!) {
74
+ if (!envKeys.has(exampleVar.key)) {
75
+ result.errors.push({
76
+ type: 'missing',
77
+ key: exampleVar.key,
78
+ message: `Missing variable: ${exampleVar.key} (defined in example)`,
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ // Check for duplicates
85
+ const seen = new Map<string, number>();
86
+ for (const envVar of envVars) {
87
+ if (seen.has(envVar.key)) {
88
+ result.errors.push({
89
+ type: 'duplicate',
90
+ key: envVar.key,
91
+ message: `Duplicate key: ${envVar.key} (lines ${seen.get(envVar.key)} and ${envVar.line})`,
92
+ line: envVar.line,
93
+ hasInlineLineInfo: true,
94
+ });
95
+ }
96
+ seen.set(envVar.key, envVar.line);
97
+ }
98
+
99
+ // Check for empty values
100
+ for (const envVar of envVars) {
101
+ if (envVar.value === '' || envVar.value.trim() === '') {
102
+ const isRequired = REQUIRED_PATTERNS.some(p => p.test(envVar.key));
103
+
104
+ if (isRequired || strict) {
105
+ result.errors.push({
106
+ type: 'empty',
107
+ key: envVar.key,
108
+ message: `Empty value: ${envVar.key}`,
109
+ line: envVar.line,
110
+ });
111
+ } else {
112
+ result.warnings.push({
113
+ type: 'empty',
114
+ key: envVar.key,
115
+ message: `Empty value: ${envVar.key}`,
116
+ line: envVar.line,
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ // Check for unused keys (in env but not in example)
123
+ if (exampleKeys) {
124
+ for (const envVar of envVars) {
125
+ if (!exampleKeys.has(envVar.key)) {
126
+ result.warnings.push({
127
+ type: 'unused',
128
+ key: envVar.key,
129
+ message: `Unused variable: ${envVar.key} (not in example)`,
130
+ line: envVar.line,
131
+ });
132
+ }
133
+ }
134
+ }
135
+
136
+ // Check for unsafe defaults
137
+ for (const envVar of envVars) {
138
+ const unsafeValues = UNSAFE_DEFAULTS[envVar.key];
139
+ if (unsafeValues && unsafeValues.includes(envVar.value.toLowerCase())) {
140
+ result.warnings.push({
141
+ type: 'unsafe',
142
+ key: envVar.key,
143
+ message: `Unsafe default: ${envVar.key}=${envVar.value} (may not be suitable for production)`,
144
+ line: envVar.line,
145
+ });
146
+ }
147
+ }
148
+
149
+ return result;
150
+ }
@@ -0,0 +1,8 @@
1
+ # Example .env file
2
+ APP_NAME=
3
+ APP_ENV=
4
+ APP_DEBUG=
5
+ DATABASE_URL=
6
+ API_KEY=
7
+ SECRET_TOKEN=
8
+ MISSING_VAR=
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": [
6
+ "ES2022"
7
+ ],
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "declaration": true
16
+ },
17
+ "include": [
18
+ "src/**/*"
19
+ ],
20
+ "exclude": [
21
+ "node_modules",
22
+ "dist"
23
+ ]
24
+ }