#!/usr/bin/env bash set -euo pipefail # Sync Everything Claude Code (ECC) assets into a local Codex CLI setup. # - Backs up ~/.codex config and AGENTS.md # - Merges ECC AGENTS.md into existing AGENTS.md (marker-based, preserves user content) # - Syncs Codex-ready skills from .agents/skills # - Generates prompt files from commands/*.md # - 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 # - Merges ECC MCP servers into config.toml (add-only via Node TOML parser) MODE="apply" 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)" CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" CONFIG_FILE="$CODEX_HOME/config.toml" AGENTS_FILE="$CODEX_HOME/AGENTS.md" AGENTS_ROOT_SRC="$REPO_ROOT/AGENTS.md" AGENTS_CODEX_SUPP_SRC="$REPO_ROOT/.codex/AGENTS.md" SKILLS_SRC="$REPO_ROOT/.agents/skills" SKILLS_DEST="$CODEX_HOME/skills" PROMPTS_SRC="$REPO_ROOT/commands" PROMPTS_DEST="$CODEX_HOME/prompts" HOOKS_INSTALLER="$REPO_ROOT/scripts/codex/install-global-git-hooks.sh" SANITY_CHECKER="$REPO_ROOT/scripts/codex/check-codex-global-state.sh" CURSOR_RULES_DIR="$REPO_ROOT/.cursor/rules" STAMP="$(date +%Y%m%d-%H%M%S)" BACKUP_DIR="$CODEX_HOME/backups/ecc-$STAMP" log() { printf '[ecc-sync] %s\n' "$*"; } run_or_echo() { if [[ "$MODE" == "dry-run" ]]; then printf '[dry-run] %s\n' "$*" else eval "$@" fi } require_path() { local p="$1" local label="$2" if [[ ! -e "$p" ]]; then log "Missing $label: $p" exit 1 fi } toml_escape() { local v="$1" v="${v//\\/\\\\}" v="${v//\"/\\\"}" printf '%s' "$v" } remove_section_inplace() { local file="$1" local section="$2" local tmp tmp="$(mktemp)" awk -v section="$section" ' BEGIN { skip = 0 } { if ($0 == "[" section "]") { skip = 1 next } if (skip && $0 ~ /^\[/) { skip = 0 } if (!skip) { print } } ' "$file" > "$tmp" mv "$tmp" "$file" } extract_toml_value() { local file="$1" local section="$2" local key="$3" awk -v section="$section" -v key="$key" ' $0 == "[" section "]" { in_section = 1; next } in_section && /^\[/ { in_section = 0 } in_section && $1 == key { line = $0 sub(/^[^=]*=[[:space:]]*"/, "", line) sub(/".*$/, "", line) print line exit } ' "$file" } extract_context7_key() { local file="$1" grep -oP -- '--key",[[:space:]]*"\K[^"]+' "$file" | head -n 1 || true } generate_prompt_file() { local src="$1" local out="$2" local cmd_name="$3" { printf '# ECC Command Prompt: /%s\n\n' "$cmd_name" printf 'Source: %s\n\n' "$src" printf 'Use this prompt to run the ECC `%s` workflow.\n\n' "$cmd_name" awk ' NR == 1 && $0 == "---" { fm = 1; next } fm == 1 && $0 == "---" { fm = 0; next } fm == 1 { next } { print } ' "$src" } > "$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" require_path "$PROMPTS_SRC" "ECC commands directory" 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" log "Codex home: $CODEX_HOME" log "Creating backup folder: $BACKUP_DIR" run_or_echo "mkdir -p \"$BACKUP_DIR\"" run_or_echo "cp \"$CONFIG_FILE\" \"$BACKUP_DIR/config.toml\"" if [[ -f "$AGENTS_FILE" ]]; then run_or_echo "cp \"$AGENTS_FILE\" \"$BACKUP_DIR/AGENTS.md\"" fi ECC_BEGIN_MARKER="" ECC_END_MARKER="" compose_ecc_block() { printf '%s\n' "$ECC_BEGIN_MARKER" cat "$AGENTS_ROOT_SRC" printf '\n\n---\n\n' printf '# Codex Supplement (From ECC .codex/AGENTS.md)\n\n' cat "$AGENTS_CODEX_SUPP_SRC" printf '\n%s\n' "$ECC_END_MARKER" } log "Merging ECC AGENTS into $AGENTS_FILE (preserving user content)" if [[ "$MODE" == "dry-run" ]]; then printf '[dry-run] merge ECC block into %s from %s + %s\n' "$AGENTS_FILE" "$AGENTS_ROOT_SRC" "$AGENTS_CODEX_SUPP_SRC" else replace_ecc_section() { # Replace the ECC block between markers in $AGENTS_FILE with fresh content. # Uses awk to correctly handle all positions including line 1. local tmp tmp="$(mktemp)" local ecc_tmp ecc_tmp="$(mktemp)" compose_ecc_block > "$ecc_tmp" awk -v begin="$ECC_BEGIN_MARKER" -v end="$ECC_END_MARKER" -v ecc="$ecc_tmp" ' { gsub(/\r$/, "") } $0 == begin { skip = 1; while ((getline line < ecc) > 0) print line; close(ecc); next } $0 == end { skip = 0; next } !skip { print } ' "$AGENTS_FILE" > "$tmp" # Write through the path (preserves symlinks) instead of mv cat "$tmp" > "$AGENTS_FILE" rm -f "$tmp" "$ecc_tmp" } if [[ ! -f "$AGENTS_FILE" ]]; then # No existing file — create fresh with markers compose_ecc_block > "$AGENTS_FILE" elif awk -v b="$ECC_BEGIN_MARKER" -v e="$ECC_END_MARKER" ' { gsub(/\r$/, "") } $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 # Exactly one BEGIN/END pair in correct order — replace only the ECC section replace_ecc_section 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, 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' compose_ecc_block } >> "$AGENTS_FILE" fi fi log "Syncing ECC Codex skills" run_or_echo "mkdir -p \"$SKILLS_DEST\"" skills_count=0 for skill_dir in "$SKILLS_SRC"/*; do [[ -d "$skill_dir" ]] || continue skill_name="$(basename "$skill_dir")" dest="$SKILLS_DEST/$skill_name" run_or_echo "rm -rf \"$dest\"" run_or_echo "cp -R \"$skill_dir\" \"$dest\"" skills_count=$((skills_count + 1)) done log "Generating prompt files from ECC commands" run_or_echo "mkdir -p \"$PROMPTS_DEST\"" manifest="$PROMPTS_DEST/ecc-prompts-manifest.txt" if [[ "$MODE" == "dry-run" ]]; then printf '[dry-run] > %s\n' "$manifest" else : > "$manifest" fi prompt_count=0 while IFS= read -r -d '' command_file; do name="$(basename "$command_file" .md)" out="$PROMPTS_DEST/ecc-$name.md" if [[ "$MODE" == "dry-run" ]]; then printf '[dry-run] generate %s from %s\n' "$out" "$command_file" else generate_prompt_file "$command_file" "$out" "$name" printf 'ecc-%s.md\n' "$name" >> "$manifest" fi prompt_count=$((prompt_count + 1)) done < <(find "$PROMPTS_SRC" -maxdepth 1 -type f -name '*.md' -print0 | sort -z) if [[ "$MODE" == "apply" ]]; then sort -u "$manifest" -o "$manifest" fi log "Generating Codex tool prompts + optional rule-pack prompts" extension_manifest="$PROMPTS_DEST/ecc-extension-prompts-manifest.txt" if [[ "$MODE" == "dry-run" ]]; then printf '[dry-run] > %s\n' "$extension_manifest" else : > "$extension_manifest" fi extension_count=0 write_extension_prompt() { local name="$1" local file="$PROMPTS_DEST/$name" if [[ "$MODE" == "dry-run" ]]; then printf '[dry-run] generate %s\n' "$file" else cat > "$file" printf '%s\n' "$name" >> "$extension_manifest" fi extension_count=$((extension_count + 1)) } write_extension_prompt "ecc-tool-run-tests.md" < Summary: Top failures: - ... Suggested next step: - ... \`\`\` EOF write_extension_prompt "ecc-tool-check-coverage.md" <% Total lines: % Total branches: % (if available) Worst files: - path: xx% Recommended focus: - ... \`\`\` EOF write_extension_prompt "ecc-tool-security-audit.md" < Secrets findings: Code risk findings: Critical issues: - ... Remediation plan: 1. ... 2. ... \`\`\` EOF write_extension_prompt "ecc-rules-pack-common.md" <