Files
wow-pi/packages/wow-core/src/config.ts
2026-06-09 00:39:43 +08:00

185 lines
4.9 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import * as path from "node:path";
import YAML from "yaml";
export interface WowConfig {
contexts?: string[];
logger?: { level?: string };
inject?: {
enabled?: boolean;
envFiles?: string[];
env?: Record<string, string> | Array<Record<string, string>>;
overrideExisting?: boolean;
};
[key: string]: unknown;
}
const AGENT_DIR_VAR = "PI_CODING_AGENT_DIR";
const CONFIG_FILES = ["wow.json", "wow.yaml", "wow.yml"];
let cachedConfig: WowConfig | undefined;
let cachedCwd = "";
export function getAgentDir(): string {
return process.env[AGENT_DIR_VAR] || path.join(homedir(), ".pi", "agent");
}
export function resolveConfigPaths(cwd = process.cwd()): string[] {
const globalConfigs = CONFIG_FILES.map((file) =>
path.join(getAgentDir(), file),
);
const projectConfigs = CONFIG_FILES.map((file) =>
path.join(cwd, ".pi", file),
);
return [...globalConfigs, ...projectConfigs].filter(
(filePath, index, all) => all.indexOf(filePath) === index,
);
}
export function resetWowConfigCache(): void {
cachedConfig = undefined;
cachedCwd = "";
}
export function getWowSettings(cwd = process.cwd()): WowConfig | undefined {
if (cachedConfig !== undefined && cachedCwd === cwd) return cachedConfig;
cachedCwd = cwd;
cachedConfig = loadWowConfig(cwd);
return cachedConfig;
}
export function getWowSettingSync<T = unknown>(
key: string,
cwd = process.cwd(),
): T | undefined {
const config = getWowSettings(cwd);
return config?.[key] as T | undefined;
}
export async function readWowConfig<T = unknown>(
key: string,
cwd = process.cwd(),
): Promise<T | undefined> {
return getWowSettingSync<T>(key, cwd);
}
function loadWowConfig(cwd: string): WowConfig | undefined {
let merged: WowConfig | undefined;
for (const filePath of resolveConfigPaths(cwd)) {
const config = readConfigFile(filePath);
if (!config) continue;
merged = mergeWowConfig(merged ?? {}, config);
}
return merged;
}
function readConfigFile(filePath: string): WowConfig | undefined {
try {
if (!existsSync(filePath)) return undefined;
const content = readFileSync(filePath, "utf-8");
const parsed = parseConfigContent(filePath, content);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
return undefined;
return parsed as WowConfig;
} catch {
return undefined;
}
}
function parseConfigContent(filePath: string, content: string): unknown {
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
return YAML.parse(content);
}
return JSON.parse(content);
}
function mergeWowConfig(base: WowConfig, override: WowConfig): WowConfig {
const merged: WowConfig = { ...base, ...override };
if (base.logger || override.logger) {
merged.logger = { ...(base.logger ?? {}), ...(override.logger ?? {}) };
}
if (base.inject || override.inject) {
merged.inject = mergeInjectConfig(base.inject, override.inject);
}
if (Array.isArray(base.contexts) || Array.isArray(override.contexts)) {
merged.contexts = mergeStringArrays(base.contexts, override.contexts);
}
return merged;
}
function mergeInjectConfig(
base: WowConfig["inject"],
override: WowConfig["inject"],
): WowConfig["inject"] {
const merged = { ...(base ?? {}), ...(override ?? {}) };
merged.envFiles = mergeStringArrays(base?.envFiles, override?.envFiles);
merged.env = mergeEnvConfig(base?.env, override?.env);
return merged;
}
type EnvConfig = NonNullable<WowConfig["inject"]>["env"];
function mergeEnvConfig(
base: EnvConfig | undefined,
override: EnvConfig | undefined,
): Record<string, string> | undefined {
const merged: Record<string, string> = {};
for (const source of [base, override]) {
for (const [key, value] of Object.entries(normaliseEnvMap(source))) {
merged[key] = value;
}
}
return Object.keys(merged).length > 0 ? merged : undefined;
}
function normaliseEnvMap(input: unknown): Record<string, string> {
if (!input) return {};
const entries = Array.isArray(input)
? Object.assign(
{},
...input.filter(
(item) => item && typeof item === "object" && !Array.isArray(item),
),
)
: input;
if (!entries || typeof entries !== "object" || Array.isArray(entries))
return {};
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(entries)) {
if (typeof value === "string" || value instanceof String) {
result[key] = String(value);
}
}
return result;
}
function mergeStringArrays(
base?: string[],
override?: string[],
): string[] | undefined {
const merged: string[] = [];
const seen = new Set<string>();
for (const list of [base, override]) {
if (!Array.isArray(list)) continue;
for (const item of list) {
if (typeof item !== "string" || seen.has(item)) continue;
seen.add(item);
merged.push(item);
}
}
return merged.length > 0 ? merged : undefined;
}