gitcortex 0.7.0

Git-aware code knowledge graph — incremental AST indexing on every commit, MCP server for AI assistants
use std::{fs, path::Path};

use anyhow::{Context, Result};

use crate::cmd::init::helpers::write_atomic;

pub(crate) const AGENTS_SECTION: &str = r#"<!-- >>> gitcortex codex integration >>> -->
## GitCortex knowledge graph

This repository is indexed by GitCortex. For structural questions, prefer the
`gcx` MCP tool before broad file scans:

- `lookup_symbol` locates a definition.
- `find_callers` and `find_callees` trace call relationships.
- `get_subgraph` maps a symbol neighbourhood.
- `start_tour` summarizes unfamiliar areas.
- `detect_changes` and `gcx blast-radius` assess change impact.

Treat graph output as navigation evidence and confirm behavior in source and
tests before editing. If the index is stale, run `gcx hook`.
<!-- <<< gitcortex codex integration <<< -->
"#;

pub(crate) const CODEX_MCP_SECTION: &str = r#"# >>> gitcortex codex MCP >>>
[mcp_servers.gitcortex]
command = "gcx"
args = ["serve"]
startup_timeout_sec = 30
# <<< gitcortex codex MCP <<<
"#;

pub fn install(repo_root: &Path, _global_editor_config: bool) -> Result<()> {
    write_agents_md(repo_root)?;
    write_codex_config(repo_root)?;
    Ok(())
}

fn write_agents_md(repo_root: &Path) -> Result<()> {
    let path = repo_root.join("AGENTS.md");

    if path.exists() {
        let existing = fs::read_to_string(&path).context("read AGENTS.md")?;
        if existing.contains("gitcortex codex integration") {
            return Ok(());
        }
        if let Some(start) = existing.find("# GitCortex - Codex Guide") {
            let prefix = existing[..start].trim_end();
            let migrated = if prefix.is_empty() {
                AGENTS_SECTION.to_owned()
            } else {
                format!("{prefix}\n\n{AGENTS_SECTION}")
            };
            write_atomic(&path, &migrated).context("migrate AGENTS.md")?;
            return Ok(());
        }
        write_atomic(&path, &format!("{existing}\n\n{AGENTS_SECTION}"))
            .context("update AGENTS.md")?;
    } else {
        fs::write(&path, AGENTS_SECTION).context("write AGENTS.md")?;
    }
    Ok(())
}

fn write_codex_config(repo_root: &Path) -> Result<()> {
    let dir = repo_root.join(".codex");
    fs::create_dir_all(&dir)?;
    let path = dir.join("config.toml");
    const CODEX_MCP: &str = CODEX_MCP_SECTION;

    if path.exists() {
        let existing = fs::read_to_string(&path).context("read .codex/config.toml")?;
        if existing.contains("[mcp_servers.gitcortex]") {
            if existing.contains("args = [\"serve\", \"--compact\"]") {
                let migrated =
                    existing.replace("args = [\"serve\", \"--compact\"]", "args = [\"serve\"]");
                write_atomic(&path, &migrated).context("migrate .codex/config.toml")?;
            }
            return Ok(());
        }
        write_atomic(&path, &format!("{}\n\n{CODEX_MCP}", existing.trim_end()))
            .context("update .codex/config.toml")?;
    } else {
        fs::write(&path, CODEX_MCP).context("write .codex/config.toml")?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn migrates_legacy_codex_files() {
        let temp = tempfile::tempdir().expect("tempdir");
        fs::write(
            temp.path().join("AGENTS.md"),
            "# Existing\n\n# GitCortex - Codex Guide\nlegacy generated body\n",
        )
        .expect("write AGENTS");
        fs::create_dir(temp.path().join(".codex")).expect("create codex dir");
        fs::write(
            temp.path().join(".codex/config.toml"),
            "[mcp_servers.gitcortex]\ncommand = \"gcx\"\nargs = [\"serve\", \"--compact\"]\n",
        )
        .expect("write config");

        install(temp.path(), false).expect("install");
        let agents = fs::read_to_string(temp.path().join("AGENTS.md")).expect("read AGENTS");
        assert!(agents.contains("# Existing"));
        assert!(agents.contains("gitcortex codex integration"));
        assert!(!agents.contains("legacy generated body"));
        let config =
            fs::read_to_string(temp.path().join(".codex/config.toml")).expect("read config");
        assert!(config.contains("args = [\"serve\"]"));
        assert!(!config.contains("--compact"));
    }
}