@@ -23,6 +23,61 @@ import { setupIntent } from './integrations/intent.js'
2323import type { Environment , Options } from './types.js'
2424import type { PersistedOptions } from './config-file.js'
2525
26+ const ENV_FILE_NAMES = new Set ( [ '.env' , '.env.local' , '.env.example' ] )
27+ const ENV_VARIABLE_PATTERN = / ^ \s * (?: e x p o r t \s + ) ? ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * = /
28+
29+ function mergeEnvFileContents ( existing : string , generated : string ) {
30+ const declaredVariables = new Set < string > ( )
31+ for ( const line of existing . split ( / \r ? \n / ) ) {
32+ const match = line . match ( ENV_VARIABLE_PATTERN )
33+ if ( match ) {
34+ declaredVariables . add ( match [ 1 ] )
35+ }
36+ }
37+
38+ const additions : Array < string > = [ ]
39+ for ( const block of generated . split ( / \r ? \n (?: [ \t ] * \r ? \n ) + / ) ) {
40+ const blockLines = block . split ( / \r ? \n / )
41+ while ( blockLines . at ( - 1 ) === '' ) {
42+ blockLines . pop ( )
43+ }
44+ const lines : Array < string > = [ ]
45+ let addedVariableCount = 0
46+ for ( const line of blockLines ) {
47+ const match = line . match ( ENV_VARIABLE_PATTERN )
48+ if ( ! match ) {
49+ lines . push ( line )
50+ continue
51+ }
52+ if ( declaredVariables . has ( match [ 1 ] ) ) {
53+ continue
54+ }
55+
56+ declaredVariables . add ( match [ 1 ] )
57+ addedVariableCount ++
58+ lines . push ( line )
59+ }
60+
61+ if ( addedVariableCount > 0 ) {
62+ additions . push ( lines . join ( '\n' ) )
63+ }
64+ }
65+
66+ if ( additions . length === 0 ) {
67+ return existing
68+ }
69+
70+ const separator = existing . length
71+ ? existing . endsWith ( '\n\n' )
72+ ? ''
73+ : existing . endsWith ( '\n' )
74+ ? '\n'
75+ : '\n\n'
76+ : ''
77+ const trailingNewline = generated . endsWith ( '\n' ) ? '\n' : ''
78+ return `${ existing } ${ separator } ${ additions . join ( '\n\n' ) } ${ trailingNewline } `
79+ }
80+
2681export async function hasPendingGitChanges (
2782 environment : Environment ,
2883 cwd : string ,
@@ -134,6 +189,20 @@ export async function writeFiles(
134189 false ,
135190 )
136191
192+ for ( const [ relativeFile , generatedContents ] of Object . entries (
193+ relativeOutputFiles ,
194+ ) ) {
195+ if (
196+ ENV_FILE_NAMES . has ( basename ( relativeFile ) ) &&
197+ relativeFile in currentFiles
198+ ) {
199+ relativeOutputFiles [ relativeFile ] = mergeEnvFileContents (
200+ currentFiles [ relativeFile ] ,
201+ generatedContents ,
202+ )
203+ }
204+ }
205+
137206 const overwrittenFiles : Array < string > = [ ]
138207 const changedFiles : Array < string > = [ ]
139208 for ( const relativeFile of Object . keys ( relativeOutputFiles ) ) {
@@ -146,22 +215,23 @@ export async function writeFiles(
146215 }
147216 }
148217
149- if ( ! forced && overwrittenFiles . length ) {
218+ const deletedFiles = output . deletedFiles
219+ . map ( toRelativePath )
220+ . filter ( ( file ) => environment . exists ( resolve ( cwd , file ) ) )
221+
222+ if ( ! forced && ( overwrittenFiles . length || deletedFiles . length ) ) {
150223 environment . warn (
151- 'The following will be overwritten ' ,
152- [ ...overwrittenFiles , ...output . deletedFiles ] . join ( '\n' ) ,
224+ 'The following files will be changed or deleted ' ,
225+ [ ...overwrittenFiles , ...deletedFiles ] . join ( '\n' ) ,
153226 )
154227 const shouldContinue = await environment . confirm ( 'Do you want to continue?' )
155228 if ( ! shouldContinue ) {
156229 throw new Error ( 'User cancelled' )
157230 }
158231 }
159232
160- for ( const filePath of output . deletedFiles ) {
161- const relativeFilePath = toRelativePath ( filePath )
162- if ( environment . exists ( resolve ( cwd , relativeFilePath ) ) ) {
163- await environment . deleteFile ( resolve ( cwd , relativeFilePath ) )
164- }
233+ for ( const relativeFilePath of deletedFiles ) {
234+ await environment . deleteFile ( resolve ( cwd , relativeFilePath ) )
165235 }
166236
167237 environment . startStep ( {
0 commit comments