@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.
- package/.envguardrc.example.json +17 -0
- package/LICENSE +21 -0
- package/README.md +320 -0
- package/dist/analyzer/envAnalyzer.d.ts +8 -0
- package/dist/analyzer/envAnalyzer.d.ts.map +1 -0
- package/dist/analyzer/envAnalyzer.js +112 -0
- package/dist/analyzer/envAnalyzer.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +52 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.d.ts +5 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +9 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/fix.d.ts +4 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +115 -0
- package/dist/commands/fix.js.map +1 -0
- package/dist/commands/scan.d.ts +9 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +274 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/config/configLoader.d.ts +35 -0
- package/dist/config/configLoader.d.ts.map +1 -0
- package/dist/config/configLoader.js +141 -0
- package/dist/config/configLoader.js.map +1 -0
- package/dist/constants/knownEnvVars.d.ts +38 -0
- package/dist/constants/knownEnvVars.d.ts.map +1 -0
- package/dist/constants/knownEnvVars.js +111 -0
- package/dist/constants/knownEnvVars.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/envParser.d.ts +13 -0
- package/dist/parser/envParser.d.ts.map +1 -0
- package/dist/parser/envParser.js +126 -0
- package/dist/parser/envParser.js.map +1 -0
- package/dist/parser/serverlessParser.d.ts +27 -0
- package/dist/parser/serverlessParser.d.ts.map +1 -0
- package/dist/parser/serverlessParser.js +162 -0
- package/dist/parser/serverlessParser.js.map +1 -0
- package/dist/scanner/codeScanner.d.ts +13 -0
- package/dist/scanner/codeScanner.d.ts.map +1 -0
- package/dist/scanner/codeScanner.js +157 -0
- package/dist/scanner/codeScanner.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
- package/src/analyzer/envAnalyzer.ts +133 -0
- package/src/cli.ts +54 -0
- package/src/commands/check.ts +6 -0
- package/src/commands/fix.ts +104 -0
- package/src/commands/scan.ts +289 -0
- package/src/config/configLoader.ts +131 -0
- package/src/constants/knownEnvVars.ts +108 -0
- package/src/index.ts +4 -0
- package/src/parser/envParser.ts +114 -0
- package/src/parser/serverlessParser.ts +146 -0
- package/src/scanner/codeScanner.ts +148 -0
- package/src/types.ts +26 -0
- package/test-project/.envguardrc.json +7 -0
- package/test-project/src/lambda1/.env.example +11 -0
- package/test-project/src/lambda1/handler.js +14 -0
- package/test-project/src/lambda2/.env.example +9 -0
- package/test-project/src/lambda2/handler.js +11 -0
- package/test-project/src/lambda2/handler2.js +13 -0
- package/test-project/src/lambda2/serverless.yml +50 -0
- package/test-project/src/payment/.env.example +23 -0
- package/test-project/src/payment/payment.js +14 -0
- package/test-project/src/payment/server.ts +11 -0
- 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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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,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
|
+
}
|