dev-env-doctor 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [0.1.0] - 2026-10-16
6
+
7
+ ### Added
8
+
9
+ - `ports`: process inspection now includes command line (redacted), parent PID, user, working directory and project association (current project / another location / unknown) on macOS, Linux and Windows, with unavailable fields labelled.
10
+ - `ports`: interactive Inspect / Stop / Skip workflow, with process selection when several ports are occupied, and a focused view for a single port.
11
+ - `ports --kill`: `--pid` for an unambiguous target, re-check before signalling, force only as a separate step while the same process still holds the port, and handling of processes that exit early or ports taken over by another process.
12
+
13
+ - `doctor` (default): full diagnostic report with PASS / WARN / FAIL / INFO statuses, `--verbose`, `--json`, `--ci`, `--fail-on` and `--cwd`.
14
+ - `node`: checks the running Node.js version against `engines.node`, `.nvmrc` and `.node-version`.
15
+ - `packages`: validates package.json, lockfiles, package manager availability and installed dependencies.
16
+ - `env`: compares `.env*` files with `.env.example` by key name and finds `process.env` / `import.meta.env` references in source code. Never prints values.
17
+ - `ports`: shows processes listening on ports (macOS, Linux, Windows adapters) and `--kill` with confirmation, graceful-first termination and release verification.
18
+ - `services`: Docker CLI/daemon checks and PostgreSQL, MongoDB and Redis TCP reachability.
19
+ - `fix`: previews and applies safe fixes (create `.env` from the template with empty values, add `.env` to `.gitignore`, stop a process blocking a project port) and re-verifies them.
20
+ - Optional `.dev-doctor.json` configuration.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sameera_mhk
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,304 @@
1
- # Temporary Holding Version
1
+ # Dev 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](https://img.shields.io/npm/v/dev-env-doctor.svg)](https://www.npmjs.com/package/dev-env-doctor)
4
+ [![CI](https://github.com/Sameera-MHK/dev-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/Sameera-MHK/dev-doctor/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ ![Node.js >= 22.12](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen.svg)
7
+
8
+ Diagnose — and safely fix — the problems that stop a JavaScript or Node.js project from starting: busy ports, the wrong Node.js version, missing dependencies, `.env` files out of sync with `.env.example`, and local services that are not running.
9
+
10
+ - **Offline.** No API key, no telemetry, nothing is uploaded.
11
+ - **Read-only by default.** Changes only happen through `fix` or `ports --kill`, with a preview and confirmation.
12
+ - **No secrets in output.** `.env` files are compared by key name only; command lines are redacted.
13
+ - **Cross-platform.** macOS, Linux and Windows.
14
+
15
+ ```bash
16
+ npx dev-env-doctor
17
+ ```
18
+
19
+ ## Contents
20
+
21
+ - [Installation](#installation)
22
+ - [Example](#example)
23
+ - [Commands](#commands)
24
+ - [Configuration](#configuration)
25
+ - [Exit codes](#exit-codes)
26
+ - [Security](#security)
27
+ - [Supported operating systems](#supported-operating-systems)
28
+ - [Limitations](#limitations)
29
+ - [Development](#development)
30
+ - [Contributing](#contributing)
31
+ - [License](#license)
32
+
33
+ ## Installation
34
+
35
+ Requires Node.js 22.12 or newer.
36
+
37
+ Run it once in any project, without installing:
38
+
39
+ ```bash
40
+ npx dev-env-doctor
41
+ ```
42
+
43
+ Or install it, then use the `dev-doctor` command:
44
+
45
+ ```bash
46
+ npm install -g dev-env-doctor
47
+ dev-doctor
48
+ ```
49
+
50
+ As a project dev dependency (for scripts and CI):
51
+
52
+ ```bash
53
+ npm install -D dev-env-doctor
54
+ npx dev-doctor --ci
55
+ ```
56
+
57
+ From source:
58
+
59
+ ```bash
60
+ git clone https://github.com/Sameera-MHK/dev-doctor.git
61
+ cd dev-doctor
62
+ npm install
63
+ npm link
64
+ ```
65
+
66
+ ## Example
67
+
68
+ ```
69
+ Dev Doctor
70
+ Project: my-react-app (Vite)
71
+
72
+ Project
73
+ PASS package.json is valid (Vite project)
74
+
75
+ Node.js
76
+ INFO Node.js 22.12.0
77
+ INFO npm 11.6.0
78
+ PASS Node.js 22.12.0 satisfies engines.node (>=20)
79
+
80
+ Packages
81
+ WARN Multiple lockfiles detected
82
+ PASS npm 11.6.0 is available
83
+ PASS All 24 declared dependencies are installed
84
+
85
+ Environment variables
86
+ INFO Template: .env.example (6 keys)
87
+ FAIL DATABASE_URL is missing
88
+ Documented in .env.example
89
+ Referenced in src/db.ts
90
+ WARN API_BASE_URL is not documented in .env.example
91
+
92
+ Ports
93
+ FAIL Port 5173 is occupied by node (PID 8421)
94
+
95
+ Services
96
+ PASS Docker daemon is running (27.3.1)
97
+ WARN PostgreSQL is not reachable at localhost:5432
98
+
99
+ Summary
100
+ 2 errors, 3 warnings, 5 passed
101
+
102
+ Recommended next steps:
103
+ 1. Agree on one package manager (likely npm) and remove the other lockfiles yourself.
104
+ 2. Set DATABASE_URL in your local .env file.
105
+ 3. Add API_BASE_URL (without its value) to .env.example.
106
+ 4. Inspect the process using port 5173, or stop it with "dev-doctor ports --kill 5173".
107
+ 5. Verify PostgreSQL is running if this project needs it (e.g. "docker compose up -d").
108
+ ```
109
+
110
+ ## Commands
111
+
112
+ | Command | What it does |
113
+ | --- | --- |
114
+ | `dev-doctor` / `dev-doctor doctor` | Full diagnosis of the current project |
115
+ | `dev-doctor node` | Node.js version vs `engines.node`, `.nvmrc`, `.node-version` |
116
+ | `dev-doctor packages` | package.json, lockfiles, package manager, installed dependencies |
117
+ | `dev-doctor env` | `.env*` files vs `.env.example` and code references (keys only) |
118
+ | `dev-doctor ports [ports...]` | Which processes listen on ports |
119
+ | `dev-doctor ports --kill <port>` | Stop the process on a port, safely |
120
+ | `dev-doctor services` | Docker, PostgreSQL, MongoDB, Redis availability |
121
+ | `dev-doctor fix` | Preview and apply safe fixes |
122
+
123
+ Common options (all diagnostic commands):
124
+
125
+ | Option | Description |
126
+ | --- | --- |
127
+ | `--cwd <path>` | Project directory (default: current directory) |
128
+ | `--json` | JSON on stdout only; errors go to stderr |
129
+ | `-v, --verbose` | Show details for every finding (process command lines, sources) |
130
+ | `--ci` | No prompts, no colors |
131
+ | `--fail-on <fail\|warn>` | Exit 1 on failures only (default) or on warnings too |
132
+ | `--no-color` | Disable colors (also disabled when piped or `NO_COLOR` is set) |
133
+
134
+ ### doctor
135
+
136
+ Checks the project (package.json and project type), Node.js and npm versions, the package manager and lockfiles, dependency installation, environment variables, ports and local services.
137
+
138
+ Ports: if the project declares ports (`.dev-doctor.json`, `PORT` in `.env`, `--port`/`-p` in scripts, or framework defaults such as 5173 for Vite, 3000 for Next.js, 4200 for Angular), a busy port is a **FAIL**. Otherwise common development ports (3000, 3001, 4200, 5000, 5173, 8000, 8080) are checked and a busy one is only a **WARN**.
139
+
140
+ ### env
141
+
142
+ - Reads `.env`, `.env.local`, `.env.development`, `.env.development.local` and the first template found from `.env.example`, `.env.sample`, `.env.template`, `.env.dist`.
143
+ - Compares **key names only**. Values are never printed or stored.
144
+ - Finds `process.env.KEY`, `process.env['KEY']` and `import.meta.env.KEY` in `.js/.jsx/.ts/.tsx/.mjs/.cjs/.vue/.svelte/.astro` files, skipping `node_modules`, `.git`, build output (`dist`, `build`, `.next`, ...) and plain names from `.gitignore`.
145
+ - A template key that is not set is a **FAIL** only when the code references it; otherwise it is a **WARN** ("may be optional"). Keys set in the shell environment count as set.
146
+ - Keys in `.env` missing from the template, keys referenced in code but defined nowhere, and `.env` not in `.gitignore` are **WARN**s, each labelled with its source.
147
+ - Lines that cannot be parsed are reported by line number, not guessed.
148
+
149
+ ### ports
150
+
151
+ ```bash
152
+ dev-doctor ports # common development ports (or "ports" in .dev-doctor.json)
153
+ dev-doctor ports 3000 # focused view of one port
154
+ dev-doctor ports 3000 5173 # specific ports; busy ones are FAIL (exit 1)
155
+ dev-doctor ports --json
156
+ dev-doctor ports --kill 3000 # stop the process on 3000, asks for confirmation
157
+ dev-doctor ports --kill 3000 --pid 8421 # pick the exact process when several share the port
158
+ dev-doctor ports --kill 3000 --yes # non-interactive
159
+ dev-doctor ports --kill 3000 --yes --force # allow force termination if it survives a graceful stop
160
+ ```
161
+
162
+ #### Process inspection
163
+
164
+ For every occupied port Dev Doctor collects what the OS allows:
165
+
166
+ | Field | macOS | Linux | Windows |
167
+ | --- | --- | --- | --- |
168
+ | PID, process name | `lsof` | `ss` (or `lsof`) | `netstat`, `tasklist` |
169
+ | Command line | `ps` | `ps` | PowerShell `Win32_Process` |
170
+ | Parent PID | `ps` | `ps` | PowerShell `Win32_Process` |
171
+ | User | `ps` | `ps` | not collected |
172
+ | Working directory | `lsof -d cwd` (own processes only) | `/proc/<pid>/cwd` | not available |
173
+ | Project association | from working directory, else command line | same | command line only |
174
+
175
+ Project association is **current project**, **another project or location**, or **unknown**. Anything the OS does not expose, or that permissions hide, is shown as `unavailable (reason)`; it is never guessed. If a port is busy but its owner cannot be seen at all, the report says so instead of calling the port free. IPv4 and IPv6 listeners of the same process are merged.
176
+
177
+ Command lines are redacted before they reach any output (human or JSON): URL credentials, `--password=…`/`--token …`-style flags, `SECRET_KEY=…`-style assignments and `Bearer`/`Basic` tokens become `***`. Environment variables of other processes are never read.
178
+
179
+ #### Interactive workflow
180
+
181
+ In an interactive terminal, after the report you can pick a process (when several ports are occupied) and choose **Inspect process**, **Stop process** or **Skip**. Stopping uses the same safe flow as `--kill`. When stdin is not a terminal, in `--ci` mode or with `--json`, no prompt is ever shown.
182
+
183
+ #### Safe termination (`--kill`)
184
+
185
+ 1. Validates the port (1–65535) and `--pid` (positive integer). PIDs are passed as arguments, never through a shell.
186
+ 2. Lists every listener on the port. The target must be unambiguous: a single listener, `--pid`, or an interactive choice. If any listener cannot be identified (for example another user's process), it refuses.
187
+ 3. Refuses to stop dev-doctor itself, the shell that started it, PIDs 0–4 and known system processes. It only ever targets that one PID; it never kills "all Node.js processes".
188
+ 4. Shows PID, name, command line and working directory, then asks for confirmation (declining cancels; non-interactive runs need `--yes`).
189
+ 5. Checks again that the same PID still holds the port, in case it exited or the port changed hands meanwhile.
190
+ 6. Sends a graceful stop (SIGTERM / `taskkill`).
191
+ 7. If the same process is still running and still holds the port, force termination (SIGKILL / `taskkill /F`) is offered as a separate step: a second confirmation, or `--force`.
192
+ 8. Re-scans the port. "Released" is only reported when that fresh check finds it free. If the process exited but something else now holds the port, it says so and touches nothing else.
193
+
194
+ Permission errors are reported as-is; Dev Doctor never uses `sudo` or requests elevation.
195
+
196
+ `--kill` exit codes: 0 when the port is free afterwards (or already was), 1 when it was refused, cancelled or the port is still in use, 2 for invalid input.
197
+
198
+ ### services
199
+
200
+ Docker CLI and daemon, plus TCP reachability of PostgreSQL, MongoDB and Redis. Services are only checked when the project appears to need them: Compose files and images, dependencies (`pg`, `mongoose`, `ioredis`, ...), or connection variables (`DATABASE_URL`, `MONGODB_URI`, `REDIS_URL`, `PGHOST`, ...). Host and port are taken from those values; credentials are never shown. A successful TCP connection does **not** mean authentication or queries work, and the report says so.
201
+
202
+ Nothing is started, stopped or reconfigured.
203
+
204
+ ### fix
205
+
206
+ ```bash
207
+ dev-doctor fix --dry-run # list fixes, change nothing
208
+ dev-doctor fix # confirm each fix
209
+ dev-doctor fix --yes # apply without prompts (required when non-interactive)
210
+ ```
211
+
212
+ Each fix shows the problem, why it matters, the proposed change, whether it modifies files or terminates a process, the expected outcome and a preview. After applying, the relevant check is re-run and the result is reported.
213
+
214
+ Available fixes:
215
+
216
+ | Fix | Change |
217
+ | --- | --- |
218
+ | `.env` missing | Creates `.env` with the key names from the template and **empty values** (never copies template values, never overwrites) |
219
+ | `.env` not ignored | Appends `.env` to `.gitignore` |
220
+ | Project port busy | Stops the process on that port (same safety rules as `ports --kill`) |
221
+
222
+ Everything else gets manual instructions. Dev Doctor never installs or upgrades packages, deletes lockfiles, changes Node.js versions, runs migrations or uses `sudo`.
223
+
224
+ ## Configuration
225
+
226
+ Optional `.dev-doctor.json` in the project root:
227
+
228
+ ```json
229
+ {
230
+ "ports": [3000, 5173],
231
+ "failOn": "fail",
232
+ "services": {
233
+ "docker": true,
234
+ "postgres": { "host": "localhost", "port": 5433 },
235
+ "redis": false
236
+ }
237
+ }
238
+ ```
239
+
240
+ - `ports`: ports the project uses (checked by `doctor` and `ports`).
241
+ - `failOn`: `"fail"` (default) or `"warn"`; `--fail-on` overrides it.
242
+ - `services`: `true`/object to require or point a check somewhere, `false` to skip it.
243
+
244
+ ## Exit codes
245
+
246
+ | Code | Meaning |
247
+ | --- | --- |
248
+ | 0 | No failures (or the requested action succeeded) |
249
+ | 1 | Diagnostics found failures, or a requested action was refused or did not succeed |
250
+ | 2 | Dev Doctor could not run (invalid arguments, bad config, unexpected error) |
251
+
252
+ In CI:
253
+
254
+ ```bash
255
+ dev-doctor --ci # exit 1 on FAIL
256
+ dev-doctor --ci --fail-on warn
257
+ dev-doctor --ci --json > dev-doctor.json
258
+ ```
259
+
260
+ ## Security
261
+
262
+ See [SECURITY.md](SECURITY.md). In short: offline, read-only by default, never prints secrets, never executes project scripts, no shell string interpolation, no `sudo`, and writes only inside the project directory.
263
+
264
+ ## Supported operating systems
265
+
266
+ | OS | Process inspection |
267
+ | --- | --- |
268
+ | macOS | `lsof`, `ps` |
269
+ | Linux | `ss` (falls back to `lsof`), `ps`, `/proc` |
270
+ | Windows | `netstat`, `tasklist`, PowerShell, `taskkill` |
271
+
272
+ When these tools are missing, Dev Doctor falls back to a bind test: it can still tell whether a port is busy, but not which process owns it.
273
+
274
+ The test suite has been run locally on macOS. Linux and Windows support is covered by unit tests with mocked tool output and by the GitHub Actions matrix (ubuntu, macos, windows × Node 22, 24); treat them as verified only once that CI is green.
275
+
276
+ ## Limitations
277
+
278
+ - Environment references are found with patterns; dynamic access (`process.env[name]`) and destructuring (`const { A } = process.env`) are not detected.
279
+ - `.gitignore` support is basic: plain names are skipped during the scan; glob patterns are not evaluated.
280
+ - Service checks are TCP-only and use default ports unless configured; ports remapped in Compose files are not parsed.
281
+ - On macOS, `lsof` only shows your own processes without elevated permissions; other users' processes appear as "process details not accessible", and their working directory as unavailable.
282
+ - Working directories are not available on Windows, so project association there relies on the command line.
283
+ - On Windows, console programs such as `node.exe` cannot be stopped gracefully (`taskkill` without `/F` is refused), so stopping them always needs the separate force step (`--force` or a second confirmation).
284
+ - Command-line redaction is pattern-based; unusual secret formats may not be recognized.
285
+ - `.nvmrc` aliases such as `lts/*` cannot be verified offline.
286
+
287
+ ## Development
288
+
289
+ ```bash
290
+ npm install
291
+ npm test
292
+ node bin/dev-doctor.js --help
293
+ npm link # makes "dev-doctor" available globally from this checkout
294
+ npm pack --dry-run # see what would be published
295
+ ```
296
+
297
+ ## Contributing
298
+
299
+ Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). Please report security issues privately as described in [SECURITY.md](SECURITY.md).
300
+
301
+ ## License
302
+
303
+ [MIT](LICENSE) © sameera_mhk
304
+ # dev-doctor
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ main(process.argv);
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "dev-env-doctor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Diagnose and safely fix common local development environment problems in JavaScript and Node.js projects: ports, Node version, dependencies, .env files and local services.",
5
+ "type": "module",
6
+ "bin": {
7
+ "dev-doctor": "bin/dev-doctor.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22.12.0"
18
+ },
19
+ "scripts": {
20
+ "start": "node bin/dev-doctor.js",
21
+ "test": "node --test"
22
+ },
23
+ "keywords": [
24
+ "cli",
25
+ "doctor",
26
+ "diagnostics",
27
+ "developer-tools",
28
+ "port",
29
+ "kill-port",
30
+ "env",
31
+ "dotenv",
32
+ "node-version",
33
+ "docker"
34
+ ],
35
+ "author": "sameera_mhk",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/Sameera-MHK/dev-doctor.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/Sameera-MHK/dev-doctor/issues"
43
+ },
44
+ "homepage": "https://github.com/Sameera-MHK/dev-doctor#readme",
45
+ "dependencies": {
46
+ "@inquirer/prompts": "^7.10.1",
47
+ "chalk": "^6.0.1",
48
+ "commander": "^15.0.0",
49
+ "semver": "^7.8.5"
50
+ }
51
+ }
@@ -0,0 +1,212 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { result, STATUS } from '../core/diagnostics.js';
4
+ import { parseEnv } from '../utils/env-parser.js';
5
+ import { readTextIfExists } from '../utils/paths.js';
6
+
7
+ const CATEGORY = 'Environment variables';
8
+
9
+ export const TEMPLATE_FILES = ['.env.example', '.env.sample', '.env.template', '.env.dist'];
10
+ export const LOCAL_FILES = ['.env', '.env.local', '.env.development', '.env.development.local'];
11
+
12
+ export const IGNORED_DIRS = new Set([
13
+ 'node_modules', '.git', 'dist', 'build', 'out', 'coverage', '.next', '.nuxt', '.svelte-kit',
14
+ '.output', '.turbo', '.cache', '.vercel', '.netlify', '.parcel-cache', 'tmp', 'vendor',
15
+ ]);
16
+
17
+ const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts', '.vue', '.svelte', '.astro']);
18
+
19
+ // Set by the runtime, shell or build tool; not expected in .env files.
20
+ const BUILTIN_KEYS = new Set([
21
+ 'NODE_ENV', 'CI', 'HOME', 'PATH', 'PWD', 'USER', 'SHELL', 'TZ', 'LANG', 'TERM', 'NO_COLOR', 'DEBUG',
22
+ 'MODE', 'DEV', 'PROD', 'SSR', 'BASE_URL', 'PORT', 'HOST',
23
+ ]);
24
+
25
+ const REFERENCE_PATTERNS = [
26
+ /process\.env\.([A-Za-z_][A-Za-z0-9_]*)/g,
27
+ /process\.env\[\s*['"`]([A-Za-z_][A-Za-z0-9_]*)['"`]\s*\]/g,
28
+ /import\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)/g,
29
+ ];
30
+
31
+ /** Plain names from .gitignore (no globs or nested paths); used to skip directories and files. */
32
+ export function gitignoreNames(text) {
33
+ const names = new Set();
34
+ for (const raw of (text ?? '').split(/\r?\n/)) {
35
+ const line = raw.trim().replace(/^\//, '').replace(/\/$/, '');
36
+ if (!line || line.startsWith('#') || line.startsWith('!') || /[*?[\]/]/.test(line)) continue;
37
+ names.add(line);
38
+ }
39
+ return names;
40
+ }
41
+
42
+ export function isEnvGitignored(text) {
43
+ const patterns = new Set(['.env', '/.env', '.env*', '/.env*', '*.env', '.env.*']);
44
+ return (text ?? '').split(/\r?\n/).some((line) => patterns.has(line.trim()));
45
+ }
46
+
47
+ export function findReferences(source) {
48
+ const keys = new Set();
49
+ for (const pattern of REFERENCE_PATTERNS) {
50
+ for (const match of source.matchAll(pattern)) keys.add(match[1]);
51
+ }
52
+ return keys;
53
+ }
54
+
55
+ /** Find env variable references in source files. Returns key -> Set of relative file paths. */
56
+ export async function scanEnvReferences(root, { maxFiles = 5000, ignoredNames = new Set() } = {}) {
57
+ const references = new Map();
58
+ let scanned = 0;
59
+ let truncated = false;
60
+ const queue = [root];
61
+
62
+ while (queue.length > 0) {
63
+ const dir = queue.shift();
64
+ let entries;
65
+ try {
66
+ entries = await fs.readdir(dir, { withFileTypes: true });
67
+ } catch {
68
+ continue;
69
+ }
70
+ for (const entry of entries) {
71
+ if (IGNORED_DIRS.has(entry.name) || ignoredNames.has(entry.name)) continue;
72
+ const full = path.join(dir, entry.name);
73
+ if (entry.isDirectory()) {
74
+ if (!entry.name.startsWith('.')) queue.push(full);
75
+ continue;
76
+ }
77
+ if (!entry.isFile() || !SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue;
78
+ if (scanned >= maxFiles) {
79
+ truncated = true;
80
+ break;
81
+ }
82
+ scanned++;
83
+ const text = readTextIfExists(full);
84
+ if (text === null) continue;
85
+ const rel = path.relative(root, full).split(path.sep).join('/');
86
+ for (const key of findReferences(text)) {
87
+ if (!references.has(key)) references.set(key, new Set());
88
+ references.get(key).add(rel);
89
+ }
90
+ }
91
+ if (truncated) break;
92
+ }
93
+ return { references, scanned, truncated };
94
+ }
95
+
96
+ function readEnvFile(root, file) {
97
+ const text = readTextIfExists(path.join(root, file));
98
+ if (text === null) return null;
99
+ const { entries, issues } = parseEnv(text);
100
+ return { file, entries, issues };
101
+ }
102
+
103
+ /** Collect env data. Values stay in memory only and are never returned in results. */
104
+ export async function analyzeEnvironment(root) {
105
+ const gitignoreText = readTextIfExists(path.join(root, '.gitignore'));
106
+ const template = TEMPLATE_FILES.map((f) => readEnvFile(root, f)).find(Boolean) ?? null;
107
+ const locals = LOCAL_FILES.map((f) => readEnvFile(root, f)).filter(Boolean);
108
+ const scan = await scanEnvReferences(root, { ignoredNames: gitignoreNames(gitignoreText) });
109
+ return { template, locals, ...scan, gitignoreText };
110
+ }
111
+
112
+ function sources(files, limit = 3) {
113
+ const list = [...files];
114
+ return list.slice(0, limit).join(', ') + (list.length > limit ? ` (+${list.length - limit} more)` : '');
115
+ }
116
+
117
+ export function environmentResults(data, { env = process.env } = {}) {
118
+ const { template, locals, references } = data;
119
+ const results = [];
120
+
121
+ if (!template && locals.length === 0 && references.size === 0) {
122
+ return [result(CATEGORY, STATUS.INFO, 'No environment files or variable references found')];
123
+ }
124
+
125
+ for (const file of [template, ...locals].filter(Boolean)) {
126
+ for (const issue of file.issues) {
127
+ results.push(result(CATEGORY, STATUS.WARN, `${file.file} line ${issue.line}: ${issue.message}`, {
128
+ hint: `Check line ${issue.line} of ${file.file}; dev-doctor does not guess ambiguous values.`,
129
+ }));
130
+ }
131
+ }
132
+
133
+ const localKeys = new Set(locals.flatMap((f) => [...f.entries.keys()]));
134
+ const isSet = (key) => localKeys.has(key) || Object.prototype.hasOwnProperty.call(env, key);
135
+ const templateKeys = template ? [...template.entries.keys()] : [];
136
+
137
+ if (template) {
138
+ results.push(result(CATEGORY, STATUS.INFO, `Template: ${template.file} (${templateKeys.length} keys)`));
139
+ } else if (locals.length > 0) {
140
+ results.push(result(CATEGORY, STATUS.WARN, 'No .env.example template found', {
141
+ hint: 'Create .env.example listing the required keys without real values.',
142
+ }));
143
+ }
144
+
145
+ if (template && locals.length === 0) {
146
+ const unset = templateKeys.filter((k) => !Object.prototype.hasOwnProperty.call(env, k));
147
+ if (unset.length > 0) {
148
+ results.push(result(CATEGORY, STATUS.FAIL, '.env file is missing', {
149
+ details: [`Keys from ${template.file} not set: ${unset.join(', ')}`],
150
+ hint: `Create .env from ${template.file} and fill in your own values.`,
151
+ fixId: 'create-env',
152
+ fixData: { template: template.file, keys: templateKeys },
153
+ }));
154
+ }
155
+ } else if (template) {
156
+ for (const key of templateKeys) {
157
+ if (isSet(key)) continue;
158
+ const usedIn = references.get(key);
159
+ if (usedIn) {
160
+ results.push(result(CATEGORY, STATUS.FAIL, `${key} is missing`, {
161
+ details: [`Documented in ${template.file}`, `Referenced in ${sources(usedIn)}`],
162
+ hint: `Set ${key} in your local .env file.`,
163
+ }));
164
+ } else {
165
+ results.push(result(CATEGORY, STATUS.WARN, `${key} is listed in ${template.file} but not set (may be optional)`, {
166
+ details: [`Documented in ${template.file}`],
167
+ }));
168
+ }
169
+ }
170
+ const templateSet = new Set(templateKeys);
171
+ for (const local of locals) {
172
+ for (const key of local.entries.keys()) {
173
+ if (!templateSet.has(key)) {
174
+ results.push(result(CATEGORY, STATUS.WARN, `${key} is not documented in ${template.file}`, {
175
+ details: [`Defined in ${local.file}`],
176
+ hint: `Add ${key} (without its value) to ${template.file}.`,
177
+ }));
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ const templateSet = new Set(templateKeys);
184
+ for (const [key, files] of references) {
185
+ if (BUILTIN_KEYS.has(key) || templateSet.has(key) || isSet(key)) continue;
186
+ results.push(result(CATEGORY, STATUS.WARN, `${key} is referenced in code but not defined`, {
187
+ details: [`Referenced in ${sources(files)}`],
188
+ hint: `Add ${key} to your .env file${template ? ` and ${template.file}` : ''} if the app needs it.`,
189
+ }));
190
+ }
191
+
192
+ if (locals.some((f) => f.file === '.env') && !isEnvGitignored(data.gitignoreText)) {
193
+ results.push(result(CATEGORY, STATUS.WARN, '.env is not listed in .gitignore', {
194
+ details: ['Secrets in .env could be committed by accident'],
195
+ hint: 'Add ".env" to .gitignore.',
196
+ fixId: 'gitignore-env',
197
+ }));
198
+ }
199
+
200
+ if (data.truncated) {
201
+ results.push(result(CATEGORY, STATUS.INFO, `Source scan stopped after ${data.scanned} files; some references may be missing`));
202
+ }
203
+
204
+ if (!results.some((r) => r.status === STATUS.FAIL || r.status === STATUS.WARN)) {
205
+ results.push(result(CATEGORY, STATUS.PASS, template ? `All keys from ${template.file} are set` : 'Environment variables look consistent'));
206
+ }
207
+ return results;
208
+ }
209
+
210
+ export async function checkEnvironment(root, options) {
211
+ return environmentResults(await analyzeEnvironment(root), options);
212
+ }