dotenv-diff 2.0.0 → 2.1.0

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
@@ -42,6 +42,27 @@ dotenv-diff --scan-usage
42
42
  ```
43
43
  This scans your entire codebase to detect which environment variables are actually used in the code — and compare them against your `.env` file(s).
44
44
 
45
+ ## CI/CD integration with `--ci` option
46
+ You can scan and compare against specific environment file, eg. `.env.example`
47
+ This is useful for CI/CD pipelines to ensure that the environment variables used in your code match those defined in your `.env.example` file.
48
+
49
+ Use the `--ci` flag for automated environments. This enables strict mode where the tool exits with code 1 on any issues, making it perfect for CI/CD pipelines.
50
+
51
+ And the `--example` option allows you to specify which `.env.example` file to compare against.
52
+
53
+ ### Use it in Github Actions:
54
+
55
+ ```yaml
56
+ - name: Check environment variables
57
+ run: dotenv-diff --scan-usage --example .env.example --ci
58
+ ```
59
+
60
+ You can also change the comparison file by using the `--example` flag to point to a different `.env.example` file.
61
+
62
+ ```bash
63
+ dotenv-diff --scan-usage --example .env.example.staging --ci
64
+ ```
65
+
45
66
  ## Show unused variables
46
67
 
47
68
  Use `--show-unused` together with `--scan-usage` to list variables that are defined in `.env` but never used in your codebase:
@@ -19,10 +19,12 @@ export async function run(program) {
19
19
  exclude: opts.excludeFiles,
20
20
  ignore: opts.ignore,
21
21
  ignoreRegex: opts.ignoreRegex,
22
+ examplePath: opts.exampleFlag || undefined,
22
23
  envPath,
23
24
  json: opts.json,
24
25
  showUnused: opts.showUnused,
25
26
  showStats: opts.showStats,
27
+ isCiMode: opts.isCiMode,
26
28
  });
27
29
  process.exit(exitWithError ? 1 : 0);
28
30
  }
@@ -1,9 +1,11 @@
1
1
  import { type ScanOptions } from '../services/codeBaseScanner.js';
2
2
  export interface ScanUsageOptions extends ScanOptions {
3
3
  envPath?: string;
4
+ examplePath?: string;
4
5
  json: boolean;
5
6
  showUnused: boolean;
6
7
  showStats: boolean;
8
+ isCiMode?: boolean;
7
9
  }
8
10
  export interface ScanJsonEntry {
9
11
  stats: {
@@ -28,14 +30,18 @@ export interface ScanJsonEntry {
28
30
  pattern: string;
29
31
  context: string;
30
32
  }>;
33
+ comparedAgainst?: string;
34
+ totalEnvVariables?: number;
31
35
  }
32
36
  /**
33
37
  * Scans codebase for environment variable usage and compares with .env file
34
38
  * @param {ScanUsageOptions} opts - Scan configuration options
35
39
  * @param {string} [opts.envPath] - Path to .env file for comparison
40
+ * @param {string} [opts.examplePath] - Path to .env.example file for comparison
36
41
  * @param {boolean} opts.json - Output as JSON instead of console
37
42
  * @param {boolean} opts.showUnused - Show unused variables from .env
38
43
  * @param {boolean} opts.showStats - Show detailed statistics
44
+ * @param {boolean} [opts.isCiMode] - Run in CI mode (exit with error code)
39
45
  * @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
40
46
  */
41
47
  export declare function scanUsage(opts: ScanUsageOptions): Promise<{
@@ -1,4 +1,6 @@
1
1
  import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
2
4
  import { parseEnvFile } from '../lib/parseEnv.js';
3
5
  import { scanCodebase, compareWithEnvFiles, } from '../services/codeBaseScanner.js';
4
6
  import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
@@ -6,9 +8,11 @@ import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
6
8
  * Scans codebase for environment variable usage and compares with .env file
7
9
  * @param {ScanUsageOptions} opts - Scan configuration options
8
10
  * @param {string} [opts.envPath] - Path to .env file for comparison
11
+ * @param {string} [opts.examplePath] - Path to .env.example file for comparison
9
12
  * @param {boolean} opts.json - Output as JSON instead of console
10
13
  * @param {boolean} opts.showUnused - Show unused variables from .env
11
14
  * @param {boolean} opts.showStats - Show detailed statistics
15
+ * @param {boolean} [opts.isCiMode] - Run in CI mode (exit with error code)
12
16
  * @returns {Promise<{exitWithError: boolean}>} Returns true if missing variables found
13
17
  */
14
18
  export async function scanUsage(opts) {
@@ -18,31 +22,61 @@ export async function scanUsage(opts) {
18
22
  }
19
23
  // Scan the codebase
20
24
  let scanResult = await scanCodebase(opts);
21
- // If we have an env file, compare with it
25
+ // Determine which file to compare against
26
+ const compareFile = determineComparisonFile(opts);
22
27
  let envVariables = {};
23
- if (opts.envPath) {
28
+ let comparedAgainst = '';
29
+ if (compareFile) {
24
30
  try {
25
- const envFull = parseEnvFile(opts.envPath);
31
+ const envFull = parseEnvFile(compareFile.path);
26
32
  const envKeys = filterIgnoredKeys(Object.keys(envFull), opts.ignore, opts.ignoreRegex);
27
33
  envVariables = Object.fromEntries(envKeys.map((k) => [k, envFull[k]]));
28
34
  scanResult = compareWithEnvFiles(scanResult, envVariables);
35
+ comparedAgainst = compareFile.name;
29
36
  }
30
37
  catch (error) {
38
+ const errorMessage = `⚠️ Could not read ${compareFile.name}: ${compareFile.path} - ${error}`;
39
+ if (opts.isCiMode) {
40
+ // In CI mode, exit with error if file doesn't exist
41
+ console.error(chalk.red(`❌ ${errorMessage}`));
42
+ return { exitWithError: true };
43
+ }
31
44
  if (!opts.json) {
32
- console.log(chalk.yellow(`⚠️ Could not read env file: ${opts.envPath} - ${error}`));
45
+ console.log(chalk.yellow(errorMessage));
33
46
  }
34
47
  }
35
48
  }
36
49
  // Prepare JSON output
37
50
  if (opts.json) {
38
- const jsonOutput = createJsonOutput(scanResult, opts);
51
+ const jsonOutput = createJsonOutput(scanResult, opts, comparedAgainst, Object.keys(envVariables).length);
39
52
  console.log(JSON.stringify(jsonOutput, null, 2));
40
53
  return { exitWithError: scanResult.missing.length > 0 };
41
54
  }
42
55
  // Console output
43
- return outputToConsole(scanResult, opts);
56
+ return outputToConsole(scanResult, opts, comparedAgainst);
57
+ }
58
+ /**
59
+ * Determines which file to use for comparison based on provided options
60
+ */
61
+ function determineComparisonFile(opts) {
62
+ // Priority: explicit flags first, then auto-discovery
63
+ if (opts.examplePath && fs.existsSync(opts.examplePath)) {
64
+ return { path: opts.examplePath, name: path.basename(opts.examplePath) };
65
+ }
66
+ if (opts.envPath && fs.existsSync(opts.envPath)) {
67
+ return { path: opts.envPath, name: path.basename(opts.envPath) };
68
+ }
69
+ // Auto-discovery: look for common env files
70
+ const candidates = ['.env', '.env.example', '.env.local', '.env.production'];
71
+ for (const candidate of candidates) {
72
+ const fullPath = path.resolve(opts.cwd, candidate);
73
+ if (fs.existsSync(fullPath)) {
74
+ return { path: fullPath, name: candidate };
75
+ }
76
+ }
77
+ return null;
44
78
  }
45
- function createJsonOutput(scanResult, opts) {
79
+ function createJsonOutput(scanResult, opts, comparedAgainst, totalEnvVariables) {
46
80
  // Group usages by variable for missing variables
47
81
  const missingGrouped = scanResult.missing.map((variable) => ({
48
82
  variable,
@@ -60,6 +94,11 @@ function createJsonOutput(scanResult, opts) {
60
94
  missing: missingGrouped,
61
95
  unused: scanResult.unused,
62
96
  };
97
+ // Add comparison info if we compared against a file
98
+ if (comparedAgainst) {
99
+ output.comparedAgainst = comparedAgainst;
100
+ output.totalEnvVariables = totalEnvVariables;
101
+ }
63
102
  // Optionally include all usages
64
103
  if (opts.showStats) {
65
104
  output.allUsages = scanResult.used.map((u) => ({
@@ -72,8 +111,13 @@ function createJsonOutput(scanResult, opts) {
72
111
  }
73
112
  return output;
74
113
  }
75
- function outputToConsole(scanResult, opts) {
114
+ function outputToConsole(scanResult, opts, comparedAgainst) {
76
115
  let exitWithError = false;
116
+ // Show what we're comparing against
117
+ if (comparedAgainst) {
118
+ console.log(chalk.gray(`📋 Comparing codebase usage against: ${comparedAgainst}`));
119
+ console.log();
120
+ }
77
121
  // Show stats if requested
78
122
  if (opts.showStats) {
79
123
  console.log(chalk.bold('📊 Scan Statistics:'));
@@ -82,8 +126,8 @@ function outputToConsole(scanResult, opts) {
82
126
  console.log(chalk.gray(` Unique variables: ${scanResult.stats.uniqueVariables}`));
83
127
  console.log();
84
128
  }
85
- // Always show found variables when not in .env comparison mode
86
- if (!opts.envPath || scanResult.missing.length === 0) {
129
+ // Always show found variables when not comparing or when no missing variables
130
+ if (!comparedAgainst || scanResult.missing.length === 0) {
87
131
  console.log(chalk.green(`✅ Found ${scanResult.stats.uniqueVariables} unique environment variables in use`));
88
132
  console.log();
89
133
  // List all variables found (if any)
@@ -113,10 +157,11 @@ function outputToConsole(scanResult, opts) {
113
157
  console.log();
114
158
  }
115
159
  }
116
- // Missing variables (used in code but not in .env)
160
+ // Missing variables (used in code but not in env file)
117
161
  if (scanResult.missing.length > 0) {
118
162
  exitWithError = true;
119
- console.log(chalk.red('❌ Missing in .env:'));
163
+ const fileType = comparedAgainst || 'environment file';
164
+ console.log(chalk.red(`❌ Missing in ${fileType}:`));
120
165
  const grouped = scanResult.missing.reduce((acc, variable) => {
121
166
  const usages = scanResult.used.filter((u) => u.variable === variable);
122
167
  acc[variable] = usages;
@@ -134,21 +179,29 @@ function outputToConsole(scanResult, opts) {
134
179
  }
135
180
  }
136
181
  console.log();
182
+ // CI mode specific message
183
+ if (opts.isCiMode) {
184
+ console.log(chalk.red(`💥 Found ${scanResult.missing.length} missing environment variable(s).`));
185
+ console.log(chalk.red(` Add these variables to ${comparedAgainst || 'your environment file'} to fix this error.`));
186
+ console.log();
187
+ }
137
188
  }
138
- // Unused variables (in .env but not used in code)
189
+ // Unused variables (in env file but not used in code)
139
190
  if (opts.showUnused && scanResult.unused.length > 0) {
140
- console.log(chalk.yellow('⚠️ Unused in codebase:'));
191
+ const fileType = comparedAgainst || 'environment file';
192
+ console.log(chalk.yellow(`⚠️ Unused in codebase (defined in ${fileType}):`));
141
193
  scanResult.unused.forEach((variable) => {
142
194
  console.log(chalk.yellow(` - ${variable}`));
143
195
  });
144
196
  console.log();
145
197
  }
146
- // Success message for .env comparison
147
- if (opts.envPath && scanResult.missing.length === 0) {
148
- console.log(chalk.green('✅ All used environment variables are defined in .env'));
198
+ // Success message for env file comparison
199
+ if (comparedAgainst && scanResult.missing.length === 0) {
200
+ console.log(chalk.green(`✅ All used environment variables are defined in ${comparedAgainst}`));
149
201
  if (opts.showUnused && scanResult.unused.length === 0) {
150
202
  console.log(chalk.green('✅ No unused environment variables found'));
151
203
  }
204
+ console.log();
152
205
  }
153
206
  return { exitWithError };
154
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotenv-diff",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "A CLI tool to find differences between .env and .env.example / .env.* files. And optionally scan your codebase to find out which environment variables are actually used in your code.",
6
6
  "bin": {