import { readFile } from "node:fs/promises" import { homedir } from "node:os" import { isAbsolute, join, resolve } from "node:path" import { Effect } from "effect" import { isFileError } from "../fs-error.ts" import { mergeConfigs } from "./merge.ts" import { ConfigError, decodeConfig, type SteConfig } from "./schema.ts" const legacyAgentConfigDirectory = (cwd: string): string => { const configured = process.env.PI_CODING_AGENT_DIR if (!configured) return join(homedir(), ".pi", "agent") if (configured === "~") return homedir() const expanded = configured.startsWith("~/") || (process.platform === "win32" && configured.startsWith("~\\")) ? join(homedir(), configured.slice(2)) : configured return isAbsolute(expanded) ? expanded : resolve(cwd, expanded) } const xdgConfigDirectory = (): string => { const configured = process.env.XDG_CONFIG_HOME return configured && isAbsolute(configured) ? configured : join(homedir(), ".config") } export const globalConfigPath = (): string => join(xdgConfigDirectory(), "simple-english", "config.json") export const projectConfigPath = (cwd: string): string => join(cwd, ".simple-english.json") export const legacyGlobalConfigPath = (cwd = process.cwd()): string => join(legacyAgentConfigDirectory(cwd), "simple-english.json") export const legacyProjectConfigPath = (cwd: string): string => join(cwd, ".pi", "simple-english.json") const readConfigFile = ( path: string, optional: boolean, ): Effect.Effect => Effect.tryPromise({ try: () => readFile(path, "utf8"), catch: (cause) => cause, }).pipe( Effect.matchEffect({ onFailure: (cause) => optional && isFileError(cause, "ENOENT") ? Effect.succeed(undefined) : Effect.fail(new ConfigError(`cannot read config file ${path}: ${cause}`)), onSuccess: (text) => Effect.try({ try: () => JSON.parse(text) as unknown, catch: (cause) => new ConfigError(`invalid JSON in ${path}: ${cause}`), }).pipe(Effect.flatMap((json) => decodeConfig(json, path))), }), ) const readConfigWithFallback = ( path: string, fallbackPath: string, ): Effect.Effect => readConfigFile(path, true).pipe( Effect.flatMap((config) => config === undefined ? readConfigFile(fallbackPath, true).pipe(Effect.map((fallback) => fallback ?? {})) : Effect.succeed(config), ), ) export const loadConfig = ( explicitPath?: string, cwd = process.cwd(), includeProjectConfig = true, ): Effect.Effect => { if (explicitPath !== undefined) { return readConfigFile(explicitPath, false).pipe(Effect.map((config) => config ?? {})) } const globalConfig = readConfigWithFallback(globalConfigPath(), legacyGlobalConfigPath(cwd)) if (!includeProjectConfig) return globalConfig const projectConfig = readConfigWithFallback(projectConfigPath(cwd), legacyProjectConfigPath(cwd)) return Effect.all([globalConfig, projectConfig]).pipe( Effect.map(([global, project]) => mergeConfigs(global, project)), ) }