dotenv-diff 1.3.0 → 1.5.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.
Files changed (3) hide show
  1. package/README.md +18 -8
  2. package/dist/cli.js +121 -61
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # dotenv-diff
2
2
 
3
- ![Terminal_view](public/image.png)
4
-
5
- Easily compare your `.env` and `.env.example` files in Node.js projects to detect missing, extra, empty, or mismatched environment variables.
3
+ Easily compare your .env, .env.example, and other environment files (like .env.local, .env.production) to detect missing, extra, empty, or mismatched variables — and ensure they’re properly ignored by Git.
6
4
 
7
5
  [![npm version](https://img.shields.io/npm/v/dotenv-diff.svg)](https://www.npmjs.com/package/dotenv-diff)
8
6
  [![npm downloads](https://img.shields.io/npm/dt/dotenv-diff.svg)](https://www.npmjs.com/package/dotenv-diff)
@@ -26,11 +24,12 @@ pnpm add -g dotenv-diff
26
24
  ```bash
27
25
  dotenv-diff
28
26
  ```
29
- ## Compares .env and .env.example in the current working directory and checks for:
30
- - Missing variables in `.env.example`
31
- - Extra variables in `.env`
32
- - Empty variables in `.env`
33
- - Mismatched values between `.env` and `.env.example`
27
+ ## What it checks
28
+ dotenv-diff will automatically compare all matching .env* files in your project against .env.example, including:
29
+ - `.env`
30
+ - `.env.local`
31
+ - `.env.production`
32
+ - Any other .env.* file
34
33
 
35
34
  ## Optional: Check values too
36
35
 
@@ -40,6 +39,17 @@ dotenv-diff --check-values
40
39
 
41
40
  When using the `--check-values` option, the tool will also compare the actual values of the variables in `.env` and `.env.example`. It will report any mismatches found and it also compares values if .env.example defines a non-empty expected value.
42
41
 
42
+ ## CI usage
43
+
44
+ Run non-interactively in CI environments with:
45
+
46
+ ```bash
47
+ dotenv-diff --ci # never create files, exit 1 if required files are missing
48
+ dotenv-diff --yes # auto-create missing files without prompts
49
+ ```
50
+
51
+ You can also use `-y` as a shorthand for `--yes`.
52
+
43
53
  ## Automatic file creation prompts
44
54
 
45
55
  If one of the files is missing, `dotenv-diff` will ask if you want to create it from the other:
package/dist/cli.js CHANGED
@@ -19,33 +19,59 @@ program
19
19
  .name('dotenv-diff')
20
20
  .description('Compare .env and .env.example files')
21
21
  .option('--check-values', 'Compare actual values if example has values')
22
+ .option('--ci', 'Run non-interactively and never create files')
23
+ .option('-y, --yes', 'Run non-interactively and answer Yes to prompts')
22
24
  .parse(process.argv);
23
25
  const options = program.opts();
24
26
  const checkValues = options.checkValues ?? false;
27
+ const isCiMode = Boolean(options.ci);
28
+ const isYesMode = Boolean(options.yes);
29
+ if (isCiMode && isYesMode) {
30
+ console.log(chalk.yellow('⚠️ Both --ci and --yes provided; proceeding with --yes.'));
31
+ }
25
32
  const cwd = process.cwd();
26
- const envPath = path.resolve(cwd, '.env');
27
- const examplePath = path.resolve(cwd, '.env.example');
33
+ const envFiles = fs
34
+ .readdirSync(cwd)
35
+ .filter((f) => f.startsWith('.env') && !f.startsWith('.env.example'))
36
+ .sort((a, b) => (a === '.env' ? -1 : b === '.env' ? 1 : a.localeCompare(b)));
37
+ // Brug første .env* fil som "main" hvis flere findes
38
+ // (resten håndteres senere i et loop)
39
+ const primaryEnv = envFiles.includes('.env') ? '.env' : envFiles[0] || '.env';
40
+ const primaryExample = '.env.example';
41
+ const envPath = path.resolve(cwd, primaryEnv);
42
+ const examplePath = path.resolve(cwd, primaryExample);
28
43
  const envExists = fs.existsSync(envPath);
29
44
  const exampleExists = fs.existsSync(examplePath);
30
- // Case 1: Neither file exists
31
- if (!envExists && !exampleExists) {
32
- console.log(chalk.yellow('⚠️ No .env or .env.example file found. Skipping comparison.'));
45
+ // Case 1: No env files and no .env.example → nothing to do
46
+ if (envFiles.length === 0 && !exampleExists) {
47
+ console.log(chalk.yellow('⚠️ No .env* or .env.example file found. Skipping comparison.'));
33
48
  process.exit(0);
34
49
  }
35
50
  // Case 2: .env is missing but .env.example exists
36
51
  if (!envExists && exampleExists) {
37
52
  console.log(chalk.yellow('📄 .env file not found.'));
38
- const response = await prompts({
39
- type: 'select',
40
- name: 'createEnv',
41
- message: '❓ Do you want to create a new .env file from .env.example?',
42
- choices: [
43
- { title: 'Yes', value: true },
44
- { title: 'No', value: false }
45
- ],
46
- initial: 0
47
- });
48
- if (!response.createEnv) {
53
+ let createEnv = false;
54
+ if (isYesMode) {
55
+ createEnv = true;
56
+ }
57
+ else if (isCiMode) {
58
+ console.log(chalk.gray('🚫 Skipping .env creation (CI mode).'));
59
+ process.exit(1);
60
+ }
61
+ else {
62
+ const response = await prompts({
63
+ type: 'select',
64
+ name: 'createEnv',
65
+ message: '❓ Do you want to create a new .env file from .env.example?',
66
+ choices: [
67
+ { title: 'Yes', value: true },
68
+ { title: 'No', value: false },
69
+ ],
70
+ initial: 0,
71
+ });
72
+ createEnv = Boolean(response.createEnv);
73
+ }
74
+ if (!createEnv) {
49
75
  console.log(chalk.gray('🚫 Skipping .env creation.'));
50
76
  process.exit(0);
51
77
  }
@@ -57,21 +83,33 @@ if (!envExists && exampleExists) {
57
83
  // Case 3: .env exists, but .env.example is missing
58
84
  if (envExists && !exampleExists) {
59
85
  console.log(chalk.yellow('📄 .env.example file not found.'));
60
- const response = await prompts({
61
- type: 'select',
62
- name: 'createExample',
63
- message: '❓ Do you want to create a new .env.example file from .env?',
64
- choices: [
65
- { title: 'Yes', value: true },
66
- { title: 'No', value: false }
67
- ],
68
- initial: 0
69
- });
70
- if (!response.createExample) {
86
+ let createExample = false;
87
+ if (isYesMode) {
88
+ createExample = true;
89
+ }
90
+ else if (isCiMode) {
91
+ console.log(chalk.gray('🚫 Skipping .env.example creation (CI mode).'));
92
+ process.exit(1);
93
+ }
94
+ else {
95
+ const response = await prompts({
96
+ type: 'select',
97
+ name: 'createExample',
98
+ message: '❓ Do you want to create a new .env.example file from .env?',
99
+ choices: [
100
+ { title: 'Yes', value: true },
101
+ { title: 'No', value: false },
102
+ ],
103
+ initial: 0,
104
+ });
105
+ createExample = Boolean(response.createExample);
106
+ }
107
+ if (!createExample) {
71
108
  console.log(chalk.gray('🚫 Skipping .env.example creation.'));
72
109
  process.exit(0);
73
110
  }
74
- const envContent = fs.readFileSync(envPath, 'utf-8')
111
+ const envContent = fs
112
+ .readFileSync(envPath, 'utf-8')
75
113
  .split('\n')
76
114
  .map((line) => {
77
115
  const trimmed = line.trim();
@@ -89,38 +127,60 @@ if (!fs.existsSync(envPath) || !fs.existsSync(examplePath)) {
89
127
  console.error(chalk.red('❌ Error: .env or .env.example is missing after setup.'));
90
128
  process.exit(1);
91
129
  }
92
- // Case 5: Both files exist, proceed with comparison
93
- warnIfEnvNotIgnored();
94
- console.log(chalk.bold('🔍 Comparing .env and .env.example...\n'));
95
- const current = parseEnvFile(envPath);
96
- const example = parseEnvFile(examplePath);
97
- const diff = diffEnv(current, example, checkValues);
98
- const emptyKeys = Object.entries(current)
99
- .filter(([, value]) => value.trim() === '')
100
- .map(([key]) => key);
101
- if (diff.missing.length === 0 &&
102
- diff.extra.length === 0 &&
103
- emptyKeys.length === 0 &&
104
- diff.valueMismatches.length === 0) {
105
- console.log(chalk.green('✅ All keys match! Your .env file is valid.'));
106
- process.exit(0);
107
- }
108
- if (diff.missing.length > 0) {
109
- console.log(chalk.red('\n❌ Missing keys in .env:'));
110
- diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
111
- }
112
- if (diff.extra.length > 0) {
113
- console.log(chalk.yellow('\n⚠️ Extra keys in .env (not defined in .env.example):'));
114
- diff.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
115
- }
116
- if (emptyKeys.length > 0) {
117
- console.log(chalk.yellow('\n⚠️ The following keys in .env have no value (empty):'));
118
- emptyKeys.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
119
- }
120
- if (checkValues && diff.valueMismatches.length > 0) {
121
- console.log(chalk.yellow('\n⚠️ The following keys have different values:'));
122
- diff.valueMismatches.forEach(({ key, expected, actual }) => {
123
- console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`));
130
+ // Case 5: Compare all found .env* files
131
+ let exitWithError = false;
132
+ for (const envName of envFiles.length > 0 ? envFiles : [primaryEnv]) {
133
+ const suffix = envName === '.env' ? '' : envName.replace('.env', '');
134
+ const exampleName = suffix ? `.env.example${suffix}` : primaryExample;
135
+ const envPathCurrent = path.resolve(cwd, envName);
136
+ const examplePathCurrent = fs.existsSync(path.resolve(cwd, exampleName))
137
+ ? path.resolve(cwd, exampleName)
138
+ : examplePath;
139
+ if (!fs.existsSync(envPathCurrent) || !fs.existsSync(examplePathCurrent)) {
140
+ console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${path.basename(examplePathCurrent)}...`));
141
+ console.log(chalk.yellow(' ⚠️ Skipping: missing matching example file.'));
142
+ console.log();
143
+ continue;
144
+ }
145
+ console.log(chalk.bold(`🔍 Comparing ${envName} ↔ ${path.basename(examplePathCurrent)}...`));
146
+ warnIfEnvNotIgnored({
147
+ cwd,
148
+ envFile: envName,
149
+ log: (msg) => console.log(msg.replace(/^/gm, ' ')),
124
150
  });
151
+ const current = parseEnvFile(envPathCurrent);
152
+ const example = parseEnvFile(examplePathCurrent);
153
+ const diff = diffEnv(current, example, checkValues);
154
+ const emptyKeys = Object.entries(current)
155
+ .filter(([, value]) => (value ?? '').trim() === '')
156
+ .map(([key]) => key);
157
+ if (diff.missing.length === 0 &&
158
+ diff.extra.length === 0 &&
159
+ emptyKeys.length === 0 &&
160
+ diff.valueMismatches.length === 0) {
161
+ console.log(chalk.green(' ✅ All keys match.'));
162
+ console.log();
163
+ continue;
164
+ }
165
+ if (diff.missing.length > 0) {
166
+ exitWithError = true;
167
+ console.log(chalk.red(' ❌ Missing keys:'));
168
+ diff.missing.forEach((key) => console.log(chalk.red(` - ${key}`)));
169
+ }
170
+ if (diff.extra.length > 0) {
171
+ console.log(chalk.yellow(' ⚠️ Extra keys (not in example):'));
172
+ diff.extra.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
173
+ }
174
+ if (emptyKeys.length > 0) {
175
+ console.log(chalk.yellow(' ⚠️ Empty values:'));
176
+ emptyKeys.forEach((key) => console.log(chalk.yellow(` - ${key}`)));
177
+ }
178
+ if (checkValues && diff.valueMismatches.length > 0) {
179
+ console.log(chalk.yellow(' ⚠️ Value mismatches:'));
180
+ diff.valueMismatches.forEach(({ key, expected, actual }) => {
181
+ console.log(chalk.yellow(` - ${key}: expected '${expected}', but got '${actual}'`));
182
+ });
183
+ }
184
+ console.log(); // blank line efter sektionen med findings
125
185
  }
126
- process.exit(diff.missing.length > 0 ? 1 : 0);
186
+ process.exit(exitWithError ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotenv-diff",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "A small CLI and library to find differences between .env and .env.example files.",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "dev": "vitest --watch",
19
19
  "test": "vitest",
20
20
  "lint": "eslint ./src --ext .ts",
21
- "format": "prettier --write ./src/**/*.ts",
21
+ "format": "prettier --write \"src/**/*.ts\" \"src/*.ts\"",
22
22
  "start": "node dist/cli.js"
23
23
  },
24
24
  "author": "Chrilleweb",