import { existsSync } from "fs"; import { mkdir, readFile, rename, rm, writeFile } from "fs/promises"; import { dirname, join } from "path"; import { expandUserHome, getGloomberbHome, isGloomberbHomeOverridden } from "../home"; import type { AppConfig } from "../../../types/config"; import { createDefaultConfig } from "../../../types/config"; import { debugLog } from "../../../utils/debug-log"; import { EXTRACTED_PLUGINS } from "../../../plugins/seed"; import { normalizeConfigForSave, normalizeLoadedConfig, } from "./normalize"; const configLog = debugLog.createLogger("config"); function getGlobalConfigFile(): string { return join(getGloomberbHome(), "config.json"); } const expandHomePath = expandUserHome; /** * The data directory the global config records, or null on a first run. * * Under `GLOOMBERB_HOME`, a recorded directory that no longer exists gives * way to the home itself: a config.json carried along in a moved folder * still names the old `~/.gloomberb`, and honouring that would recreate the * old folder, which is the one thing the move was meant to avoid. */ export async function getDataDir(): Promise { try { const raw = await readFile(getGlobalConfigFile(), "utf-8"); const config = JSON.parse(raw) as { dataDir?: string }; const recorded = config.dataDir || null; if (recorded && isGloomberbHomeOverridden() && !existsSync(recorded)) return getGloomberbHome(); return recorded; } catch { return null; } } export async function loadConfig(dataDir: string): Promise { const { config } = await loadConfigState(dataDir); return config; } async function loadConfigState(dataDir: string): Promise<{ config: AppConfig; needsSave: boolean }> { const configPath = join(dataDir, "config.json"); try { const raw = await readFile(configPath, "utf-8"); const saved = JSON.parse(raw) as Record; const state = normalizeLoadedConfig(saved, dataDir); // A config.json that names a different directory than the one it sits in // came along with a moved folder. Record where it lives now, so the next // launch reads it directly instead of through the missing-path fallback. const recorded = typeof saved.dataDir === "string" ? saved.dataDir : null; return { config: state.config, needsSave: state.needsSave || (recorded !== null && recorded !== dataDir) }; } catch { // Fresh installs have no former built-in plugins to restore. Record the // current baseline so onboarding never waits for plugin network installs. return { config: { ...createDefaultConfig(dataDir), seededPlugins: EXTRACTED_PLUGINS.map((plugin) => plugin.id) }, needsSave: true, }; } } export async function saveConfig(config: AppConfig): Promise { const configPath = join(config.dataDir, "config.json"); await mkdir(dirname(configPath), { recursive: true }); const persisted = normalizeConfigForSave(config); const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`; try { await writeFile(tempPath, JSON.stringify(persisted, null, 2), "utf-8"); await rename(tempPath, configPath); } catch (error) { await rm(tempPath, { force: true }).catch(() => {}); throw error; } } export async function initDataDir(dataDir: string): Promise { configLog.info(`Initializing data directory: ${dataDir}`); await mkdir(dataDir, { recursive: true }); const { config, needsSave } = await loadConfigState(dataDir); if (needsSave) { await saveConfig(config); } return config; } export async function resetAllData(dataDir: string): Promise { await rm(dataDir, { recursive: true, force: true }); } export async function exportConfig(config: AppConfig, destPath: string): Promise { const { dataDir, ...rest } = config; await writeFile(expandHomePath(destPath), JSON.stringify(rest, null, 2), "utf-8"); } export async function importConfig(dataDir: string, srcPath: string): Promise { const raw = await readFile(expandHomePath(srcPath), "utf-8"); const saved = JSON.parse(raw) as Record; const { config } = normalizeLoadedConfig(saved, dataDir); await saveConfig(config); return config; }