185 lines
4.9 KiB
TypeScript
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;
|
|
}
|