@scuton/dotenv-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.
package/dist/cli.js ADDED
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli.ts
5
+ var import_fs4 = require("fs");
6
+
7
+ // src/core/sync.ts
8
+ var import_fs = require("fs");
9
+
10
+ // src/core/parser.ts
11
+ function parseEnvFile(content) {
12
+ const entries = [];
13
+ const lines = content.split("\n");
14
+ for (let i = 0; i < lines.length; i++) {
15
+ const raw = lines[i];
16
+ const trimmed = raw.trim();
17
+ if (!trimmed || trimmed.startsWith("#")) continue;
18
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/);
19
+ if (!match) continue;
20
+ const key = match[1];
21
+ let value = match[2];
22
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
23
+ value = value.slice(1, -1);
24
+ }
25
+ const commentMatch = value.match(/\s+#\s/);
26
+ let comment;
27
+ if (commentMatch && !match[2].startsWith('"') && !match[2].startsWith("'")) {
28
+ comment = value.slice(commentMatch.index + commentMatch[0].length);
29
+ value = value.slice(0, commentMatch.index);
30
+ }
31
+ entries.push({ key, value, line: i + 1, comment, raw });
32
+ }
33
+ return entries;
34
+ }
35
+ function parseEnvFileToMap(content) {
36
+ const entries = parseEnvFile(content);
37
+ return new Map(entries.map((e) => [e.key, e.value]));
38
+ }
39
+
40
+ // src/core/sync.ts
41
+ function syncCheck(envPath2 = ".env", examplePath2 = ".env.example") {
42
+ if (!(0, import_fs.existsSync)(examplePath2)) {
43
+ throw new Error(`${examplePath2} not found. Run "dotenv-guard init" to create one.`);
44
+ }
45
+ const exampleContent = (0, import_fs.readFileSync)(examplePath2, "utf-8");
46
+ const exampleKeys = new Set(parseEnvFile(exampleContent).map((e) => e.key));
47
+ let envKeys = /* @__PURE__ */ new Set();
48
+ if ((0, import_fs.existsSync)(envPath2)) {
49
+ const envContent = (0, import_fs.readFileSync)(envPath2, "utf-8");
50
+ envKeys = new Set(parseEnvFile(envContent).map((e) => e.key));
51
+ }
52
+ const missing = [...exampleKeys].filter((k) => !envKeys.has(k));
53
+ const extra = [...envKeys].filter((k) => !exampleKeys.has(k));
54
+ const synced = [...exampleKeys].filter((k) => envKeys.has(k));
55
+ return { missing, extra, synced, examplePath: examplePath2, envPath: envPath2 };
56
+ }
57
+
58
+ // src/core/validator.ts
59
+ function validate(env, rules) {
60
+ const errors = [];
61
+ for (const rule of rules) {
62
+ const value = env[rule.key];
63
+ if (rule.required !== false && (value === void 0 || value === "")) {
64
+ errors.push({ key: rule.key, message: "is required but missing or empty" });
65
+ continue;
66
+ }
67
+ if (value === void 0 || value === "") continue;
68
+ switch (rule.type) {
69
+ case "number": {
70
+ const num = Number(value);
71
+ if (isNaN(num)) {
72
+ errors.push({ key: rule.key, value, message: `must be a number, got "${value}"` });
73
+ } else {
74
+ if (rule.min !== void 0 && num < rule.min)
75
+ errors.push({ key: rule.key, value, message: `must be >= ${rule.min}` });
76
+ if (rule.max !== void 0 && num > rule.max)
77
+ errors.push({ key: rule.key, value, message: `must be <= ${rule.max}` });
78
+ }
79
+ break;
80
+ }
81
+ case "boolean":
82
+ if (!["true", "false", "1", "0", "yes", "no"].includes(value.toLowerCase()))
83
+ errors.push({ key: rule.key, value, message: "must be a boolean (true/false/1/0/yes/no)" });
84
+ break;
85
+ case "url":
86
+ try {
87
+ new URL(value);
88
+ } catch {
89
+ errors.push({ key: rule.key, value, message: "must be a valid URL" });
90
+ }
91
+ break;
92
+ case "email":
93
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
94
+ errors.push({ key: rule.key, value, message: "must be a valid email" });
95
+ break;
96
+ case "port": {
97
+ const port = Number(value);
98
+ if (isNaN(port) || port < 1 || port > 65535)
99
+ errors.push({ key: rule.key, value, message: "must be a valid port (1-65535)" });
100
+ break;
101
+ }
102
+ case "enum":
103
+ if (rule.enum && !rule.enum.includes(value))
104
+ errors.push({ key: rule.key, value, message: `must be one of: ${rule.enum.join(", ")}` });
105
+ break;
106
+ }
107
+ if (rule.pattern && !rule.pattern.test(value))
108
+ errors.push({ key: rule.key, value, message: `does not match required pattern` });
109
+ }
110
+ return errors;
111
+ }
112
+ function inferRules(exampleContent) {
113
+ const lines = exampleContent.split("\n");
114
+ const rules = [];
115
+ for (const line of lines) {
116
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/);
117
+ if (!match) continue;
118
+ const key = match[1];
119
+ const rest = match[2];
120
+ const rule = { key, required: true };
121
+ const commentMatch = rest.match(/#\s*type:\s*(\w+)/i);
122
+ if (commentMatch) {
123
+ rule.type = commentMatch[1].toLowerCase();
124
+ }
125
+ if (rest.match(/#.*optional/i)) {
126
+ rule.required = false;
127
+ }
128
+ const enumMatch = rest.match(/#\s*enum:\s*([^\s]+)/i);
129
+ if (enumMatch) {
130
+ rule.type = "enum";
131
+ rule.enum = enumMatch[1].split(",");
132
+ }
133
+ if (!rule.type) {
134
+ const value = rest.split("#")[0].trim().replace(/^["']|["']$/g, "");
135
+ if (value.match(/^https?:\/\//)) rule.type = "url";
136
+ else if (value.match(/^\d+$/) && key.match(/PORT/i)) rule.type = "port";
137
+ else if (value.match(/^(true|false)$/i)) rule.type = "boolean";
138
+ else if (value.match(/^\d+$/)) rule.type = "number";
139
+ }
140
+ rules.push(rule);
141
+ }
142
+ return rules;
143
+ }
144
+
145
+ // src/core/leak.ts
146
+ var import_child_process = require("child_process");
147
+ var import_fs2 = require("fs");
148
+ var SENSITIVE_PATTERNS = [
149
+ /API[_-]?KEY/i,
150
+ /SECRET/i,
151
+ /PASSWORD/i,
152
+ /TOKEN/i,
153
+ /PRIVATE[_-]?KEY/i,
154
+ /DATABASE[_-]?URL/i,
155
+ /MONGO[_-]?URI/i,
156
+ /REDIS[_-]?URL/i,
157
+ /AWS[_-]?ACCESS/i,
158
+ /STRIPE/i,
159
+ /SENDGRID/i,
160
+ /TWILIO/i,
161
+ /ANTHROPIC/i,
162
+ /OPENAI/i
163
+ ];
164
+ function checkLeaks(dir = ".") {
165
+ const leaks = [];
166
+ let gitignoreHasEnv = false;
167
+ const gitignorePath = `${dir}/.gitignore`;
168
+ if ((0, import_fs2.existsSync)(gitignorePath)) {
169
+ const content = (0, import_fs2.readFileSync)(gitignorePath, "utf-8");
170
+ gitignoreHasEnv = content.split("\n").some(
171
+ (line) => line.trim() === ".env" || line.trim() === ".env*" || line.trim() === ".env.local"
172
+ );
173
+ }
174
+ try {
175
+ const tracked = (0, import_child_process.execSync)("git ls-files", { cwd: dir, encoding: "utf-8" });
176
+ const envFiles = tracked.split("\n").filter(
177
+ (f) => f.match(/^\.env($|\.)/) && !f.endsWith(".example") && !f.endsWith(".template")
178
+ );
179
+ for (const file of envFiles) {
180
+ const content = (0, import_fs2.readFileSync)(`${dir}/${file}`, "utf-8");
181
+ const lines = content.split("\n");
182
+ for (let i = 0; i < lines.length; i++) {
183
+ const match = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)/);
184
+ if (match && match[2].trim().length > 0) {
185
+ const key = match[1];
186
+ const severity = SENSITIVE_PATTERNS.some((p) => p.test(key)) ? "high" : "medium";
187
+ leaks.push({ file, line: i + 1, key, severity });
188
+ }
189
+ }
190
+ }
191
+ } catch {
192
+ }
193
+ let preCommitHookInstalled = false;
194
+ const hookPath = `${dir}/.git/hooks/pre-commit`;
195
+ if ((0, import_fs2.existsSync)(hookPath)) {
196
+ const hookContent = (0, import_fs2.readFileSync)(hookPath, "utf-8");
197
+ preCommitHookInstalled = hookContent.includes("dotenv-guard");
198
+ }
199
+ return { leaks, gitignoreHasEnv, preCommitHookInstalled };
200
+ }
201
+ function installPreCommitHook(dir = ".") {
202
+ const hookDir = `${dir}/.git/hooks`;
203
+ const hookPath = `${hookDir}/pre-commit`;
204
+ const hookScript = `#!/bin/sh
205
+ # dotenv-guard pre-commit hook \u2014 prevent .env leaks
206
+ # Installed by: npx dotenv-guard install-hook
207
+
208
+ ENV_FILES=$(git diff --cached --name-only | grep -E '^\\.env' | grep -v '\\.example$' | grep -v '\\.template$')
209
+
210
+ if [ -n "$ENV_FILES" ]; then
211
+ echo ""
212
+ echo "\\033[31m\u2716 dotenv-guard: Blocked commit \u2014 .env file(s) detected:\\033[0m"
213
+ echo ""
214
+ for f in $ENV_FILES; do
215
+ echo " \\033[33m\u2192 $f\\033[0m"
216
+ done
217
+ echo ""
218
+ echo " Remove with: git reset HEAD <file>"
219
+ echo " Or force commit: git commit --no-verify"
220
+ echo ""
221
+ exit 1
222
+ fi
223
+ `;
224
+ (0, import_fs2.mkdirSync)(hookDir, { recursive: true });
225
+ (0, import_fs2.writeFileSync)(hookPath, hookScript, "utf-8");
226
+ try {
227
+ (0, import_fs2.chmodSync)(hookPath, "755");
228
+ } catch {
229
+ }
230
+ }
231
+
232
+ // src/core/generator.ts
233
+ var import_fs3 = require("fs");
234
+ function generateExample(envPath2 = ".env", outputPath = ".env.example") {
235
+ if (!(0, import_fs3.existsSync)(envPath2)) throw new Error(`${envPath2} not found`);
236
+ const content = (0, import_fs3.readFileSync)(envPath2, "utf-8");
237
+ const entries = parseEnvFile(content);
238
+ const lines = ["# Environment Variables", "# Copy this file to .env and fill in the values", ""];
239
+ for (const entry of entries) {
240
+ let placeholder = "";
241
+ if (entry.value.match(/^https?:\/\//)) placeholder = "https://... # type:url";
242
+ else if (entry.value.match(/^\d+$/)) placeholder = "0 # type:number";
243
+ else if (entry.value.match(/^(true|false)$/i)) placeholder = "false # type:boolean";
244
+ else placeholder = "# type:string";
245
+ lines.push(`${entry.key}=${placeholder}`);
246
+ }
247
+ const output = lines.join("\n") + "\n";
248
+ (0, import_fs3.writeFileSync)(outputPath, output, "utf-8");
249
+ return output;
250
+ }
251
+ function ensureGitignore(dir = ".") {
252
+ const gitignorePath = `${dir}/.gitignore`;
253
+ const envEntries = [".env", ".env.local", ".env.*.local"];
254
+ let content = "";
255
+ if ((0, import_fs3.existsSync)(gitignorePath)) {
256
+ content = (0, import_fs3.readFileSync)(gitignorePath, "utf-8");
257
+ }
258
+ const lines = content.split("\n");
259
+ const toAdd = envEntries.filter((e) => !lines.some((l) => l.trim() === e));
260
+ if (toAdd.length > 0) {
261
+ const addition = "\n# Environment variables\n" + toAdd.join("\n") + "\n";
262
+ (0, import_fs3.appendFileSync)(gitignorePath, addition, "utf-8");
263
+ return true;
264
+ }
265
+ return false;
266
+ }
267
+
268
+ // src/utils/colors.ts
269
+ var isColorSupported = process.env.NO_COLOR === void 0 && process.stdout.isTTY;
270
+ var wrap = (code, resetCode) => (str) => isColorSupported ? `\x1B[${code}m${str}\x1B[${resetCode}m` : str;
271
+ var red = wrap(31, 39);
272
+ var green = wrap(32, 39);
273
+ var yellow = wrap(33, 39);
274
+ var blue = wrap(34, 39);
275
+ var gray = wrap(90, 39);
276
+ var bold = wrap(1, 22);
277
+ var dim = wrap(2, 22);
278
+
279
+ // src/cli.ts
280
+ var args = process.argv.slice(2);
281
+ var command = args[0];
282
+ function showHelp() {
283
+ console.log(`
284
+ ${bold("dotenv-guard")} \u2014 Validate env vars, sync .env files, prevent leaks
285
+
286
+ ${bold("Usage:")}
287
+ dotenv-guard <command> [options]
288
+
289
+ ${bold("Commands:")}
290
+ sync Compare .env with .env.example
291
+ validate Validate env var types (from .env.example hints)
292
+ leak-check Scan for leaked secrets in git
293
+ init Create .env.example + update .gitignore
294
+ install-hook Install git pre-commit hook to block .env commits
295
+
296
+ ${bold("Options:")}
297
+ --env <path> Path to .env file (default: .env)
298
+ --example <path> Path to .env.example (default: .env.example)
299
+ -h, --help Show help
300
+ -v, --version Show version
301
+
302
+ ${bold("Programmatic:")}
303
+ import 'dotenv-guard/auto';
304
+ // Auto-check on import
305
+ import { guard } from 'dotenv-guard'; // Manual check
306
+ `);
307
+ }
308
+ var envPath = args.includes("--env") ? args[args.indexOf("--env") + 1] : ".env";
309
+ var examplePath = args.includes("--example") ? args[args.indexOf("--example") + 1] : ".env.example";
310
+ switch (command) {
311
+ case "sync": {
312
+ try {
313
+ const result = syncCheck(envPath, examplePath);
314
+ console.log("");
315
+ console.log(bold(" dotenv-guard sync"));
316
+ console.log(gray(` ${result.envPath} \u2194 ${result.examplePath}`));
317
+ console.log("");
318
+ if (result.missing.length > 0) {
319
+ console.log(red(` \u2716 Missing (${result.missing.length}):`));
320
+ result.missing.forEach((k) => console.log(yellow(` \u2192 ${k}`)));
321
+ console.log("");
322
+ }
323
+ if (result.extra.length > 0) {
324
+ console.log(yellow(` \u26A0 Extra (${result.extra.length}):`));
325
+ result.extra.forEach((k) => console.log(gray(` \u2192 ${k}`)));
326
+ console.log("");
327
+ }
328
+ console.log(green(` \u2713 Synced: ${result.synced.length} variables`));
329
+ if (result.missing.length > 0) {
330
+ console.log("");
331
+ console.log(dim(` Add missing variables to ${result.envPath} to fix.`));
332
+ }
333
+ console.log("");
334
+ process.exit(result.missing.length > 0 ? 1 : 0);
335
+ } catch (err) {
336
+ console.error(red(`
337
+ \u2716 ${err.message}
338
+ `));
339
+ process.exit(1);
340
+ }
341
+ break;
342
+ }
343
+ case "validate": {
344
+ try {
345
+ if (!(0, import_fs4.existsSync)(examplePath)) {
346
+ console.error(red(`
347
+ \u2716 ${examplePath} not found. Run "dotenv-guard init" first.
348
+ `));
349
+ process.exit(1);
350
+ }
351
+ const exampleContent = (0, import_fs4.readFileSync)(examplePath, "utf-8");
352
+ const rules = inferRules(exampleContent);
353
+ const envMap = (0, import_fs4.existsSync)(envPath) ? parseEnvFileToMap((0, import_fs4.readFileSync)(envPath, "utf-8")) : /* @__PURE__ */ new Map();
354
+ const env = {};
355
+ for (const rule of rules) {
356
+ env[rule.key] = process.env[rule.key] || envMap.get(rule.key);
357
+ }
358
+ const errors = validate(env, rules);
359
+ console.log("");
360
+ console.log(bold(" dotenv-guard validate"));
361
+ console.log("");
362
+ if (errors.length === 0) {
363
+ console.log(green(` \u2713 All ${rules.length} variables are valid`));
364
+ } else {
365
+ console.log(red(` \u2716 ${errors.length} validation error(s):`));
366
+ console.log("");
367
+ errors.forEach((e) => {
368
+ console.log(` ${red("\u2716")} ${bold(e.key)} ${e.message}`);
369
+ if (e.value) console.log(gray(` current value: "${e.value}"`));
370
+ });
371
+ }
372
+ console.log("");
373
+ process.exit(errors.length > 0 ? 1 : 0);
374
+ } catch (err) {
375
+ console.error(red(`
376
+ \u2716 ${err.message}
377
+ `));
378
+ process.exit(1);
379
+ }
380
+ break;
381
+ }
382
+ case "leak-check": {
383
+ const result = checkLeaks(".");
384
+ console.log("");
385
+ console.log(bold(" dotenv-guard leak-check"));
386
+ console.log("");
387
+ if (result.gitignoreHasEnv) {
388
+ console.log(green(" \u2713 .gitignore includes .env"));
389
+ } else {
390
+ console.log(red(" \u2716 .gitignore does NOT include .env"));
391
+ console.log(gray(" Run: dotenv-guard init"));
392
+ }
393
+ if (result.preCommitHookInstalled) {
394
+ console.log(green(" \u2713 Pre-commit hook installed"));
395
+ } else {
396
+ console.log(yellow(" \u26A0 No pre-commit hook"));
397
+ console.log(gray(" Run: dotenv-guard install-hook"));
398
+ }
399
+ if (result.leaks.length === 0) {
400
+ console.log(green(" \u2713 No .env files tracked in git"));
401
+ } else {
402
+ console.log(red(` \u2716 ${result.leaks.length} leaked variable(s) in git:`));
403
+ console.log("");
404
+ result.leaks.forEach((l) => {
405
+ const icon = l.severity === "high" ? red("HIGH") : yellow("MED");
406
+ console.log(` [${icon}] ${l.file}:${l.line} \u2014 ${bold(l.key)}`);
407
+ });
408
+ console.log("");
409
+ console.log(dim(" Remove tracked files: git rm --cached .env"));
410
+ }
411
+ console.log("");
412
+ process.exit(result.leaks.length > 0 ? 1 : 0);
413
+ break;
414
+ }
415
+ case "init": {
416
+ console.log("");
417
+ console.log(bold(" dotenv-guard init"));
418
+ console.log("");
419
+ if ((0, import_fs4.existsSync)(".env") && !(0, import_fs4.existsSync)(".env.example")) {
420
+ generateExample(".env", ".env.example");
421
+ console.log(green(" \u2713 Created .env.example from .env"));
422
+ } else if ((0, import_fs4.existsSync)(".env.example")) {
423
+ console.log(gray(" \u2022 .env.example already exists"));
424
+ } else {
425
+ console.log(yellow(" \u26A0 No .env file found \u2014 create one first"));
426
+ }
427
+ const updated = ensureGitignore(".");
428
+ if (updated) {
429
+ console.log(green(" \u2713 Updated .gitignore with .env entries"));
430
+ } else {
431
+ console.log(gray(" \u2022 .gitignore already includes .env"));
432
+ }
433
+ console.log("");
434
+ break;
435
+ }
436
+ case "install-hook": {
437
+ try {
438
+ installPreCommitHook(".");
439
+ console.log("");
440
+ console.log(green(bold(" \u2713 Pre-commit hook installed")));
441
+ console.log(gray(" .env files will be blocked from commits"));
442
+ console.log(gray(" Bypass with: git commit --no-verify"));
443
+ console.log("");
444
+ } catch (err) {
445
+ console.error(red(`
446
+ \u2716 ${err.message}
447
+ `));
448
+ process.exit(1);
449
+ }
450
+ break;
451
+ }
452
+ case "-v":
453
+ case "--version":
454
+ console.log("1.0.0");
455
+ break;
456
+ case "-h":
457
+ case "--help":
458
+ case void 0:
459
+ showHelp();
460
+ break;
461
+ default:
462
+ console.error(red(`
463
+ Unknown command: ${command}`));
464
+ showHelp();
465
+ process.exit(1);
466
+ }
package/dist/cli.mjs ADDED
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ bold,
4
+ dim,
5
+ gray,
6
+ green,
7
+ red,
8
+ yellow
9
+ } from "./chunk-RLNPDDSP.mjs";
10
+ import {
11
+ checkLeaks,
12
+ ensureGitignore,
13
+ generateExample,
14
+ inferRules,
15
+ installPreCommitHook,
16
+ validate
17
+ } from "./chunk-QXZQR35R.mjs";
18
+ import {
19
+ parseEnvFileToMap,
20
+ syncCheck
21
+ } from "./chunk-YYFNUR7Y.mjs";
22
+
23
+ // src/cli.ts
24
+ import { readFileSync, existsSync } from "fs";
25
+ var args = process.argv.slice(2);
26
+ var command = args[0];
27
+ function showHelp() {
28
+ console.log(`
29
+ ${bold("dotenv-guard")} \u2014 Validate env vars, sync .env files, prevent leaks
30
+
31
+ ${bold("Usage:")}
32
+ dotenv-guard <command> [options]
33
+
34
+ ${bold("Commands:")}
35
+ sync Compare .env with .env.example
36
+ validate Validate env var types (from .env.example hints)
37
+ leak-check Scan for leaked secrets in git
38
+ init Create .env.example + update .gitignore
39
+ install-hook Install git pre-commit hook to block .env commits
40
+
41
+ ${bold("Options:")}
42
+ --env <path> Path to .env file (default: .env)
43
+ --example <path> Path to .env.example (default: .env.example)
44
+ -h, --help Show help
45
+ -v, --version Show version
46
+
47
+ ${bold("Programmatic:")}
48
+ import 'dotenv-guard/auto';
49
+ // Auto-check on import
50
+ import { guard } from 'dotenv-guard'; // Manual check
51
+ `);
52
+ }
53
+ var envPath = args.includes("--env") ? args[args.indexOf("--env") + 1] : ".env";
54
+ var examplePath = args.includes("--example") ? args[args.indexOf("--example") + 1] : ".env.example";
55
+ switch (command) {
56
+ case "sync": {
57
+ try {
58
+ const result = syncCheck(envPath, examplePath);
59
+ console.log("");
60
+ console.log(bold(" dotenv-guard sync"));
61
+ console.log(gray(` ${result.envPath} \u2194 ${result.examplePath}`));
62
+ console.log("");
63
+ if (result.missing.length > 0) {
64
+ console.log(red(` \u2716 Missing (${result.missing.length}):`));
65
+ result.missing.forEach((k) => console.log(yellow(` \u2192 ${k}`)));
66
+ console.log("");
67
+ }
68
+ if (result.extra.length > 0) {
69
+ console.log(yellow(` \u26A0 Extra (${result.extra.length}):`));
70
+ result.extra.forEach((k) => console.log(gray(` \u2192 ${k}`)));
71
+ console.log("");
72
+ }
73
+ console.log(green(` \u2713 Synced: ${result.synced.length} variables`));
74
+ if (result.missing.length > 0) {
75
+ console.log("");
76
+ console.log(dim(` Add missing variables to ${result.envPath} to fix.`));
77
+ }
78
+ console.log("");
79
+ process.exit(result.missing.length > 0 ? 1 : 0);
80
+ } catch (err) {
81
+ console.error(red(`
82
+ \u2716 ${err.message}
83
+ `));
84
+ process.exit(1);
85
+ }
86
+ break;
87
+ }
88
+ case "validate": {
89
+ try {
90
+ if (!existsSync(examplePath)) {
91
+ console.error(red(`
92
+ \u2716 ${examplePath} not found. Run "dotenv-guard init" first.
93
+ `));
94
+ process.exit(1);
95
+ }
96
+ const exampleContent = readFileSync(examplePath, "utf-8");
97
+ const rules = inferRules(exampleContent);
98
+ const envMap = existsSync(envPath) ? parseEnvFileToMap(readFileSync(envPath, "utf-8")) : /* @__PURE__ */ new Map();
99
+ const env = {};
100
+ for (const rule of rules) {
101
+ env[rule.key] = process.env[rule.key] || envMap.get(rule.key);
102
+ }
103
+ const errors = validate(env, rules);
104
+ console.log("");
105
+ console.log(bold(" dotenv-guard validate"));
106
+ console.log("");
107
+ if (errors.length === 0) {
108
+ console.log(green(` \u2713 All ${rules.length} variables are valid`));
109
+ } else {
110
+ console.log(red(` \u2716 ${errors.length} validation error(s):`));
111
+ console.log("");
112
+ errors.forEach((e) => {
113
+ console.log(` ${red("\u2716")} ${bold(e.key)} ${e.message}`);
114
+ if (e.value) console.log(gray(` current value: "${e.value}"`));
115
+ });
116
+ }
117
+ console.log("");
118
+ process.exit(errors.length > 0 ? 1 : 0);
119
+ } catch (err) {
120
+ console.error(red(`
121
+ \u2716 ${err.message}
122
+ `));
123
+ process.exit(1);
124
+ }
125
+ break;
126
+ }
127
+ case "leak-check": {
128
+ const result = checkLeaks(".");
129
+ console.log("");
130
+ console.log(bold(" dotenv-guard leak-check"));
131
+ console.log("");
132
+ if (result.gitignoreHasEnv) {
133
+ console.log(green(" \u2713 .gitignore includes .env"));
134
+ } else {
135
+ console.log(red(" \u2716 .gitignore does NOT include .env"));
136
+ console.log(gray(" Run: dotenv-guard init"));
137
+ }
138
+ if (result.preCommitHookInstalled) {
139
+ console.log(green(" \u2713 Pre-commit hook installed"));
140
+ } else {
141
+ console.log(yellow(" \u26A0 No pre-commit hook"));
142
+ console.log(gray(" Run: dotenv-guard install-hook"));
143
+ }
144
+ if (result.leaks.length === 0) {
145
+ console.log(green(" \u2713 No .env files tracked in git"));
146
+ } else {
147
+ console.log(red(` \u2716 ${result.leaks.length} leaked variable(s) in git:`));
148
+ console.log("");
149
+ result.leaks.forEach((l) => {
150
+ const icon = l.severity === "high" ? red("HIGH") : yellow("MED");
151
+ console.log(` [${icon}] ${l.file}:${l.line} \u2014 ${bold(l.key)}`);
152
+ });
153
+ console.log("");
154
+ console.log(dim(" Remove tracked files: git rm --cached .env"));
155
+ }
156
+ console.log("");
157
+ process.exit(result.leaks.length > 0 ? 1 : 0);
158
+ break;
159
+ }
160
+ case "init": {
161
+ console.log("");
162
+ console.log(bold(" dotenv-guard init"));
163
+ console.log("");
164
+ if (existsSync(".env") && !existsSync(".env.example")) {
165
+ generateExample(".env", ".env.example");
166
+ console.log(green(" \u2713 Created .env.example from .env"));
167
+ } else if (existsSync(".env.example")) {
168
+ console.log(gray(" \u2022 .env.example already exists"));
169
+ } else {
170
+ console.log(yellow(" \u26A0 No .env file found \u2014 create one first"));
171
+ }
172
+ const updated = ensureGitignore(".");
173
+ if (updated) {
174
+ console.log(green(" \u2713 Updated .gitignore with .env entries"));
175
+ } else {
176
+ console.log(gray(" \u2022 .gitignore already includes .env"));
177
+ }
178
+ console.log("");
179
+ break;
180
+ }
181
+ case "install-hook": {
182
+ try {
183
+ installPreCommitHook(".");
184
+ console.log("");
185
+ console.log(green(bold(" \u2713 Pre-commit hook installed")));
186
+ console.log(gray(" .env files will be blocked from commits"));
187
+ console.log(gray(" Bypass with: git commit --no-verify"));
188
+ console.log("");
189
+ } catch (err) {
190
+ console.error(red(`
191
+ \u2716 ${err.message}
192
+ `));
193
+ process.exit(1);
194
+ }
195
+ break;
196
+ }
197
+ case "-v":
198
+ case "--version":
199
+ console.log("1.0.0");
200
+ break;
201
+ case "-h":
202
+ case "--help":
203
+ case void 0:
204
+ showHelp();
205
+ break;
206
+ default:
207
+ console.error(red(`
208
+ Unknown command: ${command}`));
209
+ showHelp();
210
+ process.exit(1);
211
+ }