dotenv-diff 2.1.3 → 2.1.4

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
@@ -50,11 +50,11 @@ Use the `--ci` flag for automated environments. This enables strict mode where t
50
50
 
51
51
  And the `--example` option allows you to specify which `.env.example` file to compare against.
52
52
 
53
- ### Use it in Github Actions:
53
+ ### Use it in Github Actions for at turborepo, Example:
54
54
 
55
55
  ```yaml
56
56
  - name: Check environment variables
57
- run: dotenv-diff --scan-usage --example .env.example --ci
57
+ run: dotenv-diff --ci --scan-usage --show-unused --example .env.example --include-files \"./src/**/*,../../packages/**/*\" --ignore VITE_MODE,NODE_ENV
58
58
  ```
59
59
 
60
60
  You can also change the comparison file by using the `--example` flag to point to a different `.env.example` file.
@@ -83,6 +83,42 @@ This will:
83
83
  - Also scan files in `../../packages`(like `packages/components/src/..`)
84
84
  - Ignore variables like VITE_MODE that you only use in special cases.
85
85
 
86
+ ## Automatic fixes with `--fix`
87
+
88
+ Use the `--fix` flag to automatically fix missing keys in your `.env` and remove duplicate keys.
89
+
90
+ ```bash
91
+ dotenv-diff --fix
92
+ ```
93
+
94
+ This will:
95
+ - Add any missing keys from `.env.example` to your `.env` file with empty values
96
+ - Remove duplicate keys in your `.env` file (keeping the last occurrence)
97
+
98
+ ## Use --fix with scan-usage
99
+
100
+ You can also combine `--fix` with `--scan-usage` to automatically add any missing keys that are used in your code but not defined in your `.env` file:
101
+
102
+ ```bash
103
+ dotenv-diff --scan-usage --fix
104
+ ```
105
+
106
+ Using `--fix`with `--scan-usage` will not detect duplicate keys, it will only add missing keys.
107
+
108
+ # Example workflow
109
+
110
+ 1. You add `process.env.NEW_API_KEY` in your code.
111
+ 2. You run `dotenv-diff --scan-usage --fix`.
112
+ 3. The tool automatically adds `NEW_API_KEY=` to your `.env` file.
113
+
114
+ ## Scan your codebase for environment variables and add it directly to .env.example?
115
+
116
+ ```bash
117
+ dotenv-diff --scan-usage --example .env.example --fix
118
+ ```
119
+
120
+ This scans your codebase for environment variable usage and adds any missing keys directly to your `.env.example` file with empty values.
121
+
86
122
  ## Show unused variables
87
123
 
88
124
  Use `--show-unused` together with `--scan-usage` to list variables that are defined in `.env` but never used in your codebase:
@@ -11,6 +11,7 @@ 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('--fix', 'Automatically fix common issues: remove duplicates, add missing keys')
14
15
  .option('--json', 'Output results in JSON format')
15
16
  .option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)')
16
17
  .option('--scan-usage', 'Scan codebase for environment variable usage')
@@ -21,6 +21,7 @@ export async function run(program) {
21
21
  ignoreRegex: opts.ignoreRegex,
22
22
  examplePath: opts.exampleFlag || undefined,
23
23
  envPath,
24
+ fix: opts.fix,
24
25
  json: opts.json,
25
26
  showUnused: opts.showUnused,
26
27
  showStats: opts.showStats,
@@ -93,6 +94,7 @@ export async function run(program) {
93
94
  checkValues: opts.checkValues,
94
95
  cwd: opts.cwd,
95
96
  allowDuplicates: opts.allowDuplicates,
97
+ fix: opts.fix,
96
98
  json: opts.json,
97
99
  ignore: opts.ignore,
98
100
  ignoreRegex: opts.ignoreRegex,
@@ -7,6 +7,7 @@ export declare function compareMany(pairs: Array<{
7
7
  checkValues: boolean;
8
8
  cwd: string;
9
9
  allowDuplicates?: boolean;
10
+ fix?: boolean;
10
11
  json?: boolean;
11
12
  ignore: string[];
12
13
  ignoreRegex: RegExp[];
@@ -6,6 +6,7 @@ import { diffEnv } from '../lib/diffEnv.js';
6
6
  import { warnIfEnvNotIgnored } from '../services/git.js';
7
7
  import { findDuplicateKeys } from '../services/duplicates.js';
8
8
  import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
9
+ import { applyFixes } from '../core/fixEnv.js';
9
10
  export async function compareMany(pairs, opts) {
10
11
  let exitWithError = false;
11
12
  const onlySet = opts.only?.length
@@ -162,7 +163,7 @@ export async function compareMany(pairs, opts) {
162
163
  exitWithError = true;
163
164
  }
164
165
  if (!opts.json) {
165
- if (filtered.missing.length) {
166
+ if (filtered.missing.length && !opts.fix) {
166
167
  console.log(chalk.red(' ❌ Missing keys:'));
167
168
  filtered.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
168
169
  }
@@ -180,6 +181,40 @@ export async function compareMany(pairs, opts) {
180
181
  }
181
182
  console.log();
182
183
  }
184
+ if (!opts.json && !opts.fix) {
185
+ if (filtered.missing.length ||
186
+ filtered.duplicatesEnv.length ||
187
+ filtered.duplicatesEx.length) {
188
+ console.log(chalk.gray('💡 Tip: Run with `--fix` to automatically add missing keys and remove duplicates.'));
189
+ console.log();
190
+ }
191
+ }
192
+ if (opts.fix) {
193
+ const { changed, result } = applyFixes({
194
+ envPath,
195
+ examplePath,
196
+ missingKeys: filtered.missing,
197
+ duplicateKeys: dupsEnv.map((d) => d.key),
198
+ });
199
+ if (!opts.json) {
200
+ if (changed) {
201
+ console.log(chalk.green(' ✅ Auto-fix applied:'));
202
+ if (result.removedDuplicates.length) {
203
+ console.log(chalk.green(` - Removed ${result.removedDuplicates.length} duplicate keys from ${envName}: ${result.removedDuplicates.join(', ')}`));
204
+ }
205
+ if (result.addedEnv.length) {
206
+ console.log(chalk.green(` - Added ${result.addedEnv.length} missing keys to ${envName}: ${result.addedEnv.join(', ')}`));
207
+ }
208
+ if (result.addedExample.length) {
209
+ console.log(chalk.green(` - Synced ${result.addedExample.length} keys to ${exampleName}: ${result.addedExample.join(', ')}`));
210
+ }
211
+ }
212
+ else {
213
+ console.log(chalk.green(' ✅ Auto-fix applied: no changes needed.'));
214
+ }
215
+ console.log();
216
+ }
217
+ }
183
218
  opts.collect?.(entry);
184
219
  }
185
220
  return { exitWithError };
@@ -2,6 +2,7 @@ import { type ScanOptions } from '../services/codeBaseScanner.js';
2
2
  export interface ScanUsageOptions extends ScanOptions {
3
3
  envPath?: string;
4
4
  examplePath?: string;
5
+ fix?: boolean;
5
6
  json: boolean;
6
7
  showUnused: boolean;
7
8
  showStats: boolean;
@@ -64,6 +64,44 @@ export async function scanUsage(opts) {
64
64
  }
65
65
  }
66
66
  }
67
+ // Store fix information for later display
68
+ let fixApplied = false;
69
+ let fixedKeys = [];
70
+ if (opts.fix && compareFile) {
71
+ const missingKeys = scanResult.missing;
72
+ if (missingKeys.length > 0) {
73
+ const envFilePath = compareFile.path;
74
+ const exampleFilePath = opts.examplePath
75
+ ? resolveFromCwd(opts.cwd, opts.examplePath)
76
+ : null;
77
+ // Append missing keys to .env
78
+ const content = fs.readFileSync(envFilePath, 'utf-8');
79
+ const newContent = content +
80
+ (content.endsWith('\n') ? '' : '\n') +
81
+ missingKeys.map((k) => `${k}=`).join('\n') +
82
+ '\n';
83
+ fs.writeFileSync(envFilePath, newContent);
84
+ // Append to .env.example if it exists
85
+ if (exampleFilePath && fs.existsSync(exampleFilePath)) {
86
+ const exContent = fs.readFileSync(exampleFilePath, 'utf-8');
87
+ const existingExKeys = new Set(exContent
88
+ .split('\n')
89
+ .map((l) => l.trim().split('=')[0])
90
+ .filter(Boolean));
91
+ const newKeys = missingKeys.filter((k) => !existingExKeys.has(k));
92
+ if (newKeys.length) {
93
+ const newExContent = exContent +
94
+ (exContent.endsWith('\n') ? '' : '\n') +
95
+ newKeys.join('\n') +
96
+ '\n';
97
+ fs.writeFileSync(exampleFilePath, newExContent);
98
+ }
99
+ }
100
+ fixApplied = true;
101
+ fixedKeys = missingKeys;
102
+ scanResult.missing = [];
103
+ }
104
+ }
67
105
  // Prepare JSON output
68
106
  if (opts.json) {
69
107
  const jsonOutput = createJsonOutput(scanResult, opts, comparedAgainst, Object.keys(envVariables).length);
@@ -71,7 +109,23 @@ export async function scanUsage(opts) {
71
109
  return { exitWithError: scanResult.missing.length > 0 };
72
110
  }
73
111
  // Console output
74
- return outputToConsole(scanResult, opts, comparedAgainst);
112
+ const result = outputToConsole(scanResult, opts, comparedAgainst);
113
+ // Show fix message at the bottom (after all other output)
114
+ if (fixApplied && !opts.json) {
115
+ console.log(chalk.green('✅ Auto-fix applied (scan mode):'));
116
+ if (compareFile) {
117
+ console.log(chalk.green(` - Added ${fixedKeys.length} missing keys to ${compareFile.name}: ${fixedKeys.join(', ')}`));
118
+ }
119
+ if (opts.examplePath) {
120
+ console.log(chalk.green(` - Synced ${fixedKeys.length} keys to ${path.basename(opts.examplePath)}`));
121
+ }
122
+ console.log();
123
+ }
124
+ else if (opts.fix && !fixApplied && !opts.json) {
125
+ console.log(chalk.green('✅ Auto-fix applied: no changes needed.'));
126
+ console.log();
127
+ }
128
+ return result;
75
129
  }
76
130
  /**
77
131
  * Determines which file to use for comparison based on provided options
@@ -227,5 +281,9 @@ function outputToConsole(scanResult, opts, comparedAgainst) {
227
281
  }
228
282
  console.log();
229
283
  }
284
+ if (scanResult.missing.length > 0 && !opts.json && !opts.fix) {
285
+ console.log(chalk.gray('💡 Tip: Run with `--fix` to add these missing keys to your .env file automatically.'));
286
+ console.log();
287
+ }
230
288
  return { exitWithError };
231
289
  }
@@ -22,6 +22,7 @@ export function normalizeOptions(raw) {
22
22
  const isCiMode = Boolean(raw.ci);
23
23
  const isYesMode = Boolean(raw.yes);
24
24
  const allowDuplicates = Boolean(raw.allowDuplicates);
25
+ const fix = Boolean(raw.fix);
25
26
  const json = Boolean(raw.json);
26
27
  const onlyParsed = parseCategories(raw.only, '--only');
27
28
  const only = onlyParsed.length ? onlyParsed : undefined;
@@ -53,6 +54,7 @@ export function normalizeOptions(raw) {
53
54
  isCiMode,
54
55
  isYesMode,
55
56
  allowDuplicates,
57
+ fix,
56
58
  json,
57
59
  envFlag,
58
60
  exampleFlag,
@@ -5,6 +5,7 @@ export type Options = {
5
5
  isCiMode: boolean;
6
6
  isYesMode: boolean;
7
7
  allowDuplicates: boolean;
8
+ fix: boolean;
8
9
  json: boolean;
9
10
  envFlag: string | null;
10
11
  exampleFlag: string | null;
@@ -24,6 +25,7 @@ export type RawOptions = {
24
25
  ci?: boolean;
25
26
  yes?: boolean;
26
27
  allowDuplicates?: boolean;
28
+ fix?: boolean;
27
29
  json?: boolean;
28
30
  env?: string;
29
31
  example?: string;
@@ -0,0 +1,13 @@
1
+ export declare function applyFixes({ envPath, examplePath, missingKeys, duplicateKeys, }: {
2
+ envPath: string;
3
+ examplePath: string;
4
+ missingKeys: string[];
5
+ duplicateKeys: string[];
6
+ }): {
7
+ changed: boolean;
8
+ result: {
9
+ removedDuplicates: string[];
10
+ addedEnv: string[];
11
+ addedExample: string[];
12
+ };
13
+ };
@@ -0,0 +1,60 @@
1
+ import fs from 'fs';
2
+ export function applyFixes({ envPath, examplePath, missingKeys, duplicateKeys, }) {
3
+ const result = {
4
+ removedDuplicates: [],
5
+ addedEnv: [],
6
+ addedExample: [],
7
+ };
8
+ // --- Remove duplicates ---
9
+ if (duplicateKeys.length) {
10
+ const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
11
+ const seen = new Set();
12
+ const newLines = [];
13
+ for (let i = lines.length - 1; i >= 0; i--) {
14
+ const line = lines[i];
15
+ const match = line.match(/^\s*([\w.-]+)\s*=/);
16
+ if (match) {
17
+ const key = match[1];
18
+ if (duplicateKeys.includes(key)) {
19
+ if (seen.has(key))
20
+ continue; // skip duplicate
21
+ seen.add(key);
22
+ }
23
+ }
24
+ newLines.unshift(line);
25
+ }
26
+ fs.writeFileSync(envPath, newLines.join('\n'));
27
+ result.removedDuplicates = duplicateKeys; // save all dupe keys
28
+ }
29
+ // --- Add missing keys to .env ---
30
+ if (missingKeys.length) {
31
+ const content = fs.readFileSync(envPath, 'utf-8');
32
+ const newContent = content +
33
+ (content.endsWith('\n') ? '' : '\n') +
34
+ missingKeys.map((k) => `${k}=`).join('\n') +
35
+ '\n';
36
+ fs.writeFileSync(envPath, newContent);
37
+ result.addedEnv = missingKeys; // save all missing keys
38
+ }
39
+ // --- Add missing keys to .env.example ---
40
+ if (examplePath && missingKeys.length) {
41
+ const exContent = fs.readFileSync(examplePath, 'utf-8');
42
+ const existingExKeys = new Set(exContent
43
+ .split('\n')
44
+ .map((l) => l.trim().split('=')[0])
45
+ .filter(Boolean));
46
+ const newExampleKeys = missingKeys.filter((k) => !existingExKeys.has(k));
47
+ if (newExampleKeys.length) {
48
+ const newExContent = exContent +
49
+ (exContent.endsWith('\n') ? '' : '\n') +
50
+ newExampleKeys.join('\n') +
51
+ '\n';
52
+ fs.writeFileSync(examplePath, newExContent);
53
+ result.addedExample = newExampleKeys; // save all keys actually added
54
+ }
55
+ }
56
+ const changed = result.removedDuplicates.length > 0 ||
57
+ result.addedEnv.length > 0 ||
58
+ result.addedExample.length > 0;
59
+ return { changed, result };
60
+ }
package/dist/src/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export { parseEnvFile } from './lib/parseEnv.js';
2
2
  export { diffEnv } from './lib/diffEnv.js';
3
+ process.env.VITE_HEJ ??= 'production';
4
+ process.env.FEATURE_2FLAG ??= 'enabled';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotenv-diff",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
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": {