/** * Shared settings.json loading utilities. * * Reads global (~/.pi/agent/settings.json) and project (.pi/settings.json) * settings, merging them so the closest-to-cwd wins. */ import { getAgentDir } from "@earendil-works/pi-coding-agent"; import * as fs from "node:fs"; import * as path from "node:path"; type SettingsObject = Record; function isSettingsObject(value: unknown): value is SettingsObject { return typeof value === "object" && value !== null && !Array.isArray(value); } function mergeInto(target: SettingsObject, source: SettingsObject): void { for (const [key, value] of Object.entries(source)) { const existing = target[key]; if (isSettingsObject(existing) && isSettingsObject(value)) { mergeInto(existing, value); } else { target[key] = value; } } } /** * Read and merge settings from global + all project-level settings.json files. * Project settings are walked up from `cwd` and applied farthest-first, * so the closest `.pi/settings.json` wins over more distant ones. */ export function loadSettings(cwd: string): Record { const merged: SettingsObject = {}; function mergeFrom(raw: string) { try { const settings = JSON.parse(raw); if (isSettingsObject(settings)) { mergeInto(merged, settings); } } catch { /* ignore */ } } // Global settings try { mergeFrom(fs.readFileSync(path.join(getAgentDir(), "settings.json"), "utf-8")); } catch { /* ignore */ } // Project settings (walk up from cwd, collect all, apply farthest-first so closest wins) const projectPaths: string[] = []; let dir = cwd; while (true) { projectPaths.push(path.join(dir, ".pi", "settings.json")); const parent = path.dirname(dir); if (parent === dir) break; dir = parent; } for (let i = projectPaths.length - 1; i >= 0; i--) { try { mergeFrom(fs.readFileSync(projectPaths[i], "utf-8")); } catch { /* ignore */ } } return merged; }