@claude-agent/envcheck 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Claude Agent
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,227 @@
1
+ # envcheck
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@claude-agent/envcheck.svg)](https://www.npmjs.com/package/@claude-agent/envcheck)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@claude-agent/envcheck.svg)](https://www.npmjs.com/package/@claude-agent/envcheck)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ > Validate .env files, compare with .env.example, find missing or empty variables.
8
+
9
+ Never deploy with missing environment variables again.
10
+
11
+ **Built autonomously by [Claude](https://claude.ai)** - an AI assistant by Anthropic.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install -g @claude-agent/envcheck
17
+ ```
18
+
19
+ Or use directly with npx:
20
+
21
+ ```bash
22
+ npx @claude-agent/envcheck
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ # Check .env against .env.example (auto-detected)
29
+ envcheck
30
+
31
+ # Check specific file
32
+ envcheck .env.production
33
+
34
+ # Require specific variables
35
+ envcheck -r "DATABASE_URL,API_KEY"
36
+
37
+ # Compare two files
38
+ envcheck compare .env .env.staging
39
+
40
+ # List variables
41
+ envcheck list .env
42
+
43
+ # Get specific value
44
+ envcheck get .env API_KEY
45
+ ```
46
+
47
+ ## CLI Usage
48
+
49
+ ### Check Command (Default)
50
+
51
+ ```bash
52
+ # Auto-detect .env.example in same directory
53
+ envcheck
54
+
55
+ # Specify example file
56
+ envcheck -e .env.example.prod .env.production
57
+
58
+ # Require specific variables
59
+ envcheck -r "DB_HOST,DB_USER,DB_PASS"
60
+
61
+ # Warn on empty values
62
+ envcheck --no-empty
63
+
64
+ # Error on extra variables not in example
65
+ envcheck --no-extra
66
+
67
+ # Treat warnings as errors
68
+ envcheck --strict
69
+
70
+ # JSON output
71
+ envcheck -j
72
+
73
+ # Quiet mode (only errors)
74
+ envcheck -q
75
+ ```
76
+
77
+ ### Compare Command
78
+
79
+ ```bash
80
+ # Compare two env files
81
+ envcheck compare .env .env.example
82
+
83
+ # JSON output
84
+ envcheck compare .env .env.prod -j
85
+ ```
86
+
87
+ ### List Command
88
+
89
+ ```bash
90
+ # List all variable names
91
+ envcheck list .env
92
+
93
+ # JSON output
94
+ envcheck list .env -j
95
+ ```
96
+
97
+ ### Get Command
98
+
99
+ ```bash
100
+ # Get a specific variable value
101
+ envcheck get .env DATABASE_URL
102
+ ```
103
+
104
+ ## API Usage
105
+
106
+ ```javascript
107
+ const { check, compare, validate, parse, list, get, generate } = require('@claude-agent/envcheck');
108
+
109
+ // Full check against example
110
+ const result = check('.env', {
111
+ examplePath: '.env.example',
112
+ required: ['DATABASE_URL'],
113
+ noEmpty: true,
114
+ strict: false
115
+ });
116
+
117
+ console.log(result.valid); // true/false
118
+ console.log(result.issues); // Array of issues
119
+ console.log(result.summary); // { errors: 0, warnings: 0 }
120
+
121
+ // Compare two files
122
+ const diff = compare('.env', '.env.example');
123
+ console.log(diff.missing); // Variables in example but not in env
124
+ console.log(diff.extra); // Variables in env but not in example
125
+ console.log(diff.empty); // Variables that are empty in env
126
+
127
+ // Validate a single file
128
+ const validation = validate('.env', {
129
+ required: ['API_KEY', 'SECRET'],
130
+ noEmpty: true
131
+ });
132
+
133
+ // Parse env content directly
134
+ const parsed = parse('FOO=bar\nBAZ=qux');
135
+ console.log(parsed.variables); // { FOO: 'bar', BAZ: 'qux' }
136
+
137
+ // List variables
138
+ const vars = list('.env'); // ['FOO', 'BAZ', ...]
139
+
140
+ // Get specific variable
141
+ const value = get('.env', 'API_KEY');
142
+
143
+ // Generate .env from example with defaults
144
+ const content = generate('.env.example', {
145
+ DATABASE_URL: 'postgres://localhost/mydb',
146
+ API_KEY: 'dev-key'
147
+ });
148
+ ```
149
+
150
+ ## Exit Codes
151
+
152
+ | Code | Meaning |
153
+ |------|---------|
154
+ | 0 | All checks passed |
155
+ | 1 | Errors found (missing vars, invalid syntax, etc.) |
156
+
157
+ ## Example Output
158
+
159
+ ```
160
+ $ envcheck
161
+ Checking .env against .env.example...
162
+
163
+ ✗ Missing variable 'DATABASE_URL' (defined in example at line 3)
164
+ ✗ Missing variable 'REDIS_URL' (defined in example at line 5)
165
+ ! Variable 'API_KEY' is empty (line 7)
166
+
167
+ 2 error(s), 1 warning(s)
168
+ ```
169
+
170
+ ```
171
+ $ envcheck compare .env .env.prod
172
+ Comparing .env with .env.prod...
173
+
174
+ .env: 12 variables
175
+ .env.prod: 15 variables
176
+
177
+ Missing (in example but not in env):
178
+ - NEW_RELIC_KEY
179
+ - SENTRY_DSN
180
+ - CDN_URL
181
+
182
+ Extra (in env but not in example):
183
+ - DEBUG
184
+ ```
185
+
186
+ ## Use Cases
187
+
188
+ - **CI/CD Pipelines**: Validate env before deployment
189
+ - **Onboarding**: Check if developer has all required env vars
190
+ - **Documentation**: List required variables from example file
191
+ - **Debugging**: Compare env files across environments
192
+
193
+ ## .env File Format
194
+
195
+ Supports standard .env syntax:
196
+
197
+ ```bash
198
+ # Comments
199
+ KEY=value
200
+ QUOTED="value with spaces"
201
+ MULTILINE="line1\nline2"
202
+ EMPTY=
203
+ WITH_EQUALS=postgres://user:pass@host/db?opt=val
204
+ ```
205
+
206
+ ## Why This Tool?
207
+
208
+ - **Zero dependencies** - Fast install, no bloat
209
+ - **Auto-detection** - Finds .env.example automatically
210
+ - **CI-friendly** - Exit codes and JSON output
211
+ - **Comprehensive** - Parse, validate, compare, generate
212
+ - **Well-tested** - 46 tests covering edge cases
213
+
214
+ ## Related Tools
215
+
216
+ - [@claude-agent/changelog-gen](https://www.npmjs.com/package/@claude-agent/changelog-gen) - Generate changelogs from commits
217
+ - [@claude-agent/gitstat](https://www.npmjs.com/package/@claude-agent/gitstat) - Git repository statistics
218
+ - [@claude-agent/portfinder](https://www.npmjs.com/package/@claude-agent/portfinder) - Find/kill processes by port
219
+ - [@claude-agent/cron-explain](https://www.npmjs.com/package/@claude-agent/cron-explain) - Explain cron expressions
220
+
221
+ ## License
222
+
223
+ MIT
224
+
225
+ ---
226
+
227
+ *Part of the [claude-agent-tools](https://github.com/claude-agent-tools) collection.*
package/bin/cli.js ADDED
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { check, compare, validate, list, get, readEnvFile } = require('../src/index.js');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ const VERSION = '1.0.0';
9
+
10
+ const HELP = `
11
+ envcheck - Validate .env files
12
+
13
+ USAGE
14
+ envcheck [options] [file]
15
+ envcheck compare <env> <example>
16
+
17
+ COMMANDS
18
+ check (default) Check .env file, optionally against .env.example
19
+ compare Compare two env files
20
+ list List variables in a file
21
+ get <key> Get a specific variable value
22
+
23
+ OPTIONS
24
+ -e, --example <file> Compare with example file (default: .env.example)
25
+ -r, --required <vars> Comma-separated required variables
26
+ --no-empty Warn on empty values
27
+ --no-extra Error on variables not in example
28
+ --strict Treat warnings as errors
29
+ -q, --quiet Only output errors
30
+ -j, --json Output as JSON
31
+ -v, --version Show version
32
+ -h, --help Show this help
33
+
34
+ EXAMPLES
35
+ envcheck Check .env against .env.example
36
+ envcheck .env.production Check specific file
37
+ envcheck -r "API_KEY,DB_URL" Require specific variables
38
+ envcheck compare .env .env.prod Compare two files
39
+ envcheck list .env List all variables
40
+ envcheck get .env API_KEY Get specific value
41
+ `;
42
+
43
+ function parseArgs(args) {
44
+ const result = {
45
+ command: 'check',
46
+ file: '.env',
47
+ example: null,
48
+ required: [],
49
+ noEmpty: false,
50
+ noExtra: false,
51
+ strict: false,
52
+ quiet: false,
53
+ json: false,
54
+ args: []
55
+ };
56
+
57
+ let i = 0;
58
+ while (i < args.length) {
59
+ const arg = args[i];
60
+
61
+ if (arg === '-h' || arg === '--help') {
62
+ console.log(HELP);
63
+ process.exit(0);
64
+ }
65
+
66
+ if (arg === '-v' || arg === '--version') {
67
+ console.log(VERSION);
68
+ process.exit(0);
69
+ }
70
+
71
+ if (arg === '-e' || arg === '--example') {
72
+ result.example = args[++i];
73
+ } else if (arg === '-r' || arg === '--required') {
74
+ result.required = args[++i].split(',').map(s => s.trim());
75
+ } else if (arg === '--no-empty') {
76
+ result.noEmpty = true;
77
+ } else if (arg === '--no-extra') {
78
+ result.noExtra = true;
79
+ } else if (arg === '--strict') {
80
+ result.strict = true;
81
+ } else if (arg === '-q' || arg === '--quiet') {
82
+ result.quiet = true;
83
+ } else if (arg === '-j' || arg === '--json') {
84
+ result.json = true;
85
+ } else if (arg === 'compare' || arg === 'list' || arg === 'get') {
86
+ result.command = arg;
87
+ } else if (!arg.startsWith('-')) {
88
+ result.args.push(arg);
89
+ }
90
+
91
+ i++;
92
+ }
93
+
94
+ // Handle positional args based on command
95
+ if (result.command === 'check' && result.args.length > 0) {
96
+ result.file = result.args[0];
97
+ }
98
+
99
+ return result;
100
+ }
101
+
102
+ function formatIssue(issue) {
103
+ const prefix = issue.type === 'error' ? '\x1b[31m✗\x1b[0m' : '\x1b[33m!\x1b[0m';
104
+ const line = issue.line ? `:${issue.line}` : '';
105
+ return `${prefix} ${issue.message}${line ? ` (line ${issue.line})` : ''}`;
106
+ }
107
+
108
+ function runCheck(opts) {
109
+ // Auto-detect example file
110
+ let examplePath = opts.example;
111
+ if (!examplePath) {
112
+ const dir = path.dirname(path.resolve(opts.file));
113
+ const candidates = ['.env.example', '.env.sample', '.env.template', 'env.example'];
114
+ for (const candidate of candidates) {
115
+ const p = path.join(dir, candidate);
116
+ if (fs.existsSync(p)) {
117
+ examplePath = p;
118
+ break;
119
+ }
120
+ }
121
+ }
122
+
123
+ const result = check(opts.file, {
124
+ examplePath,
125
+ required: opts.required,
126
+ noEmpty: opts.noEmpty,
127
+ noExtra: opts.noExtra,
128
+ strict: opts.strict
129
+ });
130
+
131
+ if (opts.json) {
132
+ console.log(JSON.stringify(result, null, 2));
133
+ return result.valid ? 0 : 1;
134
+ }
135
+
136
+ if (!opts.quiet) {
137
+ const fileName = path.basename(opts.file);
138
+ if (examplePath) {
139
+ console.log(`Checking ${fileName} against ${path.basename(examplePath)}...\n`);
140
+ } else {
141
+ console.log(`Checking ${fileName}...\n`);
142
+ }
143
+ }
144
+
145
+ if (result.issues.length === 0) {
146
+ if (!opts.quiet) {
147
+ console.log('\x1b[32m✓\x1b[0m All checks passed');
148
+ }
149
+ return 0;
150
+ }
151
+
152
+ // Group by type
153
+ const errors = result.issues.filter(i => i.type === 'error');
154
+ const warnings = result.issues.filter(i => i.type === 'warning');
155
+
156
+ for (const issue of errors) {
157
+ console.log(formatIssue(issue));
158
+ }
159
+
160
+ if (!opts.quiet) {
161
+ for (const issue of warnings) {
162
+ console.log(formatIssue(issue));
163
+ }
164
+ }
165
+
166
+ console.log();
167
+ if (errors.length > 0) {
168
+ console.log(`\x1b[31m${errors.length} error(s)\x1b[0m${warnings.length > 0 ? `, ${warnings.length} warning(s)` : ''}`);
169
+ } else {
170
+ console.log(`\x1b[33m${warnings.length} warning(s)\x1b[0m`);
171
+ }
172
+
173
+ return result.valid ? 0 : 1;
174
+ }
175
+
176
+ function runCompare(opts) {
177
+ if (opts.args.length < 2) {
178
+ console.error('Usage: envcheck compare <env> <example>');
179
+ return 1;
180
+ }
181
+
182
+ const [envPath, examplePath] = opts.args;
183
+ const result = compare(envPath, examplePath);
184
+
185
+ if (opts.json) {
186
+ console.log(JSON.stringify(result, null, 2));
187
+ return 0;
188
+ }
189
+
190
+ console.log(`Comparing ${path.basename(envPath)} with ${path.basename(examplePath)}...\n`);
191
+
192
+ if (!result.env.exists) {
193
+ console.log(`\x1b[31m✗\x1b[0m File not found: ${envPath}`);
194
+ return 1;
195
+ }
196
+
197
+ if (!result.example.exists) {
198
+ console.log(`\x1b[31m✗\x1b[0m File not found: ${examplePath}`);
199
+ return 1;
200
+ }
201
+
202
+ const envCount = Object.keys(result.env.variables).length;
203
+ const exampleCount = Object.keys(result.example.variables).length;
204
+
205
+ console.log(`${path.basename(envPath)}: ${envCount} variables`);
206
+ console.log(`${path.basename(examplePath)}: ${exampleCount} variables\n`);
207
+
208
+ if (result.missing.length > 0) {
209
+ console.log('\x1b[31mMissing (in example but not in env):\x1b[0m');
210
+ for (const item of result.missing) {
211
+ console.log(` - ${item.key}`);
212
+ }
213
+ console.log();
214
+ }
215
+
216
+ if (result.extra.length > 0) {
217
+ console.log('\x1b[33mExtra (in env but not in example):\x1b[0m');
218
+ for (const item of result.extra) {
219
+ console.log(` - ${item.key}`);
220
+ }
221
+ console.log();
222
+ }
223
+
224
+ if (result.empty.length > 0) {
225
+ console.log('\x1b[33mEmpty (defined but empty in env):\x1b[0m');
226
+ for (const item of result.empty) {
227
+ console.log(` - ${item.key}`);
228
+ }
229
+ console.log();
230
+ }
231
+
232
+ if (result.missing.length === 0 && result.extra.length === 0 && result.empty.length === 0) {
233
+ console.log('\x1b[32m✓\x1b[0m Files are in sync');
234
+ }
235
+
236
+ return result.missing.length > 0 ? 1 : 0;
237
+ }
238
+
239
+ function runList(opts) {
240
+ const file = opts.args[0] || opts.file;
241
+ const vars = list(file);
242
+
243
+ if (opts.json) {
244
+ console.log(JSON.stringify(vars, null, 2));
245
+ return 0;
246
+ }
247
+
248
+ if (vars.length === 0) {
249
+ console.log('No variables found');
250
+ return 0;
251
+ }
252
+
253
+ for (const v of vars) {
254
+ console.log(v);
255
+ }
256
+
257
+ return 0;
258
+ }
259
+
260
+ function runGet(opts) {
261
+ if (opts.args.length < 2) {
262
+ console.error('Usage: envcheck get <file> <key>');
263
+ return 1;
264
+ }
265
+
266
+ const [file, key] = opts.args;
267
+ const value = get(file, key);
268
+
269
+ if (value === undefined) {
270
+ if (!opts.quiet) {
271
+ console.error(`Variable '${key}' not found`);
272
+ }
273
+ return 1;
274
+ }
275
+
276
+ console.log(value);
277
+ return 0;
278
+ }
279
+
280
+ function main() {
281
+ const args = process.argv.slice(2);
282
+ const opts = parseArgs(args);
283
+
284
+ let exitCode = 0;
285
+
286
+ switch (opts.command) {
287
+ case 'check':
288
+ exitCode = runCheck(opts);
289
+ break;
290
+ case 'compare':
291
+ exitCode = runCompare(opts);
292
+ break;
293
+ case 'list':
294
+ exitCode = runList(opts);
295
+ break;
296
+ case 'get':
297
+ exitCode = runGet(opts);
298
+ break;
299
+ default:
300
+ console.error(`Unknown command: ${opts.command}`);
301
+ exitCode = 1;
302
+ }
303
+
304
+ process.exit(exitCode);
305
+ }
306
+
307
+ main();
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@claude-agent/envcheck",
3
+ "version": "1.0.0",
4
+ "description": "Validate .env files, compare with .env.example, find missing or empty variables",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "envcheck": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test test/*.test.js"
11
+ },
12
+ "keywords": [
13
+ "env",
14
+ "dotenv",
15
+ "environment",
16
+ "variables",
17
+ "validate",
18
+ "lint",
19
+ "cli"
20
+ ],
21
+ "author": "Claude Agent <claude-agent@agentmail.to>",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/claude-agent-tools/envcheck.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/claude-agent-tools/envcheck/issues"
29
+ },
30
+ "homepage": "https://github.com/claude-agent-tools/envcheck#readme",
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "files": [
35
+ "src/",
36
+ "bin/"
37
+ ]
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,424 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Parse a .env file into an object
8
+ * @param {string} content - File content
9
+ * @returns {Object} Parsed variables
10
+ */
11
+ function parseEnv(content) {
12
+ const result = {
13
+ variables: {},
14
+ errors: [],
15
+ warnings: [],
16
+ lineInfo: {}
17
+ };
18
+
19
+ const lines = content.split('\n');
20
+
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const lineNum = i + 1;
23
+ const line = lines[i];
24
+ const trimmed = line.trim();
25
+
26
+ // Skip empty lines and comments
27
+ if (!trimmed || trimmed.startsWith('#')) {
28
+ continue;
29
+ }
30
+
31
+ // Check for valid format
32
+ const eqIndex = line.indexOf('=');
33
+ if (eqIndex === -1) {
34
+ result.errors.push({
35
+ line: lineNum,
36
+ message: `Invalid syntax: missing '=' sign`,
37
+ content: line
38
+ });
39
+ continue;
40
+ }
41
+
42
+ const key = line.slice(0, eqIndex).trim();
43
+ let value = line.slice(eqIndex + 1);
44
+
45
+ // Validate key
46
+ if (!key) {
47
+ result.errors.push({
48
+ line: lineNum,
49
+ message: 'Empty variable name',
50
+ content: line
51
+ });
52
+ continue;
53
+ }
54
+
55
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
56
+ result.warnings.push({
57
+ line: lineNum,
58
+ message: `Variable name '${key}' contains unusual characters`,
59
+ content: line
60
+ });
61
+ }
62
+
63
+ // Check for duplicate
64
+ if (key in result.variables) {
65
+ result.warnings.push({
66
+ line: lineNum,
67
+ message: `Duplicate variable '${key}' (previous at line ${result.lineInfo[key]})`,
68
+ content: line
69
+ });
70
+ }
71
+
72
+ // Parse value (handle quotes)
73
+ value = parseValue(value);
74
+
75
+ result.variables[key] = value;
76
+ result.lineInfo[key] = lineNum;
77
+ }
78
+
79
+ return result;
80
+ }
81
+
82
+ /**
83
+ * Parse a value, handling quotes and escapes
84
+ * @param {string} raw - Raw value string
85
+ * @returns {string} Parsed value
86
+ */
87
+ function parseValue(raw) {
88
+ let value = raw;
89
+
90
+ // Remove inline comments (but not if inside quotes)
91
+ if (!value.startsWith('"') && !value.startsWith("'")) {
92
+ const hashIndex = value.indexOf(' #');
93
+ if (hashIndex !== -1) {
94
+ value = value.slice(0, hashIndex);
95
+ }
96
+ }
97
+
98
+ value = value.trim();
99
+
100
+ // Handle quoted values
101
+ if ((value.startsWith('"') && value.endsWith('"')) ||
102
+ (value.startsWith("'") && value.endsWith("'"))) {
103
+ value = value.slice(1, -1);
104
+ // Handle escape sequences in double quotes
105
+ if (raw.trim().startsWith('"')) {
106
+ value = value.replace(/\\n/g, '\n')
107
+ .replace(/\\r/g, '\r')
108
+ .replace(/\\t/g, '\t')
109
+ .replace(/\\\\/g, '\\')
110
+ .replace(/\\"/g, '"');
111
+ }
112
+ }
113
+
114
+ return value;
115
+ }
116
+
117
+ /**
118
+ * Read and parse a .env file
119
+ * @param {string} filePath - Path to file
120
+ * @returns {Object} Parsed result
121
+ */
122
+ function readEnvFile(filePath) {
123
+ const absolutePath = path.resolve(filePath);
124
+
125
+ if (!fs.existsSync(absolutePath)) {
126
+ return {
127
+ exists: false,
128
+ path: absolutePath,
129
+ variables: {},
130
+ errors: [{ message: `File not found: ${absolutePath}` }],
131
+ warnings: [],
132
+ lineInfo: {}
133
+ };
134
+ }
135
+
136
+ const content = fs.readFileSync(absolutePath, 'utf8');
137
+ const result = parseEnv(content);
138
+ result.exists = true;
139
+ result.path = absolutePath;
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * Compare two env files
146
+ * @param {string} envPath - Path to .env file
147
+ * @param {string} examplePath - Path to .env.example file
148
+ * @returns {Object} Comparison result
149
+ */
150
+ function compare(envPath, examplePath) {
151
+ const env = readEnvFile(envPath);
152
+ const example = readEnvFile(examplePath);
153
+
154
+ const result = {
155
+ env,
156
+ example,
157
+ missing: [], // In example but not in env
158
+ extra: [], // In env but not in example
159
+ empty: [], // In both but empty in env
160
+ different: [] // Different values (if example has values)
161
+ };
162
+
163
+ if (!env.exists || !example.exists) {
164
+ return result;
165
+ }
166
+
167
+ const envKeys = new Set(Object.keys(env.variables));
168
+ const exampleKeys = new Set(Object.keys(example.variables));
169
+
170
+ // Find missing (in example but not in env)
171
+ for (const key of exampleKeys) {
172
+ if (!envKeys.has(key)) {
173
+ result.missing.push({
174
+ key,
175
+ exampleValue: example.variables[key],
176
+ line: example.lineInfo[key]
177
+ });
178
+ }
179
+ }
180
+
181
+ // Find extra (in env but not in example)
182
+ for (const key of envKeys) {
183
+ if (!exampleKeys.has(key)) {
184
+ result.extra.push({
185
+ key,
186
+ value: env.variables[key],
187
+ line: env.lineInfo[key]
188
+ });
189
+ }
190
+ }
191
+
192
+ // Find empty values
193
+ for (const key of envKeys) {
194
+ if (exampleKeys.has(key) && env.variables[key] === '') {
195
+ result.empty.push({
196
+ key,
197
+ line: env.lineInfo[key]
198
+ });
199
+ }
200
+ }
201
+
202
+ return result;
203
+ }
204
+
205
+ /**
206
+ * Validate an env file
207
+ * @param {string} filePath - Path to .env file
208
+ * @param {Object} options - Validation options
209
+ * @returns {Object} Validation result
210
+ */
211
+ function validate(filePath, options = {}) {
212
+ const { required = [], noEmpty = false } = options;
213
+
214
+ const env = readEnvFile(filePath);
215
+
216
+ const result = {
217
+ valid: true,
218
+ env,
219
+ issues: []
220
+ };
221
+
222
+ if (!env.exists) {
223
+ result.valid = false;
224
+ result.issues.push({
225
+ type: 'error',
226
+ message: `File not found: ${filePath}`
227
+ });
228
+ return result;
229
+ }
230
+
231
+ // Check for parse errors
232
+ if (env.errors.length > 0) {
233
+ result.valid = false;
234
+ for (const error of env.errors) {
235
+ result.issues.push({
236
+ type: 'error',
237
+ line: error.line,
238
+ message: error.message
239
+ });
240
+ }
241
+ }
242
+
243
+ // Check required variables
244
+ for (const key of required) {
245
+ if (!(key in env.variables)) {
246
+ result.valid = false;
247
+ result.issues.push({
248
+ type: 'error',
249
+ message: `Missing required variable: ${key}`
250
+ });
251
+ } else if (env.variables[key] === '') {
252
+ result.valid = false;
253
+ result.issues.push({
254
+ type: 'error',
255
+ line: env.lineInfo[key],
256
+ message: `Required variable '${key}' is empty`
257
+ });
258
+ }
259
+ }
260
+
261
+ // Check for empty values
262
+ if (noEmpty) {
263
+ for (const [key, value] of Object.entries(env.variables)) {
264
+ if (value === '' && !required.includes(key)) {
265
+ result.issues.push({
266
+ type: 'warning',
267
+ line: env.lineInfo[key],
268
+ message: `Variable '${key}' is empty`
269
+ });
270
+ }
271
+ }
272
+ }
273
+
274
+ // Add warnings
275
+ for (const warning of env.warnings) {
276
+ result.issues.push({
277
+ type: 'warning',
278
+ line: warning.line,
279
+ message: warning.message
280
+ });
281
+ }
282
+
283
+ return result;
284
+ }
285
+
286
+ /**
287
+ * Check an env file against example and validate
288
+ * @param {string} envPath - Path to .env
289
+ * @param {Object} options - Check options
290
+ * @returns {Object} Check result
291
+ */
292
+ function check(envPath, options = {}) {
293
+ const {
294
+ examplePath = null,
295
+ required = [],
296
+ noEmpty = false,
297
+ noExtra = false,
298
+ strict = false
299
+ } = options;
300
+
301
+ const result = {
302
+ valid: true,
303
+ issues: [],
304
+ summary: {
305
+ errors: 0,
306
+ warnings: 0
307
+ }
308
+ };
309
+
310
+ // First validate the env file
311
+ const validation = validate(envPath, { required, noEmpty });
312
+
313
+ if (!validation.valid) {
314
+ result.valid = false;
315
+ }
316
+
317
+ result.issues.push(...validation.issues);
318
+ result.env = validation.env;
319
+
320
+ // Then compare with example if provided
321
+ if (examplePath) {
322
+ const comparison = compare(envPath, examplePath);
323
+ result.comparison = comparison;
324
+
325
+ // Missing variables are errors
326
+ for (const item of comparison.missing) {
327
+ result.valid = false;
328
+ result.issues.push({
329
+ type: 'error',
330
+ message: `Missing variable '${item.key}' (defined in example at line ${item.line})`
331
+ });
332
+ }
333
+
334
+ // Empty variables are warnings (or errors in strict mode)
335
+ for (const item of comparison.empty) {
336
+ const type = strict ? 'error' : 'warning';
337
+ if (strict) result.valid = false;
338
+ result.issues.push({
339
+ type,
340
+ line: item.line,
341
+ message: `Variable '${item.key}' is empty`
342
+ });
343
+ }
344
+
345
+ // Extra variables are warnings (or errors if noExtra)
346
+ if (noExtra) {
347
+ for (const item of comparison.extra) {
348
+ result.valid = false;
349
+ result.issues.push({
350
+ type: 'error',
351
+ line: item.line,
352
+ message: `Extra variable '${item.key}' not in example`
353
+ });
354
+ }
355
+ }
356
+ }
357
+
358
+ // Count issues
359
+ for (const issue of result.issues) {
360
+ if (issue.type === 'error') {
361
+ result.summary.errors++;
362
+ } else {
363
+ result.summary.warnings++;
364
+ }
365
+ }
366
+
367
+ return result;
368
+ }
369
+
370
+ /**
371
+ * Generate a template .env from .env.example
372
+ * @param {string} examplePath - Path to example file
373
+ * @param {Object} defaults - Default values to fill in
374
+ * @returns {string} Generated .env content
375
+ */
376
+ function generate(examplePath, defaults = {}) {
377
+ const example = readEnvFile(examplePath);
378
+
379
+ if (!example.exists) {
380
+ throw new Error(`Example file not found: ${examplePath}`);
381
+ }
382
+
383
+ const lines = [];
384
+
385
+ for (const [key, value] of Object.entries(example.variables)) {
386
+ const newValue = key in defaults ? defaults[key] : value;
387
+ lines.push(`${key}=${newValue}`);
388
+ }
389
+
390
+ return lines.join('\n') + '\n';
391
+ }
392
+
393
+ /**
394
+ * Get list of variables from file
395
+ * @param {string} filePath - Path to env file
396
+ * @returns {string[]} Variable names
397
+ */
398
+ function list(filePath) {
399
+ const env = readEnvFile(filePath);
400
+ return Object.keys(env.variables);
401
+ }
402
+
403
+ /**
404
+ * Get a specific variable value
405
+ * @param {string} filePath - Path to env file
406
+ * @param {string} key - Variable name
407
+ * @returns {string|undefined} Value or undefined
408
+ */
409
+ function get(filePath, key) {
410
+ const env = readEnvFile(filePath);
411
+ return env.variables[key];
412
+ }
413
+
414
+ module.exports = {
415
+ parse: parseEnv,
416
+ parseValue,
417
+ readEnvFile,
418
+ compare,
419
+ validate,
420
+ check,
421
+ generate,
422
+ list,
423
+ get
424
+ };