env-file.ts44 lines · main
| 1 | /** |
| 2 | * Merge briven-owned keys into a .env.local style file. Preserves |
| 3 | * user-set keys and comments; only updates keys whose name appears in |
| 4 | * the `updates` record. Returns the new full content. |
| 5 | */ |
| 6 | export function mergeEnvFile(existing: string, updates: Record<string, string>): string { |
| 7 | const updateKeys = new Set(Object.keys(updates)); |
| 8 | // Normalize CRLF (Windows-edited .env.local) to LF so we don't leave |
| 9 | // stray \r on preserved lines and emit mixed line endings on rewrite. |
| 10 | const normalized = existing.replace(/\r\n/g, '\n'); |
| 11 | const lines = normalized.split('\n'); |
| 12 | const out: string[] = []; |
| 13 | const seen = new Set<string>(); |
| 14 | |
| 15 | for (const line of lines) { |
| 16 | const match = line.match(/^([A-Z][A-Z0-9_]*)=/); |
| 17 | if (match && updateKeys.has(match[1]!)) { |
| 18 | const key = match[1]!; |
| 19 | out.push(`${key}=${quote(updates[key]!)}`); |
| 20 | seen.add(key); |
| 21 | } else { |
| 22 | out.push(line); |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | // Strip the synthetic trailing empty line from split('\n') so we |
| 27 | // don't accumulate blank lines across merges. |
| 28 | if (out.length > 0 && out[out.length - 1] === '') out.pop(); |
| 29 | |
| 30 | for (const key of updateKeys) { |
| 31 | if (!seen.has(key)) { |
| 32 | out.push(`${key}=${quote(updates[key]!)}`); |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | return out.join('\n') + '\n'; |
| 37 | } |
| 38 | |
| 39 | function quote(value: string): string { |
| 40 | // Always quote — keeps the file readable + avoids parsing edge cases |
| 41 | // when values contain spaces or shell-special chars. |
| 42 | const escaped = value.replace(/"/g, '\\"'); |
| 43 | return `"${escaped}"`; |
| 44 | } |