@danielszlaski/envguard 0.1.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.
Files changed (75) hide show
  1. package/.envguardrc.example.json +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +320 -0
  4. package/dist/analyzer/envAnalyzer.d.ts +8 -0
  5. package/dist/analyzer/envAnalyzer.d.ts.map +1 -0
  6. package/dist/analyzer/envAnalyzer.js +112 -0
  7. package/dist/analyzer/envAnalyzer.js.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +52 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands/check.d.ts +5 -0
  13. package/dist/commands/check.d.ts.map +1 -0
  14. package/dist/commands/check.js +9 -0
  15. package/dist/commands/check.js.map +1 -0
  16. package/dist/commands/fix.d.ts +4 -0
  17. package/dist/commands/fix.d.ts.map +1 -0
  18. package/dist/commands/fix.js +115 -0
  19. package/dist/commands/fix.js.map +1 -0
  20. package/dist/commands/scan.d.ts +9 -0
  21. package/dist/commands/scan.d.ts.map +1 -0
  22. package/dist/commands/scan.js +274 -0
  23. package/dist/commands/scan.js.map +1 -0
  24. package/dist/config/configLoader.d.ts +35 -0
  25. package/dist/config/configLoader.d.ts.map +1 -0
  26. package/dist/config/configLoader.js +141 -0
  27. package/dist/config/configLoader.js.map +1 -0
  28. package/dist/constants/knownEnvVars.d.ts +38 -0
  29. package/dist/constants/knownEnvVars.d.ts.map +1 -0
  30. package/dist/constants/knownEnvVars.js +111 -0
  31. package/dist/constants/knownEnvVars.js.map +1 -0
  32. package/dist/index.d.ts +5 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +25 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/parser/envParser.d.ts +13 -0
  37. package/dist/parser/envParser.d.ts.map +1 -0
  38. package/dist/parser/envParser.js +126 -0
  39. package/dist/parser/envParser.js.map +1 -0
  40. package/dist/parser/serverlessParser.d.ts +27 -0
  41. package/dist/parser/serverlessParser.d.ts.map +1 -0
  42. package/dist/parser/serverlessParser.js +162 -0
  43. package/dist/parser/serverlessParser.js.map +1 -0
  44. package/dist/scanner/codeScanner.d.ts +13 -0
  45. package/dist/scanner/codeScanner.d.ts.map +1 -0
  46. package/dist/scanner/codeScanner.js +157 -0
  47. package/dist/scanner/codeScanner.js.map +1 -0
  48. package/dist/types.d.ts +24 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +3 -0
  51. package/dist/types.js.map +1 -0
  52. package/package.json +40 -0
  53. package/src/analyzer/envAnalyzer.ts +133 -0
  54. package/src/cli.ts +54 -0
  55. package/src/commands/check.ts +6 -0
  56. package/src/commands/fix.ts +104 -0
  57. package/src/commands/scan.ts +289 -0
  58. package/src/config/configLoader.ts +131 -0
  59. package/src/constants/knownEnvVars.ts +108 -0
  60. package/src/index.ts +4 -0
  61. package/src/parser/envParser.ts +114 -0
  62. package/src/parser/serverlessParser.ts +146 -0
  63. package/src/scanner/codeScanner.ts +148 -0
  64. package/src/types.ts +26 -0
  65. package/test-project/.envguardrc.json +7 -0
  66. package/test-project/src/lambda1/.env.example +11 -0
  67. package/test-project/src/lambda1/handler.js +14 -0
  68. package/test-project/src/lambda2/.env.example +9 -0
  69. package/test-project/src/lambda2/handler.js +11 -0
  70. package/test-project/src/lambda2/handler2.js +13 -0
  71. package/test-project/src/lambda2/serverless.yml +50 -0
  72. package/test-project/src/payment/.env.example +23 -0
  73. package/test-project/src/payment/payment.js +14 -0
  74. package/test-project/src/payment/server.ts +11 -0
  75. package/tsconfig.json +19 -0
@@ -0,0 +1,157 @@
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.CodeScanner = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const glob_1 = require("glob");
40
+ const serverlessParser_1 = require("../parser/serverlessParser");
41
+ class CodeScanner {
42
+ constructor(rootDir, excludePatterns = ['node_modules', 'dist', 'build', '.git']) {
43
+ this.rootDir = rootDir;
44
+ this.excludePatterns = excludePatterns;
45
+ this.serverlessParser = new serverlessParser_1.ServerlessParser();
46
+ }
47
+ async scan() {
48
+ const envVars = new Map();
49
+ // Scan for JavaScript/TypeScript files
50
+ const files = await (0, glob_1.glob)('**/*.{js,ts,jsx,tsx,mjs,cjs}', {
51
+ cwd: this.rootDir,
52
+ ignore: this.excludePatterns.map(p => `**/${p}/**`),
53
+ absolute: true,
54
+ });
55
+ for (const file of files) {
56
+ const vars = await this.scanFile(file);
57
+ for (const varName of vars) {
58
+ const relativePath = path.relative(this.rootDir, file);
59
+ if (!envVars.has(varName)) {
60
+ envVars.set(varName, []);
61
+ }
62
+ envVars.get(varName).push(relativePath);
63
+ }
64
+ }
65
+ // Also scan serverless.yml files for environment variable definitions
66
+ const serverlessFiles = await this.findServerlessFiles();
67
+ for (const file of serverlessFiles) {
68
+ const vars = this.scanServerlessFile(file);
69
+ for (const [varName, entry] of vars.entries()) {
70
+ const relativePath = path.relative(this.rootDir, file);
71
+ if (!envVars.has(varName)) {
72
+ envVars.set(varName, []);
73
+ }
74
+ envVars.get(varName).push(`${relativePath} (serverless config)`);
75
+ }
76
+ }
77
+ return envVars;
78
+ }
79
+ async scanFile(filePath) {
80
+ const envVars = new Set();
81
+ try {
82
+ const content = fs.readFileSync(filePath, 'utf-8');
83
+ // Pattern 1: process.env.VAR_NAME
84
+ const processEnvPattern = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
85
+ let match;
86
+ while ((match = processEnvPattern.exec(content)) !== null) {
87
+ envVars.add(match[1]);
88
+ }
89
+ // Pattern 2: process.env['VAR_NAME'] or process.env["VAR_NAME"]
90
+ const processEnvBracketPattern = /process\.env\[['"]([A-Z_][A-Z0-9_]*)['"\]]/g;
91
+ while ((match = processEnvBracketPattern.exec(content)) !== null) {
92
+ envVars.add(match[1]);
93
+ }
94
+ // Pattern 3: Destructuring - const { VAR_NAME } = process.env
95
+ const destructuringPattern = /const\s+\{\s*([^}]+)\s*\}\s*=\s*process\.env/g;
96
+ while ((match = destructuringPattern.exec(content)) !== null) {
97
+ const vars = match[1].split(',').map(v => v.trim().split(':')[0].trim());
98
+ vars.forEach(v => {
99
+ if (/^[A-Z_][A-Z0-9_]*$/.test(v)) {
100
+ envVars.add(v);
101
+ }
102
+ });
103
+ }
104
+ }
105
+ catch (error) {
106
+ console.error(`Error scanning file ${filePath}:`, error);
107
+ }
108
+ return envVars;
109
+ }
110
+ async findEnvFiles() {
111
+ const envFiles = await (0, glob_1.glob)('**/.env', {
112
+ cwd: this.rootDir,
113
+ ignore: this.excludePatterns.map(p => `**/${p}/**`),
114
+ absolute: true,
115
+ });
116
+ return envFiles;
117
+ }
118
+ async findServerlessFiles() {
119
+ const serverlessFiles = await (0, glob_1.glob)('**/serverless.{yml,yaml}', {
120
+ cwd: this.rootDir,
121
+ ignore: this.excludePatterns.map(p => `**/${p}/**`),
122
+ absolute: true,
123
+ });
124
+ return serverlessFiles;
125
+ }
126
+ scanServerlessFile(filePath) {
127
+ return this.serverlessParser.parse(filePath);
128
+ }
129
+ async scanByDirectory() {
130
+ // Returns a map of directory -> (varName -> file locations)
131
+ const dirMap = new Map();
132
+ // Scan for JavaScript/TypeScript files
133
+ const files = await (0, glob_1.glob)('**/*.{js,ts,jsx,tsx,mjs,cjs}', {
134
+ cwd: this.rootDir,
135
+ ignore: this.excludePatterns.map(p => `**/${p}/**`),
136
+ absolute: true,
137
+ });
138
+ for (const file of files) {
139
+ const vars = await this.scanFile(file);
140
+ const fileDir = path.dirname(file);
141
+ const relativePath = path.relative(this.rootDir, file);
142
+ for (const varName of vars) {
143
+ if (!dirMap.has(fileDir)) {
144
+ dirMap.set(fileDir, new Map());
145
+ }
146
+ const varMap = dirMap.get(fileDir);
147
+ if (!varMap.has(varName)) {
148
+ varMap.set(varName, []);
149
+ }
150
+ varMap.get(varName).push(relativePath);
151
+ }
152
+ }
153
+ return dirMap;
154
+ }
155
+ }
156
+ exports.CodeScanner = CodeScanner;
157
+ //# sourceMappingURL=codeScanner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codeScanner.js","sourceRoot":"","sources":["../../src/scanner/codeScanner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ export interface EnvUsage {
2
+ varName: string;
3
+ locations: string[];
4
+ }
5
+ export interface EnvDefinition {
6
+ varName: string;
7
+ value?: string;
8
+ comment?: string;
9
+ source?: 'dotenv' | 'serverless' | 'both';
10
+ isReference?: boolean;
11
+ }
12
+ export interface Issue {
13
+ type: 'missing' | 'unused' | 'undocumented';
14
+ varName: string;
15
+ details: string;
16
+ locations?: string[];
17
+ }
18
+ export interface ScanResult {
19
+ issues: Issue[];
20
+ usedVars: Map<string, string[]>;
21
+ definedVars: Set<string>;
22
+ exampleVars: Set<string>;
23
+ }
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,MAAM,CAAC;IAC1C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,cAAc,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC1B"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@danielszlaski/envguard",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to keep environment variables in sync with your codebase",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "envguard": "./dist/cli.js"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/szlaskidaniel/envguard"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "start": "node dist/cli.js",
17
+ "test": "echo \"Error: no test specified\" && exit 1"
18
+ },
19
+ "keywords": [
20
+ "environment",
21
+ "env",
22
+ "dotenv",
23
+ "config",
24
+ "security"
25
+ ],
26
+ "author": "Daniel Szlaski",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "chalk": "^4.1.2",
30
+ "commander": "^11.1.0",
31
+ "dotenv": "^16.3.1",
32
+ "glob": "^10.3.10",
33
+ "js-yaml": "^4.1.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/js-yaml": "^4.0.9",
37
+ "@types/node": "^20.10.6",
38
+ "typescript": "^5.3.3"
39
+ }
40
+ }
@@ -0,0 +1,133 @@
1
+ import { Issue, ScanResult } from '../types';
2
+ import { EnvEntry } from '../parser/envParser';
3
+
4
+ export class EnvAnalyzer {
5
+ analyze(
6
+ usedVars: Map<string, string[]>,
7
+ definedVars: Map<string, EnvEntry>,
8
+ exampleVars: Set<string>
9
+ ): ScanResult {
10
+ const issues: Issue[] = [];
11
+
12
+ // Issue 1: Variables used in code but missing from .env
13
+ for (const [varName, locations] of usedVars.entries()) {
14
+ if (!definedVars.has(varName)) {
15
+ issues.push({
16
+ type: 'missing',
17
+ varName,
18
+ details: `Used in code but not defined in .env`,
19
+ locations,
20
+ });
21
+ }
22
+ }
23
+
24
+ // Issue 2: Variables defined in .env but never used
25
+ for (const [varName] of definedVars.entries()) {
26
+ if (!usedVars.has(varName)) {
27
+ issues.push({
28
+ type: 'unused',
29
+ varName,
30
+ details: `Defined in .env but never used in code`,
31
+ });
32
+ }
33
+ }
34
+
35
+ // Issue 3: Variables used in code but not in .env.example
36
+ for (const [varName, locations] of usedVars.entries()) {
37
+ if (!exampleVars.has(varName)) {
38
+ issues.push({
39
+ type: 'undocumented',
40
+ varName,
41
+ details: `Used in code but missing from .env.example`,
42
+ locations,
43
+ });
44
+ }
45
+ }
46
+
47
+ return {
48
+ issues,
49
+ usedVars,
50
+ definedVars: new Set(definedVars.keys()),
51
+ exampleVars,
52
+ };
53
+ }
54
+
55
+ generateExampleContent(
56
+ usedVars: Map<string, string[]>,
57
+ existingEntries: Map<string, EnvEntry>
58
+ ): string {
59
+ let content = '# Auto-generated by envguard\n';
60
+ content += '# Do not put actual secrets in this file - use .env instead\n\n';
61
+
62
+ const sortedVars = Array.from(usedVars.keys()).sort();
63
+
64
+ for (const varName of sortedVars) {
65
+ const locations = usedVars.get(varName)!;
66
+ const existingEntry = existingEntries.get(varName);
67
+
68
+ // Add location comments
69
+ content += `# Used in: ${locations.slice(0, 3).join(', ')}`;
70
+ if (locations.length > 3) {
71
+ content += ` (+${locations.length - 3} more)`;
72
+ }
73
+ content += '\n';
74
+
75
+ // Add existing comment if available
76
+ if (existingEntry?.comment) {
77
+ content += `# ${existingEntry.comment}\n`;
78
+ } else {
79
+ // Add a format hint based on common patterns
80
+ const hint = this.getFormatHint(varName);
81
+ if (hint) {
82
+ content += `# Format: ${hint}\n`;
83
+ }
84
+ }
85
+
86
+ // Add the variable with empty value or existing value
87
+ if (existingEntry) {
88
+ content += `${varName}=${existingEntry.value}\n`;
89
+ } else {
90
+ content += `${varName}=\n`;
91
+ }
92
+
93
+ content += '\n';
94
+ }
95
+
96
+ return content;
97
+ }
98
+
99
+ private getFormatHint(varName: string): string | null {
100
+ const patterns: { [key: string]: string } = {
101
+ 'DATABASE_URL': 'postgresql://user:pass@host:5432/db',
102
+ 'MONGODB_URI': 'mongodb://localhost:27017/dbname',
103
+ 'REDIS_URL': 'redis://localhost:6379',
104
+ 'PORT': '3000',
105
+ 'NODE_ENV': 'development|production|test',
106
+ 'API_KEY': 'your-api-key-here',
107
+ 'SECRET': 'your-secret-here',
108
+ 'JWT_SECRET': 'your-jwt-secret',
109
+ 'STRIPE_': 'sk_test_...',
110
+ 'AWS_': 'aws-credentials',
111
+ };
112
+
113
+ for (const [pattern, hint] of Object.entries(patterns)) {
114
+ if (varName.includes(pattern)) {
115
+ return hint;
116
+ }
117
+ }
118
+
119
+ if (varName.endsWith('_URL') || varName.endsWith('_URI')) {
120
+ return 'https://example.com';
121
+ }
122
+
123
+ if (varName.endsWith('_PORT')) {
124
+ return '8080';
125
+ }
126
+
127
+ if (varName.endsWith('_KEY') || varName.endsWith('_SECRET') || varName.endsWith('_TOKEN')) {
128
+ return 'your-secret-here';
129
+ }
130
+
131
+ return null;
132
+ }
133
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { scanCommand } from './commands/scan';
5
+ import { fixCommand } from './commands/fix';
6
+ import { checkCommand } from './commands/check';
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('envguard')
12
+ .description('Keep your environment variables in sync with your codebase')
13
+ .version('0.1.0');
14
+
15
+ program
16
+ .command('scan')
17
+ .description('Scan codebase and compare with .env files')
18
+ .option('--ci', 'Exit with error code if issues found (for CI/CD)')
19
+ .option('--strict', 'Report all variables including known runtime variables (AWS_REGION, NODE_ENV, etc.)')
20
+ .action(async (options) => {
21
+ try {
22
+ await scanCommand(options);
23
+ } catch (error) {
24
+ console.error('Error:', error);
25
+ process.exit(1);
26
+ }
27
+ });
28
+
29
+ program
30
+ .command('fix')
31
+ .description('Auto-generate .env.example from codebase')
32
+ .action(async () => {
33
+ try {
34
+ await fixCommand();
35
+ } catch (error) {
36
+ console.error('Error:', error);
37
+ process.exit(1);
38
+ }
39
+ });
40
+
41
+ program
42
+ .command('check')
43
+ .description('Check for issues (alias for scan --ci)')
44
+ .option('--strict', 'Report all variables including known runtime variables (AWS_REGION, NODE_ENV, etc.)')
45
+ .action(async (options) => {
46
+ try {
47
+ await scanCommand({ ci: true, strict: options.strict });
48
+ } catch (error) {
49
+ console.error('Error:', error);
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ program.parse();
@@ -0,0 +1,6 @@
1
+ import { scanCommand } from './scan';
2
+
3
+ export async function checkCommand() {
4
+ // Check command is the same as scan but with --ci flag
5
+ return scanCommand({ ci: true });
6
+ }
@@ -0,0 +1,104 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import chalk from 'chalk';
4
+ import { CodeScanner } from '../scanner/codeScanner';
5
+ import { EnvParser } from '../parser/envParser';
6
+ import { EnvAnalyzer } from '../analyzer/envAnalyzer';
7
+
8
+ export async function fixCommand() {
9
+ const rootDir = process.cwd();
10
+
11
+ console.log(chalk.blue('🔧 Generating .env.example files...\n'));
12
+
13
+ // Step 1: Find all .env files
14
+ const scanner = new CodeScanner(rootDir);
15
+ const envFiles = await scanner.findEnvFiles();
16
+
17
+ if (envFiles.length === 0) {
18
+ console.log(chalk.yellow('⚠️ No .env files found in the project\n'));
19
+ return { success: false };
20
+ }
21
+
22
+ console.log(chalk.green(`✓ Found ${envFiles.length} .env file(s)\n`));
23
+
24
+ const parser = new EnvParser();
25
+ const analyzer = new EnvAnalyzer();
26
+ let totalVars = 0;
27
+
28
+ // Step 2: Process each .env file
29
+ for (const envFilePath of envFiles) {
30
+ const envDir = path.dirname(envFilePath);
31
+ const relativePath = path.relative(rootDir, envDir);
32
+ const displayPath = relativePath || '.';
33
+
34
+ console.log(chalk.cyan(`📂 Processing ${displayPath}/`));
35
+
36
+ // Step 3: Scan code files in this directory and subdirectories
37
+ const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
38
+
39
+ if (usedVars.size === 0) {
40
+ console.log(chalk.gray(` No environment variables found in code\n`));
41
+ continue;
42
+ }
43
+
44
+ console.log(chalk.green(` ✓ Found ${usedVars.size} variable(s) used in this directory\n`));
45
+
46
+ // Step 4: Parse existing .env.example to preserve comments
47
+ const examplePath = path.join(envDir, '.env.example');
48
+ const existingEntries = parser.parse(examplePath);
49
+
50
+ // Step 5: Generate new .env.example content
51
+ const newContent = analyzer.generateExampleContent(usedVars, existingEntries);
52
+
53
+ // Step 6: Write to .env.example
54
+ fs.writeFileSync(examplePath, newContent, 'utf-8');
55
+
56
+ console.log(chalk.green(` ✅ Generated ${path.relative(rootDir, examplePath)}\n`));
57
+
58
+ totalVars += usedVars.size;
59
+
60
+ // Step 7: Show summary for this directory
61
+ const definedVars = parser.parse(envFilePath);
62
+ const missingFromEnv = Array.from(usedVars.keys()).filter(v => !definedVars.has(v));
63
+
64
+ if (missingFromEnv.length > 0) {
65
+ console.log(chalk.yellow(` ⚠️ Missing from .env: ${missingFromEnv.join(', ')}\n`));
66
+ }
67
+ }
68
+
69
+ console.log(chalk.green(`🎉 Done! Generated ${envFiles.length} .env.example file(s) with ${totalVars} total variables\n`));
70
+
71
+ return { success: true };
72
+ }
73
+
74
+ async function scanDirectoryForVars(
75
+ rootDir: string,
76
+ targetDir: string,
77
+ scanner: CodeScanner
78
+ ): Promise<Map<string, string[]>> {
79
+ const envVars = new Map<string, string[]>();
80
+ const { glob } = require('glob');
81
+
82
+ // Find all code files in this directory and subdirectories
83
+ const relativeDir = path.relative(rootDir, targetDir);
84
+ const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
85
+
86
+ const files = await glob(pattern, {
87
+ cwd: rootDir,
88
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
89
+ absolute: true,
90
+ });
91
+
92
+ for (const file of files) {
93
+ const vars = await (scanner as any).scanFile(file);
94
+ for (const varName of vars) {
95
+ const relativePath = path.relative(rootDir, file);
96
+ if (!envVars.has(varName)) {
97
+ envVars.set(varName, []);
98
+ }
99
+ envVars.get(varName)!.push(relativePath);
100
+ }
101
+ }
102
+
103
+ return envVars;
104
+ }