@@ -2,50 +2,74 @@ import fs from 'fs';
22import path from 'path' ;
33import { isEnvIgnoredByGit , isGitRepo , findGitRoot } from '../services/git.js' ;
44
5- /**
6- * Applies fixes to the .env and .env.example files based on the detected issues.
7- * @param envPath - The path to the .env file.
8- * @param examplePath - The path to the .env.example file.
9- * @param missingKeys - The list of missing keys to add.
10- * @param duplicateKeys - The list of duplicate keys to remove.
11- * @returns An object indicating whether changes were made and details of the changes.
12- */
13- export function applyFixes ( {
14- envPath,
15- examplePath,
16- missingKeys,
17- duplicateKeys,
18- } : {
5+ export type ApplyFixesOptions = {
196 envPath : string ;
207 examplePath : string ;
218 missingKeys : string [ ] ;
229 duplicateKeys : string [ ] ;
23- } ) {
24- const result = {
25- removedDuplicates : [ ] as string [ ] ,
26- addedEnv : [ ] as string [ ] ,
27- addedExample : [ ] as string [ ] ,
28- gitignoreUpdated : false as boolean ,
10+ ensureGitignore ?: boolean ;
11+ } ;
12+
13+ export type FixResult = {
14+ removedDuplicates : string [ ] ;
15+ addedEnv : string [ ] ;
16+ addedExample : string [ ] ;
17+ gitignoreUpdated : boolean ;
18+ } ;
19+
20+ /**
21+ * Applies fixes to the .env and .env.example files based on the detected issues.
22+ *
23+ * This function will:
24+ * - Remove duplicate keys from .env (keeping the last occurrence)
25+ * - Add missing keys to .env with empty values
26+ * - Add missing keys to .env.example (if not already present)
27+ * - Ensure .env is ignored in .gitignore (if in a git repo and ensureGitignore is true)
28+ *
29+ * @param options - Fix options including file paths and keys to fix
30+ * @returns An object indicating whether changes were made and details of the changes
31+ */
32+ export function applyFixes ( options : ApplyFixesOptions ) : {
33+ changed : boolean ;
34+ result : FixResult ;
35+ } {
36+ const {
37+ envPath,
38+ examplePath,
39+ missingKeys,
40+ duplicateKeys,
41+ ensureGitignore,
42+ } = options ;
43+
44+ const result : FixResult = {
45+ removedDuplicates : [ ] ,
46+ addedEnv : [ ] ,
47+ addedExample : [ ] ,
48+ gitignoreUpdated : false ,
2949 } ;
3050
3151 // --- Remove duplicates ---
3252 if ( duplicateKeys . length ) {
3353 const lines = fs . readFileSync ( envPath , 'utf-8' ) . split ( '\n' ) ;
3454 const seen = new Set < string > ( ) ;
3555 const newLines : string [ ] = [ ] ;
56+
57+ // Process from bottom to top, keeping last occurrence
3658 for ( let i = lines . length - 1 ; i >= 0 ; i -- ) {
3759 const line = lines [ i ] ;
3860 if ( line === undefined ) continue ;
61+
3962 const match = line . match ( / ^ \s * ( [ \w . - ] + ) \s * = / ) ;
4063 if ( match ) {
4164 const key = match [ 1 ] || '' ;
4265 if ( duplicateKeys . includes ( key ) ) {
43- if ( seen . has ( key ) ) continue ; // skip duplicate
66+ if ( seen . has ( key ) ) continue ; // Skip duplicate
4467 seen . add ( key ) ;
4568 }
4669 }
4770 newLines . unshift ( line ) ;
4871 }
72+
4973 fs . writeFileSync ( envPath , newLines . join ( '\n' ) ) ;
5074 result . removedDuplicates = duplicateKeys ;
5175 }
@@ -72,6 +96,7 @@ export function applyFixes({
7296 . filter ( Boolean ) ,
7397 ) ;
7498 const newExampleKeys = missingKeys . filter ( ( k ) => ! existingExKeys . has ( k ) ) ;
99+
75100 if ( newExampleKeys . length ) {
76101 const newExContent =
77102 exContent +
@@ -83,40 +108,9 @@ export function applyFixes({
83108 }
84109 }
85110
86- // --- Ensure .env is ignored in gitignore (best-effort; write at git root) ---
87- try {
88- const startDir = path . dirname ( envPath ) ;
89- const gitRoot = findGitRoot ( startDir ) ;
90- if ( gitRoot && isGitRepo ( gitRoot ) ) {
91- const gitignorePath = path . join ( gitRoot , '.gitignore' ) ;
92- // Check against the actual file name (".env" or custom)
93- const envFileName = path . basename ( envPath ) ;
94- const ignored = isEnvIgnoredByGit ( { cwd : gitRoot , envFile : envFileName } ) ;
95-
96- if ( ignored === false || ignored === null ) {
97- const entry = '.env\n.env.*\n' ;
98- if ( fs . existsSync ( gitignorePath ) ) {
99- const current = fs . readFileSync ( gitignorePath , 'utf8' ) ;
100- // Avoid duplicate entries
101- const hasDotEnv = current . split ( / \r ? \n / ) . some ( ( l ) => l . trim ( ) === '.env' ) ;
102- const hasDotEnvStar = current . split ( / \r ? \n / ) . some ( ( l ) => l . trim ( ) === '.env.*' ) ;
103- const pieces : string [ ] = [ ] ;
104- if ( ! hasDotEnv ) pieces . push ( '.env' ) ;
105- if ( ! hasDotEnvStar ) pieces . push ( '.env.*' ) ;
106-
107- if ( pieces . length ) {
108- const toAppend = `${ current . endsWith ( '\n' ) ? '' : '\n' } ${ pieces . join ( '\n' ) } \n` ;
109- fs . appendFileSync ( gitignorePath , toAppend ) ;
110- result . gitignoreUpdated = true ;
111- }
112- } else {
113- fs . writeFileSync ( gitignorePath , entry ) ;
114- result . gitignoreUpdated = true ;
115- }
116- }
117- }
118- } catch {
119- // ignore errors - non-blocking DX
111+ // --- Ensure .env is ignored in .gitignore ---
112+ if ( ensureGitignore ) {
113+ result . gitignoreUpdated = updateGitignoreForEnv ( envPath ) ;
120114 }
121115
122116 const changed =
@@ -127,3 +121,58 @@ export function applyFixes({
127121
128122 return { changed, result } ;
129123}
124+
125+ /**
126+ * Ensures .env patterns are present in .gitignore at the git repository root.
127+ * This is a best-effort operation and will not throw errors.
128+ *
129+ * @param envPath - Path to the .env file to check gitignore for
130+ * @returns true if .gitignore was updated, false otherwise
131+ */
132+ function updateGitignoreForEnv ( envPath : string ) : boolean {
133+ try {
134+ const startDir = path . dirname ( envPath ) ;
135+ const gitRoot = findGitRoot ( startDir ) ;
136+
137+ if ( ! gitRoot || ! isGitRepo ( gitRoot ) ) {
138+ return false ;
139+ }
140+
141+ const gitignorePath = path . join ( gitRoot , '.gitignore' ) ;
142+ const envFileName = path . basename ( envPath ) ;
143+ const ignored = isEnvIgnoredByGit ( { cwd : gitRoot , envFile : envFileName } ) ;
144+
145+ // Already properly ignored
146+ if ( ignored === true ) {
147+ return false ;
148+ }
149+
150+ // Need to add patterns
151+ const patterns = [ '.env' , '.env.*' ] ;
152+
153+ if ( fs . existsSync ( gitignorePath ) ) {
154+ const current = fs . readFileSync ( gitignorePath , 'utf8' ) ;
155+ const existingLines = current . split ( / \r ? \n / ) . map ( ( l ) => l . trim ( ) ) ;
156+
157+ const missingPatterns = patterns . filter (
158+ ( pattern ) => ! existingLines . includes ( pattern )
159+ ) ;
160+
161+ if ( missingPatterns . length ) {
162+ const toAppend =
163+ `${ current . endsWith ( '\n' ) ? '' : '\n' } ${ missingPatterns . join ( '\n' ) } \n` ;
164+ fs . appendFileSync ( gitignorePath , toAppend ) ;
165+ return true ;
166+ }
167+ } else {
168+ // Create new .gitignore
169+ fs . writeFileSync ( gitignorePath , patterns . join ( '\n' ) + '\n' ) ;
170+ return true ;
171+ }
172+
173+ return false ;
174+ } catch {
175+ // Non-blocking: ignore errors
176+ return false ;
177+ }
178+ }
0 commit comments