@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,289 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import chalk from 'chalk';
4
+ import { glob } from 'glob';
5
+ import { CodeScanner } from '../scanner/codeScanner';
6
+ import { EnvParser, EnvEntry } from '../parser/envParser';
7
+ import { ServerlessParser } from '../parser/serverlessParser';
8
+ import { EnvAnalyzer } from '../analyzer/envAnalyzer';
9
+ import { Issue } from '../types';
10
+ import { isKnownRuntimeVar, getRuntimeVarCategory } from '../constants/knownEnvVars';
11
+ import { ConfigLoader } from '../config/configLoader';
12
+
13
+ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
14
+ const rootDir = process.cwd();
15
+
16
+ // Load configuration
17
+ const config = ConfigLoader.loadConfig(rootDir);
18
+
19
+ // CLI options override config file
20
+ const strictMode = options.strict !== undefined ? options.strict : config.strict;
21
+
22
+ console.log(chalk.blue('🔍 Scanning codebase for environment variables...\n'));
23
+
24
+ // Step 1: Find all .env files and serverless.yml files
25
+ const scanner = new CodeScanner(rootDir);
26
+ const envFiles = await scanner.findEnvFiles();
27
+ const serverlessFiles = await scanner.findServerlessFiles();
28
+
29
+ if (envFiles.length === 0 && serverlessFiles.length === 0) {
30
+ console.log(chalk.yellow('⚠️ No .env or serverless.yml files found in the project\n'));
31
+ return { success: false, issues: [] };
32
+ }
33
+
34
+ console.log(chalk.green(`✓ Found ${envFiles.length} .env file(s) and ${serverlessFiles.length} serverless.yml file(s)\n`));
35
+
36
+ const parser = new EnvParser();
37
+ const serverlessParser = new ServerlessParser();
38
+ const analyzer = new EnvAnalyzer();
39
+ const allIssues: Issue[] = [];
40
+
41
+ // Step 2a: Process serverless.yml files independently
42
+ for (const serverlessFilePath of serverlessFiles) {
43
+ const serverlessDir = path.dirname(serverlessFilePath);
44
+ const relativePath = path.relative(rootDir, serverlessFilePath);
45
+
46
+ console.log(chalk.cyan(`📂 Checking ${relativePath}\n`));
47
+
48
+ // Parse serverless.yml
49
+ const serverlessVars = serverlessParser.parse(serverlessFilePath);
50
+ console.log(chalk.gray(` Found ${serverlessVars.size} variable(s) in serverless.yml`));
51
+
52
+ // Scan code files in this directory to see what's actually used
53
+ const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner);
54
+ console.log(chalk.gray(` Found ${usedVars.size} variable(s) used in code\n`));
55
+
56
+ // Check for unused variables in serverless.yml
57
+ const unusedServerlessVars: string[] = [];
58
+ for (const [varName] of serverlessVars.entries()) {
59
+ if (!usedVars.has(varName)) {
60
+ // In non-strict mode, skip known runtime variables and custom ignore vars from "unused" warnings
61
+ const isIgnored = isKnownRuntimeVar(varName) || ConfigLoader.shouldIgnoreVar(varName, config);
62
+ if (strictMode || !isIgnored) {
63
+ unusedServerlessVars.push(varName);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Check for variables used in code but not defined in serverless.yml
69
+ const missingFromServerless: Array<{ varName: string; locations: string[]; category?: string }> = [];
70
+ const skippedRuntimeVars: Array<{ varName: string; category: string }> = [];
71
+
72
+ for (const [varName, locations] of usedVars.entries()) {
73
+ if (!serverlessVars.has(varName)) {
74
+ // In non-strict mode, skip known runtime variables and custom ignore vars
75
+ const isCustomIgnored = ConfigLoader.shouldIgnoreVar(varName, config);
76
+ const isRuntimeVar = isKnownRuntimeVar(varName);
77
+
78
+ if (!strictMode && (isRuntimeVar || isCustomIgnored)) {
79
+ const category = isCustomIgnored ? 'Custom (from config)' : getRuntimeVarCategory(varName);
80
+ if (category) {
81
+ skippedRuntimeVars.push({ varName, category });
82
+ }
83
+ } else {
84
+ missingFromServerless.push({ varName, locations });
85
+ }
86
+ }
87
+ }
88
+
89
+ if (unusedServerlessVars.length > 0) {
90
+ console.log(chalk.yellow.bold(' ⚠️ Unused variables in serverless.yml:'));
91
+ unusedServerlessVars.forEach((varName, index) => {
92
+ console.log(chalk.yellow(` ${index + 1}. ${varName}`));
93
+ allIssues.push({
94
+ type: 'unused',
95
+ varName,
96
+ details: `Defined in serverless.yml but never used in code`,
97
+ });
98
+ });
99
+ console.log();
100
+ }
101
+
102
+ if (missingFromServerless.length > 0) {
103
+ console.log(chalk.red.bold(' 🚨 Missing from serverless.yml:'));
104
+ missingFromServerless.forEach((item, index) => {
105
+ console.log(chalk.red(` ${index + 1}. ${item.varName}`));
106
+ if (item.locations && item.locations.length > 0) {
107
+ console.log(chalk.gray(` Used in: ${item.locations.slice(0, 2).join(', ')}`));
108
+ }
109
+ allIssues.push({
110
+ type: 'missing',
111
+ varName: item.varName,
112
+ details: `Used in code but not defined in serverless.yml`,
113
+ locations: item.locations,
114
+ });
115
+ });
116
+ console.log();
117
+ }
118
+
119
+ if (unusedServerlessVars.length === 0 && missingFromServerless.length === 0) {
120
+ console.log(chalk.green(' ✅ No issues in this serverless.yml\n'));
121
+ }
122
+
123
+ // Show skipped runtime variables in non-strict mode
124
+ if (!strictMode && skippedRuntimeVars.length > 0) {
125
+ console.log(chalk.gray(' ℹ️ Skipped known runtime variables (use --strict to show):'));
126
+ // Group by category
127
+ const grouped = new Map<string, string[]>();
128
+ for (const { varName, category } of skippedRuntimeVars) {
129
+ if (!grouped.has(category)) {
130
+ grouped.set(category, []);
131
+ }
132
+ grouped.get(category)!.push(varName);
133
+ }
134
+ for (const [category, vars] of grouped.entries()) {
135
+ console.log(chalk.gray(` ${category}: ${vars.join(', ')}`));
136
+ }
137
+ console.log();
138
+ }
139
+ }
140
+
141
+ // Step 2b: Process each .env file (including directories that also have serverless.yml)
142
+ for (const envFilePath of envFiles) {
143
+ const envDir = path.dirname(envFilePath);
144
+ const relativePath = path.relative(rootDir, envDir);
145
+ const displayPath = relativePath || '.';
146
+
147
+ console.log(chalk.cyan(`📂 Checking ${displayPath}/\n`));
148
+
149
+ // Step 3: Scan code files in this directory and subdirectories
150
+ const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
151
+
152
+ console.log(chalk.gray(` Found ${usedVars.size} variable(s) used in this scope`));
153
+
154
+ // Step 4: Parse .env file
155
+ const definedVars = parser.parse(envFilePath);
156
+ console.log(chalk.gray(` Found ${definedVars.size} variable(s) in .env`));
157
+
158
+ // Step 5: Parse .env.example
159
+ const examplePath = path.join(envDir, '.env.example');
160
+ const exampleVars = parser.parseExample(examplePath);
161
+ console.log(chalk.gray(` Found ${exampleVars.size} variable(s) in .env.example\n`));
162
+
163
+ // Step 6: Analyze and find issues
164
+ const result = analyzer.analyze(usedVars, definedVars, exampleVars);
165
+
166
+ if (result.issues.length > 0) {
167
+ // Group issues by type
168
+ const missingIssues = result.issues.filter(i => i.type === 'missing');
169
+ const unusedIssues = result.issues.filter(i => i.type === 'unused');
170
+ const undocumentedIssues = result.issues.filter(i => i.type === 'undocumented');
171
+
172
+ if (missingIssues.length > 0) {
173
+ console.log(chalk.red.bold(' 🚨 Missing from .env:'));
174
+ missingIssues.forEach((issue, index) => {
175
+ console.log(chalk.red(` ${index + 1}. ${issue.varName}`));
176
+ if (issue.locations && issue.locations.length > 0) {
177
+ console.log(chalk.gray(` Used in: ${issue.locations.slice(0, 2).join(', ')}`));
178
+ }
179
+ });
180
+ console.log();
181
+ }
182
+
183
+ if (unusedIssues.length > 0) {
184
+ console.log(chalk.yellow.bold(' ⚠️ Unused variables:'));
185
+ unusedIssues.forEach((issue, index) => {
186
+ console.log(chalk.yellow(` ${index + 1}. ${issue.varName}`));
187
+ });
188
+ console.log();
189
+ }
190
+
191
+ if (undocumentedIssues.length > 0) {
192
+ console.log(chalk.blue.bold(' 📝 Missing from .env.example:'));
193
+ undocumentedIssues.forEach((issue, index) => {
194
+ console.log(chalk.blue(` ${index + 1}. ${issue.varName}`));
195
+ });
196
+ console.log();
197
+ }
198
+
199
+ allIssues.push(...result.issues);
200
+ } else {
201
+ console.log(chalk.green(' ✅ No issues in this directory\n'));
202
+ }
203
+ }
204
+
205
+ // Display summary
206
+ console.log(chalk.bold('─'.repeat(50)));
207
+ if (allIssues.length === 0) {
208
+ console.log(chalk.green('\n✅ No issues found! All environment variables are in sync.\n'));
209
+ return { success: true, issues: [] };
210
+ }
211
+
212
+ console.log(chalk.yellow(`\n⚠️ Total: ${allIssues.length} issue(s) across ${envFiles.length} location(s)\n`));
213
+
214
+ // Suggest fix
215
+ if (!options.ci) {
216
+ console.log(chalk.cyan('💡 Run `envguard fix` to auto-generate .env.example files\n'));
217
+ }
218
+
219
+ // Exit with error code in CI mode
220
+ if (options.ci) {
221
+ console.log(chalk.red('❌ Issues found. Exiting with error code 1.\n'));
222
+ process.exit(1);
223
+ }
224
+
225
+ return { success: false, issues: allIssues };
226
+ }
227
+
228
+ async function scanDirectoryForVars(
229
+ rootDir: string,
230
+ targetDir: string,
231
+ scanner: CodeScanner
232
+ ): Promise<Map<string, string[]>> {
233
+ const envVars = new Map<string, string[]>();
234
+
235
+ // Find all code files in this directory and subdirectories
236
+ const relativeDir = path.relative(rootDir, targetDir);
237
+ const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
238
+
239
+ const files = await glob(pattern, {
240
+ cwd: rootDir,
241
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
242
+ absolute: true,
243
+ });
244
+
245
+ for (const file of files) {
246
+ const vars = await scanner.scanFile(file);
247
+ for (const varName of vars) {
248
+ const relativePath = path.relative(rootDir, file);
249
+ if (!envVars.has(varName)) {
250
+ envVars.set(varName, []);
251
+ }
252
+ envVars.get(varName)!.push(relativePath);
253
+ }
254
+ }
255
+
256
+ return envVars;
257
+ }
258
+
259
+ // Scan only code files (JS/TS), not including serverless.yml as a source
260
+ async function scanDirectoryForCodeVars(
261
+ rootDir: string,
262
+ targetDir: string,
263
+ scanner: CodeScanner
264
+ ): Promise<Map<string, string[]>> {
265
+ const envVars = new Map<string, string[]>();
266
+
267
+ // Find all code files in this directory only (not subdirectories for serverless)
268
+ const relativeDir = path.relative(rootDir, targetDir);
269
+ const pattern = relativeDir ? `${relativeDir}/**/*.{js,ts,jsx,tsx,mjs,cjs}` : '**/*.{js,ts,jsx,tsx,mjs,cjs}';
270
+
271
+ const files = await glob(pattern, {
272
+ cwd: rootDir,
273
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
274
+ absolute: true,
275
+ });
276
+
277
+ for (const file of files) {
278
+ const vars = await scanner.scanFile(file);
279
+ for (const varName of vars) {
280
+ const relativePath = path.relative(rootDir, file);
281
+ if (!envVars.has(varName)) {
282
+ envVars.set(varName, []);
283
+ }
284
+ envVars.get(varName)!.push(relativePath);
285
+ }
286
+ }
287
+
288
+ return envVars;
289
+ }
@@ -0,0 +1,131 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export interface EnvGuardConfig {
5
+ /**
6
+ * Custom environment variables to ignore in non-strict mode
7
+ * These will be treated like AWS_REGION, NODE_ENV, etc.
8
+ */
9
+ ignoreVars?: string[];
10
+
11
+ /**
12
+ * File patterns to exclude from scanning (in addition to defaults)
13
+ */
14
+ exclude?: string[];
15
+
16
+ /**
17
+ * Enable strict mode by default
18
+ */
19
+ strict?: boolean;
20
+ }
21
+
22
+ const DEFAULT_CONFIG: EnvGuardConfig = {
23
+ ignoreVars: [],
24
+ exclude: [],
25
+ strict: false,
26
+ };
27
+
28
+ const CONFIG_FILE_NAMES = [
29
+ '.envguardrc.json',
30
+ '.envguardrc',
31
+ 'envguard.config.json',
32
+ ];
33
+
34
+ /**
35
+ * Load EnvGuard configuration from various sources
36
+ * Priority: CLI args > .envguardrc.json > package.json > defaults
37
+ */
38
+ export class ConfigLoader {
39
+ /**
40
+ * Load config from the project root directory
41
+ */
42
+ static loadConfig(rootDir: string): EnvGuardConfig {
43
+ // Try to find config file
44
+ const configPath = this.findConfigFile(rootDir);
45
+
46
+ if (configPath) {
47
+ try {
48
+ const fileContent = fs.readFileSync(configPath, 'utf-8');
49
+ const userConfig = JSON.parse(fileContent) as EnvGuardConfig;
50
+
51
+ return {
52
+ ...DEFAULT_CONFIG,
53
+ ...userConfig,
54
+ ignoreVars: [
55
+ ...(DEFAULT_CONFIG.ignoreVars || []),
56
+ ...(userConfig.ignoreVars || []),
57
+ ],
58
+ exclude: [
59
+ ...(DEFAULT_CONFIG.exclude || []),
60
+ ...(userConfig.exclude || []),
61
+ ],
62
+ };
63
+ } catch (error) {
64
+ console.warn(`Warning: Failed to parse config file ${configPath}:`, error);
65
+ return DEFAULT_CONFIG;
66
+ }
67
+ }
68
+
69
+ // Try to load from package.json
70
+ const packageJsonPath = path.join(rootDir, 'package.json');
71
+ if (fs.existsSync(packageJsonPath)) {
72
+ try {
73
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
74
+ if (packageJson.envguard) {
75
+ const userConfig = packageJson.envguard as EnvGuardConfig;
76
+ return {
77
+ ...DEFAULT_CONFIG,
78
+ ...userConfig,
79
+ ignoreVars: [
80
+ ...(DEFAULT_CONFIG.ignoreVars || []),
81
+ ...(userConfig.ignoreVars || []),
82
+ ],
83
+ exclude: [
84
+ ...(DEFAULT_CONFIG.exclude || []),
85
+ ...(userConfig.exclude || []),
86
+ ],
87
+ };
88
+ }
89
+ } catch (error) {
90
+ // Silently fail if package.json doesn't have envguard config
91
+ }
92
+ }
93
+
94
+ return DEFAULT_CONFIG;
95
+ }
96
+
97
+ /**
98
+ * Find the config file by searching up the directory tree
99
+ * This allows placing config at repo root and using it in subdirectories
100
+ */
101
+ private static findConfigFile(startDir: string): string | null {
102
+ let currentDir = startDir;
103
+ const root = path.parse(currentDir).root;
104
+
105
+ // Search up the directory tree until we hit the filesystem root
106
+ while (currentDir !== root) {
107
+ for (const fileName of CONFIG_FILE_NAMES) {
108
+ const configPath = path.join(currentDir, fileName);
109
+ if (fs.existsSync(configPath)) {
110
+ return configPath;
111
+ }
112
+ }
113
+
114
+ // Move up one directory
115
+ const parentDir = path.dirname(currentDir);
116
+ if (parentDir === currentDir) {
117
+ break; // Reached the root
118
+ }
119
+ currentDir = parentDir;
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Check if a variable should be ignored based on config
127
+ */
128
+ static shouldIgnoreVar(varName: string, config: EnvGuardConfig): boolean {
129
+ return config.ignoreVars?.includes(varName) || false;
130
+ }
131
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Well-known environment variables that are provided by runtimes and don't need
3
+ * to be explicitly defined in .env or serverless.yml
4
+ */
5
+
6
+ /**
7
+ * AWS Lambda automatically provides these environment variables
8
+ * https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
9
+ */
10
+ export const AWS_PROVIDED_VARS = new Set([
11
+ 'AWS_REGION',
12
+ 'AWS_DEFAULT_REGION',
13
+ 'AWS_EXECUTION_ENV',
14
+ 'AWS_LAMBDA_FUNCTION_NAME',
15
+ 'AWS_LAMBDA_FUNCTION_VERSION',
16
+ 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
17
+ 'AWS_LAMBDA_LOG_GROUP_NAME',
18
+ 'AWS_LAMBDA_LOG_STREAM_NAME',
19
+ 'AWS_ACCESS_KEY_ID',
20
+ 'AWS_SECRET_ACCESS_KEY',
21
+ 'AWS_SESSION_TOKEN',
22
+ 'AWS_LAMBDA_RUNTIME_API',
23
+ '_HANDLER',
24
+ '_X_AMZN_TRACE_ID',
25
+ 'LAMBDA_TASK_ROOT',
26
+ 'LAMBDA_RUNTIME_DIR',
27
+ 'TZ', // Timezone
28
+ ]);
29
+
30
+ /**
31
+ * Common Node.js and development environment variables
32
+ */
33
+ export const NODEJS_RUNTIME_VARS = new Set([
34
+ 'NODE_ENV',
35
+ 'NODE_OPTIONS',
36
+ 'PATH',
37
+ 'HOME',
38
+ 'USER',
39
+ 'LANG',
40
+ 'LC_ALL',
41
+ 'PWD',
42
+ 'OLDPWD',
43
+ 'SHELL',
44
+ 'TERM',
45
+ ]);
46
+
47
+ /**
48
+ * CI/CD and testing environment variables
49
+ */
50
+ export const CI_CD_VARS = new Set([
51
+ 'CI',
52
+ 'CONTINUOUS_INTEGRATION',
53
+ 'GITHUB_ACTIONS',
54
+ 'GITLAB_CI',
55
+ 'CIRCLECI',
56
+ 'TRAVIS',
57
+ 'JENKINS_URL',
58
+ 'BUILDKITE',
59
+ ]);
60
+
61
+ /**
62
+ * Serverless Framework and local development
63
+ */
64
+ export const SERVERLESS_FRAMEWORK_VARS = new Set([
65
+ 'IS_OFFLINE',
66
+ 'SLS_OFFLINE',
67
+ 'SERVERLESS_STAGE',
68
+ 'SERVERLESS_REGION',
69
+ ]);
70
+
71
+ /**
72
+ * Testing framework variables
73
+ */
74
+ export const TEST_VARS = new Set([
75
+ 'JEST_WORKER_ID',
76
+ 'VITEST_WORKER_ID',
77
+ 'MOCHA_COLORS',
78
+ ]);
79
+
80
+ /**
81
+ * Combine all known runtime variables that don't need to be explicitly defined
82
+ */
83
+ export const KNOWN_RUNTIME_VARS = new Set([
84
+ ...AWS_PROVIDED_VARS,
85
+ ...NODEJS_RUNTIME_VARS,
86
+ ...CI_CD_VARS,
87
+ ...SERVERLESS_FRAMEWORK_VARS,
88
+ ...TEST_VARS,
89
+ ]);
90
+
91
+ /**
92
+ * Check if a variable is a known runtime variable
93
+ */
94
+ export function isKnownRuntimeVar(varName: string): boolean {
95
+ return KNOWN_RUNTIME_VARS.has(varName);
96
+ }
97
+
98
+ /**
99
+ * Get a human-readable category for a known runtime variable
100
+ */
101
+ export function getRuntimeVarCategory(varName: string): string | null {
102
+ if (AWS_PROVIDED_VARS.has(varName)) return 'AWS Lambda';
103
+ if (NODEJS_RUNTIME_VARS.has(varName)) return 'Node.js Runtime';
104
+ if (CI_CD_VARS.has(varName)) return 'CI/CD';
105
+ if (SERVERLESS_FRAMEWORK_VARS.has(varName)) return 'Serverless Framework';
106
+ if (TEST_VARS.has(varName)) return 'Testing';
107
+ return null;
108
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { CodeScanner } from './scanner/codeScanner';
2
+ export { EnvParser } from './parser/envParser';
3
+ export { EnvAnalyzer } from './analyzer/envAnalyzer';
4
+ export * from './types';
@@ -0,0 +1,114 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export interface EnvEntry {
5
+ key: string;
6
+ value: string;
7
+ comment?: string;
8
+ lineNumber: number;
9
+ }
10
+
11
+ export class EnvParser {
12
+ parse(filePath: string): Map<string, EnvEntry> {
13
+ const envVars = new Map<string, EnvEntry>();
14
+
15
+ if (!fs.existsSync(filePath)) {
16
+ return envVars;
17
+ }
18
+
19
+ try {
20
+ const content = fs.readFileSync(filePath, 'utf-8');
21
+ const lines = content.split('\n');
22
+ let currentComment = '';
23
+
24
+ lines.forEach((line, index) => {
25
+ const trimmed = line.trim();
26
+
27
+ // Skip empty lines
28
+ if (!trimmed) {
29
+ currentComment = '';
30
+ return;
31
+ }
32
+
33
+ // Capture comments
34
+ if (trimmed.startsWith('#')) {
35
+ currentComment = trimmed.substring(1).trim();
36
+ return;
37
+ }
38
+
39
+ // Parse key=value
40
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
41
+ if (match) {
42
+ const key = match[1];
43
+ let value = match[2];
44
+
45
+ // Remove quotes if present
46
+ if ((value.startsWith('"') && value.endsWith('"')) ||
47
+ (value.startsWith("'") && value.endsWith("'"))) {
48
+ value = value.slice(1, -1);
49
+ }
50
+
51
+ envVars.set(key, {
52
+ key,
53
+ value,
54
+ comment: this.isUserComment(currentComment) ? currentComment : undefined,
55
+ lineNumber: index + 1,
56
+ });
57
+
58
+ currentComment = '';
59
+ }
60
+ });
61
+ } catch (error) {
62
+ console.error(`Error parsing ${filePath}:`, error);
63
+ }
64
+
65
+ return envVars;
66
+ }
67
+
68
+ private isUserComment(comment: string): boolean {
69
+ if (!comment) return false;
70
+
71
+ // Filter out auto-generated comments to prevent duplicates when running fix multiple times
72
+ const autoGeneratedPrefixes = ['Used in:', 'Format:', 'Auto-generated by'];
73
+ return !autoGeneratedPrefixes.some(prefix => comment.startsWith(prefix));
74
+ }
75
+
76
+ parseExample(filePath: string): Set<string> {
77
+ const envVars = new Set<string>();
78
+
79
+ if (!fs.existsSync(filePath)) {
80
+ return envVars;
81
+ }
82
+
83
+ try {
84
+ const content = fs.readFileSync(filePath, 'utf-8');
85
+ const lines = content.split('\n');
86
+
87
+ lines.forEach(line => {
88
+ const trimmed = line.trim();
89
+
90
+ // Skip comments and empty lines
91
+ if (!trimmed || trimmed.startsWith('#')) {
92
+ return;
93
+ }
94
+
95
+ // Parse key=value
96
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=/);
97
+ if (match) {
98
+ envVars.add(match[1]);
99
+ }
100
+ });
101
+ } catch (error) {
102
+ console.error(`Error parsing ${filePath}:`, error);
103
+ }
104
+
105
+ return envVars;
106
+ }
107
+
108
+ getExistingContent(filePath: string): string {
109
+ if (!fs.existsSync(filePath)) {
110
+ return '';
111
+ }
112
+ return fs.readFileSync(filePath, 'utf-8');
113
+ }
114
+ }