env-detector 1.0.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/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ MIT License
2
+ Copyright (c) 2026
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # env-scan
2
+
3
+ Automatically generate .env file by scanning process.env usage.
4
+
5
+ ## Install
6
+
7
+ npx env-scan
8
+
9
+ OR
10
+
11
+ npm install -g env-scan
12
+
13
+ ## Usage
14
+
15
+ env-scan
16
+
17
+ ## Example
18
+
19
+ process.env.DB_HOST
20
+ process.env.PORT
21
+
22
+ Generated:
23
+
24
+ DB_HOST=
25
+ PORT=
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const readline = require("readline-sync");
6
+ const { scanProject, scanSecurity } = require("../src/scan");
7
+
8
+ const cwd = process.cwd();
9
+ const envPath = path.join(cwd, ".env");
10
+
11
+ const args = process.argv.slice(2);
12
+
13
+ const askMode = args.includes("--ask");
14
+ const compareMode = args.includes("--compare");
15
+ const checkMode = args.includes("--check");
16
+ const fixMode = args.includes("--fix");
17
+ const securityMode = args.includes("--security");
18
+ const strictMode = args.includes("--strict");
19
+
20
+ let result = scanProject(cwd);
21
+
22
+
23
+ // security
24
+ if (securityMode) {
25
+ const issues = scanSecurity(cwd);
26
+
27
+ if (!issues.length) {
28
+ console.log("✔ No security issues\n");
29
+ process.exit(0);
30
+ }
31
+
32
+ console.log("\nSecurity issues:\n");
33
+ issues.forEach(i => console.log(" -", i.file));
34
+ console.log("");
35
+
36
+ process.exit(0);
37
+ }
38
+
39
+
40
+ // compare
41
+ if (compareMode) {
42
+
43
+ console.log("\nUsed:");
44
+ result.used.forEach(v => console.log(" -", v));
45
+
46
+ console.log("\nMissing:");
47
+ result.missing.forEach(v => console.log(" -", v));
48
+
49
+ console.log("\nEmpty:");
50
+ result.empty.forEach(v => console.log(" -", v));
51
+
52
+ console.log("\nUnused:");
53
+ result.unused.forEach(v => console.log(" -", v));
54
+
55
+ console.log("");
56
+ process.exit(0);
57
+ }
58
+
59
+
60
+ // check
61
+ if (checkMode) {
62
+
63
+ if (result.missing.length || result.empty.length) {
64
+
65
+ console.log("\nENV check failed");
66
+
67
+ result.missing.forEach(v => console.log("Missing:", v));
68
+ result.empty.forEach(v => console.log("Empty:", v));
69
+
70
+ console.log("");
71
+ process.exit(1);
72
+ }
73
+
74
+ console.log("✔ ENV check passed\n");
75
+ process.exit(0);
76
+ }
77
+
78
+
79
+ // create env
80
+ if (!fs.existsSync(envPath)) {
81
+ fs.writeFileSync(envPath, "");
82
+ }
83
+
84
+
85
+ // grouped generation
86
+ if (Object.keys(result.grouped).length) {
87
+
88
+ let output = "";
89
+
90
+ // collect grouped keys
91
+ const groupedKeys = new Set();
92
+
93
+ Object.values(result.grouped).forEach(keys => {
94
+ keys.forEach(k => groupedKeys.add(k));
95
+ });
96
+
97
+ // GLOBAL VARIABLES
98
+ const globalKeys = result.used.filter(k => !groupedKeys.has(k));
99
+
100
+ if (globalKeys.length) {
101
+ output += "# global\n";
102
+
103
+ globalKeys.forEach(key => {
104
+ const value = result.defaults?.[key] ?? "";
105
+ output += `${key}=${value}\n`;
106
+ });
107
+
108
+ output += "\n";
109
+ }
110
+
111
+ // GROUPED ENVIRONMENTS
112
+ Object.entries(result.grouped).forEach(([env, keys]) => {
113
+
114
+ output += `# ${env}\n`;
115
+
116
+ keys.forEach(key => {
117
+ const value = result.defaults?.[key] ?? "";
118
+ output += `${key}=${value}\n`;
119
+ });
120
+
121
+ output += "\n";
122
+ });
123
+
124
+ fs.writeFileSync(envPath, output.trim() + "\n");
125
+
126
+ console.log("✔ Generated grouped env file\n");
127
+ process.exit(0);
128
+ }
129
+
130
+
131
+ // parse env to map
132
+ let content = fs.existsSync(envPath)
133
+ ? fs.readFileSync(envPath, "utf8")
134
+ : "";
135
+
136
+ const envMap = {};
137
+
138
+ content.split("\n").forEach(line => {
139
+ const [k, ...rest] = line.split("=");
140
+ if (!k) return;
141
+ envMap[k.trim()] = rest.join("=");
142
+ });
143
+
144
+
145
+ // ask mode
146
+ const askList = [...new Set([...result.missing, ...result.empty])];
147
+
148
+ if (askMode && askList.length) {
149
+
150
+ askList.forEach(key => {
151
+ const value = readline.question(`${key} = `);
152
+ envMap[key] = value;
153
+ });
154
+
155
+ const newContent = Object.entries(envMap)
156
+ .map(([k, v]) => `${k}=${v}`)
157
+ .join("\n");
158
+
159
+ fs.writeFileSync(envPath, newContent + "\n");
160
+
161
+ console.log("✔ .env updated\n");
162
+ process.exit(0);
163
+ }
164
+
165
+
166
+ // add missing
167
+ result.missing.forEach(key => {
168
+ const value = result.defaults?.[key] ?? "";
169
+ if (!envMap[key]) {
170
+ envMap[key] = value;
171
+ }
172
+ });
173
+
174
+
175
+ // fix unused
176
+ if (fixMode) {
177
+ result.unused.forEach(key => delete envMap[key]);
178
+ }
179
+
180
+
181
+ const newContent = Object.entries(envMap)
182
+ .map(([k, v]) => `${k}=${v}`)
183
+ .join("\n");
184
+
185
+ fs.writeFileSync(envPath, newContent + "\n");
186
+
187
+
188
+ // strict
189
+ if (strictMode) {
190
+ if (
191
+ result.missing.length ||
192
+ result.empty.length ||
193
+ result.unused.length
194
+ ) {
195
+ console.log("✖ strict mode failed\n");
196
+ process.exit(1);
197
+ }
198
+
199
+ console.log("✔ strict mode passed\n");
200
+ process.exit(0);
201
+ }
202
+
203
+
204
+ console.log("✔ env scan complete\n");
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "env-detector",
3
+ "version": "1.0.0",
4
+ "description": "Smart .env generator and auditor",
5
+ "main": "src/scan.js",
6
+ "bin": {
7
+ "env-detector": "./bin/env-scan.js",
8
+ "env-scan": "./bin/env-scan.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": ["env", "dotenv", "environment", "env-scan", "env-detector"],
14
+ "author": "Jenil Gajjar",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@babel/parser": "^7.29.2",
18
+ "@babel/traverse": "^7.29.0",
19
+ "readline-sync": "^1.4.10"
20
+ }
21
+ }
package/src/scan.js ADDED
@@ -0,0 +1,249 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const parser = require("@babel/parser");
4
+ const traverse = require("@babel/traverse").default;
5
+
6
+ function scanProject(rootDir) {
7
+ const usedEnvVars = new Set();
8
+ const defaultValues = new Map();
9
+ const groupedEnv = {};
10
+
11
+ function scanDir(dir) {
12
+ const files = fs.readdirSync(dir);
13
+
14
+ for (const file of files) {
15
+ if (
16
+ file === "node_modules" ||
17
+ file === ".git" ||
18
+ file === "dist" ||
19
+ file === "build" ||
20
+ file.startsWith(".")
21
+ ) continue;
22
+
23
+ const fullPath = path.join(dir, file);
24
+ const stat = fs.statSync(fullPath);
25
+
26
+ if (stat.isDirectory()) {
27
+ scanDir(fullPath);
28
+ } else if (/\.(js|ts|jsx|tsx)$/.test(file)) {
29
+ const content = fs.readFileSync(fullPath, "utf8");
30
+
31
+ try {
32
+ const ast = parser.parse(content, {
33
+ sourceType: "module",
34
+ plugins: ["typescript", "jsx"],
35
+ });
36
+
37
+ traverse(ast, {
38
+
39
+ // detect grouped config (development/staging/etc)
40
+ ObjectProperty(path) {
41
+
42
+ const envName = path.node.key.name;
43
+
44
+ if (!path.node.value || path.node.value.type !== "ObjectExpression")
45
+ return;
46
+
47
+ const vars = new Set();
48
+
49
+ path.node.value.properties.forEach(prop => {
50
+
51
+ if (!prop.value) return;
52
+
53
+ // process.env.KEY
54
+ if (
55
+ prop.value.type === "MemberExpression" &&
56
+ prop.value.object?.object?.name === "process" &&
57
+ prop.value.object?.property?.name === "env"
58
+ ) {
59
+ const key =
60
+ prop.value.property.name ||
61
+ prop.value.property.value;
62
+
63
+ vars.add(key);
64
+ usedEnvVars.add(key);
65
+ }
66
+
67
+ // process.env.KEY || value
68
+ if (prop.value.type === "LogicalExpression") {
69
+
70
+ const left = prop.value.left;
71
+ const right = prop.value.right;
72
+
73
+ if (
74
+ left.type === "MemberExpression" &&
75
+ left.object?.object?.name === "process"
76
+ ) {
77
+ const key =
78
+ left.property.name || left.property.value;
79
+
80
+ vars.add(key);
81
+ usedEnvVars.add(key);
82
+
83
+ if (
84
+ right.type === "StringLiteral" ||
85
+ right.type === "NumericLiteral" ||
86
+ right.type === "BooleanLiteral"
87
+ ) {
88
+ defaultValues.set(key, right.value);
89
+ }
90
+ }
91
+ }
92
+
93
+ });
94
+
95
+ if (vars.size) {
96
+ groupedEnv[envName] = Array.from(vars);
97
+ }
98
+ },
99
+
100
+ // standalone process.env.KEY
101
+ MemberExpression(path) {
102
+ const node = path.node;
103
+
104
+ if (
105
+ node.object &&
106
+ node.object.type === "MemberExpression" &&
107
+ node.object.object?.name === "process" &&
108
+ node.object.property?.name === "env"
109
+ ) {
110
+ const key =
111
+ node.property.name || node.property.value;
112
+
113
+ usedEnvVars.add(key);
114
+ }
115
+ },
116
+
117
+ // destructuring
118
+ VariableDeclarator(path) {
119
+ if (
120
+ path.node.init &&
121
+ path.node.init.type === "MemberExpression" &&
122
+ path.node.init.object.name === "process" &&
123
+ path.node.init.property.name === "env"
124
+ ) {
125
+ if (path.node.id.type === "ObjectPattern") {
126
+ path.node.id.properties.forEach(prop => {
127
+ if (prop.key?.name) {
128
+ usedEnvVars.add(prop.key.name);
129
+ }
130
+ });
131
+ }
132
+ }
133
+ },
134
+
135
+ // fallback detection
136
+ LogicalExpression(path) {
137
+ const left = path.node.left;
138
+ const right = path.node.right;
139
+
140
+ if (
141
+ left.type === "MemberExpression" &&
142
+ left.object?.object?.name === "process" &&
143
+ left.object?.property?.name === "env"
144
+ ) {
145
+ const key =
146
+ left.property.name || left.property.value;
147
+
148
+ usedEnvVars.add(key);
149
+
150
+ if (
151
+ right.type === "StringLiteral" ||
152
+ right.type === "NumericLiteral" ||
153
+ right.type === "BooleanLiteral"
154
+ ) {
155
+ defaultValues.set(key, right.value);
156
+ }
157
+ }
158
+ },
159
+
160
+ });
161
+
162
+ } catch (err) {}
163
+ }
164
+ }
165
+ }
166
+
167
+ scanDir(rootDir);
168
+
169
+ // read existing env
170
+ const envPath = path.join(rootDir, ".env");
171
+ const envFileVars = new Set();
172
+ const emptyVars = new Set();
173
+
174
+ if (fs.existsSync(envPath)) {
175
+ const envContent = fs.readFileSync(envPath, "utf8");
176
+
177
+ envContent.split("\n").forEach(line => {
178
+ const trimmed = line.trim();
179
+ if (!trimmed || trimmed.startsWith("#")) return;
180
+
181
+ const [key, value] = trimmed.split("=");
182
+
183
+ if (key) {
184
+ envFileVars.add(key.trim());
185
+
186
+ if (!value || value.trim() === "") {
187
+ emptyVars.add(key.trim());
188
+ }
189
+ }
190
+ });
191
+ }
192
+
193
+ const missing = [...usedEnvVars].filter(
194
+ key => !envFileVars.has(key)
195
+ );
196
+
197
+ const unused = [...envFileVars].filter(
198
+ key => !usedEnvVars.has(key)
199
+ );
200
+
201
+ return {
202
+ used: Array.from(usedEnvVars),
203
+ missing,
204
+ unused,
205
+ empty: Array.from(emptyVars),
206
+ defaults: Object.fromEntries(defaultValues),
207
+ grouped: groupedEnv
208
+ };
209
+ }
210
+
211
+
212
+ // security scan
213
+ function scanSecurity(rootDir) {
214
+
215
+ const issues = [];
216
+
217
+ const regex =
218
+ /(password|secret|token|apikey|key)\s*[:=]\s*['"][^'"]+['"]/gi;
219
+
220
+ function scan(dir) {
221
+ const files = fs.readdirSync(dir);
222
+
223
+ for (const file of files) {
224
+
225
+ if (file === "node_modules" || file === ".git") continue;
226
+
227
+ const full = path.join(dir, file);
228
+ const stat = fs.statSync(full);
229
+
230
+ if (stat.isDirectory()) {
231
+ scan(full);
232
+ } else if (/\.(js|ts|env)$/.test(file)) {
233
+ const content = fs.readFileSync(full, "utf8");
234
+
235
+ if (regex.test(content)) {
236
+ issues.push({ file: full });
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ scan(rootDir);
243
+ return issues;
244
+ }
245
+
246
+ module.exports = {
247
+ scanProject,
248
+ scanSecurity
249
+ };
package/src/writer.js ADDED
@@ -0,0 +1,24 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function updateEnv(vars) {
5
+ const envPath = path.join(process.cwd(), ".env");
6
+
7
+ let existing = "";
8
+
9
+ if (fs.existsSync(envPath)) {
10
+ existing = fs.readFileSync(envPath, "utf8");
11
+ }
12
+
13
+ let output = existing;
14
+
15
+ vars.forEach(key => {
16
+ if (!existing.includes(key + "=")) {
17
+ output += `\n${key}=`;
18
+ }
19
+ });
20
+
21
+ fs.writeFileSync(envPath, output);
22
+ }
23
+
24
+ module.exports = { updateEnv };