(filePath: string, updates: Record<string, string>)
| 90 | * @param updates - Object mapping variable names to their new values |
| 91 | */ |
| 92 | export async function updateDotEnv(filePath: string, updates: Record<string, string>): Promise<void> { |
| 93 | let content = '' |
| 94 | |
| 95 | try { |
| 96 | content = await fs.readFile(filePath, 'utf-8') |
| 97 | } catch (error) { |
| 98 | if (!isErrnoENOENT(error)) { |
| 99 | throw error |
| 100 | } |
| 101 | // File doesn't exist, we'll create it |
| 102 | } |
| 103 | |
| 104 | const pendingUpdates = new Set(Object.keys(updates)) |
| 105 | |
| 106 | // Replace existing variables using regex |
| 107 | for (const key of Object.keys(updates)) { |
| 108 | // Match KEY=... until end of line (handles quoted and unquoted values) |
| 109 | const regex = new RegExp(`^(${escapeRegex(key)})=.*$`, 'm') |
| 110 | if (regex.test(content)) { |
| 111 | content = content.replace(regex, `${key}=${formatValue(updates[key])}`) |
| 112 | pendingUpdates.delete(key) |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | // Append new variables at the end |
| 117 | for (const key of pendingUpdates) { |
| 118 | const formattedLine = `${key}=${formatValue(updates[key])}` |
| 119 | |
| 120 | if (content === '') { |
| 121 | content = formattedLine |
| 122 | } else if (content.endsWith('\n')) { |
| 123 | content += formattedLine |
| 124 | } else { |
| 125 | content += `\n${formattedLine}` |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | // Ensure trailing newline |
| 130 | if (content !== '' && !content.endsWith('\n')) { |
| 131 | content += '\n' |
| 132 | } |
| 133 | |
| 134 | // Ensure parent directory exists |
| 135 | const dir = path.dirname(filePath) |
| 136 | await fs.mkdir(dir, { recursive: true }) |
| 137 | |
| 138 | await fs.writeFile(filePath, content, 'utf-8') |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Escape special regex characters in a string. |
no test coverage detected