// lib/yaml-config.mjs — minimal YAML reader for config-vars.yaml. // // Scope is intentionally tiny: config-vars.yaml is exactly two levels deep // (top-level section header `key:` → 2-space-indented `key: value` scalars). // We deliberately support NO lists, NO flow style ([], {}), NO anchors and // NO multiline — config-vars.yaml uses none of them, so a parser that handled // them would be untested surface. Mirrors the zero-dependency, single-purpose // spirit of the dotenv parser this replaces. // // Value rules (same philosophy as the old dotenv parser): // - a single layer of matching surrounding quotes ('…' or "…") is removed, // and anything after the closing quote (e.g. a trailing ` # comment`) is dropped // - an unquoted value is cut at the first ` #` (space-hash) inline comment // - a value that is empty or starts with `#` is treated as empty // - NO variable expansion: `$FOO`, `${FOO}`, backticks stay literal // - numbers stay strings (callers Number() them, as with dotenv) /** Parse one `key:`-stripped raw value into its literal string. */ export function parseScalar(raw) { let s = String(raw).trim() if (s === '' || s[0] === '#') return '' const q = s[0] if (q === '"' || q === "'") { const end = s.indexOf(q, 1) if (end !== -1) return s.slice(1, end) // ignore anything after the closing quote // no closing quote → fall through and treat the (still-quoted) text literally } const hash = s.indexOf(' #') if (hash !== -1) s = s.slice(0, hash).trim() return s } /** * Parse config-vars.yaml text into a nested plain object. * * Top-level `section:` with no value opens a mapping; subsequent indented * `key: value` lines attach to it. A top-level `key: value` stays a root scalar. * * @param {string} text * @returns {Record} */ export function parseYamlConfig(text) { const root = {} if (typeof text !== 'string') return root let section = null for (const rawLine of text.split('\n')) { const line = rawLine.replace(/\r$/, '') // tolerate CRLF const trimmed = line.trim() if (trimmed === '' || trimmed[0] === '#') continue // blank / full-line comment const colon = line.indexOf(':') if (colon === -1) continue // not a key: line; ignore const key = line.slice(0, colon).trim() if (key === '') continue const indent = line.length - line.replace(/^\s+/, '').length const value = parseScalar(line.slice(colon + 1)) if (indent === 0) { if (value === '') { section = {} root[key] = section } else { root[key] = value section = null } } else if (section) { section[key] = value } else { root[key] = value // indented line with no open section — tolerate as root scalar } } return root }