@@ -2,7 +2,7 @@ import fs from 'fs';
22import path from 'path' ;
33import { parseEnvFile } from '../core/parseEnv.js' ;
44import { diffEnv } from '../core/diffEnv.js' ;
5- import { warnIfEnvNotIgnored , isEnvIgnoredByGit } from '../services/git.js' ;
5+ import { isEnvIgnoredByGit , checkGitignoreStatus } from '../services/git.js' ;
66import { findDuplicateKeys } from '../services/duplicates.js' ;
77import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js' ;
88import type {
@@ -20,6 +20,7 @@ import { printHeader } from '../ui/compare/printHeader.js';
2020import { printAutoFix } from '../ui/compare/printAutoFix.js' ;
2121import { printIssues } from '../ui/compare/printIssues.js' ;
2222import { printSuccess } from '../ui/compare/printSuccess.js' ;
23+ import { printGitignoreWarning } from '../ui/shared/printGitignore.js' ;
2324
2425/**
2526 * Compares multiple pairs of .env and .env.example files.
@@ -61,42 +62,11 @@ export async function compareMany(
6162 entry . skipped = { reason : 'missing file' } ;
6263 opts . collect ?.( entry ) ;
6364 continue ;
64- } else {
65- printHeader ( envName , exampleName , opts . json ?? false , skipping ) ;
66- }
67-
68- // Git ignore hint (only when not JSON)
69- let gitignoreUnsafe = false ;
70- let gitignoreMsg : string | null = null ;
71-
72- if ( run ( 'gitignore' ) ) {
73- warnIfEnvNotIgnored ( {
74- cwd : opts . cwd ,
75- envFile : envName ,
76- log : ( msg ) => {
77- gitignoreUnsafe = true ;
78- gitignoreMsg = msg ;
79- } ,
80- } ) ;
8165 }
8266
83- // Duplicate detection (skip entirely if --only excludes it)
84- let dupsEnv : Array < { key : string ; count : number } > = [ ] ;
85- let dupsEx : Array < { key : string ; count : number } > = [ ] ;
86- if ( ! opts . allowDuplicates && run ( 'duplicate' ) ) {
87- dupsEnv = findDuplicateKeys ( envPath ) . filter (
88- ( { key } ) =>
89- ! opts . ignore . includes ( key ) &&
90- ! opts . ignoreRegex . some ( ( rx ) => rx . test ( key ) ) ,
91- ) ;
92- dupsEx = findDuplicateKeys ( examplePath ) . filter (
93- ( { key } ) =>
94- ! opts . ignore . includes ( key ) &&
95- ! opts . ignoreRegex . some ( ( rx ) => rx . test ( key ) ) ,
96- ) ;
97- }
67+ printHeader ( envName , exampleName , opts . json ?? false , skipping ) ;
9868
99- // Diff + empty
69+ // Parse and filter env files
10070 const currentFull = parseEnvFile ( envPath ) ;
10171 const exampleFull = parseEnvFile ( examplePath ) ;
10272
@@ -118,12 +88,33 @@ export async function compareMany(
11888 exampleKeys . map ( ( k ) => [ k , exampleFull [ k ] ?? '' ] ) ,
11989 ) ;
12090
91+ // Run checks
12192 const diff = diffEnv ( current , example , opts . checkValues ) ;
12293
12394 const emptyKeys = Object . entries ( current )
12495 . filter ( ( [ , v ] ) => ( v ?? '' ) . trim ( ) === '' )
12596 . map ( ( [ k ] ) => k ) ;
12697
98+ let dupsEnv : Array < { key : string ; count : number } > = [ ] ;
99+ let dupsEx : Array < { key : string ; count : number } > = [ ] ;
100+ if ( ! opts . allowDuplicates && run ( 'duplicate' ) ) {
101+ dupsEnv = findDuplicateKeys ( envPath ) . filter (
102+ ( { key } ) =>
103+ ! opts . ignore . includes ( key ) &&
104+ ! opts . ignoreRegex . some ( ( rx ) => rx . test ( key ) ) ,
105+ ) ;
106+ dupsEx = findDuplicateKeys ( examplePath ) . filter (
107+ ( { key } ) =>
108+ ! opts . ignore . includes ( key ) &&
109+ ! opts . ignoreRegex . some ( ( rx ) => rx . test ( key ) ) ,
110+ ) ;
111+ }
112+
113+ const gitignoreIssue = run ( 'gitignore' )
114+ ? checkGitignoreStatus ( { cwd : opts . cwd , envFile : envName } )
115+ : null ;
116+
117+ // Collect filtered results
127118 const filtered = {
128119 missing : run ( 'missing' ) ? diff . missing : [ ] ,
129120 extra : run ( 'extra' ) ? diff . extra : [ ] ,
@@ -132,19 +123,17 @@ export async function compareMany(
132123 run ( 'mismatch' ) && opts . checkValues ? diff . valueMismatches : [ ] ,
133124 duplicatesEnv : run ( 'duplicate' ) ? dupsEnv : [ ] ,
134125 duplicatesEx : run ( 'duplicate' ) ? dupsEx : [ ] ,
135- gitignoreUnsafe : run ( 'gitignore' ) ? gitignoreUnsafe : false ,
136- gitignoreMsg : run ( 'gitignore' ) ? gitignoreMsg : null ,
126+ gitignoreIssue,
137127 } ;
138128
139- // --- Stats block for compare mode when --show- stats is active ---
129+ // Print stats if requested
140130 if ( opts . showStats && ! opts . json ) {
141131 const envCount = currentKeys . length ;
142132 const exampleCount = exampleKeys . length ;
143133 const sharedCount = new Set (
144134 currentKeys . filter ( ( k ) => exampleKeys . includes ( k ) ) ,
145135 ) . size ;
146136
147- // Duplicate "occurrences beyond the first", summed across both files
148137 const duplicateCount = [ ...dupsEnv , ...dupsEx ] . reduce (
149138 ( acc , { count } ) => acc + Math . max ( 0 , count - 1 ) ,
150139 0 ,
@@ -170,14 +159,15 @@ export async function compareMany(
170159 ) ;
171160 }
172161
162+ // Check if all is OK
173163 const allOk =
174164 filtered . missing . length === 0 &&
175165 filtered . extra . length === 0 &&
176166 filtered . empty . length === 0 &&
177167 filtered . duplicatesEnv . length === 0 &&
178168 filtered . duplicatesEx . length === 0 &&
179169 filtered . mismatches . length === 0 &&
180- ! filtered . gitignoreUnsafe ;
170+ ! filtered . gitignoreIssue ;
181171
182172 if ( allOk ) {
183173 entry . ok = true ;
@@ -186,40 +176,46 @@ export async function compareMany(
186176 continue ;
187177 }
188178
179+ // Print duplicates
189180 printDuplicates ( envName , exampleName , dupsEnv , dupsEx , opts . json ?? false ) ;
190181
182+ // Track errors and update totals
191183 if ( filtered . missing . length ) {
192184 entry . missing = filtered . missing ;
193- exitWithError = true ;
194185 totals . missing += filtered . missing . length ;
186+ exitWithError = true ;
195187 }
196188 if ( filtered . extra . length ) {
197189 entry . extra = filtered . extra ;
198- exitWithError = true ;
199190 totals . extra += filtered . extra . length ;
200191 }
201192 if ( filtered . empty . length ) {
202193 entry . empty = filtered . empty ;
203- exitWithError = true ;
204194 totals . empty += filtered . empty . length ;
205195 }
206196 if ( filtered . mismatches . length ) {
207197 entry . valueMismatches = filtered . mismatches ;
208198 totals . mismatch += filtered . mismatches . length ;
209- exitWithError = true ;
210199 }
211200 if ( filtered . duplicatesEnv . length || filtered . duplicatesEx . length ) {
212201 totals . duplicate +=
213202 filtered . duplicatesEnv . length + filtered . duplicatesEx . length ;
214- exitWithError = true ;
215203 }
216- if ( filtered . gitignoreUnsafe ) {
204+ if ( filtered . gitignoreIssue ) {
217205 totals . gitignore += 1 ;
218- exitWithError = true ;
219206 }
220207
208+ // Print all issues
221209 printIssues ( filtered , opts . json ?? false ) ;
222210
211+ if ( filtered . gitignoreIssue && ! opts . json ) {
212+ printGitignoreWarning ( {
213+ envFile : envName ,
214+ reason : filtered . gitignoreIssue . reason ,
215+ } ) ;
216+ }
217+
218+ // Print fix tips if not in JSON mode and not auto-fixing
223219 if ( ! opts . json && ! opts . fix ) {
224220 const ignored = isEnvIgnoredByGit ( { cwd : opts . cwd , envFile : '.env' } ) ;
225221 const envNotIgnored = ignored === false || ignored === null ;
@@ -231,6 +227,7 @@ export async function compareMany(
231227 ) ;
232228 }
233229
230+ // Apply auto-fix if requested
234231 if ( opts . fix ) {
235232 const { changed, result } = applyFixes ( {
236233 envPath,
@@ -243,15 +240,9 @@ export async function compareMany(
243240 }
244241
245242 opts . collect ?.( entry ) ;
246- const warningsExist =
247- filtered . extra . length > 0 ||
248- filtered . empty . length > 0 ||
249- filtered . duplicatesEnv . length > 0 ||
250- filtered . duplicatesEx . length > 0 ||
251- filtered . mismatches . length > 0 ||
252- filtered . gitignoreUnsafe ;
253243
254- if ( opts . strict && warningsExist ) {
244+ // In strict mode, any issue (not just errors) causes exit with error
245+ if ( opts . strict && ! allOk ) {
255246 exitWithError = true ;
256247 }
257248 }
0 commit comments