env-example-doctor 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Ali
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 CHANGED
@@ -1,3 +1,110 @@
1
- # Temporary Holding Version
1
+ # env-doctor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **npm package:** [`dotenv-doctor`](https://www.npmjs.com/package/dotenv-doctor) — the unscoped name `env-doctor` is blocked by npm (too similar to [`envdoctor`](https://www.npmjs.com/package/envdoctor)). The CLI command is still **`env-doctor`**.
4
+
5
+ Source: [github.com/maliuppal/env-doctor](https://github.com/maliuppal/env-doctor)
6
+
7
+ Keep **`.env.example` in sync with your code**—find env vars used in source but missing from the example file, and keys documented but never used. **CI exit codes**, `--suggest` to fix drift. Supports `process.env`, `process.env['KEY']`, Vite `import.meta.env`, and `const { X } = process.env`. Zero runtime dependencies.
8
+
9
+ ```bash
10
+ npm install dotenv-doctor
11
+ npx env-doctor . --fail-on missing
12
+ ```
13
+
14
+ ## Example output
15
+
16
+ ```text
17
+ Env file: /app/.env.example
18
+ Used in code: 4
19
+ In .env.example: 2
20
+
21
+ Missing in .env.example:
22
+ - DATABASE_URL
23
+ - VITE_API_BASE
24
+
25
+ Unused in code:
26
+ - LEGACY_FLAG
27
+ ```
28
+
29
+ Exit code **1** when `--fail-on` matches (ideal for GitHub Actions and pre-commit).
30
+
31
+ ## CLI examples
32
+
33
+ ```bash
34
+ # Fail CI if code references vars not in .env.example
35
+ env-doctor . --fail-on missing
36
+
37
+ # Only warn on stale docs, not extra keys in .env.example
38
+ env-doctor . --fail-on missing
39
+
40
+ # Skip tests and vendor trees
41
+ env-doctor . --fail-on missing --ignore '**/*.test.ts' 'vendor/**'
42
+
43
+ # Custom template file
44
+ env-doctor . --env-example .env.sample
45
+
46
+ # Print KEY= lines to paste into .env.example
47
+ env-doctor . --fail-on missing --suggest
48
+
49
+ # Machine-readable report for dashboards
50
+ env-doctor . --json
51
+ ```
52
+
53
+ ### `--fail-on` modes
54
+
55
+ | Mode | Fails when |
56
+ | --- | --- |
57
+ | `missing` | Code uses a var not listed in `.env.example` |
58
+ | `unused` | `.env.example` lists a var never referenced in code |
59
+ | `any` | Either mismatch (default) |
60
+
61
+ ## Library examples
62
+
63
+ ```ts
64
+ import { analyzeEnv, shouldFail, suggestEnvExampleLines } from 'dotenv-doctor';
65
+
66
+ const report = await analyzeEnv({
67
+ root: process.cwd(),
68
+ envExamplePath: '.env.example',
69
+ ignore: ['**/*.test.ts'],
70
+ });
71
+
72
+ if (shouldFail(report, 'missing')) {
73
+ console.error('Missing:', report.missingInExample);
74
+ process.exit(1);
75
+ }
76
+
77
+ // Copy-paste into .env.example
78
+ console.log(suggestEnvExampleLines(report.missingInExample).join('\n'));
79
+ ```
80
+
81
+ ```ts
82
+ import { scanSourceForEnv, parseEnvExample } from 'dotenv-doctor';
83
+
84
+ scanSourceForEnv("const u = process.env['API_URL']");
85
+ // => Set { 'API_URL' }
86
+
87
+ parseEnvExample('API_URL=\n# comment\nDEBUG=false\n');
88
+ // => Set { 'API_URL', 'DEBUG' }
89
+ ```
90
+
91
+ **Note:** Dynamic keys like `process.env[name]` are not detected—use dot, bracket, or destructuring with literal names.
92
+
93
+ ## CI
94
+
95
+ ```yaml
96
+ - uses: actions/checkout@v4
97
+ - uses: actions/setup-node@v4
98
+ with:
99
+ node-version: 22
100
+ - run: npm ci
101
+ working-directory: apps/web
102
+ - run: npx env-doctor . --fail-on missing --ignore '**/*.test.ts'
103
+ working-directory: apps/web
104
+ ```
105
+
106
+ ## Why teams pick this
107
+
108
+ **vs manual grep / dotenv linters:** one pass over real source (including Vite), diff against `.env.example`, non-zero exit in CI, and `--suggest` for quick fixes—no extra SaaS.
109
+
110
+ **Search terms (npm):** env-doctor, dotenv, environment variables, env example, env sync, `.env.example`, `.env.sample`, vite env, import.meta.env, process.env, twelve-factor, devops, github actions, ci, pre-commit, onboarding, typescript, node, cli
package/dist/cli.cjs ADDED
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_node_path4 = __toESM(require("path"), 1);
28
+ var import_node_util = require("util");
29
+
30
+ // src/env-file.ts
31
+ function parseEnvExample(content) {
32
+ const keys = /* @__PURE__ */ new Set();
33
+ for (const line of content.split(/\r?\n/)) {
34
+ const trimmed = line.trim();
35
+ if (!trimmed || trimmed.startsWith("#")) continue;
36
+ const eq = trimmed.indexOf("=");
37
+ const key = (eq === -1 ? trimmed : trimmed.slice(0, eq)).trim();
38
+ if (/^[A-Z_][A-Z0-9_]*$/.test(key)) keys.add(key);
39
+ }
40
+ return keys;
41
+ }
42
+ function formatEnvExampleAppend(keys) {
43
+ if (keys.length === 0) return "";
44
+ const body = keys.map((k) => `${k}=
45
+ `).join("");
46
+ return `
47
+ # suggested by env-doctor
48
+ ${body}`;
49
+ }
50
+
51
+ // src/analyze.ts
52
+ var import_promises2 = require("fs/promises");
53
+ var import_node_path2 = __toESM(require("path"), 1);
54
+
55
+ // src/scan.ts
56
+ var ENV_DOT_RX = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
57
+ var META_DOT_RX = /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g;
58
+ var ENV_BRACKET_RX = /process\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g;
59
+ var META_BRACKET_RX = /import\.meta\.env\[['"]([A-Z_][A-Z0-9_]*)['"]\]/g;
60
+ var ENV_DESTRUCT_RX = /\{\s*([^}]+)\s*\}\s*=\s*process\.env\b/g;
61
+ var KEY_RX = /^[A-Z_][A-Z0-9_]*$/;
62
+ function collectMatches(text, rx, found) {
63
+ rx.lastIndex = 0;
64
+ let m;
65
+ while ((m = rx.exec(text)) !== null) {
66
+ if (m[1]) found.add(m[1]);
67
+ }
68
+ }
69
+ function collectDestructured(text, found) {
70
+ ENV_DESTRUCT_RX.lastIndex = 0;
71
+ let m;
72
+ while ((m = ENV_DESTRUCT_RX.exec(text)) !== null) {
73
+ const inner = m[1];
74
+ if (!inner) continue;
75
+ for (const part of inner.split(",")) {
76
+ const trimmed = part.trim();
77
+ if (!trimmed) continue;
78
+ const head = trimmed.split(/\s*[=:]/)[0]?.trim() ?? "";
79
+ const name = head.replace(/^\.\.\./, "").trim();
80
+ if (KEY_RX.test(name)) found.add(name);
81
+ }
82
+ }
83
+ }
84
+ function scanSourceForEnv(text) {
85
+ const found = /* @__PURE__ */ new Set();
86
+ for (const rx of [ENV_DOT_RX, META_DOT_RX, ENV_BRACKET_RX, META_BRACKET_RX]) {
87
+ collectMatches(text, rx, found);
88
+ }
89
+ collectDestructured(text, found);
90
+ return found;
91
+ }
92
+
93
+ // src/walk.ts
94
+ var import_promises = require("fs/promises");
95
+ var import_node_path = __toESM(require("path"), 1);
96
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "coverage", ".turbo"]);
97
+ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"]);
98
+ function pathIgnored(rel, patterns) {
99
+ const norm = rel.replace(/\\/g, "/");
100
+ return patterns.some((pat) => {
101
+ if (pat.includes("*")) {
102
+ const re = new RegExp(
103
+ "^" + pat.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".") + "$"
104
+ );
105
+ return re.test(norm);
106
+ }
107
+ return norm.includes(pat.replace(/\\/g, "/"));
108
+ });
109
+ }
110
+ async function listSourceFiles(root, ignore = []) {
111
+ const out = [];
112
+ async function walk(dir) {
113
+ const entries = await (0, import_promises.readdir)(dir, { withFileTypes: true });
114
+ for (const ent of entries) {
115
+ const full = import_node_path.default.join(dir, ent.name);
116
+ const rel = import_node_path.default.relative(root, full);
117
+ if (pathIgnored(rel, ignore)) continue;
118
+ if (ent.isDirectory()) {
119
+ if (SKIP_DIRS.has(ent.name)) continue;
120
+ await walk(full);
121
+ } else if (ent.isFile()) {
122
+ const ext = import_node_path.default.extname(ent.name);
123
+ if (SOURCE_EXT.has(ext)) out.push(full);
124
+ }
125
+ }
126
+ }
127
+ const st = await (0, import_promises.stat)(root);
128
+ if (!st.isDirectory()) return out;
129
+ await walk(root);
130
+ return out;
131
+ }
132
+ async function readSources(paths) {
133
+ const map = /* @__PURE__ */ new Map();
134
+ await Promise.all(
135
+ paths.map(async (p) => {
136
+ const text = await (0, import_promises.readFile)(p, "utf8");
137
+ map.set(p, text);
138
+ })
139
+ );
140
+ return map;
141
+ }
142
+
143
+ // src/analyze.ts
144
+ async function analyzeEnv(options = {}) {
145
+ const root = import_node_path2.default.resolve(options.root ?? process.cwd());
146
+ const envPath = import_node_path2.default.resolve(options.envExamplePath ?? import_node_path2.default.join(root, ".env.example"));
147
+ const ignore = options.ignore ?? [];
148
+ let documented = /* @__PURE__ */ new Set();
149
+ let envExampleFound = false;
150
+ try {
151
+ await (0, import_promises2.access)(envPath);
152
+ envExampleFound = true;
153
+ const example = await (0, import_promises2.readFile)(envPath, "utf8");
154
+ documented = parseEnvExample(example);
155
+ } catch {
156
+ documented = /* @__PURE__ */ new Set();
157
+ }
158
+ const files = await listSourceFiles(root, ignore);
159
+ const sources = await readSources(files);
160
+ const used = /* @__PURE__ */ new Set();
161
+ for (const text of sources.values()) {
162
+ for (const k of scanSourceForEnv(text)) used.add(k);
163
+ }
164
+ const missingInExample = [...used].filter((k) => !documented.has(k)).sort();
165
+ const unusedInCode = [...documented].filter((k) => !used.has(k)).sort();
166
+ return {
167
+ usedInCode: [...used].sort(),
168
+ documented: [...documented].sort(),
169
+ missingInExample,
170
+ unusedInCode,
171
+ ok: missingInExample.length === 0 && unusedInCode.length === 0,
172
+ envExamplePath: envPath,
173
+ envExampleFound
174
+ };
175
+ }
176
+ function shouldFail(report, failOn) {
177
+ if (failOn === "missing") return report.missingInExample.length > 0;
178
+ if (failOn === "unused") return report.unusedInCode.length > 0;
179
+ return !report.ok;
180
+ }
181
+
182
+ // src/version.ts
183
+ var import_node_fs = require("fs");
184
+ var import_node_path3 = require("path");
185
+ function readPackageVersion() {
186
+ try {
187
+ const entry = process.argv[1];
188
+ if (!entry) return "0.0.0";
189
+ const pkgPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(entry), "..", "package.json");
190
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf8"));
191
+ if (parsed && typeof parsed === "object" && "version" in parsed) {
192
+ return parsed.version;
193
+ }
194
+ } catch {
195
+ }
196
+ return "0.0.0";
197
+ }
198
+
199
+ // src/cli.ts
200
+ function printHelp() {
201
+ process.stdout.write(`env-doctor \u2014 sync code env usage with .env.example
202
+
203
+ Usage:
204
+ env-doctor [project-dir] [--env-example <file>] [--json] [--ignore <glob>]
205
+ [--fail-on missing|unused|any] [--suggest]
206
+
207
+ Options:
208
+ --env-example Path to .env.example (default: <dir>/.env.example)
209
+ --json JSON report on stdout
210
+ --suggest Print lines to append for missing keys (stdout only)
211
+ --ignore Glob to skip (repeatable)
212
+ --fail-on CI failure mode (default: any)
213
+ -h, --help
214
+ --version
215
+ `);
216
+ }
217
+ function main() {
218
+ const { values, positionals } = (0, import_node_util.parseArgs)({
219
+ options: {
220
+ help: { type: "boolean", short: "h", default: false },
221
+ version: { type: "boolean", default: false },
222
+ json: { type: "boolean", default: false },
223
+ suggest: { type: "boolean", default: false },
224
+ ignore: { type: "string", multiple: true, default: [] },
225
+ "env-example": { type: "string" },
226
+ "fail-on": { type: "string", default: "any" }
227
+ },
228
+ allowPositionals: true
229
+ });
230
+ if (values.version) {
231
+ process.stdout.write(`${readPackageVersion()}
232
+ `);
233
+ return Promise.resolve(0);
234
+ }
235
+ if (values.help) {
236
+ printHelp();
237
+ return Promise.resolve(0);
238
+ }
239
+ const root = import_node_path4.default.resolve(positionals[0] ?? process.cwd());
240
+ const failOn = values["fail-on"];
241
+ const envExamplePath = values["env-example"] ? import_node_path4.default.resolve(String(values["env-example"])) : void 0;
242
+ return analyzeEnv({ root, ignore: values.ignore, envExamplePath }).then((report) => {
243
+ if (values.json) {
244
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
245
+ `);
246
+ } else {
247
+ process.stdout.write(`Env file: ${report.envExamplePath}${report.envExampleFound ? "" : " (not found)"}
248
+ `);
249
+ process.stdout.write(`Used in code: ${String(report.usedInCode.length)}
250
+ `);
251
+ process.stdout.write(`In .env.example: ${String(report.documented.length)}
252
+ `);
253
+ if (report.missingInExample.length) {
254
+ process.stdout.write(`
255
+ Missing in .env.example:
256
+ `);
257
+ for (const k of report.missingInExample) process.stdout.write(` - ${k}
258
+ `);
259
+ }
260
+ if (report.unusedInCode.length) {
261
+ process.stdout.write(`
262
+ Unused in code:
263
+ `);
264
+ for (const k of report.unusedInCode) process.stdout.write(` - ${k}
265
+ `);
266
+ }
267
+ if (report.ok) process.stdout.write("\nAll env keys are documented and used.\n");
268
+ if (values.suggest && report.missingInExample.length) {
269
+ process.stdout.write("\nSuggested append:\n");
270
+ process.stdout.write(formatEnvExampleAppend(report.missingInExample));
271
+ }
272
+ }
273
+ return shouldFail(report, failOn) ? 1 : 0;
274
+ });
275
+ }
276
+ main().then((code) => process.exit(code)).catch((err) => {
277
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
278
+ `);
279
+ process.exit(2);
280
+ });
281
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/env-file.ts","../src/analyze.ts","../src/scan.ts","../src/walk.ts","../src/version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport path from 'node:path';\nimport { parseArgs } from 'node:util';\nimport { formatEnvExampleAppend } from './env-file.js';\nimport { analyzeEnv, shouldFail, type FailOn } from './index.js';\nimport { readPackageVersion } from './version.js';\n\nfunction printHelp(): void {\n process.stdout.write(`env-doctor — sync code env usage with .env.example\n\nUsage:\n env-doctor [project-dir] [--env-example <file>] [--json] [--ignore <glob>]\n [--fail-on missing|unused|any] [--suggest]\n\nOptions:\n --env-example Path to .env.example (default: <dir>/.env.example)\n --json JSON report on stdout\n --suggest Print lines to append for missing keys (stdout only)\n --ignore Glob to skip (repeatable)\n --fail-on CI failure mode (default: any)\n -h, --help\n --version\n`);\n}\n\nfunction main(): Promise<number> {\n const { values, positionals } = parseArgs({\n options: {\n help: { type: 'boolean', short: 'h', default: false },\n version: { type: 'boolean', default: false },\n json: { type: 'boolean', default: false },\n suggest: { type: 'boolean', default: false },\n ignore: { type: 'string', multiple: true, default: [] },\n 'env-example': { type: 'string' },\n 'fail-on': { type: 'string', default: 'any' },\n },\n allowPositionals: true,\n });\n\n if (values.version) {\n process.stdout.write(`${readPackageVersion()}\\n`);\n return Promise.resolve(0);\n }\n if (values.help) {\n printHelp();\n return Promise.resolve(0);\n }\n\n const root = path.resolve(positionals[0] ?? process.cwd());\n const failOn = values['fail-on'] as FailOn;\n\n const envExamplePath = values['env-example']\n ? path.resolve(String(values['env-example']))\n : undefined;\n\n return analyzeEnv({ root, ignore: values.ignore, envExamplePath }).then((report) => {\n if (values.json) {\n process.stdout.write(`${JSON.stringify(report, null, 2)}\\n`);\n } else {\n process.stdout.write(`Env file: ${report.envExamplePath}${report.envExampleFound ? '' : ' (not found)'}\\n`);\n process.stdout.write(`Used in code: ${String(report.usedInCode.length)}\\n`);\n process.stdout.write(`In .env.example: ${String(report.documented.length)}\\n`);\n if (report.missingInExample.length) {\n process.stdout.write(`\\nMissing in .env.example:\\n`);\n for (const k of report.missingInExample) process.stdout.write(` - ${k}\\n`);\n }\n if (report.unusedInCode.length) {\n process.stdout.write(`\\nUnused in code:\\n`);\n for (const k of report.unusedInCode) process.stdout.write(` - ${k}\\n`);\n }\n if (report.ok) process.stdout.write('\\nAll env keys are documented and used.\\n');\n if (values.suggest && report.missingInExample.length) {\n process.stdout.write('\\nSuggested append:\\n');\n process.stdout.write(formatEnvExampleAppend(report.missingInExample));\n }\n }\n return shouldFail(report, failOn) ? 1 : 0;\n });\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n process.exit(2);\n });\n","/**\n * Parse `.env.example` style files (KEY=value, comments and blanks ignored).\n */\nexport function parseEnvExample(content: string): Set<string> {\n const keys = new Set<string>();\n for (const line of content.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const eq = trimmed.indexOf('=');\n const key = (eq === -1 ? trimmed : trimmed.slice(0, eq)).trim();\n if (/^[A-Z_][A-Z0-9_]*$/.test(key)) keys.add(key);\n }\n return keys;\n}\n\n/** Lines to append when keys are missing from `.env.example`. */\nexport function suggestEnvExampleLines(keys: string[]): string[] {\n return keys.map((k) => `${k}=`);\n}\n\nexport function formatEnvExampleAppend(keys: string[]): string {\n if (keys.length === 0) return '';\n const body = keys.map((k) => `${k}=\\n`).join('');\n return `\\n# suggested by env-doctor\\n${body}`;\n}\n","import { access, readFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { parseEnvExample } from './env-file.js';\nimport { scanSourceForEnv } from './scan.js';\nimport { listSourceFiles, readSources } from './walk.js';\n\nexport type FailOn = 'missing' | 'unused' | 'any';\n\nexport interface EnvDoctorOptions {\n root?: string;\n envExamplePath?: string;\n ignore?: string[];\n}\n\nexport interface EnvDoctorReport {\n usedInCode: string[];\n documented: string[];\n missingInExample: string[];\n unusedInCode: string[];\n ok: boolean;\n /** Resolved path to the env example file that was read (or attempted). */\n envExamplePath: string;\n /** False when the env example file does not exist (all keys treated as undocumented). */\n envExampleFound: boolean;\n}\n\n/**\n * Compare env usage in source against `.env.example`.\n */\nexport async function analyzeEnv(options: EnvDoctorOptions = {}): Promise<EnvDoctorReport> {\n const root = path.resolve(options.root ?? process.cwd());\n const envPath = path.resolve(options.envExamplePath ?? path.join(root, '.env.example'));\n const ignore = options.ignore ?? [];\n\n let documented = new Set<string>();\n let envExampleFound = false;\n try {\n await access(envPath);\n envExampleFound = true;\n const example = await readFile(envPath, 'utf8');\n documented = parseEnvExample(example);\n } catch {\n documented = new Set();\n }\n\n const files = await listSourceFiles(root, ignore);\n const sources = await readSources(files);\n const used = new Set<string>();\n for (const text of sources.values()) {\n for (const k of scanSourceForEnv(text)) used.add(k);\n }\n\n const missingInExample = [...used].filter((k) => !documented.has(k)).sort();\n const unusedInCode = [...documented].filter((k) => !used.has(k)).sort();\n\n return {\n usedInCode: [...used].sort(),\n documented: [...documented].sort(),\n missingInExample,\n unusedInCode,\n ok: missingInExample.length === 0 && unusedInCode.length === 0,\n envExamplePath: envPath,\n envExampleFound,\n };\n}\n\nexport function shouldFail(report: EnvDoctorReport, failOn: FailOn): boolean {\n if (failOn === 'missing') return report.missingInExample.length > 0;\n if (failOn === 'unused') return report.unusedInCode.length > 0;\n return !report.ok;\n}\n","const ENV_DOT_RX = /process\\.env\\.([A-Z_][A-Z0-9_]*)/g;\nconst META_DOT_RX = /import\\.meta\\.env\\.([A-Z_][A-Z0-9_]*)/g;\nconst ENV_BRACKET_RX = /process\\.env\\[['\"]([A-Z_][A-Z0-9_]*)['\"]\\]/g;\nconst META_BRACKET_RX = /import\\.meta\\.env\\[['\"]([A-Z_][A-Z0-9_]*)['\"]\\]/g;\nconst ENV_DESTRUCT_RX = /\\{\\s*([^}]+)\\s*\\}\\s*=\\s*process\\.env\\b/g;\n\nconst KEY_RX = /^[A-Z_][A-Z0-9_]*$/;\n\nfunction collectMatches(text: string, rx: RegExp, found: Set<string>): void {\n rx.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = rx.exec(text)) !== null) {\n if (m[1]) found.add(m[1]);\n }\n}\n\nfunction collectDestructured(text: string, found: Set<string>): void {\n ENV_DESTRUCT_RX.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = ENV_DESTRUCT_RX.exec(text)) !== null) {\n const inner = m[1];\n if (!inner) continue;\n for (const part of inner.split(',')) {\n const trimmed = part.trim();\n if (!trimmed) continue;\n const head = trimmed.split(/\\s*[=:]/)[0]?.trim() ?? '';\n const name = head.replace(/^\\.\\.\\./, '').trim();\n if (KEY_RX.test(name)) found.add(name);\n }\n }\n}\n\n/**\n * Extract environment variable names referenced in source text.\n * Supports dot access, bracket access, and simple `const { A, B } = process.env`.\n * Does not resolve dynamic keys (`process.env[name]`).\n */\nexport function scanSourceForEnv(text: string): Set<string> {\n const found = new Set<string>();\n for (const rx of [ENV_DOT_RX, META_DOT_RX, ENV_BRACKET_RX, META_BRACKET_RX]) {\n collectMatches(text, rx, found);\n }\n collectDestructured(text, found);\n return found;\n}\n","import { readdir, readFile, stat } from 'node:fs/promises';\nimport path from 'node:path';\n\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.turbo']);\n\nconst SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte']);\n\nfunction pathIgnored(rel: string, patterns: string[]): boolean {\n const norm = rel.replace(/\\\\/g, '/');\n return patterns.some((pat) => {\n if (pat.includes('*')) {\n const re = new RegExp(\n '^' + pat.replace(/\\*\\*/g, '.*').replace(/\\*/g, '[^/]*').replace(/\\?/g, '.') + '$',\n );\n return re.test(norm);\n }\n return norm.includes(pat.replace(/\\\\/g, '/'));\n });\n}\n\n/**\n * Recursively list source files under `root`, honouring ignore globs (simple `*` / `**`).\n */\nexport async function listSourceFiles(root: string, ignore: string[] = []): Promise<string[]> {\n const out: string[] = [];\n\n async function walk(dir: string): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const ent of entries) {\n const full = path.join(dir, ent.name);\n const rel = path.relative(root, full);\n if (pathIgnored(rel, ignore)) continue;\n if (ent.isDirectory()) {\n if (SKIP_DIRS.has(ent.name)) continue;\n await walk(full);\n } else if (ent.isFile()) {\n const ext = path.extname(ent.name);\n if (SOURCE_EXT.has(ext)) out.push(full);\n }\n }\n }\n\n const st = await stat(root);\n if (!st.isDirectory()) return out;\n await walk(root);\n return out;\n}\n\nexport async function readSources(paths: string[]): Promise<Map<string, string>> {\n const map = new Map<string, string>();\n await Promise.all(\n paths.map(async (p) => {\n const text = await readFile(p, 'utf8');\n map.set(p, text);\n }),\n );\n return map;\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\n\n/** Read version from package.json next to compiled CLI (works for ESM + CJS bins). */\nexport function readPackageVersion(): string {\n try {\n const entry = process.argv[1];\n if (!entry) return '0.0.0';\n const pkgPath = join(dirname(entry), '..', 'package.json');\n const parsed: unknown = JSON.parse(readFileSync(pkgPath, 'utf8'));\n if (parsed && typeof parsed === 'object' && 'version' in parsed) {\n return (parsed as { version: string }).version;\n }\n } catch {\n // ignore\n }\n return '0.0.0';\n}\n"],"mappings":"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","names":["import_node_path","import_promises","import_node_path","path","path","import_node_path","path"]}
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node