refactor: split config module

This commit is contained in:
Peter Steinberger
2026-01-04 07:05:04 +01:00
parent 5e36e2c3f3
commit c9504a6f20
16 changed files with 2236 additions and 2082 deletions

188
src/config/io.ts Normal file
View File

@@ -0,0 +1,188 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import JSON5 from "json5";
import {
applyIdentityDefaults,
applySessionDefaults,
applyTalkApiKey,
} from "./defaults.js";
import { findLegacyConfigIssues } from "./legacy.js";
import {
CONFIG_PATH_CLAWDIS,
resolveConfigPath,
resolveStateDir,
} from "./paths.js";
import type {
ClawdisConfig,
ConfigFileSnapshot,
LegacyConfigIssue,
} from "./types.js";
import { validateConfigObject } from "./validation.js";
import { ClawdisSchema } from "./zod-schema.js";
export type ParseConfigJson5Result =
| { ok: true; parsed: unknown }
| { ok: false; error: string };
export type ConfigIoDeps = {
fs?: typeof fs;
json5?: typeof JSON5;
env?: NodeJS.ProcessEnv;
homedir?: () => string;
configPath?: string;
logger?: Pick<typeof console, "error" | "warn">;
};
function resolveConfigPathForDeps(deps: Required<ConfigIoDeps>): string {
if (deps.configPath) return deps.configPath;
return resolveConfigPath(deps.env, resolveStateDir(deps.env, deps.homedir));
}
function normalizeDeps(overrides: ConfigIoDeps = {}): Required<ConfigIoDeps> {
return {
fs: overrides.fs ?? fs,
json5: overrides.json5 ?? JSON5,
env: overrides.env ?? process.env,
homedir: overrides.homedir ?? os.homedir,
configPath: overrides.configPath ?? "",
logger: overrides.logger ?? console,
};
}
export function parseConfigJson5(
raw: string,
json5: { parse: (value: string) => unknown } = JSON5,
): ParseConfigJson5Result {
try {
return { ok: true, parsed: json5.parse(raw) as unknown };
} catch (err) {
return { ok: false, error: String(err) };
}
}
export function createConfigIO(overrides: ConfigIoDeps = {}) {
const deps = normalizeDeps(overrides);
const configPath = resolveConfigPathForDeps(deps);
function loadConfig(): ClawdisConfig {
try {
if (!deps.fs.existsSync(configPath)) return {};
const raw = deps.fs.readFileSync(configPath, "utf-8");
const parsed = deps.json5.parse(raw);
if (typeof parsed !== "object" || parsed === null) return {};
const validated = ClawdisSchema.safeParse(parsed);
if (!validated.success) {
deps.logger.error("Invalid config:");
for (const iss of validated.error.issues) {
deps.logger.error(`- ${iss.path.join(".")}: ${iss.message}`);
}
return {};
}
return applySessionDefaults(
applyIdentityDefaults(validated.data as ClawdisConfig),
);
} catch (err) {
deps.logger.error(`Failed to read config at ${configPath}`, err);
return {};
}
}
async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
const exists = deps.fs.existsSync(configPath);
if (!exists) {
const config = applyTalkApiKey(applySessionDefaults({}));
const legacyIssues: LegacyConfigIssue[] = [];
return {
path: configPath,
exists: false,
raw: null,
parsed: {},
valid: true,
config,
issues: [],
legacyIssues,
};
}
try {
const raw = deps.fs.readFileSync(configPath, "utf-8");
const parsedRes = parseConfigJson5(raw, deps.json5);
if (!parsedRes.ok) {
return {
path: configPath,
exists: true,
raw,
parsed: {},
valid: false,
config: {},
issues: [
{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` },
],
legacyIssues: [],
};
}
const legacyIssues = findLegacyConfigIssues(parsedRes.parsed);
const validated = validateConfigObject(parsedRes.parsed);
if (!validated.ok) {
return {
path: configPath,
exists: true,
raw,
parsed: parsedRes.parsed,
valid: false,
config: {},
issues: validated.issues,
legacyIssues,
};
}
return {
path: configPath,
exists: true,
raw,
parsed: parsedRes.parsed,
valid: true,
config: applyTalkApiKey(applySessionDefaults(validated.config)),
issues: [],
legacyIssues,
};
} catch (err) {
return {
path: configPath,
exists: true,
raw: null,
parsed: {},
valid: false,
config: {},
issues: [{ path: "", message: `read failed: ${String(err)}` }],
legacyIssues: [],
};
}
}
async function writeConfigFile(cfg: ClawdisConfig) {
await deps.fs.promises.mkdir(path.dirname(configPath), {
recursive: true,
});
const json = JSON.stringify(cfg, null, 2).trimEnd().concat("\n");
await deps.fs.promises.writeFile(configPath, json, "utf-8");
}
return {
configPath,
loadConfig,
readConfigFileSnapshot,
writeConfigFile,
};
}
const defaultIO = createConfigIO({ configPath: CONFIG_PATH_CLAWDIS });
export const loadConfig = defaultIO.loadConfig;
export const readConfigFileSnapshot = defaultIO.readConfigFileSnapshot;
export const writeConfigFile = defaultIO.writeConfigFile;