import yaml from 'js-yaml'; export type EnvEntry = { key: string; value: string }; export type Format = 'env' | 'properties' | 'yaml'; /** Convert KEY=value env pairs to Spring-style properties (lowercase, _ -> .) */ export function envToProperties(entries: EnvEntry[]): string { return entries .filter((e) => e.key.trim()) .map((e) => `${e.key.toLowerCase().replace(/_/g, '.')}=${e.value}`) .join('\n'); } /** Convert KEY=value env pairs to nested YAML */ export function envToYaml(entries: EnvEntry[]): string { const obj: Record = {}; for (const { key, value } of entries) { if (!key.trim()) continue; const path = key.toLowerCase().replace(/_/g, '.').split('.'); let cur: Record = obj; for (let i = 0; i < path.length - 1; i++) { if (typeof cur[path[i]] !== 'object' || cur[path[i]] === null) { cur[path[i]] = {}; } cur = cur[path[i]] as Record; } cur[path[path.length - 1]] = value; } return yaml.dump(obj, { lineWidth: -1, noRefs: true }).trimEnd(); } /** Convert KEY=value env pairs to .env format */ export function envToEnvText(entries: EnvEntry[]): string { return entries .filter((e) => e.key.trim()) .map((e) => `${e.key}=${e.value}`) .join('\n'); } /** Normalize a key to UPPER_SNAKE_CASE */ function toEnvKey(key: string): string { return key.replace(/[.\-]/g, '_').toUpperCase(); } /** Parse .properties text to env entries */ export function parseProperties(text: string): EnvEntry[] { const entries: EnvEntry[] = []; for (const line of text.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!')) continue; const eq = trimmed.indexOf('='); if (eq < 0) continue; entries.push({ key: toEnvKey(trimmed.slice(0, eq).trim()), value: trimmed.slice(eq + 1).trim(), }); } return entries; } /** Parse YAML text to env entries (flattens nested keys) */ export function parseYaml(text: string): EnvEntry[] { const doc = yaml.load(text); if (!doc || typeof doc !== 'object') return []; const entries: EnvEntry[] = []; function walk(obj: Record, prefix: string) { for (const [k, v] of Object.entries(obj)) { const path = prefix ? `${prefix}_${k}` : k; if (v !== null && typeof v === 'object' && !Array.isArray(v)) { walk(v as Record, path); } else { entries.push({ key: toEnvKey(path), value: String(v ?? '') }); } } } walk(doc as Record, ''); return entries; } /** Parse .env text to env entries */ export function parseEnvText(text: string): EnvEntry[] { const entries: EnvEntry[] = []; for (const line of text.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq < 0) continue; entries.push({ key: trimmed.slice(0, eq).trim(), value: trimmed.slice(eq + 1).trim(), }); } return entries; } /** Auto-detect format from text content */ export function detectFormat(text: string): Format { const lines = text.split('\n').filter((l) => l.trim() && !l.trim().startsWith('#')); if (lines.length === 0) return 'env'; // YAML: has indented lines or lines ending with : (no =) const hasIndent = lines.some((l) => /^\s{2,}\S/.test(l)); const hasColonNoEq = lines.some((l) => l.includes(':') && !l.includes('=')); if (hasIndent || (hasColonNoEq && !lines.some((l) => /^[A-Z_]+=/.test(l)))) return 'yaml'; // Properties: lowercase keys with dots const hasPropertyKeys = lines.some((l) => /^[a-z][a-z0-9.\-]+=/.test(l)); if (hasPropertyKeys) return 'properties'; return 'env'; } /** Parse text in a given format to env entries */ export function parseText(text: string, format: Format): EnvEntry[] { switch (format) { case 'properties': return parseProperties(text); case 'yaml': return parseYaml(text); case 'env': return parseEnvText(text); } } /** Convert env entries to text in a given format */ export function toText(entries: EnvEntry[], format: Format): string { switch (format) { case 'properties': return envToProperties(entries); case 'yaml': return envToYaml(entries); case 'env': return envToEnvText(entries); } }