@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
@@ -43,8 +43,18 @@ const logger_1 = require("../utils/logger");
43
43
  async function fixCommand() {
44
44
  const rootDir = process.cwd();
45
45
  logger_1.Logger.startSpinner('Generating .env.example files...');
46
+ // Load configuration
47
+ const { ConfigLoader } = require('../config/configLoader');
48
+ const config = ConfigLoader.loadConfig(rootDir);
46
49
  // Step 1: Find all .env files
47
- const scanner = new codeScanner_1.CodeScanner(rootDir);
50
+ const excludePatterns = [
51
+ 'node_modules',
52
+ 'dist',
53
+ 'build',
54
+ '.git',
55
+ ...(config.exclude || []),
56
+ ];
57
+ const scanner = new codeScanner_1.CodeScanner(rootDir, excludePatterns);
48
58
  const envFiles = await scanner.findEnvFiles();
49
59
  logger_1.Logger.stopSpinner();
50
60
  if (envFiles.length === 0) {
@@ -64,7 +74,18 @@ async function fixCommand() {
64
74
  const displayPath = relativePath || '.';
65
75
  logger_1.Logger.path(`Processing ${displayPath}/`);
66
76
  // Step 3: Scan code files in this directory and subdirectories
67
- const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
77
+ const allUsedVars = await scanDirectoryForVars(rootDir, envDir, scanner, config.exclude);
78
+ // Filter out ignored variables based on config (they shouldn't be in .env.example)
79
+ const { isKnownRuntimeVar } = require('../constants/knownEnvVars');
80
+ const usedVars = new Map();
81
+ for (const [varName, usage] of allUsedVars.entries()) {
82
+ const isCustomIgnored = ConfigLoader.shouldIgnoreVar(varName, config);
83
+ const isRuntimeVar = isKnownRuntimeVar(varName);
84
+ // Skip known runtime variables and custom ignore vars
85
+ if (!isRuntimeVar && !isCustomIgnored) {
86
+ usedVars.set(varName, usage);
87
+ }
88
+ }
68
89
  if (usedVars.size === 0) {
69
90
  logger_1.Logger.info('No environment variables found in code', true);
70
91
  logger_1.Logger.blank();
@@ -93,25 +114,29 @@ async function fixCommand() {
93
114
  logger_1.Logger.summary(`Generated ${envFiles.length} .env.example file(s) with ${totalVars} total variables`);
94
115
  return { success: true };
95
116
  }
96
- async function scanDirectoryForVars(rootDir, targetDir, scanner) {
117
+ async function scanDirectoryForVars(rootDir, targetDir, scanner, excludePatterns = []) {
97
118
  const envVars = new Map();
98
119
  const { glob } = require('glob');
99
120
  // Find all code files in this directory and subdirectories
100
121
  const relativeDir = path.relative(rootDir, targetDir);
101
122
  const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
123
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'];
124
+ const customIgnore = excludePatterns.map(p => p.includes('*') ? p : `**/${p}/**`);
102
125
  const files = await glob(pattern, {
103
126
  cwd: rootDir,
104
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
127
+ ignore: [...defaultIgnore, ...customIgnore],
105
128
  absolute: true,
106
129
  });
107
130
  for (const file of files) {
108
131
  const vars = await scanner.scanFile(file);
109
- for (const varName of vars) {
132
+ for (const [varName, hasFallback] of vars.entries()) {
110
133
  const relativePath = path.relative(rootDir, file);
111
134
  if (!envVars.has(varName)) {
112
- envVars.set(varName, []);
135
+ envVars.set(varName, { locations: [], hasFallback: false });
113
136
  }
114
- envVars.get(varName).push(relativePath);
137
+ const entry = envVars.get(varName);
138
+ entry.locations.push(relativePath);
139
+ entry.hasFallback = entry.hasFallback || hasFallback;
115
140
  }
116
141
  }
117
142
  return envVars;
@@ -1 +1 @@
1
- {"version":3,"file":"fix.js","sourceRoot":"","sources":["../../src/commands/fix.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fix.js","sourceRoot":"","sources":["../../src/commands/fix.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOA,gCAiGC;AAxGD,2CAA6B;AAC7B,uCAAyB;AACzB,wDAAqD;AACrD,mDAAgD;AAChD,yDAAsD;AACtD,4CAAyC;AAElC,KAAK,UAAU,UAAU;IAC9B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAE9B,eAAM,CAAC,YAAY,CAAC,kCAAkC,CAAC,CAAC;IAExD,qBAAqB;IACrB,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAEhD,8BAA8B;IAC9B,MAAM,eAAe,GAAG;QACtB,cAAc;QACd,MAAM;QACN,OAAO;QACP,MAAM;QACN,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;KAC1B,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,yBAAW,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IAC1D,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;IAE9C,eAAM,CAAC,WAAW,EAAE,CAAC;IAErB,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,eAAM,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;QACrD,eAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,eAAM,CAAC,OAAO,CAAC,SAAS,QAAQ,CAAC,MAAM,eAAe,CAAC,CAAC;IACxD,eAAM,CAAC,KAAK,EAAE,CAAC;IAEf,MAAM,MAAM,GAAG,IAAI,qBAAS,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAG,IAAI,yBAAW,EAAE,CAAC;IACnC,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,iCAAiC;IACjC,KAAK,MAAM,WAAW,IAAI,QAAQ,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACzC,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACpD,MAAM,WAAW,GAAG,YAAY,IAAI,GAAG,CAAC;QAExC,eAAM,CAAC,IAAI,CAAC,cAAc,WAAW,GAAG,CAAC,CAAC;QAE1C,+DAA+D;QAC/D,MAAM,WAAW,GAAG,MAAM,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QAEzF,mFAAmF;QACnF,MAAM,EAAE,iBAAiB,EAAE,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;QACnE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyD,CAAC;QAElF,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,CAAC;YACrD,MAAM,eAAe,GAAG,YAAY,CAAC,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACtE,MAAM,YAAY,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;YAEhD,sDAAsD;YACtD,IAAI,CAAC,YAAY,IAAI,CAAC,eAAe,EAAE,CAAC;gBACtC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACxB,eAAM,CAAC,IAAI,CAAC,wCAAwC,EAAE,IAAI,CAAC,CAAC;YAC5D,eAAM,CAAC,KAAK,EAAE,CAAC;YACf,SAAS;QACX,CAAC;QAED,eAAM,CAAC,OAAO,CAAC,SAAS,QAAQ,CAAC,IAAI,qCAAqC,EAAE,IAAI,CAAC,CAAC;QAClF,eAAM,CAAC,KAAK,EAAE,CAAC;QAEf,2DAA2D;QAC3D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;QACtD,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAElD,4CAA4C;QAC5C,MAAM,UAAU,GAAG,QAAQ,CAAC,sBAAsB,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;QAE9E,gCAAgC;QAChC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;QAEnD,eAAM,CAAC,OAAO,CAAC,aAAa,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACzE,eAAM,CAAC,KAAK,EAAE,CAAC;QAEf,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC;QAE3B,0CAA0C;QAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,cAAc,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAEpF,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,eAAM,CAAC,OAAO,CAAC,sBAAsB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACxE,eAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;IACH,CAAC;IAED,eAAM,CAAC,OAAO,CAAC,aAAa,QAAQ,CAAC,MAAM,8BAA8B,SAAS,kBAAkB,CAAC,CAAC;IAEtG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAC3B,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,OAAe,EACf,SAAiB,EACjB,OAAoB,EACpB,kBAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAyD,CAAC;IACjF,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEjC,2DAA2D;IAC3D,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,+BAA+B,CAAC,CAAC,CAAC,8BAA8B,CAAC;IAE7G,MAAM,aAAa,GAAG,CAAC,oBAAoB,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC;IACxF,MAAM,YAAY,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAElF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE;QAChC,GAAG,EAAE,OAAO;QACZ,MAAM,EAAE,CAAC,GAAG,aAAa,EAAE,GAAG,YAAY,CAAC;QAC3C,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;IAEH,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAO,OAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACnD,KAAK,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACpD,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1B,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9D,CAAC;YACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;YACpC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACnC,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,IAAI,WAAW,CAAC;QACvD,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,18 @@
1
+ declare const HOOK_TYPES: readonly ["pre-commit", "pre-push"];
2
+ type HookType = typeof HOOK_TYPES[number];
3
+ interface InstallHookOptions {
4
+ type?: HookType;
5
+ force?: boolean;
6
+ }
7
+ /**
8
+ * Install a Git hook that runs envguard before commits or pushes
9
+ */
10
+ export declare function installHookCommand(options?: InstallHookOptions): Promise<void>;
11
+ /**
12
+ * Uninstall a Git hook
13
+ */
14
+ export declare function uninstallHookCommand(options?: {
15
+ type?: HookType;
16
+ }): Promise<void>;
17
+ export {};
18
+ //# sourceMappingURL=install-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-hook.d.ts","sourceRoot":"","sources":["../../src/commands/install-hook.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,UAAU,qCAAsC,CAAC;AACvD,KAAK,QAAQ,GAAG,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAE1C,UAAU,kBAAkB;IAC1B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,kBAAuB,iBA8CxE;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;CAAO,iBA6B3E"}
@@ -0,0 +1,148 @@
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.installHookCommand = installHookCommand;
37
+ exports.uninstallHookCommand = uninstallHookCommand;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const logger_1 = require("../utils/logger");
41
+ const HOOK_TYPES = ['pre-commit', 'pre-push'];
42
+ /**
43
+ * Install a Git hook that runs envguard before commits or pushes
44
+ */
45
+ async function installHookCommand(options = {}) {
46
+ const rootDir = process.cwd();
47
+ const gitDir = path.join(rootDir, '.git');
48
+ const hooksDir = path.join(gitDir, 'hooks');
49
+ // Check if this is a git repository
50
+ if (!fs.existsSync(gitDir)) {
51
+ logger_1.Logger.error('Not a git repository. Please run this command in a git repository.');
52
+ logger_1.Logger.blank();
53
+ process.exit(1);
54
+ }
55
+ // Ensure hooks directory exists
56
+ if (!fs.existsSync(hooksDir)) {
57
+ fs.mkdirSync(hooksDir, { recursive: true });
58
+ logger_1.Logger.success('Created .git/hooks directory');
59
+ }
60
+ const hookType = options.type || 'pre-commit';
61
+ const hookPath = path.join(hooksDir, hookType);
62
+ // Check if hook already exists
63
+ if (fs.existsSync(hookPath) && !options.force) {
64
+ logger_1.Logger.warning(`${hookType} hook already exists.`);
65
+ logger_1.Logger.info('Use --force to overwrite the existing hook', true);
66
+ logger_1.Logger.blank();
67
+ process.exit(1);
68
+ }
69
+ // Create the hook script
70
+ const hookContent = generateHookScript(hookType);
71
+ try {
72
+ fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
73
+ logger_1.Logger.success(`Installed ${hookType} hook successfully!`);
74
+ logger_1.Logger.blank();
75
+ logger_1.Logger.info('The hook will run `envguard check` automatically before each ' +
76
+ (hookType === 'pre-commit' ? 'commit' : 'push'), true);
77
+ logger_1.Logger.info('To bypass the hook, use: git ' +
78
+ (hookType === 'pre-commit' ? 'commit' : 'push') + ' --no-verify', true);
79
+ logger_1.Logger.blank();
80
+ }
81
+ catch (error) {
82
+ logger_1.Logger.error(`Failed to install hook: ${error}`);
83
+ logger_1.Logger.blank();
84
+ process.exit(1);
85
+ }
86
+ }
87
+ /**
88
+ * Uninstall a Git hook
89
+ */
90
+ async function uninstallHookCommand(options = {}) {
91
+ const rootDir = process.cwd();
92
+ const hookType = options.type || 'pre-commit';
93
+ const hookPath = path.join(rootDir, '.git', 'hooks', hookType);
94
+ if (!fs.existsSync(hookPath)) {
95
+ logger_1.Logger.warning(`No ${hookType} hook found.`);
96
+ logger_1.Logger.blank();
97
+ return;
98
+ }
99
+ // Check if it's our hook
100
+ const hookContent = fs.readFileSync(hookPath, 'utf-8');
101
+ if (!hookContent.includes('envguard check')) {
102
+ logger_1.Logger.warning(`The ${hookType} hook exists but was not created by envguard.`);
103
+ logger_1.Logger.info('Manual removal required if you want to delete it.', true);
104
+ logger_1.Logger.blank();
105
+ return;
106
+ }
107
+ try {
108
+ fs.unlinkSync(hookPath);
109
+ logger_1.Logger.success(`Removed ${hookType} hook successfully!`);
110
+ logger_1.Logger.blank();
111
+ }
112
+ catch (error) {
113
+ logger_1.Logger.error(`Failed to remove hook: ${error}`);
114
+ logger_1.Logger.blank();
115
+ process.exit(1);
116
+ }
117
+ }
118
+ /**
119
+ * Generate the hook script content
120
+ */
121
+ function generateHookScript(hookType) {
122
+ const hookMessage = hookType === 'pre-commit' ? 'commit' : 'push';
123
+ return `#!/bin/sh
124
+ # EnvGuard ${hookType} hook
125
+ # This hook runs envguard to check environment variables before ${hookMessage}
126
+ # To bypass this hook, use: git ${hookMessage} --no-verify
127
+
128
+ echo "Running EnvGuard environment variable check..."
129
+
130
+ # Run envguard check
131
+ npx envguard check
132
+
133
+ # Capture the exit code
134
+ EXIT_CODE=$?
135
+
136
+ if [ $EXIT_CODE -ne 0 ]; then
137
+ echo ""
138
+ echo "❌ EnvGuard check failed. Please fix the issues above before ${hookMessage}ing."
139
+ echo " Or run: git ${hookMessage} --no-verify to bypass this check."
140
+ echo ""
141
+ exit 1
142
+ fi
143
+
144
+ echo "✓ EnvGuard check passed!"
145
+ exit 0
146
+ `;
147
+ }
148
+ //# sourceMappingURL=install-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-hook.js","sourceRoot":"","sources":["../../src/commands/install-hook.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ import { Issue } from '../types';
2
2
  export declare function scanCommand(options: {
3
3
  ci?: boolean;
4
4
  strict?: boolean;
5
+ detectFallbacks?: boolean;
5
6
  }): Promise<{
6
7
  success: boolean;
7
8
  issues: Issue[];
@@ -1 +1 @@
1
- {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAKjC,wBAAsB,WAAW,CAAC,OAAO,EAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE;;;GAmO5E"}
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAKjC,wBAAsB,WAAW,CAAC,OAAO,EAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE;;;GAgVvG"}
@@ -49,9 +49,17 @@ async function scanCommand(options) {
49
49
  const config = configLoader_1.ConfigLoader.loadConfig(rootDir);
50
50
  // CLI options override config file
51
51
  const strictMode = options.strict !== undefined ? options.strict : config.strict;
52
+ const detectFallbacks = options.detectFallbacks !== undefined ? options.detectFallbacks : (config.detectFallbacks !== undefined ? config.detectFallbacks : true);
52
53
  logger_1.Logger.startSpinner('Scanning codebase for environment variables...');
53
54
  // Step 1: Find all .env files and serverless.yml files
54
- const scanner = new codeScanner_1.CodeScanner(rootDir);
55
+ const excludePatterns = [
56
+ 'node_modules',
57
+ 'dist',
58
+ 'build',
59
+ '.git',
60
+ ...(config.exclude || []),
61
+ ];
62
+ const scanner = new codeScanner_1.CodeScanner(rootDir, excludePatterns);
55
63
  const envFiles = await scanner.findEnvFiles();
56
64
  const serverlessFiles = await scanner.findServerlessFiles();
57
65
  logger_1.Logger.stopSpinner();
@@ -76,7 +84,7 @@ async function scanCommand(options) {
76
84
  const serverlessVars = serverlessParser.parse(serverlessFilePath);
77
85
  logger_1.Logger.info(`Found ${serverlessVars.size} variable(s) in serverless.yml`, true);
78
86
  // Scan code files in this directory to see what's actually used
79
- const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner);
87
+ const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner, config.exclude);
80
88
  logger_1.Logger.info(`Found ${usedVars.size} variable(s) used in code`, true);
81
89
  logger_1.Logger.blank();
82
90
  // Check for unused variables in serverless.yml
@@ -93,7 +101,7 @@ async function scanCommand(options) {
93
101
  // Check for variables used in code but not defined in serverless.yml
94
102
  const missingFromServerless = [];
95
103
  const skippedRuntimeVars = [];
96
- for (const [varName, locations] of usedVars.entries()) {
104
+ for (const [varName, usage] of usedVars.entries()) {
97
105
  if (!serverlessVars.has(varName)) {
98
106
  // In non-strict mode, skip known runtime variables and custom ignore vars
99
107
  const isCustomIgnored = configLoader_1.ConfigLoader.shouldIgnoreVar(varName, config);
@@ -105,16 +113,17 @@ async function scanCommand(options) {
105
113
  }
106
114
  }
107
115
  else {
108
- missingFromServerless.push({ varName, locations });
116
+ missingFromServerless.push({ varName, locations: usage.locations, hasFallback: usage.hasFallback });
109
117
  }
110
118
  }
111
119
  }
112
120
  if (unusedServerlessVars.length > 0) {
113
- logger_1.Logger.warning('Unused variables in serverless.yml:', true);
121
+ logger_1.Logger.info('Unused variables in serverless.yml:', true);
114
122
  unusedServerlessVars.forEach((varName) => {
115
- logger_1.Logger.warningItem(varName, 2);
123
+ logger_1.Logger.infoItem(varName, 2);
116
124
  allIssues.push({
117
125
  type: 'unused',
126
+ severity: 'info',
118
127
  varName,
119
128
  details: `Defined in serverless.yml but never used in code`,
120
129
  });
@@ -122,20 +131,46 @@ async function scanCommand(options) {
122
131
  logger_1.Logger.blank();
123
132
  }
124
133
  if (missingFromServerless.length > 0) {
125
- logger_1.Logger.error('Missing from serverless.yml:', true);
126
- missingFromServerless.forEach((item) => {
127
- logger_1.Logger.errorItem(item.varName, 2);
128
- if (item.locations && item.locations.length > 0) {
129
- logger_1.Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
130
- }
131
- allIssues.push({
132
- type: 'missing',
133
- varName: item.varName,
134
- details: `Used in code but not defined in serverless.yml`,
135
- locations: item.locations,
134
+ // Group by severity (respect detectFallbacks config)
135
+ const errors = missingFromServerless.filter(item => !detectFallbacks || !item.hasFallback);
136
+ const warnings = missingFromServerless.filter(item => detectFallbacks && item.hasFallback);
137
+ if (errors.length > 0) {
138
+ logger_1.Logger.error('Missing from serverless.yml:', true);
139
+ errors.forEach((item) => {
140
+ logger_1.Logger.errorItem(item.varName, 2);
141
+ if (item.locations && item.locations.length > 0) {
142
+ logger_1.Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
143
+ }
144
+ const details = (detectFallbacks && item.hasFallback)
145
+ ? `Used in code with fallback but not defined in serverless.yml`
146
+ : `Used in code but not defined in serverless.yml`;
147
+ allIssues.push({
148
+ type: 'missing',
149
+ severity: 'error',
150
+ varName: item.varName,
151
+ details,
152
+ locations: item.locations,
153
+ });
136
154
  });
137
- });
138
- logger_1.Logger.blank();
155
+ logger_1.Logger.blank();
156
+ }
157
+ if (warnings.length > 0) {
158
+ logger_1.Logger.warning('Missing from serverless.yml (with fallback):', true);
159
+ warnings.forEach((item) => {
160
+ logger_1.Logger.warningItem(item.varName, 2);
161
+ if (item.locations && item.locations.length > 0) {
162
+ logger_1.Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
163
+ }
164
+ allIssues.push({
165
+ type: 'missing',
166
+ severity: 'warning',
167
+ varName: item.varName,
168
+ details: `Used in code with fallback but not defined in serverless.yml`,
169
+ locations: item.locations,
170
+ });
171
+ });
172
+ logger_1.Logger.blank();
173
+ }
139
174
  }
140
175
  if (unusedServerlessVars.length === 0 && missingFromServerless.length === 0) {
141
176
  logger_1.Logger.success('No issues in this serverless.yml', true);
@@ -166,7 +201,24 @@ async function scanCommand(options) {
166
201
  logger_1.Logger.path(`Checking ${displayPath}/`);
167
202
  logger_1.Logger.blank();
168
203
  // Step 3: Scan code files in this directory and subdirectories
169
- const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
204
+ const allUsedVars = await scanDirectoryForVars(rootDir, envDir, scanner, config.exclude);
205
+ // Filter out ignored variables based on config
206
+ const usedVars = new Map();
207
+ const skippedVarsInScope = [];
208
+ for (const [varName, usage] of allUsedVars.entries()) {
209
+ const isCustomIgnored = configLoader_1.ConfigLoader.shouldIgnoreVar(varName, config);
210
+ const isRuntimeVar = (0, knownEnvVars_1.isKnownRuntimeVar)(varName);
211
+ // In non-strict mode, skip known runtime variables and custom ignore vars
212
+ if (strictMode || (!isRuntimeVar && !isCustomIgnored)) {
213
+ usedVars.set(varName, usage);
214
+ }
215
+ else {
216
+ const category = isCustomIgnored ? 'Custom (from config)' : (0, knownEnvVars_1.getRuntimeVarCategory)(varName);
217
+ if (category) {
218
+ skippedVarsInScope.push({ varName, category });
219
+ }
220
+ }
221
+ }
170
222
  logger_1.Logger.info(`Found ${usedVars.size} variable(s) used in this scope`, true);
171
223
  // Step 4: Parse .env file
172
224
  const definedVars = parser.parse(envFilePath);
@@ -177,15 +229,17 @@ async function scanCommand(options) {
177
229
  logger_1.Logger.info(`Found ${exampleVars.size} variable(s) in .env.example`, true);
178
230
  logger_1.Logger.blank();
179
231
  // Step 6: Analyze and find issues
180
- const result = analyzer.analyze(usedVars, definedVars, exampleVars);
232
+ const result = analyzer.analyze(usedVars, definedVars, exampleVars, detectFallbacks);
181
233
  if (result.issues.length > 0) {
182
- // Group issues by type
183
- const missingIssues = result.issues.filter(i => i.type === 'missing');
234
+ // Group issues by type and severity
235
+ const missingErrors = result.issues.filter(i => i.type === 'missing' && i.severity === 'error');
236
+ const missingWarnings = result.issues.filter(i => i.type === 'missing' && i.severity === 'warning');
184
237
  const unusedIssues = result.issues.filter(i => i.type === 'unused');
185
- const undocumentedIssues = result.issues.filter(i => i.type === 'undocumented');
186
- if (missingIssues.length > 0) {
238
+ const undocumentedWarnings = result.issues.filter(i => i.type === 'undocumented' && i.severity === 'warning');
239
+ const undocumentedInfo = result.issues.filter(i => i.type === 'undocumented' && i.severity === 'info');
240
+ if (missingErrors.length > 0) {
187
241
  logger_1.Logger.error('Missing from .env:', true);
188
- missingIssues.forEach((issue) => {
242
+ missingErrors.forEach((issue) => {
189
243
  logger_1.Logger.errorItem(issue.varName, 2);
190
244
  if (issue.locations && issue.locations.length > 0) {
191
245
  logger_1.Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
@@ -193,16 +247,33 @@ async function scanCommand(options) {
193
247
  });
194
248
  logger_1.Logger.blank();
195
249
  }
250
+ if (missingWarnings.length > 0) {
251
+ logger_1.Logger.warning('Missing from .env (with fallback):', true);
252
+ missingWarnings.forEach((issue) => {
253
+ logger_1.Logger.warningItem(issue.varName, 2);
254
+ if (issue.locations && issue.locations.length > 0) {
255
+ logger_1.Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
256
+ }
257
+ });
258
+ logger_1.Logger.blank();
259
+ }
196
260
  if (unusedIssues.length > 0) {
197
- logger_1.Logger.warning('Unused variables:', true);
261
+ logger_1.Logger.info('Unused variables:', true);
198
262
  unusedIssues.forEach((issue) => {
263
+ logger_1.Logger.infoItem(issue.varName, 2);
264
+ });
265
+ logger_1.Logger.blank();
266
+ }
267
+ if (undocumentedWarnings.length > 0) {
268
+ logger_1.Logger.warning('Missing from .env.example:', true);
269
+ undocumentedWarnings.forEach((issue) => {
199
270
  logger_1.Logger.warningItem(issue.varName, 2);
200
271
  });
201
272
  logger_1.Logger.blank();
202
273
  }
203
- if (undocumentedIssues.length > 0) {
204
- logger_1.Logger.info('Missing from .env.example:', true);
205
- undocumentedIssues.forEach((issue) => {
274
+ if (undocumentedInfo.length > 0) {
275
+ logger_1.Logger.info('Missing from .env.example (with fallback):', true);
276
+ undocumentedInfo.forEach((issue) => {
206
277
  logger_1.Logger.infoItem(issue.varName, 2);
207
278
  });
208
279
  logger_1.Logger.blank();
@@ -213,6 +284,22 @@ async function scanCommand(options) {
213
284
  logger_1.Logger.success('No issues in this directory', true);
214
285
  logger_1.Logger.blank();
215
286
  }
287
+ // Show skipped variables in non-strict mode
288
+ if (!strictMode && skippedVarsInScope.length > 0) {
289
+ logger_1.Logger.info('Skipped known runtime/ignored variables (use --strict to show):', true);
290
+ // Group by category
291
+ const grouped = new Map();
292
+ for (const { varName, category } of skippedVarsInScope) {
293
+ if (!grouped.has(category)) {
294
+ grouped.set(category, []);
295
+ }
296
+ grouped.get(category).push(varName);
297
+ }
298
+ for (const [category, vars] of grouped.entries()) {
299
+ logger_1.Logger.info(`${category}: ${vars.join(', ')}`, true);
300
+ }
301
+ logger_1.Logger.blank();
302
+ }
216
303
  }
217
304
  // Display summary
218
305
  logger_1.Logger.divider();
@@ -220,8 +307,22 @@ async function scanCommand(options) {
220
307
  logger_1.Logger.summary('No issues found! All environment variables are in sync.');
221
308
  return { success: true, issues: [] };
222
309
  }
310
+ // Count issues by severity
311
+ const errorCount = allIssues.filter(i => i.severity === 'error').length;
312
+ const warningCount = allIssues.filter(i => i.severity === 'warning').length;
313
+ const infoCount = allIssues.filter(i => i.severity === 'info').length;
314
+ logger_1.Logger.blank();
315
+ if (errorCount > 0) {
316
+ logger_1.Logger.error(`Errors: ${errorCount}`, false);
317
+ }
318
+ if (warningCount > 0) {
319
+ logger_1.Logger.warning(`Warnings: ${warningCount}`, false);
320
+ }
321
+ if (infoCount > 0) {
322
+ logger_1.Logger.info(`Info: ${infoCount}`, false);
323
+ }
223
324
  logger_1.Logger.blank();
224
- logger_1.Logger.warning(`Total: ${allIssues.length} issue(s) across ${envFiles.length} location(s)`);
325
+ logger_1.Logger.warning(`Total: ${allIssues.length} issue(s) across ${envFiles.length + serverlessFiles.length} location(s)`);
225
326
  logger_1.Logger.blank();
226
327
  // Suggest fix
227
328
  if (!options.ci) {
@@ -236,47 +337,55 @@ async function scanCommand(options) {
236
337
  }
237
338
  return { success: false, issues: allIssues };
238
339
  }
239
- async function scanDirectoryForVars(rootDir, targetDir, scanner) {
340
+ async function scanDirectoryForVars(rootDir, targetDir, scanner, excludePatterns = []) {
240
341
  const envVars = new Map();
241
342
  // Find all code files in this directory and subdirectories
242
343
  const relativeDir = path.relative(rootDir, targetDir);
243
344
  const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
345
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'];
346
+ const customIgnore = excludePatterns.map(p => p.includes('*') ? p : `**/${p}/**`);
244
347
  const files = await (0, glob_1.glob)(pattern, {
245
348
  cwd: rootDir,
246
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
349
+ ignore: [...defaultIgnore, ...customIgnore],
247
350
  absolute: true,
248
351
  });
249
352
  for (const file of files) {
250
353
  const vars = await scanner.scanFile(file);
251
- for (const varName of vars) {
354
+ for (const [varName, hasFallback] of vars.entries()) {
252
355
  const relativePath = path.relative(rootDir, file);
253
356
  if (!envVars.has(varName)) {
254
- envVars.set(varName, []);
357
+ envVars.set(varName, { locations: [], hasFallback: false });
255
358
  }
256
- envVars.get(varName).push(relativePath);
359
+ const entry = envVars.get(varName);
360
+ entry.locations.push(relativePath);
361
+ entry.hasFallback = entry.hasFallback || hasFallback;
257
362
  }
258
363
  }
259
364
  return envVars;
260
365
  }
261
366
  // Scan only code files (JS/TS), not including serverless.yml as a source
262
- async function scanDirectoryForCodeVars(rootDir, targetDir, scanner) {
367
+ async function scanDirectoryForCodeVars(rootDir, targetDir, scanner, excludePatterns = []) {
263
368
  const envVars = new Map();
264
369
  // Find all code files in this directory only (not subdirectories for serverless)
265
370
  const relativeDir = path.relative(rootDir, targetDir);
266
371
  const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
372
+ const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'];
373
+ const customIgnore = excludePatterns.map(p => p.includes('*') ? p : `**/${p}/**`);
267
374
  const files = await (0, glob_1.glob)(pattern, {
268
375
  cwd: rootDir,
269
- ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
376
+ ignore: [...defaultIgnore, ...customIgnore],
270
377
  absolute: true,
271
378
  });
272
379
  for (const file of files) {
273
380
  const vars = await scanner.scanFile(file);
274
- for (const varName of vars) {
381
+ for (const [varName, hasFallback] of vars.entries()) {
275
382
  const relativePath = path.relative(rootDir, file);
276
383
  if (!envVars.has(varName)) {
277
- envVars.set(varName, []);
384
+ envVars.set(varName, { locations: [], hasFallback: false });
278
385
  }
279
- envVars.get(varName).push(relativePath);
386
+ const entry = envVars.get(varName);
387
+ entry.locations.push(relativePath);
388
+ entry.hasFallback = entry.hasFallback || hasFallback;
280
389
  }
281
390
  }
282
391
  return envVars;