dotenv-diff 1.6.2 → 1.6.4
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/README.md +20 -0
- package/dist/src/cli/program.js +4 -1
- package/dist/src/cli/run.js +28 -6
- package/dist/src/commands/compare.d.ts +30 -0
- package/dist/src/commands/compare.js +81 -36
- package/dist/src/config/options.d.ts +6 -0
- package/dist/src/config/options.js +22 -0
- package/dist/src/core/filterIgnoredKeys.d.ts +1 -0
- package/dist/src/core/filterIgnoredKeys.js +3 -0
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -43,6 +43,26 @@ When using the `--check-values` option, the tool will also compare the actual va
|
|
|
43
43
|
|
|
44
44
|
`dotenv-diff` warns when a `.env*` file contains the same key multiple times. The last occurrence wins. Suppress these warnings with `--allow-duplicates`.
|
|
45
45
|
|
|
46
|
+
## Ignore specific keys
|
|
47
|
+
|
|
48
|
+
Exclude certain keys from the comparison using `--ignore` for exact names or `--ignore-regex` for patterns:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
dotenv-diff --ignore API_KEY,SESSION_ID
|
|
52
|
+
dotenv-diff --ignore-regex '^SECRET_'
|
|
53
|
+
dotenv-diff --ignore API_KEY --ignore-regex '^SECRET_'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Ignored keys are removed from all warnings and do not affect the exit code.
|
|
57
|
+
|
|
58
|
+
## Output format in JSON
|
|
59
|
+
|
|
60
|
+
You can output the results in JSON format using the `--json` option:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
dotenv-diff --json
|
|
64
|
+
```
|
|
65
|
+
|
|
46
66
|
## Compare specific files
|
|
47
67
|
|
|
48
68
|
Override the autoscan and compare exactly two files:
|
package/dist/src/cli/program.js
CHANGED
|
@@ -8,5 +8,8 @@ export function createProgram() {
|
|
|
8
8
|
.option('-y, --yes', 'Run non-interactively and answer Yes to prompts')
|
|
9
9
|
.option('--env <file>', 'Path to a specific .env file')
|
|
10
10
|
.option('--example <file>', 'Path to a specific .env.example file')
|
|
11
|
-
.option('--allow-duplicates', 'Do not warn about duplicate keys in .env* files')
|
|
11
|
+
.option('--allow-duplicates', 'Do not warn about duplicate keys in .env* files')
|
|
12
|
+
.option('--ignore <keys>', 'Comma-separated list of keys to ignore')
|
|
13
|
+
.option('--ignore-regex <pattern>', 'Regex pattern to ignore matching keys')
|
|
14
|
+
.option('--json', 'Output results in JSON format');
|
|
12
15
|
}
|
package/dist/src/cli/run.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
1
4
|
import { normalizeOptions } from '../config/options.js';
|
|
2
5
|
import { discoverEnvFiles } from '../services/envDiscovery.js';
|
|
3
6
|
import { pairWithExample } from '../services/envPairing.js';
|
|
4
7
|
import { ensureFilesOrPrompt } from '../commands/init.js';
|
|
5
8
|
import { compareMany } from '../commands/compare.js';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
9
|
export async function run(program) {
|
|
10
10
|
program.parse(process.argv);
|
|
11
11
|
const raw = program.opts();
|
|
12
12
|
const opts = normalizeOptions(raw);
|
|
13
|
-
// Special-case: both flags → direct comparison
|
|
13
|
+
// Special-case: both flags → direct comparison of exactly those two files
|
|
14
14
|
if (opts.envFlag && opts.exampleFlag) {
|
|
15
15
|
const envExists = fs.existsSync(opts.envFlag);
|
|
16
16
|
const exExists = fs.existsSync(opts.exampleFlag);
|
|
@@ -23,6 +23,7 @@ export async function run(program) {
|
|
|
23
23
|
}
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
|
+
const report = [];
|
|
26
27
|
const { exitWithError } = await compareMany([
|
|
27
28
|
{
|
|
28
29
|
envName: path.basename(opts.envFlag),
|
|
@@ -33,14 +34,23 @@ export async function run(program) {
|
|
|
33
34
|
checkValues: opts.checkValues,
|
|
34
35
|
cwd: opts.cwd,
|
|
35
36
|
allowDuplicates: opts.allowDuplicates,
|
|
37
|
+
json: opts.json,
|
|
38
|
+
ignore: opts.ignore,
|
|
39
|
+
ignoreRegex: opts.ignoreRegex,
|
|
40
|
+
collect: (e) => report.push(e),
|
|
36
41
|
});
|
|
42
|
+
if (opts.json) {
|
|
43
|
+
console.log(JSON.stringify(report, null, 2));
|
|
44
|
+
}
|
|
37
45
|
process.exit(exitWithError ? 1 : 0);
|
|
38
46
|
}
|
|
47
|
+
// Auto-discovery flow
|
|
39
48
|
const d = discoverEnvFiles({
|
|
40
49
|
cwd: opts.cwd,
|
|
41
50
|
envFlag: opts.envFlag,
|
|
42
51
|
exampleFlag: opts.exampleFlag,
|
|
43
52
|
});
|
|
53
|
+
// Init cases (may create files or early-exit)
|
|
44
54
|
const res = await ensureFilesOrPrompt({
|
|
45
55
|
cwd: d.cwd,
|
|
46
56
|
primaryEnv: d.primaryEnv,
|
|
@@ -49,14 +59,26 @@ export async function run(program) {
|
|
|
49
59
|
isYesMode: opts.isYesMode,
|
|
50
60
|
isCiMode: opts.isCiMode,
|
|
51
61
|
});
|
|
52
|
-
if (res.shouldExit)
|
|
62
|
+
if (res.shouldExit) {
|
|
63
|
+
// For JSON mode, emit an empty report to keep output machine-friendly (optional; safe).
|
|
64
|
+
if (opts.json)
|
|
65
|
+
console.log(JSON.stringify([], null, 2));
|
|
53
66
|
process.exit(res.exitCode);
|
|
54
|
-
|
|
67
|
+
}
|
|
68
|
+
// Compare all discovered pairs
|
|
55
69
|
const pairs = pairWithExample(d);
|
|
70
|
+
const report = [];
|
|
56
71
|
const { exitWithError } = await compareMany(pairs, {
|
|
57
72
|
checkValues: opts.checkValues,
|
|
58
73
|
cwd: opts.cwd,
|
|
59
74
|
allowDuplicates: opts.allowDuplicates,
|
|
75
|
+
json: opts.json,
|
|
76
|
+
ignore: opts.ignore,
|
|
77
|
+
ignoreRegex: opts.ignoreRegex,
|
|
78
|
+
collect: (e) => report.push(e),
|
|
60
79
|
});
|
|
80
|
+
if (opts.json) {
|
|
81
|
+
console.log(JSON.stringify(report, null, 2));
|
|
82
|
+
}
|
|
61
83
|
process.exit(exitWithError ? 1 : 0);
|
|
62
84
|
}
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
export type CompareJsonEntry = {
|
|
2
|
+
env: string;
|
|
3
|
+
example: string;
|
|
4
|
+
skipped?: {
|
|
5
|
+
reason: string;
|
|
6
|
+
};
|
|
7
|
+
duplicates?: {
|
|
8
|
+
env?: Array<{
|
|
9
|
+
key: string;
|
|
10
|
+
count: number;
|
|
11
|
+
}>;
|
|
12
|
+
example?: Array<{
|
|
13
|
+
key: string;
|
|
14
|
+
count: number;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
missing?: string[];
|
|
18
|
+
extra?: string[];
|
|
19
|
+
empty?: string[];
|
|
20
|
+
valueMismatches?: Array<{
|
|
21
|
+
key: string;
|
|
22
|
+
expected: string;
|
|
23
|
+
actual: string;
|
|
24
|
+
}>;
|
|
25
|
+
ok?: boolean;
|
|
26
|
+
};
|
|
1
27
|
export declare function compareMany(pairs: Array<{
|
|
2
28
|
envName: string;
|
|
3
29
|
envPath: string;
|
|
@@ -6,6 +32,10 @@ export declare function compareMany(pairs: Array<{
|
|
|
6
32
|
checkValues: boolean;
|
|
7
33
|
cwd: string;
|
|
8
34
|
allowDuplicates?: boolean;
|
|
35
|
+
json?: boolean;
|
|
36
|
+
ignore: string[];
|
|
37
|
+
ignoreRegex: RegExp[];
|
|
38
|
+
collect?: (entry: CompareJsonEntry) => void;
|
|
9
39
|
}): Promise<{
|
|
10
40
|
exitWithError: boolean;
|
|
11
41
|
}>;
|
|
@@ -5,68 +5,113 @@ import { parseEnvFile } from '../lib/parseEnv.js';
|
|
|
5
5
|
import { diffEnv } from '../lib/diffEnv.js';
|
|
6
6
|
import { warnIfEnvNotIgnored } from '../services/git.js';
|
|
7
7
|
import { findDuplicateKeys } from '../services/duplicates.js';
|
|
8
|
+
import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
|
|
8
9
|
export async function compareMany(pairs, opts) {
|
|
9
10
|
let exitWithError = false;
|
|
10
11
|
for (const { envName, envPath, examplePath } of pairs) {
|
|
12
|
+
const exampleName = path.basename(examplePath);
|
|
13
|
+
const entry = { env: envName, example: exampleName };
|
|
11
14
|
if (!fs.existsSync(envPath) || !fs.existsSync(examplePath)) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
if (!opts.json) {
|
|
16
|
+
console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
|
|
17
|
+
console.log(chalk.yellow(' ⚠️ Skipping: missing matching example file.'));
|
|
18
|
+
console.log();
|
|
19
|
+
}
|
|
20
|
+
entry.skipped = { reason: 'missing file' };
|
|
21
|
+
opts.collect?.(entry);
|
|
15
22
|
continue;
|
|
16
23
|
}
|
|
17
|
-
|
|
24
|
+
if (!opts.json) {
|
|
25
|
+
console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
|
|
26
|
+
}
|
|
27
|
+
// Git ignore hint (only when not JSON)
|
|
18
28
|
warnIfEnvNotIgnored({
|
|
19
29
|
cwd: opts.cwd,
|
|
20
30
|
envFile: envName,
|
|
21
|
-
log: (msg) =>
|
|
31
|
+
log: (msg) => {
|
|
32
|
+
if (!opts.json)
|
|
33
|
+
console.log(msg.replace(/^/gm, ' '));
|
|
34
|
+
},
|
|
22
35
|
});
|
|
36
|
+
// Duplicate detection
|
|
23
37
|
if (!opts.allowDuplicates) {
|
|
24
|
-
const dupsEnv = findDuplicateKeys(envPath)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const dupsEnv = findDuplicateKeys(envPath).filter(({ key }) => !opts.ignore.includes(key) &&
|
|
39
|
+
!opts.ignoreRegex.some((rx) => rx.test(key)));
|
|
40
|
+
const dupsEx = findDuplicateKeys(examplePath).filter(({ key }) => !opts.ignore.includes(key) &&
|
|
41
|
+
!opts.ignoreRegex.some((rx) => rx.test(key)));
|
|
42
|
+
if (dupsEnv.length || dupsEx.length) {
|
|
43
|
+
entry.duplicates = {};
|
|
28
44
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
if (dupsEnv.length) {
|
|
46
|
+
entry.duplicates.env = dupsEnv;
|
|
47
|
+
if (!opts.json) {
|
|
48
|
+
console.log(chalk.yellow(` ⚠️ Duplicate keys in ${envName} (last occurrence wins):`));
|
|
49
|
+
dupsEnv.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (dupsEx.length) {
|
|
53
|
+
entry.duplicates.example = dupsEx;
|
|
54
|
+
if (!opts.json) {
|
|
55
|
+
console.log(chalk.yellow(` ⚠️ Duplicate keys in ${exampleName} (last occurrence wins):`));
|
|
56
|
+
dupsEx.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
|
|
57
|
+
}
|
|
34
58
|
}
|
|
35
59
|
}
|
|
36
|
-
|
|
37
|
-
const
|
|
60
|
+
// Diff + empty
|
|
61
|
+
const currentFull = parseEnvFile(envPath);
|
|
62
|
+
const exampleFull = parseEnvFile(examplePath);
|
|
63
|
+
const currentKeys = filterIgnoredKeys(Object.keys(currentFull), opts.ignore, opts.ignoreRegex);
|
|
64
|
+
const exampleKeys = filterIgnoredKeys(Object.keys(exampleFull), opts.ignore, opts.ignoreRegex);
|
|
65
|
+
const current = Object.fromEntries(currentKeys.map((k) => [k, currentFull[k]]));
|
|
66
|
+
const example = Object.fromEntries(exampleKeys.map((k) => [k, exampleFull[k]]));
|
|
38
67
|
const diff = diffEnv(current, example, opts.checkValues);
|
|
39
68
|
const emptyKeys = Object.entries(current)
|
|
40
69
|
.filter(([, v]) => (v ?? '').trim() === '')
|
|
41
70
|
.map(([k]) => k);
|
|
42
|
-
|
|
71
|
+
const allOk = diff.missing.length === 0 &&
|
|
43
72
|
diff.extra.length === 0 &&
|
|
44
73
|
emptyKeys.length === 0 &&
|
|
45
|
-
diff.valueMismatches.length === 0
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
diff.valueMismatches.length === 0;
|
|
75
|
+
if (allOk) {
|
|
76
|
+
entry.ok = true;
|
|
77
|
+
if (!opts.json) {
|
|
78
|
+
console.log(chalk.green(' ✅ All keys match.'));
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
opts.collect?.(entry);
|
|
48
82
|
continue;
|
|
49
83
|
}
|
|
50
|
-
if (diff.missing.length
|
|
84
|
+
if (diff.missing.length) {
|
|
85
|
+
entry.missing = diff.missing;
|
|
51
86
|
exitWithError = true;
|
|
52
|
-
console.log(chalk.red(' ❌ Missing keys:'));
|
|
53
|
-
diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
|
|
54
87
|
}
|
|
55
|
-
if (diff.extra.length
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
if (diff.extra.length)
|
|
89
|
+
entry.extra = diff.extra;
|
|
90
|
+
if (emptyKeys.length)
|
|
91
|
+
entry.empty = emptyKeys;
|
|
92
|
+
if (opts.checkValues && diff.valueMismatches.length) {
|
|
93
|
+
entry.valueMismatches = diff.valueMismatches;
|
|
58
94
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
console.log(chalk.yellow(` - ${key}
|
|
67
|
-
}
|
|
95
|
+
if (!opts.json) {
|
|
96
|
+
if (diff.missing.length) {
|
|
97
|
+
console.log(chalk.red(' ❌ Missing keys:'));
|
|
98
|
+
diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
|
|
99
|
+
}
|
|
100
|
+
if (diff.extra.length) {
|
|
101
|
+
console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
|
|
102
|
+
diff.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
|
|
103
|
+
}
|
|
104
|
+
if (emptyKeys.length) {
|
|
105
|
+
console.log(chalk.yellow(' ⚠️ Empty values:'));
|
|
106
|
+
emptyKeys.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
|
|
107
|
+
}
|
|
108
|
+
if (opts.checkValues && diff.valueMismatches.length) {
|
|
109
|
+
console.log(chalk.yellow(' ⚠️ Value mismatches:'));
|
|
110
|
+
diff.valueMismatches.forEach(({ key, expected, actual }) => console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`)));
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
68
113
|
}
|
|
69
|
-
|
|
114
|
+
opts.collect?.(entry);
|
|
70
115
|
}
|
|
71
116
|
return { exitWithError };
|
|
72
117
|
}
|
|
@@ -3,8 +3,11 @@ export type Options = {
|
|
|
3
3
|
isCiMode: boolean;
|
|
4
4
|
isYesMode: boolean;
|
|
5
5
|
allowDuplicates: boolean;
|
|
6
|
+
json: boolean;
|
|
6
7
|
envFlag: string | null;
|
|
7
8
|
exampleFlag: string | null;
|
|
9
|
+
ignore: string[];
|
|
10
|
+
ignoreRegex: RegExp[];
|
|
8
11
|
cwd: string;
|
|
9
12
|
};
|
|
10
13
|
type RawOptions = {
|
|
@@ -12,8 +15,11 @@ type RawOptions = {
|
|
|
12
15
|
ci?: boolean;
|
|
13
16
|
yes?: boolean;
|
|
14
17
|
allowDuplicates?: boolean;
|
|
18
|
+
json?: boolean;
|
|
15
19
|
env?: string;
|
|
16
20
|
example?: string;
|
|
21
|
+
ignore?: string | string[];
|
|
22
|
+
ignoreRegex?: string | string[];
|
|
17
23
|
};
|
|
18
24
|
export declare function normalizeOptions(raw: RawOptions): Options;
|
|
19
25
|
export {};
|
|
@@ -5,6 +5,25 @@ export function normalizeOptions(raw) {
|
|
|
5
5
|
const isCiMode = Boolean(raw.ci);
|
|
6
6
|
const isYesMode = Boolean(raw.yes);
|
|
7
7
|
const allowDuplicates = Boolean(raw.allowDuplicates);
|
|
8
|
+
const json = Boolean(raw.json);
|
|
9
|
+
const parseList = (val) => {
|
|
10
|
+
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
11
|
+
return arr
|
|
12
|
+
.flatMap((s) => s.split(','))
|
|
13
|
+
.map((s) => s.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
};
|
|
16
|
+
const ignore = parseList(raw.ignore);
|
|
17
|
+
const ignoreRegex = [];
|
|
18
|
+
for (const pattern of parseList(raw.ignoreRegex)) {
|
|
19
|
+
try {
|
|
20
|
+
ignoreRegex.push(new RegExp(pattern));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
console.error(chalk.red(`❌ Error: invalid --ignore-regex pattern: ${pattern}`));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
8
27
|
if (isCiMode && isYesMode) {
|
|
9
28
|
console.log(chalk.yellow('⚠️ Both --ci and --yes provided; proceeding with --yes.'));
|
|
10
29
|
}
|
|
@@ -16,8 +35,11 @@ export function normalizeOptions(raw) {
|
|
|
16
35
|
isCiMode,
|
|
17
36
|
isYesMode,
|
|
18
37
|
allowDuplicates,
|
|
38
|
+
json,
|
|
19
39
|
envFlag,
|
|
20
40
|
exampleFlag,
|
|
41
|
+
ignore,
|
|
42
|
+
ignoreRegex,
|
|
21
43
|
cwd,
|
|
22
44
|
};
|
|
23
45
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function filterIgnoredKeys(keys: string[], ignore: string[], ignoreRegex: RegExp[]): string[];
|
package/package.json
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-diff",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A
|
|
5
|
+
"description": "A CLI tool to find differences between .env and .env.example / .env.* files.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dotenv-diff": "dist/bin/dotenv-diff.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
11
18
|
"files": [
|
|
12
19
|
"dist/",
|
|
13
20
|
"README.md",
|
|
14
21
|
"LICENSE"
|
|
15
22
|
],
|
|
23
|
+
"sideEffects": false,
|
|
16
24
|
"scripts": {
|
|
17
25
|
"build": "tsc",
|
|
18
26
|
"dev": "vitest --watch",
|
|
19
|
-
"test": "vitest",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:unit": "vitest run test/unit",
|
|
29
|
+
"test:e2e": "vitest run test/e2e",
|
|
20
30
|
"lint": "eslint ./src --ext .ts",
|
|
21
31
|
"format": "prettier --write \"src/**/*.ts\" \"src/*.ts\"",
|
|
22
|
-
"start": "node dist/bin/dotenv-diff.js"
|
|
32
|
+
"start": "node dist/bin/dotenv-diff.js",
|
|
33
|
+
"prepublishOnly": "npm run lint && npm test && npm run build"
|
|
23
34
|
},
|
|
24
35
|
"author": "Chrilleweb",
|
|
25
36
|
"license": "MIT",
|
|
@@ -36,8 +47,12 @@
|
|
|
36
47
|
"type": "git",
|
|
37
48
|
"url": "https://github.com/Chrilleweb/dotenv-diff.git"
|
|
38
49
|
},
|
|
50
|
+
"homepage": "https://github.com/Chrilleweb/dotenv-diff#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/Chrilleweb/dotenv-diff/issues"
|
|
53
|
+
},
|
|
39
54
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
55
|
+
"node": ">=20.0.0"
|
|
41
56
|
},
|
|
42
57
|
"dependencies": {
|
|
43
58
|
"chalk": "^5.4.1",
|