@danielszlaski/envguard 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.envguardrc.example.json +1 -0
  2. package/.envguardrc.json +16 -0
  3. package/.github/FUNDING.yml +15 -0
  4. package/LICENSE +1 -1
  5. package/README.md +285 -10
  6. package/dist/analyzer/envAnalyzer.d.ts +8 -2
  7. package/dist/analyzer/envAnalyzer.d.ts.map +1 -1
  8. package/dist/analyzer/envAnalyzer.js +22 -8
  9. package/dist/analyzer/envAnalyzer.js.map +1 -1
  10. package/dist/cli.js +58 -3
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/fix.d.ts.map +1 -1
  13. package/dist/commands/fix.js +32 -7
  14. package/dist/commands/fix.js.map +1 -1
  15. package/dist/commands/install-hook.d.ts +18 -0
  16. package/dist/commands/install-hook.d.ts.map +1 -0
  17. package/dist/commands/install-hook.js +148 -0
  18. package/dist/commands/install-hook.js.map +1 -0
  19. package/dist/commands/scan.d.ts +1 -0
  20. package/dist/commands/scan.d.ts.map +1 -1
  21. package/dist/commands/scan.js +150 -41
  22. package/dist/commands/scan.js.map +1 -1
  23. package/dist/config/configLoader.d.ts +6 -0
  24. package/dist/config/configLoader.d.ts.map +1 -1
  25. package/dist/config/configLoader.js +1 -0
  26. package/dist/config/configLoader.js.map +1 -1
  27. package/dist/scanner/codeScanner.d.ts +5 -2
  28. package/dist/scanner/codeScanner.d.ts.map +1 -1
  29. package/dist/scanner/codeScanner.js +72 -25
  30. package/dist/scanner/codeScanner.js.map +1 -1
  31. package/dist/types.d.ts +6 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/analyzer/envAnalyzer.ts +27 -10
  35. package/src/cli.ts +62 -3
  36. package/src/commands/fix.ts +40 -9
  37. package/src/commands/install-hook.ts +128 -0
  38. package/src/commands/scan.ts +168 -47
  39. package/src/config/configLoader.ts +8 -0
  40. package/src/scanner/codeScanner.ts +97 -28
  41. package/src/types.ts +3 -1
  42. package/test-project/src/lambda2/handler2.js +1 -1
  43. package/test-project/.envguardrc.json +0 -7
@@ -0,0 +1,128 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { Logger } from '../utils/logger';
4
+
5
+ const HOOK_TYPES = ['pre-commit', 'pre-push'] as const;
6
+ type HookType = typeof HOOK_TYPES[number];
7
+
8
+ interface InstallHookOptions {
9
+ type?: HookType;
10
+ force?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Install a Git hook that runs envguard before commits or pushes
15
+ */
16
+ export async function installHookCommand(options: InstallHookOptions = {}) {
17
+ const rootDir = process.cwd();
18
+ const gitDir = path.join(rootDir, '.git');
19
+ const hooksDir = path.join(gitDir, 'hooks');
20
+
21
+ // Check if this is a git repository
22
+ if (!fs.existsSync(gitDir)) {
23
+ Logger.error('Not a git repository. Please run this command in a git repository.');
24
+ Logger.blank();
25
+ process.exit(1);
26
+ }
27
+
28
+ // Ensure hooks directory exists
29
+ if (!fs.existsSync(hooksDir)) {
30
+ fs.mkdirSync(hooksDir, { recursive: true });
31
+ Logger.success('Created .git/hooks directory');
32
+ }
33
+
34
+ const hookType = options.type || 'pre-commit';
35
+ const hookPath = path.join(hooksDir, hookType);
36
+
37
+ // Check if hook already exists
38
+ if (fs.existsSync(hookPath) && !options.force) {
39
+ Logger.warning(`${hookType} hook already exists.`);
40
+ Logger.info('Use --force to overwrite the existing hook', true);
41
+ Logger.blank();
42
+ process.exit(1);
43
+ }
44
+
45
+ // Create the hook script
46
+ const hookContent = generateHookScript(hookType);
47
+
48
+ try {
49
+ fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
50
+ Logger.success(`Installed ${hookType} hook successfully!`);
51
+ Logger.blank();
52
+ Logger.info('The hook will run `envguard check` automatically before each ' +
53
+ (hookType === 'pre-commit' ? 'commit' : 'push'), true);
54
+ Logger.info('To bypass the hook, use: git ' +
55
+ (hookType === 'pre-commit' ? 'commit' : 'push') + ' --no-verify', true);
56
+ Logger.blank();
57
+ } catch (error) {
58
+ Logger.error(`Failed to install hook: ${error}`);
59
+ Logger.blank();
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Uninstall a Git hook
66
+ */
67
+ export async function uninstallHookCommand(options: { type?: HookType } = {}) {
68
+ const rootDir = process.cwd();
69
+ const hookType = options.type || 'pre-commit';
70
+ const hookPath = path.join(rootDir, '.git', 'hooks', hookType);
71
+
72
+ if (!fs.existsSync(hookPath)) {
73
+ Logger.warning(`No ${hookType} hook found.`);
74
+ Logger.blank();
75
+ return;
76
+ }
77
+
78
+ // Check if it's our hook
79
+ const hookContent = fs.readFileSync(hookPath, 'utf-8');
80
+ if (!hookContent.includes('envguard check')) {
81
+ Logger.warning(`The ${hookType} hook exists but was not created by envguard.`);
82
+ Logger.info('Manual removal required if you want to delete it.', true);
83
+ Logger.blank();
84
+ return;
85
+ }
86
+
87
+ try {
88
+ fs.unlinkSync(hookPath);
89
+ Logger.success(`Removed ${hookType} hook successfully!`);
90
+ Logger.blank();
91
+ } catch (error) {
92
+ Logger.error(`Failed to remove hook: ${error}`);
93
+ Logger.blank();
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Generate the hook script content
100
+ */
101
+ function generateHookScript(hookType: HookType): string {
102
+ const hookMessage = hookType === 'pre-commit' ? 'commit' : 'push';
103
+
104
+ return `#!/bin/sh
105
+ # EnvGuard ${hookType} hook
106
+ # This hook runs envguard to check environment variables before ${hookMessage}
107
+ # To bypass this hook, use: git ${hookMessage} --no-verify
108
+
109
+ echo "Running EnvGuard environment variable check..."
110
+
111
+ # Run envguard check
112
+ npx envguard check
113
+
114
+ # Capture the exit code
115
+ EXIT_CODE=$?
116
+
117
+ if [ $EXIT_CODE -ne 0 ]; then
118
+ echo ""
119
+ echo "❌ EnvGuard check failed. Please fix the issues above before ${hookMessage}ing."
120
+ echo " Or run: git ${hookMessage} --no-verify to bypass this check."
121
+ echo ""
122
+ exit 1
123
+ fi
124
+
125
+ echo "✓ EnvGuard check passed!"
126
+ exit 0
127
+ `;
128
+ }
@@ -10,7 +10,7 @@ import { isKnownRuntimeVar, getRuntimeVarCategory } from '../constants/knownEnvV
10
10
  import { ConfigLoader } from '../config/configLoader';
11
11
  import { Logger } from '../utils/logger';
12
12
 
13
- export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
13
+ export async function scanCommand(options: { ci?: boolean; strict?: boolean; detectFallbacks?: boolean }) {
14
14
  const rootDir = process.cwd();
15
15
 
16
16
  // Load configuration
@@ -18,11 +18,19 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
18
18
 
19
19
  // CLI options override config file
20
20
  const strictMode = options.strict !== undefined ? options.strict : config.strict;
21
+ const detectFallbacks = options.detectFallbacks !== undefined ? options.detectFallbacks : (config.detectFallbacks !== undefined ? config.detectFallbacks : true);
21
22
 
22
23
  Logger.startSpinner('Scanning codebase for environment variables...');
23
24
 
24
25
  // Step 1: Find all .env files and serverless.yml files
25
- const scanner = new CodeScanner(rootDir);
26
+ const excludePatterns = [
27
+ 'node_modules',
28
+ 'dist',
29
+ 'build',
30
+ '.git',
31
+ ...(config.exclude || []),
32
+ ];
33
+ const scanner = new CodeScanner(rootDir, excludePatterns);
26
34
  const envFiles = await scanner.findEnvFiles();
27
35
  const serverlessFiles = await scanner.findServerlessFiles();
28
36
 
@@ -55,7 +63,7 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
55
63
  Logger.info(`Found ${serverlessVars.size} variable(s) in serverless.yml`, true);
56
64
 
57
65
  // Scan code files in this directory to see what's actually used
58
- const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner);
66
+ const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner, config.exclude);
59
67
  Logger.info(`Found ${usedVars.size} variable(s) used in code`, true);
60
68
  Logger.blank();
61
69
 
@@ -72,10 +80,10 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
72
80
  }
73
81
 
74
82
  // Check for variables used in code but not defined in serverless.yml
75
- const missingFromServerless: Array<{ varName: string; locations: string[]; category?: string }> = [];
83
+ const missingFromServerless: Array<{ varName: string; locations: string[]; hasFallback: boolean; category?: string }> = [];
76
84
  const skippedRuntimeVars: Array<{ varName: string; category: string }> = [];
77
85
 
78
- for (const [varName, locations] of usedVars.entries()) {
86
+ for (const [varName, usage] of usedVars.entries()) {
79
87
  if (!serverlessVars.has(varName)) {
80
88
  // In non-strict mode, skip known runtime variables and custom ignore vars
81
89
  const isCustomIgnored = ConfigLoader.shouldIgnoreVar(varName, config);
@@ -87,17 +95,18 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
87
95
  skippedRuntimeVars.push({ varName, category });
88
96
  }
89
97
  } else {
90
- missingFromServerless.push({ varName, locations });
98
+ missingFromServerless.push({ varName, locations: usage.locations, hasFallback: usage.hasFallback });
91
99
  }
92
100
  }
93
101
  }
94
102
 
95
103
  if (unusedServerlessVars.length > 0) {
96
- Logger.warning('Unused variables in serverless.yml:', true);
104
+ Logger.info('Unused variables in serverless.yml:', true);
97
105
  unusedServerlessVars.forEach((varName) => {
98
- Logger.warningItem(varName, 2);
106
+ Logger.infoItem(varName, 2);
99
107
  allIssues.push({
100
108
  type: 'unused',
109
+ severity: 'info',
101
110
  varName,
102
111
  details: `Defined in serverless.yml but never used in code`,
103
112
  });
@@ -106,20 +115,48 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
106
115
  }
107
116
 
108
117
  if (missingFromServerless.length > 0) {
109
- Logger.error('Missing from serverless.yml:', true);
110
- missingFromServerless.forEach((item) => {
111
- Logger.errorItem(item.varName, 2);
112
- if (item.locations && item.locations.length > 0) {
113
- Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
114
- }
115
- allIssues.push({
116
- type: 'missing',
117
- varName: item.varName,
118
- details: `Used in code but not defined in serverless.yml`,
119
- locations: item.locations,
118
+ // Group by severity (respect detectFallbacks config)
119
+ const errors = missingFromServerless.filter(item => !detectFallbacks || !item.hasFallback);
120
+ const warnings = missingFromServerless.filter(item => detectFallbacks && item.hasFallback);
121
+
122
+ if (errors.length > 0) {
123
+ Logger.error('Missing from serverless.yml:', true);
124
+ errors.forEach((item) => {
125
+ Logger.errorItem(item.varName, 2);
126
+ if (item.locations && item.locations.length > 0) {
127
+ Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
128
+ }
129
+ const details = (detectFallbacks && item.hasFallback)
130
+ ? `Used in code with fallback but not defined in serverless.yml`
131
+ : `Used in code but not defined in serverless.yml`;
132
+ allIssues.push({
133
+ type: 'missing',
134
+ severity: 'error',
135
+ varName: item.varName,
136
+ details,
137
+ locations: item.locations,
138
+ });
120
139
  });
121
- });
122
- Logger.blank();
140
+ Logger.blank();
141
+ }
142
+
143
+ if (warnings.length > 0) {
144
+ Logger.warning('Missing from serverless.yml (with fallback):', true);
145
+ warnings.forEach((item) => {
146
+ Logger.warningItem(item.varName, 2);
147
+ if (item.locations && item.locations.length > 0) {
148
+ Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
149
+ }
150
+ allIssues.push({
151
+ type: 'missing',
152
+ severity: 'warning',
153
+ varName: item.varName,
154
+ details: `Used in code with fallback but not defined in serverless.yml`,
155
+ locations: item.locations,
156
+ });
157
+ });
158
+ Logger.blank();
159
+ }
123
160
  }
124
161
 
125
162
  if (unusedServerlessVars.length === 0 && missingFromServerless.length === 0) {
@@ -155,7 +192,26 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
155
192
  Logger.blank();
156
193
 
157
194
  // Step 3: Scan code files in this directory and subdirectories
158
- const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
195
+ const allUsedVars = await scanDirectoryForVars(rootDir, envDir, scanner, config.exclude);
196
+
197
+ // Filter out ignored variables based on config
198
+ const usedVars = new Map<string, { locations: string[], hasFallback: boolean }>();
199
+ const skippedVarsInScope: Array<{ varName: string; category: string }> = [];
200
+
201
+ for (const [varName, usage] of allUsedVars.entries()) {
202
+ const isCustomIgnored = ConfigLoader.shouldIgnoreVar(varName, config);
203
+ const isRuntimeVar = isKnownRuntimeVar(varName);
204
+
205
+ // In non-strict mode, skip known runtime variables and custom ignore vars
206
+ if (strictMode || (!isRuntimeVar && !isCustomIgnored)) {
207
+ usedVars.set(varName, usage);
208
+ } else {
209
+ const category = isCustomIgnored ? 'Custom (from config)' : getRuntimeVarCategory(varName);
210
+ if (category) {
211
+ skippedVarsInScope.push({ varName, category });
212
+ }
213
+ }
214
+ }
159
215
 
160
216
  Logger.info(`Found ${usedVars.size} variable(s) used in this scope`, true);
161
217
 
@@ -170,17 +226,19 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
170
226
  Logger.blank();
171
227
 
172
228
  // Step 6: Analyze and find issues
173
- const result = analyzer.analyze(usedVars, definedVars, exampleVars);
229
+ const result = analyzer.analyze(usedVars, definedVars, exampleVars, detectFallbacks);
174
230
 
175
231
  if (result.issues.length > 0) {
176
- // Group issues by type
177
- const missingIssues = result.issues.filter(i => i.type === 'missing');
232
+ // Group issues by type and severity
233
+ const missingErrors = result.issues.filter(i => i.type === 'missing' && i.severity === 'error');
234
+ const missingWarnings = result.issues.filter(i => i.type === 'missing' && i.severity === 'warning');
178
235
  const unusedIssues = result.issues.filter(i => i.type === 'unused');
179
- const undocumentedIssues = result.issues.filter(i => i.type === 'undocumented');
236
+ const undocumentedWarnings = result.issues.filter(i => i.type === 'undocumented' && i.severity === 'warning');
237
+ const undocumentedInfo = result.issues.filter(i => i.type === 'undocumented' && i.severity === 'info');
180
238
 
181
- if (missingIssues.length > 0) {
239
+ if (missingErrors.length > 0) {
182
240
  Logger.error('Missing from .env:', true);
183
- missingIssues.forEach((issue) => {
241
+ missingErrors.forEach((issue) => {
184
242
  Logger.errorItem(issue.varName, 2);
185
243
  if (issue.locations && issue.locations.length > 0) {
186
244
  Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
@@ -189,17 +247,36 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
189
247
  Logger.blank();
190
248
  }
191
249
 
250
+ if (missingWarnings.length > 0) {
251
+ Logger.warning('Missing from .env (with fallback):', true);
252
+ missingWarnings.forEach((issue) => {
253
+ Logger.warningItem(issue.varName, 2);
254
+ if (issue.locations && issue.locations.length > 0) {
255
+ Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
256
+ }
257
+ });
258
+ Logger.blank();
259
+ }
260
+
192
261
  if (unusedIssues.length > 0) {
193
- Logger.warning('Unused variables:', true);
262
+ Logger.info('Unused variables:', true);
194
263
  unusedIssues.forEach((issue) => {
264
+ Logger.infoItem(issue.varName, 2);
265
+ });
266
+ Logger.blank();
267
+ }
268
+
269
+ if (undocumentedWarnings.length > 0) {
270
+ Logger.warning('Missing from .env.example:', true);
271
+ undocumentedWarnings.forEach((issue) => {
195
272
  Logger.warningItem(issue.varName, 2);
196
273
  });
197
274
  Logger.blank();
198
275
  }
199
276
 
200
- if (undocumentedIssues.length > 0) {
201
- Logger.info('Missing from .env.example:', true);
202
- undocumentedIssues.forEach((issue) => {
277
+ if (undocumentedInfo.length > 0) {
278
+ Logger.info('Missing from .env.example (with fallback):', true);
279
+ undocumentedInfo.forEach((issue) => {
203
280
  Logger.infoItem(issue.varName, 2);
204
281
  });
205
282
  Logger.blank();
@@ -210,6 +287,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
210
287
  Logger.success('No issues in this directory', true);
211
288
  Logger.blank();
212
289
  }
290
+
291
+ // Show skipped variables in non-strict mode
292
+ if (!strictMode && skippedVarsInScope.length > 0) {
293
+ Logger.info('Skipped known runtime/ignored variables (use --strict to show):', true);
294
+ // Group by category
295
+ const grouped = new Map<string, string[]>();
296
+ for (const { varName, category } of skippedVarsInScope) {
297
+ if (!grouped.has(category)) {
298
+ grouped.set(category, []);
299
+ }
300
+ grouped.get(category)!.push(varName);
301
+ }
302
+ for (const [category, vars] of grouped.entries()) {
303
+ Logger.info(`${category}: ${vars.join(', ')}`, true);
304
+ }
305
+ Logger.blank();
306
+ }
213
307
  }
214
308
 
215
309
  // Display summary
@@ -219,8 +313,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
219
313
  return { success: true, issues: [] };
220
314
  }
221
315
 
316
+ // Count issues by severity
317
+ const errorCount = allIssues.filter(i => i.severity === 'error').length;
318
+ const warningCount = allIssues.filter(i => i.severity === 'warning').length;
319
+ const infoCount = allIssues.filter(i => i.severity === 'info').length;
320
+
222
321
  Logger.blank();
223
- Logger.warning(`Total: ${allIssues.length} issue(s) across ${envFiles.length} location(s)`);
322
+ if (errorCount > 0) {
323
+ Logger.error(`Errors: ${errorCount}`, false);
324
+ }
325
+ if (warningCount > 0) {
326
+ Logger.warning(`Warnings: ${warningCount}`, false);
327
+ }
328
+ if (infoCount > 0) {
329
+ Logger.info(`Info: ${infoCount}`, false);
330
+ }
331
+ Logger.blank();
332
+ Logger.warning(`Total: ${allIssues.length} issue(s) across ${envFiles.length + serverlessFiles.length} location(s)`);
224
333
  Logger.blank();
225
334
 
226
335
  // Suggest fix
@@ -242,28 +351,34 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
242
351
  async function scanDirectoryForVars(
243
352
  rootDir: string,
244
353
  targetDir: string,
245
- scanner: CodeScanner
246
- ): Promise<Map<string, string[]>> {
247
- const envVars = new Map<string, string[]>();
354
+ scanner: CodeScanner,
355
+ excludePatterns: string[] = []
356
+ ): Promise<Map<string, { locations: string[], hasFallback: boolean }>> {
357
+ const envVars = new Map<string, { locations: string[], hasFallback: boolean }>();
248
358
 
249
359
  // Find all code files in this directory and subdirectories
250
360
  const relativeDir = path.relative(rootDir, targetDir);
251
361
  const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
252
362
 
363
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'];
364
+ const customIgnore = excludePatterns.map(p => p.includes('*') ? p : `**/${p}/**`);
365
+
253
366
  const files = await glob(pattern, {
254
367
  cwd: rootDir,
255
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
368
+ ignore: [...defaultIgnore, ...customIgnore],
256
369
  absolute: true,
257
370
  });
258
371
 
259
372
  for (const file of files) {
260
373
  const vars = await scanner.scanFile(file);
261
- for (const varName of vars) {
374
+ for (const [varName, hasFallback] of vars.entries()) {
262
375
  const relativePath = path.relative(rootDir, file);
263
376
  if (!envVars.has(varName)) {
264
- envVars.set(varName, []);
377
+ envVars.set(varName, { locations: [], hasFallback: false });
265
378
  }
266
- envVars.get(varName)!.push(relativePath);
379
+ const entry = envVars.get(varName)!;
380
+ entry.locations.push(relativePath);
381
+ entry.hasFallback = entry.hasFallback || hasFallback;
267
382
  }
268
383
  }
269
384
 
@@ -274,28 +389,34 @@ async function scanDirectoryForVars(
274
389
  async function scanDirectoryForCodeVars(
275
390
  rootDir: string,
276
391
  targetDir: string,
277
- scanner: CodeScanner
278
- ): Promise<Map<string, string[]>> {
279
- const envVars = new Map<string, string[]>();
392
+ scanner: CodeScanner,
393
+ excludePatterns: string[] = []
394
+ ): Promise<Map<string, { locations: string[], hasFallback: boolean }>> {
395
+ const envVars = new Map<string, { locations: string[], hasFallback: boolean }>();
280
396
 
281
397
  // Find all code files in this directory only (not subdirectories for serverless)
282
398
  const relativeDir = path.relative(rootDir, targetDir);
283
399
  const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
284
400
 
401
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'];
402
+ const customIgnore = excludePatterns.map(p => p.includes('*') ? p : `**/${p}/**`);
403
+
285
404
  const files = await glob(pattern, {
286
405
  cwd: rootDir,
287
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
406
+ ignore: [...defaultIgnore, ...customIgnore],
288
407
  absolute: true,
289
408
  });
290
409
 
291
410
  for (const file of files) {
292
411
  const vars = await scanner.scanFile(file);
293
- for (const varName of vars) {
412
+ for (const [varName, hasFallback] of vars.entries()) {
294
413
  const relativePath = path.relative(rootDir, file);
295
414
  if (!envVars.has(varName)) {
296
- envVars.set(varName, []);
415
+ envVars.set(varName, { locations: [], hasFallback: false });
297
416
  }
298
- envVars.get(varName)!.push(relativePath);
417
+ const entry = envVars.get(varName)!;
418
+ entry.locations.push(relativePath);
419
+ entry.hasFallback = entry.hasFallback || hasFallback;
299
420
  }
300
421
  }
301
422
 
@@ -17,12 +17,20 @@ export interface EnvGuardConfig {
17
17
  * Enable strict mode by default
18
18
  */
19
19
  strict?: boolean;
20
+
21
+ /**
22
+ * Detect fallback patterns in code (||, ??, conditionals, etc.)
23
+ * When enabled, variables with fallbacks are treated as warnings instead of errors
24
+ * Default: true
25
+ */
26
+ detectFallbacks?: boolean;
20
27
  }
21
28
 
22
29
  const DEFAULT_CONFIG: EnvGuardConfig = {
23
30
  ignoreVars: [],
24
31
  exclude: [],
25
32
  strict: false,
33
+ detectFallbacks: true,
26
34
  };
27
35
 
28
36
  const CONFIG_FILE_NAMES = [