dotenv-diff 1.6.4 → 1.6.5
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 +9 -0
- package/dist/src/cli/program.js +2 -1
- package/dist/src/cli/run.js +2 -0
- package/dist/src/commands/compare.d.ts +2 -26
- package/dist/src/commands/compare.js +80 -32
- package/dist/src/config/options.d.ts +1 -24
- package/dist/src/config/options.js +20 -7
- package/dist/src/config/types.d.ts +53 -0
- package/dist/src/config/types.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,15 @@ 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
|
+
## Only show specific categories
|
|
47
|
+
|
|
48
|
+
Use the `--only` flag to restrict the comparison to specific categories. For example:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
dotenv-diff --only missing,extra
|
|
52
|
+
```
|
|
53
|
+
This will only show missing and extra keys, ignoring empty, mismatched, duplicate keys and so on.
|
|
54
|
+
|
|
46
55
|
## Ignore specific keys
|
|
47
56
|
|
|
48
57
|
Exclude certain keys from the comparison using `--ignore` for exact names or `--ignore-regex` for patterns:
|
package/dist/src/cli/program.js
CHANGED
|
@@ -11,5 +11,6 @@ export function createProgram() {
|
|
|
11
11
|
.option('--allow-duplicates', 'Do not warn about duplicate keys in .env* files')
|
|
12
12
|
.option('--ignore <keys>', 'Comma-separated list of keys to ignore')
|
|
13
13
|
.option('--ignore-regex <pattern>', 'Regex pattern to ignore matching keys')
|
|
14
|
-
.option('--json', 'Output results in JSON format')
|
|
14
|
+
.option('--json', 'Output results in JSON format')
|
|
15
|
+
.option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)');
|
|
15
16
|
}
|
package/dist/src/cli/run.js
CHANGED
|
@@ -37,6 +37,7 @@ export async function run(program) {
|
|
|
37
37
|
json: opts.json,
|
|
38
38
|
ignore: opts.ignore,
|
|
39
39
|
ignoreRegex: opts.ignoreRegex,
|
|
40
|
+
only: opts.only,
|
|
40
41
|
collect: (e) => report.push(e),
|
|
41
42
|
});
|
|
42
43
|
if (opts.json) {
|
|
@@ -75,6 +76,7 @@ export async function run(program) {
|
|
|
75
76
|
json: opts.json,
|
|
76
77
|
ignore: opts.ignore,
|
|
77
78
|
ignoreRegex: opts.ignoreRegex,
|
|
79
|
+
only: opts.only,
|
|
78
80
|
collect: (e) => report.push(e),
|
|
79
81
|
});
|
|
80
82
|
if (opts.json) {
|
|
@@ -1,29 +1,4 @@
|
|
|
1
|
-
|
|
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
|
+
import type { Category, CompareJsonEntry } from '../config/types.js';
|
|
27
2
|
export declare function compareMany(pairs: Array<{
|
|
28
3
|
envName: string;
|
|
29
4
|
envPath: string;
|
|
@@ -36,6 +11,7 @@ export declare function compareMany(pairs: Array<{
|
|
|
36
11
|
ignore: string[];
|
|
37
12
|
ignoreRegex: RegExp[];
|
|
38
13
|
collect?: (entry: CompareJsonEntry) => void;
|
|
14
|
+
only?: Category[];
|
|
39
15
|
}): Promise<{
|
|
40
16
|
exitWithError: boolean;
|
|
41
17
|
}>;
|
|
@@ -8,6 +8,18 @@ import { findDuplicateKeys } from '../services/duplicates.js';
|
|
|
8
8
|
import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
|
|
9
9
|
export async function compareMany(pairs, opts) {
|
|
10
10
|
let exitWithError = false;
|
|
11
|
+
const onlySet = opts.only?.length
|
|
12
|
+
? new Set(opts.only)
|
|
13
|
+
: undefined;
|
|
14
|
+
const run = (cat) => !onlySet || onlySet.has(cat);
|
|
15
|
+
const totals = {
|
|
16
|
+
missing: 0,
|
|
17
|
+
extra: 0,
|
|
18
|
+
empty: 0,
|
|
19
|
+
mismatch: 0,
|
|
20
|
+
duplicate: 0,
|
|
21
|
+
gitignore: 0,
|
|
22
|
+
};
|
|
11
23
|
for (const { envName, envPath, examplePath } of pairs) {
|
|
12
24
|
const exampleName = path.basename(examplePath);
|
|
13
25
|
const entry = { env: envName, example: exampleName };
|
|
@@ -25,19 +37,28 @@ export async function compareMany(pairs, opts) {
|
|
|
25
37
|
console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
|
|
26
38
|
}
|
|
27
39
|
// Git ignore hint (only when not JSON)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
let gitignoreUnsafe = false;
|
|
41
|
+
if (run('gitignore')) {
|
|
42
|
+
warnIfEnvNotIgnored({
|
|
43
|
+
cwd: opts.cwd,
|
|
44
|
+
envFile: envName,
|
|
45
|
+
log: (msg) => {
|
|
46
|
+
gitignoreUnsafe = true;
|
|
47
|
+
if (!opts.json)
|
|
48
|
+
console.log(msg.replace(/^/gm, ' '));
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// still call to keep previous hints? No—masked by --only.
|
|
54
|
+
}
|
|
55
|
+
// Duplicate detection (skip entirely if --only excludes it)
|
|
56
|
+
let dupsEnv = [];
|
|
57
|
+
let dupsEx = [];
|
|
58
|
+
if (!opts.allowDuplicates && run('duplicate')) {
|
|
59
|
+
dupsEnv = findDuplicateKeys(envPath).filter(({ key }) => !opts.ignore.includes(key) &&
|
|
39
60
|
!opts.ignoreRegex.some((rx) => rx.test(key)));
|
|
40
|
-
|
|
61
|
+
dupsEx = findDuplicateKeys(examplePath).filter(({ key }) => !opts.ignore.includes(key) &&
|
|
41
62
|
!opts.ignoreRegex.some((rx) => rx.test(key)));
|
|
42
63
|
if (dupsEnv.length || dupsEx.length) {
|
|
43
64
|
entry.duplicates = {};
|
|
@@ -68,10 +89,19 @@ export async function compareMany(pairs, opts) {
|
|
|
68
89
|
const emptyKeys = Object.entries(current)
|
|
69
90
|
.filter(([, v]) => (v ?? '').trim() === '')
|
|
70
91
|
.map(([k]) => k);
|
|
71
|
-
const
|
|
72
|
-
diff.
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
const filtered = {
|
|
93
|
+
missing: run('missing') ? diff.missing : [],
|
|
94
|
+
extra: run('extra') ? diff.extra : [],
|
|
95
|
+
empty: run('empty') ? emptyKeys : [],
|
|
96
|
+
mismatches: run('mismatch') && opts.checkValues ? diff.valueMismatches : [],
|
|
97
|
+
duplicatesEnv: run('duplicate') ? dupsEnv : [],
|
|
98
|
+
duplicatesEx: run('duplicate') ? dupsEx : [],
|
|
99
|
+
gitignoreUnsafe: run('gitignore') ? gitignoreUnsafe : false,
|
|
100
|
+
};
|
|
101
|
+
const allOk = filtered.missing.length === 0 &&
|
|
102
|
+
filtered.extra.length === 0 &&
|
|
103
|
+
filtered.empty.length === 0 &&
|
|
104
|
+
filtered.mismatches.length === 0;
|
|
75
105
|
if (allOk) {
|
|
76
106
|
entry.ok = true;
|
|
77
107
|
if (!opts.json) {
|
|
@@ -81,33 +111,51 @@ export async function compareMany(pairs, opts) {
|
|
|
81
111
|
opts.collect?.(entry);
|
|
82
112
|
continue;
|
|
83
113
|
}
|
|
84
|
-
if (
|
|
85
|
-
entry.missing =
|
|
114
|
+
if (filtered.missing.length) {
|
|
115
|
+
entry.missing = filtered.missing;
|
|
116
|
+
exitWithError = true;
|
|
117
|
+
totals.missing += filtered.missing.length;
|
|
118
|
+
}
|
|
119
|
+
if (filtered.extra.length) {
|
|
120
|
+
entry.extra = filtered.extra;
|
|
121
|
+
exitWithError = true;
|
|
122
|
+
totals.extra += filtered.extra.length;
|
|
123
|
+
}
|
|
124
|
+
if (filtered.empty.length) {
|
|
125
|
+
entry.empty = filtered.empty;
|
|
86
126
|
exitWithError = true;
|
|
127
|
+
totals.empty += filtered.empty.length;
|
|
87
128
|
}
|
|
88
|
-
if (
|
|
89
|
-
entry.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
if (filtered.mismatches.length) {
|
|
130
|
+
entry.valueMismatches = filtered.mismatches;
|
|
131
|
+
totals.mismatch += filtered.mismatches.length;
|
|
132
|
+
exitWithError = true;
|
|
133
|
+
}
|
|
134
|
+
if (filtered.duplicatesEnv.length || filtered.duplicatesEx.length) {
|
|
135
|
+
totals.duplicate +=
|
|
136
|
+
filtered.duplicatesEnv.length + filtered.duplicatesEx.length;
|
|
137
|
+
exitWithError = true;
|
|
138
|
+
}
|
|
139
|
+
if (filtered.gitignoreUnsafe) {
|
|
140
|
+
totals.gitignore += 1;
|
|
141
|
+
exitWithError = true;
|
|
94
142
|
}
|
|
95
143
|
if (!opts.json) {
|
|
96
|
-
if (
|
|
144
|
+
if (filtered.missing.length) {
|
|
97
145
|
console.log(chalk.red(' ❌ Missing keys:'));
|
|
98
|
-
|
|
146
|
+
filtered.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
|
|
99
147
|
}
|
|
100
|
-
if (
|
|
148
|
+
if (filtered.extra.length) {
|
|
101
149
|
console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
|
|
102
|
-
|
|
150
|
+
filtered.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
|
|
103
151
|
}
|
|
104
|
-
if (
|
|
152
|
+
if (filtered.empty.length) {
|
|
105
153
|
console.log(chalk.yellow(' ⚠️ Empty values:'));
|
|
106
|
-
|
|
154
|
+
filtered.empty.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
|
|
107
155
|
}
|
|
108
|
-
if (
|
|
156
|
+
if (filtered.mismatches.length) {
|
|
109
157
|
console.log(chalk.yellow(' ⚠️ Value mismatches:'));
|
|
110
|
-
|
|
158
|
+
filtered.mismatches.forEach(({ key, expected, actual }) => console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`)));
|
|
111
159
|
}
|
|
112
160
|
console.log();
|
|
113
161
|
}
|
|
@@ -1,25 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
checkValues: boolean;
|
|
3
|
-
isCiMode: boolean;
|
|
4
|
-
isYesMode: boolean;
|
|
5
|
-
allowDuplicates: boolean;
|
|
6
|
-
json: boolean;
|
|
7
|
-
envFlag: string | null;
|
|
8
|
-
exampleFlag: string | null;
|
|
9
|
-
ignore: string[];
|
|
10
|
-
ignoreRegex: RegExp[];
|
|
11
|
-
cwd: string;
|
|
12
|
-
};
|
|
13
|
-
type RawOptions = {
|
|
14
|
-
checkValues?: boolean;
|
|
15
|
-
ci?: boolean;
|
|
16
|
-
yes?: boolean;
|
|
17
|
-
allowDuplicates?: boolean;
|
|
18
|
-
json?: boolean;
|
|
19
|
-
env?: string;
|
|
20
|
-
example?: string;
|
|
21
|
-
ignore?: string | string[];
|
|
22
|
-
ignoreRegex?: string | string[];
|
|
23
|
-
};
|
|
1
|
+
import { Options, RawOptions } from './types.js';
|
|
24
2
|
export declare function normalizeOptions(raw: RawOptions): Options;
|
|
25
|
-
export {};
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { ALLOWED_CATEGORIES } from './types.js';
|
|
4
|
+
function parseList(val) {
|
|
5
|
+
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
6
|
+
return arr
|
|
7
|
+
.flatMap((s) => String(s).split(','))
|
|
8
|
+
.map((s) => s.trim())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
function parseCategories(val, flagName = '') {
|
|
12
|
+
const raw = parseList(val);
|
|
13
|
+
const bad = raw.filter((c) => !ALLOWED_CATEGORIES.includes(c));
|
|
14
|
+
if (bad.length) {
|
|
15
|
+
console.error(chalk.red(`❌ Error: invalid ${flagName} value(s): ${bad.join(', ')}. Allowed: ${ALLOWED_CATEGORIES.join(', ')}`));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
3
20
|
export function normalizeOptions(raw) {
|
|
4
21
|
const checkValues = raw.checkValues ?? false;
|
|
5
22
|
const isCiMode = Boolean(raw.ci);
|
|
6
23
|
const isYesMode = Boolean(raw.yes);
|
|
7
24
|
const allowDuplicates = Boolean(raw.allowDuplicates);
|
|
8
25
|
const json = Boolean(raw.json);
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
return arr
|
|
12
|
-
.flatMap((s) => s.split(','))
|
|
13
|
-
.map((s) => s.trim())
|
|
14
|
-
.filter(Boolean);
|
|
15
|
-
};
|
|
26
|
+
const onlyParsed = parseCategories(raw.only, '--only');
|
|
27
|
+
const only = onlyParsed.length ? onlyParsed : undefined;
|
|
16
28
|
const ignore = parseList(raw.ignore);
|
|
17
29
|
const ignoreRegex = [];
|
|
18
30
|
for (const pattern of parseList(raw.ignoreRegex)) {
|
|
@@ -41,5 +53,6 @@ export function normalizeOptions(raw) {
|
|
|
41
53
|
ignore,
|
|
42
54
|
ignoreRegex,
|
|
43
55
|
cwd,
|
|
56
|
+
only,
|
|
44
57
|
};
|
|
45
58
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const ALLOWED_CATEGORIES: readonly ["missing", "extra", "empty", "mismatch", "duplicate", "gitignore"];
|
|
2
|
+
export type Category = (typeof ALLOWED_CATEGORIES)[number];
|
|
3
|
+
export type Options = {
|
|
4
|
+
checkValues: boolean;
|
|
5
|
+
isCiMode: boolean;
|
|
6
|
+
isYesMode: boolean;
|
|
7
|
+
allowDuplicates: boolean;
|
|
8
|
+
json: boolean;
|
|
9
|
+
envFlag: string | null;
|
|
10
|
+
exampleFlag: string | null;
|
|
11
|
+
ignore: string[];
|
|
12
|
+
ignoreRegex: RegExp[];
|
|
13
|
+
cwd: string;
|
|
14
|
+
only?: Category[];
|
|
15
|
+
};
|
|
16
|
+
export type RawOptions = {
|
|
17
|
+
checkValues?: boolean;
|
|
18
|
+
ci?: boolean;
|
|
19
|
+
yes?: boolean;
|
|
20
|
+
allowDuplicates?: boolean;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
env?: string;
|
|
23
|
+
example?: string;
|
|
24
|
+
ignore?: string | string[];
|
|
25
|
+
ignoreRegex?: string | string[];
|
|
26
|
+
only?: string | string[];
|
|
27
|
+
};
|
|
28
|
+
export type CompareJsonEntry = {
|
|
29
|
+
env: string;
|
|
30
|
+
example: string;
|
|
31
|
+
skipped?: {
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
duplicates?: {
|
|
35
|
+
env?: Array<{
|
|
36
|
+
key: string;
|
|
37
|
+
count: number;
|
|
38
|
+
}>;
|
|
39
|
+
example?: Array<{
|
|
40
|
+
key: string;
|
|
41
|
+
count: number;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
missing?: string[];
|
|
45
|
+
extra?: string[];
|
|
46
|
+
empty?: string[];
|
|
47
|
+
valueMismatches?: Array<{
|
|
48
|
+
key: string;
|
|
49
|
+
expected: string;
|
|
50
|
+
actual: string;
|
|
51
|
+
}>;
|
|
52
|
+
ok?: boolean;
|
|
53
|
+
};
|