dotenv-diff 1.6.3 → 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,27 @@ 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
+
55
+ ## Ignore specific keys
56
+
57
+ Exclude certain keys from the comparison using `--ignore` for exact names or `--ignore-regex` for patterns:
58
+
59
+ ```bash
60
+ dotenv-diff --ignore API_KEY,SESSION_ID
61
+ dotenv-diff --ignore-regex '^SECRET_'
62
+ dotenv-diff --ignore API_KEY --ignore-regex '^SECRET_'
63
+ ```
64
+
65
+ Ignored keys are removed from all warnings and do not affect the exit code.
66
+
46
67
  ## Output format in JSON
47
68
 
48
69
  You can output the results in JSON format using the `--json` option:
@@ -9,5 +9,8 @@ export function createProgram() {
9
9
  .option('--env <file>', 'Path to a specific .env file')
10
10
  .option('--example <file>', 'Path to a specific .env.example file')
11
11
  .option('--allow-duplicates', 'Do not warn about duplicate keys in .env* files')
12
- .option('--json', 'Output results in JSON format');
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')
15
+ .option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)');
13
16
  }
@@ -24,11 +24,20 @@ export async function run(program) {
24
24
  process.exit(1);
25
25
  }
26
26
  const report = [];
27
- const { exitWithError } = await compareMany([{ envName: path.basename(opts.envFlag), envPath: opts.envFlag, examplePath: opts.exampleFlag }], {
27
+ const { exitWithError } = await compareMany([
28
+ {
29
+ envName: path.basename(opts.envFlag),
30
+ envPath: opts.envFlag,
31
+ examplePath: opts.exampleFlag,
32
+ },
33
+ ], {
28
34
  checkValues: opts.checkValues,
29
35
  cwd: opts.cwd,
30
36
  allowDuplicates: opts.allowDuplicates,
31
37
  json: opts.json,
38
+ ignore: opts.ignore,
39
+ ignoreRegex: opts.ignoreRegex,
40
+ only: opts.only,
32
41
  collect: (e) => report.push(e),
33
42
  });
34
43
  if (opts.json) {
@@ -65,6 +74,9 @@ export async function run(program) {
65
74
  cwd: opts.cwd,
66
75
  allowDuplicates: opts.allowDuplicates,
67
76
  json: opts.json,
77
+ ignore: opts.ignore,
78
+ ignoreRegex: opts.ignoreRegex,
79
+ only: opts.only,
68
80
  collect: (e) => report.push(e),
69
81
  });
70
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;
@@ -33,7 +8,10 @@ export declare function compareMany(pairs: Array<{
33
8
  cwd: string;
34
9
  allowDuplicates?: boolean;
35
10
  json?: boolean;
11
+ ignore: string[];
12
+ ignoreRegex: RegExp[];
36
13
  collect?: (entry: CompareJsonEntry) => void;
14
+ only?: Category[];
37
15
  }): Promise<{
38
16
  exitWithError: boolean;
39
17
  }>;
@@ -5,8 +5,21 @@ 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;
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
+ };
10
23
  for (const { envName, envPath, examplePath } of pairs) {
11
24
  const exampleName = path.basename(examplePath);
12
25
  const entry = { env: envName, example: exampleName };
@@ -24,18 +37,29 @@ export async function compareMany(pairs, opts) {
24
37
  console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
25
38
  }
26
39
  // Git ignore hint (only when not JSON)
27
- warnIfEnvNotIgnored({
28
- cwd: opts.cwd,
29
- envFile: envName,
30
- log: (msg) => {
31
- if (!opts.json)
32
- console.log(msg.replace(/^/gm, ' '));
33
- },
34
- });
35
- // Duplicate detection
36
- if (!opts.allowDuplicates) {
37
- const dupsEnv = findDuplicateKeys(envPath);
38
- const dupsEx = findDuplicateKeys(examplePath);
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) &&
60
+ !opts.ignoreRegex.some((rx) => rx.test(key)));
61
+ dupsEx = findDuplicateKeys(examplePath).filter(({ key }) => !opts.ignore.includes(key) &&
62
+ !opts.ignoreRegex.some((rx) => rx.test(key)));
39
63
  if (dupsEnv.length || dupsEx.length) {
40
64
  entry.duplicates = {};
41
65
  }
@@ -55,16 +79,29 @@ export async function compareMany(pairs, opts) {
55
79
  }
56
80
  }
57
81
  // Diff + empty
58
- const current = parseEnvFile(envPath);
59
- const example = parseEnvFile(examplePath);
82
+ const currentFull = parseEnvFile(envPath);
83
+ const exampleFull = parseEnvFile(examplePath);
84
+ const currentKeys = filterIgnoredKeys(Object.keys(currentFull), opts.ignore, opts.ignoreRegex);
85
+ const exampleKeys = filterIgnoredKeys(Object.keys(exampleFull), opts.ignore, opts.ignoreRegex);
86
+ const current = Object.fromEntries(currentKeys.map((k) => [k, currentFull[k]]));
87
+ const example = Object.fromEntries(exampleKeys.map((k) => [k, exampleFull[k]]));
60
88
  const diff = diffEnv(current, example, opts.checkValues);
61
89
  const emptyKeys = Object.entries(current)
62
90
  .filter(([, v]) => (v ?? '').trim() === '')
63
91
  .map(([k]) => k);
64
- const allOk = diff.missing.length === 0 &&
65
- diff.extra.length === 0 &&
66
- emptyKeys.length === 0 &&
67
- 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;
68
105
  if (allOk) {
69
106
  entry.ok = true;
70
107
  if (!opts.json) {
@@ -74,33 +111,51 @@ export async function compareMany(pairs, opts) {
74
111
  opts.collect?.(entry);
75
112
  continue;
76
113
  }
77
- if (diff.missing.length) {
78
- 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;
79
126
  exitWithError = true;
127
+ totals.empty += filtered.empty.length;
80
128
  }
81
- if (diff.extra.length)
82
- entry.extra = diff.extra;
83
- if (emptyKeys.length)
84
- entry.empty = emptyKeys;
85
- if (opts.checkValues && diff.valueMismatches.length) {
86
- 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;
87
142
  }
88
143
  if (!opts.json) {
89
- if (diff.missing.length) {
144
+ if (filtered.missing.length) {
90
145
  console.log(chalk.red(' ❌ Missing keys:'));
91
- diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
146
+ filtered.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
92
147
  }
93
- if (diff.extra.length) {
148
+ if (filtered.extra.length) {
94
149
  console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
95
- diff.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
150
+ filtered.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
96
151
  }
97
- if (emptyKeys.length) {
152
+ if (filtered.empty.length) {
98
153
  console.log(chalk.yellow(' ⚠️ Empty values:'));
99
- emptyKeys.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
154
+ filtered.empty.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
100
155
  }
101
- if (opts.checkValues && diff.valueMismatches.length) {
156
+ if (filtered.mismatches.length) {
102
157
  console.log(chalk.yellow(' ⚠️ Value mismatches:'));
103
- 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}'`)));
104
159
  }
105
160
  console.log();
106
161
  }
@@ -1,21 +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
- cwd: string;
10
- };
11
- type RawOptions = {
12
- checkValues?: boolean;
13
- ci?: boolean;
14
- yes?: boolean;
15
- allowDuplicates?: boolean;
16
- json?: boolean;
17
- env?: string;
18
- example?: string;
19
- };
1
+ import { Options, RawOptions } from './types.js';
20
2
  export declare function normalizeOptions(raw: RawOptions): Options;
21
- export {};
@@ -1,11 +1,41 @@
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);
26
+ const onlyParsed = parseCategories(raw.only, '--only');
27
+ const only = onlyParsed.length ? onlyParsed : undefined;
28
+ const ignore = parseList(raw.ignore);
29
+ const ignoreRegex = [];
30
+ for (const pattern of parseList(raw.ignoreRegex)) {
31
+ try {
32
+ ignoreRegex.push(new RegExp(pattern));
33
+ }
34
+ catch {
35
+ console.error(chalk.red(`❌ Error: invalid --ignore-regex pattern: ${pattern}`));
36
+ process.exit(1);
37
+ }
38
+ }
9
39
  if (isCiMode && isYesMode) {
10
40
  console.log(chalk.yellow('⚠️ Both --ci and --yes provided; proceeding with --yes.'));
11
41
  }
@@ -20,6 +50,9 @@ export function normalizeOptions(raw) {
20
50
  json,
21
51
  envFlag,
22
52
  exampleFlag,
53
+ ignore,
54
+ ignoreRegex,
23
55
  cwd,
56
+ only,
24
57
  };
25
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
+ ];
@@ -0,0 +1 @@
1
+ export declare function filterIgnoredKeys(keys: string[], ignore: string[], ignoreRegex: RegExp[]): string[];
@@ -0,0 +1,3 @@
1
+ export function filterIgnoredKeys(keys, ignore, ignoreRegex) {
2
+ return keys.filter((k) => !ignore.includes(k) && !ignoreRegex.some((rx) => rx.test(k)));
3
+ }
package/package.json CHANGED
@@ -1,25 +1,36 @@
1
1
  {
2
2
  "name": "dotenv-diff",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "type": "module",
5
- "description": "A small CLI and library to find differences between .env and .env.example files.",
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": ">=14.0.0"
55
+ "node": ">=20.0.0"
41
56
  },
42
57
  "dependencies": {
43
58
  "chalk": "^5.4.1",