dotenv-diff 1.6.2 → 1.6.3

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,14 @@ 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
+ ## Output format in JSON
47
+
48
+ You can output the results in JSON format using the `--json` option:
49
+
50
+ ```bash
51
+ dotenv-diff --json
52
+ ```
53
+
46
54
  ## Compare specific files
47
55
 
48
56
  Override the autoscan and compare exactly two files:
@@ -8,5 +8,6 @@ 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('--json', 'Output results in JSON format');
12
13
  }
@@ -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,24 +23,26 @@ export async function run(program) {
23
23
  }
24
24
  process.exit(1);
25
25
  }
26
- const { exitWithError } = await compareMany([
27
- {
28
- envName: path.basename(opts.envFlag),
29
- envPath: opts.envFlag,
30
- examplePath: opts.exampleFlag,
31
- },
32
- ], {
26
+ const report = [];
27
+ const { exitWithError } = await compareMany([{ envName: path.basename(opts.envFlag), envPath: opts.envFlag, examplePath: opts.exampleFlag }], {
33
28
  checkValues: opts.checkValues,
34
29
  cwd: opts.cwd,
35
30
  allowDuplicates: opts.allowDuplicates,
31
+ json: opts.json,
32
+ collect: (e) => report.push(e),
36
33
  });
34
+ if (opts.json) {
35
+ console.log(JSON.stringify(report, null, 2));
36
+ }
37
37
  process.exit(exitWithError ? 1 : 0);
38
38
  }
39
+ // Auto-discovery flow
39
40
  const d = discoverEnvFiles({
40
41
  cwd: opts.cwd,
41
42
  envFlag: opts.envFlag,
42
43
  exampleFlag: opts.exampleFlag,
43
44
  });
45
+ // Init cases (may create files or early-exit)
44
46
  const res = await ensureFilesOrPrompt({
45
47
  cwd: d.cwd,
46
48
  primaryEnv: d.primaryEnv,
@@ -49,14 +51,24 @@ export async function run(program) {
49
51
  isYesMode: opts.isYesMode,
50
52
  isCiMode: opts.isCiMode,
51
53
  });
52
- if (res.shouldExit)
54
+ if (res.shouldExit) {
55
+ // For JSON mode, emit an empty report to keep output machine-friendly (optional; safe).
56
+ if (opts.json)
57
+ console.log(JSON.stringify([], null, 2));
53
58
  process.exit(res.exitCode);
54
- // compare all pairs
59
+ }
60
+ // Compare all discovered pairs
55
61
  const pairs = pairWithExample(d);
62
+ const report = [];
56
63
  const { exitWithError } = await compareMany(pairs, {
57
64
  checkValues: opts.checkValues,
58
65
  cwd: opts.cwd,
59
66
  allowDuplicates: opts.allowDuplicates,
67
+ json: opts.json,
68
+ collect: (e) => report.push(e),
60
69
  });
70
+ if (opts.json) {
71
+ console.log(JSON.stringify(report, null, 2));
72
+ }
61
73
  process.exit(exitWithError ? 1 : 0);
62
74
  }
@@ -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,8 @@ export declare function compareMany(pairs: Array<{
6
32
  checkValues: boolean;
7
33
  cwd: string;
8
34
  allowDuplicates?: boolean;
35
+ json?: boolean;
36
+ collect?: (entry: CompareJsonEntry) => void;
9
37
  }): Promise<{
10
38
  exitWithError: boolean;
11
39
  }>;
@@ -8,65 +8,103 @@ import { findDuplicateKeys } from '../services/duplicates.js';
8
8
  export async function compareMany(pairs, opts) {
9
9
  let exitWithError = false;
10
10
  for (const { envName, envPath, examplePath } of pairs) {
11
+ const exampleName = path.basename(examplePath);
12
+ const entry = { env: envName, example: exampleName };
11
13
  if (!fs.existsSync(envPath) || !fs.existsSync(examplePath)) {
12
- console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${path.basename(examplePath)}...`));
13
- console.log(chalk.yellow(' ⚠️ Skipping: missing matching example file.'));
14
- console.log();
14
+ if (!opts.json) {
15
+ console.log(chalk.bold(`🔍 Comparing ${envName} ${exampleName}...`));
16
+ console.log(chalk.yellow(' ⚠️ Skipping: missing matching example file.'));
17
+ console.log();
18
+ }
19
+ entry.skipped = { reason: 'missing file' };
20
+ opts.collect?.(entry);
15
21
  continue;
16
22
  }
17
- console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${path.basename(examplePath)}...`));
23
+ if (!opts.json) {
24
+ console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${exampleName}...`));
25
+ }
26
+ // Git ignore hint (only when not JSON)
18
27
  warnIfEnvNotIgnored({
19
28
  cwd: opts.cwd,
20
29
  envFile: envName,
21
- log: (msg) => console.log(msg.replace(/^/gm, ' ')),
30
+ log: (msg) => {
31
+ if (!opts.json)
32
+ console.log(msg.replace(/^/gm, ' '));
33
+ },
22
34
  });
35
+ // Duplicate detection
23
36
  if (!opts.allowDuplicates) {
24
37
  const dupsEnv = findDuplicateKeys(envPath);
25
- if (dupsEnv.length > 0) {
26
- console.log(chalk.yellow(` ⚠️ Duplicate keys in ${envName} (last occurrence wins):`));
27
- dupsEnv.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
28
- }
29
- const exName = path.basename(examplePath);
30
38
  const dupsEx = findDuplicateKeys(examplePath);
31
- if (dupsEx.length > 0) {
32
- console.log(chalk.yellow(` ⚠️ Duplicate keys in ${exName} (last occurrence wins):`));
33
- dupsEx.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
39
+ if (dupsEnv.length || dupsEx.length) {
40
+ entry.duplicates = {};
41
+ }
42
+ if (dupsEnv.length) {
43
+ entry.duplicates.env = dupsEnv;
44
+ if (!opts.json) {
45
+ console.log(chalk.yellow(` ⚠️ Duplicate keys in ${envName} (last occurrence wins):`));
46
+ dupsEnv.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
47
+ }
48
+ }
49
+ if (dupsEx.length) {
50
+ entry.duplicates.example = dupsEx;
51
+ if (!opts.json) {
52
+ console.log(chalk.yellow(` ⚠️ Duplicate keys in ${exampleName} (last occurrence wins):`));
53
+ dupsEx.forEach(({ key, count }) => console.log(chalk.yellow(` - ${key} (${count} occurrences)`)));
54
+ }
34
55
  }
35
56
  }
57
+ // Diff + empty
36
58
  const current = parseEnvFile(envPath);
37
59
  const example = parseEnvFile(examplePath);
38
60
  const diff = diffEnv(current, example, opts.checkValues);
39
61
  const emptyKeys = Object.entries(current)
40
62
  .filter(([, v]) => (v ?? '').trim() === '')
41
63
  .map(([k]) => k);
42
- if (diff.missing.length === 0 &&
64
+ const allOk = diff.missing.length === 0 &&
43
65
  diff.extra.length === 0 &&
44
66
  emptyKeys.length === 0 &&
45
- diff.valueMismatches.length === 0) {
46
- console.log(chalk.green(' ✅ All keys match.'));
47
- console.log();
67
+ diff.valueMismatches.length === 0;
68
+ if (allOk) {
69
+ entry.ok = true;
70
+ if (!opts.json) {
71
+ console.log(chalk.green(' ✅ All keys match.'));
72
+ console.log();
73
+ }
74
+ opts.collect?.(entry);
48
75
  continue;
49
76
  }
50
- if (diff.missing.length > 0) {
77
+ if (diff.missing.length) {
78
+ entry.missing = diff.missing;
51
79
  exitWithError = true;
52
- console.log(chalk.red(' ❌ Missing keys:'));
53
- diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
54
- }
55
- if (diff.extra.length > 0) {
56
- console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
57
- diff.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
58
80
  }
59
- if (emptyKeys.length > 0) {
60
- console.log(chalk.yellow(' ⚠️ Empty values:'));
61
- emptyKeys.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
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;
62
87
  }
63
- if (opts.checkValues && diff.valueMismatches.length > 0) {
64
- console.log(chalk.yellow(' ⚠️ Value mismatches:'));
65
- diff.valueMismatches.forEach(({ key, expected, actual }) => {
66
- console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`));
67
- });
88
+ if (!opts.json) {
89
+ if (diff.missing.length) {
90
+ console.log(chalk.red(' ❌ Missing keys:'));
91
+ diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
92
+ }
93
+ if (diff.extra.length) {
94
+ console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
95
+ diff.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
96
+ }
97
+ if (emptyKeys.length) {
98
+ console.log(chalk.yellow(' ⚠️ Empty values:'));
99
+ emptyKeys.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
100
+ }
101
+ if (opts.checkValues && diff.valueMismatches.length) {
102
+ console.log(chalk.yellow(' ⚠️ Value mismatches:'));
103
+ diff.valueMismatches.forEach(({ key, expected, actual }) => console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`)));
104
+ }
105
+ console.log();
68
106
  }
69
- console.log();
107
+ opts.collect?.(entry);
70
108
  }
71
109
  return { exitWithError };
72
110
  }
@@ -3,6 +3,7 @@ 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;
8
9
  cwd: string;
@@ -12,6 +13,7 @@ type RawOptions = {
12
13
  ci?: boolean;
13
14
  yes?: boolean;
14
15
  allowDuplicates?: boolean;
16
+ json?: boolean;
15
17
  env?: string;
16
18
  example?: string;
17
19
  };
@@ -5,6 +5,7 @@ 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);
8
9
  if (isCiMode && isYesMode) {
9
10
  console.log(chalk.yellow('⚠️ Both --ci and --yes provided; proceeding with --yes.'));
10
11
  }
@@ -16,6 +17,7 @@ export function normalizeOptions(raw) {
16
17
  isCiMode,
17
18
  isYesMode,
18
19
  allowDuplicates,
20
+ json,
19
21
  envFlag,
20
22
  exampleFlag,
21
23
  cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotenv-diff",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "type": "module",
5
5
  "description": "A small CLI and library to find differences between .env and .env.example files.",
6
6
  "bin": {