gitcortex 0.7.3

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 serde_json::json;

use crate::cmd::init::helpers::{require_json_object, write_atomic};

pub(crate) const COPILOT_INSTRUCTIONS: &str = r#"<!-- >>> gitcortex copilot integration >>> -->
# GitCortex — Repository Knowledge Graph

This repository is indexed by [GitCortex](https://github.com/bharath03-a/GitCortex).
The knowledge graph is always up to date with the current branch (updated on every git
operation via post-commit/post-merge/post-rewrite/post-checkout hooks).

## MCP actions available through `gcx serve`

The compact server exposes one `gcx` dispatch tool. Use these values as its
`action` when answering structural questions.

| Action | Description |
|------|-------------|
| `lookup_symbol(name)` | Find any function, struct, class, or trait |
| `find_callers(function_name)` | Who calls this? |
| `find_callees(function_name, depth)` | What does this call? |
| `list_definitions(file)` | All symbols in a file |
| `find_implementors(trait_name)` | All implementations |
| `trace_path(from, to)` | Call paths from A to B |
| `list_symbols_in_range(file, start, end)` | Symbols in a line range |
| `find_unused_symbols(branch)` | Dead code candidates |
| `get_subgraph(seed_name, depth, direction)` | Neighbourhood of a symbol |

## Suggested Workflows

- **Understand a module**: `list_definitions` then `get_subgraph` on key types
- **Track a bug**: `lookup_symbol` → `find_callers` walking upstream
- **Pre-refactor impact**: `find_callers` + `get_subgraph(direction: "in")`
- **Clean up**: `find_unused_symbols` filtered by kind

See `.gitcortex/AGENT_GUIDE.md` for the full guide.
<!-- <<< gitcortex copilot integration <<< -->
"#;

pub fn install(repo_root: &Path, _global_editor_config: bool) -> Result<()> {
    write_vscode_mcp(repo_root)?;
    let dir = repo_root.join(".github");
    fs::create_dir_all(&dir)?;
    let path = dir.join("copilot-instructions.md");

    if path.exists() {
        let existing = fs::read_to_string(&path)?;
        if existing.contains("GitCortex") {
            return Ok(());
        }
        write_atomic(&path, &format!("{existing}\n\n{COPILOT_INSTRUCTIONS}"))
            .context("update copilot-instructions.md")?;
    } else {
        fs::write(path, COPILOT_INSTRUCTIONS).context("write copilot-instructions.md")?;
    }
    Ok(())
}

fn write_vscode_mcp(repo_root: &Path) -> Result<()> {
    let path = repo_root.join(".vscode").join("mcp.json");
    let mut root = if path.exists() {
        let text = fs::read_to_string(&path).context("read .vscode/mcp.json")?;
        serde_json::from_str::<serde_json::Value>(&text)
            .context("parse existing .vscode/mcp.json")?
    } else {
        json!({})
    };
    require_json_object(&root, &path)?;
    if root.pointer("/servers/gitcortex").is_none() {
        root["servers"]["gitcortex"] =
            json!({ "type": "stdio", "command": "gcx", "args": ["serve"] });
        write_atomic(&path, &serde_json::to_string_pretty(&root)?)
            .context("write .vscode/mcp.json")?;
    }
    Ok(())
}