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 +21 -0
- package/README.md +317 -0
- package/dist/config.js +1 -0
- package/dist/index.js +1 -0
- package/dist/interactive.js +1 -0
- package/dist/placeholder.js +1 -0
- package/dist/reporter.js +1 -0
- package/dist/scanner.js +1 -0
- package/dist/validator.js +1 -0
- package/dist-bin/env-shield.js +2 -0
- package/package.json +58 -0
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 [32m✓ No missing variables! Nothing to do.[0m\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(" [1m[36m🛡 dotenv-audit interactive mode[0m"),console.log(` [2mFound ${c.length} missing environment variables[0m`),console.log("");const h=await n(u," [1m?[0m Generate [33mENV_SETUP.md[0m file with all missing variables? [2m(yes/no)[0m: ");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 [32m✓[0m Created [1mENV_SETUP.md[0m with ${c.length} variables`),console.log(` [2m ${n}[0m`)}console.log("");const p=o.join(r,".env");let g;if(g=s.existsSync(p)?await n(u," [1m?[0m [33m.env[0m file exists. Add missing variables to it? [2m(yes/no)[0m: "):await n(u," [1m?[0m No .env file found. Create new [33m.env[0m file with missing variables? [2m(yes/no)[0m: "),"yes"===g||"y"===g){const e=writeEnvForService(r,c,null);if("skipped"===e.action)console.log(`\n [33m![0m ${e.reason}`),console.log(" [2m Check your .env file for empty values[0m\n");else{const s="created"===e.action?"Created":"Updated";console.log(`\n [32m✓[0m ${s} [1m.env[0m with ${e.count} variables`),console.log(` [2m ${e.path}[0m\n`)}}else console.log("\n [2mOkay, no changes made.[0m\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(" [1m[36m🛡 dotenv-audit interactive mode[0m [2m(monorepo detected)[0m"),console.log(` [2mScanned ${a.totalFiles} files across ${a.services.size} services[0m`),console.log(""),0===l)return console.log(" [32m✓ No missing variables across any service![0m\n"),void c.close();console.log(" [1mServices with missing variables:[0m"),console.log("");for(const e of r){const s=e.isRoot?".":e.relativePath;console.log(` [31m✗[0m [1m${e.name}[0m [2m(${s}/) — ${e.missing.length} missing[0m`)}console.log(""),console.log(` [2mTotal: ${l} missing across ${r.length} services[0m`),console.log("");const m=await n(c," [1m?[0m Generate [33mENV_SETUP.md[0m with all missing variables (service-wise)? [2m(yes/no)[0m: ");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 [32m✓[0m Created [1mENV_SETUP.md[0m with ${l} variables (${r.length} services)`),console.log(` [2m ${n}[0m`)}console.log("");const u=await n(c," [1m?[0m Create/update [33m.env[0m file [1minside each service folder[0m? [2m(yes/no)[0m: ");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(` [33m─[0m [2m${o}/.env — skipped (${s.reason})[0m`):"created"===s.action?console.log(` [32m✓[0m [1m${o}/.env[0m — created with ${s.count} variables`):console.log(` [32m✓[0m [1m${o}/.env[0m — added ${s.count} missing variables`)}console.log(""),console.log(` [32m✓[0m Done! ${r.length} service .env files processed`),console.log("")}else console.log("\n [2mOkay, no changes made.[0m\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:_};
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const e={reset:"[0m",bold:"[1m",dim:"[2m",red:"[31m",green:"[32m",yellow:"[33m",blue:"[34m",magenta:"[35m",cyan:"[36m",white:"[37m",bgRed:"[41m",bgGreen:"[42m",bgYellow:"[43m"},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};
|
package/dist/scanner.js
ADDED
|
@@ -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(` [1m[36mdotenv-audit audit[0m [2m(${o})[0m`),console.log(` [2mScanned ${e.totalFiles} files[0m`),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]?"[32m✓[0m":"[31m✗[0m",t=o.usages.map(e=>`${e.file}:${e.line}`).join(", "),i=o.hasDefault?" [2m(has default)[0m":"";console.log(` ${n} ${e}${i}`),s.verbose&&console.log(` [2m${t}[0m`)}const t=[...e.variables.entries()].filter(([,e])=>e.isDynamic);if(t.length>0){console.log(""),console.log(` [33m⚠ Dynamic access (${t.length}):[0m`);for(const[e,o]of t){const n=o.usages[0];console.log(` [2m${e} at ${n.file}:${n.line}[0m`)}}console.log(""),console.log(` [2mTotal: ${n.length} variables[0m`),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
|
+
}
|