mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: safe Codex config sync — merge AGENTS.md + add-only MCP servers (#723)
* fix: replace bash TOML surgery with Node add-only MCP merge The old sync script used awk/sed to remove and re-append MCP server sections in config.toml, causing credential extraction races, duplicate TOML tables, and 3 fragile code paths with 9 remove_section_inplace calls each. Replace with a Node script (scripts/codex/merge-mcp-config.js) that uses @iarna/toml to parse the config, then appends only missing ECC servers — preserving all existing content byte-for-byte. Warns on config drift, supports legacy aliases (context7 → context7-mcp), and adds --update-mcp flag for explicit refresh. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * fix: address PR #723 review findings for Codex MCP merge - Use package-manager abstraction (scripts/lib/package-manager.js) instead of hardcoding pnpm — respects CLAUDE_PACKAGE_MANAGER, lock files, and project config - Add Yarn 1.x fallback to npx (yarn dlx unsupported in classic) - Add missing exa server to match .codex/config.toml baseline - Wire up findSubSections for --update-mcp nested subtable removal (fixes Greptile P1: Object.keys only returned top-level keys) - Fix resolvedLabel to prefer canonical entry over legacy alias when both exist (fixes context7/context7-mcp spurious warning) - Fix removeSectionFromText to handle inline TOML comments - Fix dry-run + --update-mcp to show removals before early return - Update README parity table: 4 → 7 servers, TOML-parser-based - Add non-npm install variants to README Codex quick start - Update package-lock.json for @iarna/toml Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * fix: address PR #723 review comments (preflight, marker validation) - Add Node.js and merge-mcp-config.js to preflight checks so the script fails fast before partial writes (CodeRabbit) - Validate marker counts: require exactly 1 BEGIN + 1 END in correct order for clean replacement (CodeRabbit) - Corrupted markers: strip all marker lines and re-append fresh block, preserving user content outside markers instead of overwriting - Move MCP_MERGE_SCRIPT to preflight section, remove duplicate Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -46,6 +46,17 @@ Available skills:
|
||||
|
||||
Treat the project-local `.codex/config.toml` as the default Codex baseline for ECC. The current ECC baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking; add heavier extras in `~/.codex/config.toml` only when a task actually needs them.
|
||||
|
||||
### Automatic config.toml merging
|
||||
|
||||
The sync script (`scripts/sync-ecc-to-codex.sh`) uses a Node-based TOML parser to safely merge ECC MCP servers into `~/.codex/config.toml`:
|
||||
|
||||
- **Add-only by default** — missing ECC servers are appended; existing servers are never modified or removed.
|
||||
- **7 managed servers** — Supabase, Playwright, Context7, Exa, GitHub, Memory, Sequential Thinking.
|
||||
- **Package-manager aware** — uses the project's configured package manager (npm/pnpm/yarn/bun) instead of hardcoding `pnpm`.
|
||||
- **Drift warnings** — if an existing server's config differs from the ECC recommendation, the script logs a warning.
|
||||
- **`--update-mcp`** — explicitly replaces all ECC-managed servers with the latest recommended config (safely removes subtables like `[mcp_servers.supabase.env]`).
|
||||
- **User config is always preserved** — custom servers, args, env vars, and credentials outside ECC-managed sections are never touched.
|
||||
|
||||
## Multi-Agent Support
|
||||
|
||||
Codex now supports multi-agent workflows behind the experimental `features.multi_agent` flag.
|
||||
|
||||
14
README.md
14
README.md
@@ -986,10 +986,18 @@ ECC provides **first-class Codex support** for both the macOS app and CLI, with
|
||||
# Run Codex CLI in the repo — AGENTS.md and .codex/ are auto-detected
|
||||
codex
|
||||
|
||||
# Optional: copy the global-safe defaults to your home directory
|
||||
# Automatic setup: sync ECC assets (AGENTS.md, skills, MCP servers) into ~/.codex
|
||||
npm install && bash scripts/sync-ecc-to-codex.sh
|
||||
# or: pnpm install && bash scripts/sync-ecc-to-codex.sh
|
||||
# or: yarn install && bash scripts/sync-ecc-to-codex.sh
|
||||
# or: bun install && bash scripts/sync-ecc-to-codex.sh
|
||||
|
||||
# Or manually: copy the reference config to your home directory
|
||||
cp .codex/config.toml ~/.codex/config.toml
|
||||
```
|
||||
|
||||
The sync script safely merges ECC MCP servers into your existing `~/.codex/config.toml` using an **add-only** strategy — it never removes or modifies your existing servers. Run with `--dry-run` to preview changes, or `--update-mcp` to force-refresh ECC servers to the latest recommended config.
|
||||
|
||||
Codex macOS app:
|
||||
- Open this repository as your workspace.
|
||||
- The root `AGENTS.md` is auto-detected.
|
||||
@@ -1004,7 +1012,7 @@ Codex macOS app:
|
||||
| Config | 1 | `.codex/config.toml` — top-level approvals/sandbox/web_search, MCP servers, notifications, profiles |
|
||||
| AGENTS.md | 2 | Root (universal) + `.codex/AGENTS.md` (Codex-specific supplement) |
|
||||
| Skills | 16 | `.agents/skills/` — SKILL.md + agents/openai.yaml per skill |
|
||||
| MCP Servers | 4 | GitHub, Context7, Memory, Sequential Thinking (command-based) |
|
||||
| MCP Servers | 6 | Supabase, Playwright, Context7, GitHub, Memory, Sequential Thinking (auto-merged via add-only sync) |
|
||||
| Profiles | 2 | `strict` (read-only sandbox) and `yolo` (full auto-approve) |
|
||||
| Agent Roles | 3 | `.codex/agents/` — explorer, reviewer, docs-researcher |
|
||||
|
||||
@@ -1189,7 +1197,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|
||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
||||
| **Custom Tools** | Via hooks | Via hooks | N/A | 6 native tools |
|
||||
| **MCP Servers** | 14 | Shared (mcp.json) | 4 (command-based) | Full |
|
||||
| **MCP Servers** | 14 | Shared (mcp.json) | 7 (auto-merged via TOML parser) | Full |
|
||||
| **Config Format** | settings.json | hooks.json + rules/ | config.toml | opencode.json |
|
||||
| **Context File** | CLAUDE.md + AGENTS.md | AGENTS.md | AGENTS.md | AGENTS.md |
|
||||
| **Secret Detection** | Hook-based | beforeSubmitPrompt hook | Sandbox-based | Hook-based |
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -271,6 +272,12 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@iarna/toml": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
|
||||
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"scripts/orchestrate-worktrees.js",
|
||||
"scripts/setup-package-manager.js",
|
||||
"scripts/skill-create-output.js",
|
||||
"scripts/codex/merge-mcp-config.js",
|
||||
"scripts/repair.js",
|
||||
"scripts/harness-audit.js",
|
||||
"scripts/session-inspect.js",
|
||||
@@ -108,6 +109,7 @@
|
||||
"coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
304
scripts/codex/merge-mcp-config.js
Normal file
304
scripts/codex/merge-mcp-config.js
Normal file
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Merge ECC-recommended MCP servers into a Codex config.toml.
|
||||
*
|
||||
* Strategy: ADD-ONLY by default.
|
||||
* - Parse the TOML to detect which mcp_servers.* sections exist.
|
||||
* - Append raw TOML text for any missing servers (preserves existing file byte-for-byte).
|
||||
* - Log warnings when an existing server's config differs from the ECC recommendation.
|
||||
* - With --update-mcp, also replace existing ECC-managed servers.
|
||||
*
|
||||
* Uses the repo's package-manager abstraction (scripts/lib/package-manager.js)
|
||||
* so MCP launcher commands respect the user's configured package manager.
|
||||
*
|
||||
* Usage:
|
||||
* node merge-mcp-config.js <config.toml> [--dry-run] [--update-mcp]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let TOML;
|
||||
try {
|
||||
TOML = require('@iarna/toml');
|
||||
} catch {
|
||||
console.error('[ecc-mcp] Missing dependency: @iarna/toml');
|
||||
console.error('[ecc-mcp] Run: npm install (from the ECC repo root)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Package manager detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pmConfig;
|
||||
try {
|
||||
const { getPackageManager } = require(path.join(__dirname, '..', 'lib', 'package-manager.js'));
|
||||
pmConfig = getPackageManager();
|
||||
} catch {
|
||||
// Fallback: if package-manager.js isn't available, default to npx
|
||||
pmConfig = { name: 'npm', config: { name: 'npm', execCmd: 'npx' } };
|
||||
}
|
||||
|
||||
// Yarn 1.x doesn't support `yarn dlx` — fall back to npx for classic Yarn.
|
||||
let resolvedExecCmd = pmConfig.config.execCmd;
|
||||
if (pmConfig.name === 'yarn' && resolvedExecCmd === 'yarn dlx') {
|
||||
try {
|
||||
const { execFileSync } = require('child_process');
|
||||
const ver = execFileSync('yarn', ['--version'], { encoding: 'utf8', timeout: 5000 }).trim();
|
||||
if (ver.startsWith('1.')) {
|
||||
resolvedExecCmd = 'npx';
|
||||
}
|
||||
} catch {
|
||||
// Can't detect version — keep yarn dlx and let it fail visibly
|
||||
}
|
||||
}
|
||||
|
||||
const PM_NAME = pmConfig.config.name || pmConfig.name;
|
||||
const PM_EXEC = resolvedExecCmd; // e.g. "pnpm dlx", "npx", "bunx", "yarn dlx"
|
||||
const PM_EXEC_PARTS = PM_EXEC.split(/\s+/); // ["pnpm", "dlx"] or ["npx"] or ["bunx"]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ECC-recommended MCP servers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GitHub bootstrap uses bash for token forwarding — this is intentionally
|
||||
// shell-based regardless of package manager, since Codex runs on macOS/Linux.
|
||||
const GH_BOOTSTRAP = `token=$(gh auth token 2>/dev/null || true); if [ -n "$token" ]; then export GITHUB_PERSONAL_ACCESS_TOKEN="$token"; fi; exec ${PM_EXEC} @modelcontextprotocol/server-github`;
|
||||
|
||||
/**
|
||||
* Build a server spec with the detected package manager.
|
||||
* Returns { fields, toml } where fields is for drift detection and
|
||||
* toml is the raw text appended to the file.
|
||||
*/
|
||||
function dlxServer(name, pkg, extraFields, extraToml) {
|
||||
const args = [...PM_EXEC_PARTS.slice(1), pkg];
|
||||
const fields = { command: PM_EXEC_PARTS[0], args, ...extraFields };
|
||||
const argsStr = JSON.stringify(args).replace(/,/g, ', ');
|
||||
let toml = `[mcp_servers.${name}]\ncommand = "${PM_EXEC_PARTS[0]}"\nargs = ${argsStr}`;
|
||||
if (extraToml) toml += '\n' + extraToml;
|
||||
return { fields, toml };
|
||||
}
|
||||
|
||||
/** Each entry: key = section name under mcp_servers, value = { toml, fields } */
|
||||
const ECC_SERVERS = {
|
||||
supabase: dlxServer('supabase', '@supabase/mcp-server-supabase@latest', { startup_timeout_sec: 20.0, tool_timeout_sec: 120.0 }, 'startup_timeout_sec = 20.0\ntool_timeout_sec = 120.0'),
|
||||
playwright: dlxServer('playwright', '@playwright/mcp@latest'),
|
||||
'context7-mcp': dlxServer('context7-mcp', '@upstash/context7-mcp'),
|
||||
exa: {
|
||||
fields: { url: 'https://mcp.exa.ai/mcp' },
|
||||
toml: `[mcp_servers.exa]\nurl = "https://mcp.exa.ai/mcp"`
|
||||
},
|
||||
github: {
|
||||
fields: { command: 'bash', args: ['-lc', GH_BOOTSTRAP] },
|
||||
toml: `[mcp_servers.github]\ncommand = "bash"\nargs = ["-lc", ${JSON.stringify(GH_BOOTSTRAP)}]`
|
||||
},
|
||||
memory: dlxServer('memory', '@modelcontextprotocol/server-memory'),
|
||||
'sequential-thinking': dlxServer('sequential-thinking', '@modelcontextprotocol/server-sequential-thinking')
|
||||
};
|
||||
|
||||
// Append --features arg for supabase after dlxServer builds the base
|
||||
ECC_SERVERS.supabase.fields.args.push('--features=account,docs,database,debugging,development,functions,storage,branching');
|
||||
ECC_SERVERS.supabase.toml = ECC_SERVERS.supabase.toml.replace(/^(args = \[.*)\]$/m, '$1, "--features=account,docs,database,debugging,development,functions,storage,branching"]');
|
||||
|
||||
// Legacy section names that should be treated as an existing ECC server.
|
||||
// e.g. old configs shipped [mcp_servers.context7] instead of [mcp_servers.context7-mcp].
|
||||
const LEGACY_ALIASES = {
|
||||
'context7-mcp': ['context7']
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function log(msg) {
|
||||
console.log(`[ecc-mcp] ${msg}`);
|
||||
}
|
||||
|
||||
function warn(msg) {
|
||||
console.warn(`[ecc-mcp] WARNING: ${msg}`);
|
||||
}
|
||||
|
||||
/** Shallow-compare two objects (one level deep, arrays by JSON). */
|
||||
function configDiffers(existing, recommended) {
|
||||
for (const key of Object.keys(recommended)) {
|
||||
const a = existing[key];
|
||||
const b = recommended[key];
|
||||
if (Array.isArray(b)) {
|
||||
if (JSON.stringify(a) !== JSON.stringify(b)) return true;
|
||||
} else if (a !== b) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a TOML section and its key-value pairs from raw text.
|
||||
* Matches the section header even if followed by inline comments or whitespace
|
||||
* (e.g. `[mcp_servers.github] # comment`).
|
||||
* Returns the text with the section removed.
|
||||
*/
|
||||
function removeSectionFromText(text, sectionHeader) {
|
||||
const escaped = sectionHeader.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const headerPattern = new RegExp(`^${escaped}(\\s*(#.*)?)?$`);
|
||||
const lines = text.split('\n');
|
||||
const result = [];
|
||||
let skipping = false;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.replace(/\r$/, '');
|
||||
if (headerPattern.test(trimmed)) {
|
||||
skipping = true;
|
||||
continue;
|
||||
}
|
||||
if (skipping && /^\[/.test(trimmed)) {
|
||||
skipping = false;
|
||||
}
|
||||
if (!skipping) {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all TOML sub-section headers for a given server name.
|
||||
* @iarna/toml nests subtables, so `[mcp_servers.supabase.env]` appears as
|
||||
* `parsed.mcp_servers.supabase.env` (nested), NOT as a flat dotted key.
|
||||
* Walk the nested object to find sub-objects that represent TOML sub-tables.
|
||||
*/
|
||||
function findSubSections(serverObj, prefix) {
|
||||
const sections = [];
|
||||
if (!serverObj || typeof serverObj !== 'object') return sections;
|
||||
for (const key of Object.keys(serverObj)) {
|
||||
const val = serverObj[key];
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
const subPath = `${prefix}.${key}`;
|
||||
sections.push(subPath);
|
||||
sections.push(...findSubSections(val, subPath));
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a server and all its sub-sections from raw TOML text.
|
||||
* Uses findSubSections to walk the parsed nested object (not flat keys).
|
||||
*/
|
||||
function removeServerFromText(raw, serverName, existing) {
|
||||
let result = removeSectionFromText(raw, `[mcp_servers.${serverName}]`);
|
||||
const serverObj = existing[serverName];
|
||||
if (serverObj) {
|
||||
for (const sub of findSubSections(serverObj, serverName)) {
|
||||
result = removeSectionFromText(result, `[mcp_servers.${sub}]`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const configPath = args.find(a => !a.startsWith('-'));
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const updateMcp = args.includes('--update-mcp');
|
||||
|
||||
if (!configPath) {
|
||||
console.error('Usage: merge-mcp-config.js <config.toml> [--dry-run] [--update-mcp]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.error(`[ecc-mcp] Config file not found: ${configPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log(`Package manager: ${PM_NAME} (exec: ${PM_EXEC})`);
|
||||
|
||||
let raw = fs.readFileSync(configPath, 'utf8');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = TOML.parse(raw);
|
||||
} catch (err) {
|
||||
console.error(`[ecc-mcp] Failed to parse ${configPath}: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const existing = parsed.mcp_servers || {};
|
||||
const toAppend = [];
|
||||
const toRemoveLog = [];
|
||||
|
||||
for (const [name, spec] of Object.entries(ECC_SERVERS)) {
|
||||
const entry = existing[name];
|
||||
const aliases = LEGACY_ALIASES[name] || [];
|
||||
const legacyName = aliases.find(a => existing[a] && typeof existing[a].command === 'string');
|
||||
|
||||
// Prefer canonical entry over legacy alias
|
||||
const hasCanonical = entry && typeof entry.command === 'string';
|
||||
const resolvedEntry = hasCanonical ? entry : legacyName ? existing[legacyName] : null;
|
||||
// For URL-based servers (exa), check for url field instead of command
|
||||
const urlEntry = !resolvedEntry && entry && typeof entry.url === 'string' ? entry : null;
|
||||
const finalEntry = resolvedEntry || urlEntry;
|
||||
const resolvedLabel = hasCanonical ? name : legacyName || name;
|
||||
|
||||
if (finalEntry) {
|
||||
if (updateMcp) {
|
||||
// --update-mcp: remove existing section (and legacy alias), will re-add below
|
||||
toRemoveLog.push(`mcp_servers.${resolvedLabel}`);
|
||||
raw = removeServerFromText(raw, resolvedLabel, existing);
|
||||
if (resolvedLabel !== name) {
|
||||
raw = removeServerFromText(raw, name, existing);
|
||||
}
|
||||
toAppend.push(spec.toml);
|
||||
} else {
|
||||
// Add-only mode: skip, but warn about drift
|
||||
if (legacyName && !hasCanonical) {
|
||||
warn(`mcp_servers.${legacyName} is a legacy name for ${name} (run with --update-mcp to migrate)`);
|
||||
} else if (configDiffers(finalEntry, spec.fields)) {
|
||||
warn(`mcp_servers.${name} differs from ECC recommendation (run with --update-mcp to refresh)`);
|
||||
} else {
|
||||
log(` [ok] mcp_servers.${name}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log(` [add] mcp_servers.${name}`);
|
||||
toAppend.push(spec.toml);
|
||||
}
|
||||
}
|
||||
|
||||
if (toAppend.length === 0) {
|
||||
log('All ECC MCP servers already present. Nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
const appendText = '\n' + toAppend.join('\n\n') + '\n';
|
||||
|
||||
if (dryRun) {
|
||||
if (toRemoveLog.length > 0) {
|
||||
log('Dry run — would remove and re-add:');
|
||||
for (const label of toRemoveLog) log(` [remove] ${label}`);
|
||||
}
|
||||
log('Dry run — would append:');
|
||||
console.log(appendText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write: for add-only, append to preserve existing content byte-for-byte.
|
||||
// For --update-mcp, we modified `raw` above, so write the full file + appended sections.
|
||||
if (updateMcp) {
|
||||
for (const label of toRemoveLog) log(` [update] ${label}`);
|
||||
const cleaned = raw.replace(/\n+$/, '\n');
|
||||
fs.writeFileSync(configPath, cleaned + appendText, 'utf8');
|
||||
} else {
|
||||
fs.appendFileSync(configPath, appendText, 'utf8');
|
||||
}
|
||||
|
||||
log(`Done. ${toAppend.length} server(s) ${updateMcp ? 'updated' : 'added'}.`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -9,12 +9,16 @@ set -euo pipefail
|
||||
# - Generates Codex QA wrappers and optional language rule-pack prompts
|
||||
# - Installs global git safety hooks (pre-commit and pre-push)
|
||||
# - Runs a post-sync global regression sanity check
|
||||
# - Normalizes MCP server entries to pnpm dlx and removes duplicate Context7 block
|
||||
# - Merges ECC MCP servers into config.toml (add-only via Node TOML parser)
|
||||
|
||||
MODE="apply"
|
||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||
MODE="dry-run"
|
||||
fi
|
||||
UPDATE_MCP=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) MODE="dry-run" ;;
|
||||
--update-mcp) UPDATE_MCP="--update-mcp" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
@@ -123,6 +127,8 @@ generate_prompt_file() {
|
||||
} > "$out"
|
||||
}
|
||||
|
||||
MCP_MERGE_SCRIPT="$REPO_ROOT/scripts/codex/merge-mcp-config.js"
|
||||
|
||||
require_path "$REPO_ROOT/AGENTS.md" "ECC AGENTS.md"
|
||||
require_path "$AGENTS_CODEX_SUPP_SRC" "ECC Codex AGENTS supplement"
|
||||
require_path "$SKILLS_SRC" "ECC skills directory"
|
||||
@@ -131,6 +137,12 @@ require_path "$HOOKS_INSTALLER" "ECC global git hooks installer"
|
||||
require_path "$SANITY_CHECKER" "ECC global sanity checker"
|
||||
require_path "$CURSOR_RULES_DIR" "ECC Cursor rules directory"
|
||||
require_path "$CONFIG_FILE" "Codex config.toml"
|
||||
require_path "$MCP_MERGE_SCRIPT" "ECC MCP merge script"
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
log "ERROR: node is required for MCP config merging but was not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Mode: $MODE"
|
||||
log "Repo root: $REPO_ROOT"
|
||||
@@ -183,22 +195,36 @@ else
|
||||
compose_ecc_block > "$AGENTS_FILE"
|
||||
elif awk -v b="$ECC_BEGIN_MARKER" -v e="$ECC_END_MARKER" '
|
||||
{ gsub(/\r$/, "") }
|
||||
$0 == b { found_b = NR } $0 == e { found_e = NR }
|
||||
END { exit !(found_b && found_e && found_b < found_e) }
|
||||
$0 == b { bc++; if (!fb) fb = NR }
|
||||
$0 == e { ec++; if (!fe) fe = NR }
|
||||
END { exit !(bc == 1 && ec == 1 && fb < fe) }
|
||||
' "$AGENTS_FILE"; then
|
||||
# Existing file with matched, correctly ordered ECC markers — replace only the ECC section
|
||||
# Exactly one BEGIN/END pair in correct order — replace only the ECC section
|
||||
replace_ecc_section
|
||||
elif grep -qF "$ECC_BEGIN_MARKER" "$AGENTS_FILE"; then
|
||||
# BEGIN marker exists but END marker is missing (corrupted). Warn and
|
||||
# replace the file entirely to restore a valid state. Backup was saved.
|
||||
log "WARNING: found BEGIN marker but no END marker — replacing file (backup saved)"
|
||||
compose_ecc_block > "$AGENTS_FILE"
|
||||
elif awk -v b="$ECC_BEGIN_MARKER" -v e="$ECC_END_MARKER" '
|
||||
{ gsub(/\r$/, "") }
|
||||
$0 == b { bc++ } $0 == e { ec++ }
|
||||
END { exit !((bc + ec) > 0) }
|
||||
' "$AGENTS_FILE"; then
|
||||
# Markers present but not exactly one valid BEGIN/END pair (missing END,
|
||||
# duplicates, or out-of-order). Strip all marker lines, then append a
|
||||
# fresh marked block. This preserves user content outside markers.
|
||||
log "WARNING: ECC markers found but not a clean pair — stripping markers and re-appending"
|
||||
_fix_tmp="$(mktemp)"
|
||||
awk -v b="$ECC_BEGIN_MARKER" -v e="$ECC_END_MARKER" '
|
||||
{ gsub(/\r$/, "") }
|
||||
$0 == b { skip = 1; next }
|
||||
$0 == e { skip = 0; next }
|
||||
!skip { print }
|
||||
' "$AGENTS_FILE" > "$_fix_tmp"
|
||||
cat "$_fix_tmp" > "$AGENTS_FILE"
|
||||
rm -f "$_fix_tmp"
|
||||
{ printf '\n\n'; compose_ecc_block; } >> "$AGENTS_FILE"
|
||||
else
|
||||
# Existing file without markers — append ECC block, preserve user content.
|
||||
# Note: legacy ECC-only files (from old '>' overwrite) will get a second copy
|
||||
# on this first run. This is intentional — the alternative (heading-match
|
||||
# heuristic) risks false-positive overwrites of user-authored files. The next
|
||||
# run deduplicates via markers, and a timestamped backup was saved above.
|
||||
# Existing file without markers — append ECC block, preserving existing content.
|
||||
# Legacy ECC-only files will have duplicate content after this first run, but
|
||||
# subsequent runs use marker-based replacement so only the marked section updates.
|
||||
# A timestamped backup was already saved above for recovery if needed.
|
||||
log "No ECC markers found — appending managed block (backup saved)"
|
||||
{
|
||||
printf '\n\n'
|
||||
@@ -435,63 +461,11 @@ if [[ "$MODE" == "apply" ]]; then
|
||||
sort -u "$extension_manifest" -o "$extension_manifest"
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "apply" ]]; then
|
||||
log "Normalizing MCP server config to pnpm"
|
||||
|
||||
supabase_token="$(extract_toml_value "$CONFIG_FILE" "mcp_servers.supabase.env" "SUPABASE_ACCESS_TOKEN")"
|
||||
context7_key="$(extract_context7_key "$CONFIG_FILE")"
|
||||
github_bootstrap='token=$(gh auth token 2>/dev/null || true); if [ -n "$token" ]; then export GITHUB_PERSONAL_ACCESS_TOKEN="$token"; fi; exec pnpm dlx @modelcontextprotocol/server-github'
|
||||
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.github.env"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.github"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.memory"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.sequential-thinking"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.context7"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.context7-mcp"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.playwright"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.supabase.env"
|
||||
remove_section_inplace "$CONFIG_FILE" "mcp_servers.supabase"
|
||||
|
||||
{
|
||||
printf '\n[mcp_servers.supabase]\n'
|
||||
printf 'command = "pnpm"\n'
|
||||
printf 'args = ["dlx", "@supabase/mcp-server-supabase@latest", "--features=account,docs,database,debugging,development,functions,storage,branching"]\n'
|
||||
printf 'startup_timeout_sec = 20.0\n'
|
||||
printf 'tool_timeout_sec = 120.0\n'
|
||||
|
||||
if [[ -n "$supabase_token" ]]; then
|
||||
printf '\n[mcp_servers.supabase.env]\n'
|
||||
printf 'SUPABASE_ACCESS_TOKEN = "%s"\n' "$(toml_escape "$supabase_token")"
|
||||
fi
|
||||
|
||||
printf '\n[mcp_servers.playwright]\n'
|
||||
printf 'command = "pnpm"\n'
|
||||
printf 'args = ["dlx", "@playwright/mcp@latest"]\n'
|
||||
|
||||
if [[ -n "$context7_key" ]]; then
|
||||
printf '\n[mcp_servers.context7-mcp]\n'
|
||||
printf 'command = "pnpm"\n'
|
||||
printf 'args = ["dlx", "@smithery/cli@latest", "run", "@upstash/context7-mcp", "--key", "%s"]\n' "$(toml_escape "$context7_key")"
|
||||
else
|
||||
printf '\n[mcp_servers.context7-mcp]\n'
|
||||
printf 'command = "pnpm"\n'
|
||||
printf 'args = ["dlx", "@upstash/context7-mcp"]\n'
|
||||
fi
|
||||
|
||||
printf '\n[mcp_servers.github]\n'
|
||||
printf 'command = "bash"\n'
|
||||
printf 'args = ["-lc", "%s"]\n' "$(toml_escape "$github_bootstrap")"
|
||||
|
||||
printf '\n[mcp_servers.memory]\n'
|
||||
printf 'command = "pnpm"\n'
|
||||
printf 'args = ["dlx", "@modelcontextprotocol/server-memory"]\n'
|
||||
|
||||
printf '\n[mcp_servers.sequential-thinking]\n'
|
||||
printf 'command = "pnpm"\n'
|
||||
printf 'args = ["dlx", "@modelcontextprotocol/server-sequential-thinking"]\n'
|
||||
} >> "$CONFIG_FILE"
|
||||
log "Merging ECC MCP servers into $CONFIG_FILE (add-only, preserving user config)"
|
||||
if [[ "$MODE" == "dry-run" ]]; then
|
||||
node "$MCP_MERGE_SCRIPT" "$CONFIG_FILE" --dry-run $UPDATE_MCP
|
||||
else
|
||||
log "Skipping MCP config normalization in dry-run mode"
|
||||
node "$MCP_MERGE_SCRIPT" "$CONFIG_FILE" $UPDATE_MCP
|
||||
fi
|
||||
|
||||
log "Installing global git safety hooks"
|
||||
|
||||
Reference in New Issue
Block a user