dotenv-audit 1.0.2

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 env-shield contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,317 @@
1
+ <p align="center">
2
+ <h1 align="center">dotenv-audit</h1>
3
+ <p align="center">Auto-detect and validate environment variables by scanning your codebase.<br/>Zero config. Zero schema. Zero dependencies.</p>
4
+ </p>
5
+
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/dotenv-audit"><img src="https://img.shields.io/npm/v/dotenv-audit.svg" alt="npm version"></a>
8
+ <a href="https://www.npmjs.com/package/dotenv-audit"><img src="https://img.shields.io/npm/dm/dotenv-audit.svg" alt="npm downloads"></a>
9
+ <a href="https://github.com/AkashGupta/dotenv-audit/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/dotenv-audit.svg" alt="license"></a>
10
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen.svg" alt="zero dependencies">
11
+ </p>
12
+
13
+ ---
14
+
15
+ Unlike other env validators that force you to write schemas manually, **dotenv-audit scans your actual code** to find every `process.env` usage - then validates them all.
16
+
17
+ ## The Problem
18
+
19
+ ```
20
+ > App crashes in production
21
+ > "Cannot read property of undefined"
22
+ > Turns out someone forgot to set DATABASE_URL
23
+ > 3 hours of debugging...
24
+ ```
25
+
26
+ **dotenv-audit prevents this.** One command. It finds every env variable your code uses and tells you exactly what's missing.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install dotenv-audit
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ # Just run it - no config needed
38
+ npx dotenv-audit
39
+ ```
40
+
41
+ That's it. dotenv-audit will:
42
+
43
+ 1. Scan all your project files (`.js`, `.ts`, `.jsx`, `.tsx`, `.vue`, `.svelte`)
44
+ 2. Find every `process.env.XXXX` usage automatically
45
+ 3. Check which variables are actually set
46
+ 4. Show a clear report of what's missing
47
+
48
+ ### Output
49
+
50
+ ```
51
+ dotenv-audit v1.0.0
52
+ ──────────────────────────────────────────
53
+ Scanned 47 files · Found 12 env variables
54
+
55
+ x MISSING (2)
56
+
57
+ x DATABASE_URL
58
+ > src/db/connect.ts:14
59
+
60
+ x JWT_SECRET
61
+ > src/auth/middleware.js:7
62
+
63
+ ! 3 warning(s) - use --verbose to see details
64
+
65
+ / SET (10)
66
+
67
+ ──────────────────────────────────────────
68
+ 2 missing · 10 set
69
+ ```
70
+
71
+ ## Interactive Mode
72
+
73
+ ```bash
74
+ npx dotenv-audit --ask
75
+ ```
76
+
77
+ Asks you step by step:
78
+
79
+ ```
80
+ ? Generate ENV_SETUP.md file with all missing variables? (yes/no): yes
81
+ Created ENV_SETUP.md with 12 variables
82
+
83
+ ? Create new .env file with missing variables? (yes/no): yes
84
+ Created .env with 12 variables
85
+ ```
86
+
87
+ Generated `.env` has **smart placeholder values** based on variable names:
88
+
89
+ ```env
90
+ # -- Database ────────────────────────────────
91
+ DATABASE_URL=mongodb://localhost:27017/your_database_name
92
+ REDIS_CONNECTION_STRING=redis://localhost:6379
93
+
94
+ # -- Authentication ──────────────────────────
95
+ JWT_SECRET=your_jwt_secret_key_min_32_chars_long
96
+
97
+ # -- AI / LLM ────────────────────────────────
98
+ OPENAI_API_KEY=sk-your_openai_api_key_here
99
+ ANTHROPIC_API_KEY=sk-ant-your_anthropic_api_key_here
100
+
101
+ # -- AWS ─────────────────────────────────────
102
+ AWS_ACCESS_KEY_ID=your_aws_access_key_id
103
+ AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key
104
+ AWS_REGION=us-east-1
105
+ ```
106
+
107
+ It even **auto-detects your database** from `package.json` - if you use `mongoose`, it gives `mongodb://` URLs, not `postgresql://`.
108
+
109
+ ## Monorepo Support
110
+
111
+ dotenv-audit auto-detects monorepos (pnpm workspaces, lerna, turbo) and creates **separate `.env` files per service**:
112
+
113
+ ```
114
+ ? Create/update .env inside each service folder? (yes/no): yes
115
+
116
+ api/.env — created with 25 variables
117
+ client/.env — created with 14 variables
118
+ services/payments/.env — created with 7 variables
119
+
120
+ Done! 3 service .env files processed
121
+ ```
122
+
123
+ ## Detection Patterns
124
+
125
+ ```js
126
+ // All of these are detected:
127
+
128
+ process.env.DATABASE_URL // dot access
129
+ process.env["API_KEY"] // bracket access
130
+ process.env['SECRET'] // single quotes
131
+ const { PORT, HOST } = process.env // destructuring
132
+ process.env.PORT || 3000 // with defaults (warning, not error)
133
+ process.env.DEBUG ?? false // nullish coalescing
134
+ import.meta.env.VITE_API_URL // Vite env
135
+
136
+ // Smart filtering:
137
+ import.meta.env.DEV // SKIPPED (Vite built-in)
138
+ import.meta.env.MODE // SKIPPED (Vite built-in)
139
+ // process.env.COMMENTED // SKIPPED (in comments)
140
+ process.env[dynamicVar] // warned (can't validate)
141
+ ```
142
+
143
+ ## CLI Commands
144
+
145
+ ```bash
146
+ # Scan current directory
147
+ npx dotenv-audit
148
+
149
+ # Scan specific directory
150
+ npx dotenv-audit ./src
151
+
152
+ # Interactive mode - generates .env and ENV_SETUP.md
153
+ npx dotenv-audit --ask
154
+
155
+ # Verbose output (show warnings + all set vars)
156
+ npx dotenv-audit --verbose
157
+
158
+ # Strict mode (empty strings = error)
159
+ npx dotenv-audit --strict
160
+
161
+ # JSON output (great for CI)
162
+ npx dotenv-audit --json
163
+
164
+ # CI mode (exit code 1 if vars missing)
165
+ npx dotenv-audit --ci
166
+
167
+ # List all detected vars
168
+ npx dotenv-audit audit
169
+
170
+ # Auto-generate .env.example
171
+ npx dotenv-audit gen
172
+
173
+ # Create config file
174
+ npx dotenv-audit init
175
+ ```
176
+
177
+ ## Programmatic API
178
+
179
+ ```js
180
+ // At app startup - validate and throw on missing
181
+ require('dotenv-audit').protect()
182
+
183
+ // Same but never throws - returns results
184
+ const result = require('dotenv-audit').check()
185
+ console.log(result.ok) // true/false
186
+ console.log(result.missing) // ['DB_URL', 'SECRET']
187
+ console.log(result.set) // ['PORT', 'NODE_ENV']
188
+
189
+ // Detailed audit
190
+ const audit = require('dotenv-audit').audit()
191
+ // audit.variables = [{ name, locations, hasDefault, value }]
192
+
193
+ // Generate .env.example content
194
+ const content = require('dotenv-audit').genExample()
195
+ ```
196
+
197
+ ### protect() Options
198
+
199
+ ```js
200
+ require('dotenv-audit').protect({
201
+ rootDir: __dirname,
202
+ strict: true, // treat empty strings as errors
203
+ required: ['DB_URL'], // always require these
204
+ optional: ['DEBUG'], // never error on these
205
+ ignoreVars: ['TEST'], // completely ignore
206
+ exitOnError: true, // throw on missing (default: true)
207
+ format: 'pretty', // 'pretty' | 'json' | 'silent'
208
+ verbose: false
209
+ })
210
+ ```
211
+
212
+ ## Configuration
213
+
214
+ ### Option 1: package.json
215
+
216
+ ```json
217
+ {
218
+ "dotenv-audit": {
219
+ "strict": false,
220
+ "required": ["DATABASE_URL", "JWT_SECRET"],
221
+ "optional": ["DEBUG", "LOG_LEVEL"],
222
+ "ignoreVars": ["npm_package_version"],
223
+ "ignore": ["scripts/"]
224
+ }
225
+ }
226
+ ```
227
+
228
+ ### Option 2: .dotenv-auditrc
229
+
230
+ ```json
231
+ {
232
+ "strict": false,
233
+ "required": ["DATABASE_URL"],
234
+ "optional": ["DEBUG"],
235
+ "ignoreVars": [],
236
+ "ignore": []
237
+ }
238
+ ```
239
+
240
+ ### Option 3: dotenv-audit.config.js
241
+
242
+ ```js
243
+ module.exports = {
244
+ strict: process.env.NODE_ENV === 'production',
245
+ required: ['DATABASE_URL', 'JWT_SECRET'],
246
+ optional: ['DEBUG']
247
+ }
248
+ ```
249
+
250
+ ## Framework Support
251
+
252
+ Auto-detects your framework and understands framework-specific patterns:
253
+
254
+ | Framework | Prefix | Auto-detected |
255
+ |-----------|--------|:---:|
256
+ | Next.js | `NEXT_PUBLIC_*` | Yes |
257
+ | Vite | `VITE_*` | Yes |
258
+ | Nuxt | `NUXT_*` | Yes |
259
+ | Gatsby | `GATSBY_*` | Yes |
260
+ | SvelteKit | `PUBLIC_*` | Yes |
261
+ | Astro | `PUBLIC_*` | Yes |
262
+ | Express/Fastify/NestJS | - | Yes |
263
+
264
+ ## Database Auto-Detection
265
+
266
+ dotenv-audit reads your `package.json` to give correct placeholder values:
267
+
268
+ | Dependency | DATABASE_URL placeholder |
269
+ |---|---|
270
+ | `mongoose` / `mongodb` | `mongodb://localhost:27017/your_database_name` |
271
+ | `pg` / `postgres` | `postgresql://user:password@localhost:5432/your_database_name` |
272
+ | `mysql` / `mysql2` | `mysql://user:password@localhost:3306/your_database_name` |
273
+ | `prisma` | Reads `schema.prisma` to detect actual DB |
274
+
275
+ ## Use in CI/CD
276
+
277
+ ```yaml
278
+ # GitHub Actions
279
+ - name: Validate env vars
280
+ run: npx dotenv-audit --ci --json
281
+ env:
282
+ DATABASE_URL: ${{ secrets.DATABASE_URL }}
283
+ JWT_SECRET: ${{ secrets.JWT_SECRET }}
284
+ ```
285
+
286
+ ## Best Practice
287
+
288
+ ```js
289
+ // app.js
290
+ require('dotenv').config() // 1. load vars
291
+ require('dotenv-audit').protect() // 2. validate vars
292
+ // ... rest of your app
293
+ ```
294
+
295
+ ## Comparison
296
+
297
+ | Feature | dotenv | envalid | env-var | **dotenv-audit** |
298
+ |---------|--------|---------|---------|-------------|
299
+ | Load .env | Yes | Yes | No | No* |
300
+ | Validate | No | Yes | Yes | **Yes** |
301
+ | Auto-detect from code | No | No | No | **Yes** |
302
+ | Zero schema needed | - | No | No | **Yes** |
303
+ | .env.example cross-check | No | No | No | **Yes** |
304
+ | Generate .env file | No | No | No | **Yes** |
305
+ | Generate ENV_SETUP.md | No | No | No | **Yes** |
306
+ | Framework detection | No | No | No | **Yes** |
307
+ | Database auto-detect | No | No | No | **Yes** |
308
+ | Monorepo support | No | No | No | **Yes** |
309
+ | Interactive mode | No | No | No | **Yes** |
310
+ | CLI tool | No | No | No | **Yes** |
311
+ | Zero dependencies | Yes | No | No | **Yes** |
312
+
313
+ \* dotenv-audit validates, not loads. Use alongside `dotenv` for loading.
314
+
315
+ ## License
316
+
317
+ MIT
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";const e=require("fs"),t=require("path"),n=[".dotenv-auditrc",".dotenv-auditrc.json","dotenv-audit.config.js","dotenv-audit.config.cjs"],r={ignore:[],extensions:[],required:[],optional:[],ignoreVars:[],strict:!1,exitOnError:!0,format:"pretty",verbose:!1,maxDepth:20,framework:"auto"};function loadConfig(s){const o=t.join(s,"package.json");if(e.existsSync(o))try{const t=JSON.parse(e.readFileSync(o,"utf-8"));if(t.dotenv-audit||t["dotenv-audit"])return mergeConfig(t.dotenv-audit||t["dotenv-audit"])}catch{}for(const r of n){const n=t.join(s,r);if(e.existsSync(n))try{if(r.endsWith(".js")||r.endsWith(".cjs"))return mergeConfig(require(n));const t=e.readFileSync(n,"utf-8");return mergeConfig(JSON.parse(t))}catch{}}return{...r}}function mergeConfig(e){const t={...r};for(const n of Object.keys(e))n in r&&(Array.isArray(r[n])&&Array.isArray(e[n])?t[n]=[...r[n],...e[n]]:t[n]=e[n]);return t}function detectFramework(n){const r=t.join(n,"package.json");if(!e.existsSync(r))return"node";try{const t=JSON.parse(e.readFileSync(r,"utf-8")),n={...t.dependencies,...t.devDependencies};return n.next?"nextjs":n.nuxt||n.nuxt3?"nuxt":n.vite?"vite":n["@remix-run/node"]||n["@remix-run/react"]?"remix":n.gatsby?"gatsby":n["@angular/core"]?"angular":n.svelte||n["@sveltejs/kit"]?"sveltekit":n.astro?"astro":n.express?"express":n.fastify?"fastify":n.koa?"koa":n.nestjs||n["@nestjs/core"]?"nestjs":"node"}catch{return"node"}}function getFrameworkPrefixes(e){return{nextjs:["NEXT_PUBLIC_"],nuxt:["NUXT_","NUXT_PUBLIC_"],vite:["VITE_"],remix:[],gatsby:["GATSBY_"],angular:["NG_"],sveltekit:["PUBLIC_"],astro:["PUBLIC_"],express:[],fastify:[],koa:[],nestjs:[],node:[]}[e]||[]}module.exports={loadConfig:loadConfig,mergeConfig:mergeConfig,detectFramework:detectFramework,getFrameworkPrefixes:getFrameworkPrefixes,DEFAULT_CONFIG:r};
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";const e=require("path"),{scan:scan}=require("./scanner"),{validate:validate,isValid:isValid}=require("./validator"),{formatReport:formatReport,formatJSON:formatJSON,generateEnvExample:generateEnvExample}=require("./reporter"),{loadConfig:loadConfig,detectFramework:detectFramework,getFrameworkPrefixes:getFrameworkPrefixes}=require("./config");function protect(e={}){const t=e.rootDir||r(),n={...loadConfig(t),...e};"auto"===n.framework&&(n.framework=detectFramework(t));const o=scan(t,{extensions:n.extensions.length>0?n.extensions:void 0,ignore:n.ignore.length>0?n.ignore:void 0,maxDepth:n.maxDepth}),s=validate(o,{strict:n.strict,required:n.required,optional:n.optional,ignore:n.ignoreVars});if("silent"!==n.format&&("json"===n.format?console.log(formatJSON(s,o)):console.log(formatReport(s,o,{noColor:n.noColor||!1,verbose:n.verbose}))),n.exitOnError&&s.stats.missing>0){const e=s.missing.map(e=>e.name).join(", "),r=new Error(`dotenv-audit: ${s.stats.missing} required environment variable(s) missing: ${e}`);if(r.code="ENV_SHIELD_MISSING",r.missing=s.missing.map(e=>e.name),!1!==n.throw)throw r}return{ok:0===s.stats.missing&&0===s.stats.empty,missing:s.missing.map(e=>e.name),empty:s.empty.map(e=>e.name),set:s.set.map(e=>e.name),warnings:s.warnings.length,stats:s.stats,raw:s}}function check(e={}){return protect({...e,exitOnError:!1,throw:!1})}function audit(e={}){const t=e.rootDir||r(),n={...loadConfig(t),...e},o=scan(t,{extensions:n.extensions.length>0?n.extensions:void 0,ignore:n.ignore.length>0?n.ignore:void 0,maxDepth:n.maxDepth}),s=[];for(const[e,r]of o.variables)r.isDynamic||s.push({name:e,locations:r.usages.map(e=>({file:e.file,line:e.line,pattern:e.pattern})),hasDefault:r.hasDefault,inExample:r.inExample,value:void 0!==process.env[e]?"(set)":"(not set)"});return{variables:s.sort((e,r)=>e.name.localeCompare(r.name)),totalFiles:o.totalFiles,framework:"auto"===n.framework?detectFramework(t):n.framework}}function genExample(e={}){const t=e.rootDir||r(),n=scan(t,e);return generateEnvExample(n)}function r(){let r=process.cwd();for(let t=0;t<10;t++){const t=e.join(r,"package.json");try{return require("fs").accessSync(t),r}catch{const t=e.dirname(r);if(t===r)break;r=t}}return process.cwd()}module.exports={protect:protect,check:check,audit:audit,genExample:genExample};
@@ -0,0 +1 @@
1
+ "use strict";const e=require("readline"),s=require("fs"),o=require("path"),{getPlaceholder:getPlaceholder,getCategory:getCategory,detectDatabaseFromDir:detectDatabaseFromDir}=require("./placeholder"),{isMonorepo:isMonorepo,scanMonorepo:scanMonorepo}=require("./scanner"),{validate:validate}=require("./validator");function n(e,s){return new Promise(o=>{e.question(s,e=>{o(e.trim().toLowerCase())})})}function t(e){const s=new Map;for(const o of e){const e=getCategory(o.name);s.has(e)||s.set(e,[]),s.get(e).push(o)}return s}function generateEnvContent(e,s,o={}){const n=t(e),i=[];i.push("# ──────────────────────────────────────────"),i.push("# Auto-generated by dotenv-audit"),s&&i.push(`# Service: ${s}`),i.push(`# ${(new Date).toLocaleDateString("en-IN",{day:"2-digit",month:"short",year:"numeric"})}`),i.push("# Replace placeholder values with actual values"),i.push("# ──────────────────────────────────────────"),i.push("");for(const[e,s]of n){i.push(`# ── ${e} ${"─".repeat(Math.max(0,40-e.length))}`);for(const e of s){const s=getPlaceholder(e.name,o);i.push(`${e.name}=${s}`)}i.push("")}return i.join("\n")}function appendToEnv(e,s,o={}){const n=t(s),i=[];i.push(""),i.push("# ──────────────────────────────────────────"),i.push("# Missing variables added by dotenv-audit"),i.push(`# ${(new Date).toLocaleDateString("en-IN",{day:"2-digit",month:"short",year:"numeric"})}`),i.push("# ──────────────────────────────────────────"),i.push("");for(const[e,s]of n){i.push(`# ── ${e} ${"─".repeat(Math.max(0,40-e.length))}`);for(const e of s){const s=getPlaceholder(e.name,o);i.push(`${e.name}=${s}`)}i.push("")}return e.trimEnd()+"\n"+i.join("\n")}function writeEnvForService(e,n,t){const i=o.join(e,".env"),a=s.existsSync(i),r={db:detectDatabaseFromDir(e)};if(a){const e=s.readFileSync(i,"utf-8"),o=new Set;for(const s of e.split("\n")){const e=s.match(/^([A-Z_][A-Z0-9_]*)\s*=/);e&&o.add(e[1])}const t=n.filter(e=>!o.has(e.name));if(0===t.length)return{action:"skipped",path:i,count:0,reason:"all vars already exist"};const a=appendToEnv(e,t,r);return s.writeFileSync(i,a,"utf-8"),{action:"updated",path:i,count:t.length}}const l=generateEnvContent(n,t,r);return s.writeFileSync(i,l,"utf-8"),{action:"created",path:i,count:n.length}}function generateMissingEnvMd(e,s,n){const i=[];i.push("# Missing Environment Variables"),i.push(""),i.push(`> Auto-generated by **dotenv-audit** on ${(new Date).toLocaleDateString("en-IN",{day:"2-digit",month:"short",year:"numeric"})}`),i.push(`> Project: \`${o.basename(s)}\``),n&&i.push(`> Type: **Monorepo** (${e.length} services detected)`),i.push(""),i.push("---"),i.push(""),i.push("## How to use"),i.push(""),n?(i.push("1. Each service has its own section below"),i.push("2. Copy the variables to the `.env` file **inside that service folder**"),i.push("3. Replace placeholder values with your actual values")):(i.push("1. Copy the variable names and values below"),i.push("2. Add them to your `.env` file"),i.push("3. Replace placeholder values with your actual values")),i.push(""),i.push("---"),i.push("");let a=0;for(const r of e){if(0===r.missing.length)continue;if(a+=r.missing.length,n){const e=o.join(r.relativePath,".env");i.push(`# ${r.name}`),i.push(""),i.push(`> **Path:** \`${r.relativePath}/\``),i.push(`> **Env file:** \`${e}\``),i.push("")}const e=r.dir||s,l={db:detectDatabaseFromDir(e)},c=t(r.missing);for(const[e,s]of c){i.push(`## ${n?`[${r.name}] `:""}${e}`),i.push(""),i.push("```env");for(const e of s){const s=getPlaceholder(e.name,l);i.push(`${e.name}=${s}`)}i.push("```"),i.push(""),i.push("<details>"),i.push(`<summary>Where these are used (${s.length} variables)</summary>`),i.push("");for(const e of s)if(e.usages&&e.usages.length>0){const s=e.usages.map(e=>`\`${e.file}:${e.line}\``).join(", ");i.push(`- **${e.name}** — ${s}`)}else i.push(`- **${e.name}** — (from config)`);i.push(""),i.push("</details>"),i.push("")}i.push("---"),i.push("")}return i.push(`**Total: ${a} missing variables${n?` across ${e.length} services`:""}**`),i.push(""),i.join("\n")}async function runInteractive(t,a,r,l){if(isMonorepo(r))return i(r,l||{});const{missing:c}=t;if(0===c.length)return void console.log("\n ✓ No missing variables! Nothing to do.\n");const m=[{name:o.basename(r),dir:r,relativePath:".",missing:c}],u=e.createInterface({input:process.stdin,output:process.stdout});console.log(""),console.log(" 🛡 dotenv-audit interactive mode"),console.log(` Found ${c.length} missing environment variables`),console.log("");const h=await n(u," ? Generate ENV_SETUP.md file with all missing variables? (yes/no): ");if("yes"===h||"y"===h){const e=generateMissingEnvMd(m,r,!1),n=o.join(r,"ENV_SETUP.md");s.writeFileSync(n,e,"utf-8"),console.log(`\n ✓ Created ENV_SETUP.md with ${c.length} variables`),console.log(`  ${n}`)}console.log("");const p=o.join(r,".env");let g;if(g=s.existsSync(p)?await n(u," ? .env file exists. Add missing variables to it? (yes/no): "):await n(u," ? No .env file found. Create new .env file with missing variables? (yes/no): "),"yes"===g||"y"===g){const e=writeEnvForService(r,c,null);if("skipped"===e.action)console.log(`\n ! ${e.reason}`),console.log("  Check your .env file for empty values\n");else{const s="created"===e.action?"Created":"Updated";console.log(`\n ✓ ${s} .env with ${e.count} variables`),console.log(`  ${e.path}\n`)}}else console.log("\n Okay, no changes made.\n");u.close()}async function i(t,i){const a=scanMonorepo(t,i),r=[];let l=0;for(const[e,s]of a.services){const e={variables:s.variables},o=validate(e,{strict:i.strict||!1,required:i.required||[],optional:i.optional||[],ignore:i.ignoreVars||[]});o.missing.length>0&&(r.push({name:s.name,dir:s.dir,relativePath:s.relativePath||".",isRoot:s.isRoot||!1,missing:o.missing,stats:o.stats}),l+=o.missing.length)}const c=e.createInterface({input:process.stdin,output:process.stdout});if(console.log(""),console.log(" 🛡 dotenv-audit interactive mode (monorepo detected)"),console.log(` Scanned ${a.totalFiles} files across ${a.services.size} services`),console.log(""),0===l)return console.log(" ✓ No missing variables across any service!\n"),void c.close();console.log(" Services with missing variables:"),console.log("");for(const e of r){const s=e.isRoot?".":e.relativePath;console.log(` ✗ ${e.name} (${s}/) — ${e.missing.length} missing`)}console.log(""),console.log(` Total: ${l} missing across ${r.length} services`),console.log("");const m=await n(c," ? Generate ENV_SETUP.md with all missing variables (service-wise)? (yes/no): ");if("yes"===m||"y"===m){const e=generateMissingEnvMd(r,t,!0),n=o.join(t,"ENV_SETUP.md");s.writeFileSync(n,e,"utf-8"),console.log(`\n ✓ Created ENV_SETUP.md with ${l} variables (${r.length} services)`),console.log(`  ${n}`)}console.log("");const u=await n(c," ? Create/update .env file inside each service folder? (yes/no): ");if("yes"===u||"y"===u){console.log("");for(const e of r){const s=writeEnvForService(e.dir,e.missing,e.name),o=e.isRoot?"(root)":e.relativePath;"skipped"===s.action?console.log(` ─ ${o}/.env — skipped (${s.reason})`):"created"===s.action?console.log(` ✓ ${o}/.env — created with ${s.count} variables`):console.log(` ✓ ${o}/.env — added ${s.count} missing variables`)}console.log(""),console.log(` ✓ Done! ${r.length} service .env files processed`),console.log("")}else console.log("\n Okay, no changes made.\n");c.close()}module.exports={runInteractive:runInteractive,generateMissingEnvMd:generateMissingEnvMd,generateEnvContent:generateEnvContent,appendToEnv:appendToEnv,writeEnvForService:writeEnvForService};
@@ -0,0 +1 @@
1
+ "use strict";const e=require("fs"),s=require("path"),_=new Set(["DEV","PROD","SSR","MODE","BASE_URL","HEADED","SLOW_MO","CI","NODE_ENV","NODE_DEBUG"]);function detectDatabase(_){const n=s.join(_,"package.json");if(!e.existsSync(n))return"unknown";try{const u=JSON.parse(e.readFileSync(n,"utf-8")),i={...u.dependencies,...u.devDependencies};if(i.mongoose||i.mongodb)return"mongodb";if(i.pg||i.postgres||i["@neondatabase/serverless"])return"postgres";if(i.mysql||i.mysql2)return"mysql";if(i["better-sqlite3"]||i.sqlite3)return"sqlite";if(i.prisma||i["@prisma/client"]){const n=s.join(_,"prisma","schema.prisma");if(e.existsSync(n)){const s=e.readFileSync(n,"utf-8");if(s.includes("mongodb"))return"mongodb";if(s.includes("mysql"))return"mysql";if(s.includes("sqlite"))return"sqlite"}return"postgres"}return i.typeorm||i.knex||i.objection||i["drizzle-orm"]?"postgres":"unknown"}catch{return"unknown"}}function detectDatabaseFromDir(e){let _=e;for(let e=0;e<5;e++){const e=detectDatabase(_);if("unknown"!==e)return e;const n=s.dirname(_);if(n===_)break;_=n}return"unknown"}function n(e){switch(e){case"mongodb":return"mongodb://localhost:27017/your_database_name";case"mysql":return"mysql://user:password@localhost:3306/your_database_name";case"sqlite":return"file:./dev.db";case"postgres":return"postgresql://user:password@localhost:5432/your_database_name";default:return"your_database_connection_string_here"}}function u(e){switch(e){case"mongodb":return"27017";case"mysql":return"3306";default:return"5432"}}function getPlaceholder(e,s={}){const _=e.toUpperCase(),i=s.db||"unknown";return"DATABASE_URL"===_||"DB_URL"===_?n(i):"MONGODB_URI"===_||"MONGO_URI"===_||"MONGO_URL"===_?"mongodb://localhost:27017/your_database_name":"REDIS_URL"===_||"REDIS_CONNECTION_STRING"===_?"redis://localhost:6379":"CONNECTION_STRING"===_?n(i):"DB_HOST"===_||"DATABASE_HOST"===_?"localhost":"DB_PORT"===_||"DATABASE_PORT"===_?u(i):"DB_NAME"===_||"DATABASE_NAME"===_||"DB_DATABASE"===_?"your_database_name":"DB_USER"===_||"DB_USERNAME"===_||"DATABASE_USER"===_?"your_db_user":"DB_PASSWORD"===_||"DB_PASS"===_||"DATABASE_PASSWORD"===_?"your_db_password":_.includes("REDIS_HOST")?"localhost":_.includes("REDIS_PORT")?"6379":_.includes("REDIS_PASSWORD")?"your_redis_password":"JWT_SECRET"===_||"TOKEN_SECRET"===_||"SESSION_SECRET"===_?"your_jwt_secret_key_min_32_chars_long":"JWT_EXPIRY"===_||"JWT_EXPIRES_IN"===_||"TOKEN_EXPIRY"===_?"7d":"REFRESH_TOKEN_EXPIRY"===_||"REFRESH_TOKEN_EXPIRY_DAYS"===_?"30d":"ACCESS_TOKEN_EXPIRY"===_?"15m":"OPENAI_API_KEY"===_?"sk-your_openai_api_key_here":"ANTHROPIC_API_KEY"===_?"sk-ant-your_anthropic_api_key_here":"GOOGLE_GENERATIVE_AI_API_KEY"===_?"your_google_ai_api_key_here":"STRIPE_KEY"===_||"STRIPE_SECRET_KEY"===_?"sk_test_your_stripe_secret_key_here":"STRIPE_PUBLISHABLE_KEY"===_?"pk_test_your_stripe_publishable_key_here":"STRIPE_WEBHOOK_SECRET"===_?"whsec_your_stripe_webhook_secret_here":_.includes("WORKOS_API_KEY")?"sk_your_workos_api_key_here":_.includes("WORKOS_CLIENT_ID")?"client_your_workos_client_id":_.includes("WORKOS_CLIENT_SECRET")?"your_workos_client_secret_here":_.includes("WORKOS")&&_.includes("CONNECTION_ID")?"conn_your_workos_connection_id":_.includes("WORKOS")&&_.includes("ORGANIZATION_ID")?"org_your_workos_organization_id":"AWS_ACCESS_KEY_ID"===_?"your_aws_access_key_id":"AWS_SECRET_ACCESS_KEY"===_?"your_aws_secret_access_key":"AWS_REGION"===_?"us-east-1":"AWS_S3_BUCKET_NAME"===_||"S3_BUCKET"===_?"your-s3-bucket-name":"R2_ACCOUNT_ID"===_?"your_cloudflare_account_id":"R2_ACCESS_KEY_ID"===_?"your_r2_access_key_id":"R2_SECRET_ACCESS_KEY"===_?"your_r2_secret_access_key":_.includes("R2_BUCKET_NAME")||_.includes("R2_PUBLIC_BUCKET_NAME")?"your-r2-bucket-name":_.includes("R2_PUBLIC_BUCKET_URL")?"https://pub-xxxx.r2.dev":_.includes("R2")&&_.includes("EXPIRY")?"3600":_.includes("PUBSUB_TOPIC")?"projects/your-project/topics/your-topic-name":_.includes("GMAIL_CLIENT_ID")||_.includes("EMAIL")&&_.includes("CLIENT_ID")?"your_google_client_id.apps.googleusercontent.com":_.includes("GMAIL_CLIENT_SECRET")||_.includes("EMAIL")&&_.includes("CLIENT_SECRET")?"your_google_client_secret":_.includes("GMAIL_REDIRECT")||_.includes("EMAIL")&&_.includes("REDIRECT")?"http://localhost:3000/auth/google/callback":"BASE_URL"===_||"APP_URL"===_||"SITE_URL"===_||_.includes("BACKEND_URL")||_.includes("BACKEND_SERVER_URL")?"http://localhost:3000":_.includes("API_URL")&&!_.includes("KEY")?"http://localhost:3000/api":_.includes("CLIENT_URL")||_.includes("FRONTEND_URL")?"http://localhost:5173":_.includes("SOCKET_URL")||_.includes("SOCKET_SERVER_URL")?"http://localhost:3001":_.includes("WEBHOOK_URL")?"https://your-domain.com/webhooks":_.includes("CALLBACK_URL")||_.includes("REDIRECT_URI")?"http://localhost:3000/auth/callback":"PORT"===_||_.includes("_PORT")?"3000":"HOST"===_||"HOSTNAME"===_?"localhost":"NODE_ENV"===_?"development":_.includes("SMTP_HOST")||_.includes("MAIL_HOST")?"smtp.gmail.com":_.includes("SMTP_PORT")||_.includes("MAIL_PORT")?"587":_.includes("SMTP_USER")||_.includes("MAIL_USER")||_.includes("EMAIL_USER")?"your_email@gmail.com":_.includes("SMTP_PASS")||_.includes("MAIL_PASS")||_.includes("EMAIL_PASS")?"your_email_app_password":_.includes("EMAIL")&&_.includes("DOMAIN")?"your-domain.com":_.includes("INBOUND_EMAIL")?"your-inbound-domain.com":_.includes("CLIENT_ID")&&_.includes("GOOGLE")?"your_google_client_id.apps.googleusercontent.com":_.includes("CLIENT_SECRET")&&_.includes("GOOGLE")?"your_google_client_secret":_.includes("CLIENT_ID")?"your_client_id_here":_.includes("CLIENT_SECRET")?"your_client_secret_here":_.includes("AXIOM_TOKEN")?"your_axiom_api_token":_.includes("AXIOM_DATASET")?"your_axiom_dataset_name":_.includes("SENTRY_DSN")?"https://your_key@sentry.io/your_project_id":_.includes("OTEL")&&_.includes("SERVICE_NAME")?"your-service-name":_.includes("OTEL")&&(_.includes("DEBUG")||_.includes("ENABLED")||_.includes("TRACING"))||"DEBUG"===_||_.includes("ENABLE_")||_.includes("_ENABLED")||_.includes("SUPPRESS")||_.includes("DISABLE")||_.includes("PROCESS_LOCAL")||_.includes("HEADED")?"false":_.includes("TEST_USER_EMAIL")||_.includes("TEST_EMAIL")?"test@example.com":_.includes("TEST_USER_PASSWORD")||_.includes("TEST_PASSWORD")?"your_test_password":_.includes("SLOW_MO")?"0":_.includes("TIMEOUT")||_.includes("_MS")||_.includes("WAIT")?"30000":_.includes("INTERVAL")&&_.includes("MINUTE")?"60":_.includes("INTERVAL")&&_.includes("HOUR")?"24":_.includes("BATCH_SIZE")||_.includes("MAX_")||_.includes("LIMIT")?"100":_.includes("DUMP_INTERVAL")?"60":_.includes("CRON")?"0 * * * *":_.includes("SECRET")?"your_secret_key_here":_.includes("TOKEN")?"your_token_here":_.includes("API_KEY")||_.includes("APIKEY")?"your_api_key_here":_.includes("KEY")?"your_key_here":_.includes("PASSWORD")||_.includes("PASS")?"your_password_here":_.includes("URL")||_.includes("URI")||_.includes("ENDPOINT")?"https://your-url-here.com":_.includes("DOMAIN")?"your-domain.com":_.includes("RETAILER_ID")||_.includes("ORGANIZATION_ID")||_.includes("ID")?"your_id_here":_.includes("NAME")?"your_name_here":_.includes("EMAIL")?"your_email@example.com":_.includes("PATH")||_.includes("DIR")?"./your/path/here":_.includes("VERSION")?"1.0.0":_.includes("LEVEL")?"info":"your_value_here"}function isFrameworkBuiltin(e,s){return!("vite_env"!==s||!_.has(e))||"DEV"===e||"PROD"===e||"SSR"===e||"MODE"===e}function getCategory(e){const s=e.toUpperCase();return s.includes("DB")||s.includes("DATABASE")||s.includes("MONGO")||s.includes("REDIS")||s.includes("CONNECTION")?"Database":s.includes("JWT")||s.includes("AUTH")||s.includes("WORKOS")||s.includes("SESSION")?"Authentication":s.includes("AWS")||s.includes("S3")?"AWS":s.includes("R2")?"Cloudflare R2":s.includes("STRIPE")?"Stripe":s.includes("OPENAI")||s.includes("ANTHROPIC")||s.includes("GOOGLE_GENERATIVE")?"AI / LLM":s.includes("SMTP")||s.includes("MAIL")||s.includes("EMAIL")&&!s.includes("INBOUND")?"Email":s.includes("GMAIL")||s.includes("PUBSUB")?"Google / Gmail":s.includes("SOCKET")||s.includes("WEBCHAT")?"WebSocket":s.includes("OTEL")||s.includes("AXIOM")||s.includes("SENTRY")||s.includes("TELEMETRY")?"Telemetry / Monitoring":s.includes("VITE_")||s.includes("NEXT_PUBLIC_")?"Client / Frontend":"PORT"===s||"HOST"===s||"NODE_ENV"===s||"BASE_URL"===s||s.includes("BACKEND")||s.includes("SERVER")?"Server":s.includes("CRON")||s.includes("INTERVAL")||s.includes("SCHEDULE")?"Cron / Scheduling":s.includes("TEST")?"Testing":s.includes("TOKEN")&&!s.includes("JWT")?"Authentication":"Other"}module.exports={getPlaceholder:getPlaceholder,getCategory:getCategory,isFrameworkBuiltin:isFrameworkBuiltin,detectDatabase:detectDatabase,detectDatabaseFromDir:detectDatabaseFromDir,FRAMEWORK_BUILTINS:_};
@@ -0,0 +1 @@
1
+ "use strict";const e={reset:"",bold:"",dim:"",red:"",green:"",yellow:"",blue:"",magenta:"",cyan:"",white:"",bgRed:"",bgGreen:"",bgYellow:""},s={cross:"win32"===process.platform?"x":"✗",check:"win32"===process.platform?"√":"✓",warn:"win32"===process.platform?"!":"⚠",info:"win32"===process.platform?"i":"ℹ",arrow:"win32"===process.platform?">":"→",shield:"win32"===process.platform?"#":"🛡",dot:"win32"===process.platform?".":"·"};function t(e,s){const t=`┌${"─".repeat(s-2)}┐`,o=`└${"─".repeat(s-2)}┘`;return[t,...e.map(e=>{const t=n(e),o=Math.max(0,s-4-t.length);return`│ ${e}${" ".repeat(o)} │`}),o].join("\n")}function n(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}function formatReport(t,n,o={}){const{missing:r,empty:i,set:$,warnings:a,stats:l}=t,u=o.noColor||!1,p=o.verbose||!1,m=u?Object.fromEntries(Object.keys(e).map(e=>[e,""])):e,h=[],f=Math.min(process.stdout.columns||80,80);if(h.push(""),h.push(`${m.bold}${m.cyan} ${s.shield} dotenv-audit${m.reset} ${m.dim}v1.0.0${m.reset}`),h.push(`${m.dim} ${"─".repeat(f-4)}${m.reset}`),h.push(`${m.dim} Scanned ${n.totalFiles} files ${s.dot} Found ${l.total} env variables${m.reset}`),h.push(""),r.length>0){h.push(` ${m.red}${m.bold}${s.cross} MISSING (${r.length})${m.reset}`),h.push("");for(const e of r){const t=e.usages&&e.usages.length>0?`${m.dim}${e.usages[0].file}:${e.usages[0].line}${m.reset}`:`${m.dim}(from config)${m.reset}`;if(h.push(` ${m.red}${s.cross}${m.reset} ${m.bold}${e.name}${m.reset}`),h.push(` ${s.arrow} ${t}`),p&&e.usages&&e.usages.length>1)for(let t=1;t<e.usages.length;t++)h.push(` ${s.arrow} ${m.dim}${e.usages[t].file}:${e.usages[t].line}${m.reset}`)}h.push("")}if(i.length>0){h.push(` ${m.yellow}${m.bold}${s.warn} EMPTY (${i.length})${m.reset}`),h.push("");for(const e of i){const t=e.usages&&e.usages.length>0?`${m.dim}${e.usages[0].file}:${e.usages[0].line}${m.reset}`:"";h.push(` ${m.yellow}${s.warn}${m.reset} ${m.bold}${e.name}${m.reset} ${t}`)}h.push("")}if(a.length>0&&p){h.push(` ${m.yellow}${s.info} WARNINGS (${a.length})${m.reset}`),h.push("");for(const e of a)h.push(` ${m.yellow}${s.dot}${m.reset} ${m.dim}${e.message}${m.reset}`);h.push("")}else a.length>0&&(h.push(` ${m.dim}${s.info} ${a.length} warning(s) — use --verbose to see details${m.reset}`),h.push(""));if($.length>0){if(h.push(` ${m.green}${s.check} SET (${$.length})${m.reset}`),p){h.push("");for(const e of $)h.push(` ${m.green}${s.check}${m.reset} ${m.dim}${e.name}${m.reset}`)}h.push("")}if(h.push(` ${m.dim}${"─".repeat(f-4)}${m.reset}`),0===l.missing&&0===l.empty)h.push(` ${m.green}${m.bold}${s.check} All environment variables are set!${m.reset}`);else{const e=[];l.missing>0&&e.push(`${m.red}${l.missing} missing${m.reset}`),l.empty>0&&e.push(`${m.yellow}${l.empty} empty${m.reset}`),e.push(`${m.green}${l.set} set${m.reset}`),h.push(` ${e.join(` ${m.dim}${s.dot}${m.reset} `)}`)}return h.push(""),h.join("\n")}function generateEnvExample(e){const{variables:s}=e,t=["# Auto-generated by dotenv-audit",`# Generated on ${(new Date).toISOString()}`,""],n=new Map;for(const[e,t]of s){if(t.isDynamic||t.onlyInExample)continue;const s=t.usages.length>0?t.usages[0].file:"unknown";n.has(s)||n.set(s,[]),n.get(s).push(e)}for(const[e,s]of n){t.push(`# ${e}`);for(const e of s.sort())t.push(`${e}=`);t.push("")}return t.join("\n")}function formatJSON(e,s){const t=[];for(const s of e.missing)t.push({name:s.name,status:"missing",locations:(s.usages||[]).map(e=>({file:e.file,line:e.line}))});for(const s of e.empty)t.push({name:s.name,status:"empty",locations:(s.usages||[]).map(e=>({file:e.file,line:e.line}))});for(const s of e.set)t.push({name:s.name,status:"set",locations:(s.usages||[]).map(e=>({file:e.file,line:e.line}))});return JSON.stringify({ok:0===e.stats.missing&&0===e.stats.empty,stats:e.stats,variables:t,scannedFiles:s.totalFiles},null,2)}module.exports={formatReport:formatReport,formatJSON:formatJSON,generateEnvExample:generateEnvExample,stripAnsi:n,ICONS:s};
@@ -0,0 +1 @@
1
+ "use strict";const e=require("fs"),n=require("path"),{isFrameworkBuiltin:isFrameworkBuiltin}=require("./placeholder"),t=[".js",".ts",".jsx",".tsx",".mjs",".cjs",".vue",".svelte",".astro"],s=["node_modules",".git","dist","build","coverage",".next",".nuxt",".output",".vercel",".netlify","vendor","__pycache__",".turbo",".cache"];function collectFiles(a,i={}){const o=i.extensions&&i.extensions.length>0?i.extensions:t,r=i.ignore&&i.ignore.length>0?i.ignore:s,c=i.maxDepth||20,l=[];return function t(s,a){if(a>c)return;let i;try{i=e.readdirSync(s,{withFileTypes:!0})}catch{return}for(const e of i){const i=n.join(s,e.name);if(e.isDirectory())r.includes(e.name)||e.name.startsWith(".")||t(i,a+1);else if(e.isFile()){const t=n.extname(e.name).toLowerCase();o.includes(t)&&l.push(i)}}}(a,0),l}function a(n){try{return e.readFileSync(n,"utf-8")}catch{return null}}function scanFile(e,t){const s=a(e);if(!s)return[];const i=[],o=s.split("\n"),r=n.relative(t,e),c=/process\.env\.([A-Z_][A-Z0-9_]*)/g,l=/process\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g,u=/(?:const|let|var)\s*\{([^}]+)\}\s*=\s*process\.env/g,m=/import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g,f=/process\.env\[([^'"[\]]+)\]/g;for(let e=0;e<o.length;e++){const n=o[e],t=e+1,s=n.trim();if(s.startsWith("//")||s.startsWith("*")||s.startsWith("/*"))continue;let a;for(c.lastIndex=0;null!==(a=c.exec(n));){const e=/\|\||(\?\?)|\?\./.test(n.slice(a.index));i.push({name:a[1],file:r,line:t,pattern:"dot_access",hasDefault:e,raw:s})}for(l.lastIndex=0;null!==(a=l.exec(n));){const e=/\|\||(\?\?)/.test(n.slice(a.index));i.push({name:a[1],file:r,line:t,pattern:"bracket_access",hasDefault:e,raw:s})}for(u.lastIndex=0;null!==(a=u.exec(n));){const e=a[1].split(",").map(e=>e.trim().split(/[=:]/)[0].trim()).filter(e=>/^[A-Z_][A-Z0-9_]*$/.test(e));for(const n of e){const e=a[1].includes(n+" =")||a[1].includes(n+"=");i.push({name:n,file:r,line:t,pattern:"destructure",hasDefault:e,raw:s})}}for(m.lastIndex=0;null!==(a=m.exec(n));)i.push({name:a[1],file:r,line:t,pattern:"vite_env",hasDefault:!1,raw:s});for(f.lastIndex=0;null!==(a=f.exec(n));){const e=a[1].trim();/^['"]/.test(e)||i.push({name:`<dynamic:${e}>`,file:r,line:t,pattern:"dynamic",hasDefault:!1,raw:s,isDynamic:!0})}}return i}function parseEnvExample(e){const t=a(e);if(!t)return[];const s=[],i=t.split("\n");for(let t=0;t<i.length;t++){const a=i[t].trim();if(!a||a.startsWith("#"))continue;const o=a.match(/^([A-Z_][A-Z0-9_]*)\s*=/);if(o){const i=a.slice(o[0].length).trim();s.push({name:o[1],hasExampleValue:i.length>0,line:t+1,source:n.basename(e)})}}return s}function scan(t,s={}){const a=collectFiles(t,s),i=[];for(const e of a){const n=scanFile(e,t);i.push(...n)}const o=[".env.example",".env.sample",".env.template"],r=[];for(const s of o){const a=n.join(t,s);e.existsSync(a)&&r.push(...parseEnvExample(a))}const c=new Map;for(const e of i){if(!e.isDynamic&&isFrameworkBuiltin(e.name,e.pattern))continue;if(e.isDynamic){c.has(e.name)||c.set(e.name,{name:e.name,usages:[],isDynamic:!0,hasDefault:!1,inExample:!1}),c.get(e.name).usages.push(e);continue}c.has(e.name)||c.set(e.name,{name:e.name,usages:[],isDynamic:!1,hasDefault:!1,inExample:!1});const n=c.get(e.name);n.usages.push(e),e.hasDefault&&(n.hasDefault=!0)}for(const e of r)c.has(e.name)?c.get(e.name).inExample=!0:c.set(e.name,{name:e.name,usages:[],isDynamic:!1,hasDefault:!1,inExample:!0,onlyInExample:!0});return{variables:c,totalFiles:a.length,totalUsages:i.length,exampleVars:r}}function detectServices(t,a={}){const i=a.ignore&&a.ignore.length>0?a.ignore:s,o=[];return function s(a,r){if(r>10)return;let c;try{c=e.readdirSync(a,{withFileTypes:!0})}catch{return}for(const l of c){if(!l.isDirectory())continue;if(i.includes(l.name)||l.name.startsWith("."))continue;const c=n.join(a,l.name),u=n.join(c,"package.json");if(e.existsSync(u))try{const s=JSON.parse(e.readFileSync(u,"utf-8"));o.push({name:s.name||l.name,dir:c,relativePath:n.relative(t,c),packageJson:s})}catch{o.push({name:l.name,dir:c,relativePath:n.relative(t,c),packageJson:{}})}s(c,r+1)}}(t,0),o}function isMonorepo(t){const s=n.join(t,"package.json");if(!e.existsSync(s))return!1;try{if(JSON.parse(e.readFileSync(s,"utf-8")).workspaces)return!0}catch{}return!!e.existsSync(n.join(t,"pnpm-workspace.yaml"))||(!!e.existsSync(n.join(t,"lerna.json"))||(!!e.existsSync(n.join(t,"turbo.json"))||detectServices(t).length>=2))}function resolveService(e,t,s){const a=n.relative(t,e),i=[...s].sort((e,n)=>n.relativePath.length-e.relativePath.length);for(const e of i)if(a.startsWith(e.relativePath+n.sep)||a.startsWith(e.relativePath+"/"))return e;return null}function scanMonorepo(t,s={}){const a=detectServices(t,s),i=collectFiles(t,s),o=[];for(const e of i){const n=scanFile(e,t);o.push(...n)}const r=new Map;r.set("root",{name:n.basename(t),dir:t,relativePath:".",variables:new Map,isRoot:!0});for(const e of a)r.set(e.relativePath,{...e,variables:new Map,isRoot:!1});for(const e of o){if(!e.isDynamic&&isFrameworkBuiltin(e.name,e.pattern))continue;const s=resolveService(n.join(t,e.file),t,a),i=s?s.relativePath:"root";if(!r.has(i))continue;const o=r.get(i).variables;if(e.isDynamic){o.has(e.name)||o.set(e.name,{name:e.name,usages:[],isDynamic:!0,hasDefault:!1,inExample:!1}),o.get(e.name).usages.push(e);continue}o.has(e.name)||o.set(e.name,{name:e.name,usages:[],isDynamic:!1,hasDefault:!1,inExample:!1});const c=o.get(e.name);c.usages.push(e),e.hasDefault&&(c.hasDefault=!0)}const c=[".env.example",".env.sample",".env.template"];for(const[,t]of r)for(const s of c){const a=n.join(t.dir,s);if(e.existsSync(a)){const e=parseEnvExample(a);for(const n of e)t.variables.has(n.name)&&(t.variables.get(n.name).inExample=!0)}}for(const[e,n]of r)0===n.variables.size&&r.delete(e);return{services:r,totalFiles:i.length,totalUsages:o.length,isMonorepo:a.length>0}}module.exports={scan:scan,scanFile:scanFile,collectFiles:collectFiles,parseEnvExample:parseEnvExample,detectServices:detectServices,isMonorepo:isMonorepo,resolveService:resolveService,scanMonorepo:scanMonorepo,DEFAULT_EXTENSIONS:t,DEFAULT_IGNORE:s};
@@ -0,0 +1 @@
1
+ "use strict";function validate(s,t={}){const{variables:n}=s,e=t.strict||!1,i=t.required||[],a=t.optional||[],o=t.ignore||[],r={missing:[],empty:[],set:[],warnings:[],ignored:[],stats:{total:0,missing:0,empty:0,set:0,warnings:0,ignored:0}};for(const[s,t]of n){if(o.includes(s)){r.ignored.push({...t,reason:"ignored by config"}),r.stats.ignored++;continue}if(t.isDynamic){r.warnings.push({...t,severity:"info",message:`Dynamic env access detected: ${s} — cannot validate statically`}),r.stats.warnings++;continue}if(t.onlyInExample){r.warnings.push({...t,severity:"info",message:`${s} is in .env.example but not referenced in code (possibly unused)`}),r.stats.warnings++;continue}r.stats.total++;const n=process.env[s];void 0!==n?""!==n.trim()?(r.set.push(t),r.stats.set++):e?(r.empty.push(t),r.stats.empty++):(r.warnings.push({...t,severity:"warn",message:`${s} is set but empty`}),r.stats.warnings++):a.includes(s)?(r.warnings.push({...t,severity:"info",message:`${s} is not set (marked optional)`}),r.stats.warnings++):t.hasDefault&&!e?(r.warnings.push({...t,severity:"warn",message:`${s} is not set but has a fallback default in code`}),r.stats.warnings++):(r.missing.push(t),r.stats.missing++)}for(const s of i)n.has(s)||o.includes(s)||void 0===process.env[s]&&(r.missing.push({name:s,usages:[],source:"required_config",hasDefault:!1}),r.stats.missing++,r.stats.total++);return r}function isValid(s,t={}){const n=validate(s,t);return 0===n.stats.missing&&0===n.stats.empty}module.exports={validate:validate,isValid:isValid};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ "use strict";const e=require("path"),o=require("fs"),{scan:scan}=require("../dist/scanner"),{validate:validate}=require("../dist/validator"),{formatReport:formatReport,formatJSON:formatJSON,generateEnvExample:generateEnvExample}=require("../dist/reporter"),{loadConfig:loadConfig,detectFramework:detectFramework}=require("../dist/config"),{runInteractive:runInteractive}=require("../dist/interactive"),n=process.argv.slice(2),s={help:n.includes("--help")||n.includes("-h"),version:n.includes("--version")||n.includes("-v"),verbose:n.includes("--verbose")||n.includes("-V"),strict:n.includes("--strict")||n.includes("-s"),json:n.includes("--json"),noColor:n.includes("--no-color"),genExample:n.includes("--gen-example")||n.includes("-g"),init:n.includes("--init"),ci:n.includes("--ci"),ask:n.includes("--ask")},t=n.filter(e=>!e.startsWith("-")),i=t[0];if(s.help&&(console.log('\n dotenv-audit - Auto-detect and validate environment variables\n\n USAGE\n $ dotenv-audit [directory] [options]\n $ dotenv-audit scan [directory] Scan and validate (default)\n $ dotenv-audit audit [directory] List all detected env variables\n $ dotenv-audit gen [directory] Generate .env.example file\n $ dotenv-audit init Create config file\n\n OPTIONS\n -h, --help Show this help\n -v, --version Show version\n -V, --verbose Show detailed output\n -s, --strict Treat empty vars as errors\n -g, --gen-example Generate .env.example\n --ask Interactive mode: asks what to do with missing vars\n --json Output as JSON\n --no-color Disable colors\n --ci CI mode: exit code 1 on missing vars\n\n CONFIG\n Add to package.json:\n "dotenv-audit": { "required": ["DB_HOST"], "optional": ["DEBUG"] }\n\n Or create .dotenv-auditrc / dotenv-audit.config.js\n\n EXAMPLES\n $ dotenv-audit # Scan current directory\n $ dotenv-audit ./src --strict # Scan src/ in strict mode\n $ dotenv-audit --json --ci # JSON output, fail on missing (CI)\n $ dotenv-audit gen # Generate .env.example\n $ dotenv-audit audit --verbose # List all env vars with details\n $ dotenv-audit --ask # Interactive mode\n'),process.exit(0)),s.version){const e=require("../package.json");console.log(`dotenv-audit v${e.version}`),process.exit(0)}if(s.init||"init"===i){const n=e.join(process.cwd(),".dotenv-auditrc");o.existsSync(n)&&(console.log(" .dotenv-auditrc already exists!"),process.exit(0));const s={strict:!1,required:[],optional:[],ignoreVars:[],ignore:[],verbose:!1};o.writeFileSync(n,JSON.stringify(s,null,2)+"\n"),console.log(" Created .dotenv-auditrc with default config"),process.exit(0)}let r=process.cwd();const a=["scan","audit","gen"],c=a.includes(i)?i:"scan";a.includes(i)&&t[1]?r=e.resolve(t[1]):!a.includes(i)&&t[0]&&(r=e.resolve(t[0])),o.existsSync(r)||(console.error(` Error: directory not found: ${r}`),process.exit(1));const l=loadConfig(r);if(s.verbose&&(l.verbose=!0),s.strict&&(l.strict=!0),s.json&&(l.format="json"),s.noColor&&(l.noColor=!0),s.genExample||"gen"===c){const n=scan(r,l),s=generateEnvExample(n),t=e.join(r,".env.example");o.writeFileSync(t,s),console.log(` Generated ${t}`),console.log(` Found ${n.variables.size} environment variables`),process.exit(0)}if("audit"===c){const e=scan(r,l),o=detectFramework(r);console.log(""),console.log(` dotenv-audit audit (${o})`),console.log(` Scanned ${e.totalFiles} files`),console.log("");const n=[...e.variables.entries()].filter(([,e])=>!e.isDynamic).sort(([e],[o])=>e.localeCompare(o));for(const[e,o]of n){const n=void 0!==process.env[e]?"✓":"✗",t=o.usages.map(e=>`${e.file}:${e.line}`).join(", "),i=o.hasDefault?" (has default)":"";console.log(` ${n} ${e}${i}`),s.verbose&&console.log(` ${t}`)}const t=[...e.variables.entries()].filter(([,e])=>e.isDynamic);if(t.length>0){console.log(""),console.log(` ⚠ Dynamic access (${t.length}):`);for(const[e,o]of t){const n=o.usages[0];console.log(` ${e} at ${n.file}:${n.line}`)}}console.log(""),console.log(` Total: ${n.length} variables`),console.log(""),process.exit(0)}const d=scan(r,l),u=validate(d,{strict:l.strict,required:l.required,optional:l.optional,ignore:l.ignoreVars});"json"===l.format||s.json?console.log(formatJSON(u,d)):console.log(formatReport(u,d,{noColor:l.noColor,verbose:l.verbose})),s.ask?runInteractive(u,d,r,l).then(()=>{s.ci&&u.stats.missing>0&&process.exit(1)}):s.ci&&u.stats.missing>0&&process.exit(1);
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "dotenv-audit",
3
+ "version": "1.0.2",
4
+ "description": "Auto-detect and validate environment variables by scanning your codebase. Zero config, zero schema, zero dependencies.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "dotenv-audit": "./dist-bin/env-shield.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node tests/run.js",
11
+ "build": "node scripts/build.js",
12
+ "prepublishOnly": "node scripts/build.js && node tests/run.js",
13
+ "scan": "node bin/env-shield.js"
14
+ },
15
+ "keywords": [
16
+ "env",
17
+ "environment",
18
+ "variables",
19
+ "validator",
20
+ "dotenv",
21
+ "scan",
22
+ "detect",
23
+ "process.env",
24
+ "config",
25
+ "guard",
26
+ "check",
27
+ "missing",
28
+ "production",
29
+ "monorepo",
30
+ "typescript",
31
+ "vite",
32
+ "nextjs",
33
+ "env-file",
34
+ "auto-detect"
35
+ ],
36
+ "author": "Akash Gupta",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": ""
41
+ },
42
+ "homepage": "",
43
+ "bugs": {
44
+ "url": ""
45
+ },
46
+ "engines": {
47
+ "node": ">=14.0.0"
48
+ },
49
+ "files": [
50
+ "dist/",
51
+ "dist-bin/",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ "devDependencies": {
56
+ "terser": "^5.46.1"
57
+ }
58
+ }