dotenv-diff 2.1.3 → 2.1.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 +48 -2
- package/dist/src/cli/program.js +2 -0
- package/dist/src/cli/run.js +5 -0
- package/dist/src/commands/compare.d.ts +1 -0
- package/dist/src/commands/compare.js +36 -1
- package/dist/src/commands/scanUsage.d.ts +1 -0
- package/dist/src/commands/scanUsage.js +59 -1
- package/dist/src/config/options.js +2 -0
- package/dist/src/config/types.d.ts +3 -0
- package/dist/src/core/fixEnv.d.ts +13 -0
- package/dist/src/core/fixEnv.js +60 -0
- package/package.json +2 -2
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 --
|
|
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:
|
|
@@ -158,6 +194,16 @@ You can output the results in JSON format using the `--json` option:
|
|
|
158
194
|
dotenv-diff --json
|
|
159
195
|
```
|
|
160
196
|
|
|
197
|
+
## Disable colored output
|
|
198
|
+
|
|
199
|
+
By default, `dotenv-diff` uses colored output to enhance readability.
|
|
200
|
+
|
|
201
|
+
If you prefer plain text output, you can disable colored output using the `--no-color` option:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
dotenv-diff --no-color
|
|
205
|
+
```
|
|
206
|
+
|
|
161
207
|
## Compare specific files
|
|
162
208
|
|
|
163
209
|
Override the autoscan and compare exactly two files:
|
package/dist/src/cli/program.js
CHANGED
|
@@ -11,7 +11,9 @@ 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')
|
|
16
|
+
.option('--no-color', 'Disable colored output')
|
|
15
17
|
.option('--only <list>', 'Comma-separated categories to only run (missing,extra,empty,mismatch,duplicate,gitignore)')
|
|
16
18
|
.option('--scan-usage', 'Scan codebase for environment variable usage')
|
|
17
19
|
.option('--include-files <patterns>', '[requires --scan-usage] Comma-separated file patterns to ADD to default scan patterns (extends default)')
|
package/dist/src/cli/run.js
CHANGED
|
@@ -11,6 +11,9 @@ export async function run(program) {
|
|
|
11
11
|
program.parse(process.argv);
|
|
12
12
|
const raw = program.opts();
|
|
13
13
|
const opts = normalizeOptions(raw);
|
|
14
|
+
if (opts.noColor) {
|
|
15
|
+
chalk.level = 0; // disable colors globally
|
|
16
|
+
}
|
|
14
17
|
if (opts.scanUsage) {
|
|
15
18
|
const envPath = opts.envFlag || (fs.existsSync('.env') ? '.env' : undefined);
|
|
16
19
|
const { exitWithError } = await scanUsage({
|
|
@@ -21,6 +24,7 @@ export async function run(program) {
|
|
|
21
24
|
ignoreRegex: opts.ignoreRegex,
|
|
22
25
|
examplePath: opts.exampleFlag || undefined,
|
|
23
26
|
envPath,
|
|
27
|
+
fix: opts.fix,
|
|
24
28
|
json: opts.json,
|
|
25
29
|
showUnused: opts.showUnused,
|
|
26
30
|
showStats: opts.showStats,
|
|
@@ -93,6 +97,7 @@ export async function run(program) {
|
|
|
93
97
|
checkValues: opts.checkValues,
|
|
94
98
|
cwd: opts.cwd,
|
|
95
99
|
allowDuplicates: opts.allowDuplicates,
|
|
100
|
+
fix: opts.fix,
|
|
96
101
|
json: opts.json,
|
|
97
102
|
ignore: opts.ignore,
|
|
98
103
|
ignoreRegex: opts.ignoreRegex,
|
|
@@ -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 };
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -18,12 +19,14 @@ export type Options = {
|
|
|
18
19
|
showUnused: boolean;
|
|
19
20
|
showStats: boolean;
|
|
20
21
|
files?: string[];
|
|
22
|
+
noColor?: boolean;
|
|
21
23
|
};
|
|
22
24
|
export type RawOptions = {
|
|
23
25
|
checkValues?: boolean;
|
|
24
26
|
ci?: boolean;
|
|
25
27
|
yes?: boolean;
|
|
26
28
|
allowDuplicates?: boolean;
|
|
29
|
+
fix?: boolean;
|
|
27
30
|
json?: boolean;
|
|
28
31
|
env?: string;
|
|
29
32
|
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/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-diff",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Find differences between .env and .env.example / .env.* files. And optionally scan your codebase to find environment variables in use.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dotenv-diff": "dist/bin/dotenv-diff.js"
|
|
8
8
|
},
|