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 +20 -0
- package/LICENSE +21 -0
- package/README.md +303 -2
- package/bin/dev-doctor.js +4 -0
- package/package.json +49 -4
- package/src/checks/environment.js +212 -0
- package/src/checks/node-version.js +65 -0
- package/src/checks/packages.js +101 -0
- package/src/checks/ports.js +197 -0
- package/src/checks/project.js +69 -0
- package/src/checks/services.js +162 -0
- package/src/cli.js +81 -0
- package/src/commands/doctor.js +18 -0
- package/src/commands/fix.js +83 -0
- package/src/commands/ports.js +70 -0
- package/src/core/diagnostics.js +24 -0
- package/src/core/exit-codes.js +6 -0
- package/src/core/port-workflow.js +39 -0
- package/src/core/reporter.js +54 -0
- package/src/core/runner.js +34 -0
- package/src/core/terminate.js +140 -0
- package/src/fixes/index.js +97 -0
- package/src/platform/darwin.js +25 -0
- package/src/platform/index.js +10 -0
- package/src/platform/linux.js +48 -0
- package/src/platform/unix.js +79 -0
- package/src/platform/win32.js +96 -0
- package/src/utils/config.js +31 -0
- package/src/utils/env-parser.js +64 -0
- package/src/utils/errors.js +15 -0
- package/src/utils/paths.js +44 -0
- package/src/utils/process-info.js +34 -0
- package/src/utils/prompt.js +24 -0
- package/src/utils/redact.js +18 -0
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
|
-
#
|
|
1
|
+
# Dev Doctor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dev-env-doctor)
|
|
4
|
+
[](https://github.com/Sameera-MHK/dev-doctor/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+

|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev-env-doctor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|