/** * Config loader for APOPHIS CLI. * * Responsibilities: * - Config file discovery (.js, .ts, .json, or "apophis" field in package.json) * - Config loading with tsx for .ts files * - Profile resolution from config.profiles * - Preset resolution and application * - Environment-specific overrides * - Unknown-key hard failure with exact path * - Monorepo boundary detection */ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { pathToFileURL } from 'node:url'; import { resolvePacks } from '../../protocol-packs/index.js'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface Config { mode?: 'verify' | 'observe' | 'qualify'; profile?: string; preset?: string; routes?: string[]; seed?: number; artifactDir?: string; environments?: Record; profiles?: Record; presets?: Record; scenarios?: ScenarioConfigDef[]; [key: string]: unknown; } export interface EnvironmentPolicy { allowedModes?: ('verify' | 'observe' | 'qualify')[]; blockQualify?: boolean; allowChaosOnProtected?: boolean; allowVerify?: boolean; allowObserve?: boolean; allowQualify?: boolean; allowChaos?: boolean; allowBlocking?: boolean; requireSink?: boolean; sinks?: Record; [key: string]: unknown; } export interface ProfileDefinition { name?: string; mode?: 'verify' | 'observe' | 'qualify'; preset?: string; routes?: string[]; seed?: number; artifactDir?: string; environment?: string; observe?: boolean; sampling?: number; blocking?: boolean; sinks?: Record; features?: string[]; chaos?: boolean; chaosStrategy?: 'one' | 'all' | 'sample' | 'routes'; chaosSampleSize?: number; chaosSampleRoutes?: string[]; } export interface PresetDefinition { name?: string; mode?: 'verify' | 'observe' | 'qualify'; routes?: string[]; seed?: number; timeout?: number; parallel?: boolean; chaos?: boolean; observe?: boolean; runs?: number; sampling?: number; blocking?: boolean; sinks?: Record; features?: string[]; chaosStrategy?: 'one' | 'all' | 'sample' | 'routes'; chaosSampleSize?: number; chaosSampleRoutes?: string[]; } export interface ScenarioConfigDef { name: string; steps: Array<{ name: string; request: { method: string; url: string; body?: unknown; headers?: Record; }; expect: string[]; capture?: Record; }>; } export interface LoadConfigOptions { cwd: string; configPath?: string; profileName?: string; env?: string; } export interface LoadConfigResult { config: Config; configPath: string | null; isMonorepo: boolean; profileName: string | null; presetName: string | null; } // --------------------------------------------------------------------------- // Schema definition (TypeBox-style, plain TS for now) // --------------------------------------------------------------------------- interface SchemaField { type: 'string' | 'number' | 'boolean' | 'array' | 'object'; optional?: boolean; items?: SchemaField; properties?: Record; enumValues?: string[]; min?: number; } // Schema for top-level config keys const CONFIG_SCHEMA: Record = { mode: { type: 'string', optional: true, enumValues: ['verify', 'observe', 'qualify'] }, profile: { type: 'string', optional: true }, preset: { type: 'string', optional: true }, routes: { type: 'array', optional: true, items: { type: 'string' } }, seed: { type: 'number', optional: true }, artifactDir: { type: 'string', optional: true }, environments: { type: 'object', optional: true, }, profiles: { type: 'object', optional: true, properties: {}, }, presets: { type: 'object', optional: true, properties: {}, }, packs: { type: 'array', optional: true, items: { type: 'string' }, }, metadata: { type: 'object', optional: true, }, scenarios: { type: 'array', optional: true, items: { type: 'object' }, }, pluginContracts: { type: 'object', optional: true, properties: {}, }, }; // Schema for EnvironmentPolicy values (inside environments.) const ENVIRONMENT_POLICY_SCHEMA: Record = { name: { type: 'string', optional: false }, allowVerify: { type: 'boolean', optional: true }, allowObserve: { type: 'boolean', optional: true }, allowQualify: { type: 'boolean', optional: true }, allowChaos: { type: 'boolean', optional: true }, allowBlocking: { type: 'boolean', optional: true }, requireSink: { type: 'boolean', optional: true }, allowedModes: { type: 'array', optional: true, items: { type: 'string' } }, blockQualify: { type: 'boolean', optional: true }, allowChaosOnProtected: { type: 'boolean', optional: true }, }; // Schema for ProfileDefinition values (inside profiles.) const PROFILE_SCHEMA: Record = { name: { type: 'string', optional: false }, mode: { type: 'string', optional: true, enumValues: ['verify', 'observe', 'qualify'] }, preset: { type: 'string', optional: true }, routes: { type: 'array', optional: true, items: { type: 'string' } }, seed: { type: 'number', optional: true }, artifactDir: { type: 'string', optional: true }, environment: { type: 'string', optional: true }, features: { type: 'array', optional: true, items: { type: 'string' } }, sampling: { type: 'number', optional: true }, blocking: { type: 'boolean', optional: true }, sinks: { type: 'object', optional: true }, chaos: { type: 'object', optional: true }, chaosStrategy: { type: 'string', optional: true, enumValues: ['one', 'all', 'sample', 'routes'] }, chaosSampleSize: { type: 'number', optional: true, min: 1 }, chaosSampleRoutes: { type: 'array', optional: true, items: { type: 'string' } }, }; // Schema for PresetDefinition values (inside presets.) const PRESET_SCHEMA: Record = { name: { type: 'string', optional: false }, timeout: { type: 'number', optional: true, min: 0 }, parallel: { type: 'boolean', optional: true }, chaos: { type: 'boolean', optional: true }, chaosStrategy: { type: 'string', optional: true, enumValues: ['one', 'all', 'sample', 'routes'] }, chaosSampleSize: { type: 'number', optional: true, min: 1 }, chaosSampleRoutes: { type: 'array', optional: true, items: { type: 'string' } }, observe: { type: 'boolean', optional: true }, features: { type: 'array', optional: true, items: { type: 'string' } }, sampling: { type: 'number', optional: true }, blocking: { type: 'boolean', optional: true }, sinks: { type: 'object', optional: true }, runs: { type: 'number', optional: true, min: 1 }, }; // --------------------------------------------------------------------------- // Config discovery // --------------------------------------------------------------------------- const CONFIG_FILES = [ 'apophis.config.js', 'apophis.config.ts', 'apophis.config.json', ]; /** * Discover config file in cwd or return null. */ export function discoverConfig(cwd: string): string | null { for (const file of CONFIG_FILES) { const fullPath = resolve(cwd, file); if (existsSync(fullPath)) { return fullPath; } } return null; } /** * Load package.json and check for "apophis" field. */ export function loadPackageJsonConfig(cwd: string): { config: Config | null; path: string | null } { const pkgPath = resolve(cwd, 'package.json'); if (!existsSync(pkgPath)) { return { config: null, path: null }; } const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); if (pkg.apophis && typeof pkg.apophis === 'object') { return { config: pkg.apophis as Config, path: pkgPath }; } return { config: null, path: pkgPath }; } // --------------------------------------------------------------------------- // Config loading // --------------------------------------------------------------------------- /** * Load a config file by path. * Supports .js, .ts (via dynamic import when tsx is available), and .json. */ export async function loadConfigFile(configPath: string): Promise { if (configPath.endsWith('.json')) { const content = readFileSync(configPath, 'utf-8'); return JSON.parse(content) as Config; } if (configPath.endsWith('.ts') && !process.execArgv.some(a => a.includes('tsx') || a.includes('ts-node'))) { throw new Error( `TypeScript config file ${configPath} requires a TS loader.\n` + 'Convert to apophis.config.js or apophis.config.json, or run through tsx:\n' + ' npx tsx ./node_modules/.bin/apophis verify' ); } // For .js and .ts, use dynamic import. // tsx handles .ts files in dev environments. const fileUrl = pathToFileURL(configPath).href; const mod = await import(fileUrl); // Support both default export and direct export const config = mod.default ?? mod; if (!config || typeof config !== 'object') { throw new Error(`Config file at ${configPath} must export an object`); } return config as Config; } // --------------------------------------------------------------------------- // Schema validation (unknown-key rejection) // --------------------------------------------------------------------------- export class ConfigValidationError extends Error { constructor( message: string, public readonly path: string, public readonly key: string, public readonly value?: unknown, public readonly guidance?: string, ) { super(message); this.name = 'ConfigValidationError'; } } /** * Get the appropriate schema for a dynamic container's child objects. * Returns the schema to validate values inside profiles., presets., or environments.. */ function getDynamicContainerSchema(path: string): Record | null { if (path === 'profiles') return PROFILE_SCHEMA; if (path === 'presets') return PRESET_SCHEMA; if (path === 'environments') return ENVIRONMENT_POLICY_SCHEMA; return null; } /** * Check if a path is inside a dynamic container (e.g., profiles.foo, presets.bar). */ function isInsideDynamicContainer(path: string): boolean { return path.startsWith('profiles.') || path.startsWith('presets.') || path.startsWith('environments.'); } /** * User-managed extension keys that pass through schema validation. * APOPHIS will never interpret these fields; they are reserved for team metadata. * Keys starting with 'x-' are always allowed at any level. */ function isUserExtensionKey(key: string): boolean { return key.startsWith('x-') } /** * Validate that a value matches the expected type for a schema field. * Throws ConfigValidationError on type mismatch. */ function validateType( fieldValue: unknown, fieldSchema: SchemaField, currentPath: string, key: string, ): void { // Null/undefined is only valid if optional if (fieldValue === null || fieldValue === undefined) { if (!fieldSchema.optional) { throw new ConfigValidationError( `Missing required config key at ${currentPath}`, currentPath, key, fieldValue, `This field is required. Provide a ${fieldSchema.type} value.`, ); } return; } const actualType = Array.isArray(fieldValue) ? 'array' : typeof fieldValue; if (actualType !== fieldSchema.type) { throw new ConfigValidationError( `Invalid type at ${currentPath}: expected ${fieldSchema.type}, got ${actualType}`, currentPath, key, fieldValue, `Expected ${fieldSchema.type}. Received ${actualType === 'object' ? JSON.stringify(fieldValue) : String(fieldValue)}.`, ); } // Validate enum values if (fieldSchema.enumValues && fieldSchema.type === 'string' && typeof fieldValue === 'string') { if (!fieldSchema.enumValues.includes(fieldValue)) { throw new ConfigValidationError( `Invalid value at ${currentPath}: "${fieldValue}" is not a valid ${key}. Allowed: ${fieldSchema.enumValues.join(', ')}`, currentPath, key, fieldValue, `Must be one of: ${fieldSchema.enumValues.join(', ')}.`, ); } } // Validate numeric constraints if (fieldSchema.type === 'number' && typeof fieldValue === 'number') { if (fieldSchema.min !== undefined && fieldValue < fieldSchema.min) { throw new ConfigValidationError( `Invalid value at ${currentPath}: ${fieldValue} is less than minimum ${fieldSchema.min}`, currentPath, key, fieldValue, `Must be >= ${fieldSchema.min}.`, ); } } // Validate array item types if (fieldSchema.type === 'array' && Array.isArray(fieldValue) && fieldSchema.items) { for (let i = 0; i < fieldValue.length; i++) { const item = fieldValue[i]; const itemPath = `${currentPath}[${i}]`; const itemType = Array.isArray(item) ? 'array' : typeof item; if (itemType !== fieldSchema.items.type) { throw new ConfigValidationError( `Invalid type at ${itemPath}: expected ${fieldSchema.items.type}, got ${itemType}`, itemPath, `${key}[${i}]`, item, `Array items must be ${fieldSchema.items.type}. Received ${itemType === 'object' ? JSON.stringify(item) : String(item)}.`, ); } } } } /** * Recursively validate an object against a schema. * Checks: * - Unknown keys (hard failure) * - Type mismatches (hard failure) * - Enum value violations (hard failure) * - Array item type mismatches (hard failure) * - Numeric constraints (hard failure) * * Throws ConfigValidationError on any validation failure. */ export function validateConfigAgainstSchema( value: unknown, schema: Record, path: string = '', ): void { if (value === null || typeof value !== 'object') { return; } const obj = value as Record; for (const key of Object.keys(obj)) { const currentPath = path ? `${path}.${key}` : key; const fieldSchema = schema[key]; // Handle dynamic containers: profiles, presets, environments // The keys are user-defined names; their values have specific schemas const isDynamicContainer = path === 'profiles' || path === 'presets' || path === 'environments'; if (!fieldSchema && isDynamicContainer) { const childSchema = getDynamicContainerSchema(path); const fieldValue = obj[key]; if (childSchema && fieldValue !== null && typeof fieldValue === 'object') { // Validate the dynamic container value against its specific schema validateConfigAgainstSchema(fieldValue, childSchema, currentPath); } else if (childSchema) { // Value is a primitive inside a dynamic container — type check it validateType(fieldValue, { type: 'object', optional: false }, currentPath, key); } continue; } // Handle nested keys inside profile/preset/environment objects if (!fieldSchema && isInsideDynamicContainer(path)) { const parentContainer = path.split('.')[0] || ''; const childSchema = getDynamicContainerSchema(parentContainer); if (childSchema) { const nestedSchema = childSchema[key]; if (nestedSchema) { const fieldValue = obj[key]; validateType(fieldValue, nestedSchema, currentPath, key); // Recurse into nested objects if (nestedSchema.type === 'object' && fieldValue !== null && typeof fieldValue === 'object') { if (nestedSchema.properties) { validateConfigAgainstSchema(fieldValue, nestedSchema.properties, currentPath); } } if (nestedSchema.type === 'array' && Array.isArray(fieldValue) && nestedSchema.items?.properties) { for (let i = 0; i < fieldValue.length; i++) { const item = fieldValue[i]; if (item !== null && typeof item === 'object') { validateConfigAgainstSchema(item, nestedSchema.items.properties, `${currentPath}[${i}]`); } } } } else { if (isUserExtensionKey(key)) continue // Unknown key inside a profile/preset/environment object throw new ConfigValidationError( `Unknown config key at ${currentPath}`, currentPath, key, obj[key], `Valid keys for ${parentContainer} entries: ${Object.keys(childSchema || {}).join(', ')}. Use x- prefixed keys for team metadata.`, ); } } continue; } if (!fieldSchema) { if (isUserExtensionKey(key)) continue throw new ConfigValidationError( `Unknown config key at ${currentPath}`, currentPath, key, obj[key], `Valid top-level keys: ${Object.keys(CONFIG_SCHEMA).join(', ')}. Use the metadata field or x- prefixed keys for team data.`, ); } const fieldValue = obj[key]; // Validate type for known fields validateType(fieldValue, fieldSchema, currentPath, key); // Recurse into objects with known properties if (fieldSchema.type === 'object') { if (fieldValue !== null && typeof fieldValue === 'object') { if (fieldSchema.properties) { validateConfigAgainstSchema(fieldValue, fieldSchema.properties, currentPath); } else { // For objects without explicit properties (like profiles/presets/environments), // we still recurse to validate nested objects, but we pass the same schema // and the skip logic above will handle dynamic container keys validateConfigAgainstSchema(fieldValue, schema, currentPath); } } } // Recurse into array items if they are objects if (fieldSchema.type === 'array' && fieldSchema.items && Array.isArray(fieldValue)) { for (let i = 0; i < fieldValue.length; i++) { const item = fieldValue[i]; if (item !== null && typeof item === 'object' && fieldSchema.items.properties) { validateConfigAgainstSchema(item, fieldSchema.items.properties, `${currentPath}[${i}]`); } } } } } // --------------------------------------------------------------------------- // Monorepo detection // --------------------------------------------------------------------------- /** * Check if cwd is inside a monorepo (has workspaces in root package.json). */ export function detectMonorepo(cwd: string): boolean { let current = cwd; while (current !== dirname(current)) { const pkgPath = resolve(current, 'package.json'); if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); if (pkg.workspaces && Array.isArray(pkg.workspaces)) { return true; } // Also check for pnpm workspaces const pnpmWorkspacePath = resolve(current, 'pnpm-workspace.yaml'); if (existsSync(pnpmWorkspacePath)) { return true; } } catch { // Ignore parse errors } // Stop at first package.json found return false; } current = dirname(current); } return false; } /** * Find all workspace package directories under cwd. * Supports npm workspaces (package.json workspaces field) and pnpm-workspace.yaml. * Returns absolute paths to each package directory. */ export function findWorkspacePackages(cwd: string): string[] { let root = cwd; while (root !== dirname(root)) { const pkgPath = resolve(root, 'package.json'); if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); if (pkg.workspaces && Array.isArray(pkg.workspaces)) { return expandWorkspacePatterns(root, pkg.workspaces); } } catch { // Ignore parse errors } // Check for pnpm-workspace.yaml const pnpmWorkspacePath = resolve(root, 'pnpm-workspace.yaml'); if (existsSync(pnpmWorkspacePath)) { const patterns = parsePnpmWorkspaceYaml(pnpmWorkspacePath); return expandWorkspacePatterns(root, patterns); } // Stop at first package.json found return []; } root = dirname(root); } return []; } function expandWorkspacePatterns(root: string, patterns: string[]): string[] { const packages: string[] = []; for (const pattern of patterns) { if (pattern.endsWith('/*')) { const dir = pattern.slice(0, -2); const dirPath = resolve(root, dir); if (existsSync(dirPath)) { const entries = readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { packages.push(resolve(dirPath, entry.name)); } } } } else { const exactPath = resolve(root, pattern); if (existsSync(exactPath)) { const stat = statSync(exactPath); if (stat.isDirectory()) { packages.push(exactPath); } } } } return packages; } function parsePnpmWorkspaceYaml(yamlPath: string): string[] { try { const content = readFileSync(yamlPath, 'utf-8'); const patterns: string[] = []; for (const line of content.split('\n')) { const trimmed = line.trim(); if (trimmed.startsWith('- ')) { patterns.push(trimmed.slice(2).trim()); } } return patterns; } catch { return []; } } // --------------------------------------------------------------------------- // Semantic validation (cross-references, value constraints) // --------------------------------------------------------------------------- /** * Validate semantic constraints that go beyond schema types: * - Profile references nonexistent preset * - Environment policy references unknown mode * - Timeout is a positive number * - Routes array elements are non-empty strings * - Seed is an integer * * Throws ConfigValidationError on any semantic violation. */ export function validateConfigSemantics(config: Config): void { // Validate profile references if (config.profiles) { for (const [profileName, profile] of Object.entries(config.profiles)) { if (profile.preset) { const availablePresets = config.presets ? Object.keys(config.presets) : []; if (!config.presets || !(profile.preset in config.presets)) { throw new ConfigValidationError( `Profile "${profileName}" references unknown preset "${profile.preset}"`, `profiles.${profileName}.preset`, 'preset', profile.preset, `Available presets: ${availablePresets.join(', ') || 'none'}. Define preset "${profile.preset}" in config.presets.`, ); } } } } // Validate preset values if (config.presets) { for (const [presetName, preset] of Object.entries(config.presets)) { if (preset.timeout !== undefined) { if (typeof preset.timeout !== 'number' || preset.timeout < 0) { throw new ConfigValidationError( `Preset "${presetName}" has invalid timeout: ${preset.timeout}`, `presets.${presetName}.timeout`, 'timeout', preset.timeout, `Timeout must be a non-negative number (milliseconds).`, ); } } } } // Validate environment policy allowedModes if (config.environments) { for (const [envName, envPolicy] of Object.entries(config.environments)) { if (envPolicy.allowedModes) { const validModes = ['verify', 'observe', 'qualify']; for (const mode of envPolicy.allowedModes) { if (!validModes.includes(mode)) { throw new ConfigValidationError( `Environment "${envName}" has invalid allowedMode: "${mode}"`, `environments.${envName}.allowedModes`, 'allowedModes', mode, `Allowed modes must be one of: ${validModes.join(', ')}.`, ); } } } } } // Validate routes are non-empty strings if (config.routes) { for (let i = 0; i < config.routes.length; i++) { const route = config.routes[i]; if (typeof route !== 'string' || route.trim().length === 0) { throw new ConfigValidationError( `Invalid route at routes[${i}]: ${JSON.stringify(route)}`, `routes[${i}]`, 'routes', route, `Routes must be non-empty strings like "GET /users" or "POST /api/items".`, ); } } } // Validate seed is an integer if (config.seed !== undefined) { if (typeof config.seed !== 'number' || !Number.isInteger(config.seed)) { throw new ConfigValidationError( `Invalid seed: ${config.seed}`, 'seed', 'seed', config.seed, `Seed must be an integer number.`, ); } } } // --------------------------------------------------------------------------- // Profile and preset resolution // --------------------------------------------------------------------------- /** * Resolve profile from config.profiles. * Returns merged config: preset defaults + profile overrides. */ export function resolveProfile( config: Config, profileName: string | undefined, ): { config: Config; profileName: string | null; presetName: string | null } { if (!profileName) { return { config, profileName: null, presetName: config.preset ?? null }; } const profiles = config.profiles ?? {}; const profile = profiles[profileName]; if (!profile) { const available = Object.keys(profiles).join(', '); throw new Error( `Unknown profile "${profileName}". Available profiles: ${available || 'none'}.`, ); } // Start with preset if profile references one let merged: Config = { ...config }; let presetName: string | null = null; if (profile.preset && config.presets) { const preset = config.presets[profile.preset]; if (preset) { merged = { ...merged, ...preset }; presetName = profile.preset; } } // Apply profile overrides merged = { ...merged, ...profile, // Don't overwrite the top-level preset with the profile's preset string preset: profile.preset ? undefined : merged.preset, }; // Clean up undefined values if (merged.preset === undefined) { delete merged.preset; } return { config: merged, profileName, presetName }; } // --------------------------------------------------------------------------- // Environment-specific overrides // --------------------------------------------------------------------------- /** * Apply environment-specific policy overrides. */ export function applyEnvironmentOverrides( config: Config, env: string | undefined, ): Config { if (!env || !config.environments) { return config; } const envPolicy = config.environments[env]; if (!envPolicy) { return config; } // Environment policy doesn't override config values directly, // but we merge it for policy engine consumption return { ...config, environments: { ...config.environments, [env]: envPolicy, }, }; } // --------------------------------------------------------------------------- // Main entry point // --------------------------------------------------------------------------- /** * Load and resolve config for the CLI. * * Discovery order: * 1. --config override * 2. apophis.config.js * 3. apophis.config.ts * 4. apophis.config.json * 5. "apophis" field in package.json * * Resolution order: * 1. Load raw config * 2. Validate against schema (unknown keys = hard failure) * 3. Resolve profile (preset defaults + profile overrides) * 4. Apply environment-specific overrides * 5. Detect monorepo */ export async function loadConfig(options: LoadConfigOptions): Promise { const { cwd, configPath: explicitPath, profileName, env } = options; let configPath: string | null = null; let rawConfig: Config; // 1. Explicit --config override if (explicitPath) { const resolvedPath = resolve(cwd, explicitPath); if (!existsSync(resolvedPath)) { throw new Error(`Config file not found: ${resolvedPath}`); } configPath = resolvedPath; rawConfig = await loadConfigFile(resolvedPath); } else { // 2. Discover config file const discoveredPath = discoverConfig(cwd); if (discoveredPath) { configPath = discoveredPath; rawConfig = await loadConfigFile(discoveredPath); } else { // 3. Check package.json "apophis" field const pkgConfig = loadPackageJsonConfig(cwd); if (pkgConfig.config) { configPath = pkgConfig.path; rawConfig = pkgConfig.config; } else { // No config found return { config: {}, configPath: null, isMonorepo: detectMonorepo(cwd), profileName: null, presetName: null, }; } } } // 4. Resolve protocol packs if specified if (rawConfig.packs && Array.isArray(rawConfig.packs) && rawConfig.packs.length > 0) { const packFragment = resolvePacks(rawConfig.packs as string[], { seed: rawConfig.seed, }); rawConfig = { ...packFragment, ...rawConfig, profiles: { ...packFragment.profiles, ...rawConfig.profiles, }, presets: { ...packFragment.presets, ...rawConfig.presets, }, environments: { ...packFragment.environments, ...rawConfig.environments, }, }; } // 5. Validate against schema (unknown keys = hard failure with exact path) validateConfigAgainstSchema(rawConfig, CONFIG_SCHEMA); // 5b. Validate semantic constraints (cross-references, value constraints) validateConfigSemantics(rawConfig); // 5. Resolve profile and preset const { config: profiledConfig, profileName: resolvedProfile, presetName } = resolveProfile( rawConfig, profileName, ); // 6. Apply environment overrides const envConfig = applyEnvironmentOverrides(profiledConfig, env); // 7. Detect monorepo const isMonorepo = detectMonorepo(cwd); return { config: envConfig, configPath, isMonorepo, profileName: resolvedProfile, presetName, }; } // --------------------------------------------------------------------------- // Re-export for convenience // --------------------------------------------------------------------------- export { CONFIG_SCHEMA };