import { existsSync, readFileSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import path from 'node:path'; // biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension. import defaultOverrides from './features.json' with { type: 'json' }; export type FeatureOverrides = { global?: Record; sets?: Record>; }; type NormalizedFeatureOverrides = { global: Record; sets: Record>; }; export type FeatureOverridesSnapshot = { cachePath: string; overrides: FeatureOverrides; }; const DEFAULT_CACHE_FILENAME = 'features.json'; let cachedOverrides: NormalizedFeatureOverrides | null = null; function normalizeFeatureMap(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { return {}; } const result: Record = {}; for (const [key, entry] of Object.entries(value as Record)) { if (typeof entry === 'boolean') { result[key] = entry; } } return result; } function normalizeOverrides(value: unknown): NormalizedFeatureOverrides { if (!value || typeof value !== 'object' || Array.isArray(value)) { return { global: {}, sets: {} }; } const record = value as Record; const global = normalizeFeatureMap(record.global); const sets: Record> = {}; const rawSets = record.sets && typeof record.sets === 'object' && !Array.isArray(record.sets) ? (record.sets as Record) : {}; for (const [setName, setValue] of Object.entries(rawSets)) { const normalized = normalizeFeatureMap(setValue); if (Object.keys(normalized).length > 0) { sets[setName] = normalized; } } return { global, sets }; } function mergeOverrides( base: NormalizedFeatureOverrides, next: NormalizedFeatureOverrides, ): NormalizedFeatureOverrides { const sets: Record> = { ...base.sets }; for (const [setName, overrides] of Object.entries(next.sets)) { const existing = sets[setName]; sets[setName] = existing ? { ...existing, ...overrides } : { ...overrides }; } return { global: { ...base.global, ...next.global }, sets, }; } function toFeatureOverrides(overrides: NormalizedFeatureOverrides): FeatureOverrides { const result: FeatureOverrides = {}; if (Object.keys(overrides.global).length > 0) { result.global = overrides.global; } const setEntries = Object.entries(overrides.sets).filter(([, value]) => Object.keys(value).length > 0); if (setEntries.length > 0) { result.sets = Object.fromEntries(setEntries); } return result; } function resolveFeaturesCachePath(): string { const override = process.env.BIRD_FEATURES_CACHE ?? process.env.BIRD_FEATURES_PATH; if (override && override.trim().length > 0) { return path.resolve(override.trim()); } return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME); } function readOverridesFromFile(cachePath: string): NormalizedFeatureOverrides | null { if (!existsSync(cachePath)) { return null; } try { const raw = readFileSync(cachePath, 'utf8'); return normalizeOverrides(JSON.parse(raw)); } catch { return null; } } function readOverridesFromEnv(): NormalizedFeatureOverrides | null { const raw = process.env.BIRD_FEATURES_JSON; if (!raw || raw.trim().length === 0) { return null; } try { return normalizeOverrides(JSON.parse(raw)); } catch { return null; } } function writeOverridesToDisk(cachePath: string, overrides: NormalizedFeatureOverrides): Promise { const payload = toFeatureOverrides(overrides); return mkdir(path.dirname(cachePath), { recursive: true }).then(() => writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'), ); } export function loadFeatureOverrides(): NormalizedFeatureOverrides { if (cachedOverrides) { return cachedOverrides; } const base = normalizeOverrides(defaultOverrides); const fromFile = readOverridesFromFile(resolveFeaturesCachePath()); const fromEnv = readOverridesFromEnv(); let merged = base; if (fromFile) { merged = mergeOverrides(merged, fromFile); } if (fromEnv) { merged = mergeOverrides(merged, fromEnv); } cachedOverrides = merged; return merged; } export function getFeatureOverridesSnapshot(): FeatureOverridesSnapshot { const overrides = toFeatureOverrides(loadFeatureOverrides()); return { cachePath: resolveFeaturesCachePath(), overrides, }; } export function applyFeatureOverrides(setName: string, base: Record): Record { const overrides = loadFeatureOverrides(); const globalOverrides = overrides.global; const setOverrides = overrides.sets[setName]; if (Object.keys(globalOverrides).length === 0 && (!setOverrides || Object.keys(setOverrides).length === 0)) { return base; } if (setOverrides) { return { ...base, ...globalOverrides, ...setOverrides, }; } return { ...base, ...globalOverrides, }; } export async function refreshFeatureOverridesCache(): Promise { const cachePath = resolveFeaturesCachePath(); const base = normalizeOverrides(defaultOverrides); const fromFile = readOverridesFromFile(cachePath); const merged = mergeOverrides(base, fromFile ?? { global: {}, sets: {} }); await writeOverridesToDisk(cachePath, merged); cachedOverrides = null; return { cachePath, overrides: toFeatureOverrides(merged) }; } export function clearFeatureOverridesCache(): void { cachedOverrides = null; }