#!/usr/bin/env node 'use strict'; /** * Merge the non-MCP Codex baseline from `.codex/config.toml` into a target * `config.toml` without overwriting existing user choices. * * Strategy: add-only. * - Missing root keys are inserted before the first TOML table. * - Missing table keys are appended to existing tables. * - Missing tables are appended to the end of the file. */ const fs = require('fs'); const path = require('path'); let TOML; try { TOML = require('@iarna/toml'); } catch { console.error('[ecc-codex] Missing dependency: @iarna/toml'); console.error('[ecc-codex] Run: npm install (from the ECC repo root)'); process.exit(1); } const ROOT_KEYS = ['approval_policy', 'sandbox_mode', 'web_search', 'notify', 'persistent_instructions']; const TABLE_PATHS = [ 'features', 'profiles.strict', 'profiles.yolo', 'agents', 'agents.explorer', 'agents.reviewer', 'agents.docs_researcher', ]; const TOML_HEADER_RE = /^[ \t]*(?:\[[^[\]\n][^\]\n]*\]|\[\[[^[\]\n][^\]\n]*\]\])[ \t]*(?:#.*)?$/m; function log(message) { console.log(`[ecc-codex] ${message}`); } function warn(message) { console.warn(`[ecc-codex] WARNING: ${message}`); } function getNested(obj, pathParts) { let current = obj; for (const part of pathParts) { if (!current || typeof current !== 'object' || !(part in current)) { return undefined; } current = current[part]; } return current; } function setNested(obj, pathParts, value) { let current = obj; for (let i = 0; i < pathParts.length - 1; i += 1) { const part = pathParts[i]; if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) { current[part] = {}; } current = current[part]; } current[pathParts[pathParts.length - 1]] = value; } function findFirstTableIndex(raw) { const match = TOML_HEADER_RE.exec(raw); return match ? match.index : -1; } function findTableRange(raw, tablePath) { const escaped = tablePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const headerPattern = new RegExp(`^[ \\t]*\\[${escaped}\\][ \\t]*(?:#.*)?$`, 'm'); const match = headerPattern.exec(raw); if (!match) { return null; } const headerEnd = raw.indexOf('\n', match.index); const bodyStart = headerEnd === -1 ? raw.length : headerEnd + 1; const nextHeaderRel = raw.slice(bodyStart).search(TOML_HEADER_RE); const bodyEnd = nextHeaderRel === -1 ? raw.length : bodyStart + nextHeaderRel; return { bodyStart, bodyEnd }; } function ensureTrailingNewline(text) { return text.endsWith('\n') ? text : `${text}\n`; } function insertBeforeFirstTable(raw, block) { const normalizedBlock = ensureTrailingNewline(block.trimEnd()); const firstTableIndex = findFirstTableIndex(raw); if (firstTableIndex === -1) { const prefix = raw.trimEnd(); return prefix ? `${prefix}\n${normalizedBlock}` : normalizedBlock; } const before = raw.slice(0, firstTableIndex).trimEnd(); const after = raw.slice(firstTableIndex).replace(/^\n+/, ''); return `${before}\n\n${normalizedBlock}\n${after}`; } function appendBlock(raw, block) { const prefix = raw.trimEnd(); const normalizedBlock = block.trimEnd(); return prefix ? `${prefix}\n\n${normalizedBlock}\n` : `${normalizedBlock}\n`; } function stringifyValue(value) { return TOML.stringify({ value }).trim().replace(/^value = /, ''); } function updateInlineTableKeys(raw, tablePath, missingKeys) { const pathParts = tablePath.split('.'); if (pathParts.length < 2) { return null; } const parentPath = pathParts.slice(0, -1).join('.'); const parentRange = findTableRange(raw, parentPath); if (!parentRange) { return null; } const tableKey = pathParts[pathParts.length - 1]; const escapedKey = tableKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const body = raw.slice(parentRange.bodyStart, parentRange.bodyEnd); const lines = body.split('\n'); for (let index = 0; index < lines.length; index += 1) { const inlinePattern = new RegExp(`^(\\s*${escapedKey}\\s*=\\s*\\{)(.*?)(\\}\\s*(?:#.*)?)$`); const match = inlinePattern.exec(lines[index]); if (!match) { continue; } const additions = Object.entries(missingKeys) .map(([key, value]) => `${key} = ${stringifyValue(value)}`) .join(', '); const existingEntries = match[2].trim(); const nextEntries = existingEntries ? `${existingEntries}, ${additions}` : additions; lines[index] = `${match[1]}${nextEntries}${match[3]}`; return `${raw.slice(0, parentRange.bodyStart)}${lines.join('\n')}${raw.slice(parentRange.bodyEnd)}`; } return null; } function appendImplicitTable(raw, tablePath, missingKeys) { const candidate = appendBlock(raw, stringifyTable(tablePath, missingKeys)); try { TOML.parse(candidate); return candidate; } catch { return null; } } function appendToTable(raw, tablePath, block, missingKeys = null) { const range = findTableRange(raw, tablePath); if (!range) { if (missingKeys) { const inlineUpdated = updateInlineTableKeys(raw, tablePath, missingKeys); if (inlineUpdated) { return inlineUpdated; } const appendedTable = appendImplicitTable(raw, tablePath, missingKeys); if (appendedTable) { return appendedTable; } } warn(`Skipping missing keys for [${tablePath}] because it has no standalone header and could not be safely updated`); return raw; } const before = raw.slice(0, range.bodyEnd).trimEnd(); const after = raw.slice(range.bodyEnd).replace(/^\n*/, '\n'); return `${before}\n${block.trimEnd()}\n${after}`; } function stringifyRootKeys(keys) { return TOML.stringify(keys).trim(); } function stringifyTable(tablePath, value) { const scalarOnly = {}; for (const [key, entryValue] of Object.entries(value)) { if (entryValue && typeof entryValue === 'object' && !Array.isArray(entryValue)) { continue; } scalarOnly[key] = entryValue; } const snippet = {}; setNested(snippet, tablePath.split('.'), scalarOnly); return TOML.stringify(snippet).trim(); } function stringifyTableKeys(tableValue) { const lines = []; for (const [key, value] of Object.entries(tableValue)) { if (value && typeof value === 'object' && !Array.isArray(value)) { continue; } lines.push(TOML.stringify({ [key]: value }).trim()); } return lines.join('\n'); } function main() { const args = process.argv.slice(2); const configPath = args.find(arg => !arg.startsWith('-')); const dryRun = args.includes('--dry-run'); if (!configPath) { console.error('Usage: merge-codex-config.js [--dry-run]'); process.exit(1); } const referencePath = path.join(__dirname, '..', '..', '.codex', 'config.toml'); if (!fs.existsSync(referencePath)) { console.error(`[ecc-codex] Reference config not found: ${referencePath}`); process.exit(1); } if (!fs.existsSync(configPath)) { console.error(`[ecc-codex] Config file not found: ${configPath}`); process.exit(1); } const raw = fs.readFileSync(configPath, 'utf8'); const referenceRaw = fs.readFileSync(referencePath, 'utf8'); let targetConfig; let referenceConfig; try { targetConfig = TOML.parse(raw); referenceConfig = TOML.parse(referenceRaw); } catch (error) { console.error(`[ecc-codex] Failed to parse TOML: ${error.message}`); process.exit(1); } const missingRootKeys = {}; for (const key of ROOT_KEYS) { if (referenceConfig[key] !== undefined && targetConfig[key] === undefined) { missingRootKeys[key] = referenceConfig[key]; } } const missingTables = []; const missingTableKeys = []; for (const tablePath of TABLE_PATHS) { const pathParts = tablePath.split('.'); const referenceValue = getNested(referenceConfig, pathParts); if (referenceValue === undefined) { continue; } const targetValue = getNested(targetConfig, pathParts); if (targetValue === undefined) { missingTables.push(tablePath); continue; } const missingKeys = {}; for (const [key, value] of Object.entries(referenceValue)) { if (value && typeof value === 'object' && !Array.isArray(value)) { continue; } if (targetValue[key] === undefined) { missingKeys[key] = value; } } if (Object.keys(missingKeys).length > 0) { missingTableKeys.push({ tablePath, missingKeys }); } } if ( Object.keys(missingRootKeys).length === 0 && missingTables.length === 0 && missingTableKeys.length === 0 ) { log('All baseline Codex settings already present. Nothing to do.'); return; } let nextRaw = raw; if (Object.keys(missingRootKeys).length > 0) { log(` [add-root] ${Object.keys(missingRootKeys).join(', ')}`); nextRaw = insertBeforeFirstTable(nextRaw, stringifyRootKeys(missingRootKeys)); } for (const { tablePath, missingKeys } of missingTableKeys) { log(` [add-keys] [${tablePath}] -> ${Object.keys(missingKeys).join(', ')}`); nextRaw = appendToTable(nextRaw, tablePath, stringifyTableKeys(missingKeys), missingKeys); } for (const tablePath of missingTables) { log(` [add-table] [${tablePath}]`); nextRaw = appendBlock(nextRaw, stringifyTable(tablePath, getNested(referenceConfig, tablePath.split('.')))); } if (dryRun) { log('Dry run — would write the merged Codex baseline.'); return; } fs.writeFileSync(configPath, nextRaw, 'utf8'); log('Done. Baseline Codex settings merged.'); } main();