const fs = require('fs'); const path = require('path'); const { CONFIG } = require('../config'); const { getValidator } = require('./configSchema'); const ENV_PATH = process.env.ENV_FILE ? path.resolve(process.env.ENV_FILE) : path.resolve(process.cwd(), '.env'); /** * Read the current .env file and parse into a key->value Map. */ function readEnvFile() { if (!fs.existsSync(ENV_PATH)) return new Map(); const lines = fs.readFileSync(ENV_PATH, 'utf8').split('\n'); const map = new Map(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const idx = line.indexOf('='); if (idx === -1) continue; const key = line.slice(0, idx).trim(); const value = line.slice(idx + 1).trim(); map.set(key, value); } return map; } /** * Write a Map of key->value back to the .env file, * preserving comments and blank lines. */ function writeEnvFile(updates) { if (!fs.existsSync(ENV_PATH)) { const lines = []; for (const [k, v] of updates) lines.push(`${k}=${v}`); fs.writeFileSync(ENV_PATH, lines.join('\n') + '\n', 'utf8'); return; } const raw = fs.readFileSync(ENV_PATH, 'utf8'); const lines = raw.split('\n'); const written = new Set(); const result = lines.map(line => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) return line; const idx = line.indexOf('='); if (idx === -1) return line; const key = line.slice(0, idx).trim(); if (updates.has(key)) { written.add(key); return `${key}=${updates.get(key)}`; } return line; }); // Append any new keys not already in the file for (const [k, v] of updates) { if (!written.has(k)) result.push(`${k}=${v}`); } fs.writeFileSync(ENV_PATH, result.join('\n'), 'utf8'); } /** * Apply a flat object of { KEY: value } to both CONFIG and .env. * Returns { applied: string[], errors: Array<{ key, error }> }. * * Every value is routed through services/configSchema so invalid inputs are * rejected before being persisted. Valid values are typed via the validator's * coerced return (boolean/number/string) into CONFIG; .env always stores * String(coerced). Invalid values are reported and NEITHER written to .env NOR * applied to CONFIG — callers see them in the `errors` array. */ function applyConfigUpdates(updates) { const applied = []; const errors = []; const coercedForEnv = new Map(); for (const [key, rawValue] of Object.entries(updates)) { const validator = getValidator(key); const result = validator.validate(rawValue); if (!result.ok) { errors.push({ key, error: result.error }); continue; } CONFIG[key] = result.coerced; coercedForEnv.set(key, String(result.coerced)); applied.push(key); } // Write only the valid entries to .env. Skip the disk write entirely when // nothing validated, so a fully-rejected save doesn't rewrite the file. if (applied.length > 0) { const envMap = readEnvFile(); for (const [key, value] of coercedForEnv) { envMap.set(key, value); } writeEnvFile(envMap); } return { applied, errors }; } /** * Read all current env values for the settings UI. */ function readAllConfig() { return readEnvFile(); } module.exports = { applyConfigUpdates, readAllConfig, readEnvFile, writeEnvFile };