import { loadGlobalConfig } from './global/globalConfig.js'; import { loadProjectConfig } from './project/projectConfig.js'; import { envVarNameFromPath } from './env/config-env-overrides.js'; import { getCachedProjectConfig, getCachedResolvedValue, hasCachedResolvedValue, setCachedProjectConfig, setCachedResolvedValue, } from './resolutionCache.js'; import type { ConfigParameterKey, LoadedConfig } from './resolvedConfig.js'; export type { ConfigParameterKey } from './resolvedConfig.js'; export { invalidateResolvedConfigCache, invalidateAllResolvedConfigCache } from './resolutionCache.js'; export interface PieceContext { provider?: LoadedConfig['provider']; model?: LoadedConfig['model']; providerOptions?: LoadedConfig['providerOptions']; } export interface ResolveConfigOptions { pieceContext?: PieceContext; } export type ConfigValueSource = 'env' | 'project' | 'piece' | 'global' | 'default'; export interface ResolvedConfigValue { value: LoadedConfig[K]; source: ConfigValueSource; } type ResolutionLayer = 'local' | 'piece' | 'global'; interface ResolutionRule { layers: readonly ResolutionLayer[]; defaultValue?: LoadedConfig[K]; mergeMode?: 'analytics'; pieceValue?: (pieceContext: PieceContext | undefined) => LoadedConfig[K] | undefined; } function loadProjectConfigCached(projectDir: string) { const cached = getCachedProjectConfig(projectDir); if (cached !== undefined) { return cached; } const loaded = loadProjectConfig(projectDir); setCachedProjectConfig(projectDir, loaded); return loaded; } const DEFAULT_RULE: ResolutionRule = { layers: ['local', 'global'], }; const PROVIDER_OPTIONS_ENV_PATHS = [ 'provider_options', 'provider_options.codex.network_access', 'provider_options.opencode.network_access', 'provider_options.claude.sandbox.allow_unsandboxed_commands', 'provider_options.claude.sandbox.excluded_commands', ] as const; const RESOLUTION_REGISTRY: Partial<{ [K in ConfigParameterKey]: ResolutionRule }> = { piece: { layers: ['local', 'global'], defaultValue: 'default' }, provider: { layers: ['local', 'piece', 'global'], defaultValue: 'claude', pieceValue: (pieceContext) => pieceContext?.provider, }, model: { layers: ['local', 'piece', 'global'], pieceValue: (pieceContext) => pieceContext?.model, }, providerOptions: { layers: ['local', 'piece', 'global'], pieceValue: (pieceContext) => pieceContext?.providerOptions, }, autoPr: { layers: ['local', 'global'] }, draftPr: { layers: ['local', 'global'] }, analytics: { layers: ['local', 'global'], mergeMode: 'analytics' }, verbose: { layers: ['local', 'global'], defaultValue: false }, }; function resolveAnalyticsMerged( project: ReturnType, global: ReturnType, ): LoadedConfig['analytics'] { const localAnalytics = project.analytics; const globalAnalytics = global.analytics; const enabled = localAnalytics?.enabled ?? globalAnalytics?.enabled; const eventsPath = localAnalytics?.eventsPath ?? globalAnalytics?.eventsPath; const retentionDays = localAnalytics?.retentionDays ?? globalAnalytics?.retentionDays; if (enabled === undefined && eventsPath === undefined && retentionDays === undefined) { return undefined; } return { enabled, eventsPath, retentionDays }; } function resolveAnalyticsSource( project: ReturnType, global: ReturnType, ): ConfigValueSource { if (project.analytics !== undefined) return 'project'; if (global.analytics !== undefined) return 'global'; return 'default'; } function getLocalLayerValue( project: ReturnType, key: K, ): LoadedConfig[K] | undefined { switch (key) { case 'piece': return project.piece as LoadedConfig[K] | undefined; case 'provider': return project.provider as LoadedConfig[K] | undefined; case 'model': return project.model as LoadedConfig[K] | undefined; case 'autoPr': return project.auto_pr as LoadedConfig[K] | undefined; case 'draftPr': return project.draft_pr as LoadedConfig[K] | undefined; case 'verbose': return project.verbose as LoadedConfig[K] | undefined; case 'analytics': return project.analytics as LoadedConfig[K] | undefined; case 'providerOptions': return project.providerOptions as LoadedConfig[K] | undefined; case 'providerProfiles': return project.providerProfiles as LoadedConfig[K] | undefined; default: return undefined; } } function getGlobalLayerValue( global: ReturnType, key: K, ): LoadedConfig[K] | undefined { return global[key as keyof typeof global] as LoadedConfig[K] | undefined; } function resolveByRegistry( key: K, project: ReturnType, global: ReturnType, options: ResolveConfigOptions | undefined, ): ResolvedConfigValue { const rule = (RESOLUTION_REGISTRY[key] ?? DEFAULT_RULE) as ResolutionRule; if (rule.mergeMode === 'analytics') { return { value: resolveAnalyticsMerged(project, global) as LoadedConfig[K], source: resolveAnalyticsSource(project, global), }; } for (const layer of rule.layers) { let value: LoadedConfig[K] | undefined; if (layer === 'local') { value = getLocalLayerValue(project, key); } else if (layer === 'piece') { value = rule.pieceValue?.(options?.pieceContext); } else { value = getGlobalLayerValue(global, key); } if (value !== undefined) { if (layer === 'local') { if (key === 'providerOptions' && hasProviderOptionsEnvOverride()) { return { value, source: 'env' }; } return { value, source: 'project' }; } if (layer === 'piece') { return { value, source: 'piece' }; } return { value, source: 'global' }; } } return { value: rule.defaultValue as LoadedConfig[K], source: 'default' }; } function hasProviderOptionsEnvOverride(): boolean { return PROVIDER_OPTIONS_ENV_PATHS.some((path) => process.env[envVarNameFromPath(path)] !== undefined); } function resolveUncachedConfigValue( projectDir: string, key: K, options?: ResolveConfigOptions, ): ResolvedConfigValue { const project = loadProjectConfigCached(projectDir); const global = loadGlobalConfig(); return resolveByRegistry(key, project, global, options); } export function resolveConfigValueWithSource( projectDir: string, key: K, options?: ResolveConfigOptions, ): ResolvedConfigValue { const resolved = resolveUncachedConfigValue(projectDir, key, options); if (!options?.pieceContext) { setCachedResolvedValue(projectDir, key, resolved.value); } return resolved; } export function resolveConfigValue( projectDir: string, key: K, options?: ResolveConfigOptions, ): LoadedConfig[K] { if (!options?.pieceContext && hasCachedResolvedValue(projectDir, key)) { return getCachedResolvedValue(projectDir, key) as LoadedConfig[K]; } return resolveConfigValueWithSource(projectDir, key, options).value; } export function resolveConfigValues( projectDir: string, keys: readonly K[], options?: ResolveConfigOptions, ): Pick { const result = {} as Pick; for (const key of keys) { result[key] = resolveConfigValue(projectDir, key, options); } return result; }