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 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:
@@ -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
  }
@@ -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
- 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
+ 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
- warnIfEnvNotIgnored({
29
- cwd: opts.cwd,
30
- envFile: envName,
31
- log: (msg) => {
32
- if (!opts.json)
33
- console.log(msg.replace(/^/gm, ' '));
34
- },
35
- });
36
- // Duplicate detection
37
- if (!opts.allowDuplicates) {
38
- const dupsEnv = findDuplicateKeys(envPath).filter(({ key }) => !opts.ignore.includes(key) &&
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
- const dupsEx = findDuplicateKeys(examplePath).filter(({ key }) => !opts.ignore.includes(key) &&
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 allOk = diff.missing.length === 0 &&
72
- diff.extra.length === 0 &&
73
- emptyKeys.length === 0 &&
74
- diff.valueMismatches.length === 0;
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 (diff.missing.length) {
85
- entry.missing = diff.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 (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;
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 (diff.missing.length) {
144
+ if (filtered.missing.length) {
97
145
  console.log(chalk.red(' ❌ Missing keys:'));
98
- diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
146
+ filtered.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
99
147
  }
100
- if (diff.extra.length) {
148
+ if (filtered.extra.length) {
101
149
  console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
102
- diff.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
150
+ filtered.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
103
151
  }
104
- if (emptyKeys.length) {
152
+ if (filtered.empty.length) {
105
153
  console.log(chalk.yellow(' ⚠️ Empty values:'));
106
- emptyKeys.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
154
+ filtered.empty.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
107
155
  }
108
- if (opts.checkValues && diff.valueMismatches.length) {
156
+ if (filtered.mismatches.length) {
109
157
  console.log(chalk.yellow(' ⚠️ Value mismatches:'));
110
- diff.valueMismatches.forEach(({ key, expected, actual }) => console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`)));
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
- export type Options = {
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 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
- };
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
+ };
@@ -0,0 +1,8 @@
1
+ export const ALLOWED_CATEGORIES = [
2
+ 'missing',
3
+ 'extra',
4
+ 'empty',
5
+ 'mismatch',
6
+ 'duplicate',
7
+ 'gitignore',
8
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotenv-diff",
3
- "version": "1.6.4",
3
+ "version": "1.6.5",
4
4
  "type": "module",
5
5
  "description": "A CLI tool to find differences between .env and .env.example / .env.* files.",
6
6
  "bin": {