Skip to content
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ This project follows [Keep a Changelog](https://keepachangelog.com/) and [Semant
### Fixed
-

## [2.2.3] - 2025-09-08
### Added
- Warning for HTTPS URLs detected in codebase.
- Added duplicate key detection to codebase scanner.
- added `--strict` flag to enable strict mode (treat warnings as errors).
- duplicate key detection for `.env.example` files.

### Fixed
- Fixed issue with false warnings on secrets in certain edge cases.
- Updated README

### Changed
- No breaking changes.
- `--compare` feature coloring improved for better readability.
- added `duplicate` warnings to scan results.

## [2.2.2] - 2025-09-07
### Fixed
- Fixed issue where it would give a false warning on secrets with process.env
Expand Down
30 changes: 5 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,13 @@ Also works well in modern JavaScript/TypeScript projects and frameworks like Nod

---

## Installation

```bash
# npm
npm install -g dotenv-diff

# yarn
yarn global add dotenv-diff

# pnpm
pnpm add -g dotenv-diff
```
## Usage

```bash
dotenv-diff
```

This scans your entire codebase to detect which environment variables are actually used in the code — and compare them against your `.env` file.

## Why dotenv-diff?

- **Prevent production issues**: Ensure all required environment variables are defined before deploying.
- **Avoid runtime errors**: Catch missing or misconfigured variables early in development.
- **Improve collaboration**: Keep your team aligned on necessary environment variables.
- **Enhance security**: Ensure sensitive variables are not accidentally committed to version control.
- **Scale confidently**: Perfect for turbo monorepos and multi-environment setups.
- Ensure all required environment variables are defined before deploying.
- Catch missing or misconfigured variables early in development.
- Improve collaboration: Keep your team aligned on necessary environment variables.
- Enhance security: Ensure sensitive variables are not accidentally committed to version control.
- Scale confidently: Perfect for turbo monorepos and multi-environment setups.

### Use it in Github Actions Example:

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dotenv-diff",
"version": "2.2.2",
"version": "2.2.3",
"type": "module",
"description": "Scan your codebase to find environment variables in use.",
"bin": {
Expand Down Expand Up @@ -30,6 +30,7 @@
"lint": "eslint ./src --ext .ts",
"format": "prettier --write \"src/**/*.ts\" \"src/*.ts\"",
"start": "node dist/bin/dotenv-diff.js",
"dotenv-diff": "dotenv-diff",
"prepublishOnly": "npm run lint && npm test && npm run build"
},
"author": "Chrilleweb",
Expand All @@ -55,7 +56,7 @@
"node": ">=20.0.0"
},
"dependencies": {
"chalk": "^5.4.1",
"chalk": "5.4.1",
"commander": "^14.0.0",
"prompts": "^2.4.2"
},
Expand Down
Binary file modified public/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function createProgram() {
'Do not list variables that are defined in .env but not used in code',
)
.option('--no-show-stats', 'Do not show statistics')
.option('--strict', 'Enable fail on warnings')
.option(
'--no-secrets',
'Disable secret detection during scan (enabled by default)',
Expand Down
1 change: 1 addition & 0 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export async function run(program: Command) {
showStats: opts.showStats,
isCiMode: opts.isCiMode,
secrets: opts.secrets,
strict: opts.strict ?? false,
...(opts.files ? { files: opts.files } : {}),
});

Expand Down
172 changes: 97 additions & 75 deletions src/commands/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function compareMany(
collect?: (entry: CompareJsonEntry) => void;
only?: Category[];
showStats?: boolean;
strict?: boolean;
},
) {
let exitWithError = false;
Expand All @@ -52,9 +53,10 @@ export async function compareMany(

if (!fs.existsSync(envPath) || !fs.existsSync(examplePath)) {
if (!opts.json) {
console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
console.log();
console.log(chalk.blue(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
console.log(
chalk.yellow(' ⚠️ Skipping: missing matching example file.'),
chalk.yellow('⚠️ Skipping: missing matching example file.'),
);
console.log();
}
Expand All @@ -64,7 +66,9 @@ export async function compareMany(
}

if (!opts.json) {
console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
console.log();
console.log(chalk.blue(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
console.log();
}

// Git ignore hint (only when not JSON)
Expand Down Expand Up @@ -96,36 +100,6 @@ export async function compareMany(
);
}

if (dupsEnv.length || dupsEx.length) {
entry.duplicates = {};
}
if (dupsEnv.length) {
entry.duplicates!.env = dupsEnv;
if (!opts.json) {
console.log(
chalk.yellow(
` ⚠️ Duplicate keys in ${envName} (last occurrence wins):`,
),
);
dupsEnv.forEach(({ key, count }) =>
console.log(chalk.yellow(` - ${key} (${count} occurrences)`)),
);
}
}
if (dupsEx.length) {
entry.duplicates!.example = dupsEx;
if (!opts.json) {
console.log(
chalk.yellow(
` ⚠️ Duplicate keys in ${exampleName} (last occurrence wins):`,
),
);
dupsEx.forEach(({ key, count }) =>
console.log(chalk.yellow(` - ${key} (${count} occurrences)`)),
);
}
}

// Diff + empty
const currentFull = parseEnvFile(envPath);
const exampleFull = parseEnvFile(examplePath);
Expand Down Expand Up @@ -165,23 +139,7 @@ export async function compareMany(
gitignoreUnsafe: run('gitignore') ? gitignoreUnsafe : false,
};

const allOk =
filtered.missing.length === 0 &&
filtered.extra.length === 0 &&
filtered.empty.length === 0 &&
filtered.mismatches.length === 0;

if (allOk) {
entry.ok = true;
if (!opts.json) {
console.log(chalk.green(' ✅ All keys match.'));
console.log();
}
opts.collect?.(entry);
continue;
}

// --- Stats block for compare mode when --show-stats is active ---
// --- Stats block for compare mode when --show-stats is active ---
if (opts.showStats && !opts.json) {
const envCount = currentKeys.length;
const exampleCount = exampleKeys.length;
Expand All @@ -199,24 +157,73 @@ export async function compareMany(
? filtered.mismatches.length
: 0;

console.log(chalk.bold(' 📊 Compare Statistics:'));
console.log(chalk.gray(` Keys in ${envName}: ${envCount}`));
console.log(chalk.gray(` Keys in ${exampleName}: ${exampleCount}`));
console.log(chalk.gray(` Shared keys: ${sharedCount}`));
console.log(chalk.magenta('📊 Compare Statistics:'));
console.log(chalk.magenta.dim(` Keys in ${envName}: ${envCount}`));
console.log(chalk.magenta.dim(` Keys in ${exampleName}: ${exampleCount}`));
console.log(chalk.magenta.dim(` Shared keys: ${sharedCount}`));
console.log(
chalk.gray(` Missing (in ${envName}): ${filtered.missing.length}`),
chalk.magenta.dim(` Missing (in ${envName}): ${filtered.missing.length}`),
);
console.log(
chalk.gray(
` Extra (not in ${exampleName}): ${filtered.extra.length}`,
),
chalk.magenta.dim(` Extra (not in ${exampleName}): ${filtered.extra.length}`),
);
console.log(chalk.gray(` Empty values: ${filtered.empty.length}`));
console.log(chalk.gray(` Duplicate keys: ${duplicateCount}`));
console.log(chalk.gray(` Value mismatches: ${valueMismatchCount}`));
console.log(chalk.magenta.dim(` Empty values: ${filtered.empty.length}`));
console.log(chalk.magenta.dim(` Duplicate keys: ${duplicateCount}`));
console.log(chalk.magenta.dim(` Value mismatches: ${valueMismatchCount}`));
console.log();
}

const allOk =
filtered.missing.length === 0 &&
filtered.extra.length === 0 &&
filtered.empty.length === 0 &&
filtered.duplicatesEnv.length === 0 &&
filtered.duplicatesEx.length === 0 &&
filtered.mismatches.length === 0;

if (allOk) {
entry.ok = true;
if (!opts.json) {
console.log(chalk.green('✅ All keys match.'));
console.log();
}
opts.collect?.(entry);
continue;
}

// --- move duplicate logging AFTER stats ---
if (dupsEnv.length || dupsEx.length) {
entry.duplicates = {};
}
if (dupsEnv.length) {
entry.duplicates!.env = dupsEnv;
if (!opts.json) {
console.log(
chalk.yellow(
`⚠️ Duplicate keys in ${envName} (last occurrence wins):`,
),
);
dupsEnv.forEach(({ key, count }) =>
console.log(chalk.yellow(` - ${key} (${count} occurrences)`)),
);
console.log();
}
}
if (dupsEx.length) {
entry.duplicates!.example = dupsEx;
if (!opts.json) {
console.log(
chalk.yellow(
`⚠️ Duplicate keys in ${exampleName} (last occurrence wins):`,
),
);
dupsEx.forEach(({ key, count }) =>
console.log(chalk.yellow(` - ${key} (${count} occurrences)`)),
);
console.log();
}
}

if (filtered.missing.length) {
entry.missing = filtered.missing;
exitWithError = true;
Expand Down Expand Up @@ -249,34 +256,37 @@ export async function compareMany(

if (!opts.json) {
if (filtered.missing.length && !opts.fix) {
console.log(chalk.red(' ❌ Missing keys:'));
console.log(chalk.red('❌ Missing keys:'));
filtered.missing.forEach((key) =>
console.log(chalk.red(` - ${key}`)),
console.log(chalk.red(` - ${key}`)),
);
console.log();
}
if (filtered.extra.length) {
console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
console.log(chalk.yellow('⚠️ Extra keys (not in example):'));
filtered.extra.forEach((key) =>
console.log(chalk.yellow(` - ${key}`)),
console.log(chalk.yellow(` - ${key}`)),
);
console.log();
}
if (filtered.empty.length) {
console.log(chalk.yellow(' ⚠️ Empty values:'));
console.log(chalk.yellow('⚠️ Empty values:'));
filtered.empty.forEach((key) =>
console.log(chalk.yellow(` - ${key}`)),
console.log(chalk.yellow(` - ${key}`)),
);
console.log();
}
if (filtered.mismatches.length) {
console.log(chalk.yellow(' ⚠️ Value mismatches:'));
console.log(chalk.yellow('⚠️ Value mismatches:'));
filtered.mismatches.forEach(({ key, expected, actual }) =>
console.log(
chalk.yellow(
` - ${key}: expected '${expected}', but got '${actual}'`,
` - ${key}: expected '${expected}', but got '${actual}'`,
),
),
);
console.log();
}
console.log();
}

if (!opts.json && !opts.fix) {
Expand Down Expand Up @@ -304,36 +314,48 @@ export async function compareMany(

if (!opts.json) {
if (changed) {
console.log(chalk.green(' ✅ Auto-fix applied:'));
console.log(chalk.green('✅ Auto-fix applied:'));
if (result.removedDuplicates.length) {
console.log(
chalk.green(
` - Removed ${result.removedDuplicates.length} duplicate keys from ${envName}: ${result.removedDuplicates.join(', ')}`,
` - Removed ${result.removedDuplicates.length} duplicate keys from ${envName}: ${result.removedDuplicates.join(', ')}`,
),
);
}
if (result.addedEnv.length) {
console.log(
chalk.green(
` - Added ${result.addedEnv.length} missing keys to ${envName}: ${result.addedEnv.join(', ')}`,
` - Added ${result.addedEnv.length} missing keys to ${envName}: ${result.addedEnv.join(', ')}`,
),
);
}
if (result.addedExample.length) {
console.log(
chalk.green(
` - Synced ${result.addedExample.length} keys to ${exampleName}: ${result.addedExample.join(', ')}`,
` - Synced ${result.addedExample.length} keys to ${exampleName}: ${result.addedExample.join(', ')}`,
),
);
}
} else {
console.log(chalk.green(' ✅ Auto-fix applied: no changes needed.'));
console.log(chalk.green('✅ Auto-fix applied: no changes needed.'));
}
console.log();
}
}

opts.collect?.(entry);
const warningsExist =
filtered.extra.length > 0 ||
filtered.empty.length > 0 ||
filtered.duplicatesEnv.length > 0 ||
filtered.duplicatesEx.length > 0 ||
filtered.mismatches.length > 0 ||
filtered.gitignoreUnsafe;

if (opts.strict && warningsExist) {
exitWithError = true;
}

}

return { exitWithError };
Expand Down
Loading