Files
oh-my-openagent/src/plugin-config.ts

214 lines
6.3 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
import {
log,
deepMerge,
getOpenCodeConfigDir,
addConfigLoadError,
parseJsonc,
detectPluginConfigFile,
migrateConfigFile,
} from "./shared";
import { migrateLegacyConfigFile } from "./shared/migrate-legacy-config-file";
import { LEGACY_CONFIG_BASENAME } from "./shared/plugin-identity";
const PARTIAL_STRING_ARRAY_KEYS = new Set([
"disabled_mcps",
"disabled_agents",
"disabled_skills",
"disabled_hooks",
"disabled_commands",
"disabled_tools",
]);
export function parseConfigPartially(
rawConfig: Record<string, unknown>
): OhMyOpenCodeConfig | null {
const fullResult = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
if (fullResult.success) {
return fullResult.data;
}
const partialConfig: Record<string, unknown> = {};
const invalidSections: string[] = [];
for (const key of Object.keys(rawConfig)) {
if (PARTIAL_STRING_ARRAY_KEYS.has(key)) {
const sectionValue = rawConfig[key];
if (Array.isArray(sectionValue) && sectionValue.every((value) => typeof value === "string")) {
partialConfig[key] = sectionValue;
}
continue;
}
const sectionResult = OhMyOpenCodeConfigSchema.safeParse({ [key]: rawConfig[key] });
if (sectionResult.success) {
const parsed = sectionResult.data as Record<string, unknown>;
if (parsed[key] !== undefined) {
partialConfig[key] = parsed[key];
}
} else {
const sectionErrors = sectionResult.error.issues
.filter((i) => i.path[0] === key)
.map((i) => `${i.path.join(".")}: ${i.message}`)
.join(", ");
if (sectionErrors) {
invalidSections.push(`${key}: ${sectionErrors}`);
}
}
}
if (invalidSections.length > 0) {
log("Partial config loaded — invalid sections skipped:", invalidSections);
}
return partialConfig as OhMyOpenCodeConfig;
}
export function loadConfigFromPath(
configPath: string,
_ctx: unknown
): OhMyOpenCodeConfig | null {
try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "utf-8");
const rawConfig = parseJsonc<Record<string, unknown>>(content);
migrateConfigFile(configPath, rawConfig);
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
if (result.success) {
log(`Config loaded from ${configPath}`, { agents: result.data.agents });
return result.data;
}
const errorMsg = result.error.issues
.map((i) => `${i.path.join(".")}: ${i.message}`)
.join(", ");
log(`Config validation error in ${configPath}:`, result.error.issues);
addConfigLoadError({
path: configPath,
error: `Partial config loaded — invalid sections skipped: ${errorMsg}`,
});
const partialResult = parseConfigPartially(rawConfig);
if (partialResult) {
log(`Partial config loaded from ${configPath}`, { agents: partialResult.agents });
return partialResult;
}
return null;
}
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
log(`Error loading config from ${configPath}:`, err);
addConfigLoadError({ path: configPath, error: errorMsg });
}
return null;
}
export function mergeConfigs(
base: OhMyOpenCodeConfig,
override: OhMyOpenCodeConfig
): OhMyOpenCodeConfig {
return {
...base,
...override,
agents: deepMerge(base.agents, override.agents),
categories: deepMerge(base.categories, override.categories),
disabled_agents: [
...new Set([
...(base.disabled_agents ?? []),
...(override.disabled_agents ?? []),
]),
],
disabled_mcps: [
...new Set([
...(base.disabled_mcps ?? []),
...(override.disabled_mcps ?? []),
]),
],
disabled_hooks: [
...new Set([
...(base.disabled_hooks ?? []),
...(override.disabled_hooks ?? []),
]),
],
disabled_commands: [
...new Set([
...(base.disabled_commands ?? []),
...(override.disabled_commands ?? []),
]),
],
disabled_skills: [
...new Set([
...(base.disabled_skills ?? []),
...(override.disabled_skills ?? []),
]),
],
disabled_tools: [
...new Set([
...(base.disabled_tools ?? []),
...(override.disabled_tools ?? []),
]),
],
claude_code: deepMerge(base.claude_code, override.claude_code),
};
}
export function loadPluginConfig(
directory: string,
ctx: unknown
): OhMyOpenCodeConfig {
// User-level config path - prefer .jsonc over .json
const configDir = getOpenCodeConfigDir({ binary: "opencode" });
const userDetected = detectPluginConfigFile(configDir);
const userConfigPath =
userDetected.format !== "none"
? userDetected.path
: path.join(configDir, "oh-my-opencode.json");
// Auto-copy legacy config file to canonical name if needed
if (userDetected.format !== "none" && path.basename(userDetected.path).startsWith(LEGACY_CONFIG_BASENAME)) {
migrateLegacyConfigFile(userDetected.path);
}
// Project-level config path - prefer .jsonc over .json
const projectBasePath = path.join(directory, ".opencode");
const projectDetected = detectPluginConfigFile(projectBasePath);
const projectConfigPath =
projectDetected.format !== "none"
? projectDetected.path
: path.join(projectBasePath, "oh-my-opencode.json");
// Auto-copy legacy project config file to canonical name if needed
if (projectDetected.format !== "none" && path.basename(projectDetected.path).startsWith(LEGACY_CONFIG_BASENAME)) {
migrateLegacyConfigFile(projectDetected.path);
}
// Load user config first (base). Parse empty config through Zod to apply field defaults.
let config: OhMyOpenCodeConfig =
loadConfigFromPath(userConfigPath, ctx) ?? OhMyOpenCodeConfigSchema.parse({});
// Override with project config
const projectConfig = loadConfigFromPath(projectConfigPath, ctx);
if (projectConfig) {
config = mergeConfigs(config, projectConfig);
}
config = {
...config,
};
log("Final merged config", {
agents: config.agents,
disabled_agents: config.disabled_agents,
disabled_mcps: config.disabled_mcps,
disabled_hooks: config.disabled_hooks,
claude_code: config.claude_code,
});
return config;
}