# 🩺 envfix

![npm](https://img.shields.io/npm/v/envfix)
![CI](https://github.com/gokhanozgezer/envfix/actions/workflows/ci.yml/badge.svg)
![license](https://img.shields.io/npm/l/envfix)

> A tiny environment configuration doctor for Node.js projects — zero dependencies, CI friendly.

Every developer knows the pain: you pull the latest code, the app crashes, and twenty minutes later you discover someone added a new environment variable to `.env.example` that your local `.env` doesn't have. **envfix** finds those problems in one second — and fixes them. It also spots duplicate and malformed declarations, checks that your `.env` is safely gitignored, and can generate or synchronize your `.env.example` without ever leaking a secret.

## Quick start

No install needed:

```bash
npx envfix
```

```
🩺 envfix — .env vs .env.example

✖ Missing in .env (2)
   STRIPE_SECRET_KEY
   REDIS_URL

○ Empty value (1)
   SMTP_PASSWORD

✖ 3 problem(s) found.
  Run npx envfix fix to add missing variables.
```

Fix it:

```bash
npx envfix fix
```

```
✔ Added 2 variable(s) to .env:
   STRIPE_SECRET_KEY=
   REDIS_URL=
```

Missing keys are **appended** to your `.env` — existing lines and values are never touched.

## Commands & options

| Command / flag | Description |
| --- | --- |
| `envfix` / `envfix check` | Compare `.env` with `.env.example` |
| `envfix fix` | Append missing keys to `.env` (creates the file if needed) |
| `envfix doctor` | Full health check: files, syntax, consistency, Git safety |
| `envfix example` | Generate `.env.example` from `.env` (values omitted) |
| `envfix example --sync` | Append new `.env` keys to the example, report stale ones |
| `envfix --all` | Check (or doctor) every `.env*.example` ↔ env pair in the directory |
| `--env <path>` | Custom env file path (default: `.env`) |
| `--example <path>` | Custom example file path (default: `.env.example`) |
| `--copy-values` | With `fix`: copy values from the example file instead of leaving them empty |
| `--fill-empty` | With `fix`: also fill existing empty values (requires `--copy-values`) |
| `--dry-run` | fix/example: preview changes, write nothing |
| `--strict` | treat warnings (extras, duplicates, Git ignore status) as problems |
| `--json` | print a single JSON document to stdout |
| `--annotations` | force GitHub annotations on (auto-enabled on GitHub Actions) |
| `--no-annotations` | force GitHub annotations off |
| `-h, --help` | Show help |
| `-v, --version` | Show version |

## Full checkup with `doctor`

`envfix doctor` answers a broader question than `check`: *is my project's environment configuration healthy, consistent and safe?*

```bash
npx envfix doctor
```

```
🩺 envfix doctor

Environment
✔ .env found
✔ .env.example found
✔ 14 variable(s) configured

Syntax
⚠ Duplicate variable: API_URL
   lines 7 and 19 in .env

Consistency
✖ Missing: REDIS_URL
○ Empty: SMTP_PASSWORD

Git safety
✔ .env is not tracked by Git
✔ .env is ignored by Git

✖ 1 error(s), 2 warning(s)
```

The Git safety section is read-only and uses your local `git` (no dependency), evaluated against the repository that contains the env file: it warns when `.env` is not ignored, and reports an **error** when `.env` is tracked — a tracked env file stays a security risk even after it lands in `.gitignore`. A "safe" state is only claimed when Git confirms it; if a Git command fails, the state is reported as undetermined rather than green. Outside a Git repository (or without Git installed) the checks are simply skipped:

```
○ Git checks skipped — not a Git repository
```

Doctor severities: missing files, missing variables, syntax errors (invalid names, unclosed quotes) and a tracked `.env` are **errors** (exit `1`); empty variables, extras, duplicates and an unignored `.env` are **warnings** (exit `0`, or `1` with `--strict`). Skipped Git checks never fail the run. `doctor --all` checks every discovered pair, and `doctor --json` / annotations work like the other commands.

## Fill empty values

`fix` never touches existing lines by default. With explicit opt-in it can fill variables that exist but are empty:

```bash
npx envfix fix --fill-empty --copy-values
```

```
✔ Filled 2 empty variable(s) in .env:
   PORT=<copied>
   API_URL=<copied>
```

- `--fill-empty` requires `--copy-values` — fill values always come from the example file.
- Copied values round-trip exactly: reparsing the written `.env` yields the original value byte-for-byte (quoting is chosen automatically). A value the dotenv grammar cannot represent aborts with an error instead of being written corrupted.
- Non-empty values are **never** overwritten.
- Only truly empty lines (`KEY=`) are edited, in place; comments, order, quotes and line endings stay untouched.
- Copied values are written to `.env` but masked as `<copied>` in terminal output, and appear as key lists (`"filled": ["PORT"]`) in `--json`.
- Works with `--dry-run` to preview (`Would fill …`) without writing.

## Generate & sync `.env.example`

Create an example file from your real `.env` — every value is treated as a potential secret and replaced with an empty placeholder:

```bash
npx envfix example
```

```env
# .env                                # generated .env.example
DATABASE_URL=postgres://user:pw@db    DATABASE_URL=
PORT=3000                       →     PORT=
JWT_SECRET=super-secret               JWT_SECRET=
```

Key order, full-line comments and blank lines are preserved; values are always removed. Duplicates collapse to the first occurrence; CRLF files stay CRLF. If `.env.example` already exists, `envfix example` refuses to overwrite it — use sync instead:

```bash
npx envfix example --sync
```

Sync appends keys that are new in `.env` (as empty placeholders, using the same dated append block as `fix`) and **reports** keys that no longer exist in `.env` as stale — it never deletes them, and a real sync always exits `0` on completion. `--dry-run` is the full drift check: it exits `1` when keys would be added **or** when stale keys exist, and `0` only when the two files are perfectly in sync — which makes `envfix example --sync --dry-run` a handy CI guard against a drifting example file.

## Multiple environments

`--env` and `--example` accept any pair of files, so every naming convention works:

```bash
# .env.prod.example ↔ .env.prod
npx envfix --env .env.prod --example .env.prod.example

# .env.example ↔ .env.production
npx envfix --env .env.production --example .env.example

# fix works the same way
npx envfix fix --env .env.prod --example .env.prod.example --copy-values

# files can live in another directory
npx envfix --env config/.env.staging --example config/.env.example
```

### Check everything at once with `--all`

Projects often carry several env files. `--all` scans the current directory for every `.env*.example` file and checks each one against its matching env file:

```bash
npx envfix --all
```

```
🩺 envfix — checking all env pairs

.env.example       ↔  .env       ✔ healthy
.env.prod.example  ↔  .env.prod  ✖ 2 missing
.env.test.example  ↔  .env.test  ⚠ file not found

✖ 2 of 3 pair(s) need attention.
```

It exits with code `1` when any pair needs attention, so a single `npx envfix --all` step in CI covers all your environments.

A pair with extra variables (present in `.env` but not in the example) is still reported healthy by default — you'll see a note like `✔ healthy · 2 extra`. Pass `--strict` and extras turn into failures (`✖ 2 extra`), which also flips that pair's exit contribution.

Note: `fix` works on a single pair — run it per environment with `--env`/`--example`.

## Use it in CI

`envfix` exits with code `1` when variables are missing or empty, so you can catch configuration drift before it hits production:

```yaml
# GitHub Actions example
- name: Validate environment
  run: npx envfix --env .env.production --example .env.example
```

Exit codes depend on the command:

| Command | `0` | `1` | `2` |
| --- | --- | --- | --- |
| `envfix` / `envfix check` | Healthy | Missing/empty (or extra with `--strict`) found | Usage or file error |
| `envfix doctor` | No errors (no warnings either with `--strict`) | Errors found (or warnings with `--strict`) | Usage or file error |
| `envfix fix --dry-run` | Healthy | Missing/empty (or extra with `--strict`) found | Usage or file error |
| `envfix fix` | Completed (added variables, or nothing to add) | *(never)* | Usage or file error |
| `envfix example` | Completed | *(never)* | Usage or file error |
| `envfix example --dry-run` | In sync / nothing to create | Changes pending or stale keys found | Usage or file error |

A real `fix` never exits `1` — it either completes or fails with a usage/file error. In every case, `missing`/`empty`/`extra` describe the state **before** the operation ran (pre-operation detections), so `envfix fix --json`'s document still tells you what was wrong going in.

## GitHub Actions

`envfix` can speak [GitHub Actions workflow-command annotations](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions) — no extra setup required:

- Annotations **auto-enable** when `GITHUB_ACTIONS=true` (set on every GitHub-hosted and self-hosted Actions runner).
- `--annotations` forces them on locally, e.g. to preview what CI will show.
- `--no-annotations` forces them off, even on a runner.
- Missing variables become `error`, empty variables become `warning`, and extra variables (not in the example) become `notice` — or `error` when combined with `--strict`.

```yaml
- run: npx envfix --all --strict
```

That single step is enough: it checks every env pair, fails the build on drift, and surfaces missing/empty/extra variables as annotations in the Actions run summary — no flags needed beyond `--strict`. (Annotated files typically aren't part of the PR diff — `.env` is almost always gitignored — so annotations show up on the run summary / Checks tab rather than inline on the "Files changed" tab.) Forcing annotations on locally (`envfix check --annotations`) produces:

```
::error file=.env,title=envfix::Missing environment variable: API_KEY
::warning file=.env,title=envfix::Empty environment variable: SMTP_PASSWORD
::notice file=.env,title=envfix::Extra environment variable not in .env.example: EXTRA_VAR
```

## JSON output

With `--json`, stdout is always **exactly one JSON document** — whether the run succeeded or failed. stderr stays empty in JSON mode. `--help` and `--version` bypass this guarantee: they always print plain text, `--json` or not.

Success example — `envfix check --json` on a project with one missing variable:

```json
{
  "command": "check",
  "strict": false,
  "dryRun": false,
  "operationSucceeded": true,
  "healthyBefore": false,
  "healthyAfter": false,
  "changed": false,
  "wouldChange": false,
  "error": null,
  "pairs": [
    {
      "env": ".env",
      "example": ".env.example",
      "missing": [
        "STRIPE_SECRET_KEY"
      ],
      "empty": [],
      "extra": [],
      "issues": [],
      "okCount": 2,
      "healthyBefore": false,
      "healthyAfter": false,
      "added": [],
      "wouldAdd": [],
      "error": null
    }
  ]
}
```

Error example — `envfix check --json --dry-run` (`--dry-run` is only valid with `fix` or `example`, so this is rejected before anything runs):

```json
{
  "command": null,
  "strict": false,
  "dryRun": false,
  "operationSucceeded": false,
  "healthyBefore": false,
  "healthyAfter": false,
  "changed": false,
  "wouldChange": false,
  "error": {
    "code": "invalid-arguments",
    "message": "--dry-run is only valid with fix or example."
  },
  "pairs": []
}
```

`operationSucceeded: false` and a non-null `error` always mean exit code `2` — check `error` first when scripting against the JSON output.

## What it understands

- `KEY=value`, `export KEY=value`
- Comments (`# ...`) and inline comments (`KEY=value #note` — a `#` outside quotes ends the value, dotenv-style)
- Quoted values: `KEY="hello world"`, `KEY='...'`, and `` KEY=`...` `` — `#` inside quotes is part of the value
- Multiline quoted values (private keys, certificates):
  ```env
  PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
  abc
  -----END PRIVATE KEY-----"
  ```
- Values containing `=` (URLs, connection strings)
- CRLF (Windows) line endings — `fix` also appends using the file's own line-ending convention, so a CRLF file stays pure CRLF
- `NO_COLOR` environment variable (plain output for logs)

## Diagnostics

Beyond missing/empty/extra variables, envfix reports problems inside the files themselves:

- **Duplicate variables** (warning) — the last declaration wins, but you're told about it:
  ```
  ⚠ Duplicate variable: API_URL
     lines 3 and 9 in .env
  ```
- **Invalid variable names** (error) — `DATABASE-HOST=x` no longer disappears silently
- **Unclosed quotes** (error) — a `KEY="value` without a closing quote is flagged instead of corrupting the parse

Every diagnostic carries a line number, appears in `--json` output (`pairs[].issues`), and becomes a GitHub annotation with `file` and `line` set. In `check`, diagnostics are informational for exit-code purposes by default; with `--strict`, every diagnostic — duplicates included — makes the check fail. In `doctor`, error-severity diagnostics fail by default, while warnings fail only with `--strict`. `fix` never rewrites or removes existing lines — diagnostics are informational, not destructive.

With `fix --copy-values`, copied values are written to `.env` but shown masked in the terminal (`API_KEY=<copied>`) so secrets from the example file don't end up in logs.

## Why another env tool?

- **Zero dependencies** — nothing to audit, nothing to break
- **npx-first** — no global install, no project dependency required
- **Append-only fix** — your existing `.env` values are sacred
- **CI friendly** — meaningful exit codes out of the box

## Safety guarantees

By default envfix never:

- prints environment values to the terminal, JSON, or CI annotations
- overwrites an existing non-empty variable (not even with `--fill-empty`)
- deletes duplicate lines or stale example keys
- rewrites, reorders, or re-formats your files (changes are append-only or single-line fills you explicitly opted into)
- changes your line endings — CRLF files stay CRLF
- modifies `.gitignore`, commits, or runs any mutating Git command (Git checks are read-only)

## Development

```bash
npm test        # runs the built-in node:test suite
```

Requires Node.js >= 18.

## License

MIT © [Gokhan Ozgezer](https://github.com/gokhanozgezer)
