#!/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 # - Normalizes MCP server entries to pnpm dlx and removes duplicate Context7 block MODE="apply" if [[ "${1:-}" == "--dry-run" ]]; then MODE="dry-run" fi 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" } 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" 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 { found_b = NR } $0 == e { found_e = NR } END { exit !(found_b && found_e && found_b < found_e) } ' "$AGENTS_FILE"; then # Existing file with matched, correctly ordered ECC markers — 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" 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. 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" <> "$CONFIG_FILE" else log "Skipping MCP config normalization in dry-run mode" fi log "Installing global git safety hooks" if [[ "$MODE" == "dry-run" ]]; then "$HOOKS_INSTALLER" --dry-run else "$HOOKS_INSTALLER" fi log "Running global regression sanity check" if [[ "$MODE" == "dry-run" ]]; then printf '[dry-run] %s\n' "$SANITY_CHECKER" else "$SANITY_CHECKER" fi log "Sync complete" log "Backup saved at: $BACKUP_DIR" log "Skills synced: $skills_count" log "Prompts generated: $((prompt_count + extension_count)) (commands: $prompt_count, extensions: $extension_count)" if [[ "$MODE" == "apply" ]]; then log "Done. Restart Codex CLI to reload AGENTS, prompts, and MCP servers." fi